Load stage map into level definition

This commit is contained in:
2026-06-09 22:16:19 +08:00
parent 45a8712b00
commit 3fc9caa80b
9 changed files with 574 additions and 61 deletions
+64 -6
View File
@@ -1,10 +1,68 @@
# 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
collision 0 600 3200 80
collision 230 500 220 28
collision 560 430 260 28
collision 930 510 260 28
collision 1250 420 230 28
collision 1760 500 300 28
collision 2180 455 260 28
collision 2650 515 360 28
layer background_far BackgroundFar 0.25
rect 0 0 3200 720 0.08 0.10 0.14 1.0
rect 360 120 420 220 0.13 0.16 0.21 1.0
rect 1380 90 560 260 0.12 0.15 0.20 1.0
rect 2440 140 520 210 0.13 0.16 0.21 1.0
endlayer
layer background_mid BackgroundMid 0.55
rect 80 360 280 240 0.18 0.22 0.27 1.0
rect 1020 310 340 290 0.18 0.22 0.27 1.0
rect 2020 330 360 270 0.18 0.22 0.27 1.0
rect 2860 280 260 320 0.18 0.22 0.27 1.0
endlayer
layer level_visual LevelVisual 1.0
rect 0 600 3200 80 0.62 0.68 0.73 1.0
rect 230 500 220 28 0.62 0.68 0.73 1.0
rect 560 430 260 28 0.62 0.68 0.73 1.0
rect 930 510 260 28 0.62 0.68 0.73 1.0
rect 1250 420 230 28 0.62 0.68 0.73 1.0
rect 1760 500 300 28 0.62 0.68 0.73 1.0
rect 2180 455 260 28 0.62 0.68 0.73 1.0
rect 2650 515 360 28 0.62 0.68 0.73 1.0
endlayer
layer props_back PropsBack 1.0
rect 118 392 88 208 0.26 0.30 0.34 1.0
rect 780 250 72 180 0.26 0.30 0.34 1.0
rect 1440 300 80 300 0.26 0.30 0.34 1.0
rect 1880 360 110 240 0.26 0.30 0.34 1.0
rect 2520 390 92 210 0.26 0.30 0.34 1.0
rect 3040 315 82 285 0.26 0.30 0.34 1.0
endlayer
layer props_front PropsFront 1.0
rect 520 580 220 34 0.14 0.17 0.18 0.90
rect 1510 570 280 42 0.14 0.17 0.18 0.90
rect 2580 572 260 38 0.14 0.17 0.18 0.90
endlayer
battle_zone zone_01_intro 520 260 560 340 0 0 1940 720
spawn zone_01_enemy_a 760 560
spawn zone_01_enemy_b 980 560
endbattle_zone
battle_zone zone_02_platform 1420 250 680 350 660 0 2300 720
spawn zone_02_enemy_a 1620 560
spawn zone_02_enemy_b 1900 560
endbattle_zone
battle_zone zone_03_exit 2380 250 720 350 1600 0 1600 720
spawn zone_03_enemy_a 2580 560
spawn zone_03_enemy_b 2860 560
endbattle_zone
+23 -25
View File
@@ -8,7 +8,7 @@
最重要的调整是:**场景地图不要继续在 C++ 里手写 `Rect`,下一步应接入 `.map` 文件加载,并优先通过引擎 `Asset` / `PvfArchive` / `ScriptParser` 读取和解析。**
当前第一轮骨架接入已完成:游戏启动时通过 `GameServices` 探测/初始化 `AudioSystem``PvfArchive``NpkArchive``SoundPackArchive``AudioDatabase``SaveSystem` 状态,并通过 `StageMapResource` 接入 `.map` 资源读取入口。详细状态见 `game_skeleton.md`
当前第一轮骨架接入已完成:游戏启动时通过 `GameServices` 探测/初始化 `AudioSystem``PvfArchive``NpkArchive``SoundPackArchive``AudioDatabase``SaveSystem` 状态,并通过 `StageMapResource` + `StageMapLoader` 接入 `.map` 资源读取和解析入口。详细状态见 `game_skeleton.md`
推荐方向:
@@ -74,15 +74,15 @@
- `Asset` 被引擎纹理加载间接使用。
- Windows / Switch 的资源根目录、RomFS 路径已经由引擎 `Asset` 解析。
未充分使用
当前状态
- 游戏层目前没有直接用 `Asset::readTextFile` / `readBinaryFile` 读取地图和配置
- `stage_01` 地图仍写死在 C++
- `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` 文件。
下一步:
- `.map` 普通文件先用 `Asset` 读取
- 同一套 loader 后续支持从 PVF 中读取。
- 继续把输入配置、角色动画配置等文本/脚本配置接入 `Asset` 或 PVF
### PVF / ScriptParser
@@ -98,8 +98,9 @@
当前状态:
- 游戏层还没使用
- 引擎已经具备从 `Script.pvf` 按逻辑路径取脚本文件的能力
- `StageMapResource` 已能在 `PvfArchive` 打开后尝试读取 `map/stage_01.map`
- 若资源像 PVF 二进制脚本,会先用 `ScriptParser` 解析出 script values
- 若资源是 PVF 文本,会作为普通 `.map` 文本交给 `StageMapLoader`
- `ScriptParser` 支持 PVF 脚本二进制格式,不是普通文本 parser。
地图结论:
@@ -185,20 +186,17 @@
- `game/src/data/whitebox_level.cpp`
问题
当前状态
- 世界尺寸、平台、视觉层、props、battle zone、spawn point 全部写在 C++
- 这不适合后续反复调地图,也不适合你要的 `.map` 工作流
- 已新增 `game/assets/map/stage_01.map`
- 已新增 `StageMapLoader`,把 `.map` 文本解析成 `LevelDefinition`
- `CreateWhiteboxLevel()` 已改为优先加载 `.map`,失败时才回退 C++ 白盒数据。
- C++ 中保留的世界尺寸、平台、视觉层、props、battle zone、spawn point 现在只作为 fallback。
建议:
后续建议:
- 新增 `StageMapLoader`
- 创建 `game/assets/stage/stage_01/stage_01.map``game/assets/map/stage_01.map`
-`CreateWhiteboxLevel()` 临时变成:
```cpp
return LoadStageMap("map/stage_01.map").value_or(CreateFallbackWhiteboxLevel());
```
- 继续扩展 `.map` 的 tileset/tile entry 表达
- 逐步把 `level_visual` 的长色块替换成 tile 数据
### 2. 手写动画帧切换
@@ -245,13 +243,13 @@ return LoadStageMap("map/stage_01.map").value_or(CreateFallbackWhiteboxLevel());
### 加载优先级
第一阶段支持普通资源文件:
第一阶段支持普通资源文件:
```text
Asset::readTextFile("map/stage_01.map")
```
第二阶段支持 PVF
第二阶段已预留 PVF
```text
PvfArchive::getFileContent("map/stage_01.map")
@@ -312,10 +310,10 @@ StageMapLoader
## 推荐执行顺序
1. `StageMapResource`定义地图文件路径、普通资源读取和 PVF 读取优先级。
2. `StageMapLoader``.map` 转成 `LevelDefinition`
3. `stage_01.map`把当前 `whitebox_level.cpp` 数据迁移成外部地图文件。
4. `CreateWhiteboxLevel()`改为加载 `stage_01.map`,失败时回退旧白盒数据。
1. 已完成:`StageMapResource` 定义地图文件路径、普通资源读取和 PVF 读取优先级。
2. 已完成:`StageMapLoader` `.map` 转成 `LevelDefinition`
3. 已完成:`stage_01.map` 把当前 `whitebox_level.cpp` 数据迁移成外部地图文件。
4. 已完成:`CreateWhiteboxLevel()` 改为加载 `stage_01.map`,失败时回退旧白盒数据。
5. `StageRenderer`:继续消费 `LevelDefinition`,不直接关心 `.map`
6. 文档:把地图编辑流程写入 `scene_acceptance.md` 和场景计划。
+24 -9
View File
@@ -38,10 +38,11 @@
## 地图资源入口
新增 `StageMapResource`
新增 `StageMapResource``StageMapLoader`
```text
LoadStageMapResource("map/stage_01.map")
LoadStageMap("map/stage_01.map")
```
读取优先级:
@@ -53,22 +54,36 @@ LoadStageMapResource("map/stage_01.map")
当前状态:
- 已新增 `game/assets/map/stage_01.map` 占位文件
- `CreateWhiteboxLevel()` 会读取地图资源并记录来源
- 地图内容尚未接管 `LevelDefinition`,当前仍使用 C++ 白盒数据作为 fallback
- `game/assets/map/stage_01.map` 已接管第一关主数据
- `StageMapLoader` 会把 `.map` 文本解析成 `LevelDefinition`
- 已迁移世界尺寸、玩家出生点、相机边界、碰撞平台、视觉层、props、battle zone 和 spawn point
- `CreateWhiteboxLevel()` 优先读取 `.map`,读取或解析失败时才使用 C++ 白盒 fallback。
- Windows 和 Switch 构建会把 `game/assets/*` 复制到产物 `assets/`,保证运行时 `assets/map/stage_01.map` 路径可用。
下一步
`.map` 当前支持的命令
- 实现 `StageMapLoader`,把 `.map` 转成 `LevelDefinition`
-`whitebox_level.cpp` 中的硬编码平台、props、battle zone 迁移到 `.map`
- 失败时保留 `CreateFallbackWhiteboxLevel()`
```text
map <id>
world <width> <height>
camera <x> <y> <width> <height>
player_spawn <x> <y>
collision <x> <y> <width> <height>
layer <id> <BackgroundFar|BackgroundMid|LevelVisual|PropsBack|PropsFront> <parallax>
rect <x> <y> <width> <height> <r> <g> <b> <a>
rect_sprite <x> <y> <width> <height> <r> <g> <b> <a> <texture>
rect_sprite_src <x> <y> <width> <height> <r> <g> <b> <a> <texture> <sx> <sy> <sw> <sh>
endlayer
battle_zone <id> <trigger_x> <trigger_y> <trigger_w> <trigger_h> <camera_x> <camera_y> <camera_w> <camera_h>
spawn <id> <x> <y>
endbattle_zone
```
## 仍待迁移的能力
- 玩家动画:从手写 spritesheet 帧切换迁移到引擎 `Animation` / PVF ANI。
- 场景视觉资源:如果素材进入 NPK,迁移到 `NpkArchive` / `Sprite::createFromNpk`
- 音频:使用 `AudioSystem``Sound``Music``AudioDatabase`
- 地图:完成 `.map` parser,不再用 C++ 手写地图
- 地图:后续可把 `.map` 扩展为工具导出格式或 PVF 脚本格式,当前 C++ 地图只作为 fallback
- 输入配置:通过 `Asset` 或 PVF 读取 `InputConfig`
## 原则
+12 -2
View File
@@ -269,9 +269,19 @@ game/assets/stage/stage_01/
当前场景骨架已经达到“可直接放素材”的工程状态。后续素材接入主要修改 `game/assets/stage/stage_01/` 和关卡 layer 数据,不需要再重写 `WhiteboxScene` 主流程。
下一步地图框架调整:优先复用引擎 `Asset``PvfArchive``ScriptParser` 能力,把地图从 C++ 硬编码迁移到 `.map` 外部资源文件。详细复盘见 `../engine_capability_audit.md`
地图框架调整已完成第一步:优先复用引擎 `Asset``PvfArchive``ScriptParser` 能力,`stage_01.map` 已从占位文件升级为第一关主数据源。`StageMapLoader` 会把 `.map` 解析成 `LevelDefinition`,C++ 白盒地图仅作为读取或解析失败时的 fallback。详细复盘见 `../engine_capability_audit.md`
第一轮引擎能力接入已完成:`GameServices` 统一接入音频、PVF、NPK、SoundPack、AudioDB、SaveSystem 状态;`StageMapResource` 已能读取 `map/stage_01.map`当前仍使用白盒 C++ 地图作为 fallback。完整骨架说明见 `../game_skeleton.md`
第一轮引擎能力接入已完成:`GameServices` 统一接入音频、PVF、NPK、SoundPack、AudioDB、SaveSystem 状态;`StageMapResource` 负责从 Asset/PVF 读取 `map/stage_01.map``StageMapLoader` 负责转成游戏层 `LevelDefinition`。完整骨架说明见 `../game_skeleton.md`
### 下一轮建议任务
继续沿着“优先使用引擎能力”的方向,把地图从色块灰盒推进到可放素材的完整骨架:
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`
### Debug 快捷键
+17 -2
View File
@@ -5,13 +5,15 @@
## 验收范围
- 场景入口:`WhiteboxScene`
- 关卡数据:`CreateWhiteboxLevel()`
- 关卡数据:`game/assets/map/stage_01.map`
- 关卡 fallback`CreateWhiteboxLevel()` 内置 C++ 白盒数据
- 世界尺寸:`3200 x 720`
- 玩家出生点:`(160, 380)`
- 关卡边界:`0 -> 3200`
- 视口:`1280 x 720`
- Battle zones`zone_01_intro``zone_02_platform``zone_03_exit`
- 资源槽位:`game/assets/stage/stage_01/`
- 地图资源:`game/assets/map/stage_01.map`
## 构建检查
@@ -31,6 +33,12 @@ Start-Sleep -Seconds 3
if (!$p.HasExited) { Stop-Process -Id $p.Id -Force }
```
资源路径检查:
```powershell
Test-Path build\windows\x64\release\assets\map\stage_01.map
```
### Switch
```powershell
@@ -44,6 +52,12 @@ if (!$p.HasExited) { Stop-Process -Id $p.Id -Force }
build\switch\aarch64\release\switch_game\NSUnknownGame.nro
```
RomFS 地图检查:
```powershell
Test-Path build\switch\aarch64\release\switch_game\romfs\assets\map\stage_01.map
```
## 手动跑图检查
操作映射见 `controls.md`
@@ -66,7 +80,8 @@ build\switch\aarch64\release\switch_game\NSUnknownGame.nro
- Windows release 构建:通过。
- Windows 3 秒短启动:通过。
- Switch `Frostbite2DGameSwitch` 构建:通过。
- Switch RomFS:已包含 character、shader`stage_01` 资源槽位。
- Switch RomFS:已包含 character、shader`stage_01` 资源槽位`assets/map/stage_01.map`
- 地图加载:`stage_01.map` 已作为第一关主数据源;C++ 白盒数据仅作为 fallback。
- 手动跑图:未在本轮执行,等待后续素材/战斗区域进一步接入时复测。
## 后续补充
+396
View File
@@ -0,0 +1,396 @@
#include "stage_map_loader.h"
#include "stage_map_resource.h"
#include <SDL2/SDL.h>
#include <charconv>
#include <sstream>
namespace ns_game {
namespace {
struct ParseContext {
LevelDefinition level;
StageLayer* currentLayer = nullptr;
BattleZoneDefinition* currentBattleZone = nullptr;
bool hasMap = false;
bool hasWorld = false;
bool hasCamera = false;
bool hasPlayerSpawn = false;
};
std::optional<float> parseFloat(const std::string& value) {
float result = 0.0f;
const char* begin = value.data();
const char* end = value.data() + value.size();
const auto parsed = std::from_chars(begin, end, result);
if (parsed.ec != std::errc() || parsed.ptr != end) {
return std::nullopt;
}
return result;
}
std::optional<frostbite2D::Rect>
parseRect(const std::vector<std::string>& tokens, size_t start) {
if (tokens.size() < start + 4) {
return std::nullopt;
}
const auto x = parseFloat(tokens[start]);
const auto y = parseFloat(tokens[start + 1]);
const auto w = parseFloat(tokens[start + 2]);
const auto h = parseFloat(tokens[start + 3]);
if (!x || !y || !w || !h) {
return std::nullopt;
}
return frostbite2D::Rect(*x, *y, *w, *h);
}
std::optional<frostbite2D::Color>
parseColor(const std::vector<std::string>& tokens, size_t start) {
if (tokens.size() < start + 4) {
return std::nullopt;
}
const auto r = parseFloat(tokens[start]);
const auto g = parseFloat(tokens[start + 1]);
const auto b = parseFloat(tokens[start + 2]);
const auto a = parseFloat(tokens[start + 3]);
if (!r || !g || !b || !a) {
return std::nullopt;
}
return frostbite2D::Color(*r, *g, *b, *a);
}
std::optional<StageLayerRole> parseLayerRole(const std::string& value) {
if (value == "BackgroundFar") {
return StageLayerRole::BackgroundFar;
}
if (value == "BackgroundMid") {
return StageLayerRole::BackgroundMid;
}
if (value == "LevelVisual") {
return StageLayerRole::LevelVisual;
}
if (value == "PropsBack") {
return StageLayerRole::PropsBack;
}
if (value == "PropsFront") {
return StageLayerRole::PropsFront;
}
return std::nullopt;
}
std::vector<std::string> tokenize(const std::string& line) {
std::istringstream stream(line);
std::vector<std::string> tokens;
std::string token;
while (stream >> token) {
if (token.rfind("#", 0) == 0) {
break;
}
tokens.push_back(token);
}
return tokens;
}
bool warnLine(const std::string& path, size_t lineNumber,
const std::string& message) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "StageMapLoader: %s:%zu: %s",
path.c_str(), lineNumber, message.c_str());
return false;
}
bool ensureNoOpenBlock(const ParseContext& context, const std::string& path,
size_t lineNumber) {
if (context.currentLayer) {
return warnLine(path, lineNumber,
"expected endlayer before starting this block");
}
if (context.currentBattleZone) {
return warnLine(path, lineNumber,
"expected endbattle_zone before starting this block");
}
return true;
}
bool parseLine(ParseContext& context, const std::string& path,
size_t lineNumber, const std::vector<std::string>& tokens) {
const std::string& command = tokens[0];
if (command == "map") {
if (!ensureNoOpenBlock(context, path, lineNumber) || tokens.size() != 2) {
return warnLine(path, lineNumber, "expected: map <id>");
}
context.hasMap = true;
return true;
}
if (command == "world") {
if (!ensureNoOpenBlock(context, path, lineNumber) || tokens.size() != 3) {
return warnLine(path, lineNumber, "expected: world <width> <height>");
}
const auto width = parseFloat(tokens[1]);
const auto height = parseFloat(tokens[2]);
if (!width || !height || *width <= 0.0f || *height <= 0.0f) {
return warnLine(path, lineNumber, "invalid world size");
}
context.level.worldWidth = *width;
context.level.worldHeight = *height;
context.level.collision.levelLeft = 0.0f;
context.level.collision.levelRight = *width;
context.hasWorld = true;
return true;
}
if (command == "camera") {
if (!ensureNoOpenBlock(context, path, lineNumber) || tokens.size() != 5) {
return warnLine(path, lineNumber,
"expected: camera <x> <y> <width> <height>");
}
const auto rect = parseRect(tokens, 1);
if (!rect) {
return warnLine(path, lineNumber, "invalid camera rect");
}
context.level.cameraBounds = *rect;
context.hasCamera = true;
return true;
}
if (command == "player_spawn") {
if (!ensureNoOpenBlock(context, path, lineNumber) || tokens.size() != 3) {
return warnLine(path, lineNumber, "expected: player_spawn <x> <y>");
}
const auto x = parseFloat(tokens[1]);
const auto y = parseFloat(tokens[2]);
if (!x || !y) {
return warnLine(path, lineNumber, "invalid player spawn");
}
context.level.playerSpawn = frostbite2D::Vec2(*x, *y);
context.hasPlayerSpawn = true;
return true;
}
if (command == "collision") {
if (!ensureNoOpenBlock(context, path, lineNumber) || tokens.size() != 5) {
return warnLine(path, lineNumber,
"expected: collision <x> <y> <width> <height>");
}
const auto rect = parseRect(tokens, 1);
if (!rect) {
return warnLine(path, lineNumber, "invalid collision rect");
}
context.level.collision.platforms.push_back(*rect);
return true;
}
if (command == "layer") {
if (!ensureNoOpenBlock(context, path, lineNumber) || tokens.size() != 4) {
return warnLine(path, lineNumber,
"expected: layer <id> <role> <parallax>");
}
const auto role = parseLayerRole(tokens[2]);
const auto parallax = parseFloat(tokens[3]);
if (!role || !parallax) {
return warnLine(path, lineNumber, "invalid layer role or parallax");
}
context.level.layers.push_back({tokens[1], *role, *parallax, {}});
context.currentLayer = &context.level.layers.back();
return true;
}
if (command == "endlayer") {
if (!context.currentLayer || tokens.size() != 1) {
return warnLine(path, lineNumber, "unexpected endlayer");
}
context.currentLayer = nullptr;
return true;
}
if (command == "rect") {
if (!context.currentLayer || tokens.size() != 9) {
return warnLine(path, lineNumber,
"expected in layer: rect <x> <y> <w> <h> <r> <g> <b> <a>");
}
const auto rect = parseRect(tokens, 1);
const auto color = parseColor(tokens, 5);
if (!rect || !color) {
return warnLine(path, lineNumber, "invalid layer rect");
}
context.currentLayer->rects.push_back(
{*rect, *color, context.currentLayer->role, {}});
return true;
}
if (command == "rect_sprite" || command == "rect_sprite_src") {
if (!context.currentLayer) {
return warnLine(path, lineNumber,
"sprite rect must be declared inside a layer");
}
const bool hasSourceRect = command == "rect_sprite_src";
if ((!hasSourceRect && tokens.size() != 10) ||
(hasSourceRect && tokens.size() != 14)) {
return warnLine(path, lineNumber,
hasSourceRect
? "expected: rect_sprite_src <x> <y> <w> <h> <r> <g> <b> <a> <texture> <sx> <sy> <sw> <sh>"
: "expected: rect_sprite <x> <y> <w> <h> <r> <g> <b> <a> <texture>");
}
const auto rect = parseRect(tokens, 1);
const auto color = parseColor(tokens, 5);
if (!rect || !color) {
return warnLine(path, lineNumber, "invalid sprite rect");
}
StageLayerSource source;
source.texturePath = tokens[9];
source.tint = *color;
if (hasSourceRect) {
const auto sourceRect = parseRect(tokens, 10);
if (!sourceRect) {
return warnLine(path, lineNumber, "invalid sprite source rect");
}
source.sourceRect = *sourceRect;
source.useSourceRect = true;
}
context.currentLayer->rects.push_back(
{*rect, *color, context.currentLayer->role, source});
return true;
}
if (command == "battle_zone") {
if (!ensureNoOpenBlock(context, path, lineNumber) || tokens.size() != 10) {
return warnLine(path, lineNumber,
"expected: battle_zone <id> <tx> <ty> <tw> <th> <cx> <cy> <cw> <ch>");
}
const auto trigger = parseRect(tokens, 2);
const auto camera = parseRect(tokens, 6);
if (!trigger || !camera) {
return warnLine(path, lineNumber, "invalid battle zone rect");
}
context.level.battleZones.push_back({tokens[1], *trigger, *camera, {}});
context.currentBattleZone = &context.level.battleZones.back();
return true;
}
if (command == "spawn") {
if (!context.currentBattleZone || tokens.size() != 4) {
return warnLine(path, lineNumber,
"expected in battle_zone: spawn <id> <x> <y>");
}
const auto x = parseFloat(tokens[2]);
const auto y = parseFloat(tokens[3]);
if (!x || !y) {
return warnLine(path, lineNumber, "invalid spawn point");
}
context.currentBattleZone->enemySpawns.push_back(
{tokens[1], frostbite2D::Vec2(*x, *y)});
return true;
}
if (command == "endbattle_zone") {
if (!context.currentBattleZone || tokens.size() != 1) {
return warnLine(path, lineNumber, "unexpected endbattle_zone");
}
context.currentBattleZone = nullptr;
return true;
}
return warnLine(path, lineNumber, "unknown command: " + command);
}
std::optional<LevelDefinition> parseMapText(const StageMapResource& resource) {
ParseContext context;
std::istringstream stream(resource.text);
std::string line;
size_t lineNumber = 0;
while (std::getline(stream, line)) {
++lineNumber;
const std::vector<std::string> tokens = tokenize(line);
if (tokens.empty()) {
continue;
}
if (!parseLine(context, resource.path, lineNumber, tokens)) {
return std::nullopt;
}
}
if (context.currentLayer) {
warnLine(resource.path, lineNumber, "missing endlayer at end of file");
return std::nullopt;
}
if (context.currentBattleZone) {
warnLine(resource.path, lineNumber,
"missing endbattle_zone at end of file");
return std::nullopt;
}
if (!context.hasMap || !context.hasWorld || !context.hasPlayerSpawn) {
warnLine(resource.path, lineNumber,
"map requires map, world and player_spawn commands");
return std::nullopt;
}
if (!context.hasCamera) {
context.level.cameraBounds = frostbite2D::Rect(
0.0f, 0.0f, context.level.worldWidth, context.level.worldHeight);
}
if (context.level.collision.platforms.empty()) {
warnLine(resource.path, lineNumber, "map has no collision platforms");
return std::nullopt;
}
if (context.level.layers.empty()) {
warnLine(resource.path, lineNumber, "map has no visual layers");
return std::nullopt;
}
return context.level;
}
const char* sourceName(StageMapResourceSource source) {
switch (source) {
case StageMapResourceSource::AssetText:
return "asset";
case StageMapResourceSource::PvfText:
return "pvf-text";
case StageMapResourceSource::PvfBinaryScript:
return "pvf-script";
case StageMapResourceSource::None:
return "none";
}
return "unknown";
}
} // namespace
std::optional<LevelDefinition> LoadStageMap(const std::string& path) {
const std::optional<StageMapResource> resource = LoadStageMapResource(path);
if (!resource) {
return std::nullopt;
}
if (resource->text.empty()) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"StageMapLoader: %s loaded from %s but has no text map body",
resource->path.c_str(), sourceName(resource->source));
return std::nullopt;
}
std::optional<LevelDefinition> level = parseMapText(*resource);
if (!level) {
return std::nullopt;
}
SDL_Log("StageMapLoader: loaded %s from %s (%zu platforms, %zu layers, %zu zones)",
resource->path.c_str(), sourceName(resource->source),
level->collision.platforms.size(), level->layers.size(),
level->battleZones.size());
return level;
}
} // namespace ns_game
+12
View File
@@ -0,0 +1,12 @@
#pragma once
#include "level_definition.h"
#include <optional>
#include <string>
namespace ns_game {
std::optional<LevelDefinition> LoadStageMap(const std::string& path);
} // namespace ns_game
+15 -11
View File
@@ -1,21 +1,13 @@
#include "level_definition.h"
#include "stage_map_resource.h"
#include "stage_map_loader.h"
#include <SDL2/SDL.h>
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());
}
namespace {
LevelDefinition CreateFallbackWhiteboxLevel() {
LevelDefinition level;
level.worldWidth = 3200.0f;
level.worldHeight = 720.0f;
@@ -146,4 +138,16 @@ LevelDefinition CreateWhiteboxLevel() {
return level;
}
} // namespace
LevelDefinition CreateWhiteboxLevel() {
if (auto loadedLevel = LoadStageMap("map/stage_01.map")) {
return *loadedLevel;
}
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"CreateWhiteboxLevel: using C++ fallback level");
return CreateFallbackWhiteboxLevel();
}
} // namespace ns_game