Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8bbb847a1b | |||
| c87b245cea | |||
| e8b05e93b9 | |||
| b78aaa5cad | |||
| cc75b94c44 | |||
| ce815ed58b | |||
| b6bc29ae19 | |||
| 3db02af593 | |||
| e52cf94336 |
@@ -237,6 +237,72 @@ Frostbite2D Switch smoke test
|
|||||||
Press PLUS to exit.
|
Press PLUS to exit.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 2.4 编译 Switch 游戏框架 NRO
|
||||||
|
|
||||||
|
当前游戏框架已经接入 Switch 构建目标:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Frostbite2DGameSwitch
|
||||||
|
```
|
||||||
|
|
||||||
|
该目标和 Windows 共用:
|
||||||
|
|
||||||
|
```text
|
||||||
|
game\src\main.cpp
|
||||||
|
```
|
||||||
|
|
||||||
|
入口内部通过 `__SWITCH__` 编译分支处理平台差异,运行同一套
|
||||||
|
`WhiteboxScene`、`StageRenderer`、`DebugOverlay`、`PlayerActor` 和
|
||||||
|
`LevelDefinition` 游戏框架。
|
||||||
|
|
||||||
|
构建命令:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$env:PATH='C:\devkitPro\devkitA64\bin;C:\devkitPro\tools\bin;C:\devkitPro\msys2\usr\bin;C:\Program Files\CMake\bin;C:\Program Files\xmake;' + $env:PATH
|
||||||
|
$env:DEVKITPRO='C:\devkitPro'
|
||||||
|
$env:DEVKITA64='C:\devkitPro\devkitA64'
|
||||||
|
|
||||||
|
& 'C:\Program Files\xmake\xmake.exe' f -c -p switch -a aarch64 -m release --toolchain=cross --sdk='C:\devkitPro\devkitA64' --bin='C:\devkitPro\devkitA64\bin' --cross='aarch64-none-elf-' -y
|
||||||
|
& 'C:\Program Files\xmake\xmake.exe' -b Frostbite2DGameSwitch
|
||||||
|
```
|
||||||
|
|
||||||
|
产物:
|
||||||
|
|
||||||
|
```text
|
||||||
|
build\switch\aarch64\release\Frostbite2DGame.elf
|
||||||
|
build\switch\aarch64\release\switch_game\NSUnknownGame.nro
|
||||||
|
build\switch\aarch64\release\switch_game\control.nacp
|
||||||
|
build\switch\aarch64\release\switch_game\assets\
|
||||||
|
```
|
||||||
|
|
||||||
|
`xmake.lua` 会在构建后自动:
|
||||||
|
|
||||||
|
- 复制 `game\assets` 到 `build\switch\aarch64\release\switch_game\assets`
|
||||||
|
- 生成 `control.nacp`
|
||||||
|
- 调用 `elf2nro` 生成带元数据的 `NSUnknownGame.nro`
|
||||||
|
- 将 `game\assets` 复制到临时 `romfs\assets` 后内嵌到 NRO 中,因此运行时可以直接读取
|
||||||
|
`romfs:/assets/...`
|
||||||
|
|
||||||
|
Switch 运行时资源目录当前优先由引擎解析为:
|
||||||
|
|
||||||
|
```text
|
||||||
|
romfs:/assets
|
||||||
|
```
|
||||||
|
|
||||||
|
如果 RomFS 挂载失败,才会回退到:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/switch/Frostbite2D/NS Unknown Game/assets
|
||||||
|
```
|
||||||
|
|
||||||
|
构建输出目录中的 `switch_game\assets` 是调试和外部部署用的资源副本。
|
||||||
|
|
||||||
|
当前已验证:
|
||||||
|
|
||||||
|
- `Frostbite2DGameSwitch` 可成功编译为 ELF/NRO
|
||||||
|
- PC 和 Switch 共用同一套 GLES 300 es shader
|
||||||
|
- Windows 游戏目标仍可正常编译和启动
|
||||||
|
|
||||||
## 3. 常见问题
|
## 3. 常见问题
|
||||||
|
|
||||||
### 3.1 切换平台后编译异常
|
### 3.1 切换平台后编译异常
|
||||||
@@ -267,3 +333,25 @@ Frostbite2D_smoke_meta.nro
|
|||||||
|
|
||||||
不要优先使用裸的 `Frostbite2D_smoke.nro`。
|
不要优先使用裸的 `Frostbite2D_smoke.nro`。
|
||||||
|
|
||||||
|
### 3.4 Switch 游戏 NRO 启动后资源加载失败或黑屏
|
||||||
|
|
||||||
|
优先确认 `NSUnknownGame.nro` 是新构建的 RomFS 版本。构建时 `elf2nro`
|
||||||
|
命令应包含:
|
||||||
|
|
||||||
|
```text
|
||||||
|
--romfsdir=<build>\switch_game\romfs
|
||||||
|
```
|
||||||
|
|
||||||
|
NRO 内部应能访问:
|
||||||
|
|
||||||
|
```text
|
||||||
|
romfs:/assets/shaders/shaders.json
|
||||||
|
romfs:/assets/shaders/sprite.vert
|
||||||
|
romfs:/assets/shaders/sprite.frag
|
||||||
|
romfs:/assets/shaders/colored_quad.vert
|
||||||
|
romfs:/assets/shaders/colored_quad.frag
|
||||||
|
romfs:/assets/character/male/*.png
|
||||||
|
```
|
||||||
|
|
||||||
|
旧版本曾只把资源复制到 NRO 旁边,运行时引擎找不到 shader 和纹理,
|
||||||
|
会表现为黑屏或没有场景内容。
|
||||||
|
|||||||
@@ -253,6 +253,11 @@ private:
|
|||||||
bool running_ = false;
|
bool running_ = false;
|
||||||
bool paused_ = false;
|
bool paused_ = false;
|
||||||
bool shouldQuit_ = false;
|
bool shouldQuit_ = false;
|
||||||
|
bool sdlInitialized_ = false;
|
||||||
|
#ifdef __SWITCH__
|
||||||
|
bool switchPlatformInitialized_ = false;
|
||||||
|
bool switchRomfsMounted_ = false;
|
||||||
|
#endif
|
||||||
bool textInputEnabled_ = false;
|
bool textInputEnabled_ = false;
|
||||||
bool immEnabled_ = false;
|
bool immEnabled_ = false;
|
||||||
int textInputRequestCount_ = 0;
|
int textInputRequestCount_ = 0;
|
||||||
|
|||||||
@@ -389,6 +389,7 @@ private:
|
|||||||
fs::path toPath(const std::string &path) const;
|
fs::path toPath(const std::string &path) const;
|
||||||
std::string fromPath(const fs::path &path) const;
|
std::string fromPath(const fs::path &path) const;
|
||||||
std::string resolveFullPath(const std::string &path) const;
|
std::string resolveFullPath(const std::string &path) const;
|
||||||
|
bool isDevicePath(const std::string &path) const;
|
||||||
|
|
||||||
std::string workingDirectory_;
|
std::string workingDirectory_;
|
||||||
std::string assetRoot_;
|
std::string assetRoot_;
|
||||||
|
|||||||
@@ -27,6 +27,9 @@
|
|||||||
#include <frostbite2D/scene/scene_manager.h>
|
#include <frostbite2D/scene/scene_manager.h>
|
||||||
#include <frostbite2D/types/type_math.h>
|
#include <frostbite2D/types/type_math.h>
|
||||||
#include <frostbite2D/utils/startup_trace.h>
|
#include <frostbite2D/utils/startup_trace.h>
|
||||||
|
#ifdef __SWITCH__
|
||||||
|
#include <switch.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace frostbite2D {
|
namespace frostbite2D {
|
||||||
|
|
||||||
@@ -55,6 +58,7 @@ bool Application::init(const AppConfig& config) {
|
|||||||
{
|
{
|
||||||
ScopedStartupTrace stageTrace("switchInit");
|
ScopedStartupTrace stageTrace("switchInit");
|
||||||
switchInit();
|
switchInit();
|
||||||
|
switchPlatformInitialized_ = true;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -77,7 +81,11 @@ bool Application::init(const AppConfig& config) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Application::shutdown() {
|
void Application::shutdown() {
|
||||||
if (!initialized_)
|
if (!initialized_ && !sdlInitialized_
|
||||||
|
#ifdef __SWITCH__
|
||||||
|
&& !switchPlatformInitialized_ && !switchRomfsMounted_
|
||||||
|
#endif
|
||||||
|
)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
running_ = false;
|
running_ = false;
|
||||||
@@ -112,17 +120,25 @@ void Application::shutdown() {
|
|||||||
window_ = nullptr;
|
window_ = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (sdlInitialized_) {
|
||||||
|
SDL_Quit();
|
||||||
|
sdlInitialized_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef __SWITCH__
|
||||||
|
if (switchRomfsMounted_) {
|
||||||
|
romfsExit();
|
||||||
|
switchRomfsMounted_ = false;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
// 平台相关清理
|
// 平台相关清理
|
||||||
#ifdef __SWITCH__
|
#ifdef __SWITCH__
|
||||||
switchShutdown();
|
if (switchPlatformInitialized_) {
|
||||||
#endif
|
switchShutdown();
|
||||||
|
switchPlatformInitialized_ = false;
|
||||||
// 退出 SDL(添加重复调用保护)
|
|
||||||
static bool sdlQuitCalled = false;
|
|
||||||
if (!sdlQuitCalled) {
|
|
||||||
SDL_Quit();
|
|
||||||
sdlQuitCalled = true;
|
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
ObjectRegistry::get().clear();
|
ObjectRegistry::get().clear();
|
||||||
|
|
||||||
@@ -150,13 +166,22 @@ bool Application::initCoreModules() {
|
|||||||
SDL_Log("Asset working directory: %s", workingDir.c_str());
|
SDL_Log("Asset working directory: %s", workingDir.c_str());
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
asset.setWorkingDirectory("/switch/Frostbite2D/" + config_.appName);
|
if (!switchRomfsMounted_ && R_SUCCEEDED(romfsInit())) {
|
||||||
|
switchRomfsMounted_ = true;
|
||||||
|
asset.setWorkingDirectory("romfs:/");
|
||||||
|
} else {
|
||||||
|
asset.setWorkingDirectory("/switch/Frostbite2D/" + config_.appName);
|
||||||
|
}
|
||||||
SDL_Log("Asset working directory: %s", asset.getWorkingDirectory().c_str());
|
SDL_Log("Asset working directory: %s", asset.getWorkingDirectory().c_str());
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
{
|
{
|
||||||
ScopedStartupTrace stageTrace("SaveSystem::init");
|
ScopedStartupTrace stageTrace("SaveSystem::init");
|
||||||
if (!SaveSystem::get().init()) {
|
SaveSystemConfig saveConfig;
|
||||||
|
#ifdef __SWITCH__
|
||||||
|
saveConfig.saveRoot = "sdmc:/switch/NSUnknownGame/save";
|
||||||
|
#endif
|
||||||
|
if (!SaveSystem::get().init(saveConfig)) {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to initialize save system");
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to initialize save system");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -168,6 +193,7 @@ bool Application::initCoreModules() {
|
|||||||
SDL_Log("Failed to initialize SDL: %s", SDL_GetError());
|
SDL_Log("Failed to initialize SDL: %s", SDL_GetError());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
sdlInitialized_ = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disable text input by default. UI widgets should enable it explicitly.
|
// Disable text input by default. UI widgets should enable it explicitly.
|
||||||
|
|||||||
@@ -12,6 +12,23 @@ Asset &Asset::get() {
|
|||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool Asset::isDevicePath(const std::string &path) const {
|
||||||
|
auto colon = path.find(':');
|
||||||
|
if (colon == std::string::npos || colon == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (size_t i = 0; i < colon; ++i) {
|
||||||
|
unsigned char ch = static_cast<unsigned char>(path[i]);
|
||||||
|
if (!std::isalnum(ch) && ch != '_' && ch != '-') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return colon + 1 < path.size() &&
|
||||||
|
(path[colon + 1] == '/' || path[colon + 1] == '\\');
|
||||||
|
}
|
||||||
|
|
||||||
fs::path Asset::toPath(const std::string &path) const {
|
fs::path Asset::toPath(const std::string &path) const {
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
return fs::u8path(path);
|
return fs::u8path(path);
|
||||||
@@ -34,7 +51,7 @@ std::string Asset::resolveFullPath(const std::string &path) const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fs::path p = toPath(path);
|
fs::path p = toPath(path);
|
||||||
if (p.is_absolute()) {
|
if (p.is_absolute() || isDevicePath(path)) {
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -37,7 +37,7 @@ function add_frostbite2d_target()
|
|||||||
local libnx = path.join(devkitpro, "libnx")
|
local libnx = path.join(devkitpro, "libnx")
|
||||||
|
|
||||||
add_cxflags("-D__SWITCH__", "-march=armv8-a+crc+crypto", "-mtune=cortex-a57", "-mtp=soft", {public = true})
|
add_cxflags("-D__SWITCH__", "-march=armv8-a+crc+crypto", "-mtune=cortex-a57", "-mtp=soft", {public = true})
|
||||||
add_cxflags("-ffunction-sections", "-fdata-sections", {public = true})
|
add_cxflags("-ffunction-sections", "-fdata-sections", "-fPIE", {public = true})
|
||||||
add_cxflags("-specs=" .. path.join(libnx, "switch.specs"), {public = true, force = true})
|
add_cxflags("-specs=" .. path.join(libnx, "switch.specs"), {public = true, force = true})
|
||||||
add_ldflags("-specs=" .. path.join(libnx, "switch.specs"), {public = true, force = true})
|
add_ldflags("-specs=" .. path.join(libnx, "switch.specs"), {public = true, force = true})
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ function add_frostbite2d_target()
|
|||||||
add_includedirs(path.join(libnx, "include"), {public = true})
|
add_includedirs(path.join(libnx, "include"), {public = true})
|
||||||
add_linkdirs(path.join(portlibs, "lib"), path.join(libnx, "lib"), {public = true})
|
add_linkdirs(path.join(portlibs, "lib"), path.join(libnx, "lib"), {public = true})
|
||||||
add_links("SDL2_ttf", "freetype", "SDL2_mixer", "vorbisidec", "opusfile", "opus", "modplug", "mpg123", "mikmod", "FLAC", "ogg", {public = true})
|
add_links("SDL2_ttf", "freetype", "SDL2_mixer", "vorbisidec", "opusfile", "opus", "modplug", "mpg123", "mikmod", "FLAC", "ogg", {public = true})
|
||||||
add_links("SDL2_image", "png", "jpeg", "webp", "z", "SDL2", "EGL", "GLESv2", "glapi", "drm_nouveau", "nx", "m", "pthread", {public = true})
|
add_links("SDL2_image", "png", "jpeg", "webp", "z", "SDL2", "EGL", "GLESv2", "glapi", "drm_nouveau", "nx", "m", "pthread", "stdc++", {public = true})
|
||||||
else
|
else
|
||||||
add_packages("libsdl2", {public = true})
|
add_packages("libsdl2", {public = true})
|
||||||
add_packages("libsdl2_image", {public = true})
|
add_packages("libsdl2_image", {public = true})
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 176 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 114 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 79 KiB |
@@ -0,0 +1,10 @@
|
|||||||
|
#version 300 es
|
||||||
|
precision mediump float;
|
||||||
|
|
||||||
|
in vec4 v_color;
|
||||||
|
|
||||||
|
out vec4 FragColor;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
FragColor = v_color;
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
#version 300 es
|
||||||
|
precision mediump float;
|
||||||
|
|
||||||
|
layout (location = 0) in vec2 a_position;
|
||||||
|
layout (location = 1) in vec2 a_texCoord;
|
||||||
|
layout (location = 2) in vec4 a_color;
|
||||||
|
|
||||||
|
uniform mat4 u_view;
|
||||||
|
uniform mat4 u_projection;
|
||||||
|
|
||||||
|
out vec4 v_color;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
v_color = a_color;
|
||||||
|
gl_Position = u_projection * u_view * vec4(a_position, 0.0, 1.0);
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"shaders": {
|
||||||
|
"sprite": {
|
||||||
|
"vertex": "sprite.vert",
|
||||||
|
"fragment": "sprite.frag"
|
||||||
|
},
|
||||||
|
"colored_quad": {
|
||||||
|
"vertex": "colored_quad.vert",
|
||||||
|
"fragment": "colored_quad.frag"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
#version 300 es
|
||||||
|
precision mediump float;
|
||||||
|
|
||||||
|
in vec2 v_texCoord;
|
||||||
|
in vec4 v_color;
|
||||||
|
|
||||||
|
uniform sampler2D u_texture;
|
||||||
|
|
||||||
|
out vec4 FragColor;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
FragColor = texture(u_texture, v_texCoord) * v_color;
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
#version 300 es
|
||||||
|
precision mediump float;
|
||||||
|
|
||||||
|
layout (location = 0) in vec2 a_position;
|
||||||
|
layout (location = 1) in vec2 a_texCoord;
|
||||||
|
layout (location = 2) in vec4 a_color;
|
||||||
|
|
||||||
|
uniform mat4 u_view;
|
||||||
|
uniform mat4 u_projection;
|
||||||
|
|
||||||
|
out vec2 v_texCoord;
|
||||||
|
out vec4 v_color;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
v_texCoord = a_texCoord;
|
||||||
|
v_color = a_color;
|
||||||
|
gl_Position = u_projection * u_view * vec4(a_position, 0.0, 1.0);
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# Game Docs
|
||||||
|
|
||||||
|
本文档目录只放游戏层相关文档;引擎、构建和仓库级说明仍保留在项目根目录。
|
||||||
|
|
||||||
|
## Planning
|
||||||
|
|
||||||
|
- `planning/game_architecture_board.md`:游戏整体架构白板,包含思维导图、模块划分、数据流和阶段路线。
|
||||||
|
- `planning/scene_build_plan.md`:第一关场景搭建计划,包含素材清单、场景分层和验收标准。
|
||||||
|
|
||||||
|
## Naming
|
||||||
|
|
||||||
|
- 文件名使用小写 `snake_case`。
|
||||||
|
- 长期有效的计划放入 `planning/`。
|
||||||
|
- 临时调研、实验记录后续单独放入 `research/` 或 `notes/`,避免混在根目录。
|
||||||
@@ -0,0 +1,420 @@
|
|||||||
|
# 游戏整体架构计划:思维导图与白板
|
||||||
|
|
||||||
|
目标:把项目从“能跑的白盒 Demo”推进为可持续扩展的横板 2D ACT 清版游戏工程。本文先作为整体架构白板,后续每个模块再拆成独立任务文档。
|
||||||
|
|
||||||
|
## 思维导图
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
mindmap
|
||||||
|
root((横板 2D ACT 游戏))
|
||||||
|
核心体验
|
||||||
|
横向推进
|
||||||
|
清版战斗
|
||||||
|
平台移动
|
||||||
|
近战连段
|
||||||
|
关卡节奏
|
||||||
|
游戏循环
|
||||||
|
进入关卡
|
||||||
|
探索与移动
|
||||||
|
触发战斗区域
|
||||||
|
击败敌人
|
||||||
|
解锁推进
|
||||||
|
结算与升级
|
||||||
|
角色系统
|
||||||
|
玩家
|
||||||
|
输入
|
||||||
|
移动
|
||||||
|
跳跃
|
||||||
|
攻击
|
||||||
|
受击
|
||||||
|
动画状态机
|
||||||
|
敌人
|
||||||
|
巡逻
|
||||||
|
追击
|
||||||
|
攻击
|
||||||
|
受击硬直
|
||||||
|
死亡
|
||||||
|
NPC与可交互物
|
||||||
|
对话
|
||||||
|
机关
|
||||||
|
可破坏物
|
||||||
|
战斗系统
|
||||||
|
Hitbox
|
||||||
|
Hurtbox
|
||||||
|
伤害结算
|
||||||
|
击退
|
||||||
|
霸体
|
||||||
|
连击窗口
|
||||||
|
命中特效
|
||||||
|
调试显示
|
||||||
|
关卡系统
|
||||||
|
场景数据
|
||||||
|
碰撞数据
|
||||||
|
视觉层
|
||||||
|
战斗区域
|
||||||
|
敌人出生点
|
||||||
|
镜头限制
|
||||||
|
出入口
|
||||||
|
渲染与资源
|
||||||
|
Sprite
|
||||||
|
Tileset
|
||||||
|
背景视差
|
||||||
|
前景遮挡
|
||||||
|
特效
|
||||||
|
音频
|
||||||
|
资源清单
|
||||||
|
数据驱动
|
||||||
|
动画表
|
||||||
|
角色参数
|
||||||
|
敌人配置
|
||||||
|
关卡配置
|
||||||
|
攻击配置
|
||||||
|
掉落配置
|
||||||
|
工具与调试
|
||||||
|
白盒编辑
|
||||||
|
碰撞可视化
|
||||||
|
战斗区域可视化
|
||||||
|
Hitbox可视化
|
||||||
|
帧率与日志
|
||||||
|
构建文档
|
||||||
|
NS平台构建
|
||||||
|
```
|
||||||
|
|
||||||
|
## 白板总览
|
||||||
|
|
||||||
|
```text
|
||||||
|
+----------------------+ +----------------------+ +----------------------+
|
||||||
|
| Game Loop | ---> | Scene | ---> | Stage |
|
||||||
|
| 关卡进入/推进/结算 | | 场景编排与生命周期 | | 关卡数据与视觉层 |
|
||||||
|
+----------------------+ +----------------------+ +----------------------+
|
||||||
|
| | |
|
||||||
|
v v v
|
||||||
|
+----------------------+ +----------------------+ +----------------------+
|
||||||
|
| Actor | ---> | Movement | ---> | Collision |
|
||||||
|
| 玩家/敌人/道具/物体 | | 重力/速度/平台移动 | | 地面/平台/边界 |
|
||||||
|
+----------------------+ +----------------------+ +----------------------+
|
||||||
|
| |
|
||||||
|
v v
|
||||||
|
+----------------------+ +----------------------+ +----------------------+
|
||||||
|
| Combat | ---> | Animation | ---> | Render |
|
||||||
|
| Hitbox/伤害/击退 | | 状态机/帧表/过渡 | | Sprite/Tile/特效 |
|
||||||
|
+----------------------+ +----------------------+ +----------------------+
|
||||||
|
|
|
||||||
|
v
|
||||||
|
+----------------------+ +----------------------+ +----------------------+
|
||||||
|
| Data | ---> | Debug | ---> | Build |
|
||||||
|
| 配置表/关卡/参数 | | 可视化/日志/测试 | | PC/Switch/资源复制 |
|
||||||
|
+----------------------+ +----------------------+ +----------------------+
|
||||||
|
```
|
||||||
|
|
||||||
|
## 模块白板
|
||||||
|
|
||||||
|
### 1. Core
|
||||||
|
|
||||||
|
职责:
|
||||||
|
|
||||||
|
- 管理游戏级常量、资源路径、输入快照、运行配置。
|
||||||
|
- 隔离 SDL 等底层输入细节,让角色逻辑只消费 `InputState`。
|
||||||
|
- 为后续手柄、Switch Joy-Con、键位配置留接口。
|
||||||
|
|
||||||
|
已有:
|
||||||
|
|
||||||
|
- `core/game_config.h`
|
||||||
|
- `core/asset_paths.h`
|
||||||
|
- `core/input_state.*`
|
||||||
|
- `core/debug_flags.h`
|
||||||
|
|
||||||
|
下一步:
|
||||||
|
|
||||||
|
- 增加 `GameContext`,统一传递全局服务。
|
||||||
|
- 输入层支持键盘和手柄统一映射。
|
||||||
|
|
||||||
|
### 2. Scene
|
||||||
|
|
||||||
|
职责:
|
||||||
|
|
||||||
|
- 管理场景生命周期。
|
||||||
|
- 装载关卡数据。
|
||||||
|
- 创建玩家、敌人、道具和场景对象。
|
||||||
|
- 控制镜头、战斗区域、关卡推进。
|
||||||
|
|
||||||
|
已有:
|
||||||
|
|
||||||
|
- `scene/whitebox_scene.*`
|
||||||
|
|
||||||
|
下一步:
|
||||||
|
|
||||||
|
- 抽象 `GameplayScene` 作为通用玩法场景基类。
|
||||||
|
- `WhiteboxScene` 只保留第一关测试内容。
|
||||||
|
- 引入 battle zone 触发逻辑。
|
||||||
|
|
||||||
|
### 3. Stage
|
||||||
|
|
||||||
|
职责:
|
||||||
|
|
||||||
|
- 表达关卡世界:碰撞、视觉层、背景、道具、出生点、镜头范围。
|
||||||
|
- 将白盒数据逐步替换为 tileset 和 props。
|
||||||
|
|
||||||
|
已有:
|
||||||
|
|
||||||
|
- `data/level_definition.h`
|
||||||
|
- `data/whitebox_level.cpp`
|
||||||
|
- `movement/platform_world.*`
|
||||||
|
- `stage/stage_renderer.*`
|
||||||
|
|
||||||
|
下一步:
|
||||||
|
|
||||||
|
- 增加 `StageLayer` 数据结构。
|
||||||
|
- 关卡宽度扩展到 2400 到 3200 像素。
|
||||||
|
- 引入远景/中景/地面三层绘制。
|
||||||
|
|
||||||
|
### 4. Actor
|
||||||
|
|
||||||
|
职责:
|
||||||
|
|
||||||
|
- 表达游戏对象:玩家、敌人、道具、投射物、可破坏物。
|
||||||
|
- 每个 Actor 只处理自身状态,不直接管理全局流程。
|
||||||
|
|
||||||
|
已有:
|
||||||
|
|
||||||
|
- `actor/player_actor.*`
|
||||||
|
|
||||||
|
下一步:
|
||||||
|
|
||||||
|
- 增加 `EnemyActor`。
|
||||||
|
- 增加 `BreakablePropActor`。
|
||||||
|
- 抽出 `CharacterActor` 作为玩家和敌人的共同基类。
|
||||||
|
- 将玩家动画切换改为数据驱动。
|
||||||
|
|
||||||
|
### 5. Movement
|
||||||
|
|
||||||
|
职责:
|
||||||
|
|
||||||
|
- 处理速度、重力、平台落地、边界限制。
|
||||||
|
- 后续支持斜坡、墙体、击退、空中状态。
|
||||||
|
|
||||||
|
已有:
|
||||||
|
|
||||||
|
- `MovementBody`
|
||||||
|
- `PlatformWorld`
|
||||||
|
- `PlatformMover`
|
||||||
|
|
||||||
|
下一步:
|
||||||
|
|
||||||
|
- 支持水平墙体阻挡。
|
||||||
|
- 支持击退速度覆盖。
|
||||||
|
- 支持落地事件。
|
||||||
|
- 支持移动调试可视化。
|
||||||
|
|
||||||
|
### 6. Combat
|
||||||
|
|
||||||
|
职责:
|
||||||
|
|
||||||
|
- 管理攻击盒、受击盒、伤害结算、击退、硬直、命中特效。
|
||||||
|
- 保证战斗逻辑不散落在 Actor 内部。
|
||||||
|
|
||||||
|
已有:
|
||||||
|
|
||||||
|
- `combat/hitbox.h`
|
||||||
|
|
||||||
|
下一步:
|
||||||
|
|
||||||
|
- 增加 `CombatSystem`。
|
||||||
|
- 每帧收集 active hitbox / hurtbox。
|
||||||
|
- 支持 team 过滤、重复命中过滤、伤害事件。
|
||||||
|
- 增加 debug hitbox 绘制。
|
||||||
|
|
||||||
|
### 7. Animation
|
||||||
|
|
||||||
|
职责:
|
||||||
|
|
||||||
|
- 管理 spritesheet 帧、动画状态、播放速度、循环和过渡。
|
||||||
|
- 避免每个 Actor 手写帧切换。
|
||||||
|
|
||||||
|
当前状态:
|
||||||
|
|
||||||
|
- `PlayerActor` 内部硬编码 idle/run/punch 动画。
|
||||||
|
|
||||||
|
下一步:
|
||||||
|
|
||||||
|
- 增加 `AnimationClip`。
|
||||||
|
- 增加 `SpriteAnimator`。
|
||||||
|
- 增加动画配置表。
|
||||||
|
- 攻击动画和 hitbox active frame 绑定。
|
||||||
|
|
||||||
|
### 8. Data
|
||||||
|
|
||||||
|
职责:
|
||||||
|
|
||||||
|
- 将玩法参数从 C++ 硬编码逐步迁移到数据。
|
||||||
|
- 支持关卡、敌人、动画、攻击、掉落配置。
|
||||||
|
|
||||||
|
当前状态:
|
||||||
|
|
||||||
|
- 关卡仍由 `CreateWhiteboxLevel()` 硬编码。
|
||||||
|
|
||||||
|
下一步:
|
||||||
|
|
||||||
|
- 先用 C++ struct 稳定字段。
|
||||||
|
- 再迁移到 JSON 或编辑器导出格式。
|
||||||
|
- 资源路径统一记录,避免散落字符串。
|
||||||
|
|
||||||
|
### 9. Render 与资源
|
||||||
|
|
||||||
|
职责:
|
||||||
|
|
||||||
|
- 绘制角色、关卡、背景、道具、特效和调试图形。
|
||||||
|
- 管理资源目录和运行时拷贝。
|
||||||
|
|
||||||
|
已有:
|
||||||
|
|
||||||
|
- `game/assets/character/male/`
|
||||||
|
- `game/assets/shaders/`
|
||||||
|
- `xmake.lua` 自动复制 `game/assets`
|
||||||
|
|
||||||
|
下一步:
|
||||||
|
|
||||||
|
- 增加 `game/assets/stage/stage_01/`。
|
||||||
|
- 引入 tileset、背景、props。
|
||||||
|
- 增加视差背景绘制。
|
||||||
|
- 建立素材来源和授权记录。
|
||||||
|
|
||||||
|
### 10. Debug 与工具
|
||||||
|
|
||||||
|
职责:
|
||||||
|
|
||||||
|
- 提供开发时必要的可视化和状态观察。
|
||||||
|
- 降低调试碰撞、战斗和关卡触发的成本。
|
||||||
|
|
||||||
|
下一步:
|
||||||
|
|
||||||
|
- `debug/debug_overlay.*` 已经提供碰撞和玩家包围盒绘制。
|
||||||
|
- `F1` 切换 debug overlay。
|
||||||
|
- 绘制平台碰撞。
|
||||||
|
- 绘制 battle zone。
|
||||||
|
- 绘制 spawn point。
|
||||||
|
- 绘制 hitbox / hurtbox。
|
||||||
|
- 输出当前场景、角色状态、FPS。
|
||||||
|
|
||||||
|
### 11. Platform
|
||||||
|
|
||||||
|
职责:
|
||||||
|
|
||||||
|
- 保持 PC 和 NS 共用游戏逻辑。
|
||||||
|
- 只在入口、构建、资源部署、平台输入处做差异化。
|
||||||
|
|
||||||
|
已有:
|
||||||
|
|
||||||
|
- `game/src/main.cpp`:统一游戏入口,通过 `__SWITCH__` 区分 Windows/PC 与 Switch/NS。
|
||||||
|
- `Frostbite2DGame`:Windows 游戏目标。
|
||||||
|
- `Frostbite2DGameSwitch`:Switch 游戏目标,生成 `NSUnknownGame.nro`。
|
||||||
|
- Switch NRO 已内嵌 `game/` 为 RomFS,运行时优先读取 `romfs:/assets`。
|
||||||
|
|
||||||
|
下一步:
|
||||||
|
|
||||||
|
- 将 Switch Joy-Con 输入接入 `InputStateProvider`。
|
||||||
|
- 在 Ryujinx 或真机上验证主循环、shader 和角色纹理。
|
||||||
|
- 如果 RomFS 不可用,再补 SD 卡外部资源部署脚本。
|
||||||
|
|
||||||
|
## 数据流白板
|
||||||
|
|
||||||
|
```text
|
||||||
|
输入
|
||||||
|
SDL Keyboard / Controller
|
||||||
|
-> InputStateProvider
|
||||||
|
-> InputState
|
||||||
|
-> PlayerActor
|
||||||
|
|
||||||
|
移动
|
||||||
|
PlayerActor / EnemyActor
|
||||||
|
-> MovementBody
|
||||||
|
-> PlatformMover
|
||||||
|
-> PlatformWorld
|
||||||
|
-> 更新 Actor position / grounded
|
||||||
|
|
||||||
|
战斗
|
||||||
|
Actor 当前攻击状态
|
||||||
|
-> 生成 Hitbox
|
||||||
|
-> CombatSystem
|
||||||
|
-> Hurtbox 匹配
|
||||||
|
-> DamageEvent
|
||||||
|
-> Actor 受击/击退/死亡
|
||||||
|
|
||||||
|
关卡
|
||||||
|
LevelDefinition
|
||||||
|
-> Scene 创建 Actor
|
||||||
|
-> StageRenderer 绘制视觉层
|
||||||
|
-> PlatformWorld 提供碰撞
|
||||||
|
-> BattleZone 控制推进
|
||||||
|
|
||||||
|
渲染
|
||||||
|
Scene::Render
|
||||||
|
-> StageRenderer
|
||||||
|
-> Actor::Render
|
||||||
|
-> Effects
|
||||||
|
-> DebugOverlay
|
||||||
|
```
|
||||||
|
|
||||||
|
## 开发路线白板
|
||||||
|
|
||||||
|
### Milestone 1:白盒玩法闭环
|
||||||
|
|
||||||
|
目标:验证 ACT 基础手感。
|
||||||
|
|
||||||
|
- 玩家移动、跳跃、出拳稳定。
|
||||||
|
- 第一关横向推进可从头跑到尾。
|
||||||
|
- battle zone 可触发、锁镜头、解锁。
|
||||||
|
- 有 1 种敌人占位。
|
||||||
|
- 有 hitbox/hurtbox debug。
|
||||||
|
|
||||||
|
### Milestone 2:灰盒战斗验证
|
||||||
|
|
||||||
|
目标:验证清版节奏。
|
||||||
|
|
||||||
|
- 玩家 2 到 3 段基础连击。
|
||||||
|
- 敌人追击、攻击、受击、死亡。
|
||||||
|
- 每个 battle zone 有敌人波次。
|
||||||
|
- 有简单掉落或奖励。
|
||||||
|
- 关卡能完整结算。
|
||||||
|
|
||||||
|
### Milestone 3:第一关美术测试
|
||||||
|
|
||||||
|
目标:替换白盒视觉。
|
||||||
|
|
||||||
|
- 场景有 2 到 3 层视差背景。
|
||||||
|
- 地面和平台使用 tileset。
|
||||||
|
- props 丰富空间轮廓。
|
||||||
|
- 保留 debug overlay。
|
||||||
|
- 记录素材来源和授权。
|
||||||
|
|
||||||
|
### Milestone 4:数据化
|
||||||
|
|
||||||
|
目标:降低后续扩展成本。
|
||||||
|
|
||||||
|
- 动画配置数据化。
|
||||||
|
- 攻击参数数据化。
|
||||||
|
- 敌人参数数据化。
|
||||||
|
- 关卡数据逐步外部化。
|
||||||
|
|
||||||
|
## 当前优先级
|
||||||
|
|
||||||
|
下一步建议按这个顺序做:
|
||||||
|
|
||||||
|
1. `BattleZone`:做清版推进的核心框架。
|
||||||
|
2. `SwitchInput`:把 Joy-Con/手柄输入接入 `InputStateProvider`。
|
||||||
|
3. `EnemyActor`:做第一个敌人占位。
|
||||||
|
4. `CombatSystem`:把 hitbox/hurtbox 真正跑起来。
|
||||||
|
5. `SpriteAnimator`:把玩家动画从硬编码改成组件。
|
||||||
|
6. `StageLayer`:把白盒视觉层扩展为背景、tiles 和 props。
|
||||||
|
|
||||||
|
## 素材协作提示
|
||||||
|
|
||||||
|
你找素材时优先找:
|
||||||
|
|
||||||
|
- 横版 tileset:地面、平台、墙面。
|
||||||
|
- 远景/中景背景:可以横向拼接。
|
||||||
|
- props:箱子、桶、路障、门、灯、标牌。
|
||||||
|
- 敌人临时素材:有 idle/run/attack/hurt/death 最好。
|
||||||
|
- 特效:命中火花、尘土、落地、破坏碎片。
|
||||||
|
|
||||||
|
素材回来后先不急着追求最终题材,先按 `stage_01` 做一个可玩的美术测试关。
|
||||||
@@ -0,0 +1,210 @@
|
|||||||
|
# 场景搭建计划
|
||||||
|
|
||||||
|
目标:先把第一关从白盒推进到可替换素材的灰盒/美术测试场景。当前游戏方向暂定为横板 2D ACT 清版,因此场景需要服务于移动、跳跃、攻击判定、敌人刷出、镜头推进和战斗节奏。
|
||||||
|
|
||||||
|
## 1. 当前起点
|
||||||
|
|
||||||
|
已有内容:
|
||||||
|
|
||||||
|
- `WhiteboxScene`:当前白盒场景入口。
|
||||||
|
- `CreateWhiteboxLevel()`:第一版关卡数据,包含平台碰撞和白盒装饰块。
|
||||||
|
- `PlatformWorld` / `PlatformMover`:基础平台碰撞和重力移动。
|
||||||
|
- `PlayerActor`:玩家移动、跳跃、出拳和基础动画。
|
||||||
|
- `Frostbite2DGame`:Windows 游戏目标,可直接构建运行。
|
||||||
|
|
||||||
|
当前白盒只解决“能跑、能跳、能看到关卡结构”。下一步要把场景拆成数据层、视觉层和玩法层。
|
||||||
|
|
||||||
|
## 2. 第一关场景目标
|
||||||
|
|
||||||
|
第一关不急着做最终题材,先做通用 ACT 测试关:
|
||||||
|
|
||||||
|
- 横向推进,总宽度先控制在 2400 到 3200 像素。
|
||||||
|
- 主要视口仍用 1280x720。
|
||||||
|
- 地面以平路为主,加入 2 到 3 段平台高低差。
|
||||||
|
- 至少预留 3 个战斗区域,每个区域宽度 480 到 720 像素。
|
||||||
|
- 每个战斗区域有明确的入口、站位空间、敌人刷出点和退出方向。
|
||||||
|
- 保留一个跳跃测试段,用来验证移动手感。
|
||||||
|
- 保留一个窄平台/边缘段,用来验证镜头和碰撞。
|
||||||
|
|
||||||
|
## 3. 场景分层
|
||||||
|
|
||||||
|
后续场景按以下层拆:
|
||||||
|
|
||||||
|
```text
|
||||||
|
background_far 远景天空、城市、山体、洞窟深处等,不参与碰撞
|
||||||
|
background_mid 中景建筑、树、墙面、远处平台,不参与碰撞
|
||||||
|
level_collision 地面、平台、墙体,只负责碰撞
|
||||||
|
level_visual 和碰撞对齐的地面/墙面贴图
|
||||||
|
props_back 柱子、牌子、箱子、草丛等后景装饰
|
||||||
|
actors 玩家、敌人、道具、投射物
|
||||||
|
props_front 前景遮挡物、栏杆、近景草等
|
||||||
|
effects 攻击特效、命中特效、尘土、碎片
|
||||||
|
ui HUD、提示、调试信息
|
||||||
|
```
|
||||||
|
|
||||||
|
白盒阶段先实现:
|
||||||
|
|
||||||
|
- `level_collision`
|
||||||
|
- `level_visual`
|
||||||
|
- `props_back`
|
||||||
|
- `actors`
|
||||||
|
|
||||||
|
前景遮挡和特效后面再加。
|
||||||
|
|
||||||
|
## 4. 素材寻找清单
|
||||||
|
|
||||||
|
你先找这些素材,优先像素风,尺寸最好能和 128x128 角色兼容。
|
||||||
|
|
||||||
|
### 必需素材
|
||||||
|
|
||||||
|
- 地面 tileset:至少包含平地、左边缘、右边缘、平台中段。
|
||||||
|
- 墙面或背景 tileset:用于挡板、台阶、平台侧面。
|
||||||
|
- 远景背景:一张或多张可横向拼接背景。
|
||||||
|
- 中景背景:建筑、树、管线、洞窟墙等可以做视差的图层。
|
||||||
|
- 基础装饰物:箱子、桶、路灯、告示牌、碎石、草丛等。
|
||||||
|
- 关卡边界标识:门、路障、出口牌、传送门等任选。
|
||||||
|
|
||||||
|
### ACT 玩法相关素材
|
||||||
|
|
||||||
|
- 敌人站位/出生点可用的临时标记图。
|
||||||
|
- 可破坏物:木箱、罐子、障碍物。
|
||||||
|
- 攻击命中特效:小型 hit spark。
|
||||||
|
- 落地/跑步尘土:小型 dust effect。
|
||||||
|
|
||||||
|
### 可选素材
|
||||||
|
|
||||||
|
- 前景遮挡:栏杆、柱子、近景树叶。
|
||||||
|
- 光照叠加图:窗口光、路灯光、火光等。
|
||||||
|
- 环境动画:旗子、火焰、流水、机器灯。
|
||||||
|
- 背景音乐和环境音。
|
||||||
|
|
||||||
|
## 5. 素材规格建议
|
||||||
|
|
||||||
|
角色当前是 128x128 帧,场景素材建议:
|
||||||
|
|
||||||
|
- tileset 单格优先 `32x32`、`48x48` 或 `64x64`。
|
||||||
|
- 平台碰撞块可以和视觉块分离,不要求每张图都精准碰撞。
|
||||||
|
- 背景图宽度最好大于 1280,或能无缝横向重复。
|
||||||
|
- PNG 优先,透明背景用于 props 和 effects。
|
||||||
|
- 同一套素材尽量保持统一像素密度。
|
||||||
|
- 文件名使用英文小写和下划线,例如 `factory_floor_tiles.png`。
|
||||||
|
|
||||||
|
建议素材目录:
|
||||||
|
|
||||||
|
```text
|
||||||
|
game/assets/stage/stage_01/
|
||||||
|
├─ background/
|
||||||
|
├─ tiles/
|
||||||
|
├─ props/
|
||||||
|
├─ effects/
|
||||||
|
└─ audio/
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. 工程实现步骤
|
||||||
|
|
||||||
|
### Step 1:扩展关卡数据结构
|
||||||
|
|
||||||
|
把 `LevelDefinition` 从纯白盒数据扩展为:
|
||||||
|
|
||||||
|
- 世界宽高。
|
||||||
|
- 碰撞平台列表。
|
||||||
|
- 视觉 tile/prop 列表。
|
||||||
|
- 玩家出生点。
|
||||||
|
- 敌人出生点。
|
||||||
|
- 战斗区域触发范围。
|
||||||
|
- 镜头限制范围。
|
||||||
|
|
||||||
|
### Step 2:实现 StageRenderer
|
||||||
|
|
||||||
|
新增 `scene` 或 `stage` 模块,负责绘制:
|
||||||
|
|
||||||
|
- 背景色。
|
||||||
|
- 远景背景。
|
||||||
|
- 中景背景。
|
||||||
|
- tile 层。
|
||||||
|
- prop 层。
|
||||||
|
- debug 碰撞层。
|
||||||
|
|
||||||
|
第一版可以继续用 `Renderer::drawQuad` 和 `Sprite`,不急着做 tilemap 优化。
|
||||||
|
|
||||||
|
### Step 3:替换白盒视觉
|
||||||
|
|
||||||
|
保持碰撞数据不变,先把地面和平台换成 tileset。
|
||||||
|
|
||||||
|
验收标准:
|
||||||
|
|
||||||
|
- 玩家仍然能正常落地和跳跃。
|
||||||
|
- 视觉地面和碰撞地面误差不超过 4 像素。
|
||||||
|
- 镜头移动时背景不黑屏、不闪烁。
|
||||||
|
|
||||||
|
### Step 4:加入视差背景
|
||||||
|
|
||||||
|
远景和中景随相机不同速度移动:
|
||||||
|
|
||||||
|
- 远景速度:相机速度的 20% 到 35%。
|
||||||
|
- 中景速度:相机速度的 45% 到 65%。
|
||||||
|
- 地面和角色速度:100%。
|
||||||
|
|
||||||
|
### Step 5:加入战斗区域数据
|
||||||
|
|
||||||
|
在关卡中标出 3 个 battle zone:
|
||||||
|
|
||||||
|
- `zone_01_intro`
|
||||||
|
- `zone_02_platform`
|
||||||
|
- `zone_03_exit`
|
||||||
|
|
||||||
|
每个区域包含:
|
||||||
|
|
||||||
|
- 触发矩形。
|
||||||
|
- 敌人刷出点。
|
||||||
|
- 镜头锁定范围。
|
||||||
|
- 清场后解锁出口。
|
||||||
|
|
||||||
|
第一版可以只画 debug 框,不立刻做敌人。
|
||||||
|
|
||||||
|
### Step 6:加入 debug 显示开关
|
||||||
|
|
||||||
|
需要能显示/隐藏:
|
||||||
|
|
||||||
|
- 碰撞矩形。
|
||||||
|
- 玩家 hurtbox。
|
||||||
|
- 攻击 hitbox。
|
||||||
|
- 敌人出生点。
|
||||||
|
- battle zone。
|
||||||
|
|
||||||
|
建议先用键盘 `F1` 切换。
|
||||||
|
|
||||||
|
## 7. 第一版验收标准
|
||||||
|
|
||||||
|
第一阶段完成时应该达到:
|
||||||
|
|
||||||
|
- 能启动 `Frostbite2DGame`。
|
||||||
|
- 第一关宽度至少 2400 像素。
|
||||||
|
- 玩家可以从起点跑到终点。
|
||||||
|
- 至少 5 个平台/地面碰撞块。
|
||||||
|
- 至少 2 层背景。
|
||||||
|
- 至少 8 个装饰物。
|
||||||
|
- 至少 3 个 battle zone debug 标记。
|
||||||
|
- 场景资源路径集中在 `game/assets/stage/stage_01/`。
|
||||||
|
- 文档记录已使用素材来源和授权信息。
|
||||||
|
|
||||||
|
## 8. 素材回来后的落地顺序
|
||||||
|
|
||||||
|
素材准备好后按这个顺序处理:
|
||||||
|
|
||||||
|
1. 整理素材到 `game/assets/stage/stage_01/`。
|
||||||
|
2. 记录素材来源和授权。
|
||||||
|
3. 先接背景图,验证相机移动。
|
||||||
|
4. 再接地面/平台 tiles,验证碰撞对齐。
|
||||||
|
5. 再接 props,丰富场景轮廓。
|
||||||
|
6. 最后接 effects 和音频。
|
||||||
|
|
||||||
|
## 9. 下一轮建议任务
|
||||||
|
|
||||||
|
你找到素材后,下一轮优先做:
|
||||||
|
|
||||||
|
- 创建 `stage_01` 资源目录。
|
||||||
|
- 新增 `StageRenderer`。
|
||||||
|
- 扩展 `LevelDefinition`。
|
||||||
|
- 把当前白盒平台替换成第一版 tile 视觉。
|
||||||
|
- 保留 debug 碰撞显示。
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
#include "player_actor.h"
|
||||||
|
|
||||||
|
#include "../core/asset_paths.h"
|
||||||
|
#include "../core/game_config.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
namespace ns_game {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr float kSpriteOffsetX = -32.0f;
|
||||||
|
constexpr float kSpriteOffsetY = -24.0f;
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
PlayerActor::PlayerActor() {
|
||||||
|
SetSize(64.0f, 104.0f);
|
||||||
|
SetTopLeftPosition(160.0f, 380.0f);
|
||||||
|
loadSprites();
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlayerActor::SetMovementWorld(const PlatformWorld* world) {
|
||||||
|
movementWorld_ = world;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlayerActor::loadSprites() {
|
||||||
|
idleSprite_ = frostbite2D::Sprite::createFromFile(assets::kMaleIdle);
|
||||||
|
runSprite_ = frostbite2D::Sprite::createFromFile(assets::kMaleRun);
|
||||||
|
punchSprite_ = frostbite2D::Sprite::createFromFile(assets::kMalePunch);
|
||||||
|
|
||||||
|
auto configure = [this](frostbite2D::Ptr<frostbite2D::Sprite> sprite) {
|
||||||
|
if (!sprite) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sprite->SetSize(config::kPlayerFrameSize, config::kPlayerFrameSize);
|
||||||
|
sprite->SetSourceRect(frostbite2D::Rect(0.0f, 0.0f,
|
||||||
|
config::kPlayerFrameSize,
|
||||||
|
config::kPlayerFrameSize));
|
||||||
|
sprite->SetTopLeftPosition(kSpriteOffsetX, kSpriteOffsetY);
|
||||||
|
sprite->SetVisible(false);
|
||||||
|
AddChild(sprite);
|
||||||
|
};
|
||||||
|
|
||||||
|
configure(idleSprite_);
|
||||||
|
configure(runSprite_);
|
||||||
|
configure(punchSprite_);
|
||||||
|
setAnimState(AnimState::Idle);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlayerActor::OnUpdate(float deltaTime) {
|
||||||
|
deltaTime = std::min(deltaTime, 1.0f / 30.0f);
|
||||||
|
updateInput();
|
||||||
|
updatePhysics(deltaTime);
|
||||||
|
updateAnimation(deltaTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlayerActor::updateInput() {
|
||||||
|
const InputState input = inputProvider_.Sample();
|
||||||
|
velocity_.x = input.moveX * config::kPlayerMoveSpeed;
|
||||||
|
if (input.moveX < 0.0f) {
|
||||||
|
facingLeft_ = true;
|
||||||
|
} else if (input.moveX > 0.0f) {
|
||||||
|
facingLeft_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.jumpPressed && grounded_) {
|
||||||
|
velocity_.y = config::kPlayerJumpSpeed;
|
||||||
|
grounded_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.attackPressed) {
|
||||||
|
attackTimer_ = config::kAttackDuration;
|
||||||
|
setAnimState(AnimState::Punch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlayerActor::updatePhysics(float deltaTime) {
|
||||||
|
if (!movementWorld_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
MovementBody body;
|
||||||
|
body.position = GetTopLeftPosition();
|
||||||
|
body.size = GetSize();
|
||||||
|
body.velocity = velocity_;
|
||||||
|
body.grounded = grounded_;
|
||||||
|
|
||||||
|
body = platformMover_.Step(body, *movementWorld_, config::kGravity, deltaTime);
|
||||||
|
velocity_ = body.velocity;
|
||||||
|
grounded_ = body.grounded;
|
||||||
|
SetTopLeftPosition(body.position);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlayerActor::updateAnimation(float deltaTime) {
|
||||||
|
if (attackTimer_ > 0.0f) {
|
||||||
|
attackTimer_ = std::max(0.0f, attackTimer_ - deltaTime);
|
||||||
|
setAnimState(AnimState::Punch);
|
||||||
|
} else if (std::abs(velocity_.x) > 1.0f) {
|
||||||
|
setAnimState(AnimState::Run);
|
||||||
|
} else {
|
||||||
|
setAnimState(AnimState::Idle);
|
||||||
|
}
|
||||||
|
|
||||||
|
int frameCount = 10;
|
||||||
|
float frameDuration = 0.1f;
|
||||||
|
if (animState_ == AnimState::Idle) {
|
||||||
|
frameDuration = 0.16f;
|
||||||
|
} else if (animState_ == AnimState::Punch) {
|
||||||
|
frameDuration = 0.07f;
|
||||||
|
}
|
||||||
|
|
||||||
|
animTimer_ += deltaTime;
|
||||||
|
while (animTimer_ >= frameDuration) {
|
||||||
|
animTimer_ -= frameDuration;
|
||||||
|
frameIndex_ = (frameIndex_ + 1) % frameCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto sprite = activeSprite();
|
||||||
|
if (sprite) {
|
||||||
|
sprite->SetSourceRect(
|
||||||
|
frostbite2D::Rect(frameIndex_ * config::kPlayerFrameSize, 0.0f,
|
||||||
|
config::kPlayerFrameSize,
|
||||||
|
config::kPlayerFrameSize));
|
||||||
|
sprite->SetFlippedX(facingLeft_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlayerActor::setAnimState(AnimState state) {
|
||||||
|
if (animState_ != state) {
|
||||||
|
animState_ = state;
|
||||||
|
animTimer_ = 0.0f;
|
||||||
|
frameIndex_ = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (idleSprite_) {
|
||||||
|
idleSprite_->SetVisible(state == AnimState::Idle);
|
||||||
|
}
|
||||||
|
if (runSprite_) {
|
||||||
|
runSprite_->SetVisible(state == AnimState::Run);
|
||||||
|
}
|
||||||
|
if (punchSprite_) {
|
||||||
|
punchSprite_->SetVisible(state == AnimState::Punch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
frostbite2D::Ptr<frostbite2D::Sprite> PlayerActor::activeSprite() const {
|
||||||
|
switch (animState_) {
|
||||||
|
case AnimState::Run:
|
||||||
|
return runSprite_;
|
||||||
|
case AnimState::Punch:
|
||||||
|
return punchSprite_;
|
||||||
|
case AnimState::Idle:
|
||||||
|
default:
|
||||||
|
return idleSprite_;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ns_game
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "../core/input_state.h"
|
||||||
|
#include "../movement/platform_world.h"
|
||||||
|
|
||||||
|
#include <frostbite2D/2d/actor.h>
|
||||||
|
#include <frostbite2D/2d/sprite.h>
|
||||||
|
#include <frostbite2D/types/type_math.h>
|
||||||
|
|
||||||
|
namespace ns_game {
|
||||||
|
|
||||||
|
class PlayerActor : public frostbite2D::Actor {
|
||||||
|
public:
|
||||||
|
PlayerActor();
|
||||||
|
|
||||||
|
void SetMovementWorld(const PlatformWorld* world);
|
||||||
|
void OnUpdate(float deltaTime) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
enum class AnimState {
|
||||||
|
Idle,
|
||||||
|
Run,
|
||||||
|
Punch
|
||||||
|
};
|
||||||
|
|
||||||
|
void loadSprites();
|
||||||
|
void updateInput();
|
||||||
|
void updatePhysics(float deltaTime);
|
||||||
|
void updateAnimation(float deltaTime);
|
||||||
|
void setAnimState(AnimState state);
|
||||||
|
frostbite2D::Ptr<frostbite2D::Sprite> activeSprite() const;
|
||||||
|
|
||||||
|
const PlatformWorld* movementWorld_ = nullptr;
|
||||||
|
InputStateProvider inputProvider_;
|
||||||
|
PlatformMover platformMover_;
|
||||||
|
frostbite2D::Ptr<frostbite2D::Sprite> idleSprite_;
|
||||||
|
frostbite2D::Ptr<frostbite2D::Sprite> runSprite_;
|
||||||
|
frostbite2D::Ptr<frostbite2D::Sprite> punchSprite_;
|
||||||
|
|
||||||
|
frostbite2D::Vec2 velocity_;
|
||||||
|
AnimState animState_ = AnimState::Idle;
|
||||||
|
float animTimer_ = 0.0f;
|
||||||
|
float attackTimer_ = 0.0f;
|
||||||
|
int frameIndex_ = 0;
|
||||||
|
bool facingLeft_ = false;
|
||||||
|
bool grounded_ = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ns_game
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <frostbite2D/types/type_math.h>
|
||||||
|
|
||||||
|
namespace ns_game {
|
||||||
|
|
||||||
|
enum class Team {
|
||||||
|
Player,
|
||||||
|
Enemy
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Hitbox {
|
||||||
|
frostbite2D::Rect bounds;
|
||||||
|
Team ownerTeam = Team::Player;
|
||||||
|
int damage = 1;
|
||||||
|
float activeSeconds = 0.0f;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Hurtbox {
|
||||||
|
frostbite2D::Rect bounds;
|
||||||
|
Team ownerTeam = Team::Enemy;
|
||||||
|
};
|
||||||
|
|
||||||
|
inline bool CanHit(const Hitbox& hitbox, const Hurtbox& hurtbox) {
|
||||||
|
return hitbox.ownerTeam != hurtbox.ownerTeam &&
|
||||||
|
hitbox.bounds.intersects(hurtbox.bounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ns_game
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace ns_game::assets {
|
||||||
|
|
||||||
|
constexpr const char* kMaleIdle =
|
||||||
|
"assets/character/male/Male_spritesheet_idle.png";
|
||||||
|
constexpr const char* kMaleRun =
|
||||||
|
"assets/character/male/Male_spritesheet_run.png";
|
||||||
|
constexpr const char* kMalePunch =
|
||||||
|
"assets/character/male/Male_spritesheet_punch_1.png";
|
||||||
|
|
||||||
|
} // namespace ns_game::assets
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace ns_game {
|
||||||
|
|
||||||
|
struct DebugFlags {
|
||||||
|
bool showStageGrid = true;
|
||||||
|
bool showCollision = true;
|
||||||
|
bool showActorBounds = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ns_game
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace ns_game::config {
|
||||||
|
|
||||||
|
constexpr int kVirtualWidth = 1280;
|
||||||
|
constexpr int kVirtualHeight = 720;
|
||||||
|
constexpr float kPlayerFrameSize = 128.0f;
|
||||||
|
constexpr float kPlayerMoveSpeed = 300.0f;
|
||||||
|
constexpr float kPlayerJumpSpeed = -620.0f;
|
||||||
|
constexpr float kGravity = 1800.0f;
|
||||||
|
constexpr float kAttackDuration = 0.28f;
|
||||||
|
|
||||||
|
} // namespace ns_game::config
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
#include "input_state.h"
|
||||||
|
|
||||||
|
#include <SDL2/SDL.h>
|
||||||
|
|
||||||
|
namespace ns_game {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
bool isDown(const Uint8* keys, SDL_Scancode key) {
|
||||||
|
return keys && keys[key] != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
InputState InputStateProvider::Sample() {
|
||||||
|
const Uint8* keys = SDL_GetKeyboardState(nullptr);
|
||||||
|
InputState state;
|
||||||
|
|
||||||
|
if (isDown(keys, SDL_SCANCODE_A) || isDown(keys, SDL_SCANCODE_LEFT)) {
|
||||||
|
state.moveX -= 1.0f;
|
||||||
|
}
|
||||||
|
if (isDown(keys, SDL_SCANCODE_D) || isDown(keys, SDL_SCANCODE_RIGHT)) {
|
||||||
|
state.moveX += 1.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool jumpHeld =
|
||||||
|
isDown(keys, SDL_SCANCODE_SPACE) || isDown(keys, SDL_SCANCODE_W) ||
|
||||||
|
isDown(keys, SDL_SCANCODE_UP);
|
||||||
|
const bool attackHeld =
|
||||||
|
isDown(keys, SDL_SCANCODE_J) || isDown(keys, SDL_SCANCODE_K);
|
||||||
|
|
||||||
|
state.jumpPressed = jumpHeld && !jumpHeldLastFrame_;
|
||||||
|
state.attackPressed = attackHeld && !attackHeldLastFrame_;
|
||||||
|
|
||||||
|
jumpHeldLastFrame_ = jumpHeld;
|
||||||
|
attackHeldLastFrame_ = attackHeld;
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ns_game
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace ns_game {
|
||||||
|
|
||||||
|
struct InputState {
|
||||||
|
float moveX = 0.0f;
|
||||||
|
bool jumpPressed = false;
|
||||||
|
bool attackPressed = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
class InputStateProvider {
|
||||||
|
public:
|
||||||
|
InputState Sample();
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool jumpHeldLastFrame_ = false;
|
||||||
|
bool attackHeldLastFrame_ = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ns_game
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "../movement/platform_world.h"
|
||||||
|
#include <frostbite2D/types/type_color.h>
|
||||||
|
#include <frostbite2D/types/type_math.h>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace ns_game {
|
||||||
|
|
||||||
|
struct WhiteboxProp {
|
||||||
|
frostbite2D::Rect bounds;
|
||||||
|
frostbite2D::Color color;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct LevelDefinition {
|
||||||
|
PlatformWorld collision;
|
||||||
|
std::vector<WhiteboxProp> props;
|
||||||
|
};
|
||||||
|
|
||||||
|
LevelDefinition CreateWhiteboxLevel();
|
||||||
|
|
||||||
|
} // namespace ns_game
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
#include "level_definition.h"
|
||||||
|
|
||||||
|
namespace ns_game {
|
||||||
|
|
||||||
|
LevelDefinition CreateWhiteboxLevel() {
|
||||||
|
LevelDefinition level;
|
||||||
|
level.collision.levelLeft = 0.0f;
|
||||||
|
level.collision.levelRight = 1600.0f;
|
||||||
|
level.collision.platforms = {
|
||||||
|
frostbite2D::Rect(0.0f, 600.0f, 1600.0f, 80.0f),
|
||||||
|
frostbite2D::Rect(230.0f, 500.0f, 220.0f, 28.0f),
|
||||||
|
frostbite2D::Rect(560.0f, 430.0f, 260.0f, 28.0f),
|
||||||
|
frostbite2D::Rect(930.0f, 510.0f, 260.0f, 28.0f),
|
||||||
|
frostbite2D::Rect(1250.0f, 420.0f, 230.0f, 28.0f),
|
||||||
|
};
|
||||||
|
level.props = {
|
||||||
|
{frostbite2D::Rect(118.0f, 392.0f, 88.0f, 208.0f),
|
||||||
|
frostbite2D::Color(0.26f, 0.30f, 0.34f, 1.0f)},
|
||||||
|
{frostbite2D::Rect(780.0f, 250.0f, 72.0f, 180.0f),
|
||||||
|
frostbite2D::Color(0.26f, 0.30f, 0.34f, 1.0f)},
|
||||||
|
{frostbite2D::Rect(1440.0f, 300.0f, 80.0f, 300.0f),
|
||||||
|
frostbite2D::Color(0.26f, 0.30f, 0.34f, 1.0f)},
|
||||||
|
};
|
||||||
|
return level;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ns_game
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
#include "debug_overlay.h"
|
||||||
|
|
||||||
|
#include <frostbite2D/graphics/renderer.h>
|
||||||
|
|
||||||
|
namespace ns_game {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
void drawRect(const frostbite2D::Rect& rect, const frostbite2D::Color& color) {
|
||||||
|
frostbite2D::Renderer::get().drawQuad(rect, color);
|
||||||
|
}
|
||||||
|
|
||||||
|
void drawOutline(const frostbite2D::Rect& rect, const frostbite2D::Color& color,
|
||||||
|
float thickness = 2.0f) {
|
||||||
|
drawRect(frostbite2D::Rect(rect.left(), rect.top(), rect.width(), thickness),
|
||||||
|
color);
|
||||||
|
drawRect(frostbite2D::Rect(rect.left(), rect.bottom() - thickness,
|
||||||
|
rect.width(), thickness),
|
||||||
|
color);
|
||||||
|
drawRect(frostbite2D::Rect(rect.left(), rect.top(), thickness, rect.height()),
|
||||||
|
color);
|
||||||
|
drawRect(frostbite2D::Rect(rect.right() - thickness, rect.top(), thickness,
|
||||||
|
rect.height()),
|
||||||
|
color);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void DebugOverlay::Render(const LevelDefinition& level, const DebugFlags& flags,
|
||||||
|
const frostbite2D::Rect* playerBounds) const {
|
||||||
|
if (flags.showCollision) {
|
||||||
|
drawCollision(level);
|
||||||
|
}
|
||||||
|
if (flags.showActorBounds && playerBounds) {
|
||||||
|
drawActorBounds(*playerBounds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DebugOverlay::drawCollision(const LevelDefinition& level) const {
|
||||||
|
for (const auto& platform : level.collision.platforms) {
|
||||||
|
drawOutline(platform, frostbite2D::Color(0.1f, 0.95f, 0.45f, 0.85f));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DebugOverlay::drawActorBounds(const frostbite2D::Rect& bounds) const {
|
||||||
|
drawOutline(bounds, frostbite2D::Color(1.0f, 0.78f, 0.1f, 0.9f));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ns_game
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "../core/debug_flags.h"
|
||||||
|
#include "../data/level_definition.h"
|
||||||
|
|
||||||
|
#include <frostbite2D/types/type_math.h>
|
||||||
|
|
||||||
|
namespace ns_game {
|
||||||
|
|
||||||
|
class DebugOverlay {
|
||||||
|
public:
|
||||||
|
void Render(const LevelDefinition& level, const DebugFlags& flags,
|
||||||
|
const frostbite2D::Rect* playerBounds) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void drawCollision(const LevelDefinition& level) const;
|
||||||
|
void drawActorBounds(const frostbite2D::Rect& bounds) const;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ns_game
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
#include "scene/whitebox_scene.h"
|
||||||
|
#include "core/game_config.h"
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
|
||||||
|
#ifndef __SWITCH__
|
||||||
|
#define SDL_MAIN_HANDLED
|
||||||
|
#else
|
||||||
|
#include <switch.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <frostbite2D/core/application.h>
|
||||||
|
#include <frostbite2D/scene/scene_manager.h>
|
||||||
|
|
||||||
|
#ifdef __SWITCH__
|
||||||
|
namespace {
|
||||||
|
void showSwitchInitFailure() {
|
||||||
|
consoleInit(nullptr);
|
||||||
|
|
||||||
|
PadState pad;
|
||||||
|
padInitializeDefault(&pad);
|
||||||
|
|
||||||
|
std::puts("NS Unknown Game");
|
||||||
|
std::puts("");
|
||||||
|
std::puts("Switch init failed.");
|
||||||
|
std::puts("Check nxlink/Ryujinx logs for the failed init stage.");
|
||||||
|
std::puts("");
|
||||||
|
std::puts("Press + to exit.");
|
||||||
|
|
||||||
|
while (appletMainLoop()) {
|
||||||
|
padUpdate(&pad);
|
||||||
|
if (padGetButtonsDown(&pad) & HidNpadButton_Plus) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
consoleUpdate(nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
consoleExit(nullptr);
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
#endif
|
||||||
|
|
||||||
|
int main(int argc, char** argv) {
|
||||||
|
(void)argc;
|
||||||
|
(void)argv;
|
||||||
|
|
||||||
|
#ifdef __SWITCH__
|
||||||
|
padConfigureInput(1, HidNpadStyleSet_NpadStandard);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
auto config = frostbite2D::AppConfig::createDefault();
|
||||||
|
config.appName = "NS Unknown Game";
|
||||||
|
#ifdef __SWITCH__
|
||||||
|
config.windowConfig.title = "NS Unknown Game";
|
||||||
|
config.windowConfig.width = ns_game::config::kVirtualWidth;
|
||||||
|
config.windowConfig.height = ns_game::config::kVirtualHeight;
|
||||||
|
config.windowConfig.resizable = false;
|
||||||
|
#else
|
||||||
|
config.windowConfig.title = "NS Unknown Game - Whitebox";
|
||||||
|
config.windowConfig.width = 1280;
|
||||||
|
config.windowConfig.height = 720;
|
||||||
|
config.windowConfig.resizable = true;
|
||||||
|
#endif
|
||||||
|
config.windowConfig.vsync = true;
|
||||||
|
config.useVirtualResolution = true;
|
||||||
|
config.virtualWidth = ns_game::config::kVirtualWidth;
|
||||||
|
config.virtualHeight = ns_game::config::kVirtualHeight;
|
||||||
|
config.resolutionMode = frostbite2D::ResolutionScaleMode::Fit;
|
||||||
|
config.renderStyleProfile = frostbite2D::RenderStyleProfileId::PixelArt2D;
|
||||||
|
config.maxFps = 60;
|
||||||
|
|
||||||
|
auto& app = frostbite2D::Application::get();
|
||||||
|
if (!app.init(config)) {
|
||||||
|
#ifdef __SWITCH__
|
||||||
|
std::puts("NS Unknown Game Switch init failed.");
|
||||||
|
showSwitchInitFailure();
|
||||||
|
#else
|
||||||
|
std::puts("NS Unknown Game init failed.");
|
||||||
|
#endif
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
app.run([]() {
|
||||||
|
frostbite2D::SceneManager::get().ReplaceScene(
|
||||||
|
frostbite2D::MakePtr<ns_game::WhiteboxScene>());
|
||||||
|
});
|
||||||
|
|
||||||
|
app.shutdown();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
#include "platform_world.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
namespace ns_game {
|
||||||
|
|
||||||
|
MovementBody PlatformMover::Step(const MovementBody& body,
|
||||||
|
const PlatformWorld& world, float gravity,
|
||||||
|
float deltaTime) const {
|
||||||
|
MovementBody next = body;
|
||||||
|
const frostbite2D::Vec2 previousPosition = next.position;
|
||||||
|
|
||||||
|
next.velocity.y += gravity * deltaTime;
|
||||||
|
next.position += next.velocity * deltaTime;
|
||||||
|
next.grounded = false;
|
||||||
|
|
||||||
|
const float previousBottom = previousPosition.y + next.size.y;
|
||||||
|
const float newBottom = next.position.y + next.size.y;
|
||||||
|
|
||||||
|
for (const auto& platform : world.platforms) {
|
||||||
|
const bool horizontallyOverlaps =
|
||||||
|
next.position.x + next.size.x > platform.left() &&
|
||||||
|
next.position.x < platform.right();
|
||||||
|
const bool wasAbove = previousBottom <= platform.top() + 4.0f;
|
||||||
|
const bool crossedTop = newBottom >= platform.top();
|
||||||
|
if (next.velocity.y >= 0.0f && horizontallyOverlaps && wasAbove &&
|
||||||
|
crossedTop) {
|
||||||
|
next.position.y = platform.top() - next.size.y;
|
||||||
|
next.velocity.y = 0.0f;
|
||||||
|
next.grounded = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
next.position.x =
|
||||||
|
std::clamp(next.position.x, world.levelLeft, world.levelRight - next.size.x);
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ns_game
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <frostbite2D/types/type_math.h>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace ns_game {
|
||||||
|
|
||||||
|
struct PlatformWorld {
|
||||||
|
float levelLeft = 0.0f;
|
||||||
|
float levelRight = 1280.0f;
|
||||||
|
std::vector<frostbite2D::Rect> platforms;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct MovementBody {
|
||||||
|
frostbite2D::Vec2 position;
|
||||||
|
frostbite2D::Vec2 size;
|
||||||
|
frostbite2D::Vec2 velocity;
|
||||||
|
bool grounded = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
class PlatformMover {
|
||||||
|
public:
|
||||||
|
MovementBody Step(const MovementBody& body, const PlatformWorld& world,
|
||||||
|
float gravity, float deltaTime) const;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ns_game
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
#include "whitebox_scene.h"
|
||||||
|
|
||||||
|
#include "../core/game_config.h"
|
||||||
|
|
||||||
|
#include <frostbite2D/core/application.h>
|
||||||
|
#include <frostbite2D/graphics/camera.h>
|
||||||
|
#include <frostbite2D/graphics/renderer.h>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
namespace ns_game {
|
||||||
|
|
||||||
|
void WhiteboxScene::onEnter() {
|
||||||
|
frostbite2D::Scene::onEnter();
|
||||||
|
level_ = CreateWhiteboxLevel();
|
||||||
|
|
||||||
|
player_ = frostbite2D::MakePtr<PlayerActor>();
|
||||||
|
player_->SetMovementWorld(&level_.collision);
|
||||||
|
AddChild(player_);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WhiteboxScene::OnUpdate(float deltaTime) {
|
||||||
|
(void)deltaTime;
|
||||||
|
updateCamera();
|
||||||
|
}
|
||||||
|
|
||||||
|
void WhiteboxScene::Render() {
|
||||||
|
stageRenderer_.Render(level_, debugFlags_);
|
||||||
|
frostbite2D::Scene::Render();
|
||||||
|
const frostbite2D::Rect playerBounds =
|
||||||
|
player_ ? player_->GetWorldBounds() : frostbite2D::Rect::Zero();
|
||||||
|
debugOverlay_.Render(level_, debugFlags_, player_ ? &playerBounds : nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WhiteboxScene::updateCamera() {
|
||||||
|
if (!player_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* renderer = frostbite2D::Application::get().getRenderer();
|
||||||
|
if (!renderer || !renderer->getCamera()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto playerPos = player_->GetTopLeftPosition();
|
||||||
|
const float targetX = std::clamp(playerPos.x - 420.0f,
|
||||||
|
level_.collision.levelLeft,
|
||||||
|
level_.collision.levelRight -
|
||||||
|
static_cast<float>(config::kVirtualWidth));
|
||||||
|
renderer->getCamera()->setPosition(frostbite2D::Vec2(targetX, 0.0f));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ns_game
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "../actor/player_actor.h"
|
||||||
|
#include "../core/debug_flags.h"
|
||||||
|
#include "../data/level_definition.h"
|
||||||
|
#include "../debug/debug_overlay.h"
|
||||||
|
#include "../stage/stage_renderer.h"
|
||||||
|
|
||||||
|
#include <frostbite2D/scene/scene.h>
|
||||||
|
#include <frostbite2D/types/type_math.h>
|
||||||
|
|
||||||
|
namespace ns_game {
|
||||||
|
|
||||||
|
class WhiteboxScene : public frostbite2D::Scene {
|
||||||
|
public:
|
||||||
|
void onEnter() override;
|
||||||
|
void OnUpdate(float deltaTime) override;
|
||||||
|
void Render() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void updateCamera();
|
||||||
|
|
||||||
|
LevelDefinition level_;
|
||||||
|
DebugFlags debugFlags_;
|
||||||
|
StageRenderer stageRenderer_;
|
||||||
|
DebugOverlay debugOverlay_;
|
||||||
|
frostbite2D::Ptr<PlayerActor> player_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ns_game
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
#include "stage_renderer.h"
|
||||||
|
|
||||||
|
#include "../core/game_config.h"
|
||||||
|
|
||||||
|
#include <frostbite2D/graphics/renderer.h>
|
||||||
|
|
||||||
|
namespace ns_game {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
void drawRect(const frostbite2D::Rect& rect, const frostbite2D::Color& color) {
|
||||||
|
frostbite2D::Renderer::get().drawQuad(rect, color);
|
||||||
|
}
|
||||||
|
|
||||||
|
float worldWidthOf(const LevelDefinition& level) {
|
||||||
|
return level.collision.levelRight - level.collision.levelLeft;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void StageRenderer::Render(const LevelDefinition& level,
|
||||||
|
const DebugFlags& debugFlags) const {
|
||||||
|
drawBackground(level);
|
||||||
|
if (debugFlags.showStageGrid) {
|
||||||
|
drawGrid(level);
|
||||||
|
}
|
||||||
|
drawProps(level);
|
||||||
|
drawPlatforms(level);
|
||||||
|
}
|
||||||
|
|
||||||
|
void StageRenderer::drawBackground(const LevelDefinition& level) const {
|
||||||
|
drawRect(frostbite2D::Rect(level.collision.levelLeft, 0.0f,
|
||||||
|
worldWidthOf(level), config::kVirtualHeight),
|
||||||
|
frostbite2D::Color(0.10f, 0.12f, 0.14f, 1.0f));
|
||||||
|
}
|
||||||
|
|
||||||
|
void StageRenderer::drawGrid(const LevelDefinition& level) const {
|
||||||
|
const float worldWidth = worldWidthOf(level);
|
||||||
|
for (float x = level.collision.levelLeft; x <= level.collision.levelRight;
|
||||||
|
x += 80.0f) {
|
||||||
|
drawRect(frostbite2D::Rect(x, 0.0f, 1.0f, config::kVirtualHeight),
|
||||||
|
frostbite2D::Color(0.20f, 0.22f, 0.25f, 1.0f));
|
||||||
|
}
|
||||||
|
for (float y = 0.0f; y <= config::kVirtualHeight; y += 80.0f) {
|
||||||
|
drawRect(frostbite2D::Rect(level.collision.levelLeft, y, worldWidth, 1.0f),
|
||||||
|
frostbite2D::Color(0.20f, 0.22f, 0.25f, 1.0f));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void StageRenderer::drawProps(const LevelDefinition& level) const {
|
||||||
|
for (const auto& prop : level.props) {
|
||||||
|
drawRect(prop.bounds, prop.color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void StageRenderer::drawPlatforms(const LevelDefinition& level) const {
|
||||||
|
for (const auto& platform : level.collision.platforms) {
|
||||||
|
drawRect(platform, frostbite2D::Color(0.62f, 0.68f, 0.73f, 1.0f));
|
||||||
|
drawRect(frostbite2D::Rect(platform.left(), platform.top(),
|
||||||
|
platform.width(), 4.0f),
|
||||||
|
frostbite2D::Color(0.91f, 0.95f, 0.98f, 1.0f));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ns_game
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "../data/level_definition.h"
|
||||||
|
#include "../core/debug_flags.h"
|
||||||
|
|
||||||
|
namespace ns_game {
|
||||||
|
|
||||||
|
class StageRenderer {
|
||||||
|
public:
|
||||||
|
void Render(const LevelDefinition& level, const DebugFlags& debugFlags) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void drawBackground(const LevelDefinition& level) const;
|
||||||
|
void drawGrid(const LevelDefinition& level) const;
|
||||||
|
void drawProps(const LevelDefinition& level) const;
|
||||||
|
void drawPlatforms(const LevelDefinition& level) const;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ns_game
|
||||||
@@ -31,4 +31,58 @@ if is_plat("windows") then
|
|||||||
add_packages("zlib")
|
add_packages("zlib")
|
||||||
add_syslinks("imm32")
|
add_syslinks("imm32")
|
||||||
target_end()
|
target_end()
|
||||||
|
|
||||||
|
target("Frostbite2DGame")
|
||||||
|
set_kind("binary")
|
||||||
|
add_files("game/src/**.cpp")
|
||||||
|
add_deps("Frostbite2D")
|
||||||
|
add_packages("libsdl2")
|
||||||
|
add_packages("libsdl2_image")
|
||||||
|
add_packages("libsdl2_mixer")
|
||||||
|
add_packages("libsdl2_ttf")
|
||||||
|
add_packages("glm")
|
||||||
|
add_packages("zlib")
|
||||||
|
add_syslinks("imm32")
|
||||||
|
after_build(function (target)
|
||||||
|
os.cp(path.join(os.projectdir(), "game/assets"),
|
||||||
|
path.join(target:targetdir(), "assets"))
|
||||||
|
end)
|
||||||
|
target_end()
|
||||||
|
end
|
||||||
|
|
||||||
|
if is_plat("switch") then
|
||||||
|
target("Frostbite2DGameSwitch")
|
||||||
|
set_kind("binary")
|
||||||
|
set_filename("Frostbite2DGame.elf")
|
||||||
|
add_files("game/src/**.cpp")
|
||||||
|
add_deps("Frostbite2D")
|
||||||
|
add_cxflags("-D__SWITCH__", "-fPIE", {public = true})
|
||||||
|
add_ldflags("-specs=" .. path.join(os.getenv("DEVKITPRO") or "C:/devkitPro", "libnx/switch.specs"), "-fPIE", {force = true})
|
||||||
|
after_build(function (target)
|
||||||
|
local outdir = path.join(target:targetdir(), "switch_game")
|
||||||
|
os.mkdir(outdir)
|
||||||
|
os.cp(path.join(os.projectdir(), "game/assets"),
|
||||||
|
path.join(outdir, "assets"))
|
||||||
|
|
||||||
|
local devkitpro = os.getenv("DEVKITPRO") or "C:/devkitPro"
|
||||||
|
local tools = path.join(devkitpro, "tools/bin")
|
||||||
|
local libnx = path.join(devkitpro, "libnx")
|
||||||
|
local nacp = path.join(outdir, "control.nacp")
|
||||||
|
local nro = path.join(outdir, "NSUnknownGame.nro")
|
||||||
|
local romfsdir = path.join(outdir, "romfs")
|
||||||
|
os.rm(romfsdir)
|
||||||
|
os.mkdir(romfsdir)
|
||||||
|
os.cp(path.join(os.projectdir(), "game/assets"),
|
||||||
|
path.join(romfsdir, "assets"))
|
||||||
|
os.execv(path.join(tools, "nacptool.exe"), {
|
||||||
|
"--create", "NS Unknown Game", "Frostbite2D", "1.0.0", nacp
|
||||||
|
})
|
||||||
|
os.execv(path.join(tools, "elf2nro.exe"), {
|
||||||
|
target:targetfile(), nro,
|
||||||
|
"--nacp=" .. nacp,
|
||||||
|
"--icon=" .. path.join(libnx, "default_icon.jpg"),
|
||||||
|
"--romfsdir=" .. romfsdir
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
target_end()
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user