PlayerMovement:新增 _facingLocked 字段 + LockFacing(bool) 方法;UpdateFacing() 锁定时直接返回 WallSlideState:OnStateEnter 调用 LockFacing(true) + FlipFacing(_wallDir);OnStateExit 调用 LockFacing(false) 解锁 WallJumpState:OnStateEnter 保险性再调一次 LockFacing(false);WallJumpAway/Toward 同步写入 _inputVelocityX,确保解锁后 UpdateFacing 朝向正确(背墙跳 = 离墙方向,对墙跳 = 朝墙方向)
236 lines
10 KiB
C#
236 lines
10 KiB
C#
using System.Collections;
|
||
using System.Collections.Generic;
|
||
using BaseGames.Core;
|
||
using BaseGames.Core.Events;
|
||
using UnityEngine;
|
||
|
||
namespace BaseGames.World
|
||
{
|
||
/// <summary>
|
||
/// 移动平台。Kinematic Rigidbody2D,支持三种移动模式。
|
||
///
|
||
/// 乘客跟随(Velocity 双缓冲方案):
|
||
/// - PassengerSensor(Trigger)通过 attachedRigidbody 检测乘客(<see cref="IPassengerReceiver"/>)。
|
||
/// - FixedUpdate(-300) 计算本帧期望位移 delta → SetPlatformDelta(delta) 写入乘客缓冲区。
|
||
/// - 乘客 FixedUpdate(-200) 换入缓冲速度;状态机 FixedUpdate(-100) 的 Move() 将其叠加到 velocity。
|
||
/// - 基于 velocity 驱动,与 RigidbodyInterpolation2D.Interpolate 完全兼容,无视觉抖动。
|
||
/// - 乘客离台时 OnLeavePlatform 继承垂直速度分量;水平分量已由 Move() 自然携带。
|
||
///
|
||
/// 此方案不修改 Transform 层级,与动画/摄像机/HitBox 系统完全兼容。
|
||
/// </summary>
|
||
[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<IPassengerReceiver> _passengers = new();
|
||
private readonly List<IPassengerReceiver> _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<Rigidbody2D>();
|
||
_rb.bodyType = RigidbodyType2D.Kinematic;
|
||
_rb.interpolation = RigidbodyInterpolation2D.Interpolate;
|
||
_waitForEndpoint = new WaitForSeconds(_waitAtEndpoint);
|
||
|
||
// 缓存物理碰撞体(非 Trigger),并自动挂载零摩擦材质。
|
||
// 零摩擦确保平台从侧面压向角色时,接触力只有法向分量(水平推开),
|
||
// 无切向摩擦分量,彻底消除"平台侧面摩擦托住角色/角色无法下落"的问题。
|
||
EnsureZeroFrictionMaterial();
|
||
foreach (var col in GetComponentsInChildren<Collider2D>())
|
||
{
|
||
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)
|
||
{
|
||
if (_wayPoints.Length < 2) return; // 至少需要两个路径点
|
||
_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<IPassengerReceiver>(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<IPassengerReceiver>(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;
|
||
|
||
/// <summary>
|
||
/// 判断乘客碰撞体是否从上方进入传感器。
|
||
/// 以物理碰撞体(或传感器)顶面 Y 为基准,允许少量穿透容差 <c>kTopTolerance</c>。
|
||
/// 当平台从侧面撞上角色时,角色底部远低于平台顶面,返回 false,不注册为乘客。
|
||
/// </summary>
|
||
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;
|
||
}
|
||
|
||
/// <summary>初始化共享零摩擦材质(静态单例,整个项目只创建一次)。</summary>
|
||
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
|
||
}
|
||
}
|
||
|