617 lines
31 KiB
C#
617 lines
31 KiB
C#
using BaseGames.Core;
|
||
using UnityEngine;
|
||
|
||
namespace BaseGames.Player
|
||
{
|
||
/// <summary>
|
||
/// 玩家物理移动组件。封装 Rigidbody2D 操作,提供跑动、跳跃、击退等接口。
|
||
|
||
/// </summary>
|
||
// 执行顺序必须早于 PlayerController(-100),确保每帧 FixedUpdate
|
||
// 开头能在状态机写入速度之前先应用"强制清零"标记。
|
||
[DefaultExecutionOrder(-200)]
|
||
[RequireComponent(typeof(Rigidbody2D))]
|
||
public class PlayerMovement : MonoBehaviour, IPassengerReceiver
|
||
{
|
||
[Header("配置")]
|
||
[SerializeField] private PlayerMovementConfigSO _config;
|
||
|
||
[Header("地面检测")]
|
||
[SerializeField] private Transform _groundCheck;
|
||
[SerializeField] private Vector2 _groundCheckSize = new Vector2(0.8f, 0.05f);
|
||
[SerializeField] private LayerMask _groundLayer;
|
||
|
||
// ── 运行时状态 ────────────────────────────────────────────────────────
|
||
private Rigidbody2D _rb;
|
||
private float _coyoteTimer;
|
||
private float _wallCoyoteTimer;
|
||
private int _wallCoyoteDir; // 离墙时记录的墙壁方向(+1 右 / -1 左)
|
||
private bool _isGrounded;
|
||
// Update 中调用 ZeroHorizontalVelocity 后设置此标记;
|
||
// 下一个 FixedUpdate(-200,先于状态机 -100)读取并清零,
|
||
// 防止状态机用旧输入把速度重新写成非零值。
|
||
private bool _pendingHorizontalZero;
|
||
// 移动平台速度双缓冲: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;
|
||
private int _facingDirection = 1;
|
||
private bool _facingLocked; // 为 true 时 UpdateFacing() 不覆盖朝向
|
||
private bool _cancelWindowOpen;
|
||
private SurfaceType _currentSurface = SurfaceType.Ground;
|
||
private bool _wasGrounded;
|
||
// 跳跃/二段跳期间禁用斜坡吸附,防止把起跳判定成斜坡而立即下压
|
||
private bool _slopeSnapDisabled;
|
||
private readonly Collider2D[] _groundBuffer = new Collider2D[4];
|
||
private int _groundHitCount;
|
||
private readonly ContactPoint2D[] _slopeContactBuffer = new ContactPoint2D[8];
|
||
private readonly ContactPoint2D[] _wallContactBuffer = new ContactPoint2D[8];
|
||
// 跳跃上升阶段贴墙时保护 vy:物理摩擦会在碰墙瞬间降低垂直速度,
|
||
// 通过 OnCollisionEnter/Stay2D 将 vy 恢复到碰撞前的值。
|
||
private float _savedVy;
|
||
private bool _preserveVyOnWallContact;
|
||
|
||
#if UNITY_EDITOR
|
||
// ── 运行时调试(Inspector 中可见)───────────────────────────────
|
||
[Header("\u2500\u2500 \u8fd0\u884c\u65f6\u8c03\u8bd5 \u2500\u2500")]
|
||
[SerializeField] private string _dbg_Position;
|
||
[SerializeField] private float _dbg_VelocityX;
|
||
[SerializeField] private float _dbg_VelocityY;
|
||
[SerializeField] private bool _dbg_IsGrounded;
|
||
[SerializeField] private bool _dbg_OnOneWayPlatform;
|
||
[SerializeField] private bool _dbg_HasCoyoteTime;
|
||
[SerializeField] private bool _dbg_HasWallCoyoteTime;
|
||
[SerializeField] private bool _dbg_IsWallLeft;
|
||
[SerializeField] private bool _dbg_IsWallRight;
|
||
[SerializeField] private bool _dbg_CancelWindowOpen;
|
||
[SerializeField] private int _dbg_FacingDirection;
|
||
#endif
|
||
|
||
public bool IsGrounded => _isGrounded;
|
||
public bool HasCoyoteTime => _coyoteTimer > 0f;
|
||
public bool HasWallCoyoteTime => _wallCoyoteTimer > 0f;
|
||
public int WallCoyoteDir => _wallCoyoteDir;
|
||
public bool IsWallLeft => _isWallLeft;
|
||
public bool IsWallRight => _isWallRight;
|
||
public bool OnOneWayPlatform => _onOneWayPlatform;
|
||
public int FacingDirection => _facingDirection;
|
||
public Rigidbody2D Rb => _rb;
|
||
public bool CancelWindowOpen => _cancelWindowOpen;
|
||
public SurfaceType CurrentSurface => _currentSurface;
|
||
/// <summary>垂直速度大于零(处于上升弧段)。供 WallJumpState 判断起跳是否结束。</summary>
|
||
public bool IsRising => _rb.velocity.y > 0f;
|
||
|
||
private void Awake()
|
||
{
|
||
Debug.Assert(_config != null, "[PlayerMovement] _config 未赋值,请在 Inspector 中指定 PlayerMovementConfigSO。", this);
|
||
Debug.Assert(_groundCheck != null, "[PlayerMovement] GroundCheck 子节点未赋值,地面检测将无法工作,请在 Inspector 中指定 GroundCheck Transform。", this);
|
||
_rb = GetComponent<Rigidbody2D>();
|
||
// 开启位置插值:在物理帧(50Hz)与渲染帧(60Hz+)之间平滑视觉位置,消除跳帧抖动。
|
||
// SpritePixelSnapper(LateUpdate +1000)在插值结果基础上吸附到像素网格,
|
||
// 与 CameraPixelSnapper 同格对齐,消除亚像素模糊;停止时 ≤2 帧像素追赶不可感知。
|
||
_rb.interpolation = RigidbodyInterpolation2D.Interpolate;
|
||
// 开启连续碰撞检测(CCD):Kinematic 移动平台通过 MovePosition 将 Dynamic 角色推向墙体时,
|
||
// CCD 会沿移动路径追踪碰撞,确保角色在物理层被墙体表面拦截,而不是在离散步骤中穿透墙体。
|
||
_rb.collisionDetectionMode = CollisionDetectionMode2D.Continuous;
|
||
}
|
||
|
||
private void FixedUpdate()
|
||
{
|
||
// 平台速度双缓冲换入: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;
|
||
}
|
||
|
||
// 保存本帧物理步开始前的垂直速度,用于 OnCollisionEnter/Stay2D 中恢复被墙壁
|
||
// 摩擦力降低的 vy(状态机 -100 比本脚本 -200 晚执行,不会影响此处的读取时机)。
|
||
_savedVy = _rb.velocity.y;
|
||
|
||
CheckGrounded();
|
||
CheckWalls();
|
||
|
||
if (_isGrounded)
|
||
_coyoteTimer = _config.CoyoteTime;
|
||
else
|
||
_coyoteTimer = Mathf.Max(0f, _coyoteTimer - Time.fixedDeltaTime);
|
||
|
||
_wallCoyoteTimer = Mathf.Max(0f, _wallCoyoteTimer - Time.fixedDeltaTime);
|
||
|
||
#if UNITY_EDITOR
|
||
// 值类型字段每帧同步(无分配)
|
||
_dbg_VelocityX = _rb.velocity.x;
|
||
_dbg_VelocityY = _rb.velocity.y;
|
||
_dbg_IsGrounded = _isGrounded;
|
||
_dbg_OnOneWayPlatform = _onOneWayPlatform;
|
||
_dbg_HasCoyoteTime = _coyoteTimer > 0f;
|
||
_dbg_HasWallCoyoteTime = _wallCoyoteTimer > 0f;
|
||
_dbg_IsWallLeft = _isWallLeft;
|
||
_dbg_IsWallRight = _isWallRight;
|
||
_dbg_CancelWindowOpen = _cancelWindowOpen;
|
||
_dbg_FacingDirection = _facingDirection;
|
||
// 字符串格式化限速到 ~10 Hz,避免每帧分配
|
||
if (Time.frameCount % 6 == 0)
|
||
_dbg_Position = $"({transform.position.x:F1}, {transform.position.y:F1})";
|
||
#endif
|
||
}
|
||
|
||
// ── 移动 ──────────────────────────────────────────────────────────────
|
||
/// <summary>
|
||
/// 直接赋予目标水平速度(按键即全速,松键即停,无加速过渡)。
|
||
/// 地面状态每帧直接到达全速;空中调用时同样即时,但配合 ApplyAirDrag
|
||
/// 在无输入时自然减速,保留跳出时的动量。
|
||
/// 若当前站在移动平台上,自动叠加 <c>_platformVelocity.x</c> 保持同步;
|
||
/// <c>_inputVelocityX</c> 只记录玩家输入分量,供 UpdateFacing / ApplyAirDrag 使用。
|
||
/// 当平台速度方向与已贴墙方向相同时,不叠加平台水平速度,
|
||
/// 防止速度叠加把角色推入墙体(物理接触已被 CCD 拦截,但速度覆盖会继续施力)。
|
||
/// </summary>
|
||
public void Move(float speedX)
|
||
{
|
||
_inputVelocityX = speedX;
|
||
|
||
// 若平台将角色推向已贴墙一侧,忽略平台水平分量:
|
||
// 物理层(CCD + 碰撞体)已阻止角色穿墙,速度层若继续施加同向力,
|
||
// 会在每帧产生累积穿透,导致角色卡入或被弹出墙体。
|
||
float platformX = _platformVelocity.x;
|
||
if ((platformX > 0f && _isWallRight) || (platformX < 0f && _isWallLeft))
|
||
platformX = 0f;
|
||
|
||
_rb.velocity = new Vector2(speedX + platformX, _rb.velocity.y);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 空中无输入时施加空气阻力:水平速度乘以 <paramref name="factor"/>,
|
||
/// 低于阈值时归零,避免速度无限趋近 0。
|
||
/// 在 FallState / JumpState / WallJumpState 的 OnStateFixedUpdate 中调用。
|
||
/// </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);
|
||
}
|
||
|
||
// ── 跳跃 ──────────────────────────────────────────────────────────────
|
||
public void Jump(bool isVariable = true)
|
||
{
|
||
_rb.velocity = new Vector2(_rb.velocity.x, _config.JumpForce);
|
||
_coyoteTimer = 0f;
|
||
_slopeSnapDisabled = true;
|
||
}
|
||
|
||
public void CutJump()
|
||
{
|
||
if (_rb.velocity.y > 0f)
|
||
_rb.velocity = new Vector2(_rb.velocity.x, _rb.velocity.y * _config.JumpCutMultiplier);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 二段跳。覆盖当前垂直速度为 DoubleJumpForce。
|
||
/// FallState / JumpState 在检测到 HasAbility(DoubleJump) && AirJumpsLeft > 0 时调用。
|
||
/// </summary>
|
||
public void DoubleJump()
|
||
{
|
||
_rb.velocity = new Vector2(_rb.velocity.x, _config.DoubleJumpForce);
|
||
_coyoteTimer = 0f;
|
||
_slopeSnapDisabled = true;
|
||
}
|
||
|
||
// ── 重力 ──────────────────────────────────────────────────────────────
|
||
public void SetGravityScale(float scale) => _rb.gravityScale = scale;
|
||
|
||
// ── 击退 ──────────────────────────────────────────────────────────────
|
||
public void ApplyKnockback(Vector2 direction, float force)
|
||
=> _rb.velocity = direction.normalized * force;
|
||
|
||
// ── 速度控制 ──────────────────────────────────────────────────────────
|
||
public void ZeroVelocity() { _inputVelocityX = 0f; _rb.velocity = Vector2.zero; }
|
||
public void ZeroHorizontalVelocity()
|
||
{
|
||
_inputVelocityX = 0f;
|
||
_rb.velocity = new Vector2(0f, _rb.velocity.y);
|
||
// 设置标记:下一个 FixedUpdate 开头再次强制清零,
|
||
// 防止因读到旧输入而把速度重新写成非零值。
|
||
_pendingHorizontalZero = true;
|
||
}
|
||
|
||
// ── 朝向 ──────────────────────────────────────────────────────────────
|
||
public void UpdateFacing()
|
||
{
|
||
if (_facingLocked) return;
|
||
// 读取玩家输入速度(不含平台分量),避免平台横向运动驱动朝向翻转。
|
||
float vx = _inputVelocityX;
|
||
if (Mathf.Abs(vx) < 0.1f) return;
|
||
int dir = vx > 0f ? 1 : -1;
|
||
if (dir == _facingDirection) return;
|
||
_facingDirection = dir;
|
||
// 翻转整个 Transform(而非仅 SpriteRenderer.flipX),
|
||
// 确保 WeaponSocket / SkillSocket 等所有子节点的 HitBox 及投射物生成点随朝向镜像。
|
||
transform.localScale = new Vector3(dir, 1f, 1f);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 强制设定朝向,并同步清零速度,确保传送/出生后不被旧速度立即覆盖。
|
||
/// dir:+1 = 朝右,-1 = 朝左。
|
||
/// </summary>
|
||
public void SetFacingImmediate(int dir)
|
||
{
|
||
if (dir == 0) return;
|
||
_facingDirection = dir;
|
||
transform.localScale = new Vector3(dir, 1f, 1f);
|
||
_rb.velocity = Vector2.zero;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 仅翻转视觉朝向,不修改速度。用于蹬墙跳瞬间需要立即转向而不中断跳跃速度的场景。
|
||
/// dir:+1 = 朝右,-1 = 朝左。
|
||
/// </summary>
|
||
public void FlipFacing(int dir)
|
||
{
|
||
if (dir == 0 || dir == _facingDirection) return;
|
||
_facingDirection = dir;
|
||
transform.localScale = new Vector3(dir, 1f, 1f);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 锁定/解锁自动朝向(UpdateFacing)。
|
||
/// 传入 true 后 UpdateFacing 不再根据输入速度覆盖朝向,
|
||
/// 直到传入 false 解锁。适用于抓墙、蹬墙跳等需要手动控制朝向的状态。
|
||
/// </summary>
|
||
public void LockFacing(bool locked) => _facingLocked = locked;
|
||
|
||
// ── 取消窗口 ──────────────────────────────────────────────────────────
|
||
public void SetCancelWindowOpen(bool open) => _cancelWindowOpen = open;
|
||
|
||
// ── 墙壁土狼时间 ──────────────────────────────────────────────────────
|
||
/// <summary>
|
||
/// 由 WallSlideState 在切换到 FallState 时调用:启动墙壁土狼计时器。
|
||
/// 窗口内(HasWallCoyoteTime == true)按跳跃键仍可触发蹬墙跳。
|
||
/// wallDir:+1 = 右墙 / -1 = 左墙。
|
||
/// </summary>
|
||
public void StartWallCoyote(int wallDir)
|
||
{
|
||
_wallCoyoteTimer = _config.WallCoyoteTime;
|
||
_wallCoyoteDir = wallDir;
|
||
}
|
||
|
||
/// <summary>消耗墙壁土狼时间,防止同一帧被多次触发。</summary>
|
||
public void ConsumeWallCoyote() => _wallCoyoteTimer = 0f;
|
||
|
||
// ── 跳跃上升贴墙 vy 保护 ──────────────────────────────────────────────
|
||
/// <summary>
|
||
/// JumpState.OnStateEnter/Exit 调用,开启/关闭跳跃上升阶段的 vy 保护。
|
||
/// 开启后,OnCollisionEnter/Stay2D 检测到水平墙壁接触且角色有朝墙速度时,
|
||
/// 将 vy 恢复到本帧物理步前的值,消除物理摩擦对跳跃最高点的影响。
|
||
/// </summary>
|
||
public void SetPreserveVyOnWallContact(bool preserve)
|
||
=> _preserveVyOnWallContact = preserve;
|
||
|
||
private void OnCollisionEnter2D(Collision2D collision)
|
||
=> TryRestoreVyFromWallFriction(collision);
|
||
|
||
private void OnCollisionStay2D(Collision2D collision)
|
||
=> TryRestoreVyFromWallFriction(collision);
|
||
|
||
/// <summary>
|
||
/// 检测水平墙壁碰撞时是否因摩擦力降低了 vy,若是则恢复到碰撞前的值。
|
||
/// 只在角色确实以朝向墙壁的水平速度(_inputVelocityX)发生碰撞时才恢复,
|
||
/// 防止 ZeroHVel 正常工作(vx=0,无摩擦)的帧中错误地抵消重力。
|
||
/// </summary>
|
||
private void TryRestoreVyFromWallFriction(Collision2D collision)
|
||
{
|
||
if (!_preserveVyOnWallContact || _savedVy <= 0f) return;
|
||
for (int i = 0; i < collision.contactCount; i++)
|
||
{
|
||
float nx = collision.GetContact(i).normal.x;
|
||
if (Mathf.Abs(nx) > 0.5f)
|
||
{
|
||
// 法线朝右(nx > 0.5)= 左侧墙,角色朝左运动时产生摩擦(vx < 0)
|
||
// 法线朝左(nx < -0.5)= 右侧墙,角色朝右运动时产生摩擦(vx > 0)
|
||
bool hadVelocityIntoWall = (nx > 0.5f && _inputVelocityX < -0.1f)
|
||
|| (nx < -0.5f && _inputVelocityX > 0.1f);
|
||
if (hadVelocityIntoWall)
|
||
{
|
||
_rb.velocity = new Vector2(_rb.velocity.x, _savedVy);
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// ── IPassengerReceiver ────────────────────────────────────────────────
|
||
/// <summary>
|
||
/// MovingPlatform.FixedUpdate(-300) 推送本帧平台期望位移。
|
||
/// 转换为速度(delta / fixedDeltaTime)写入下帧缓冲区 _next,
|
||
/// 在本类 FixedUpdate(-200) 换入,由 Move() 叠加到水平速度。
|
||
/// </summary>
|
||
public void SetPlatformDelta(Vector2 delta)
|
||
=> _nextPlatformVelocity += delta / Time.fixedDeltaTime;
|
||
|
||
/// <summary>
|
||
/// 乘客离开平台时调用,传入平台当前帧速度。
|
||
/// 水平速度已由最后一次 Move() 自然携带(包含 _platformVelocity.x),仅继承垂直分量。
|
||
/// </summary>
|
||
public void OnLeavePlatform(Vector2 platformVelocity)
|
||
=> _rb.velocity += new Vector2(0f, platformVelocity.y);
|
||
|
||
// ── 冲刺 ──────────────────────────────────────────────────────────────
|
||
/// <summary>
|
||
/// 施加冲刺速度(DashState/DashState 调用)。
|
||
/// direction 应为归一化水平向量(±1, 0)。
|
||
/// 冲刺期间调用 SetGravityScale(0) 可关闭重力;结束后恢复默认重力。
|
||
/// </summary>
|
||
public void Dash(Vector2 direction, float speed)
|
||
{
|
||
_rb.velocity = new Vector2(direction.x * speed, 0f);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 施加向下冲刺速度(DownDashState 调用)。
|
||
/// 冲刺期间调用 SetGravityScale(0) 可关闭重力;结束后恢复默认重力。
|
||
/// </summary>
|
||
public void DownDash(float speed)
|
||
{
|
||
_rb.velocity = new Vector2(0f, -speed);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 壁滑:将垂直速度限制为 -speed(每帧调用以约束最大下滑速度)。
|
||
/// WallSlideState.OnStateFixedUpdate 根据正常/受限模式传入不同速度。
|
||
/// </summary>
|
||
public void ApplyWallSlide(float speed)
|
||
{
|
||
float targetY = -speed;
|
||
if (_rb.velocity.y < targetY)
|
||
_rb.velocity = new Vector2(_rb.velocity.x, targetY);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 背墙跳(Jump Away):远离墙壁斜上方弹出。
|
||
/// wallDir = +1 (右墙) 或 -1 (左墙),水平方向与之相反。
|
||
/// </summary>
|
||
public void WallJumpAway(int wallDir)
|
||
{
|
||
_inputVelocityX = -wallDir * _config.WallJumpAwayForceX;
|
||
_rb.velocity = new Vector2(_inputVelocityX, _config.WallJumpAwayForceY);
|
||
_coyoteTimer = 0f;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 对墙跳(Jump Toward):沿墙壁方向偏向正上方弹出,水平分量较小。
|
||
/// wallDir = +1 (右墙) 或 -1 (左墙),水平方向与之相同。
|
||
/// </summary>
|
||
public void WallJumpToward(int wallDir)
|
||
{
|
||
_inputVelocityX = wallDir * _config.WallJumpTowardForceX;
|
||
_rb.velocity = new Vector2(_inputVelocityX, _config.WallJumpTowardForceY);
|
||
_coyoteTimer = 0f;
|
||
}
|
||
|
||
/// <summary>将垂直速度归零(抓墙悬挂时每帧调用,防止下滑)。</summary>
|
||
public void ZeroVerticalVelocity()
|
||
{
|
||
if (_rb.velocity.y < 0f)
|
||
_rb.velocity = new Vector2(_rb.velocity.x, 0f);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 单向平台穿落(↓ + 跳跃键触发)。
|
||
/// 找到脚下的 <see cref="IDropThrough"/> 并临时禁用其碰撞器;
|
||
/// 同帧清除 IsGrounded / OnOneWayPlatform,状态机无需等到下一物理帧即可转换到 FallState。
|
||
/// </summary>
|
||
public void DropThroughPlatform()
|
||
{
|
||
if (!_onOneWayPlatform) return;
|
||
|
||
for (int i = 0; i < _groundHitCount; i++)
|
||
{
|
||
if (_groundBuffer[i] != null &&
|
||
_groundBuffer[i].TryGetComponent<IDropThrough>(out var platform))
|
||
{
|
||
platform.TriggerDropThrough();
|
||
// 立即清除着地状态,让状态机本帧即可切换到 FallState
|
||
_isGrounded = false;
|
||
_onOneWayPlatform = false;
|
||
_coyoteTimer = 0f;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
// ── Physics 检测 ──────────────────────────────────────────────────────
|
||
private void CheckGrounded()
|
||
{
|
||
if (_groundCheck == null) return;
|
||
|
||
_wasGrounded = _isGrounded;
|
||
|
||
_groundHitCount = Physics2D.OverlapBoxNonAlloc(
|
||
_groundCheck.position, _groundCheckSize, 0f, _groundBuffer, _groundLayer);
|
||
_isGrounded = _groundHitCount > 0;
|
||
|
||
// 斜坡吸附禁用标记:仅在重新落地(从空中→地面)时重置,
|
||
// 而非每帧在地面时都重置。
|
||
// 这样 Jump() 设置的 _slopeSnapDisabled = true 可以存活到玩家真正离开地面,
|
||
// 防止起跳后的首个 FixedUpdate 仍检测到地面时把标记清零,
|
||
// 导致紧接着的斜坡吸附把垂直速度归零(即"一直按方向键起跳立即落地"bug)。
|
||
if (_isGrounded && !_wasGrounded)
|
||
_slopeSnapDisabled = false;
|
||
|
||
// 斜坡吸附:OverlapBox 是水平矩形,在平地→斜坡转折处可能短暂离地。
|
||
// 读取 Rigidbody2D 已有的物理接触点(零额外物理查询开销),
|
||
// 接触法线 Y > 0.5 即视为地面接触,保持 IsGrounded 为 true。
|
||
if (!_isGrounded && _wasGrounded && !_slopeSnapDisabled
|
||
&& Mathf.Abs(_rb.velocity.x) > 0.1f)
|
||
{
|
||
int contactCount = _rb.GetContacts(_slopeContactBuffer);
|
||
for (int i = 0; i < contactCount; i++)
|
||
{
|
||
if (_slopeContactBuffer[i].normal.y > 0.5f)
|
||
{
|
||
_isGrounded = true;
|
||
_rb.velocity = new Vector2(_rb.velocity.x, 0f);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 检测是否站在单向平台(含 IDropThrough 组件的碰撞体)
|
||
_onOneWayPlatform = false;
|
||
for (int i = 0; i < _groundHitCount; i++)
|
||
{
|
||
if (_groundBuffer[i] != null &&
|
||
_groundBuffer[i].TryGetComponent<IDropThrough>(out _))
|
||
{
|
||
_onOneWayPlatform = true;
|
||
break;
|
||
}
|
||
}
|
||
|
||
_currentSurface = (_isGrounded && _onOneWayPlatform)
|
||
? SurfaceType.OneWayPlatform
|
||
: SurfaceType.Ground;
|
||
}
|
||
|
||
private void CheckWalls()
|
||
{
|
||
float len = _config.WallRayLength;
|
||
float offY = _config.WallRayOffsetY;
|
||
Vector2 pos = (Vector2)transform.position + Vector2.up * offY;
|
||
|
||
_isWallLeft = Physics2D.Raycast(pos, Vector2.left, len, _groundLayer);
|
||
_isWallRight = Physics2D.Raycast(pos, Vector2.right, len, _groundLayer);
|
||
|
||
// 物理接触点兜底:补充射线未覆盖图层或长度不足时的漏检。
|
||
// GetContacts 返回上一物理步的接触点,由本脚本(-200)读取时先于状态机(-100),
|
||
// 可在状态机决定是否施加水平速度之前获得"当帧最新"的墙壁接触信息。
|
||
if (!_isWallLeft || !_isWallRight)
|
||
{
|
||
int cnt = _rb.GetContacts(_wallContactBuffer);
|
||
for (int i = 0; i < cnt; i++)
|
||
{
|
||
float nx = _wallContactBuffer[i].normal.x;
|
||
if (nx > 0.5f) _isWallLeft = true; // 法线朝右 = 左侧有墙
|
||
if (nx < -0.5f) _isWallRight = true; // 法线朝左 = 右侧有墙
|
||
}
|
||
}
|
||
}
|
||
|
||
private void OnDrawGizmos()
|
||
{
|
||
#if UNITY_EDITOR
|
||
// ── 1. 角色物理轮廓(浅绿,始终可见)────────────────────────────
|
||
Gizmos.color = new Color(0.4f, 1f, 0.4f, 0.65f);
|
||
foreach (var col in GetComponents<Collider2D>())
|
||
{
|
||
if (col.isTrigger) continue;
|
||
BaseGames.Combat.HitBox.DrawCollider2DWire(col);
|
||
}
|
||
|
||
// ── 2. 朝向箭头(金黄)──────────────────────────────────────────
|
||
Vector3 center = transform.position;
|
||
Vector3 arrowEnd = center + new Vector3(_facingDirection * 0.55f, 0f, 0f);
|
||
DrawArrow2D(center, arrowEnd, new Color(1f, 0.85f, 0.1f, 0.95f));
|
||
|
||
// ── 3. 站立检测框(落地亮绿 / 未落地淡绿;未赋值时显示红色警告框)──
|
||
if (_groundCheck == null)
|
||
{
|
||
Gizmos.color = new Color(1f, 0.1f, 0.1f, 0.9f);
|
||
Gizmos.DrawWireSphere(transform.position, 0.25f);
|
||
#if UNITY_EDITOR
|
||
UnityEditor.Handles.color = new Color(1f, 0.1f, 0.1f, 1f);
|
||
UnityEditor.Handles.Label(transform.position + Vector3.up * 0.6f, "GroundCheck 未赋值!");
|
||
#endif
|
||
}
|
||
else
|
||
{
|
||
bool grounded = Application.isPlaying && _isGrounded;
|
||
Gizmos.color = grounded
|
||
? new Color(0.1f, 1f, 0.3f, 0.9f)
|
||
: new Color(0.4f, 0.85f, 0.4f, 0.35f);
|
||
// 绘制地面检测矩形
|
||
{
|
||
Vector3 c = _groundCheck.position;
|
||
float hx = _groundCheckSize.x * 0.5f, hy = _groundCheckSize.y * 0.5f;
|
||
Gizmos.DrawLine(new Vector3(c.x - hx, c.y + hy), new Vector3(c.x + hx, c.y + hy));
|
||
Gizmos.DrawLine(new Vector3(c.x + hx, c.y + hy), new Vector3(c.x + hx, c.y - hy));
|
||
Gizmos.DrawLine(new Vector3(c.x + hx, c.y - hy), new Vector3(c.x - hx, c.y - hy));
|
||
Gizmos.DrawLine(new Vector3(c.x - hx, c.y - hy), new Vector3(c.x - hx, c.y + hy));
|
||
}
|
||
}
|
||
|
||
// ── 4. 靠墙检测射线(碰墙橙色 / 未碰淡黄,带端点圆点)─────────
|
||
if (_config == null) return;
|
||
float wLen = _config.WallRayLength;
|
||
float wOffY = _config.WallRayOffsetY;
|
||
Vector2 wallPos = (Vector2)transform.position + Vector2.up * wOffY;
|
||
|
||
bool lHit = Application.isPlaying && _isWallLeft;
|
||
bool rHit = Application.isPlaying && _isWallRight;
|
||
|
||
Gizmos.color = lHit ? new Color(1f, 0.45f, 0f, 1f) : new Color(1f, 0.85f, 0.3f, 0.4f);
|
||
Gizmos.DrawRay(wallPos, Vector2.left * wLen);
|
||
Gizmos.DrawWireSphere((Vector3)(wallPos + Vector2.left * wLen), 0.04f);
|
||
|
||
Gizmos.color = rHit ? new Color(1f, 0.45f, 0f, 1f) : new Color(1f, 0.85f, 0.3f, 0.4f);
|
||
Gizmos.DrawRay(wallPos, Vector2.right * wLen);
|
||
Gizmos.DrawWireSphere((Vector3)(wallPos + Vector2.right * wLen), 0.04f);
|
||
#endif
|
||
}
|
||
|
||
private void OnDrawGizmosSelected()
|
||
{
|
||
#if UNITY_EDITOR
|
||
// 运行时:用青色箭头显示速度向量(仅选中时,按比例缩放)
|
||
if (!Application.isPlaying || _rb == null) return;
|
||
Vector2 vel = _rb.velocity;
|
||
if (vel.sqrMagnitude < 0.01f) return;
|
||
DrawArrow2D(transform.position, transform.position + (Vector3)(vel * 0.12f),
|
||
new Color(0.2f, 0.9f, 1f, 0.9f), 0.1f);
|
||
#endif
|
||
}
|
||
|
||
// 在 Gizmos 空间绘制带箭头的 2D 有向线段
|
||
private static void DrawArrow2D(Vector3 from, Vector3 to, Color color, float headLen = 0.15f)
|
||
{
|
||
Vector3 dir = to - from;
|
||
if (dir.sqrMagnitude < 0.0001f) return;
|
||
dir = dir.normalized;
|
||
Gizmos.color = color;
|
||
Gizmos.DrawLine(from, to);
|
||
|
||
// 将 -dir 分别旋转 ±35° 得到箭头两翼
|
||
float cos = 0.8192f; // Mathf.Cos(35° in radians)
|
||
float sin = 0.5736f; // Mathf.Sin(35°)
|
||
float bx = -dir.x, by = -dir.y;
|
||
Vector3 wing1 = new Vector3(bx * cos - by * sin, bx * sin + by * cos, 0f) * headLen;
|
||
Vector3 wing2 = new Vector3(bx * cos + by * sin, -bx * sin + by * cos, 0f) * headLen;
|
||
Gizmos.DrawLine(to, to + wing1);
|
||
Gizmos.DrawLine(to, to + wing2);
|
||
}
|
||
}
|
||
|
||
/// <summary>当前所在地面类型(用于脚步声等反馈)。</summary>
|
||
public enum SurfaceType
|
||
{
|
||
Ground,
|
||
OneWayPlatform,
|
||
Slope,
|
||
Ice,
|
||
}
|
||
}
|