修复 LightGame 玩家地面抖动、动画切换异常并完善 spike/终点视觉
## 物理与动画(核心修复) - PlayerController::update:地面状态下跳过 apply_gravity。之前每帧重力累入 velocity.y,sub_pixel_y_ 会缓慢累到 ≥1000 像素阈值,导致玩家下沉 1px 后被 collide_tilemap 推回,每隔几帧抖动一次。这是"动画机切不回 idle"的真正根因 —— 单帧 grounded=false 让状态切到 Falling,下一帧又回 Idle,视觉表现为 idle/fall 帧间歇错位。 - Physics2D::collide_tilemap:发生轴向推回时清掉对应 sub_pixel_x_/y_,防止小数残留在下一帧继续推进位置。 - Physics2D::is_grounded:feet 探测窗口(max.y..max.y+2)跨越的所有 tile 行都要扫描,而非只查 feet.min.y/ts 那一行。原写法在边界帧(feet.max.y 临时为 31n+0)会错把上一行空 tile 当判定行返回 false,触发上述的单帧 Falling 抖动。 - PlayerController::update:grounded && velocity.y >= 0 时无条件清零 velocity.y(不再要求 !was_grounded_),避免重力残留导致连续下沉。 ## Spike 行为 - Physics2D::is_tile_solid:spike(tile id 4) 改为非实体,玩家可踩入触发死亡。原先 spike 与普通地块都被当 solid,玩家停在 spike 顶部不会死。 - PlayerController::check_death:spike 击杀区域收紧到 tile 底部 14px(视觉高度),不再用整 32px tile 误判。 ## 关卡视觉(出生点 / 检查点 / 终点) - LevelLoader:玩家出生点附近生成 flag 装饰对象;Checkpoint 触发器初始显示 checkpoint 精灵,被激活后由 LightGameApp 在 check_triggers 中替换为 flag 精灵;LevelComplete 触发器使用 checkpoint 精灵作为终点。 - LevelLoader 暴露 flag_sprite() 静态访问器,供 LightGameApp 触发激活时复用同一份 sprite 引用。 ## 资源与配套 - 重新打包 tile_atlas(重新生成 generated/tile_atlas.h),更新 assets/tile/checkpoint.png、flag.png 的视觉素材。 - Level1Data、LevelEditor、LevelRenderer、GameObject、tests 跟随接口调整同步更新;game_engine_tests 重新编译通过。
This commit is contained in:
+22
-21
@@ -16,7 +16,7 @@ namespace
|
||||
{
|
||||
GameObject obj;
|
||||
assert(obj.id == 0);
|
||||
assert(obj.type == GameObjectType::StaticPlatform);
|
||||
assert(obj.type == GameObjectType::Player);
|
||||
assert(obj.position.x == 0);
|
||||
assert(obj.position.y == 0);
|
||||
assert(obj.velocity.x == 0);
|
||||
@@ -48,7 +48,7 @@ namespace
|
||||
assert(level.object_count() == 0);
|
||||
|
||||
GameObject platform;
|
||||
platform.type = GameObjectType::StaticPlatform;
|
||||
platform.type = GameObjectType::Collectible;
|
||||
platform.position = Math::Vector2Int(0, 400);
|
||||
uint32_t id1 = level.add_object(platform);
|
||||
assert(id1 == 1);
|
||||
@@ -63,7 +63,7 @@ namespace
|
||||
|
||||
const GameObject* found = level.get_object(id1);
|
||||
assert(found != nullptr);
|
||||
assert(found->type == GameObjectType::StaticPlatform);
|
||||
assert(found->type == GameObjectType::Collectible);
|
||||
assert(found->id == 1);
|
||||
|
||||
const GameObject* not_found = level.get_object(999);
|
||||
@@ -88,7 +88,7 @@ namespace
|
||||
Level level;
|
||||
|
||||
GameObject platform;
|
||||
platform.type = GameObjectType::StaticPlatform;
|
||||
platform.type = GameObjectType::Collectible;
|
||||
level.add_object(platform);
|
||||
|
||||
GameObject light_plat;
|
||||
@@ -99,15 +99,15 @@ namespace
|
||||
player.type = GameObjectType::Player;
|
||||
level.add_object(player);
|
||||
|
||||
auto platforms = level.get_objects_by_type(GameObjectType::StaticPlatform);
|
||||
auto platforms = level.get_objects_by_type(GameObjectType::Collectible);
|
||||
assert(platforms.size() == 1);
|
||||
assert(platforms[0]->type == GameObjectType::StaticPlatform);
|
||||
assert(platforms[0]->type == GameObjectType::Collectible);
|
||||
|
||||
auto players = level.get_objects_by_type(GameObjectType::Player);
|
||||
assert(players.size() == 1);
|
||||
|
||||
auto hazards = level.get_objects_by_type(GameObjectType::Hazard);
|
||||
assert(hazards.size() == 0);
|
||||
auto doors = level.get_objects_by_type(GameObjectType::Door);
|
||||
assert(doors.size() == 0);
|
||||
}
|
||||
|
||||
void TestLevelQueryRegion()
|
||||
@@ -115,14 +115,14 @@ namespace
|
||||
Level level;
|
||||
|
||||
GameObject obj1;
|
||||
obj1.type = GameObjectType::StaticPlatform;
|
||||
obj1.type = GameObjectType::Collectible;
|
||||
obj1.position = Math::Vector2Int(100, 100);
|
||||
obj1.collider = RenderData::BoundingBox2D(
|
||||
Math::Vector2Int(0, 0), Math::Vector2Int(32, 32));
|
||||
level.add_object(obj1);
|
||||
|
||||
GameObject obj2;
|
||||
obj2.type = GameObjectType::StaticPlatform;
|
||||
obj2.type = GameObjectType::Collectible;
|
||||
obj2.position = Math::Vector2Int(500, 500);
|
||||
obj2.collider = RenderData::BoundingBox2D(
|
||||
Math::Vector2Int(0, 0), Math::Vector2Int(32, 32));
|
||||
@@ -226,7 +226,7 @@ namespace
|
||||
level.set_bounds(LevelBounds(0, 0, 800, 600));
|
||||
|
||||
GameObject ground;
|
||||
ground.type = GameObjectType::StaticPlatform;
|
||||
ground.type = GameObjectType::Collectible;
|
||||
ground.position = Math::Vector2Int(0, 400);
|
||||
ground.collider = RenderData::BoundingBox2D(
|
||||
Math::Vector2Int(0, 0), Math::Vector2Int(800, 32));
|
||||
@@ -249,7 +249,8 @@ namespace
|
||||
void TestOneWayPlatform()
|
||||
{
|
||||
Level level;
|
||||
level.set_bounds(LevelBounds(0, 0, 800, 600));
|
||||
LevelBounds bounds(0, 0, 800, 600);
|
||||
level.set_bounds(bounds);
|
||||
|
||||
GameObject platform;
|
||||
platform.type = GameObjectType::LightPlatform;
|
||||
@@ -349,17 +350,17 @@ namespace
|
||||
{
|
||||
LightEffectSystem system;
|
||||
|
||||
GameObject platform;
|
||||
platform.type = GameObjectType::StaticPlatform;
|
||||
platform.solid = true;
|
||||
GameObject collectible;
|
||||
collectible.type = GameObjectType::Collectible;
|
||||
collectible.solid = true;
|
||||
|
||||
GameObject hazard;
|
||||
hazard.type = GameObjectType::Hazard;
|
||||
hazard.solid = true;
|
||||
GameObject trigger;
|
||||
trigger.type = GameObjectType::Trigger;
|
||||
trigger.solid = true;
|
||||
|
||||
std::vector<GameObject> objects;
|
||||
objects.push_back(platform);
|
||||
objects.push_back(hazard);
|
||||
objects.push_back(collectible);
|
||||
objects.push_back(trigger);
|
||||
|
||||
system.update(objects, 0);
|
||||
assert(objects[0].solid == true);
|
||||
@@ -380,7 +381,7 @@ namespace
|
||||
|
||||
const ObjectSpawn spawns[] = {
|
||||
{ GameObjectType::Player, 1, 1, 0, 0, TriggerAction::None, 0 },
|
||||
{ GameObjectType::StaticPlatform, 0, 2, 0, 0, TriggerAction::None, 0 },
|
||||
{ GameObjectType::Collectible, 0, 2, 0, 0, TriggerAction::None, 0 },
|
||||
};
|
||||
|
||||
const LevelData data = {
|
||||
|
||||
Reference in New Issue
Block a user