Compare commits

..

9 Commits

Author SHA1 Message Date
wy 8bbb847a1b Fix Switch startup save path 2026-06-09 01:13:07 +08:00
wy c87b245cea Organize game planning docs 2026-06-09 01:02:36 +08:00
wy e8b05e93b9 Embed Switch game assets in RomFS 2026-06-09 01:00:20 +08:00
wy b78aaa5cad Add Switch game framework target 2026-06-09 00:52:04 +08:00
wy cc75b94c44 Split stage rendering and debug overlay 2026-06-09 00:39:15 +08:00
wy ce815ed58b Localize game architecture documentation 2026-06-09 00:29:23 +08:00
wy b6bc29ae19 Add game architecture board 2026-06-09 00:07:21 +08:00
wy 3db02af593 Add scene build plan 2026-06-08 23:56:45 +08:00
wy e52cf94336 Add game framework and whitebox level 2026-06-08 23:47:32 +08:00
37 changed files with 1692 additions and 14 deletions
+88
View File
@@ -237,6 +237,72 @@ Frostbite2D Switch smoke test
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.1 切换平台后编译异常
@@ -267,3 +333,25 @@ Frostbite2D_smoke_meta.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 paused_ = false;
bool shouldQuit_ = false;
bool sdlInitialized_ = false;
#ifdef __SWITCH__
bool switchPlatformInitialized_ = false;
bool switchRomfsMounted_ = false;
#endif
bool textInputEnabled_ = false;
bool immEnabled_ = false;
int textInputRequestCount_ = 0;
@@ -389,6 +389,7 @@ private:
fs::path toPath(const std::string &path) const;
std::string fromPath(const fs::path &path) const;
std::string resolveFullPath(const std::string &path) const;
bool isDevicePath(const std::string &path) const;
std::string workingDirectory_;
std::string assetRoot_;
@@ -27,6 +27,9 @@
#include <frostbite2D/scene/scene_manager.h>
#include <frostbite2D/types/type_math.h>
#include <frostbite2D/utils/startup_trace.h>
#ifdef __SWITCH__
#include <switch.h>
#endif
namespace frostbite2D {
@@ -55,6 +58,7 @@ bool Application::init(const AppConfig& config) {
{
ScopedStartupTrace stageTrace("switchInit");
switchInit();
switchPlatformInitialized_ = true;
}
#endif
@@ -77,7 +81,11 @@ bool Application::init(const AppConfig& config) {
}
void Application::shutdown() {
if (!initialized_)
if (!initialized_ && !sdlInitialized_
#ifdef __SWITCH__
&& !switchPlatformInitialized_ && !switchRomfsMounted_
#endif
)
return;
running_ = false;
@@ -112,17 +120,25 @@ void Application::shutdown() {
window_ = nullptr;
}
if (sdlInitialized_) {
SDL_Quit();
sdlInitialized_ = false;
}
#ifdef __SWITCH__
if (switchRomfsMounted_) {
romfsExit();
switchRomfsMounted_ = false;
}
#endif
// 平台相关清理
#ifdef __SWITCH__
switchShutdown();
#endif
// 退出 SDL(添加重复调用保护)
static bool sdlQuitCalled = false;
if (!sdlQuitCalled) {
SDL_Quit();
sdlQuitCalled = true;
if (switchPlatformInitialized_) {
switchShutdown();
switchPlatformInitialized_ = false;
}
#endif
ObjectRegistry::get().clear();
@@ -150,13 +166,22 @@ bool Application::initCoreModules() {
SDL_Log("Asset working directory: %s", workingDir.c_str());
}
#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());
#endif
{
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");
return false;
}
@@ -168,6 +193,7 @@ bool Application::initCoreModules() {
SDL_Log("Failed to initialize SDL: %s", SDL_GetError());
return false;
}
sdlInitialized_ = true;
}
// Disable text input by default. UI widgets should enable it explicitly.
+18 -1
View File
@@ -12,6 +12,23 @@ Asset &Asset::get() {
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 {
#ifdef _WIN32
return fs::u8path(path);
@@ -34,7 +51,7 @@ std::string Asset::resolveFullPath(const std::string &path) const {
}
fs::path p = toPath(path);
if (p.is_absolute()) {
if (p.is_absolute() || isDevicePath(path)) {
return path;
}
+2 -2
View File
@@ -37,7 +37,7 @@ function add_frostbite2d_target()
local libnx = path.join(devkitpro, "libnx")
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_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_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_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
add_packages("libsdl2", {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

+10
View File
@@ -0,0 +1,10 @@
#version 300 es
precision mediump float;
in vec4 v_color;
out vec4 FragColor;
void main() {
FragColor = v_color;
}
+16
View File
@@ -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);
}
+12
View File
@@ -0,0 +1,12 @@
{
"shaders": {
"sprite": {
"vertex": "sprite.vert",
"fragment": "sprite.frag"
},
"colored_quad": {
"vertex": "colored_quad.vert",
"fragment": "colored_quad.frag"
}
}
}
+13
View File
@@ -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;
}
+18
View File
@@ -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);
}
+14
View File
@@ -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` 做一个可玩的美术测试关。
+210
View File
@@ -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 碰撞显示。
+160
View File
@@ -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
+49
View File
@@ -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
+29
View File
@@ -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
+12
View File
@@ -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
+11
View File
@@ -0,0 +1,11 @@
#pragma once
namespace ns_game {
struct DebugFlags {
bool showStageGrid = true;
bool showCollision = true;
bool showActorBounds = true;
};
} // namespace ns_game
+13
View File
@@ -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
+40
View File
@@ -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
+20
View File
@@ -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
+22
View File
@@ -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
+27
View File
@@ -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
+49
View File
@@ -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
+20
View File
@@ -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
+90
View File
@@ -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;
}
+39
View File
@@ -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
+27
View File
@@ -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
+52
View File
@@ -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
+30
View File
@@ -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
+65
View File
@@ -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
+19
View File
@@ -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
+54
View File
@@ -31,4 +31,58 @@ if is_plat("windows") then
add_packages("zlib")
add_syslinks("imm32")
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