Wire engine services into game skeleton
This commit is contained in:
@@ -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
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
- `controls.md`:玩家操作、平台输入映射和开发调试快捷键。
|
- `controls.md`:玩家操作、平台输入映射和开发调试快捷键。
|
||||||
- `engine_capability_audit.md`:引擎已有能力复盘,以及游戏层应优先复用的系统。
|
- `engine_capability_audit.md`:引擎已有能力复盘,以及游戏层应优先复用的系统。
|
||||||
|
- `game_skeleton.md`:当前游戏骨架如何接入引擎基础能力。
|
||||||
- `scene_acceptance.md`:第一关场景骨架验收流程和最近一次验收结果。
|
- `scene_acceptance.md`:第一关场景骨架验收流程和最近一次验收结果。
|
||||||
|
|
||||||
## Naming
|
## Naming
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
|
|
||||||
最重要的调整是:**场景地图不要继续在 C++ 里手写 `Rect`,下一步应接入 `.map` 文件加载,并优先通过引擎 `Asset` / `PvfArchive` / `ScriptParser` 读取和解析。**
|
最重要的调整是:**场景地图不要继续在 C++ 里手写 `Rect`,下一步应接入 `.map` 文件加载,并优先通过引擎 `Asset` / `PvfArchive` / `ScriptParser` 读取和解析。**
|
||||||
|
|
||||||
|
当前第一轮骨架接入已完成:游戏启动时通过 `GameServices` 探测/初始化 `AudioSystem`、`PvfArchive`、`NpkArchive`、`SoundPackArchive`、`AudioDatabase` 和 `SaveSystem` 状态,并通过 `StageMapResource` 接入 `.map` 资源读取入口。详细状态见 `game_skeleton.md`。
|
||||||
|
|
||||||
推荐方向:
|
推荐方向:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
|
|||||||
@@ -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。
|
||||||
@@ -271,6 +271,8 @@ game/assets/stage/stage_01/
|
|||||||
|
|
||||||
下一步地图框架调整:优先复用引擎 `Asset`、`PvfArchive` 和 `ScriptParser` 能力,把地图从 C++ 硬编码迁移到 `.map` 外部资源文件。详细复盘见 `../engine_capability_audit.md`。
|
下一步地图框架调整:优先复用引擎 `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 快捷键
|
||||||
|
|
||||||
场景 debug 快捷键统一记录在 `../controls.md`。
|
场景 debug 快捷键统一记录在 `../controls.md`。
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
#include "game_services.h"
|
||||||
|
|
||||||
|
#include <frostbite2D/audio/audio_system.h>
|
||||||
|
#include <frostbite2D/resource/asset.h>
|
||||||
|
#include <frostbite2D/resource/audio_database.h>
|
||||||
|
#include <frostbite2D/resource/npk_archive.h>
|
||||||
|
#include <frostbite2D/resource/pvf_archive.h>
|
||||||
|
#include <frostbite2D/resource/save_system.h>
|
||||||
|
#include <frostbite2D/resource/sound_pack_archive.h>
|
||||||
|
|
||||||
|
#include <SDL2/SDL.h>
|
||||||
|
|
||||||
|
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
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
#include "stage_map_resource.h"
|
||||||
|
|
||||||
|
#include <frostbite2D/resource/asset.h>
|
||||||
|
#include <frostbite2D/resource/pvf_archive.h>
|
||||||
|
#include <frostbite2D/resource/script_parser.h>
|
||||||
|
|
||||||
|
#include <SDL2/SDL.h>
|
||||||
|
|
||||||
|
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<unsigned char>(rawData.data[2]);
|
||||||
|
switch (static_cast<frostbite2D::ScriptOpcode>(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<StageMapResource> 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
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace ns_game {
|
||||||
|
|
||||||
|
enum class StageMapResourceSource {
|
||||||
|
None,
|
||||||
|
AssetText,
|
||||||
|
PvfText,
|
||||||
|
PvfBinaryScript
|
||||||
|
};
|
||||||
|
|
||||||
|
struct StageMapResource {
|
||||||
|
StageMapResourceSource source = StageMapResourceSource::None;
|
||||||
|
std::string path;
|
||||||
|
std::string text;
|
||||||
|
std::vector<std::string> scriptValues;
|
||||||
|
|
||||||
|
bool hasContent() const {
|
||||||
|
return !text.empty() || !scriptValues.empty();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
std::optional<StageMapResource> LoadStageMapResource(const std::string& path);
|
||||||
|
|
||||||
|
} // namespace ns_game
|
||||||
@@ -1,8 +1,21 @@
|
|||||||
#include "level_definition.h"
|
#include "level_definition.h"
|
||||||
|
#include "stage_map_resource.h"
|
||||||
|
|
||||||
|
#include <SDL2/SDL.h>
|
||||||
|
|
||||||
namespace ns_game {
|
namespace ns_game {
|
||||||
|
|
||||||
LevelDefinition CreateWhiteboxLevel() {
|
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;
|
LevelDefinition level;
|
||||||
level.worldWidth = 3200.0f;
|
level.worldWidth = 3200.0f;
|
||||||
level.worldHeight = 720.0f;
|
level.worldHeight = 720.0f;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#include "scene/whitebox_scene.h"
|
#include "scene/whitebox_scene.h"
|
||||||
#include "core/game_config.h"
|
#include "core/game_config.h"
|
||||||
|
#include "core/game_services.h"
|
||||||
|
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
|
|
||||||
@@ -80,6 +81,8 @@ int main(int argc, char** argv) {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ns_game::GameServices::Get().Initialize();
|
||||||
|
|
||||||
app.run([]() {
|
app.run([]() {
|
||||||
frostbite2D::SceneManager::get().ReplaceScene(
|
frostbite2D::SceneManager::get().ReplaceScene(
|
||||||
frostbite2D::MakePtr<ns_game::WhiteboxScene>());
|
frostbite2D::MakePtr<ns_game::WhiteboxScene>());
|
||||||
|
|||||||
Reference in New Issue
Block a user