Files
zeling_v2/Assets/_Game/Scripts/World/MovingPlatform.cs
Joywayer 68d4c699ae 修复内容:
PlayerMovement:新增 _facingLocked 字段 + LockFacing(bool) 方法;UpdateFacing() 锁定时直接返回
WallSlideState:OnStateEnter 调用 LockFacing(true) + FlipFacing(_wallDir);OnStateExit 调用 LockFacing(false) 解锁
WallJumpState:OnStateEnter 保险性再调一次 LockFacing(false);WallJumpAway/Toward 同步写入 _inputVelocityX,确保解锁后 UpdateFacing 朝向正确(背墙跳 = 离墙方向,对墙跳 = 朝墙方向)
2026-05-22 10:48:52 +08:00

236 lines
10 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Collections;
using System.Collections.Generic;
using BaseGames.Core;
using BaseGames.Core.Events;
using UnityEngine;
namespace BaseGames.World
{
/// <summary>
/// 移动平台。Kinematic Rigidbody2D支持三种移动模式。
///
/// 乘客跟随Velocity 双缓冲方案):
/// - PassengerSensorTrigger通过 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
}
}