diff --git a/Assets/_Game/Scripts/Core/IPassengerReceiver.cs b/Assets/_Game/Scripts/Core/IPassengerReceiver.cs
index 001a98e..1431f21 100644
--- a/Assets/_Game/Scripts/Core/IPassengerReceiver.cs
+++ b/Assets/_Game/Scripts/Core/IPassengerReceiver.cs
@@ -9,28 +9,30 @@ namespace BaseGames.Core
///
/// 协议(执行顺序保障):
/// 1. MovingPlatform.FixedUpdate(-300)计算本帧期望位移 delta → 调用 SetPlatformDelta,
- /// 实现方将其累积到 _pendingPlatformDelta。
- /// 2. 实现方 FixedUpdate(-200)执行 _rb.position += _pendingPlatformDelta 并清零。
- /// 使用 Rigidbody2D.position(而非 transform.position):Unity 保证在 FixedUpdate 中
- /// 赋值时会同步更新插值参考点,不引起视觉抖动,与 RigidbodyInterpolation2D.Interpolate 兼容。
- /// 3. 状态机 FixedUpdate(-100)正常调用 Move(speedX);_rb.velocity.x 仅含玩家输入速度,
- /// UpdateFacing / ApplyAirDrag 无需减去任何平台速度,语义清晰。
- /// 4. 乘客离开传感器时 MovingPlatform 调用 OnLeavePlatform,继承完整平台速度(含水平),
- /// 保证从移动平台起跳时具有平台动量,手感自然。
+ /// 实现方将其转换为速度(delta / fixedDeltaTime)存入下帧缓冲区。
+ /// 2. 实现方 FixedUpdate(-200)将缓冲速度换入 _platformVelocity,清零缓冲区。
+ /// 3. 状态机 FixedUpdate(-100)通过 Move(speedX) 将 _platformVelocity.x 叠加到 velocity,
+ /// 与 RigidbodyInterpolation2D.Interpolate 完全兼容,无视觉抖动。
+ /// _inputVelocityX(纯玩家输入)由 Move() 单独记录,UpdateFacing 读此值而非 velocity.x,
+ /// 彻底避免平台速度污染朝向逻辑。
+ /// 4. 乘客离开传感器时 MovingPlatform 调用 OnLeavePlatform,继承垂直速度分量;
+ /// 水平分量已由最后一次 Move() 自然携带(包含 _platformVelocity.x),无需重复叠加。
+ ///
+ /// 注意:不使用 _rb.position += delta 方案——Kinematic 平台通过 MovePosition 移动时,
+ /// 物理引擎会通过接触力推动 Dynamic 乘客;若再手动写 position,等效于双重施加(2× 速度)。
///
public interface IPassengerReceiver
{
///
/// 由 MovingPlatform 每帧推送本帧平台期望位移量。
- /// 实现方将其累积到内部字段,在自己的 FixedUpdate 中通过 Rigidbody2D.position
- /// 直接施加位移,不混入 velocity,保持玩家速度语义纯净。
+ /// 实现方将其转换为速度(delta / fixedDeltaTime)存入缓冲区;
+ /// 在自己的 FixedUpdate 换入后,由 Move() 叠加到 velocity.x,保持插值平滑。
///
void SetPlatformDelta(Vector2 delta);
///
/// 乘客离开平台时调用,传入平台当前帧速度。
- /// 实现方应将完整平台速度(含水平)叠加到自身 velocity,
- /// 保证离台瞬间具有平台动量,手感自然。
+ /// 实现方仅需继承垂直分量;水平分量已由最后一次 Move() 自然携带(包含平台 X 速度)。
///
void OnLeavePlatform(Vector2 platformVelocity);
}
diff --git a/Assets/_Game/Scripts/Player/PlayerMovement.cs b/Assets/_Game/Scripts/Player/PlayerMovement.cs
index 8489bd2..cffad50 100644
--- a/Assets/_Game/Scripts/Player/PlayerMovement.cs
+++ b/Assets/_Game/Scripts/Player/PlayerMovement.cs
@@ -31,11 +31,14 @@ namespace BaseGames.Player
// 下一个 FixedUpdate(-200,先于状态机 -100)读取并清零,
// 防止状态机用旧输入把速度重新写成非零值。
private bool _pendingHorizontalZero;
- // 移动平台位移累积:Platform(-300) 写入;Player(-200) 通过 _rb.position += 消费后清零。
- // 使用位置偏移而非速度叠加,_rb.velocity.x 保持纯粹的玩家输入速度,
- // UpdateFacing / ApplyAirDrag 无需任何修正。
- // Unity 文档明确:FixedUpdate 中设置 Rigidbody2D.position 与 Interpolate 完全兼容。
- private Vector2 _pendingPlatformDelta;
+ // 移动平台速度双缓冲:Platform(-300) 写入 _next;Player(-200) 将 _next 换入 _current 并清零;
+ // 状态机(-100) 的 Move() 将 _platformVelocity.x 叠加到水平速度,保持与平台同步。
+ // 使用速度(而非 _rb.position+=):Kinematic 平台的 MovePosition 已通过物理接触推动 Dynamic 乘客;
+ // 若再手动写 position 会产生双重施加(2× 速度)。速度方案仅覆盖 velocity,无此问题。
+ private Vector2 _platformVelocity; // 本帧消费(Move() 叠加到 velocity)
+ private Vector2 _nextPlatformVelocity; // Platform(-300) 写入缓冲区
+ // 玩家自身输入水平速度(不含平台分量);由 Move() 写入,UpdateFacing / ApplyAirDrag 读此值。
+ private float _inputVelocityX;
private bool _isWallLeft;
private bool _isWallRight;
private bool _onOneWayPlatform;
@@ -88,15 +91,14 @@ namespace BaseGames.Player
private void FixedUpdate()
{
- // 消费本帧平台位移:直接修改物理位置,不污染 _rb.velocity。
- // Rigidbody2D.position 在 FixedUpdate 内赋值时,Unity 会同步更新插值参考点,
- // 不会引起视觉抖动。
- _rb.position += _pendingPlatformDelta;
- _pendingPlatformDelta = Vector2.zero;
+ // 平台速度双缓冲换入:Platform(-300) 已写入 _next;换入 _current 供 Move() 叠加。
+ _platformVelocity = _nextPlatformVelocity;
+ _nextPlatformVelocity = Vector2.zero;
// 优先处理来自 Update 的强制清零请求(在状态机 OnStateFixedUpdate 之前执行)。
if (_pendingHorizontalZero)
{
+ _inputVelocityX = 0f;
_rb.velocity = new Vector2(0f, _rb.velocity.y);
_pendingHorizontalZero = false;
}
@@ -131,11 +133,13 @@ namespace BaseGames.Player
/// 直接赋予目标水平速度(按键即全速,松键即停,无加速过渡)。
/// 地面状态每帧直接到达全速;空中调用时同样即时,但配合 ApplyAirDrag
/// 在无输入时自然减速,保留跳出时的动量。
- /// 平台携带通过 _rb.position 偏移实现,此处无需叠加平台速度。
+ /// 若当前站在移动平台上,自动叠加 _platformVelocity.x 保持同步;
+ /// _inputVelocityX 只记录玩家输入分量,供 UpdateFacing / ApplyAirDrag 使用。
///
public void Move(float speedX)
{
- _rb.velocity = new Vector2(speedX, _rb.velocity.y);
+ _inputVelocityX = speedX;
+ _rb.velocity = new Vector2(speedX + _platformVelocity.x, _rb.velocity.y);
}
///
@@ -145,8 +149,10 @@ namespace BaseGames.Player
///
public void ApplyAirDrag(float factor)
{
+ // 空中不在平台上,_platformVelocity = 0,velocity.x == _inputVelocityX;同步写回保持一致。
float newX = _rb.velocity.x * factor;
if (Mathf.Abs(newX) < 0.05f) newX = 0f;
+ _inputVelocityX = newX;
_rb.velocity = new Vector2(newX, _rb.velocity.y);
}
@@ -181,9 +187,10 @@ namespace BaseGames.Player
=> _rb.velocity = direction.normalized * force;
// ── 速度控制 ──────────────────────────────────────────────────────────
- public void ZeroVelocity() => _rb.velocity = Vector2.zero;
+ public void ZeroVelocity() { _inputVelocityX = 0f; _rb.velocity = Vector2.zero; }
public void ZeroHorizontalVelocity()
{
+ _inputVelocityX = 0f;
_rb.velocity = new Vector2(0f, _rb.velocity.y);
// 设置标记:下一个 FixedUpdate 开头再次强制清零,
// 防止因读到旧输入而把速度重新写成非零值。
@@ -193,9 +200,8 @@ namespace BaseGames.Player
// ── 朝向 ──────────────────────────────────────────────────────────────
public void UpdateFacing()
{
- // _rb.velocity.x 是纯玩家输入速度(平台携带通过位置偏移,不混入 velocity),
- // 直接读取即可,无需减去平台速度。
- float vx = _rb.velocity.x;
+ // 读取玩家输入速度(不含平台分量),避免平台横向运动驱动朝向翻转。
+ float vx = _inputVelocityX;
if (Mathf.Abs(vx) < 0.1f) return;
int dir = vx > 0f ? 1 : -1;
if (dir == _facingDirection) return;
@@ -238,19 +244,18 @@ namespace BaseGames.Player
// ── IPassengerReceiver ────────────────────────────────────────────────
///
/// MovingPlatform.FixedUpdate(-300) 推送本帧平台期望位移。
- /// 累积到 _pendingPlatformDelta;在本类 FixedUpdate(-200) 通过 _rb.position += 消费,
- /// 与玩家 velocity 完全解耦,UpdateFacing / ApplyAirDrag 无需修正。
+ /// 转换为速度(delta / fixedDeltaTime)写入下帧缓冲区 _next,
+ /// 在本类 FixedUpdate(-200) 换入,由 Move() 叠加到水平速度。
///
public void SetPlatformDelta(Vector2 delta)
- => _pendingPlatformDelta += delta;
+ => _nextPlatformVelocity += delta / Time.fixedDeltaTime;
///
/// 乘客离开平台时调用,传入平台当前帧速度。
- /// 此方案下 _rb.velocity 为纯玩家速度,离台时直接继承平台完整速度(含水平),
- /// 保证从移动平台起跳后具有平台动量,手感自然。
+ /// 水平速度已由最后一次 Move() 自然携带(包含 _platformVelocity.x),仅继承垂直分量。
///
public void OnLeavePlatform(Vector2 platformVelocity)
- => _rb.velocity += platformVelocity;
+ => _rb.velocity += new Vector2(0f, platformVelocity.y);
// ── 冲刺 ──────────────────────────────────────────────────────────────
///
diff --git a/Assets/_Game/Scripts/Player/States/AttackState.cs b/Assets/_Game/Scripts/Player/States/AttackState.cs
index 4cb25ed..9b36264 100644
--- a/Assets/_Game/Scripts/Player/States/AttackState.cs
+++ b/Assets/_Game/Scripts/Player/States/AttackState.cs
@@ -76,7 +76,13 @@ namespace BaseGames.Player.States
TryConsumeCancelInput();
}
- public override void OnStateFixedUpdate() { }
+ public override void OnStateFixedUpdate()
+ {
+ // 攻击动画播放期间无水平输入,仍需每帧调用 Move(0) 叠加 _platformVelocity.x,
+ // 使玩家在移动平台上攻击时仍随平台同步移动。
+ if (Move.IsGrounded)
+ Move.Move(0f);
+ }
// ── 内部 ──────────────────────────────────────────────────────────────
diff --git a/Assets/_Game/Scripts/Player/States/IdleState.cs b/Assets/_Game/Scripts/Player/States/IdleState.cs
index 368e889..d08b0a4 100644
--- a/Assets/_Game/Scripts/Player/States/IdleState.cs
+++ b/Assets/_Game/Scripts/Player/States/IdleState.cs
@@ -68,7 +68,13 @@ namespace BaseGames.Player.States
// OnStateUpdate 中仍保留同样的检测作为跨帧补充,两者不会发生双重转换:
// 本帧若在 FixedUpdate 中转换到 FallState,Update 调用的将是 FallState.OnStateUpdate()。
if (!Move.IsGrounded)
+ {
_owner.TransitionTo(_owner.GetState());
+ return;
+ }
+ // 无水平输入时仍需每帧调用 Move(0),使 _platformVelocity.x 被叠加进 velocity.x,
+ // 确保站在移动平台上的玩家随平台同步移动,不产生相对位移。
+ Move.Move(0f);
}
}
}
diff --git a/Assets/_Game/Scripts/Player/States/ParryState.cs b/Assets/_Game/Scripts/Player/States/ParryState.cs
index a78a6cd..817fcc0 100644
--- a/Assets/_Game/Scripts/Player/States/ParryState.cs
+++ b/Assets/_Game/Scripts/Player/States/ParryState.cs
@@ -42,5 +42,13 @@ namespace BaseGames.Player.States
{
Owner.TransitionTo(Owner.GetState());
}
+
+ public override void OnStateFixedUpdate()
+ {
+ // 弹反预备期间无水平输入,仍需每帧调用 Move(0) 叠加 _platformVelocity.x,
+ // 使玩家在移动平台上弹反时随平台同步移动。
+ if (Move != null && Move.IsGrounded)
+ Move.Move(0f);
+ }
}
}
diff --git a/Assets/_Game/Scripts/Player/States/UpAttackState.cs b/Assets/_Game/Scripts/Player/States/UpAttackState.cs
index d6a3dd9..77f3481 100644
--- a/Assets/_Game/Scripts/Player/States/UpAttackState.cs
+++ b/Assets/_Game/Scripts/Player/States/UpAttackState.cs
@@ -49,5 +49,12 @@ namespace BaseGames.Player.States
else
Owner.TransitionTo(Owner.GetState());
}
+
+ public override void OnStateFixedUpdate()
+ {
+ // 地面上劈期间调用 Move(0) 叠加 _platformVelocity.x,使玩家随移动平台同步移动。
+ if (Move != null && Move.IsGrounded)
+ Move.Move(0f);
+ }
}
}