修复 LightGame 多项 gameplay bug 并完善光敏渲染体系
- 出生点校验: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 最后绘制玩家,确保玩家始终在最上层
This commit is contained in:
parent
acf162d1b9
commit
46c76ec7fc
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
#include <vector>
|
||||
#include "GameObject.h"
|
||||
#include "Tilemap.h"
|
||||
#include "LevelData.h"
|
||||
|
||||
namespace LightGame
|
||||
{
|
||||
|
|
@ -33,6 +34,9 @@ namespace LightGame
|
|||
private:
|
||||
std::vector<GameObject> objects_;
|
||||
std::vector<Checkpoint> checkpoints_;
|
||||
std::vector<uint16_t> original_tiles_;
|
||||
std::vector<uint16_t> tile_buffer_;
|
||||
std::vector<TileLightRule> 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<TileLightRule>& 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<Checkpoint>& get_checkpoints() { return checkpoints_; }
|
||||
const std::vector<Checkpoint>& get_checkpoints() const { return checkpoints_; }
|
||||
Math::Vector2Int get_active_checkpoint() const;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@
|
|||
#include "Level2Data.h"
|
||||
#include "Level3Data.h"
|
||||
#include "sprite_atlas.h"
|
||||
#include "Tilemap.h"
|
||||
#include <cassert>
|
||||
|
||||
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&>(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:
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -45,6 +45,8 @@ namespace LightGame
|
|||
tile_atlas_columns,
|
||||
spawns,
|
||||
8,
|
||||
nullptr,
|
||||
0,
|
||||
Math::Vector2Int(2 * kTileSize, 5 * kTileSize),
|
||||
0,
|
||||
0,
|
||||
|
|
|
|||
|
|
@ -55,6 +55,8 @@ namespace LightGame
|
|||
tile_atlas_columns,
|
||||
spawns,
|
||||
15,
|
||||
nullptr,
|
||||
0,
|
||||
Math::Vector2Int(2 * kTileSize, 5 * kTileSize),
|
||||
0,
|
||||
0,
|
||||
|
|
|
|||
|
|
@ -58,6 +58,8 @@ namespace LightGame
|
|||
tile_atlas_columns,
|
||||
spawns,
|
||||
15,
|
||||
nullptr,
|
||||
0,
|
||||
Math::Vector2Int(2 * kTileSize, 5 * kTileSize),
|
||||
0,
|
||||
0,
|
||||
|
|
|
|||
|
|
@ -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<GameObject>& 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;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
#include <cstdint>
|
||||
#include <vector>
|
||||
#include "GameObject.h"
|
||||
#include "Level.h"
|
||||
|
||||
namespace LightGame
|
||||
{
|
||||
|
|
@ -10,8 +11,9 @@ namespace LightGame
|
|||
{
|
||||
public:
|
||||
void update(std::vector<GameObject>& 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);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue