Load stage map into level definition
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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` 和场景计划。
|
||||
|
||||
|
||||
@@ -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`。
|
||||
|
||||
## 原则
|
||||
|
||||
@@ -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 快捷键
|
||||
|
||||
|
||||
@@ -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。
|
||||
- 手动跑图:未在本轮执行,等待后续素材/战斗区域进一步接入时复测。
|
||||
|
||||
## 后续补充
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user