Compare commits
3 Commits
c7c8171b8a
...
acc5471c47
| Author | SHA1 | Date | |
|---|---|---|---|
| acc5471c47 | |||
| 8ccfa22a0e | |||
| 39483c4460 |
@@ -10,24 +10,29 @@ namespace BaseGames.Core
|
||||
/// 协议(执行顺序保障):
|
||||
/// 1. MovingPlatform.FixedUpdate(-300)计算本帧期望位移 delta → 调用 SetPlatformDelta,
|
||||
/// 实现方将其转换为速度(delta / fixedDeltaTime)存入下帧缓冲区。
|
||||
/// 2. 实现方 FixedUpdate(-200)将缓冲区速度换入当前帧变量。
|
||||
/// 3. 状态机 FixedUpdate(-100)通过 Move() 将平台速度叠加到水平速度,
|
||||
/// 由 velocity 驱动运动,与 RigidbodyInterpolation2D.Interpolate 完全兼容。
|
||||
/// 2. 实现方 FixedUpdate(-200)将缓冲速度换入 _platformVelocity,清零缓冲区。
|
||||
/// 3. 状态机 FixedUpdate(-100)通过 Move(speedX) 将 _platformVelocity.x 叠加到 velocity,
|
||||
/// 与 RigidbodyInterpolation2D.Interpolate 完全兼容,无视觉抖动。
|
||||
/// _inputVelocityX(纯玩家输入)由 Move() 单独记录,UpdateFacing 读此值而非 velocity.x,
|
||||
/// 彻底避免平台速度污染朝向逻辑。
|
||||
/// 4. 乘客离开传感器时 MovingPlatform 调用 OnLeavePlatform,继承垂直速度分量;
|
||||
/// 水平分量已通过最后一次 Move() 自然携带,无需重复叠加。
|
||||
/// 水平分量已由最后一次 Move() 自然携带(包含 _platformVelocity.x),无需重复叠加。
|
||||
///
|
||||
/// 注意:不使用 _rb.position += delta 方案——Kinematic 平台通过 MovePosition 移动时,
|
||||
/// 物理引擎会通过接触力推动 Dynamic 乘客;若再手动写 position,等效于双重施加(2× 速度)。
|
||||
/// </summary>
|
||||
public interface IPassengerReceiver
|
||||
{
|
||||
/// <summary>
|
||||
/// 由 MovingPlatform 每帧推送本帧平台期望位移量。
|
||||
/// 实现方将其转换为速度并存入缓冲区,在自己的 FixedUpdate 换入后
|
||||
/// 经 Move() 叠加到水平速度,保持插值平滑。
|
||||
/// 实现方将其转换为速度(delta / fixedDeltaTime)存入缓冲区;
|
||||
/// 在自己的 FixedUpdate 换入后,由 Move() 叠加到 velocity.x,保持插值平滑。
|
||||
/// </summary>
|
||||
void SetPlatformDelta(Vector2 delta);
|
||||
|
||||
/// <summary>
|
||||
/// 乘客离开平台时调用,传入平台当前帧速度。
|
||||
/// 实现方仅需继承垂直分量;水平分量已由最后一次 Move() 自然携带。
|
||||
/// 实现方仅需继承垂直分量;水平分量已由最后一次 Move() 自然携带(包含平台 X 速度)。
|
||||
/// </summary>
|
||||
void OnLeavePlatform(Vector2 platformVelocity);
|
||||
}
|
||||
|
||||
@@ -31,10 +31,14 @@ namespace BaseGames.Player
|
||||
// 下一个 FixedUpdate(-200,先于状态机 -100)读取并清零,
|
||||
// 防止状态机用旧输入把速度重新写成非零值。
|
||||
private bool _pendingHorizontalZero;
|
||||
// 移动平台速度双缓冲:Platform(-300) 写入 _next;Player(-200) 将 _next 换入 _current 并清零 _next;
|
||||
// StateM(-100) 的 Move() 读取 _current,确保速度基于 velocity 设置,与 Interpolate 系统兼容。
|
||||
private Vector2 _platformVelocity; // 本帧消费(StateM 通过 Move() 读取)
|
||||
// 移动平台速度双缓冲:Platform(-300) 写入 _next;Player(-200) 将 _next 换入 _current 并清零;
|
||||
// 状态机(-100) 的 Move() 将 _platformVelocity.x 叠加到水平速度,保持与平台同步。
|
||||
// 使用速度(而非 _rb.position+=):Kinematic 平台的 MovePosition 已通过物理接触推动 Dynamic 乘客;
|
||||
// 若再手动写 position 会产生双重施加(2× 速度)。速度方案仅覆盖 velocity,无此问题。
|
||||
private Vector2 _platformVelocity; // 本帧消费(Move() 叠加到 velocity)
|
||||
private Vector2 _nextPlatformVelocity; // Platform(-300) 写入缓冲区
|
||||
// 玩家自身输入水平速度(不含平台分量);由 Move() 写入,UpdateFacing / ApplyAirDrag 读此值。
|
||||
private float _inputVelocityX;
|
||||
private bool _isWallLeft;
|
||||
private bool _isWallRight;
|
||||
private bool _onOneWayPlatform;
|
||||
@@ -87,15 +91,14 @@ namespace BaseGames.Player
|
||||
|
||||
private void FixedUpdate()
|
||||
{
|
||||
// 平台速度双缓冲换入:Platform(-300) 已将本帧速度写入 _next;
|
||||
// 这里将其换入 _current 供 Move() 叠加,并清零 _next 以备下帧使用。
|
||||
// 若本帧玩家已离台,Platform 不写 _next,换入结果为 Vector2.zero,自然恢复独立运动。
|
||||
// 平台速度双缓冲换入:Platform(-300) 已写入 _next;换入 _current 供 Move() 叠加。
|
||||
_platformVelocity = _nextPlatformVelocity;
|
||||
_nextPlatformVelocity = Vector2.zero;
|
||||
|
||||
// 优先处理来自 Update 的强制清零请求(在状态机 OnStateFixedUpdate 之前执行)。
|
||||
if (_pendingHorizontalZero)
|
||||
{
|
||||
_inputVelocityX = 0f;
|
||||
_rb.velocity = new Vector2(0f, _rb.velocity.y);
|
||||
_pendingHorizontalZero = false;
|
||||
}
|
||||
@@ -130,10 +133,12 @@ namespace BaseGames.Player
|
||||
/// 直接赋予目标水平速度(按键即全速,松键即停,无加速过渡)。
|
||||
/// 地面状态每帧直接到达全速;空中调用时同样即时,但配合 ApplyAirDrag
|
||||
/// 在无输入时自然减速,保留跳出时的动量。
|
||||
/// 若当前站在移动平台上,自动叠加 <c>_platformVelocity.x</c>,保持与平台同步。
|
||||
/// 若当前站在移动平台上,自动叠加 <c>_platformVelocity.x</c> 保持同步;
|
||||
/// <c>_inputVelocityX</c> 只记录玩家输入分量,供 UpdateFacing / ApplyAirDrag 使用。
|
||||
/// </summary>
|
||||
public void Move(float speedX)
|
||||
{
|
||||
_inputVelocityX = speedX;
|
||||
_rb.velocity = new Vector2(speedX + _platformVelocity.x, _rb.velocity.y);
|
||||
}
|
||||
|
||||
@@ -144,8 +149,10 @@ namespace BaseGames.Player
|
||||
/// </summary>
|
||||
public void ApplyAirDrag(float factor)
|
||||
{
|
||||
// 空中不在平台上,_platformVelocity = 0,velocity.x == _inputVelocityX;同步写回保持一致。
|
||||
float newX = _rb.velocity.x * factor;
|
||||
if (Mathf.Abs(newX) < 0.05f) newX = 0f;
|
||||
_inputVelocityX = newX;
|
||||
_rb.velocity = new Vector2(newX, _rb.velocity.y);
|
||||
}
|
||||
|
||||
@@ -180,9 +187,10 @@ namespace BaseGames.Player
|
||||
=> _rb.velocity = direction.normalized * force;
|
||||
|
||||
// ── 速度控制 ──────────────────────────────────────────────────────────
|
||||
public void ZeroVelocity() => _rb.velocity = Vector2.zero;
|
||||
public void ZeroVelocity() { _inputVelocityX = 0f; _rb.velocity = Vector2.zero; }
|
||||
public void ZeroHorizontalVelocity()
|
||||
{
|
||||
_inputVelocityX = 0f;
|
||||
_rb.velocity = new Vector2(0f, _rb.velocity.y);
|
||||
// 设置标记:下一个 FixedUpdate 开头再次强制清零,
|
||||
// 防止因读到旧输入而把速度重新写成非零值。
|
||||
@@ -192,9 +200,8 @@ namespace BaseGames.Player
|
||||
// ── 朝向 ──────────────────────────────────────────────────────────────
|
||||
public void UpdateFacing()
|
||||
{
|
||||
// 减去平台速度分量,只根据玩家自身输入速度判断朝向;
|
||||
// 否则站在横向移动平台上静止时会被平台速度驱动朝向翻转。
|
||||
float vx = _rb.velocity.x - _platformVelocity.x;
|
||||
// 读取玩家输入速度(不含平台分量),避免平台横向运动驱动朝向翻转。
|
||||
float vx = _inputVelocityX;
|
||||
if (Mathf.Abs(vx) < 0.1f) return;
|
||||
int dir = vx > 0f ? 1 : -1;
|
||||
if (dir == _facingDirection) return;
|
||||
@@ -238,17 +245,14 @@ namespace BaseGames.Player
|
||||
/// <summary>
|
||||
/// MovingPlatform.FixedUpdate(-300) 推送本帧平台期望位移。
|
||||
/// 转换为速度(delta / fixedDeltaTime)写入下帧缓冲区 _next,
|
||||
/// 在本类 FixedUpdate(-200) 换入 _current,由 Move() 叠加到水平速度。
|
||||
/// 基于 velocity 的方案与 RigidbodyInterpolation2D.Interpolate 完全兼容,
|
||||
/// 消除直接写 _rb.position 导致的视觉抖动/速度不一致。
|
||||
/// 在本类 FixedUpdate(-200) 换入,由 Move() 叠加到水平速度。
|
||||
/// </summary>
|
||||
public void SetPlatformDelta(Vector2 delta)
|
||||
=> _nextPlatformVelocity += delta / Time.fixedDeltaTime;
|
||||
|
||||
/// <summary>
|
||||
/// 乘客离开平台时调用。
|
||||
/// 水平速度已通过 Move() 自然继承(最后一次 Move 调用已包含 platformVelocity.x),
|
||||
/// 此处仅继承垂直分量,保证从竖向移动平台离台时无速度突变。
|
||||
/// 乘客离开平台时调用,传入平台当前帧速度。
|
||||
/// 水平速度已由最后一次 Move() 自然携带(包含 _platformVelocity.x),仅继承垂直分量。
|
||||
/// </summary>
|
||||
public void OnLeavePlatform(Vector2 platformVelocity)
|
||||
=> _rb.velocity += new Vector2(0f, platformVelocity.y);
|
||||
|
||||
@@ -76,7 +76,13 @@ namespace BaseGames.Player.States
|
||||
TryConsumeCancelInput();
|
||||
}
|
||||
|
||||
public override void OnStateFixedUpdate() { }
|
||||
public override void OnStateFixedUpdate()
|
||||
{
|
||||
// 攻击动画播放期间无水平输入,仍需每帧调用 Move(0) 叠加 _platformVelocity.x,
|
||||
// 使玩家在移动平台上攻击时仍随平台同步移动。
|
||||
if (Move.IsGrounded)
|
||||
Move.Move(0f);
|
||||
}
|
||||
|
||||
// ── 内部 ──────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -68,7 +68,13 @@ namespace BaseGames.Player.States
|
||||
// OnStateUpdate 中仍保留同样的检测作为跨帧补充,两者不会发生双重转换:
|
||||
// 本帧若在 FixedUpdate 中转换到 FallState,Update 调用的将是 FallState.OnStateUpdate()。
|
||||
if (!Move.IsGrounded)
|
||||
{
|
||||
_owner.TransitionTo(_owner.GetState<FallState>());
|
||||
return;
|
||||
}
|
||||
// 无水平输入时仍需每帧调用 Move(0),使 _platformVelocity.x 被叠加进 velocity.x,
|
||||
// 确保站在移动平台上的玩家随平台同步移动,不产生相对位移。
|
||||
Move.Move(0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,5 +42,13 @@ namespace BaseGames.Player.States
|
||||
{
|
||||
Owner.TransitionTo(Owner.GetState<IdleState>());
|
||||
}
|
||||
|
||||
public override void OnStateFixedUpdate()
|
||||
{
|
||||
// 弹反预备期间无水平输入,仍需每帧调用 Move(0) 叠加 _platformVelocity.x,
|
||||
// 使玩家在移动平台上弹反时随平台同步移动。
|
||||
if (Move != null && Move.IsGrounded)
|
||||
Move.Move(0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,5 +49,12 @@ namespace BaseGames.Player.States
|
||||
else
|
||||
Owner.TransitionTo(Owner.GetState<FallState>());
|
||||
}
|
||||
|
||||
public override void OnStateFixedUpdate()
|
||||
{
|
||||
// 地面上劈期间调用 Move(0) 叠加 _platformVelocity.x,使玩家随移动平台同步移动。
|
||||
if (Move != null && Move.IsGrounded)
|
||||
Move.Move(0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ namespace BaseGames.World
|
||||
[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;
|
||||
@@ -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<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()
|
||||
@@ -147,6 +165,10 @@ namespace BaseGames.World
|
||||
// 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);
|
||||
}
|
||||
@@ -164,6 +186,37 @@ namespace BaseGames.World
|
||||
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()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user