refactor(character): 使用整数坐标优化角色位置同步

- 将CharacterWorldPosition改为使用整数坐标,避免浮点精度问题
- 添加位置余数处理,确保移动平滑性
- 统一角色位置同步逻辑到SyncActorPositionFromWorld方法
- 修改地图移动检测使用整数坐标判断
This commit is contained in:
2026-04-05 11:42:39 +08:00
parent c4eefab70c
commit 2b0cfc6ce5
5 changed files with 122 additions and 37 deletions

View File

@@ -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<float>(x), static_cast<float>(y - z));
}
Vec2 ToGroundPosition() const {
return Vec2(static_cast<float>(x), static_cast<float>(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<int32>(std::lround(value));
}
static int32 ConsumeWholeUnits(float& remainder) {
if (remainder >= 1.0f) {
int32 wholeUnits = static_cast<int32>(std::floor(remainder));
remainder -= static_cast<float>(wholeUnits);
return wholeUnits;
}
if (remainder <= -1.0f) {
int32 wholeUnits = static_cast<int32>(std::ceil(remainder));
remainder -= static_cast<float>(wholeUnits);
return wholeUnits;
}
return 0;
}
void ClearPositionRemainders() {
positionRemainder_ = Vec2::Zero();
verticalPositionRemainder_ = 0.0f;
}
Vec2 positionRemainder_ = Vec2::Zero();
float verticalPositionRemainder_ = 0.0f;
};
/// 逻辑动作定义。这里只保留显式注册的逻辑入口名。

View File

@@ -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);

View File

@@ -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<int>(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);
}

View File

@@ -7,6 +7,7 @@
#include <frostbite2D/graphics/renderer.h>
#include <frostbite2D/resource/audio_database.h>
#include <algorithm>
#include <cmath>
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<int>(std::lround(value));
}
Vec2 MakeIntegerWorldPoint(int x, int y) {
return Vec2(static_cast<float>(x), static_cast<float>(y));
}
Vec3 MakeIntegerWorldPosition(int x, int y, int z) {
return Vec3(static_cast<float>(x), static_cast<float>(y),
static_cast<float>(z));
}
// 地图里的 tile/img 统一走这里创建,失败时返回空 Sprite
// 占位,避免整张地图中断。
Ptr<Sprite> createMapSprite(const std::string& path, int index) {
@@ -349,20 +363,24 @@ void GameMap::AddObject(RefPtr<Actor> 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<float>(targetX);
}
if (isYValid) {
result.y = targetY;
result.y = static_cast<float>(targetY);
}
result.z += posOffset.z;
result.z = static_cast<float>(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];
}
}

View File

@@ -1,9 +1,20 @@
#include "world/GameTown.h"
#include "character/CharacterObject.h"
#include <SDL2/SDL.h>
#include <algorithm>
#include <cmath>
namespace frostbite2D {
namespace {
Vec2 RoundWorldPoint(const Vec2& pos) {
return Vec2(static_cast<float>(std::lround(pos.x)),
static_cast<float>(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> 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<CharacterObject*>(actor.Get())) {
character->SetCharacterPosition(spawnPos);
} else {
actor->SetPosition(spawnPos);
}
}
mapIt->map->AddObject(actor);
cameraController_.SetFocus(actor->GetPosition());