实现移动平台乘客接口,优化乘客跟随逻辑

This commit is contained in:
2026-05-21 17:09:06 +08:00
parent fcd3e2dcdd
commit 247a218182
5 changed files with 122 additions and 31 deletions

View File

@@ -0,0 +1,29 @@
using UnityEngine;
namespace BaseGames.Core
{
/// <summary>
/// 移动平台乘客接口。
/// 实现此接口的对象可被 <see cref="BaseGames.World.MovingPlatform"/> 携带,
/// 无需修改 Transform 父子关系,避免与动画/摄像机/HitBox 系统冲突。
///
/// 协议(执行顺序保障):
/// 1. MovingPlatform.FixedUpdate-300计算本帧期望位移 delta → 调用 SetPlatformDelta。
/// 2. 实现方 FixedUpdate-200最前端消费 delta_rb.position += delta。
/// 3. 乘客离开传感器时 MovingPlatform 调用 OnLeavePlatform继承平台速度防止卡顿。
/// </summary>
public interface IPassengerReceiver
{
/// <summary>
/// 由 MovingPlatform 每帧推送本帧平台期望位移量(不依赖 MovePosition 是否已执行)。
/// 实现方存入待处理字段,在自己的 FixedUpdate 最前端消费,避免与 velocity 冲突。
/// </summary>
void SetPlatformDelta(Vector2 delta);
/// <summary>
/// 乘客离开平台时调用,传入平台当前帧速度,由实现方叠加到自身速度。
/// 防止离台瞬间速度突变产生卡顿。
/// </summary>
void OnLeavePlatform(Vector2 platformVelocity);
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3f719c44d9262914baec9ba887be8c54
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -804,6 +804,7 @@ namespace BaseGames.Editor
MovingPlatform platform = GetOrAddComponent<MovingPlatform>(go); MovingPlatform platform = GetOrAddComponent<MovingPlatform>(go);
AssignReference(platform, "_passengerSensor", sensorCol, report); AssignReference(platform, "_passengerSensor", sensorCol, report);
AssignLayerMask(platform, "_passengerLayer", new[] { "Player", "Enemy" }, report);
AssignObjectArray(platform, "_wayPoints", new Object[] { wpA, wpB }, report); AssignObjectArray(platform, "_wayPoints", new Object[] { wpA, wpB }, report);
report.Add("WaypointA / WaypointB 为移动端点,可将其拖出平台并在场景中调整位置。"); report.Add("WaypointA / WaypointB 为移动端点,可将其拖出平台并在场景中调整位置。");

View File

@@ -11,7 +11,7 @@ namespace BaseGames.Player
// 开头能在状态机写入速度之前先应用"强制清零"标记。 // 开头能在状态机写入速度之前先应用"强制清零"标记。
[DefaultExecutionOrder(-200)] [DefaultExecutionOrder(-200)]
[RequireComponent(typeof(Rigidbody2D))] [RequireComponent(typeof(Rigidbody2D))]
public class PlayerMovement : MonoBehaviour public class PlayerMovement : MonoBehaviour, IPassengerReceiver
{ {
[Header("配置")] [Header("配置")]
[SerializeField] private PlayerMovementConfigSO _config; [SerializeField] private PlayerMovementConfigSO _config;
@@ -31,6 +31,7 @@ namespace BaseGames.Player
// 下一个 FixedUpdate-200先于状态机 -100读取并清零 // 下一个 FixedUpdate-200先于状态机 -100读取并清零
// 防止状态机用旧输入把速度重新写成非零值。 // 防止状态机用旧输入把速度重新写成非零值。
private bool _pendingHorizontalZero; private bool _pendingHorizontalZero;
private Vector2 _pendingPlatformDelta; // MovingPlatform 推送的本帧位移,在 FixedUpdate 最前端消费
private bool _isWallLeft; private bool _isWallLeft;
private bool _isWallRight; private bool _isWallRight;
private bool _onOneWayPlatform; private bool _onOneWayPlatform;
@@ -83,6 +84,13 @@ namespace BaseGames.Player
private void FixedUpdate() private void FixedUpdate()
{ {
// 消费移动平台推送的位移(在 velocity 系统之前直接写 position避免冲突
if (_pendingPlatformDelta != Vector2.zero)
{
_rb.position += _pendingPlatformDelta;
_pendingPlatformDelta = Vector2.zero;
}
// 优先处理来自 Update 的强制清零请求(在状态机 OnStateFixedUpdate 之前执行)。 // 优先处理来自 Update 的强制清零请求(在状态机 OnStateFixedUpdate 之前执行)。
if (_pendingHorizontalZero) if (_pendingHorizontalZero)
{ {
@@ -221,6 +229,17 @@ namespace BaseGames.Player
/// <summary>消耗墙壁土狼时间,防止同一帧被多次触发。</summary> /// <summary>消耗墙壁土狼时间,防止同一帧被多次触发。</summary>
public void ConsumeWallCoyote() => _wallCoyoteTimer = 0f; public void ConsumeWallCoyote() => _wallCoyoteTimer = 0f;
// ── IPassengerReceiver ────────────────────────────────────────────────
/// <summary>
/// MovingPlatform.FixedUpdate(-300) 推送本帧平台位移。
/// 在本类 FixedUpdate(-200) 最前端消费,直接写 _rb.position
/// 与 velocity 系统完全隔离,避免 Dynamic Rigidbody 上 MovePosition + velocity 叠加。
/// </summary>
public void SetPlatformDelta(Vector2 delta) => _pendingPlatformDelta += delta;
/// <summary>离开移动平台时继承平台速度,防止速度骤变卡顿。</summary>
public void OnLeavePlatform(Vector2 platformVelocity) => _rb.velocity += platformVelocity;
// ── 冲刺 ────────────────────────────────────────────────────────────── // ── 冲刺 ──────────────────────────────────────────────────────────────
/// <summary> /// <summary>
/// 施加冲刺速度DashState/DashState 调用)。 /// 施加冲刺速度DashState/DashState 调用)。

View File

@@ -1,5 +1,6 @@
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using BaseGames.Core;
using BaseGames.Core.Events; using BaseGames.Core.Events;
using UnityEngine; using UnityEngine;
@@ -7,8 +8,16 @@ namespace BaseGames.World
{ {
/// <summary> /// <summary>
/// 移动平台。Kinematic Rigidbody2D支持三种移动模式。 /// 移动平台。Kinematic Rigidbody2D支持三种移动模式。
/// 乘客跟随OnTriggerEnter2D 时 SetParent离开时还原并附加速度。 ///
/// 乘客跟随Delta Position 方案,替代 SetParent
/// - PassengerSensorTrigger检测进入乘客<see cref="IPassengerReceiver"/>)。
/// - FixedUpdate(-300) 计算本帧期望位移 delta推送给所有乘客再执行 MovePosition。
/// - 乘客在自己的 FixedUpdate(-200) 最前端消费 delta_rb.position += delta。
/// - 乘客离台时通过 OnLeavePlatform 继承平台速度,避免卡顿。
///
/// 此方案不修改 Transform 层级,与动画/摄像机/HitBox 系统完全兼容。
/// </summary> /// </summary>
[DefaultExecutionOrder(-300)]
[RequireComponent(typeof(Rigidbody2D))] [RequireComponent(typeof(Rigidbody2D))]
public class MovingPlatform : MonoBehaviour public class MovingPlatform : MonoBehaviour
{ {
@@ -24,15 +33,18 @@ namespace BaseGames.World
[SerializeField] private VoidEventChannelSO _activationChannel; [SerializeField] private VoidEventChannelSO _activationChannel;
[Header("乘客检测")] [Header("乘客检测")]
[SerializeField] private BoxCollider2D _passengerSensor; // IsTrigger用于乘客 SetParent [SerializeField] private BoxCollider2D _passengerSensor; // isTrigger检测乘客
[SerializeField] private LayerMask _passengerLayer; // 应包含 Player + Enemy 层
private Rigidbody2D _rb; private Rigidbody2D _rb;
private List<Transform> _passengers = new(); private readonly List<IPassengerReceiver> _passengers = new();
private int _waypointIndex; private readonly List<IPassengerReceiver> _passengerSnapshot = new(); // 迭代快照,防并发修改
private bool _movingForward = true; private int _waypointIndex;
private bool _triggered; private bool _movingForward = true;
private bool _waiting; private bool _triggered;
private WaitForSeconds _waitForEndpoint; private bool _waiting;
private Vector2 _frameVelocity; // 本帧实际速度,供离台时继承
private WaitForSeconds _waitForEndpoint;
private readonly CompositeDisposable _subs = new(); private readonly CompositeDisposable _subs = new();
private void Awake() private void Awake()
@@ -55,17 +67,40 @@ namespace BaseGames.World
private void FixedUpdate() private void FixedUpdate()
{ {
if (_wayPoints == null || _wayPoints.Length == 0) return; if (_wayPoints == null || _wayPoints.Length == 0)
if (_moveType == MoveType.TriggeredLinear && !_triggered) return; {
if (_waiting) return; _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 target = (Vector2)_wayPoints[_waypointIndex].position;
var next = Vector2.MoveTowards(_rb.position, target, _speed * Time.fixedDeltaTime); 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); _rb.MovePosition(next);
if (Vector2.Distance(_rb.position, target) < 0.02f) if (Vector2.Distance(_rb.position, target) < 0.02f)
@@ -103,32 +138,27 @@ namespace BaseGames.World
private void OnTriggered() => _triggered = true; private void OnTriggered() => _triggered = true;
// ── Passenger Pattern ───────────────────────────────────────────────── // ── Passenger Detection ───────────────────────────────────────────────
private void OnTriggerEnter2D(Collider2D other) private void OnTriggerEnter2D(Collider2D other)
{ {
if (!IsPassenger(other.gameObject)) return; if (!IsPassengerLayer(other)) return;
other.transform.SetParent(transform); if (!other.TryGetComponent<IPassengerReceiver>(out var receiver)) return;
_passengers.Add(other.transform); if (!_passengers.Contains(receiver))
_passengers.Add(receiver);
} }
private void OnTriggerExit2D(Collider2D other) private void OnTriggerExit2D(Collider2D other)
{ {
if (!_passengers.Contains(other.transform)) return; if (!other.TryGetComponent<IPassengerReceiver>(out var receiver)) return;
if (!_passengers.Remove(receiver)) return;
other.transform.SetParent(null); // 继承平台速度,防止离台时速度骤变卡顿
_passengers.Remove(other.transform); receiver.OnLeavePlatform(_frameVelocity);
// 离开时附加平台速度,避免卡顿
var passengerRb = other.GetComponentInParent<Rigidbody2D>();
if (passengerRb != null)
passengerRb.AddForce(_rb.velocity, ForceMode2D.Impulse);
} }
private static bool IsPassenger(GameObject go) private bool IsPassengerLayer(Collider2D col)
{ => (_passengerLayer.value & (1 << col.gameObject.layer)) != 0;
return go.CompareTag("Player") || go.CompareTag("Enemy");
}
#if UNITY_EDITOR #if UNITY_EDITOR
private void OnDrawGizmos() private void OnDrawGizmos()
@@ -144,3 +174,4 @@ namespace BaseGames.World
#endif #endif
} }
} }