Add stage map tile rendering path

This commit is contained in:
2026-06-09 23:32:47 +08:00
parent 0257c8058e
commit 51a3776494
10 changed files with 200 additions and 19 deletions
+11 -8
View File
@@ -12,6 +12,9 @@ collision 1760 500 300 28
collision 2180 455 260 28 collision 2180 455 260 28
collision 2650 515 360 28 collision 2650 515 360 28
tileset graybox_ground assets/stage/stage_01/tiles/graybox_ground.png 10 10 0.62 0.68 0.73 1.0
tileset graybox_platform assets/stage/stage_01/tiles/graybox_platform.png 10 14 0.62 0.68 0.73 1.0
layer background_far BackgroundFar 0.25 layer background_far BackgroundFar 0.25
rect 0 0 3200 720 0.08 0.10 0.14 1.0 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 360 120 420 220 0.13 0.16 0.21 1.0
@@ -27,14 +30,14 @@ rect 2860 280 260 320 0.18 0.22 0.27 1.0
endlayer endlayer
layer level_visual LevelVisual 1.0 layer level_visual LevelVisual 1.0
rect 0 600 3200 80 0.62 0.68 0.73 1.0 tile_rect graybox_ground 0 600 320 8 0 0.62 0.68 0.73
rect 230 500 220 28 0.62 0.68 0.73 1.0 tile_rect graybox_platform 230 500 22 2 0 0.62 0.68 0.73
rect 560 430 260 28 0.62 0.68 0.73 1.0 tile_rect graybox_platform 560 430 26 2 0 0.62 0.68 0.73
rect 930 510 260 28 0.62 0.68 0.73 1.0 tile_rect graybox_platform 930 510 26 2 0 0.62 0.68 0.73
rect 1250 420 230 28 0.62 0.68 0.73 1.0 tile_rect graybox_platform 1250 420 23 2 0 0.62 0.68 0.73
rect 1760 500 300 28 0.62 0.68 0.73 1.0 tile_rect graybox_platform 1760 500 30 2 0 0.62 0.68 0.73
rect 2180 455 260 28 0.62 0.68 0.73 1.0 tile_rect graybox_platform 2180 455 26 2 0 0.62 0.68 0.73
rect 2650 515 360 28 0.62 0.68 0.73 1.0 tile_rect graybox_platform 2650 515 36 2 0 0.62 0.68 0.73
endlayer endlayer
layer props_back PropsBack 1.0 layer props_back PropsBack 1.0
+2
View File
@@ -9,6 +9,8 @@ This directory is reserved for the first playable side-scrolling ACT stage.
| Far background | `background/background_far.png` | Large or horizontally tileable image. | TBD | | Far background | `background/background_far.png` | Large or horizontally tileable image. | TBD |
| Mid background | `background/background_mid.png` | Parallax midground image or prop sheet. | TBD | | Mid background | `background/background_mid.png` | Parallax midground image or prop sheet. | TBD |
| Tileset | `tiles/stage_01_tileset.png` | Ground, platform, wall, edge tiles. | TBD | | Tileset | `tiles/stage_01_tileset.png` | Ground, platform, wall, edge tiles. | TBD |
| Graybox ground tiles | `tiles/graybox_ground.png` | Optional temporary tileset referenced by `stage_01.map`; fallback color is used if missing. | TBD |
| Graybox platform tiles | `tiles/graybox_platform.png` | Optional temporary platform tileset referenced by `stage_01.map`; fallback color is used if missing. | TBD |
| Props sheet | `props/stage_01_props.png` | Crates, signs, lamps, blockers, exits. | TBD | | Props sheet | `props/stage_01_props.png` | Crates, signs, lamps, blockers, exits. | TBD |
| Hit spark | `effects/hit_spark.png` | Small combat impact effect. | TBD | | Hit spark | `effects/hit_spark.png` | Small combat impact effect. | TBD |
| Dust | `effects/dust.png` | Run, jump, and landing dust. | TBD | | Dust | `effects/dust.png` | Run, jump, and landing dust. | TBD |
+17 -2
View File
@@ -37,6 +37,7 @@ stage_01.map
- 玩家出生点。 - 玩家出生点。
- 摄像机边界。 - 摄像机边界。
- 平台碰撞世界。 - 平台碰撞世界。
- tileset 定义和 tile entry。
- 场景视觉层。 - 场景视觉层。
- battle zone。 - battle zone。
- enemy spawn point。 - enemy spawn point。
@@ -49,18 +50,32 @@ world <width> <height>
camera <x> <y> <width> <height> camera <x> <y> <width> <height>
player_spawn <x> <y> player_spawn <x> <y>
collision <x> <y> <width> <height> collision <x> <y> <width> <height>
tileset <id> <texture> <tile_w> <tile_h> <r> <g> <b> <a>
layer <id> <BackgroundFar|BackgroundMid|LevelVisual|PropsBack|PropsFront> <parallax> layer <id> <BackgroundFar|BackgroundMid|LevelVisual|PropsBack|PropsFront> <parallax>
rect <x> <y> <width> <height> <r> <g> <b> <a> rect <x> <y> <width> <height> <r> <g> <b> <a>
rect_sprite <x> <y> <width> <height> <r> <g> <b> <a> <texture> 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> rect_sprite_src <x> <y> <width> <height> <r> <g> <b> <a> <texture> <sx> <sy> <sw> <sh>
tile_rect <tileset> <x> <y> <columns> <rows> <tile_index> <r> <g> <b>
endlayer endlayer
battle_zone <id> <trigger_x> <trigger_y> <trigger_w> <trigger_h> <camera_x> <camera_y> <camera_w> <camera_h> battle_zone <id> <trigger_x> <trigger_y> <trigger_w> <trigger_h> <camera_x> <camera_y> <camera_w> <camera_h>
spawn <id> <x> <y> spawn <id> <x> <y>
endbattle_zone endbattle_zone
``` ```
## Tile 约定
`tileset` 定义 tile 图片来源、单格尺寸和 fallback 色。图片不存在时,`StageRenderer` 会按 tile fallback 色绘制,不阻断场景启动。
`tile_rect` 只能写在 `layer` 内,用于批量铺同一种 tile
- `columns` / `rows` 是重复格数。
- `tile_index` 当前表示 tileset 第一行的横向 tile 序号。
- `r g b` 是该批 tile 的灰盒 fallback 色,alpha 固定为 `1.0`
当前 `stage_01.map` 已把 `level_visual` 地面和平台从长色块替换为 `tile_rect`,碰撞仍由 `collision` 独立定义。
## 下一步 ## 下一步
- `.map` 增加 tileset/tile entry - `tile_rect` 扩展成可表达 tile row/column 或 tile id
- 增加重复 block、空 layer、battle zone 无 spawn、tile 引用不存在等错误日志。 - 增加空 layer、battle zone 无 spawn、tile texture 不存在等更严格错误日志。
- 保留 `whitebox_level.cpp` 作为 fallback,不再把主地图写死在 C++。 - 保留 `whitebox_level.cpp` 作为 fallback,不再把主地图写死在 C++。
+4 -2
View File
@@ -31,6 +31,8 @@ UI
- 支持色块占位。 - 支持色块占位。
- 支持普通 PNG texture path。 - 支持普通 PNG texture path。
- 支持 source rect。 - 支持 source rect。
- 支持 `.map` tile entry 绘制。
- 支持 tileset 图片缺失时按 tile fallback 色绘制。
- 资源缺失时回退色块,不阻断场景运行。 - 资源缺失时回退色块,不阻断场景运行。
## 约束 ## 约束
@@ -41,6 +43,6 @@ UI
## 下一步 ## 下一步
- 增加 tile entry 绘制路径 - tile entry 增加 tile id 或二维 source index
- `level_visual` 长色块逐步替换为 tile -正式 tileset 素材接入 `stage_01.map`
- 如果引擎提供统一 Texture cache,再迁移当前局部贴图缓存。 - 如果引擎提供统一 Texture cache,再迁移当前局部贴图缓存。
+15 -5
View File
@@ -286,11 +286,21 @@ game/assets/stage/stage_01/
继续沿着“优先使用引擎能力”的方向,把地图从色块灰盒推进到可放素材的完整骨架。当前不进入敌人和战斗玩法,下一轮只处理地图视觉表达: 继续沿着“优先使用引擎能力”的方向,把地图从色块灰盒推进到可放素材的完整骨架。当前不进入敌人和战斗玩法,下一轮只处理地图视觉表达:
1.`.map` 增加 tileset/tile entry 约定,先不做编辑器,保证工具或手写数据能表达重复地砖。 1. 已完成:`.map` 增加 `tileset` / `tile_rect` 约定,先不做编辑器,保证手写数据能表达重复地砖。
2.`StageRenderer` 中增加 tile 绘制路径,复用当前 `Texture` 加载和 source rect 支持。 2. 已完成:`StageRenderer` 中增加 tile 绘制路径,复用当前 `Texture` 加载和 source rect 支持。
3.`level_visual` 的长色块逐步替换成 tile entries,碰撞仍独立保留在 `collision` 3. 已完成:`level_visual` 的长色块替换成 tile entries,碰撞仍独立保留在 `collision`
4. 给 map loader 增加更严格的错误日志,例如重复 block、空 layer、battle zone 无 spawn、tile 引用不存在的提示 4. 已完成:给 map loader 增加重复 tileset、未知 tileset、非法 tile rect 等错误日志
5.一个轻量 map 验收流程:启动时日志确认加载 `.map`Windows/Switch 产物确认存在 `assets/map/stage_01.map` 和全局素材槽位。 5. 已完成:补轻量 map 验收流程:启动时日志确认加载 `.map`Windows/Switch 产物确认存在 `assets/map/stage_01.map` 和全局素材槽位。
### 下一轮建议任务
继续围绕地图视觉骨架,不进入战斗玩法:
1.`tile_rect` 增加二维 source index 或 tile id,避免只支持 tileset 第一行。
2. 按正式素材规格整理 `stage_01_tileset.png` 的格子约定。
3. 用一个临时 tileset PNG 替换当前 fallback 色块,验证 source rect 对齐。
4. 增加 tile texture 缺失的日志去重,避免大量 tile 重复报同一个资源。
5. 手动跑图确认所有平台视觉和碰撞误差不超过 4 像素。
### Debug 快捷键 ### Debug 快捷键
+2
View File
@@ -14,6 +14,7 @@
- Battle zones`zone_01_intro``zone_02_platform``zone_03_exit` - Battle zones`zone_01_intro``zone_02_platform``zone_03_exit`
- 资源槽位:`game/assets/stage/stage_01/` - 资源槽位:`game/assets/stage/stage_01/`
- 地图资源:`game/assets/map/stage_01.map` - 地图资源:`game/assets/map/stage_01.map`
- Tile 数据:`stage_01.map` 中的 `tileset` / `tile_rect`
- 全局资源槽位:`game/assets/ASSETS.md` - 全局资源槽位:`game/assets/ASSETS.md`
- UI overlay`GameUiOverlay`,有字体时使用 `TextSprite`,无字体时使用 fallback 色条。 - UI overlay`GameUiOverlay`,有字体时使用 `TextSprite`,无字体时使用 fallback 色条。
- 音频 cue`stage_01``player_jump``player_attack``battle_zone_enter` - 音频 cue`stage_01``player_jump``player_attack``battle_zone_enter`
@@ -91,6 +92,7 @@ Test-Path build\switch\aarch64\release\switch_game\romfs\assets\fonts
- Switch `Frostbite2DGameSwitch` 构建:通过。 - Switch `Frostbite2DGameSwitch` 构建:通过。
- Switch RomFS:已包含 character、shader、`stage_01` 资源槽位和 `assets/map/stage_01.map` - Switch RomFS:已包含 character、shader、`stage_01` 资源槽位和 `assets/map/stage_01.map`
- 地图加载:`stage_01.map` 已作为第一关主数据源;C++ 白盒数据仅作为 fallback。 - 地图加载:`stage_01.map` 已作为第一关主数据源;C++ 白盒数据仅作为 fallback。
- 地图 tile 化:`level_visual` 已由 `tileset` / `tile_rect` 表达;tileset 图片缺失时使用 fallback 色绘制。
- 引擎能力接入:`Animation`/NPK/PNG 角色资源优先级、`UIScene`/`TextSprite` UI、`Sound`/`Music` cue、`SaveSystem` debug 存档已接入。 - 引擎能力接入:`Animation`/NPK/PNG 角色资源优先级、`UIScene`/`TextSprite` UI、`Sound`/`Music` cue、`SaveSystem` debug 存档已接入。
- 手动跑图:未在本轮执行,等待后续素材/战斗区域进一步接入时复测。 - 手动跑图:未在本轮执行,等待后续素材/战斗区域进一步接入时复测。
+17
View File
@@ -32,6 +32,21 @@ struct StageRect {
StageLayerSource source; StageLayerSource source;
}; };
struct TileSetDefinition {
std::string id;
std::string texturePath;
float tileWidth = 0.0f;
float tileHeight = 0.0f;
frostbite2D::Color fallbackColor;
};
struct StageTile {
std::string tileSetId;
frostbite2D::Rect bounds;
frostbite2D::Rect sourceRect;
frostbite2D::Color fallbackColor;
};
struct SpawnPoint { struct SpawnPoint {
std::string id; std::string id;
frostbite2D::Vec2 position; frostbite2D::Vec2 position;
@@ -60,6 +75,7 @@ struct StageLayer {
StageLayerRole role = StageLayerRole::LevelVisual; StageLayerRole role = StageLayerRole::LevelVisual;
float parallax = 1.0f; float parallax = 1.0f;
std::vector<StageRect> rects; std::vector<StageRect> rects;
std::vector<StageTile> tiles;
}; };
struct LevelDefinition { struct LevelDefinition {
@@ -68,6 +84,7 @@ struct LevelDefinition {
frostbite2D::Vec2 playerSpawn; frostbite2D::Vec2 playerSpawn;
frostbite2D::Rect cameraBounds; frostbite2D::Rect cameraBounds;
PlatformWorld collision; PlatformWorld collision;
std::vector<TileSetDefinition> tileSets;
std::vector<StageLayer> layers; std::vector<StageLayer> layers;
std::vector<BattleZoneDefinition> battleZones; std::vector<BattleZoneDefinition> battleZones;
}; };
+93 -2
View File
@@ -5,6 +5,7 @@
#include <SDL2/SDL.h> #include <SDL2/SDL.h>
#include <charconv> #include <charconv>
#include <cmath>
#include <sstream> #include <sstream>
namespace ns_game { namespace ns_game {
@@ -66,6 +67,16 @@ parseColor(const std::vector<std::string>& tokens, size_t start) {
return frostbite2D::Color(*r, *g, *b, *a); return frostbite2D::Color(*r, *g, *b, *a);
} }
std::optional<TileSetDefinition*>
findTileSet(ParseContext& context, const std::string& id) {
for (TileSetDefinition& tileSet : context.level.tileSets) {
if (tileSet.id == id) {
return &tileSet;
}
}
return std::nullopt;
}
std::optional<StageLayerRole> parseLayerRole(const std::string& value) { std::optional<StageLayerRole> parseLayerRole(const std::string& value) {
if (value == "BackgroundFar") { if (value == "BackgroundFar") {
return StageLayerRole::BackgroundFar; return StageLayerRole::BackgroundFar;
@@ -188,6 +199,28 @@ bool parseLine(ParseContext& context, const std::string& path,
return true; return true;
} }
if (command == "tileset") {
if (!ensureNoOpenBlock(context, path, lineNumber) || tokens.size() != 9) {
return warnLine(path, lineNumber,
"expected: tileset <id> <texture> <tile_w> <tile_h> <r> <g> <b> <a>");
}
if (findTileSet(context, tokens[1])) {
return warnLine(path, lineNumber, "duplicate tileset id: " + tokens[1]);
}
const auto tileWidth = parseFloat(tokens[3]);
const auto tileHeight = parseFloat(tokens[4]);
const auto fallbackColor = parseColor(tokens, 5);
if (!tileWidth || !tileHeight || *tileWidth <= 0.0f ||
*tileHeight <= 0.0f || !fallbackColor) {
return warnLine(path, lineNumber, "invalid tileset definition");
}
context.level.tileSets.push_back(
{tokens[1], tokens[2], *tileWidth, *tileHeight, *fallbackColor});
return true;
}
if (command == "layer") { if (command == "layer") {
if (!ensureNoOpenBlock(context, path, lineNumber) || tokens.size() != 4) { if (!ensureNoOpenBlock(context, path, lineNumber) || tokens.size() != 4) {
return warnLine(path, lineNumber, return warnLine(path, lineNumber,
@@ -226,6 +259,59 @@ bool parseLine(ParseContext& context, const std::string& path,
return true; return true;
} }
if (command == "tile_rect") {
if (!context.currentLayer || tokens.size() != 10) {
return warnLine(path, lineNumber,
"expected in layer: tile_rect <tileset> <x> <y> <columns> <rows> <tile_index> <r> <g> <b>");
}
const auto tileSet = findTileSet(context, tokens[1]);
if (!tileSet) {
return warnLine(path, lineNumber, "unknown tileset id: " + tokens[1]);
}
const auto x = parseFloat(tokens[2]);
const auto y = parseFloat(tokens[3]);
const auto columnsFloat = parseFloat(tokens[4]);
const auto rowsFloat = parseFloat(tokens[5]);
const auto tileIndexFloat = parseFloat(tokens[6]);
const auto r = parseFloat(tokens[7]);
const auto g = parseFloat(tokens[8]);
const auto b = parseFloat(tokens[9]);
if (!x || !y || !columnsFloat || !rowsFloat || !tileIndexFloat || !r ||
!g || !b) {
return warnLine(path, lineNumber, "invalid tile_rect values");
}
const int columns = static_cast<int>(*columnsFloat);
const int rows = static_cast<int>(*rowsFloat);
const int tileIndex = static_cast<int>(*tileIndexFloat);
if (columns <= 0 || rows <= 0 || tileIndex < 0 ||
std::floor(*columnsFloat) != *columnsFloat ||
std::floor(*rowsFloat) != *rowsFloat ||
std::floor(*tileIndexFloat) != *tileIndexFloat) {
return warnLine(path, lineNumber,
"tile_rect columns, rows and tile_index must be non-negative integers");
}
const TileSetDefinition& definition = **tileSet;
const frostbite2D::Color fallbackColor(*r, *g, *b, 1.0f);
for (int row = 0; row < rows; ++row) {
for (int column = 0; column < columns; ++column) {
const frostbite2D::Rect bounds(
*x + static_cast<float>(column) * definition.tileWidth,
*y + static_cast<float>(row) * definition.tileHeight,
definition.tileWidth, definition.tileHeight);
const frostbite2D::Rect sourceRect(
static_cast<float>(tileIndex) * definition.tileWidth, 0.0f,
definition.tileWidth, definition.tileHeight);
context.currentLayer->tiles.push_back(
{definition.id, bounds, sourceRect, fallbackColor});
}
}
return true;
}
if (command == "rect_sprite" || command == "rect_sprite_src") { if (command == "rect_sprite" || command == "rect_sprite_src") {
if (!context.currentLayer) { if (!context.currentLayer) {
return warnLine(path, lineNumber, return warnLine(path, lineNumber,
@@ -386,9 +472,14 @@ std::optional<LevelDefinition> LoadStageMap(const std::string& path) {
return std::nullopt; return std::nullopt;
} }
SDL_Log("StageMapLoader: loaded %s from %s (%zu platforms, %zu layers, %zu zones)", size_t tileCount = 0;
for (const StageLayer& layer : level->layers) {
tileCount += layer.tiles.size();
}
SDL_Log("StageMapLoader: loaded %s from %s (%zu platforms, %zu layers, %zu tiles, %zu zones)",
resource->path.c_str(), sourceName(resource->source), resource->path.c_str(), sourceName(resource->source),
level->collision.platforms.size(), level->layers.size(), level->collision.platforms.size(), level->layers.size(), tileCount,
level->battleZones.size()); level->battleZones.size());
return level; return level;
} }
+37
View File
@@ -38,6 +38,16 @@ float worldWidthOf(const LevelDefinition& level) {
return level.collision.levelRight - level.collision.levelLeft; return level.collision.levelRight - level.collision.levelLeft;
} }
const TileSetDefinition* findTileSet(const LevelDefinition& level,
const std::string& id) {
for (const TileSetDefinition& tileSet : level.tileSets) {
if (tileSet.id == id) {
return &tileSet;
}
}
return nullptr;
}
} // namespace } // namespace
void StageRenderer::Render(const LevelDefinition& level, void StageRenderer::Render(const LevelDefinition& level,
@@ -102,6 +112,13 @@ void StageRenderer::drawLayer(const LevelDefinition& level,
frostbite2D::Color(0.91f, 0.95f, 0.98f, 1.0f)); frostbite2D::Color(0.91f, 0.95f, 0.98f, 1.0f));
} }
} }
for (const StageTile& tile : layer.tiles) {
frostbite2D::Rect bounds(tile.bounds.left() + parallaxOffset.x,
tile.bounds.top() + parallaxOffset.y,
tile.bounds.width(), tile.bounds.height());
drawStageTile(level, tile, bounds);
}
} }
void StageRenderer::drawStageRect(const StageRect& rect, void StageRenderer::drawStageRect(const StageRect& rect,
@@ -130,6 +147,26 @@ void StageRenderer::drawStageRect(const StageRect& rect,
texture, rect.source.tint); texture, rect.source.tint);
} }
void StageRenderer::drawStageTile(const LevelDefinition& level,
const StageTile& tile,
const frostbite2D::Rect& bounds) const {
const TileSetDefinition* tileSet = findTileSet(level, tile.tileSetId);
if (!tileSet || tileSet->texturePath.empty()) {
drawRect(bounds, tile.fallbackColor);
return;
}
frostbite2D::Ptr<frostbite2D::Texture> texture =
getTexture(tileSet->texturePath);
if (!texture) {
drawRect(bounds, tile.fallbackColor);
return;
}
drawTexture(bounds, tile.sourceRect, texture,
frostbite2D::Color(1.0f, 1.0f, 1.0f, 1.0f));
}
frostbite2D::Ptr<frostbite2D::Texture> frostbite2D::Ptr<frostbite2D::Texture>
StageRenderer::getTexture(const std::string& texturePath) const { StageRenderer::getTexture(const std::string& texturePath) const {
const auto existing = textureCache_.find(texturePath); const auto existing = textureCache_.find(texturePath);
+2
View File
@@ -20,6 +20,8 @@ private:
void drawLayer(const LevelDefinition& level, const StageLayer& layer) const; void drawLayer(const LevelDefinition& level, const StageLayer& layer) const;
void drawStageRect(const StageRect& rect, void drawStageRect(const StageRect& rect,
const frostbite2D::Rect& bounds) const; const frostbite2D::Rect& bounds) const;
void drawStageTile(const LevelDefinition& level, const StageTile& tile,
const frostbite2D::Rect& bounds) const;
frostbite2D::Ptr<frostbite2D::Texture> frostbite2D::Ptr<frostbite2D::Texture>
getTexture(const std::string& texturePath) const; getTexture(const std::string& texturePath) const;