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); + } } }