From 45a8712b00d548bce2d946cf6e65e4143f8d5351 Mon Sep 17 00:00:00 2001 From: 1165565692-lang <1165565692@qq.com> Date: Tue, 9 Jun 2026 21:47:07 +0800 Subject: [PATCH] Wire engine services into game skeleton --- game/assets/map/stage_01.map | 10 +++ game/docs/README.md | 1 + game/docs/engine_capability_audit.md | 2 + game/docs/game_skeleton.md | 79 +++++++++++++++++ game/docs/planning/scene_build_plan.md | 2 + game/src/core/game_services.cpp | 112 +++++++++++++++++++++++++ game/src/core/game_services.h | 37 ++++++++ game/src/data/stage_map_resource.cpp | 93 ++++++++++++++++++++ game/src/data/stage_map_resource.h | 29 +++++++ game/src/data/whitebox_level.cpp | 13 +++ game/src/main.cpp | 3 + 11 files changed, 381 insertions(+) create mode 100644 game/assets/map/stage_01.map create mode 100644 game/docs/game_skeleton.md create mode 100644 game/src/core/game_services.cpp create mode 100644 game/src/core/game_services.h create mode 100644 game/src/data/stage_map_resource.cpp create mode 100644 game/src/data/stage_map_resource.h diff --git a/game/assets/map/stage_01.map b/game/assets/map/stage_01.map new file mode 100644 index 0000000..0079ad2 --- /dev/null +++ b/game/assets/map/stage_01.map @@ -0,0 +1,10 @@ +# stage_01.map +# +# 当前文件先作为地图资源链占位:Asset 可以读取它,后续 StageMapLoader 会把 +# world/camera/player/layers/collision/battle_zones 等字段解析成 LevelDefinition。 +# 现阶段 WhiteboxScene 仍使用 C++ fallback 白盒地图,避免未完成 parser 影响构建。 + +map stage_01 +world 3200 720 +camera 0 0 3200 720 +player_spawn 160 380 diff --git a/game/docs/README.md b/game/docs/README.md index 9347b4b..47c4bd0 100644 --- a/game/docs/README.md +++ b/game/docs/README.md @@ -11,6 +11,7 @@ - `controls.md`:玩家操作、平台输入映射和开发调试快捷键。 - `engine_capability_audit.md`:引擎已有能力复盘,以及游戏层应优先复用的系统。 +- `game_skeleton.md`:当前游戏骨架如何接入引擎基础能力。 - `scene_acceptance.md`:第一关场景骨架验收流程和最近一次验收结果。 ## Naming diff --git a/game/docs/engine_capability_audit.md b/game/docs/engine_capability_audit.md index a999700..e598386 100644 --- a/game/docs/engine_capability_audit.md +++ b/game/docs/engine_capability_audit.md @@ -8,6 +8,8 @@ 最重要的调整是:**场景地图不要继续在 C++ 里手写 `Rect`,下一步应接入 `.map` 文件加载,并优先通过引擎 `Asset` / `PvfArchive` / `ScriptParser` 读取和解析。** +当前第一轮骨架接入已完成:游戏启动时通过 `GameServices` 探测/初始化 `AudioSystem`、`PvfArchive`、`NpkArchive`、`SoundPackArchive`、`AudioDatabase` 和 `SaveSystem` 状态,并通过 `StageMapResource` 接入 `.map` 资源读取入口。详细状态见 `game_skeleton.md`。 + 推荐方向: ```text diff --git a/game/docs/game_skeleton.md b/game/docs/game_skeleton.md new file mode 100644 index 0000000..d4f9cff --- /dev/null +++ b/game/docs/game_skeleton.md @@ -0,0 +1,79 @@ +# Game Skeleton + +本文档记录当前游戏骨架如何优先复用 Frostbite2D 引擎能力。目标是让游戏层只负责游戏语义,不重复实现引擎已经提供的资源、归档、音频、存档、场景和渲染基础设施。 + +## 当前接入状态 + +### Application 基础服务 + +由引擎 `Application::init()` 负责: + +- `Asset` 工作目录和资源路径解析。 +- `SaveSystem` 初始化。 +- SDL video/events/controller 初始化。 +- Window / Renderer / Camera 初始化。 +- PixelArt2D 渲染风格和虚拟分辨率。 +- `TaskSystem` 初始化。 + +由引擎 `Application::shutdown()` 负责: + +- `SceneManager` 清理。 +- `Renderer` 清理。 +- `AudioSystem` 清理。 +- `NpkArchive` / `PvfArchive` / `SoundPackArchive` 清理。 +- 平台资源清理。 + +### GameServices + +游戏层新增 `GameServices`,只做游戏项目需要的引擎服务接入和状态记录: + +- 初始化 `AudioSystem`。 +- 探测并打开可选 `Script.pvf`。 +- 初始化 `NpkArchive`。 +- 初始化 `SoundPackArchive`。 +- 探测并加载可选 `AudioDatabase`。 +- 记录 `SaveSystem` 是否已由引擎初始化。 + +这些服务是可选能力。缺少 `Script.pvf`、`ImagePacks2`、`SoundPacks` 或 `audio.xml` 时,游戏仍然使用当前 PNG/白盒 fallback 启动。 + +## 地图资源入口 + +新增 `StageMapResource`: + +```text +LoadStageMapResource("map/stage_01.map") +``` + +读取优先级: + +1. `Asset` 普通文本资源:`map/stage_01.map` +2. `Asset` fallback:`assets/map/stage_01.map` +3. `PvfArchive` 二进制脚本:`map/stage_01.map` +4. `PvfArchive` 文本内容:`map/stage_01.map` + +当前状态: + +- 已新增 `game/assets/map/stage_01.map` 占位文件。 +- `CreateWhiteboxLevel()` 会读取地图资源并记录来源。 +- 地图内容尚未接管 `LevelDefinition`,当前仍使用 C++ 白盒数据作为 fallback。 + +下一步: + +- 实现 `StageMapLoader`,把 `.map` 转成 `LevelDefinition`。 +- 将 `whitebox_level.cpp` 中的硬编码平台、props、battle zone 迁移到 `.map`。 +- 失败时保留 `CreateFallbackWhiteboxLevel()`。 + +## 仍待迁移的能力 + +- 玩家动画:从手写 spritesheet 帧切换迁移到引擎 `Animation` / PVF ANI。 +- 场景视觉资源:如果素材进入 NPK,迁移到 `NpkArchive` / `Sprite::createFromNpk`。 +- 音频:使用 `AudioSystem`、`Sound`、`Music`、`AudioDatabase`。 +- 地图:完成 `.map` parser,不再用 C++ 手写地图。 +- 输入配置:通过 `Asset` 或 PVF 读取 `InputConfig`。 + +## 原则 + +- 优先使用引擎能力。 +- 游戏层只做数据转换和玩法语义。 +- 外部资源统一走 `Asset` / `PvfArchive`。 +- C++ 白盒数据只保留为 fallback。 diff --git a/game/docs/planning/scene_build_plan.md b/game/docs/planning/scene_build_plan.md index 83cf570..cf4d572 100644 --- a/game/docs/planning/scene_build_plan.md +++ b/game/docs/planning/scene_build_plan.md @@ -271,6 +271,8 @@ game/assets/stage/stage_01/ 下一步地图框架调整:优先复用引擎 `Asset`、`PvfArchive` 和 `ScriptParser` 能力,把地图从 C++ 硬编码迁移到 `.map` 外部资源文件。详细复盘见 `../engine_capability_audit.md`。 +第一轮引擎能力接入已完成:`GameServices` 统一接入音频、PVF、NPK、SoundPack、AudioDB、SaveSystem 状态;`StageMapResource` 已能读取 `map/stage_01.map`,当前仍使用白盒 C++ 地图作为 fallback。完整骨架说明见 `../game_skeleton.md`。 + ### Debug 快捷键 场景 debug 快捷键统一记录在 `../controls.md`。 diff --git a/game/src/core/game_services.cpp b/game/src/core/game_services.cpp new file mode 100644 index 0000000..fd4fa30 --- /dev/null +++ b/game/src/core/game_services.cpp @@ -0,0 +1,112 @@ +#include "game_services.h" + +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace ns_game { + +namespace { + +bool assetExists(const char* path) { + return frostbite2D::Asset::get().exists(path); +} + +const char* firstExistingPath(const char* primary, const char* fallback) { + if (assetExists(primary)) { + return primary; + } + if (fallback && assetExists(fallback)) { + return fallback; + } + return nullptr; +} + +} // namespace + +GameServices& GameServices::Get() { + static GameServices services; + return services; +} + +void GameServices::Initialize() { + if (initialized_) { + return; + } + + status_.saveInitialized = frostbite2D::SaveSystem::get().listSlots().size() > 0; + initializeAudio(); + initializePvfArchive(); + initializeNpkArchive(); + initializeSoundPackArchive(); + initializeAudioDatabase(); + + SDL_Log("GameServices: audio=%d save=%d pvf=%d npk=%d soundpack=%d audiodb=%d", + status_.audioInitialized ? 1 : 0, + status_.saveInitialized ? 1 : 0, + status_.pvfArchiveOpen ? 1 : 0, + status_.npkArchiveOpen ? 1 : 0, + status_.soundPackOpen ? 1 : 0, + status_.audioDatabaseLoaded ? 1 : 0); + + initialized_ = true; +} + +void GameServices::initializeAudio() { + frostbite2D::AudioConfig config; + status_.audioInitialized = frostbite2D::AudioSystem::get().init(config); +} + +void GameServices::initializePvfArchive() { + const char* pvfPath = firstExistingPath("Script.pvf", "assets/Script.pvf"); + if (!pvfPath) { + SDL_Log("GameServices: optional Script.pvf not found"); + return; + } + + auto& pvf = frostbite2D::PvfArchive::get(); + if (pvf.open(pvfPath)) { + pvf.init(); + status_.pvfArchiveOpen = pvf.isOpen(); + } +} + +void GameServices::initializeNpkArchive() { + auto& asset = frostbite2D::Asset::get(); + auto& npk = frostbite2D::NpkArchive::get(); + if (asset.isDirectory("assets/ImagePacks2")) { + npk.setImagePackDirectory("assets/ImagePacks2"); + } + npk.init(); + status_.npkArchiveOpen = npk.isOpen(); +} + +void GameServices::initializeSoundPackArchive() { + auto& asset = frostbite2D::Asset::get(); + auto& soundPack = frostbite2D::SoundPackArchive::get(); + if (asset.isDirectory("assets/SoundPacks")) { + soundPack.setSoundPackDirectory("assets/SoundPacks"); + } + soundPack.init(); + status_.soundPackOpen = soundPack.isOpen(); +} + +void GameServices::initializeAudioDatabase() { + const char* audioDbPath = + firstExistingPath("assets/audio/audio.xml", "assets/audio.xml"); + if (!audioDbPath) { + SDL_Log("GameServices: optional audio database not found"); + return; + } + + status_.audioDatabaseLoaded = + frostbite2D::AudioDatabase::get().loadFromFile(audioDbPath); +} + +} // namespace ns_game diff --git a/game/src/core/game_services.h b/game/src/core/game_services.h new file mode 100644 index 0000000..4eecace --- /dev/null +++ b/game/src/core/game_services.h @@ -0,0 +1,37 @@ +#pragma once + +namespace ns_game { + +struct GameServiceStatus { + bool audioInitialized = false; + bool saveInitialized = false; + bool pvfArchiveOpen = false; + bool npkArchiveOpen = false; + bool soundPackOpen = false; + bool audioDatabaseLoaded = false; +}; + +class GameServices { +public: + static GameServices& Get(); + + GameServices(const GameServices&) = delete; + GameServices& operator=(const GameServices&) = delete; + + void Initialize(); + const GameServiceStatus& Status() const { return status_; } + +private: + GameServices() = default; + + void initializeAudio(); + void initializePvfArchive(); + void initializeNpkArchive(); + void initializeSoundPackArchive(); + void initializeAudioDatabase(); + + GameServiceStatus status_; + bool initialized_ = false; +}; + +} // namespace ns_game diff --git a/game/src/data/stage_map_resource.cpp b/game/src/data/stage_map_resource.cpp new file mode 100644 index 0000000..6779c02 --- /dev/null +++ b/game/src/data/stage_map_resource.cpp @@ -0,0 +1,93 @@ +#include "stage_map_resource.h" + +#include +#include +#include + +#include + +namespace ns_game { + +namespace { + +std::string assetFallbackPath(const std::string& path) { + if (path.rfind("assets/", 0) == 0) { + return path; + } + return "assets/" + path; +} + +bool isLikelyPvfScript(const frostbite2D::RawData& rawData) { + if (!rawData.data || rawData.size < 7) { + return false; + } + + const auto opcode = static_cast(rawData.data[2]); + switch (static_cast(opcode)) { + case frostbite2D::ScriptOpcode::Integer: + case frostbite2D::ScriptOpcode::Float: + case frostbite2D::ScriptOpcode::StringRef5: + case frostbite2D::ScriptOpcode::StringRef6: + case frostbite2D::ScriptOpcode::StringRef7: + case frostbite2D::ScriptOpcode::StringRef8: + case frostbite2D::ScriptOpcode::ExtendedString9: + case frostbite2D::ScriptOpcode::ExtendedString10: + return true; + default: + return false; + } +} + +} // namespace + +std::optional LoadStageMapResource(const std::string& path) { + frostbite2D::Asset& asset = frostbite2D::Asset::get(); + if (auto content = asset.readFileToString(path)) { + StageMapResource resource; + resource.source = StageMapResourceSource::AssetText; + resource.path = path; + resource.text = *content; + return resource; + } + + const std::string fallbackPath = assetFallbackPath(path); + if (fallbackPath != path) { + if (auto content = asset.readFileToString(fallbackPath)) { + StageMapResource resource; + resource.source = StageMapResourceSource::AssetText; + resource.path = fallbackPath; + resource.text = *content; + return resource; + } + } + + frostbite2D::PvfArchive& pvf = frostbite2D::PvfArchive::get(); + if (pvf.isOpen()) { + if (auto rawData = pvf.getFileRawData(path); + rawData && isLikelyPvfScript(*rawData)) { + frostbite2D::ScriptParser parser(*rawData, path); + if (parser.isValid()) { + StageMapResource resource; + resource.source = StageMapResourceSource::PvfBinaryScript; + resource.path = path; + for (const frostbite2D::ScriptValue& value : parser.parseAll()) { + resource.scriptValues.push_back(value.toString()); + } + return resource; + } + } + + if (auto content = pvf.getFileContent(path)) { + StageMapResource resource; + resource.source = StageMapResourceSource::PvfText; + resource.path = path; + resource.text = *content; + return resource; + } + } + + SDL_Log("StageMapResource: map not found: %s", path.c_str()); + return std::nullopt; +} + +} // namespace ns_game diff --git a/game/src/data/stage_map_resource.h b/game/src/data/stage_map_resource.h new file mode 100644 index 0000000..aa7cea1 --- /dev/null +++ b/game/src/data/stage_map_resource.h @@ -0,0 +1,29 @@ +#pragma once + +#include +#include +#include + +namespace ns_game { + +enum class StageMapResourceSource { + None, + AssetText, + PvfText, + PvfBinaryScript +}; + +struct StageMapResource { + StageMapResourceSource source = StageMapResourceSource::None; + std::string path; + std::string text; + std::vector scriptValues; + + bool hasContent() const { + return !text.empty() || !scriptValues.empty(); + } +}; + +std::optional LoadStageMapResource(const std::string& path); + +} // namespace ns_game diff --git a/game/src/data/whitebox_level.cpp b/game/src/data/whitebox_level.cpp index 9220b2a..74d1855 100644 --- a/game/src/data/whitebox_level.cpp +++ b/game/src/data/whitebox_level.cpp @@ -1,8 +1,21 @@ #include "level_definition.h" +#include "stage_map_resource.h" + +#include namespace ns_game { LevelDefinition CreateWhiteboxLevel() { + if (auto mapResource = LoadStageMapResource("map/stage_01.map")) { + SDL_Log("CreateWhiteboxLevel: loaded %s map resource: %s", + mapResource->source == StageMapResourceSource::AssetText ? "asset" + : mapResource->source == StageMapResourceSource::PvfText ? "pvf-text" + : mapResource->source == StageMapResourceSource::PvfBinaryScript + ? "pvf-script" + : "unknown", + mapResource->path.c_str()); + } + LevelDefinition level; level.worldWidth = 3200.0f; level.worldHeight = 720.0f; diff --git a/game/src/main.cpp b/game/src/main.cpp index 88b2105..699b2d7 100644 --- a/game/src/main.cpp +++ b/game/src/main.cpp @@ -1,5 +1,6 @@ #include "scene/whitebox_scene.h" #include "core/game_config.h" +#include "core/game_services.h" #include @@ -80,6 +81,8 @@ int main(int argc, char** argv) { return 1; } + ns_game::GameServices::Get().Initialize(); + app.run([]() { frostbite2D::SceneManager::get().ReplaceScene( frostbite2D::MakePtr());