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