Wire engine feature services into game skeleton
This commit is contained in:
@@ -121,6 +121,8 @@ Switch 版依赖不走 XMake 的桌面包,而是使用 devkitPro 的 portlibs
|
||||
- SDL2_ttf
|
||||
- GLM
|
||||
- zlib
|
||||
- HarfBuzz
|
||||
- bzip2
|
||||
- libnx
|
||||
|
||||
### 2.2 编译 Switch 引擎静态库
|
||||
@@ -302,6 +304,7 @@ romfs:/assets
|
||||
- `Frostbite2DGameSwitch` 可成功编译为 ELF/NRO
|
||||
- PC 和 Switch 共用同一套 GLES 300 es shader
|
||||
- Windows 游戏目标仍可正常编译和启动
|
||||
- devkitPro 当前版本的 `SDL2_ttf` / `freetype` 需要静态链接 `harfbuzz` 和 `bz2`,该链接项已写入 `engine.lua`
|
||||
|
||||
## 3. 常见问题
|
||||
|
||||
@@ -355,3 +358,13 @@ romfs:/assets/character/male/*.png
|
||||
|
||||
旧版本曾只把资源复制到 NRO 旁边,运行时引擎找不到 shader 和纹理,
|
||||
会表现为黑屏或没有场景内容。
|
||||
|
||||
### 3.5 Switch 链接时报 `hb_*` 或 `BZ2_*` 未定义
|
||||
|
||||
这是 devkitPro portlibs 静态库依赖未完整链接。当前 `engine.lua` 已把
|
||||
`harfbuzz` 和 `bz2` 加入 Switch 链接列表;如果换机器后再次出现,先确认:
|
||||
|
||||
```powershell
|
||||
Test-Path C:\devkitPro\portlibs\switch\lib\libharfbuzz.a
|
||||
Test-Path C:\devkitPro\portlibs\switch\lib\libbz2.a
|
||||
```
|
||||
|
||||
+1
-1
@@ -46,7 +46,7 @@ function add_frostbite2d_target()
|
||||
add_includedirs(path.join(portlibs, "include/freetype2"), {public = true})
|
||||
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_ttf", "freetype", "harfbuzz", "bz2", "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", "stdc++", {public = true})
|
||||
else
|
||||
add_packages("libsdl2", {public = true})
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# Game Asset Slots
|
||||
|
||||
This file records project-wide asset slots that are consumed by engine-backed
|
||||
systems. Keep final assets in these folders so Windows and Switch builds can
|
||||
copy them into `assets/` and RomFS without code changes.
|
||||
|
||||
## Global Slots
|
||||
|
||||
| Slot | Path | Engine path | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| Debug UI font | `fonts/debug.ttf` | `FontManager` + `TextSprite` | Optional. UI falls back to colored bars if missing. |
|
||||
| UI font | `fonts/ui.ttf` | `FontManager` + `TextSprite` | Optional fallback font name for UI. |
|
||||
| Stage music | `music/stage_01.ogg` | `Music::loadFromFile` | Optional BGM cue id: `stage_01`. |
|
||||
| Player jump SFX | `sounds/player_jump.wav` | `Sound::loadFromFile` | Optional cue id: `player_jump`. |
|
||||
| Player attack SFX | `sounds/player_attack.wav` | `Sound::loadFromFile` | Optional cue id: `player_attack`. |
|
||||
| Battle zone enter SFX | `sounds/battle_zone_enter.wav` | `Sound::loadFromFile` | Optional cue id: `battle_zone_enter`. |
|
||||
| Audio database | `audio/audio.xml` | `AudioDatabase` | Optional cue database; can override direct file lookup. |
|
||||
| PVF scripts | `Script.pvf` | `PvfArchive` / `ScriptParser` | Optional `.map` and `.ani` source. |
|
||||
| Image packs | `ImagePacks2/*.npk` | `NpkArchive` / `Sprite::createFromNpk` | Optional packed sprite source. |
|
||||
| Sound packs | `SoundPacks/*.npk` | `SoundPackArchive` | Optional packed sound source. |
|
||||
|
||||
## Character Animation Slots
|
||||
|
||||
Current temporary PNG spritesheets stay in `character/male/`. If PVF/NPK assets
|
||||
are provided later, the game will try these engine paths first:
|
||||
|
||||
| State | ANI path | NPK IMG path | PNG fallback |
|
||||
| --- | --- | --- | --- |
|
||||
| Idle | `character/male/idle.ani` | `sprite/character/male/idle.img` | `character/male/Male_spritesheet_idle.png` |
|
||||
| Run | `character/male/run.ani` | `sprite/character/male/run.img` | `character/male/Male_spritesheet_run.png` |
|
||||
| Punch | `character/male/punch_1.ani` | `sprite/character/male/punch_1.img` | `character/male/Male_spritesheet_punch_1.png` |
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -12,7 +12,8 @@ This directory is reserved for the first playable side-scrolling ACT stage.
|
||||
| Props sheet | `props/stage_01_props.png` | Crates, signs, lamps, blockers, exits. | TBD |
|
||||
| Hit spark | `effects/hit_spark.png` | Small combat impact effect. | TBD |
|
||||
| Dust | `effects/dust.png` | Run, jump, and landing dust. | TBD |
|
||||
| Music / ambience | `audio/` | Optional loopable stage audio. | TBD |
|
||||
| Music / ambience | `../../music/stage_01.ogg` | Optional loopable stage audio cue. | TBD |
|
||||
| Stage SFX | `../../sounds/` | Optional one-shot SFX cues. | TBD |
|
||||
|
||||
## Import Rules
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
## References
|
||||
|
||||
- `../assets/ASSETS.md`:全局资源槽位,包含字体、音频、PVF、NPK、SoundPack 和角色动画 fallback 路径。
|
||||
- `controls.md`:玩家操作、平台输入映射和开发调试快捷键。
|
||||
- `engine_capability_audit.md`:引擎已有能力复盘,以及游戏层应优先复用的系统。
|
||||
- `game_skeleton.md`:当前游戏骨架如何接入引擎基础能力。
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
|
||||
## 结论
|
||||
|
||||
当前游戏层已经用了引擎的核心框架,但地图和角色动画仍有明显“游戏层自造/硬编码”倾向。
|
||||
当前游戏层已经用了引擎核心框架,并且已把地图、动画、UI、音频、存档和 NPK/SoundPack 资源入口接到真实调用路径。
|
||||
|
||||
最重要的调整是:**场景地图不要继续在 C++ 里手写 `Rect`,下一步应接入 `.map` 文件加载,并优先通过引擎 `Asset` / `PvfArchive` / `ScriptParser` 读取和解析。**
|
||||
最重要的调整已经完成:场景地图不再以 C++ 硬编码为主,`stage_01.map` 通过 `Asset` / `PvfArchive` / `ScriptParser` 读取入口进入 `LevelDefinition`。
|
||||
|
||||
当前第一轮骨架接入已完成:游戏启动时通过 `GameServices` 探测/初始化 `AudioSystem`、`PvfArchive`、`NpkArchive`、`SoundPackArchive`、`AudioDatabase` 和 `SaveSystem` 状态,并通过 `StageMapResource` + `StageMapLoader` 接入 `.map` 资源读取和解析入口。详细状态见 `game_skeleton.md`。
|
||||
当前骨架接入状态:游戏启动时通过 `GameServices` 探测/初始化 `AudioSystem`、`FontManager`、`PvfArchive`、`NpkArchive`、`SoundPackArchive`、`AudioDatabase` 和 `SaveSystem` 状态;`PlayerActor` 已接入 `Animation`/NPK/PNG 优先级;`WhiteboxScene` 已接入 `UIScene`、音频 cue 和 debug 存档。详细状态见 `game_skeleton.md`。
|
||||
|
||||
推荐方向:
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
|
||||
- `StageRenderer` 已复用引擎 Renderer 和 Texture。
|
||||
- `CameraController` 使用引擎 `Camera` 做位置控制。
|
||||
- `GameUiOverlay` 通过 `UIScene` 进入 UI scene stack。
|
||||
- 游戏入口已设置像素风渲染风格。
|
||||
|
||||
需要调整:
|
||||
@@ -79,6 +80,7 @@
|
||||
- `StageMapResource` 已直接使用 `Asset::readFileToString` 读取 `map/stage_01.map` 或 `assets/map/stage_01.map`。
|
||||
- Windows 和 Switch 构建会把 `game/assets/*` 复制到产物 `assets/`,保证 `assets/map/stage_01.map` 路径可用。
|
||||
- `stage_01` 主地图已迁移到外部 `.map` 文件。
|
||||
- `GameAudio`、`GameServices`、`PlayerAnimationAssets` 都通过 `Asset` 检查可选资源。
|
||||
|
||||
下一步:
|
||||
|
||||
@@ -122,19 +124,19 @@
|
||||
|
||||
当前状态:
|
||||
|
||||
- 玩家现在使用 `Sprite::createFromFile` 加载 PNG spritesheet。
|
||||
- 玩家动画帧切换在 `PlayerActor` 内手写。
|
||||
- 还没有使用 `Animation`、`.ani`、`.als`、PVF 动画数据或 NPK 图包。
|
||||
- 玩家动画加载顺序为 PVF `.ani` -> NPK IMG -> PNG fallback。
|
||||
- 如果 PVF 中存在 `character/male/*.ani`,`PlayerActor` 会使用引擎 `Animation`。
|
||||
- 如果 NPK 中存在 `sprite/character/male/*.img`,会使用 `Sprite::createFromNpk`。
|
||||
- 当前仓库没有正式 PVF/NPK 动画资源,因此运行时仍会回退到 PNG spritesheet。
|
||||
|
||||
风险:
|
||||
|
||||
- 如果继续手写动画,后续敌人、玩家连招、受击、死亡都会重复造动画系统。
|
||||
- 只要正式动画资源未进入 PVF/NPK,临时 PNG fallback 仍需要少量手写帧切换。
|
||||
|
||||
下一步建议:
|
||||
|
||||
- 在地图框架后,角色动画应迁移到引擎 `Animation`。
|
||||
- 若素材来自 NPK/PVF 体系,优先用 `Animation + NpkArchive + parseAniFromPvf`。
|
||||
- 若当前临时素材仍是 PNG spritesheet,可以短期保留,但不要扩展成复杂自研动画状态系统。
|
||||
- 为玩家和敌人补齐 PVF `.ani` 或 NPK IMG 后,逐步移除 PNG fallback 的手写帧切换。
|
||||
- 将 attack/damage box 从 `Animation::GetCurrentFrameInfo()` 接入战斗 hitbox。
|
||||
|
||||
### Audio / Save
|
||||
|
||||
@@ -149,12 +151,29 @@
|
||||
|
||||
当前状态:
|
||||
|
||||
- 游戏层尚未使用音频和存档。
|
||||
- `GameAudio` 已接入 `Sound`、`Music` 和 `AudioDatabase` 查询。
|
||||
- `WhiteboxScene` 会尝试播放 `stage_01` BGM 和 battle zone 进入音效。
|
||||
- `PlayerActor` 会尝试播放跳跃、攻击音效。
|
||||
- `GameSave` 已用 `SaveSystem` 保存 debug overlay 开关。
|
||||
|
||||
后续原则:
|
||||
|
||||
- 音效、BGM、环境音直接接引擎音频系统。
|
||||
- 设置、进度、调试存档优先用 `SaveSystem`,不要在游戏层重新写文件存档服务。
|
||||
- 正式音效、BGM、环境音直接放入资源槽位或 `audio.xml`。
|
||||
- 设置、进度继续用 `SaveSystem`,不要在游戏层重新写文件存档服务。
|
||||
|
||||
### UI / Text
|
||||
|
||||
引擎已有:
|
||||
|
||||
- `UIScene`
|
||||
- `TextSprite`
|
||||
- `FontManager`
|
||||
|
||||
当前状态:
|
||||
|
||||
- `GameUiOverlay` 已作为 `UIScene` 接入。
|
||||
- 有 `assets/fonts/debug.ttf` 或 `assets/fonts/ui.ttf` 时,状态 UI 使用 `TextSprite`。
|
||||
- 缺字体时使用 `Renderer::drawQuad` 色条 fallback,避免 Switch/PC 因字体缺失黑屏。
|
||||
|
||||
### Event / Input
|
||||
|
||||
@@ -198,20 +217,20 @@
|
||||
- 继续扩展 `.map` 的 tileset/tile entry 表达。
|
||||
- 逐步把 `level_visual` 的长色块替换成 tile 数据。
|
||||
|
||||
### 2. 手写动画帧切换
|
||||
### 2. PNG fallback 手写动画帧切换
|
||||
|
||||
文件:
|
||||
|
||||
- `game/src/actor/player_actor.cpp`
|
||||
|
||||
问题:
|
||||
当前状态:
|
||||
|
||||
- idle/run/punch 都在 C++ 里硬编码帧数、帧时间和 source rect。
|
||||
- 已接入 `Animation` 和 NPK 优先路径。
|
||||
- 临时 PNG fallback 仍在 C++ 里硬编码帧数、帧时间和 source rect。
|
||||
|
||||
建议:
|
||||
|
||||
- 先记录为短期可接受。
|
||||
- 后续迁移到引擎 `Animation`,尤其当角色素材进入 PVF/NPK/ANI 体系后。
|
||||
- 等正式 `.ani` / `.img` 资源补齐后,删除 PNG fallback 的手写帧切换。
|
||||
|
||||
### 3. 游戏层贴图缓存
|
||||
|
||||
|
||||
@@ -28,13 +28,26 @@
|
||||
游戏层新增 `GameServices`,只做游戏项目需要的引擎服务接入和状态记录:
|
||||
|
||||
- 初始化 `AudioSystem`。
|
||||
- 初始化 `FontManager`,有 `assets/fonts/debug.ttf` 或 `assets/fonts/ui.ttf` 时注册 debug 字体。
|
||||
- 探测并打开可选 `Script.pvf`。
|
||||
- 初始化 `NpkArchive`。
|
||||
- 初始化 `SoundPackArchive`。
|
||||
- 探测并加载可选 `AudioDatabase`。
|
||||
- 记录 `SaveSystem` 是否已由引擎初始化。
|
||||
|
||||
这些服务是可选能力。缺少 `Script.pvf`、`ImagePacks2`、`SoundPacks` 或 `audio.xml` 时,游戏仍然使用当前 PNG/白盒 fallback 启动。
|
||||
这些服务是可选能力。缺少 `Script.pvf`、`ImagePacks2`、`SoundPacks`、字体、音频或 `audio.xml` 时,游戏仍然使用当前 PNG/白盒 fallback 启动。
|
||||
|
||||
## 引擎能力接入点
|
||||
|
||||
当前游戏骨架已把以下引擎能力接入到真实调用路径:
|
||||
|
||||
- `Animation`:`PlayerActor` 会优先尝试 PVF `.ani`,失败后回退 NPK IMG,再回退 PNG spritesheet。
|
||||
- `NpkArchive` / `Sprite::createFromNpk`:角色资源和后续场景素材可从 `assets/ImagePacks2/*.npk` 进入。
|
||||
- `TextSprite` / `UIScene`:`GameUiOverlay` 已接入 UI scene stack;有字体时显示文字状态,没有字体时用色条 fallback。
|
||||
- `Sound` / `Music` / `AudioDatabase`:`GameAudio` 已支持 cue id,优先使用 `audio.xml`,否则读取 `assets/sounds/*.wav` 和 `assets/music/*.ogg`。
|
||||
- `SaveSystem`:`GameSave` 已保存/读取 debug overlay 开关状态。
|
||||
|
||||
资源槽位见 `../assets/ASSETS.md`。
|
||||
|
||||
## 地图资源入口
|
||||
|
||||
@@ -80,9 +93,9 @@ endbattle_zone
|
||||
|
||||
## 仍待迁移的能力
|
||||
|
||||
- 玩家动画:从手写 spritesheet 帧切换迁移到引擎 `Animation` / PVF ANI。
|
||||
- 场景视觉资源:如果素材进入 NPK,迁移到 `NpkArchive` / `Sprite::createFromNpk`。
|
||||
- 音频:使用 `AudioSystem`、`Sound`、`Music`、`AudioDatabase`。
|
||||
- 玩家动画:当前已接入 `Animation` 优先路径,但临时 PNG fallback 仍保留手写帧切换,等待正式 PVF/NPK 动画资源。
|
||||
- 场景视觉资源:`StageRenderer` 支持普通 PNG;若素材进入 NPK,后续补 stage layer 的 NPK sprite path。
|
||||
- 音频:调用路径已接入,等待正式音频资源和 `audio.xml`。
|
||||
- 地图:后续可把 `.map` 扩展为工具导出格式或 PVF 脚本格式,当前 C++ 地图只作为 fallback。
|
||||
- 输入配置:通过 `Asset` 或 PVF 读取 `InputConfig`。
|
||||
|
||||
|
||||
@@ -273,15 +273,24 @@ game/assets/stage/stage_01/
|
||||
|
||||
第一轮引擎能力接入已完成:`GameServices` 统一接入音频、PVF、NPK、SoundPack、AudioDB、SaveSystem 状态;`StageMapResource` 负责从 Asset/PVF 读取 `map/stage_01.map`,`StageMapLoader` 负责转成游戏层 `LevelDefinition`。完整骨架说明见 `../game_skeleton.md`。
|
||||
|
||||
第二轮引擎能力接入已完成:
|
||||
|
||||
- `Animation` 已接入玩家资源加载链路,优先读取 PVF 内 `.ani`,不可用时回退到 NPK IMG,再回退到 PNG 精灵表。
|
||||
- `NpkArchive` / `Sprite::createFromNpk` 已接入角色动画资源兜底链路,后续把角色 IMG 放入 `ImagePacks2` 后无需改玩家主流程。
|
||||
- `TextSprite` / `UIScene` 已接入局内 UI overlay;有 debug 字体时显示文本状态,没有字体时回退成低依赖状态条。
|
||||
- `Sound` / `Music` 已通过 `GameAudio` 接入 cue 播放;优先读取 `AudioDatabase`,缺少音频表时从 `assets/sounds` 和 `assets/music` 直读占位资源。
|
||||
- `SaveSystem` 已用于保存/读取场景 debug 开关,避免每次启动后重复配置调试显示。
|
||||
- 全局素材槽位已建立在 `game/assets/ASSETS.md`,字体、音乐、音效、PVF、NPK、SoundPack、角色 ANI/IMG/PNG 的落位规则已经记录。
|
||||
|
||||
### 下一轮建议任务
|
||||
|
||||
继续沿着“优先使用引擎能力”的方向,把地图从色块灰盒推进到可放素材的完整骨架:
|
||||
继续沿着“优先使用引擎能力”的方向,把地图从色块灰盒推进到可放素材的完整骨架。当前不进入敌人和战斗玩法,下一轮只处理地图视觉表达:
|
||||
|
||||
1. 给 `.map` 增加 tileset/tile entry 约定,先不做编辑器,只保证工具或手写数据能表达重复地砖。
|
||||
2. 在 `StageRenderer` 中增加 tile 绘制路径,复用当前 `Texture` 加载和 source rect 支持。
|
||||
3. 把 `level_visual` 的长色块逐步替换成 tile entries,碰撞仍独立保留在 `collision`。
|
||||
4. 给 map loader 增加更严格的错误日志,例如重复 block、空 layer、battle zone 无 spawn 的提示。
|
||||
5. 补一个轻量 map 验收流程:启动时日志确认加载 `.map`,RomFS/Windows 产物确认存在 `assets/map/stage_01.map`。
|
||||
4. 给 map loader 增加更严格的错误日志,例如重复 block、空 layer、battle zone 无 spawn、tile 引用不存在的提示。
|
||||
5. 补一个轻量 map 验收流程:启动时日志确认加载 `.map`,Windows/Switch 产物确认存在 `assets/map/stage_01.map` 和全局素材槽位。
|
||||
|
||||
### Debug 快捷键
|
||||
|
||||
|
||||
@@ -14,6 +14,9 @@
|
||||
- Battle zones:`zone_01_intro`、`zone_02_platform`、`zone_03_exit`
|
||||
- 资源槽位:`game/assets/stage/stage_01/`
|
||||
- 地图资源:`game/assets/map/stage_01.map`
|
||||
- 全局资源槽位:`game/assets/ASSETS.md`
|
||||
- UI overlay:`GameUiOverlay`,有字体时使用 `TextSprite`,无字体时使用 fallback 色条。
|
||||
- 音频 cue:`stage_01`、`player_jump`、`player_attack`、`battle_zone_enter`。
|
||||
|
||||
## 构建检查
|
||||
|
||||
@@ -37,6 +40,8 @@ if (!$p.HasExited) { Stop-Process -Id $p.Id -Force }
|
||||
|
||||
```powershell
|
||||
Test-Path build\windows\x64\release\assets\map\stage_01.map
|
||||
Test-Path build\windows\x64\release\assets\ASSETS.md
|
||||
Test-Path build\windows\x64\release\assets\fonts
|
||||
```
|
||||
|
||||
### Switch
|
||||
@@ -56,6 +61,8 @@ RomFS 地图检查:
|
||||
|
||||
```powershell
|
||||
Test-Path build\switch\aarch64\release\switch_game\romfs\assets\map\stage_01.map
|
||||
Test-Path build\switch\aarch64\release\switch_game\romfs\assets\ASSETS.md
|
||||
Test-Path build\switch\aarch64\release\switch_game\romfs\assets\fonts
|
||||
```
|
||||
|
||||
## 手动跑图检查
|
||||
@@ -72,6 +79,8 @@ Test-Path build\switch\aarch64\release\switch_game\romfs\assets\map\stage_01.map
|
||||
- battle zone 触发框和 camera bounds。
|
||||
- 敌人出生点。
|
||||
6. 从起点移动到终点附近,确认所有平台、props、前景遮挡和背景层没有闪烁或黑屏。
|
||||
7. 确认 UI overlay 可见;若没有字体资源,应显示左上角色条 fallback。
|
||||
8. 切换 F1/F3/F4/F5/F6 后重启,确认 debug 开关能通过 `SaveSystem` 记住。
|
||||
|
||||
## 当前验收结果
|
||||
|
||||
@@ -82,6 +91,7 @@ Test-Path build\switch\aarch64\release\switch_game\romfs\assets\map\stage_01.map
|
||||
- Switch `Frostbite2DGameSwitch` 构建:通过。
|
||||
- Switch RomFS:已包含 character、shader、`stage_01` 资源槽位和 `assets/map/stage_01.map`。
|
||||
- 地图加载:`stage_01.map` 已作为第一关主数据源;C++ 白盒数据仅作为 fallback。
|
||||
- 引擎能力接入:`Animation`/NPK/PNG 角色资源优先级、`UIScene`/`TextSprite` UI、`Sound`/`Music` cue、`SaveSystem` debug 存档已接入。
|
||||
- 手动跑图:未在本轮执行,等待后续素材/战斗区域进一步接入时复测。
|
||||
|
||||
## 后续补充
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "player_actor.h"
|
||||
|
||||
#include "../core/asset_paths.h"
|
||||
#include "../core/game_audio.h"
|
||||
#include "../core/game_config.h"
|
||||
|
||||
#include <algorithm>
|
||||
@@ -8,13 +9,6 @@
|
||||
|
||||
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);
|
||||
@@ -26,29 +20,29 @@ void PlayerActor::SetMovementWorld(const PlatformWorld* world) {
|
||||
}
|
||||
|
||||
void PlayerActor::loadSprites() {
|
||||
idleSprite_ = frostbite2D::Sprite::createFromFile(assets::kMaleIdle);
|
||||
runSprite_ = frostbite2D::Sprite::createFromFile(assets::kMaleRun);
|
||||
punchSprite_ = frostbite2D::Sprite::createFromFile(assets::kMalePunch);
|
||||
idleAsset_ = LoadPlayerAnimationAsset(assets::kMaleIdleAni,
|
||||
assets::kMaleIdleImg,
|
||||
assets::kMaleIdle, 0);
|
||||
runAsset_ = LoadPlayerAnimationAsset(assets::kMaleRunAni, assets::kMaleRunImg,
|
||||
assets::kMaleRun, 0);
|
||||
punchAsset_ = LoadPlayerAnimationAsset(assets::kMalePunchAni,
|
||||
assets::kMalePunchImg,
|
||||
assets::kMalePunch, 0);
|
||||
|
||||
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_);
|
||||
addAnimationAsset(idleAsset_);
|
||||
addAnimationAsset(runAsset_);
|
||||
addAnimationAsset(punchAsset_);
|
||||
setAnimState(AnimState::Idle);
|
||||
}
|
||||
|
||||
void PlayerActor::addAnimationAsset(PlayerAnimationAsset& asset) {
|
||||
if (asset.animation) {
|
||||
AddChild(asset.animation);
|
||||
} else if (asset.sprite) {
|
||||
AddChild(asset.sprite);
|
||||
}
|
||||
}
|
||||
|
||||
void PlayerActor::OnUpdate(float deltaTime) {
|
||||
deltaTime = std::min(deltaTime, 1.0f / 30.0f);
|
||||
updateInput();
|
||||
@@ -68,11 +62,13 @@ void PlayerActor::updateInput() {
|
||||
if (input.jumpPressed && grounded_) {
|
||||
velocity_.y = config::kPlayerJumpSpeed;
|
||||
grounded_ = false;
|
||||
GameAudio::Get().PlaySoundCue("player_jump");
|
||||
}
|
||||
|
||||
if (input.attackPressed) {
|
||||
attackTimer_ = config::kAttackDuration;
|
||||
setAnimState(AnimState::Punch);
|
||||
GameAudio::Get().PlaySoundCue("player_attack");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,43 +113,57 @@ void PlayerActor::updateAnimation(float deltaTime) {
|
||||
frameIndex_ = (frameIndex_ + 1) % frameCount;
|
||||
}
|
||||
|
||||
auto sprite = activeSprite();
|
||||
if (sprite) {
|
||||
sprite->SetSourceRect(
|
||||
PlayerAnimationAsset* asset = activeAsset();
|
||||
if (!asset) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (asset->animation) {
|
||||
asset->animation->SetDirection(facingLeft_ ? -1 : 1);
|
||||
}
|
||||
if (asset->sprite) {
|
||||
asset->sprite->SetSourceRect(
|
||||
frostbite2D::Rect(frameIndex_ * config::kPlayerFrameSize, 0.0f,
|
||||
config::kPlayerFrameSize,
|
||||
config::kPlayerFrameSize));
|
||||
sprite->SetFlippedX(facingLeft_);
|
||||
asset->sprite->SetFlippedX(facingLeft_);
|
||||
}
|
||||
}
|
||||
|
||||
void PlayerActor::setAnimState(AnimState state) {
|
||||
if (animState_ != state) {
|
||||
const bool stateChanged = animState_ != state;
|
||||
if (stateChanged) {
|
||||
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);
|
||||
}
|
||||
auto setVisible = [stateChanged](PlayerAnimationAsset& asset, bool visible) {
|
||||
if (asset.animation) {
|
||||
asset.animation->SetVisible(visible);
|
||||
if (visible && stateChanged) {
|
||||
asset.animation->Reset();
|
||||
}
|
||||
}
|
||||
if (asset.sprite) {
|
||||
asset.sprite->SetVisible(visible);
|
||||
}
|
||||
};
|
||||
|
||||
setVisible(idleAsset_, state == AnimState::Idle);
|
||||
setVisible(runAsset_, state == AnimState::Run);
|
||||
setVisible(punchAsset_, state == AnimState::Punch);
|
||||
}
|
||||
|
||||
frostbite2D::Ptr<frostbite2D::Sprite> PlayerActor::activeSprite() const {
|
||||
PlayerAnimationAsset* PlayerActor::activeAsset() {
|
||||
switch (animState_) {
|
||||
case AnimState::Run:
|
||||
return runSprite_;
|
||||
return &runAsset_;
|
||||
case AnimState::Punch:
|
||||
return punchSprite_;
|
||||
return &punchAsset_;
|
||||
case AnimState::Idle:
|
||||
default:
|
||||
return idleSprite_;
|
||||
return &idleAsset_;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
#include "../core/input_state.h"
|
||||
#include "../movement/platform_world.h"
|
||||
#include "player_animation_assets.h"
|
||||
|
||||
#include <frostbite2D/2d/actor.h>
|
||||
#include <frostbite2D/2d/sprite.h>
|
||||
#include <frostbite2D/types/type_math.h>
|
||||
|
||||
namespace ns_game {
|
||||
@@ -24,18 +24,19 @@ private:
|
||||
};
|
||||
|
||||
void loadSprites();
|
||||
void addAnimationAsset(PlayerAnimationAsset& asset);
|
||||
void updateInput();
|
||||
void updatePhysics(float deltaTime);
|
||||
void updateAnimation(float deltaTime);
|
||||
void setAnimState(AnimState state);
|
||||
frostbite2D::Ptr<frostbite2D::Sprite> activeSprite() const;
|
||||
PlayerAnimationAsset* activeAsset();
|
||||
|
||||
const PlatformWorld* movementWorld_ = nullptr;
|
||||
InputStateProvider inputProvider_;
|
||||
PlatformMover platformMover_;
|
||||
frostbite2D::Ptr<frostbite2D::Sprite> idleSprite_;
|
||||
frostbite2D::Ptr<frostbite2D::Sprite> runSprite_;
|
||||
frostbite2D::Ptr<frostbite2D::Sprite> punchSprite_;
|
||||
PlayerAnimationAsset idleAsset_;
|
||||
PlayerAnimationAsset runAsset_;
|
||||
PlayerAnimationAsset punchAsset_;
|
||||
|
||||
frostbite2D::Vec2 velocity_;
|
||||
AnimState animState_ = AnimState::Idle;
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
#include "player_animation_assets.h"
|
||||
|
||||
#include <frostbite2D/resource/asset.h>
|
||||
#include <frostbite2D/resource/npk_archive.h>
|
||||
#include <frostbite2D/resource/pvf_archive.h>
|
||||
|
||||
#include <SDL2/SDL.h>
|
||||
|
||||
namespace ns_game {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr float kSpriteOffsetX = -32.0f;
|
||||
constexpr float kSpriteOffsetY = -24.0f;
|
||||
|
||||
} // namespace
|
||||
|
||||
PlayerAnimationAsset LoadPlayerAnimationAsset(const std::string& aniPath,
|
||||
const std::string& npkImgPath,
|
||||
const std::string& pngPath,
|
||||
int zOrder) {
|
||||
PlayerAnimationAsset asset;
|
||||
|
||||
frostbite2D::PvfArchive& pvf = frostbite2D::PvfArchive::get();
|
||||
if (pvf.isOpen() && pvf.hasFile(aniPath)) {
|
||||
asset.animation = frostbite2D::MakePtr<frostbite2D::Animation>(aniPath);
|
||||
if (asset.animation && asset.animation->IsUsable()) {
|
||||
asset.animation->SetTopLeftPosition(kSpriteOffsetX, kSpriteOffsetY);
|
||||
asset.animation->SetVisible(false);
|
||||
asset.animation->SetZOrder(zOrder);
|
||||
return asset;
|
||||
}
|
||||
asset.animation = nullptr;
|
||||
}
|
||||
|
||||
frostbite2D::NpkArchive& npk = frostbite2D::NpkArchive::get();
|
||||
if (npk.isOpen() && npk.hasImg(npkImgPath)) {
|
||||
asset.sprite = frostbite2D::Sprite::createFromNpk(npkImgPath, 0);
|
||||
}
|
||||
|
||||
if (!asset.sprite && frostbite2D::Asset::get().exists(pngPath)) {
|
||||
asset.sprite = frostbite2D::Sprite::createFromFile(pngPath);
|
||||
}
|
||||
|
||||
ConfigurePlayerSpriteFallback(asset.sprite);
|
||||
if (asset.sprite) {
|
||||
asset.sprite->SetVisible(false);
|
||||
asset.sprite->SetZOrder(zOrder);
|
||||
}
|
||||
return asset;
|
||||
}
|
||||
|
||||
void ConfigurePlayerSpriteFallback(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);
|
||||
}
|
||||
|
||||
} // namespace ns_game
|
||||
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include "../core/game_config.h"
|
||||
|
||||
#include <frostbite2D/2d/sprite.h>
|
||||
#include <frostbite2D/animation/animation.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace ns_game {
|
||||
|
||||
struct PlayerAnimationAsset {
|
||||
frostbite2D::Ptr<frostbite2D::Animation> animation;
|
||||
frostbite2D::Ptr<frostbite2D::Sprite> sprite;
|
||||
bool usesEngineAnimation() const { return animation != nullptr; }
|
||||
};
|
||||
|
||||
PlayerAnimationAsset LoadPlayerAnimationAsset(const std::string& aniPath,
|
||||
const std::string& npkImgPath,
|
||||
const std::string& pngPath,
|
||||
int zOrder);
|
||||
void ConfigurePlayerSpriteFallback(frostbite2D::Ptr<frostbite2D::Sprite> sprite);
|
||||
|
||||
} // namespace ns_game
|
||||
@@ -9,4 +9,12 @@ constexpr const char* kMaleRun =
|
||||
constexpr const char* kMalePunch =
|
||||
"assets/character/male/Male_spritesheet_punch_1.png";
|
||||
|
||||
constexpr const char* kMaleIdleAni = "character/male/idle.ani";
|
||||
constexpr const char* kMaleRunAni = "character/male/run.ani";
|
||||
constexpr const char* kMalePunchAni = "character/male/punch_1.ani";
|
||||
|
||||
constexpr const char* kMaleIdleImg = "sprite/character/male/idle.img";
|
||||
constexpr const char* kMaleRunImg = "sprite/character/male/run.img";
|
||||
constexpr const char* kMalePunchImg = "sprite/character/male/punch_1.img";
|
||||
|
||||
} // namespace ns_game::assets
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
#include "game_audio.h"
|
||||
|
||||
#include "game_services.h"
|
||||
|
||||
#include <frostbite2D/audio/audio_system.h>
|
||||
#include <frostbite2D/resource/asset.h>
|
||||
#include <frostbite2D/resource/audio_database.h>
|
||||
|
||||
#include <SDL2/SDL.h>
|
||||
|
||||
namespace ns_game {
|
||||
|
||||
GameAudio& GameAudio::Get() {
|
||||
static GameAudio audio;
|
||||
return audio;
|
||||
}
|
||||
|
||||
void GameAudio::PlaySoundCue(const std::string& cueId) {
|
||||
if (!frostbite2D::AudioSystem::get().isInitialized()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string path = ResolveAudioPath(cueId);
|
||||
if (path.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto cached = soundCache_.find(path);
|
||||
if (cached == soundCache_.end()) {
|
||||
frostbite2D::Ptr<frostbite2D::Sound> sound = LoadSound(path);
|
||||
if (!sound) {
|
||||
return;
|
||||
}
|
||||
cached = soundCache_.emplace(path, sound).first;
|
||||
}
|
||||
|
||||
cached->second->play();
|
||||
}
|
||||
|
||||
void GameAudio::PlayMusicCue(const std::string& cueId) {
|
||||
if (!frostbite2D::AudioSystem::get().isInitialized()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string path = ResolveAudioPath(cueId);
|
||||
if (path.empty() || frostbite2D::Music::isPathPlaying(path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
frostbite2D::Ptr<frostbite2D::Music> music = LoadMusic(path);
|
||||
if (!music) {
|
||||
return;
|
||||
}
|
||||
|
||||
currentMusic_ = music;
|
||||
currentMusic_->fadeIn(250);
|
||||
}
|
||||
|
||||
frostbite2D::Ptr<frostbite2D::Sound>
|
||||
GameAudio::LoadSound(const std::string& path) const {
|
||||
frostbite2D::Asset& asset = frostbite2D::Asset::get();
|
||||
if (path.rfind("assets/", 0) == 0 && asset.exists(path)) {
|
||||
return frostbite2D::Sound::loadFromFile(path);
|
||||
}
|
||||
return frostbite2D::Sound::loadFromPath(path);
|
||||
}
|
||||
|
||||
frostbite2D::Ptr<frostbite2D::Music>
|
||||
GameAudio::LoadMusic(const std::string& path) const {
|
||||
frostbite2D::Asset& asset = frostbite2D::Asset::get();
|
||||
if (path.rfind("assets/", 0) == 0 && asset.exists(path)) {
|
||||
return frostbite2D::Music::loadFromFile(path);
|
||||
}
|
||||
return frostbite2D::Music::loadFromPath(path);
|
||||
}
|
||||
|
||||
std::string GameAudio::ResolveAudioPath(const std::string& cueId) const {
|
||||
frostbite2D::AudioDatabase& database = frostbite2D::AudioDatabase::get();
|
||||
if (database.isLoaded()) {
|
||||
if (auto path = database.getFilePath(cueId); path && !path->empty()) {
|
||||
return *path;
|
||||
}
|
||||
}
|
||||
|
||||
frostbite2D::Asset& asset = frostbite2D::Asset::get();
|
||||
const std::string soundPath = "assets/sounds/" + cueId + ".wav";
|
||||
if (asset.exists(soundPath)) {
|
||||
return soundPath;
|
||||
}
|
||||
|
||||
const std::string musicPath = "assets/music/" + cueId + ".ogg";
|
||||
if (asset.exists(musicPath)) {
|
||||
return musicPath;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace ns_game
|
||||
@@ -0,0 +1,30 @@
|
||||
#pragma once
|
||||
|
||||
#include <frostbite2D/audio/music.h>
|
||||
#include <frostbite2D/audio/sound.h>
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace ns_game {
|
||||
|
||||
class GameAudio {
|
||||
public:
|
||||
static GameAudio& Get();
|
||||
|
||||
void PlaySoundCue(const std::string& cueId);
|
||||
void PlayMusicCue(const std::string& cueId);
|
||||
|
||||
private:
|
||||
GameAudio() = default;
|
||||
|
||||
frostbite2D::Ptr<frostbite2D::Sound> LoadSound(const std::string& path) const;
|
||||
frostbite2D::Ptr<frostbite2D::Music> LoadMusic(const std::string& path) const;
|
||||
std::string ResolveAudioPath(const std::string& cueId) const;
|
||||
|
||||
std::unordered_map<std::string, frostbite2D::Ptr<frostbite2D::Sound>>
|
||||
soundCache_;
|
||||
frostbite2D::Ptr<frostbite2D::Music> currentMusic_;
|
||||
};
|
||||
|
||||
} // namespace ns_game
|
||||
@@ -0,0 +1,70 @@
|
||||
#include "game_save.h"
|
||||
|
||||
#include <frostbite2D/resource/save_system.h>
|
||||
|
||||
#include <SDL2/SDL.h>
|
||||
#include <json/json.hpp>
|
||||
|
||||
namespace ns_game {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int kDebugSlot = 0;
|
||||
constexpr int kSchemaVersion = 1;
|
||||
|
||||
nlohmann::json toJson(const DebugFlags& flags) {
|
||||
return {
|
||||
{"schema", kSchemaVersion},
|
||||
{"debug", {
|
||||
{"enabled", flags.enabled},
|
||||
{"showCollision", flags.showCollision},
|
||||
{"showStageGrid", flags.showStageGrid},
|
||||
{"showActorBounds", flags.showActorBounds},
|
||||
{"showBattleZones", flags.showBattleZones},
|
||||
{"showSpawnPoints", flags.showSpawnPoints},
|
||||
}},
|
||||
};
|
||||
}
|
||||
|
||||
DebugFlags fromJson(const nlohmann::json& json) {
|
||||
DebugFlags flags;
|
||||
const nlohmann::json debug = json.value("debug", nlohmann::json::object());
|
||||
flags.enabled = debug.value("enabled", flags.enabled);
|
||||
flags.showCollision = debug.value("showCollision", flags.showCollision);
|
||||
flags.showStageGrid = debug.value("showStageGrid", flags.showStageGrid);
|
||||
flags.showActorBounds = debug.value("showActorBounds", flags.showActorBounds);
|
||||
flags.showBattleZones =
|
||||
debug.value("showBattleZones", flags.showBattleZones);
|
||||
flags.showSpawnPoints =
|
||||
debug.value("showSpawnPoints", flags.showSpawnPoints);
|
||||
return flags;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
GameSave& GameSave::Get() {
|
||||
static GameSave save;
|
||||
return save;
|
||||
}
|
||||
|
||||
void GameSave::SaveDebugFlags(const DebugFlags& flags) {
|
||||
frostbite2D::SaveMeta meta;
|
||||
meta.displayName = "Debug Flags";
|
||||
meta.userTag = "debug";
|
||||
if (!frostbite2D::SaveSystem::get().saveSlot(kDebugSlot, toJson(flags),
|
||||
meta)) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"GameSave: failed to save debug flags");
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<DebugFlags> GameSave::LoadDebugFlags() const {
|
||||
const std::optional<nlohmann::json> payload =
|
||||
frostbite2D::SaveSystem::get().loadSlotPayload(kDebugSlot);
|
||||
if (!payload || !payload->is_object()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return fromJson(*payload);
|
||||
}
|
||||
|
||||
} // namespace ns_game
|
||||
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include "../core/debug_flags.h"
|
||||
|
||||
#include <optional>
|
||||
|
||||
namespace ns_game {
|
||||
|
||||
class GameSave {
|
||||
public:
|
||||
static GameSave& Get();
|
||||
|
||||
void SaveDebugFlags(const DebugFlags& flags);
|
||||
std::optional<DebugFlags> LoadDebugFlags() const;
|
||||
|
||||
private:
|
||||
GameSave() = default;
|
||||
};
|
||||
|
||||
} // namespace ns_game
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "game_services.h"
|
||||
|
||||
#include <frostbite2D/audio/audio_system.h>
|
||||
#include <frostbite2D/graphics/font_manager.h>
|
||||
#include <frostbite2D/resource/asset.h>
|
||||
#include <frostbite2D/resource/audio_database.h>
|
||||
#include <frostbite2D/resource/npk_archive.h>
|
||||
@@ -42,14 +43,17 @@ void GameServices::Initialize() {
|
||||
|
||||
status_.saveInitialized = frostbite2D::SaveSystem::get().listSlots().size() > 0;
|
||||
initializeAudio();
|
||||
initializeFonts();
|
||||
initializePvfArchive();
|
||||
initializeNpkArchive();
|
||||
initializeSoundPackArchive();
|
||||
initializeAudioDatabase();
|
||||
|
||||
SDL_Log("GameServices: audio=%d save=%d pvf=%d npk=%d soundpack=%d audiodb=%d",
|
||||
SDL_Log("GameServices: audio=%d save=%d font=%d debugfont=%d pvf=%d npk=%d soundpack=%d audiodb=%d",
|
||||
status_.audioInitialized ? 1 : 0,
|
||||
status_.saveInitialized ? 1 : 0,
|
||||
status_.fontInitialized ? 1 : 0,
|
||||
status_.debugFontLoaded ? 1 : 0,
|
||||
status_.pvfArchiveOpen ? 1 : 0,
|
||||
status_.npkArchiveOpen ? 1 : 0,
|
||||
status_.soundPackOpen ? 1 : 0,
|
||||
@@ -63,6 +67,24 @@ void GameServices::initializeAudio() {
|
||||
status_.audioInitialized = frostbite2D::AudioSystem::get().init(config);
|
||||
}
|
||||
|
||||
void GameServices::initializeFonts() {
|
||||
frostbite2D::FontManager& fontManager = frostbite2D::FontManager::get();
|
||||
status_.fontInitialized = fontManager.init();
|
||||
if (!status_.fontInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
const char* debugFontPath =
|
||||
firstExistingPath("assets/fonts/debug.ttf", "assets/fonts/ui.ttf");
|
||||
if (!debugFontPath) {
|
||||
SDL_Log("GameServices: optional debug font not found");
|
||||
return;
|
||||
}
|
||||
|
||||
status_.debugFontLoaded =
|
||||
fontManager.registerFont("debug", debugFontPath, 18);
|
||||
}
|
||||
|
||||
void GameServices::initializePvfArchive() {
|
||||
const char* pvfPath = firstExistingPath("Script.pvf", "assets/Script.pvf");
|
||||
if (!pvfPath) {
|
||||
|
||||
@@ -5,6 +5,8 @@ namespace ns_game {
|
||||
struct GameServiceStatus {
|
||||
bool audioInitialized = false;
|
||||
bool saveInitialized = false;
|
||||
bool fontInitialized = false;
|
||||
bool debugFontLoaded = false;
|
||||
bool pvfArchiveOpen = false;
|
||||
bool npkArchiveOpen = false;
|
||||
bool soundPackOpen = false;
|
||||
@@ -25,6 +27,7 @@ private:
|
||||
GameServices() = default;
|
||||
|
||||
void initializeAudio();
|
||||
void initializeFonts();
|
||||
void initializePvfArchive();
|
||||
void initializeNpkArchive();
|
||||
void initializeSoundPackArchive();
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
#include "whitebox_scene.h"
|
||||
|
||||
#include "../core/game_audio.h"
|
||||
#include "../core/game_config.h"
|
||||
#include "../core/game_save.h"
|
||||
#include "../core/game_services.h"
|
||||
|
||||
#include <frostbite2D/core/application.h>
|
||||
#include <frostbite2D/event/key_event.h>
|
||||
#include <frostbite2D/graphics/camera.h>
|
||||
#include <frostbite2D/graphics/renderer.h>
|
||||
#include <frostbite2D/scene/scene_manager.h>
|
||||
#include <algorithm>
|
||||
|
||||
namespace ns_game {
|
||||
@@ -13,6 +17,9 @@ namespace ns_game {
|
||||
void WhiteboxScene::onEnter() {
|
||||
frostbite2D::Scene::onEnter();
|
||||
level_ = CreateWhiteboxLevel();
|
||||
if (auto savedDebugFlags = GameSave::Get().LoadDebugFlags()) {
|
||||
debugFlags_ = *savedDebugFlags;
|
||||
}
|
||||
battleZoneStates_.clear();
|
||||
battleZoneStates_.resize(level_.battleZones.size());
|
||||
activeBattleZoneIndex_ = -1;
|
||||
@@ -22,6 +29,11 @@ void WhiteboxScene::onEnter() {
|
||||
player_->SetMovementWorld(&level_.collision);
|
||||
AddChild(player_);
|
||||
configureCamera();
|
||||
|
||||
uiOverlay_ = frostbite2D::MakePtr<GameUiOverlay>();
|
||||
frostbite2D::SceneManager::get().ReplaceUIScene(uiOverlay_);
|
||||
updateUiOverlay();
|
||||
GameAudio::Get().PlayMusicCue("stage_01");
|
||||
}
|
||||
|
||||
void WhiteboxScene::Update(float deltaTime) {
|
||||
@@ -29,6 +41,7 @@ void WhiteboxScene::Update(float deltaTime) {
|
||||
updateBattleZones();
|
||||
updateCameraBoundsBlend(deltaTime);
|
||||
updateCamera(deltaTime);
|
||||
updateUiOverlay();
|
||||
}
|
||||
|
||||
bool WhiteboxScene::OnEvent(const frostbite2D::Event& event) {
|
||||
@@ -107,15 +120,19 @@ bool WhiteboxScene::handleDebugKey(frostbite2D::KeyCode keyCode) {
|
||||
case frostbite2D::KeyCode::F3:
|
||||
debugFlags_.showCollision = !debugFlags_.showCollision;
|
||||
debugFlags_.showStageGrid = debugFlags_.showCollision;
|
||||
persistDebugFlags();
|
||||
return true;
|
||||
case frostbite2D::KeyCode::F4:
|
||||
debugFlags_.showActorBounds = !debugFlags_.showActorBounds;
|
||||
persistDebugFlags();
|
||||
return true;
|
||||
case frostbite2D::KeyCode::F5:
|
||||
debugFlags_.showBattleZones = !debugFlags_.showBattleZones;
|
||||
persistDebugFlags();
|
||||
return true;
|
||||
case frostbite2D::KeyCode::F6:
|
||||
debugFlags_.showSpawnPoints = !debugFlags_.showSpawnPoints;
|
||||
persistDebugFlags();
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
@@ -124,6 +141,7 @@ bool WhiteboxScene::handleDebugKey(frostbite2D::KeyCode keyCode) {
|
||||
|
||||
void WhiteboxScene::toggleDebugOverlay() {
|
||||
debugFlags_.enabled = !debugFlags_.enabled;
|
||||
persistDebugFlags();
|
||||
}
|
||||
|
||||
void WhiteboxScene::beginCameraBoundsBlend(
|
||||
@@ -197,6 +215,7 @@ void WhiteboxScene::updateBattleZones() {
|
||||
runtime.triggered = true;
|
||||
activeBattleZoneIndex_ = static_cast<int>(i);
|
||||
beginCameraBoundsBlend(level_.battleZones[i].cameraBounds);
|
||||
GameAudio::Get().PlaySoundCue("battle_zone_enter");
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -214,4 +233,20 @@ void WhiteboxScene::updateCamera(float deltaTime) {
|
||||
cameraController_.Update(*camera, player_->GetWorldBounds(), deltaTime);
|
||||
}
|
||||
|
||||
void WhiteboxScene::updateUiOverlay() {
|
||||
if (!uiOverlay_) {
|
||||
return;
|
||||
}
|
||||
|
||||
GameUiOverlayState state;
|
||||
state.debugFlags = debugFlags_;
|
||||
state.services = GameServices::Get().Status();
|
||||
state.activeBattleZoneIndex = activeBattleZoneIndex_;
|
||||
uiOverlay_->SetState(state);
|
||||
}
|
||||
|
||||
void WhiteboxScene::persistDebugFlags() {
|
||||
GameSave::Get().SaveDebugFlags(debugFlags_);
|
||||
}
|
||||
|
||||
} // namespace ns_game
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "../core/debug_flags.h"
|
||||
#include "../data/level_definition.h"
|
||||
#include "../debug/debug_overlay.h"
|
||||
#include "../ui/game_ui_overlay.h"
|
||||
#include "camera_controller.h"
|
||||
#include "../stage/stage_renderer.h"
|
||||
|
||||
@@ -37,6 +38,8 @@ private:
|
||||
void updateCameraBoundsBlend(float deltaTime);
|
||||
void updateBattleZones();
|
||||
void updateCamera(float deltaTime);
|
||||
void updateUiOverlay();
|
||||
void persistDebugFlags();
|
||||
|
||||
LevelDefinition level_;
|
||||
DebugFlags debugFlags_;
|
||||
@@ -51,6 +54,7 @@ private:
|
||||
CameraController cameraController_;
|
||||
StageRenderer stageRenderer_;
|
||||
DebugOverlay debugOverlay_;
|
||||
frostbite2D::Ptr<GameUiOverlay> uiOverlay_;
|
||||
frostbite2D::Ptr<PlayerActor> player_;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
#include "game_ui_overlay.h"
|
||||
|
||||
#include "../core/game_config.h"
|
||||
|
||||
#include <frostbite2D/graphics/font_manager.h>
|
||||
#include <frostbite2D/graphics/renderer.h>
|
||||
|
||||
#include <sstream>
|
||||
|
||||
namespace ns_game {
|
||||
|
||||
void GameUiOverlay::onEnter() {
|
||||
frostbite2D::UIScene::onEnter();
|
||||
|
||||
if (frostbite2D::FontManager::get().hasFont("debug")) {
|
||||
statusText_ = frostbite2D::TextSprite::create();
|
||||
statusText_->SetFont("debug");
|
||||
statusText_->SetTextColor(0.88f, 0.94f, 1.0f, 1.0f);
|
||||
statusText_->SetTopLeftPosition(16.0f, 14.0f);
|
||||
statusText_->SetZOrder(10);
|
||||
AddChild(statusText_);
|
||||
textReady_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
void GameUiOverlay::SetState(const GameUiOverlayState& state) {
|
||||
state_ = state;
|
||||
if (textReady_ && statusText_) {
|
||||
statusText_->SetText(BuildStatusText());
|
||||
}
|
||||
}
|
||||
|
||||
void GameUiOverlay::Render() {
|
||||
if (!textReady_) {
|
||||
RenderFallbackPanel();
|
||||
}
|
||||
frostbite2D::UIScene::Render();
|
||||
}
|
||||
|
||||
std::string GameUiOverlay::BuildStatusText() const {
|
||||
std::ostringstream text;
|
||||
text << "NS Unknown Game\n";
|
||||
text << "Debug: " << (state_.debugFlags.enabled ? "on" : "off")
|
||||
<< " Zone: " << state_.activeBattleZoneIndex << "\n";
|
||||
text << "Audio " << (state_.services.audioInitialized ? "on" : "off")
|
||||
<< " Save " << (state_.services.saveInitialized ? "on" : "off")
|
||||
<< " PVF " << (state_.services.pvfArchiveOpen ? "on" : "off")
|
||||
<< " NPK " << (state_.services.npkArchiveOpen ? "on" : "off")
|
||||
<< " AudioDB "
|
||||
<< (state_.services.audioDatabaseLoaded ? "on" : "off");
|
||||
return text.str();
|
||||
}
|
||||
|
||||
void GameUiOverlay::RenderFallbackPanel() const {
|
||||
frostbite2D::Renderer& renderer = frostbite2D::Renderer::get();
|
||||
renderer.drawQuad(frostbite2D::Rect(12.0f, 12.0f, 250.0f, 54.0f),
|
||||
frostbite2D::Color(0.04f, 0.05f, 0.06f, 0.75f));
|
||||
|
||||
const float serviceCount =
|
||||
(state_.services.audioInitialized ? 1.0f : 0.0f) +
|
||||
(state_.services.saveInitialized ? 1.0f : 0.0f) +
|
||||
(state_.services.pvfArchiveOpen ? 1.0f : 0.0f) +
|
||||
(state_.services.npkArchiveOpen ? 1.0f : 0.0f) +
|
||||
(state_.services.audioDatabaseLoaded ? 1.0f : 0.0f);
|
||||
renderer.drawQuad(frostbite2D::Rect(20.0f, 24.0f, serviceCount * 38.0f,
|
||||
10.0f),
|
||||
frostbite2D::Color(0.25f, 0.72f, 0.94f, 0.95f));
|
||||
renderer.drawQuad(frostbite2D::Rect(20.0f, 44.0f,
|
||||
state_.debugFlags.enabled ? 180.0f
|
||||
: 52.0f,
|
||||
10.0f),
|
||||
state_.debugFlags.enabled
|
||||
? frostbite2D::Color(0.95f, 0.78f, 0.25f, 0.95f)
|
||||
: frostbite2D::Color(0.45f, 0.48f, 0.52f, 0.8f));
|
||||
}
|
||||
|
||||
} // namespace ns_game
|
||||
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
#include "../core/debug_flags.h"
|
||||
#include "../core/game_services.h"
|
||||
|
||||
#include <frostbite2D/2d/text_sprite.h>
|
||||
#include <frostbite2D/scene/ui_scene.h>
|
||||
#include <string>
|
||||
|
||||
namespace ns_game {
|
||||
|
||||
struct GameUiOverlayState {
|
||||
DebugFlags debugFlags;
|
||||
GameServiceStatus services;
|
||||
int activeBattleZoneIndex = -1;
|
||||
};
|
||||
|
||||
class GameUiOverlay : public frostbite2D::UIScene {
|
||||
public:
|
||||
void onEnter() override;
|
||||
void SetState(const GameUiOverlayState& state);
|
||||
void Render() override;
|
||||
|
||||
private:
|
||||
std::string BuildStatusText() const;
|
||||
void RenderFallbackPanel() const;
|
||||
|
||||
GameUiOverlayState state_;
|
||||
frostbite2D::Ptr<frostbite2D::TextSprite> statusText_;
|
||||
bool textReady_ = false;
|
||||
};
|
||||
|
||||
} // namespace ns_game
|
||||
Reference in New Issue
Block a user