diff --git a/Assets/_Game/Scripts/Core/IPassengerReceiver.cs b/Assets/_Game/Scripts/Core/IPassengerReceiver.cs
new file mode 100644
index 0000000..5710677
--- /dev/null
+++ b/Assets/_Game/Scripts/Core/IPassengerReceiver.cs
@@ -0,0 +1,29 @@
+using UnityEngine;
+
+namespace BaseGames.Core
+{
+ ///
+ /// 移动平台乘客接口。
+ /// 实现此接口的对象可被 携带,
+ /// 无需修改 Transform 父子关系,避免与动画/摄像机/HitBox 系统冲突。
+ ///
+ /// 协议(执行顺序保障):
+ /// 1. MovingPlatform.FixedUpdate(-300)计算本帧期望位移 delta → 调用 SetPlatformDelta。
+ /// 2. 实现方 FixedUpdate(-200)最前端消费 delta:_rb.position += delta。
+ /// 3. 乘客离开传感器时 MovingPlatform 调用 OnLeavePlatform,继承平台速度防止卡顿。
+ ///
+ public interface IPassengerReceiver
+ {
+ ///
+ /// 由 MovingPlatform 每帧推送本帧平台期望位移量(不依赖 MovePosition 是否已执行)。
+ /// 实现方存入待处理字段,在自己的 FixedUpdate 最前端消费,避免与 velocity 冲突。
+ ///
+ void SetPlatformDelta(Vector2 delta);
+
+ ///
+ /// 乘客离开平台时调用,传入平台当前帧速度,由实现方叠加到自身速度。
+ /// 防止离台瞬间速度突变产生卡顿。
+ ///
+ void OnLeavePlatform(Vector2 platformVelocity);
+ }
+}
diff --git a/Assets/_Game/Scripts/Core/IPassengerReceiver.cs.meta b/Assets/_Game/Scripts/Core/IPassengerReceiver.cs.meta
new file mode 100644
index 0000000..40d8c71
--- /dev/null
+++ b/Assets/_Game/Scripts/Core/IPassengerReceiver.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 3f719c44d9262914baec9ba887be8c54
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/_Game/Scripts/Editor/Scene/SceneObjectPlacerTool.cs b/Assets/_Game/Scripts/Editor/Scene/SceneObjectPlacerTool.cs
index cbedfe4..b03e8f6 100644
--- a/Assets/_Game/Scripts/Editor/Scene/SceneObjectPlacerTool.cs
+++ b/Assets/_Game/Scripts/Editor/Scene/SceneObjectPlacerTool.cs
@@ -804,6 +804,7 @@ namespace BaseGames.Editor
MovingPlatform platform = GetOrAddComponent(go);
AssignReference(platform, "_passengerSensor", sensorCol, report);
+ AssignLayerMask(platform, "_passengerLayer", new[] { "Player", "Enemy" }, report);
AssignObjectArray(platform, "_wayPoints", new Object[] { wpA, wpB }, report);
report.Add("WaypointA / WaypointB 为移动端点,可将其拖出平台并在场景中调整位置。");
diff --git a/Assets/_Game/Scripts/Player/PlayerMovement.cs b/Assets/_Game/Scripts/Player/PlayerMovement.cs
index 8475d44..46bc9e1 100644
--- a/Assets/_Game/Scripts/Player/PlayerMovement.cs
+++ b/Assets/_Game/Scripts/Player/PlayerMovement.cs
@@ -11,7 +11,7 @@ namespace BaseGames.Player
// 开头能在状态机写入速度之前先应用"强制清零"标记。
[DefaultExecutionOrder(-200)]
[RequireComponent(typeof(Rigidbody2D))]
- public class PlayerMovement : MonoBehaviour
+ public class PlayerMovement : MonoBehaviour, IPassengerReceiver
{
[Header("配置")]
[SerializeField] private PlayerMovementConfigSO _config;
@@ -31,6 +31,7 @@ namespace BaseGames.Player
// 下一个 FixedUpdate(-200,先于状态机 -100)读取并清零,
// 防止状态机用旧输入把速度重新写成非零值。
private bool _pendingHorizontalZero;
+ private Vector2 _pendingPlatformDelta; // MovingPlatform 推送的本帧位移,在 FixedUpdate 最前端消费
private bool _isWallLeft;
private bool _isWallRight;
private bool _onOneWayPlatform;
@@ -83,6 +84,13 @@ namespace BaseGames.Player
private void FixedUpdate()
{
+ // 消费移动平台推送的位移(在 velocity 系统之前直接写 position,避免冲突)
+ if (_pendingPlatformDelta != Vector2.zero)
+ {
+ _rb.position += _pendingPlatformDelta;
+ _pendingPlatformDelta = Vector2.zero;
+ }
+
// 优先处理来自 Update 的强制清零请求(在状态机 OnStateFixedUpdate 之前执行)。
if (_pendingHorizontalZero)
{
@@ -221,6 +229,17 @@ namespace BaseGames.Player
/// 消耗墙壁土狼时间,防止同一帧被多次触发。
public void ConsumeWallCoyote() => _wallCoyoteTimer = 0f;
+ // ── IPassengerReceiver ────────────────────────────────────────────────
+ ///
+ /// MovingPlatform.FixedUpdate(-300) 推送本帧平台位移。
+ /// 在本类 FixedUpdate(-200) 最前端消费,直接写 _rb.position,
+ /// 与 velocity 系统完全隔离,避免 Dynamic Rigidbody 上 MovePosition + velocity 叠加。
+ ///
+ public void SetPlatformDelta(Vector2 delta) => _pendingPlatformDelta += delta;
+
+ /// 离开移动平台时继承平台速度,防止速度骤变卡顿。
+ public void OnLeavePlatform(Vector2 platformVelocity) => _rb.velocity += platformVelocity;
+
// ── 冲刺 ──────────────────────────────────────────────────────────────
///
/// 施加冲刺速度(DashState/DashState 调用)。
diff --git a/Assets/_Game/Scripts/World/MovingPlatform.cs b/Assets/_Game/Scripts/World/MovingPlatform.cs
index 4353d90..e0c1de6 100644
--- a/Assets/_Game/Scripts/World/MovingPlatform.cs
+++ b/Assets/_Game/Scripts/World/MovingPlatform.cs
@@ -1,5 +1,6 @@
using System.Collections;
using System.Collections.Generic;
+using BaseGames.Core;
using BaseGames.Core.Events;
using UnityEngine;
@@ -7,8 +8,16 @@ namespace BaseGames.World
{
///
/// 移动平台。Kinematic Rigidbody2D,支持三种移动模式。
- /// 乘客跟随:OnTriggerEnter2D 时 SetParent,离开时还原并附加速度。
+ ///
+ /// 乘客跟随(Delta Position 方案,替代 SetParent):
+ /// - PassengerSensor(Trigger)检测进入乘客()。
+ /// - FixedUpdate(-300) 计算本帧期望位移 delta,推送给所有乘客,再执行 MovePosition。
+ /// - 乘客在自己的 FixedUpdate(-200) 最前端消费 delta:_rb.position += delta。
+ /// - 乘客离台时通过 OnLeavePlatform 继承平台速度,避免卡顿。
+ ///
+ /// 此方案不修改 Transform 层级,与动画/摄像机/HitBox 系统完全兼容。
///
+ [DefaultExecutionOrder(-300)]
[RequireComponent(typeof(Rigidbody2D))]
public class MovingPlatform : MonoBehaviour
{
@@ -24,15 +33,18 @@ namespace BaseGames.World
[SerializeField] private VoidEventChannelSO _activationChannel;
[Header("乘客检测")]
- [SerializeField] private BoxCollider2D _passengerSensor; // IsTrigger,用于乘客 SetParent
+ [SerializeField] private BoxCollider2D _passengerSensor; // isTrigger,检测乘客
+ [SerializeField] private LayerMask _passengerLayer; // 应包含 Player + Enemy 层
- private Rigidbody2D _rb;
- private List _passengers = new();
- private int _waypointIndex;
- private bool _movingForward = true;
- private bool _triggered;
- private bool _waiting;
- private WaitForSeconds _waitForEndpoint;
+ private Rigidbody2D _rb;
+ private readonly List _passengers = new();
+ private readonly List _passengerSnapshot = new(); // 迭代快照,防并发修改
+ private int _waypointIndex;
+ private bool _movingForward = true;
+ private bool _triggered;
+ private bool _waiting;
+ private Vector2 _frameVelocity; // 本帧实际速度,供离台时继承
+ private WaitForSeconds _waitForEndpoint;
private readonly CompositeDisposable _subs = new();
private void Awake()
@@ -55,17 +67,40 @@ namespace BaseGames.World
private void FixedUpdate()
{
- if (_wayPoints == null || _wayPoints.Length == 0) return;
- if (_moveType == MoveType.TriggeredLinear && !_triggered) return;
- if (_waiting) return;
+ if (_wayPoints == null || _wayPoints.Length == 0)
+ {
+ _frameVelocity = Vector2.zero;
+ return;
+ }
+ if (_moveType == MoveType.TriggeredLinear && !_triggered)
+ {
+ _frameVelocity = Vector2.zero;
+ return;
+ }
+ if (_waiting)
+ {
+ _frameVelocity = Vector2.zero;
+ return;
+ }
- MoveTowardsNextWaypoint();
+ MoveAndBroadcast();
}
- private void MoveTowardsNextWaypoint()
+ private void MoveAndBroadcast()
{
var target = (Vector2)_wayPoints[_waypointIndex].position;
var next = Vector2.MoveTowards(_rb.position, target, _speed * Time.fixedDeltaTime);
+
+ // 计算期望 delta(在 MovePosition 执行前,不依赖物理步骤是否完成)
+ Vector2 delta = next - _rb.position;
+ _frameVelocity = delta / Time.fixedDeltaTime;
+
+ // 先广播 delta,乘客在下一个 FixedUpdate(-200) 消费
+ _passengerSnapshot.Clear();
+ _passengerSnapshot.AddRange(_passengers);
+ foreach (var r in _passengerSnapshot)
+ r.SetPlatformDelta(delta);
+
_rb.MovePosition(next);
if (Vector2.Distance(_rb.position, target) < 0.02f)
@@ -103,32 +138,27 @@ namespace BaseGames.World
private void OnTriggered() => _triggered = true;
- // ── Passenger Pattern ─────────────────────────────────────────────────
+ // ── Passenger Detection ───────────────────────────────────────────────
private void OnTriggerEnter2D(Collider2D other)
{
- if (!IsPassenger(other.gameObject)) return;
- other.transform.SetParent(transform);
- _passengers.Add(other.transform);
+ if (!IsPassengerLayer(other)) return;
+ if (!other.TryGetComponent(out var receiver)) return;
+ if (!_passengers.Contains(receiver))
+ _passengers.Add(receiver);
}
private void OnTriggerExit2D(Collider2D other)
{
- if (!_passengers.Contains(other.transform)) return;
+ if (!other.TryGetComponent(out var receiver)) return;
+ if (!_passengers.Remove(receiver)) return;
- other.transform.SetParent(null);
- _passengers.Remove(other.transform);
-
- // 离开时附加平台速度,避免卡顿
- var passengerRb = other.GetComponentInParent();
- if (passengerRb != null)
- passengerRb.AddForce(_rb.velocity, ForceMode2D.Impulse);
+ // 继承平台速度,防止离台时速度骤变卡顿
+ receiver.OnLeavePlatform(_frameVelocity);
}
- private static bool IsPassenger(GameObject go)
- {
- return go.CompareTag("Player") || go.CompareTag("Enemy");
- }
+ private bool IsPassengerLayer(Collider2D col)
+ => (_passengerLayer.value & (1 << col.gameObject.layer)) != 0;
#if UNITY_EDITOR
private void OnDrawGizmos()
@@ -144,3 +174,4 @@ namespace BaseGames.World
#endif
}
}
+