From 2b0cfc6ce505ae18b0c4ce23a9d4acdd5f207a32 Mon Sep 17 00:00:00 2001 From: Lenheart <947330670@qq.com> Date: Sun, 5 Apr 2026 11:42:39 +0800 Subject: [PATCH] =?UTF-8?q?refactor(character):=20=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=E6=95=B4=E6=95=B0=E5=9D=90=E6=A0=87=E4=BC=98=E5=8C=96=E8=A7=92?= =?UTF-8?q?=E8=89=B2=E4=BD=8D=E7=BD=AE=E5=90=8C=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将CharacterWorldPosition改为使用整数坐标,避免浮点精度问题 - 添加位置余数处理,确保移动平滑性 - 统一角色位置同步逻辑到SyncActorPositionFromWorld方法 - 修改地图移动检测使用整数坐标判断 --- Game/include/character/CharacterActionTypes.h | 79 ++++++++++++++----- Game/include/character/CharacterObject.h | 1 + Game/src/character/CharacterObject.cpp | 15 ++-- Game/src/map/GameMap.cpp | 46 ++++++++--- Game/src/world/GameTown.cpp | 18 ++++- 5 files changed, 122 insertions(+), 37 deletions(-) diff --git a/Game/include/character/CharacterActionTypes.h b/Game/include/character/CharacterActionTypes.h index bd8edca..161fec2 100644 --- a/Game/include/character/CharacterActionTypes.h +++ b/Game/include/character/CharacterActionTypes.h @@ -54,13 +54,17 @@ enum class CharacterStateId { /// 角色的逻辑世界坐标。x/y 是地面平面,z 是高度轴。 struct CharacterWorldPosition { - float x = 0.0f; - float y = 0.0f; - float z = 0.0f; + int32 x = 0; + int32 y = 0; + int32 z = 0; /// 当前项目的 2.5D 投影:地面 y 决定站位,高度 z 决定视觉抬升。 - Vec2 ToScreenPosition() const { return Vec2(x, y - z); } - Vec2 ToGroundPosition() const { return Vec2(x, y); } + Vec2 ToScreenPosition() const { + return Vec2(static_cast(x), static_cast(y - z)); + } + Vec2 ToGroundPosition() const { + return Vec2(static_cast(x), static_cast(y)); + } }; /// 运动器只负责推进坐标和速度,不直接决定状态流转。 @@ -77,8 +81,14 @@ struct CharacterMotor { float gravity = 1600.0f; void SetGroundPosition(const Vec2& groundPosition) { - position.x = groundPosition.x; - position.y = groundPosition.y; + position.x = RoundWorldCoordinate(groundPosition.x); + position.y = RoundWorldCoordinate(groundPosition.y); + positionRemainder_ = Vec2::Zero(); + } + + void SetWorldPosition(const CharacterWorldPosition& worldPosition) { + position = worldPosition; + ClearPositionRemainders(); } void ApplyGroundInput(float moveX, float moveY, float speedScale = 1.0f) { @@ -113,42 +123,75 @@ struct CharacterMotor { void Jump() { grounded = false; verticalVelocity = jumpSpeed; - if (position.z < 0.0f) { - position.z = 0.0f; + if (position.z < 0) { + position.z = 0; + verticalPositionRemainder_ = 0.0f; } } void Update(float deltaTime) { - position.x += groundVelocity.x * deltaTime; - position.y += groundVelocity.y * deltaTime; + positionRemainder_.x += groundVelocity.x * deltaTime; + positionRemainder_.y += groundVelocity.y * deltaTime; if (forcedSlideRemainingTime > 0.0f && forcedSlideVelocity.lengthSquared() > 0.0f) { float slideDeltaTime = deltaTime < forcedSlideRemainingTime ? deltaTime : forcedSlideRemainingTime; - position.x += forcedSlideVelocity.x * slideDeltaTime; - position.y += forcedSlideVelocity.y * slideDeltaTime; + positionRemainder_.x += forcedSlideVelocity.x * slideDeltaTime; + positionRemainder_.y += forcedSlideVelocity.y * slideDeltaTime; forcedSlideRemainingTime -= slideDeltaTime; if (forcedSlideRemainingTime <= 0.0f) { ClearForcedSlide(); } } + position.x += ConsumeWholeUnits(positionRemainder_.x); + position.y += ConsumeWholeUnits(positionRemainder_.y); - if (!grounded || position.z > 0.0f || verticalVelocity > 0.0f) { - position.z += verticalVelocity * deltaTime; + if (!grounded || position.z > 0 || verticalVelocity > 0.0f) { + verticalPositionRemainder_ += verticalVelocity * deltaTime; verticalVelocity -= gravity * deltaTime; grounded = false; + position.z += ConsumeWholeUnits(verticalPositionRemainder_); - if (position.z <= 0.0f) { - position.z = 0.0f; + if (position.z <= 0) { + position.z = 0; verticalVelocity = 0.0f; + verticalPositionRemainder_ = 0.0f; grounded = true; } } else { - position.z = 0.0f; + position.z = 0; verticalVelocity = 0.0f; + verticalPositionRemainder_ = 0.0f; grounded = true; } } + +private: + static int32 RoundWorldCoordinate(float value) { + return static_cast(std::lround(value)); + } + + static int32 ConsumeWholeUnits(float& remainder) { + if (remainder >= 1.0f) { + int32 wholeUnits = static_cast(std::floor(remainder)); + remainder -= static_cast(wholeUnits); + return wholeUnits; + } + if (remainder <= -1.0f) { + int32 wholeUnits = static_cast(std::ceil(remainder)); + remainder -= static_cast(wholeUnits); + return wholeUnits; + } + return 0; + } + + void ClearPositionRemainders() { + positionRemainder_ = Vec2::Zero(); + verticalPositionRemainder_ = 0.0f; + } + + Vec2 positionRemainder_ = Vec2::Zero(); + float verticalPositionRemainder_ = 0.0f; }; /// 逻辑动作定义。这里只保留显式注册的逻辑入口名。 diff --git a/Game/include/character/CharacterObject.h b/Game/include/character/CharacterObject.h index 2ecb685..9fc8f90 100644 --- a/Game/include/character/CharacterObject.h +++ b/Game/include/character/CharacterObject.h @@ -170,6 +170,7 @@ private: void CommitPendingActionContext(const std::string& defaultRequestedActionId, const std::string& defaultSourceActionId, CharacterStateId defaultSourceStateId); + void SyncActorPositionFromWorld(); bool SetActionStrict(const std::string& actionName, const char* phase, const std::string& requestedActionId); diff --git a/Game/src/character/CharacterObject.cpp b/Game/src/character/CharacterObject.cpp index 8e987bc..3091353 100644 --- a/Game/src/character/CharacterObject.cpp +++ b/Game/src/character/CharacterObject.cpp @@ -133,13 +133,18 @@ void CharacterObject::SetDirection(int direction) { void CharacterObject::SetCharacterPosition(const Vec2& pos) { motor_.SetGroundPosition(pos); - SetWorldPosition(motor_.position); + SyncActorPositionFromWorld(); } void CharacterObject::SetWorldPosition(const CharacterWorldPosition& pos) { - motor_.position = pos; - SetPosition(pos.ToScreenPosition()); - SetZOrder(static_cast(pos.y)); + motor_.SetWorldPosition(pos); + SyncActorPositionFromWorld(); +} + +void CharacterObject::SyncActorPositionFromWorld() { + const CharacterWorldPosition& worldPosition = motor_.position; + SetPosition(worldPosition.ToScreenPosition()); + SetZOrder(worldPosition.y); } void CharacterObject::PushCommand(const CharacterCommand& command) { @@ -240,7 +245,7 @@ void CharacterObject::OnUpdate(float deltaTime) { currentIntent_ = commandBuffer_.BuildIntent(); stateMachine_.Update(*this, commandBuffer_, currentIntent_, deltaTime); motor_.Update(deltaTime); - SetWorldPosition(motor_.position); + SyncActorPositionFromWorld(); SetFacing(motor_.facing); } diff --git a/Game/src/map/GameMap.cpp b/Game/src/map/GameMap.cpp index 2c02e1d..83e1b84 100644 --- a/Game/src/map/GameMap.cpp +++ b/Game/src/map/GameMap.cpp @@ -7,6 +7,7 @@ #include #include #include +#include namespace frostbite2D { @@ -23,6 +24,19 @@ static const int kLayerOrders[] = { constexpr int kTileRootZOrder = -1000000; constexpr float kExtendedTileStepY = 120.0f; +int RoundWorldCoordinate(float value) { + return static_cast(std::lround(value)); +} + +Vec2 MakeIntegerWorldPoint(int x, int y) { + return Vec2(static_cast(x), static_cast(y)); +} + +Vec3 MakeIntegerWorldPosition(int x, int y, int z) { + return Vec3(static_cast(x), static_cast(y), + static_cast(z)); +} + // 地图里的 tile/img 统一走这里创建,失败时返回空 Sprite // 占位,避免整张地图中断。 Ptr createMapSprite(const std::string& path, int index) { @@ -349,20 +363,24 @@ void GameMap::AddObject(RefPtr object) { } Vec3 GameMap::CheckIsItMovable(const Vec3& curPos, const Vec3& posOffset) const { - Vec3 result = curPos; + int currentX = RoundWorldCoordinate(curPos.x); + int currentY = RoundWorldCoordinate(curPos.y); + int currentZ = RoundWorldCoordinate(curPos.z); + Vec3 result = MakeIntegerWorldPosition(currentX, currentY, currentZ); if (movableArea_.empty()) { - result += posOffset; - return result; + return MakeIntegerWorldPosition(RoundWorldCoordinate(curPos.x + posOffset.x), + RoundWorldCoordinate(curPos.y + posOffset.y), + RoundWorldCoordinate(curPos.z + posOffset.z)); } - float targetX = curPos.x + posOffset.x; - float targetY = curPos.y + posOffset.y; + int targetX = RoundWorldCoordinate(curPos.x + posOffset.x); + int targetY = RoundWorldCoordinate(curPos.y + posOffset.y); // X/Y - // 分开判断,这样贴着边移动时不会因为一个方向越界而把另一个方向也一起锁死。 + // Check X and Y separately so edge sliding does not lock both axes. bool isXValid = false; for (const auto& area : movableArea_) { - if (area.containsPoint(Vec2(targetX, curPos.y))) { + if (area.containsPoint(MakeIntegerWorldPoint(targetX, currentY))) { isXValid = true; break; } @@ -370,26 +388,28 @@ Vec3 GameMap::CheckIsItMovable(const Vec3& curPos, const Vec3& posOffset) const bool isYValid = false; for (const auto& area : movableArea_) { - if (area.containsPoint(Vec2(curPos.x, targetY))) { + if (area.containsPoint(MakeIntegerWorldPoint(currentX, targetY))) { isYValid = true; break; } } if (isXValid) { - result.x = targetX; + result.x = static_cast(targetX); } if (isYValid) { - result.y = targetY; + result.y = static_cast(targetY); } - result.z += posOffset.z; + result.z = static_cast(RoundWorldCoordinate(curPos.z + posOffset.z)); return result; } GameMap::MapMoveArea GameMap::CheckIsItMoveArea(const Vec3& curPos) const { - // moveArea_ 和配置里的 target 是一一对应的,命中后直接返回同索引的目标信息。 + // moveArea_ and townMovableAreaTargets share the same index mapping. + int currentX = RoundWorldCoordinate(curPos.x); + int currentY = RoundWorldCoordinate(curPos.y); for (size_t i = 0; i < moveArea_.size() && i < mapConfig_.townMovableAreaTargets.size(); ++i) { - if (moveArea_[i].containsPoint(Vec2(curPos.x, curPos.y))) { + if (moveArea_[i].containsPoint(MakeIntegerWorldPoint(currentX, currentY))) { return mapConfig_.townMovableAreaTargets[i]; } } diff --git a/Game/src/world/GameTown.cpp b/Game/src/world/GameTown.cpp index a6f3e33..699415c 100644 --- a/Game/src/world/GameTown.cpp +++ b/Game/src/world/GameTown.cpp @@ -1,9 +1,20 @@ #include "world/GameTown.h" +#include "character/CharacterObject.h" #include #include +#include namespace frostbite2D { +namespace { + +Vec2 RoundWorldPoint(const Vec2& pos) { + return Vec2(static_cast(std::lround(pos.x)), + static_cast(std::lround(pos.y))); +} + +} // namespace + GameTown::GameTown() = default; bool GameTown::Init(int index, const std::string& townPath) { @@ -96,7 +107,12 @@ void GameTown::AddCharacter(RefPtr actor, int areaIndex) { if (actor) { cameraController_.SetDebugEnabled(false); if (areaIndex == -2 && sariaRoomPos_.x >= 0.0f && sariaRoomPos_.y >= 0.0f) { - actor->SetPosition(sariaRoomPos_); + Vec2 spawnPos = RoundWorldPoint(sariaRoomPos_); + if (auto* character = dynamic_cast(actor.Get())) { + character->SetCharacterPosition(spawnPos); + } else { + actor->SetPosition(spawnPos); + } } mapIt->map->AddObject(actor); cameraController_.SetFocus(actor->GetPosition());