diff --git a/game/assets/map/stage_01.map b/game/assets/map/stage_01.map index 42ae66f..2b6e000 100644 --- a/game/assets/map/stage_01.map +++ b/game/assets/map/stage_01.map @@ -12,6 +12,9 @@ collision 1760 500 300 28 collision 2180 455 260 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 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 @@ -27,14 +30,14 @@ 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 +tile_rect graybox_ground 0 600 320 8 0 0.62 0.68 0.73 +tile_rect graybox_platform 230 500 22 2 0 0.62 0.68 0.73 +tile_rect graybox_platform 560 430 26 2 0 0.62 0.68 0.73 +tile_rect graybox_platform 930 510 26 2 0 0.62 0.68 0.73 +tile_rect graybox_platform 1250 420 23 2 0 0.62 0.68 0.73 +tile_rect graybox_platform 1760 500 30 2 0 0.62 0.68 0.73 +tile_rect graybox_platform 2180 455 26 2 0 0.62 0.68 0.73 +tile_rect graybox_platform 2650 515 36 2 0 0.62 0.68 0.73 endlayer layer props_back PropsBack 1.0 diff --git a/game/assets/stage/stage_01/ASSETS.md b/game/assets/stage/stage_01/ASSETS.md index 063e64a..8c6b117 100644 --- a/game/assets/stage/stage_01/ASSETS.md +++ b/game/assets/stage/stage_01/ASSETS.md @@ -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 | | 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 | +| 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 | | Hit spark | `effects/hit_spark.png` | Small combat impact effect. | TBD | | Dust | `effects/dust.png` | Run, jump, and landing dust. | TBD | diff --git a/game/docs/modules/data_map.md b/game/docs/modules/data_map.md index 62b3abb..2815c5a 100644 --- a/game/docs/modules/data_map.md +++ b/game/docs/modules/data_map.md @@ -37,6 +37,7 @@ stage_01.map - 玩家出生点。 - 摄像机边界。 - 平台碰撞世界。 +- tileset 定义和 tile entry。 - 场景视觉层。 - battle zone。 - enemy spawn point。 @@ -49,18 +50,32 @@ world camera player_spawn collision +tileset layer rect rect_sprite rect_sprite_src +tile_rect endlayer battle_zone spawn 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。 -- 增加重复 block、空 layer、battle zone 无 spawn、tile 引用不存在等错误日志。 +- 把 `tile_rect` 扩展成可表达 tile row/column 或 tile id。 +- 增加空 layer、battle zone 无 spawn、tile texture 不存在等更严格错误日志。 - 保留 `whitebox_level.cpp` 作为 fallback,不再把主地图写死在 C++。 diff --git a/game/docs/modules/stage_rendering.md b/game/docs/modules/stage_rendering.md index 7784d88..5b6872c 100644 --- a/game/docs/modules/stage_rendering.md +++ b/game/docs/modules/stage_rendering.md @@ -31,6 +31,8 @@ UI - 支持色块占位。 - 支持普通 PNG texture path。 - 支持 source rect。 +- 支持 `.map` tile entry 绘制。 +- 支持 tileset 图片缺失时按 tile fallback 色绘制。 - 资源缺失时回退色块,不阻断场景运行。 ## 约束 @@ -41,6 +43,6 @@ UI ## 下一步 -- 增加 tile entry 绘制路径。 -- 把 `level_visual` 长色块逐步替换为 tile。 +- 给 tile entry 增加 tile id 或二维 source index。 +- 把正式 tileset 素材接入 `stage_01.map`。 - 如果引擎提供统一 Texture cache,再迁移当前局部贴图缓存。 diff --git a/game/docs/planning/scene_build_plan.md b/game/docs/planning/scene_build_plan.md index afb460e..e9ea7a4 100644 --- a/game/docs/planning/scene_build_plan.md +++ b/game/docs/planning/scene_build_plan.md @@ -286,11 +286,21 @@ game/assets/stage/stage_01/ 继续沿着“优先使用引擎能力”的方向,把地图从色块灰盒推进到可放素材的完整骨架。当前不进入敌人和战斗玩法,下一轮只处理地图视觉表达: -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、tile 引用不存在的提示。 -5. 补一个轻量 map 验收流程:启动时日志确认加载 `.map`,Windows/Switch 产物确认存在 `assets/map/stage_01.map` 和全局素材槽位。 +1. 已完成:给 `.map` 增加 `tileset` / `tile_rect` 约定,先不做编辑器,保证手写数据能表达重复地砖。 +2. 已完成:在 `StageRenderer` 中增加 tile 绘制路径,复用当前 `Texture` 加载和 source rect 支持。 +3. 已完成:把 `level_visual` 的长色块替换成 tile entries,碰撞仍独立保留在 `collision`。 +4. 已完成:给 map loader 增加重复 tileset、未知 tileset、非法 tile rect 等错误日志。 +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 快捷键 diff --git a/game/docs/scene_acceptance.md b/game/docs/scene_acceptance.md index 1e3a818..f328552 100644 --- a/game/docs/scene_acceptance.md +++ b/game/docs/scene_acceptance.md @@ -14,6 +14,7 @@ - Battle zones:`zone_01_intro`、`zone_02_platform`、`zone_03_exit` - 资源槽位:`game/assets/stage/stage_01/` - 地图资源:`game/assets/map/stage_01.map` +- Tile 数据:`stage_01.map` 中的 `tileset` / `tile_rect` - 全局资源槽位:`game/assets/ASSETS.md` - UI overlay:`GameUiOverlay`,有字体时使用 `TextSprite`,无字体时使用 fallback 色条。 - 音频 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 RomFS:已包含 character、shader、`stage_01` 资源槽位和 `assets/map/stage_01.map`。 - 地图加载:`stage_01.map` 已作为第一关主数据源;C++ 白盒数据仅作为 fallback。 +- 地图 tile 化:`level_visual` 已由 `tileset` / `tile_rect` 表达;tileset 图片缺失时使用 fallback 色绘制。 - 引擎能力接入:`Animation`/NPK/PNG 角色资源优先级、`UIScene`/`TextSprite` UI、`Sound`/`Music` cue、`SaveSystem` debug 存档已接入。 - 手动跑图:未在本轮执行,等待后续素材/战斗区域进一步接入时复测。 diff --git a/game/src/data/level_definition.h b/game/src/data/level_definition.h index 723210c..788f218 100644 --- a/game/src/data/level_definition.h +++ b/game/src/data/level_definition.h @@ -32,6 +32,21 @@ struct StageRect { 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 { std::string id; frostbite2D::Vec2 position; @@ -60,6 +75,7 @@ struct StageLayer { StageLayerRole role = StageLayerRole::LevelVisual; float parallax = 1.0f; std::vector rects; + std::vector tiles; }; struct LevelDefinition { @@ -68,6 +84,7 @@ struct LevelDefinition { frostbite2D::Vec2 playerSpawn; frostbite2D::Rect cameraBounds; PlatformWorld collision; + std::vector tileSets; std::vector layers; std::vector battleZones; }; diff --git a/game/src/data/stage_map_loader.cpp b/game/src/data/stage_map_loader.cpp index 4d0e98e..5e0014b 100644 --- a/game/src/data/stage_map_loader.cpp +++ b/game/src/data/stage_map_loader.cpp @@ -5,6 +5,7 @@ #include #include +#include #include namespace ns_game { @@ -66,6 +67,16 @@ parseColor(const std::vector& tokens, size_t start) { return frostbite2D::Color(*r, *g, *b, *a); } +std::optional +findTileSet(ParseContext& context, const std::string& id) { + for (TileSetDefinition& tileSet : context.level.tileSets) { + if (tileSet.id == id) { + return &tileSet; + } + } + return std::nullopt; +} + std::optional parseLayerRole(const std::string& value) { if (value == "BackgroundFar") { return StageLayerRole::BackgroundFar; @@ -188,6 +199,28 @@ bool parseLine(ParseContext& context, const std::string& path, return true; } + if (command == "tileset") { + if (!ensureNoOpenBlock(context, path, lineNumber) || tokens.size() != 9) { + return warnLine(path, lineNumber, + "expected: tileset "); + } + 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 (!ensureNoOpenBlock(context, path, lineNumber) || tokens.size() != 4) { return warnLine(path, lineNumber, @@ -226,6 +259,59 @@ bool parseLine(ParseContext& context, const std::string& path, return true; } + if (command == "tile_rect") { + if (!context.currentLayer || tokens.size() != 10) { + return warnLine(path, lineNumber, + "expected in layer: tile_rect "); + } + + 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(*columnsFloat); + const int rows = static_cast(*rowsFloat); + const int tileIndex = static_cast(*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(column) * definition.tileWidth, + *y + static_cast(row) * definition.tileHeight, + definition.tileWidth, definition.tileHeight); + const frostbite2D::Rect sourceRect( + static_cast(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 (!context.currentLayer) { return warnLine(path, lineNumber, @@ -386,9 +472,14 @@ std::optional LoadStageMap(const std::string& path) { 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), - level->collision.platforms.size(), level->layers.size(), + level->collision.platforms.size(), level->layers.size(), tileCount, level->battleZones.size()); return level; } diff --git a/game/src/stage/stage_renderer.cpp b/game/src/stage/stage_renderer.cpp index fa306d2..db9fe39 100644 --- a/game/src/stage/stage_renderer.cpp +++ b/game/src/stage/stage_renderer.cpp @@ -38,6 +38,16 @@ float worldWidthOf(const LevelDefinition& level) { 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 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)); } } + + 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, @@ -130,6 +147,26 @@ void StageRenderer::drawStageRect(const StageRect& rect, 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 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 StageRenderer::getTexture(const std::string& texturePath) const { const auto existing = textureCache_.find(texturePath); diff --git a/game/src/stage/stage_renderer.h b/game/src/stage/stage_renderer.h index ffc9552..2430436 100644 --- a/game/src/stage/stage_renderer.h +++ b/game/src/stage/stage_renderer.h @@ -20,6 +20,8 @@ private: void drawLayer(const LevelDefinition& level, const StageLayer& layer) const; void drawStageRect(const StageRect& rect, const frostbite2D::Rect& bounds) const; + void drawStageTile(const LevelDefinition& level, const StageTile& tile, + const frostbite2D::Rect& bounds) const; frostbite2D::Ptr getTexture(const std::string& texturePath) const;