From 46c76ec7fc6ab2fbcf437e3b0814bc4a11717e0e Mon Sep 17 00:00:00 2001 From: SepComet <2428390463@qq.com> Date: Thu, 11 Jun 2026 11:33:36 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20LightGame=20=E5=A4=9A?= =?UTF-8?q?=E9=A1=B9=20gameplay=20bug=20=E5=B9=B6=E5=AE=8C=E5=96=84?= =?UTF-8?q?=E5=85=89=E6=95=8F=E6=B8=B2=E6=9F=93=E4=BD=93=E7=B3=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 出生点校验:load_level() 加载后 assert 验证 spawn point 是否站在固体地面且不穿墙 - Checkpoint 系统:LevelLoader 为 Checkpoint Trigger 注册 checkpoint 位置,check_triggers() 正确激活 checkpoint,respawn() 使用最后激活的 checkpoint - Death 触发器:PlayerController 新增 kill(),TriggerAction::Death 正确触发死亡 - Tilemap 光敏 tile:新增 TileLightRule 与 mutable tile buffer,LightEffectSystem::update_tilemap() 按光照条件动态显示/隐藏 tilemap 中的 tile - GameObject 渲染:LevelLoader 为所有对象分配 tile atlas sprite(金币、门、陷阱、平台、checkpoint 等不再隐形) - 光敏对象双状态:LightPlatform / ShadowPlatform / Door 均支持 on/off(或 open/closed)两种 sprite 切换,should_render() 始终返回 true,视觉状态由 sprite 承担 - 渲染层级:LevelRenderer 最后绘制玩家,确保玩家始终在最上层 --- src/Apps/LightGame/src/engine/Level.cpp | 16 +++++ src/Apps/LightGame/src/engine/Level.h | 12 ++++ src/Apps/LightGame/src/engine/LevelData.h | 11 ++++ src/Apps/LightGame/src/engine/LevelLoader.cpp | 26 ++++++++ .../LightGame/src/engine/LevelRenderer.cpp | 30 +++++++--- .../LightGame/src/engine/LightGameApp.cpp | 53 +++++++++++++++- .../LightGame/src/engine/PlayerController.cpp | 9 +++ .../LightGame/src/engine/PlayerController.h | 1 + src/Apps/LightGame/src/levels/Level1Data.h | 2 + src/Apps/LightGame/src/levels/Level2Data.h | 2 + src/Apps/LightGame/src/levels/Level3Data.h | 2 + .../src/systems/LightEffectSystem.cpp | 60 +++++++++++++++++-- .../LightGame/src/systems/LightEffectSystem.h | 4 +- 13 files changed, 215 insertions(+), 13 deletions(-) diff --git a/src/Apps/LightGame/src/engine/Level.cpp b/src/Apps/LightGame/src/engine/Level.cpp index 891d790..651f1e6 100644 --- a/src/Apps/LightGame/src/engine/Level.cpp +++ b/src/Apps/LightGame/src/engine/Level.cpp @@ -135,4 +135,20 @@ namespace LightGame } return spawn_point_; } + + void Level::init_tile_buffer(const uint16_t* source, int32_t count) + { + if (count <= 0 || source == nullptr) + { + return; + } + original_tiles_.resize(count); + tile_buffer_.resize(count); + for (int32_t i = 0; i < count; ++i) + { + original_tiles_[i] = source[i]; + tile_buffer_[i] = source[i]; + } + tilemap_.tiles = tile_buffer_.data(); + } } diff --git a/src/Apps/LightGame/src/engine/Level.h b/src/Apps/LightGame/src/engine/Level.h index e92772b..c6f2255 100644 --- a/src/Apps/LightGame/src/engine/Level.h +++ b/src/Apps/LightGame/src/engine/Level.h @@ -4,6 +4,7 @@ #include #include "GameObject.h" #include "Tilemap.h" +#include "LevelData.h" namespace LightGame { @@ -33,6 +34,9 @@ namespace LightGame private: std::vector objects_; std::vector checkpoints_; + std::vector original_tiles_; + std::vector tile_buffer_; + std::vector tile_light_rules_; RenderData::Tilemap tilemap_; LevelBounds bounds_; Math::Vector2Int spawn_point_; @@ -55,6 +59,13 @@ namespace LightGame void set_tilemap(const RenderData::Tilemap& tilemap) { tilemap_ = tilemap; } const RenderData::Tilemap& get_tilemap() const { return tilemap_; } + void init_tile_buffer(const uint16_t* source, int32_t count); + uint16_t* get_mutable_tiles() { return tile_buffer_.empty() ? nullptr : tile_buffer_.data(); } + const uint16_t* get_original_tiles() const { return original_tiles_.empty() ? nullptr : original_tiles_.data(); } + + void add_tile_light_rule(const TileLightRule& rule) { tile_light_rules_.push_back(rule); } + const std::vector& get_tile_light_rules() const { return tile_light_rules_; } + void set_bounds(const LevelBounds& bounds) { bounds_ = bounds; } const LevelBounds& get_bounds() const { return bounds_; } @@ -62,6 +73,7 @@ namespace LightGame const Math::Vector2Int& get_spawn_point() const { return spawn_point_; } void add_checkpoint(const Checkpoint& cp); + std::vector& get_checkpoints() { return checkpoints_; } const std::vector& get_checkpoints() const { return checkpoints_; } Math::Vector2Int get_active_checkpoint() const; diff --git a/src/Apps/LightGame/src/engine/LevelData.h b/src/Apps/LightGame/src/engine/LevelData.h index c69e806..6e37a84 100644 --- a/src/Apps/LightGame/src/engine/LevelData.h +++ b/src/Apps/LightGame/src/engine/LevelData.h @@ -20,6 +20,14 @@ namespace LightGame uint32_t trigger_target; }; + struct TileLightRule + { + int32_t tile_x; + int32_t tile_y; + uint16_t light_min; + uint16_t light_max; + }; + struct LevelData { const uint16_t* tiles; @@ -32,6 +40,9 @@ namespace LightGame const ObjectSpawn* spawns; int32_t spawn_count; + const TileLightRule* tile_light_rules; + int32_t tile_light_rule_count; + Math::Vector2Int spawn_point; int32_t bounds_min_x; int32_t bounds_min_y; diff --git a/src/Apps/LightGame/src/engine/LevelLoader.cpp b/src/Apps/LightGame/src/engine/LevelLoader.cpp index 1eb9eb0..33ebd92 100644 --- a/src/Apps/LightGame/src/engine/LevelLoader.cpp +++ b/src/Apps/LightGame/src/engine/LevelLoader.cpp @@ -1,5 +1,6 @@ #include "LevelLoader.h" #include "Sprite.h" +#include "tile_atlas.h" namespace LightGame { @@ -12,6 +13,14 @@ namespace LightGame const int32_t row = tile_id / atlas_columns; return RenderData::Sprite(atlas, col * tile_size, row * tile_size, tile_size, tile_size); } + + static const RenderData::Sprite spr_platform = MakeTileSprite(&tile_atlas_image, 3, 32, 4); + static const RenderData::Sprite spr_spike = MakeTileSprite(&tile_atlas_image, 4, 32, 4); + static const RenderData::Sprite spr_light_platform_on = MakeTileSprite(&tile_atlas_image, 6, 32, 4); + static const RenderData::Sprite spr_shadow_platform_on = MakeTileSprite(&tile_atlas_image, 8, 32, 4); + static const RenderData::Sprite spr_door_closed = MakeTileSprite(&tile_atlas_image, 9, 32, 4); + static const RenderData::Sprite spr_coin = MakeTileSprite(&tile_atlas_image, 11, 32, 4); + static const RenderData::Sprite spr_checkpoint = MakeTileSprite(&tile_atlas_image, 13, 32, 4); } void LevelLoader::load(Level& level, const LevelData& data) @@ -25,6 +34,7 @@ namespace LightGame tilemap.atlas = data.atlas; tilemap.atlas_columns = data.atlas_columns; level.set_tilemap(tilemap); + level.init_tile_buffer(data.tiles, data.map_width * data.map_height); level.set_bounds(LevelBounds( data.bounds_min_x, @@ -53,6 +63,7 @@ namespace LightGame obj.collider = RenderData::BoundingBox2D( Math::Vector2Int(0, 0), Math::Vector2Int(ts, ts)); obj.solid = true; + obj.sprite = &spr_platform; break; case GameObjectType::LightPlatform: @@ -60,6 +71,7 @@ namespace LightGame Math::Vector2Int(0, 0), Math::Vector2Int(ts, ts)); obj.solid = false; obj.light_threshold = LightThreshold(spawn.light_min, spawn.light_max); + obj.sprite = &spr_light_platform_on; break; case GameObjectType::ShadowPlatform: @@ -67,6 +79,7 @@ namespace LightGame Math::Vector2Int(0, 0), Math::Vector2Int(ts, ts)); obj.solid = true; obj.light_threshold = LightThreshold(spawn.light_min, spawn.light_max); + obj.sprite = &spr_shadow_platform_on; break; case GameObjectType::Door: @@ -74,6 +87,7 @@ namespace LightGame Math::Vector2Int(0, 0), Math::Vector2Int(ts, ts * 2)); obj.solid = true; obj.light_threshold = LightThreshold(spawn.light_min, spawn.light_max); + obj.sprite = &spr_door_closed; break; case GameObjectType::Hazard: @@ -81,6 +95,7 @@ namespace LightGame Math::Vector2Int(0, ts / 2), Math::Vector2Int(ts, ts)); obj.solid = false; + obj.sprite = &spr_spike; break; case GameObjectType::Collectible: @@ -88,6 +103,7 @@ namespace LightGame Math::Vector2Int(ts / 4, ts / 4), Math::Vector2Int(ts * 3 / 4, ts * 3 / 4)); obj.solid = false; + obj.sprite = &spr_coin; break; case GameObjectType::Trigger: @@ -96,6 +112,11 @@ namespace LightGame obj.solid = false; obj.trigger_action = spawn.trigger_action; obj.trigger_target_id = spawn.trigger_target; + if (spawn.trigger_action == TriggerAction::Checkpoint) + { + level.add_checkpoint(Checkpoint(obj.position.x, obj.position.y)); + obj.sprite = &spr_checkpoint; + } break; case GameObjectType::Player: @@ -110,5 +131,10 @@ namespace LightGame level.add_object(obj); } + + for (int32_t i = 0; i < data.tile_light_rule_count; ++i) + { + level.add_tile_light_rule(data.tile_light_rules[i]); + } } } diff --git a/src/Apps/LightGame/src/engine/LevelRenderer.cpp b/src/Apps/LightGame/src/engine/LevelRenderer.cpp index c62e161..571b0fc 100644 --- a/src/Apps/LightGame/src/engine/LevelRenderer.cpp +++ b/src/Apps/LightGame/src/engine/LevelRenderer.cpp @@ -17,18 +17,18 @@ namespace LightGame } const auto& objects = level.get_all_objects(); - for (size_t i = 0; i < objects.size(); ++i) + + auto try_draw = [&](const GameObject& obj) { - const GameObject& obj = objects[i]; if (!obj.active) { - continue; + return; } const RenderData::Sprite* sprite = obj.sprite; if (!sprite) { - continue; + return; } const int32_t screen_x = obj.position.x - camera.get_x(); @@ -37,19 +37,35 @@ namespace LightGame if (screen_x + sprite->width < 0 || screen_x > camera.get_screen_width() || screen_y + sprite->height < 0 || screen_y > camera.get_screen_height()) { - continue; + return; } const bool is_light_obj = (obj.type == GameObjectType::LightPlatform || obj.type == GameObjectType::ShadowPlatform || obj.type == GameObjectType::Door); - if (is_light_obj && !light_system.is_platform_active(obj, light_level)) + if (is_light_obj && !light_system.should_render(obj, light_level)) { - continue; + return; } ctx.draw_sprite_ex(screen_x, screen_y, *sprite, 1, obj.flip_h, false); + }; + + for (size_t i = 0; i < objects.size(); ++i) + { + if (objects[i].type != GameObjectType::Player) + { + try_draw(objects[i]); + } + } + + for (size_t i = 0; i < objects.size(); ++i) + { + if (objects[i].type == GameObjectType::Player) + { + try_draw(objects[i]); + } } } diff --git a/src/Apps/LightGame/src/engine/LightGameApp.cpp b/src/Apps/LightGame/src/engine/LightGameApp.cpp index 18b58bf..5d6f7d7 100644 --- a/src/Apps/LightGame/src/engine/LightGameApp.cpp +++ b/src/Apps/LightGame/src/engine/LightGameApp.cpp @@ -10,6 +10,8 @@ #include "Level2Data.h" #include "Level3Data.h" #include "sprite_atlas.h" +#include "Tilemap.h" +#include namespace LightGame { @@ -156,6 +158,40 @@ namespace LightGame level_ = Level(); LevelLoader::load(level_, *kLevels[level_index]); + // Validate spawn point: must be on solid ground and not inside solid tiles. + { + GameObject temp_player; + temp_player.position = level_.get_spawn_point(); + temp_player.collider = RenderData::BoundingBox2D( + Math::Vector2Int(8, 0), Math::Vector2Int(24, 32)); + + assert(physics_.is_grounded(temp_player, level_) && + "Player spawn point is not on solid ground"); + + const RenderData::Tilemap& tilemap = level_.get_tilemap(); + if (tilemap.tile_w > 0) + { + const RenderData::BoundingBox2D world_box = temp_player.get_world_collider(); + const int32_t ts = tilemap.tile_w; + const int32_t tile_left = world_box.min.x / ts; + const int32_t tile_right = (world_box.max.x - 1) / ts; + const int32_t tile_top = world_box.min.y / ts; + const int32_t tile_bottom = (world_box.max.y - 1) / ts; + + for (int32_t ty = tile_top; ty <= tile_bottom; ++ty) + { + for (int32_t tx = tile_left; tx <= tile_right; ++tx) + { + if (tx < 0 || tx >= tilemap.width || ty < 0 || ty >= tilemap.height) + continue; + const uint16_t tile_id = tilemap.get_tile(tx, ty); + assert((tile_id == 0 || tile_id == RenderData::Tilemap::EmptyTile) && + "Player spawn point overlaps solid tiles"); + } + } + } + } + const auto players = level_.get_objects_by_type(GameObjectType::Player); if (!players.empty()) { @@ -187,6 +223,7 @@ namespace LightGame { const uint16_t light = manual_light_level_; light_system_.update(level_.get_all_objects(), light); + light_system_.update_tilemap(level_, light); state_manager_.get_hud().light_level = light; GameObject* player = level_.get_object(player_id_); @@ -268,11 +305,25 @@ namespace LightGame case TriggerAction::Checkpoint: { - const_cast(level_).get_checkpoints(); + const Math::Vector2Int trigger_pos = obj.position; + auto& checkpoints = level_.get_checkpoints(); + for (size_t j = 0; j < checkpoints.size(); ++j) + { + if (checkpoints[j].position.x == trigger_pos.x && + checkpoints[j].position.y == trigger_pos.y) + { + checkpoints[j].activated = true; + break; + } + } break; } case TriggerAction::Death: + if (!player_controller_.is_dead()) + { + player_controller_.kill(); + } break; default: diff --git a/src/Apps/LightGame/src/engine/PlayerController.cpp b/src/Apps/LightGame/src/engine/PlayerController.cpp index d27a2cd..982482f 100644 --- a/src/Apps/LightGame/src/engine/PlayerController.cpp +++ b/src/Apps/LightGame/src/engine/PlayerController.cpp @@ -257,6 +257,15 @@ namespace LightGame } } + void PlayerController::kill() + { + if (state_ != PlayerState::Dead) + { + state_ = PlayerState::Dead; + respawn_timer_ms_ = 0; + } + } + void PlayerController::respawn(GameObject& obj, Level& level) { const Math::Vector2Int spawn = level.get_active_checkpoint(); diff --git a/src/Apps/LightGame/src/engine/PlayerController.h b/src/Apps/LightGame/src/engine/PlayerController.h index 28abc72..0fe8584 100644 --- a/src/Apps/LightGame/src/engine/PlayerController.h +++ b/src/Apps/LightGame/src/engine/PlayerController.h @@ -78,6 +78,7 @@ namespace LightGame PlayerState get_state() const { return state_; } bool is_dead() const { return state_ == PlayerState::Dead; } + void kill(); private: void update_input(const Platform::IKeyboardState* keyboard, const Platform::IPointerInput* pointer, int32_t screen_width, int32_t screen_height); diff --git a/src/Apps/LightGame/src/levels/Level1Data.h b/src/Apps/LightGame/src/levels/Level1Data.h index a65f546..7e350e7 100644 --- a/src/Apps/LightGame/src/levels/Level1Data.h +++ b/src/Apps/LightGame/src/levels/Level1Data.h @@ -45,6 +45,8 @@ namespace LightGame tile_atlas_columns, spawns, 8, + nullptr, + 0, Math::Vector2Int(2 * kTileSize, 5 * kTileSize), 0, 0, diff --git a/src/Apps/LightGame/src/levels/Level2Data.h b/src/Apps/LightGame/src/levels/Level2Data.h index 68c1324..fcc30cb 100644 --- a/src/Apps/LightGame/src/levels/Level2Data.h +++ b/src/Apps/LightGame/src/levels/Level2Data.h @@ -55,6 +55,8 @@ namespace LightGame tile_atlas_columns, spawns, 15, + nullptr, + 0, Math::Vector2Int(2 * kTileSize, 5 * kTileSize), 0, 0, diff --git a/src/Apps/LightGame/src/levels/Level3Data.h b/src/Apps/LightGame/src/levels/Level3Data.h index 2ad88aa..7695fda 100644 --- a/src/Apps/LightGame/src/levels/Level3Data.h +++ b/src/Apps/LightGame/src/levels/Level3Data.h @@ -58,6 +58,8 @@ namespace LightGame tile_atlas_columns, spawns, 15, + nullptr, + 0, Math::Vector2Int(2 * kTileSize, 5 * kTileSize), 0, 0, diff --git a/src/Apps/LightGame/src/systems/LightEffectSystem.cpp b/src/Apps/LightGame/src/systems/LightEffectSystem.cpp index d0a6b2e..95f92da 100644 --- a/src/Apps/LightGame/src/systems/LightEffectSystem.cpp +++ b/src/Apps/LightGame/src/systems/LightEffectSystem.cpp @@ -1,7 +1,26 @@ #include "LightEffectSystem.h" +#include "tile_atlas.h" namespace LightGame { + namespace + { + static RenderData::Sprite MakeTileSprite(const RenderData::Image* atlas, int32_t tile_id, + int32_t tile_size, int32_t atlas_columns) + { + const int32_t col = tile_id % atlas_columns; + const int32_t row = tile_id / atlas_columns; + return RenderData::Sprite(atlas, col * tile_size, row * tile_size, tile_size, tile_size); + } + + static const RenderData::Sprite spr_light_platform_on = MakeTileSprite(&tile_atlas_image, 6, 32, 4); + static const RenderData::Sprite spr_light_platform_off = MakeTileSprite(&tile_atlas_image, 5, 32, 4); + static const RenderData::Sprite spr_shadow_platform_on = MakeTileSprite(&tile_atlas_image, 8, 32, 4); + static const RenderData::Sprite spr_shadow_platform_off = MakeTileSprite(&tile_atlas_image, 7, 32, 4); + static const RenderData::Sprite spr_door_closed = MakeTileSprite(&tile_atlas_image, 9, 32, 4); + static const RenderData::Sprite spr_door_open = MakeTileSprite(&tile_atlas_image, 10, 32, 4); + } + void LightEffectSystem::update(std::vector& objects, uint16_t light_level) { for (size_t i = 0; i < objects.size(); ++i) @@ -12,14 +31,17 @@ namespace LightGame { case GameObjectType::LightPlatform: obj.solid = (light_level >= obj.light_threshold.min_level); + obj.sprite = obj.solid ? &spr_light_platform_on : &spr_light_platform_off; break; case GameObjectType::ShadowPlatform: obj.solid = (light_level <= obj.light_threshold.max_level); + obj.sprite = obj.solid ? &spr_shadow_platform_on : &spr_shadow_platform_off; break; case GameObjectType::Door: obj.solid = !is_door_open(obj, light_level); + obj.sprite = obj.solid ? &spr_door_closed : &spr_door_open; break; default: @@ -28,15 +50,45 @@ namespace LightGame } } - bool LightEffectSystem::is_platform_active(const GameObject& obj, uint16_t light_level) + void LightEffectSystem::update_tilemap(Level& level, uint16_t light_level) { + const auto& rules = level.get_tile_light_rules(); + if (rules.empty()) + { + return; + } + + uint16_t* tiles = level.get_mutable_tiles(); + const uint16_t* original = level.get_original_tiles(); + if (!tiles || !original) + { + return; + } + + const RenderData::Tilemap& tilemap = level.get_tilemap(); + for (size_t i = 0; i < rules.size(); ++i) + { + const TileLightRule& rule = rules[i]; + if (rule.tile_x < 0 || rule.tile_x >= tilemap.width || + rule.tile_y < 0 || rule.tile_y >= tilemap.height) + { + continue; + } + const int32_t index = rule.tile_y * tilemap.width + rule.tile_x; + const bool active = (light_level >= rule.light_min && light_level <= rule.light_max); + tiles[index] = active ? original[index] : RenderData::Tilemap::EmptyTile; + } + } + + bool LightEffectSystem::should_render(const GameObject& obj, uint16_t light_level) + { + (void)light_level; switch (obj.type) { case GameObjectType::LightPlatform: - return light_level >= obj.light_threshold.min_level; - case GameObjectType::ShadowPlatform: - return light_level <= obj.light_threshold.max_level; + case GameObjectType::Door: + return true; default: return obj.solid; diff --git a/src/Apps/LightGame/src/systems/LightEffectSystem.h b/src/Apps/LightGame/src/systems/LightEffectSystem.h index a81e893..91ba5d5 100644 --- a/src/Apps/LightGame/src/systems/LightEffectSystem.h +++ b/src/Apps/LightGame/src/systems/LightEffectSystem.h @@ -3,6 +3,7 @@ #include #include #include "GameObject.h" +#include "Level.h" namespace LightGame { @@ -10,8 +11,9 @@ namespace LightGame { public: void update(std::vector& objects, uint16_t light_level); + void update_tilemap(Level& level, uint16_t light_level); - static bool is_platform_active(const GameObject& obj, uint16_t light_level); + static bool should_render(const GameObject& obj, uint16_t light_level); static bool is_door_open(const GameObject& obj, uint16_t light_level); }; }