using System.Collections; using System.Collections.Generic; using BaseGames.Core; using BaseGames.Core.Events; using UnityEngine; namespace BaseGames.World { /// /// 移动平台。Kinematic Rigidbody2D,支持三种移动模式。 /// /// 乘客跟随(Velocity 双缓冲方案): /// - PassengerSensor(Trigger)通过 attachedRigidbody 检测乘客()。 /// - FixedUpdate(-300) 计算本帧期望位移 delta → SetPlatformDelta(delta) 写入乘客缓冲区。 /// - 乘客 FixedUpdate(-200) 换入缓冲速度;状态机 FixedUpdate(-100) 的 Move() 将其叠加到 velocity。 /// - 基于 velocity 驱动,与 RigidbodyInterpolation2D.Interpolate 完全兼容,无视觉抖动。 /// - 乘客离台时 OnLeavePlatform 继承垂直速度分量;水平分量已由 Move() 自然携带。 /// /// 此方案不修改 Transform 层级,与动画/摄像机/HitBox 系统完全兼容。 /// [DefaultExecutionOrder(-300)] [RequireComponent(typeof(Rigidbody2D))] public class MovingPlatform : MonoBehaviour { public enum MoveType { LinearAB, WayPoints, TriggeredLinear } [Header("移动配置")] [SerializeField] private MoveType _moveType = MoveType.LinearAB; [SerializeField] private Transform[] _wayPoints; [SerializeField] private float _speed = 3f; [SerializeField] private float _waitAtEndpoint = 0.5f; [Header("TriggeredLinear 模式")] [SerializeField] private VoidEventChannelSO _activationChannel; [Header("乘客检测")] [SerializeField] private BoxCollider2D _passengerSensor; // isTrigger,检测乘客 [SerializeField] private LayerMask _passengerLayer; // 应包含 Player + Enemy 层 private Rigidbody2D _rb; private Collider2D _physicsCollider; // 物理碰撞体(非 Trigger),用于判断乘客接触方向 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 static PhysicsMaterial2D s_zeroFriction; private void Awake() { _rb = GetComponent(); _rb.bodyType = RigidbodyType2D.Kinematic; _rb.interpolation = RigidbodyInterpolation2D.Interpolate; _waitForEndpoint = new WaitForSeconds(_waitAtEndpoint); // 缓存物理碰撞体(非 Trigger),并自动挂载零摩擦材质。 // 零摩擦确保平台从侧面压向角色时,接触力只有法向分量(水平推开), // 无切向摩擦分量,彻底消除"平台侧面摩擦托住角色/角色无法下落"的问题。 EnsureZeroFrictionMaterial(); foreach (var col in GetComponentsInChildren()) { if (col.isTrigger) continue; _physicsCollider = col; if (col.sharedMaterial == null || col.sharedMaterial.friction > 0f) col.sharedMaterial = s_zeroFriction; break; } } private void OnEnable() { _activationChannel?.Subscribe(OnTriggered).AddTo(_subs); } private void OnDisable() { _subs.Clear(); } private void FixedUpdate() { 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; } MoveAndBroadcast(); } 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) StartCoroutine(WaitAndAdvance()); } private IEnumerator WaitAndAdvance() { _waiting = true; yield return _waitForEndpoint; AdvanceWaypoint(); _waiting = false; } private void AdvanceWaypoint() { if (_moveType == MoveType.TriggeredLinear) { _waypointIndex = Mathf.Min(_waypointIndex + 1, _wayPoints.Length - 1); if (_waypointIndex == _wayPoints.Length - 1) _triggered = false; return; } if (_moveType == MoveType.LinearAB) { _movingForward = !_movingForward; _waypointIndex = _movingForward ? 1 : 0; } else // WayPoints { _waypointIndex = (_waypointIndex + 1) % _wayPoints.Length; } } private void OnTriggered() => _triggered = true; // ── Passenger Detection ─────────────────────────────────────────────── private void OnTriggerEnter2D(Collider2D other) { if (!IsPassengerLayer(other)) return; // attachedRigidbody:若玩家碰撞体在子对象上,也能正确找到根对象上的 IPassengerReceiver if (!other.attachedRigidbody) return; if (!other.attachedRigidbody.TryGetComponent(out var receiver)) return; // 仅接受从上方进入的乘客:乘客碰撞体底部须位于平台顶面附近。 // 当平台从侧面移向角色时,角色底部远低于平台顶面,此检测将其排除, // 防止角色被错误注册为乘客并获得平台水平速度(被"推着走")。 if (!IsPassengerFromAbove(other)) return; if (!_passengers.Contains(receiver)) _passengers.Add(receiver); } private void OnTriggerExit2D(Collider2D other) { if (!other.attachedRigidbody) return; if (!other.attachedRigidbody.TryGetComponent(out var receiver)) return; if (!_passengers.Remove(receiver)) return; // 继承平台垂直速度;水平速度已由 IPassengerReceiver.Move() 自然携带,无需重复叠加 receiver.OnLeavePlatform(_frameVelocity); } private bool IsPassengerLayer(Collider2D col) => (_passengerLayer.value & (1 << col.gameObject.layer)) != 0; /// /// 判断乘客碰撞体是否从上方进入传感器。 /// 以物理碰撞体(或传感器)顶面 Y 为基准,允许少量穿透容差 kTopTolerance。 /// 当平台从侧面撞上角色时,角色底部远低于平台顶面,返回 false,不注册为乘客。 /// private bool IsPassengerFromAbove(Collider2D passengerCollider) { // 平台顶面 Y:优先用物理碰撞体 bounds,其次用传感器 bounds,最后退回 transform.position.y float platformTop = _physicsCollider != null ? _physicsCollider.bounds.max.y : (_passengerSensor != null ? _passengerSensor.bounds.max.y : transform.position.y); // 容差 0.12f:允许落地时的正常物理穿透深度(通常 < 0.05f),同时比角色半高(约 0.9f)小得多, // 足以可靠区分"从上方落入"与"从侧面进入"。 const float kTopTolerance = 0.12f; return passengerCollider.bounds.min.y >= platformTop - kTopTolerance; } /// 初始化共享零摩擦材质(静态单例,整个项目只创建一次)。 private static void EnsureZeroFrictionMaterial() { if (s_zeroFriction != null) return; s_zeroFriction = new PhysicsMaterial2D("MovingPlatform_ZeroFriction") { friction = 0f, bounciness = 0f, }; } #if UNITY_EDITOR private void OnDrawGizmos() { if (_wayPoints == null || _wayPoints.Length < 2) return; Gizmos.color = new Color(1f, 0.8f, 0f, 0.8f); for (int i = 0; i < _wayPoints.Length - 1; i++) { if (_wayPoints[i] != null && _wayPoints[i + 1] != null) Gizmos.DrawLine(_wayPoints[i].position, _wayPoints[i + 1].position); } } #endif } }