diff --git a/Assets/_Game/Scripts/World/MovingPlatform.cs b/Assets/_Game/Scripts/World/MovingPlatform.cs index 452b3d2..26faded 100644 --- a/Assets/_Game/Scripts/World/MovingPlatform.cs +++ b/Assets/_Game/Scripts/World/MovingPlatform.cs @@ -38,6 +38,7 @@ namespace BaseGames.World [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; @@ -48,12 +49,29 @@ namespace BaseGames.World 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() @@ -147,6 +165,10 @@ namespace BaseGames.World // 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); } @@ -164,6 +186,37 @@ namespace BaseGames.World 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() {