307 lines
14 KiB
C#
307 lines
14 KiB
C#
using UnityEngine;
|
||
|
||
namespace BaseGames.Player
|
||
{
|
||
/// <summary>
|
||
/// 玩家物理移动组件。封装 Rigidbody2D 操作,提供跑动、跳跃、击退等接口。
|
||
|
||
/// </summary>
|
||
// 执行顺序必须早于 PlayerController(-100),确保每帧 FixedUpdate
|
||
// 开头能在状态机写入速度之前先应用"强制清零"标记。
|
||
[DefaultExecutionOrder(-200)]
|
||
[RequireComponent(typeof(Rigidbody2D))]
|
||
public class PlayerMovement : MonoBehaviour
|
||
{
|
||
[Header("配置")]
|
||
[SerializeField] private PlayerMovementConfigSO _config;
|
||
|
||
[Header("地面检测")]
|
||
[SerializeField] private Transform _groundCheck;
|
||
[SerializeField] private Vector2 _groundCheckSize = new Vector2(0.8f, 0.05f);
|
||
[SerializeField] private LayerMask _groundLayer;
|
||
|
||
[Header("朝向")]
|
||
[SerializeField] private SpriteRenderer _spriteRenderer;
|
||
|
||
// ── 运行时状态 ────────────────────────────────────────────────────────
|
||
private Rigidbody2D _rb;
|
||
private float _coyoteTimer;
|
||
private bool _isGrounded;
|
||
// Update 中调用 ZeroHorizontalVelocity 后设置此标记;
|
||
// 下一个 FixedUpdate(-200,先于状态机 -100)读取并清零,
|
||
// 防止状态机用旧输入把速度重新写成非零值。
|
||
private bool _pendingHorizontalZero;
|
||
private bool _isWallLeft;
|
||
private bool _isWallRight;
|
||
private bool _onOneWayPlatform;
|
||
private int _facingDirection = 1;
|
||
private bool _cancelWindowOpen;
|
||
private SurfaceType _currentSurface = SurfaceType.Ground;
|
||
private readonly Collider2D[] _groundBuffer = new Collider2D[4];
|
||
|
||
public bool IsGrounded => _isGrounded;
|
||
public bool HasCoyoteTime => _coyoteTimer > 0f;
|
||
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);
|
||
_rb = GetComponent<Rigidbody2D>();
|
||
// 关闭位置插值:若开启插值,渲染位置会在速度清零后仍追赶 1~2 渲染帧,产生视觉滑行。
|
||
_rb.interpolation = RigidbodyInterpolation2D.None;
|
||
if (_spriteRenderer == null)
|
||
_spriteRenderer = GetComponentInChildren<SpriteRenderer>();
|
||
}
|
||
|
||
private void FixedUpdate()
|
||
{
|
||
// 优先处理来自 Update 的强制清零请求(在状态机 OnStateFixedUpdate 之前执行)。
|
||
if (_pendingHorizontalZero)
|
||
{
|
||
_rb.velocity = new Vector2(0f, _rb.velocity.y);
|
||
_pendingHorizontalZero = false;
|
||
}
|
||
|
||
CheckGrounded();
|
||
CheckWalls();
|
||
|
||
if (_isGrounded)
|
||
_coyoteTimer = _config.CoyoteTime;
|
||
else
|
||
_coyoteTimer = Mathf.Max(0f, _coyoteTimer - Time.fixedDeltaTime);
|
||
}
|
||
|
||
// ── 移动 ──────────────────────────────────────────────────────────────
|
||
/// <summary>
|
||
/// 直接赋予目标水平速度(按键即全速,松键即停,无加速过渡)。
|
||
/// 地面状态每帧直接到达全速;空中调用时同样即时,但配合 ApplyAirDrag
|
||
/// 在无输入时自然减速,保留跳出时的动量。
|
||
/// </summary>
|
||
public void Move(float speedX)
|
||
{
|
||
_rb.velocity = new Vector2(speedX, _rb.velocity.y);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 空中无输入时施加空气阻力:水平速度乘以 <paramref name="factor"/>,
|
||
/// 低于阈值时归零,避免速度无限趋近 0。
|
||
/// 在 FallState / JumpState / WallJumpState 的 OnStateFixedUpdate 中调用。
|
||
/// </summary>
|
||
public void ApplyAirDrag(float factor)
|
||
{
|
||
float newX = _rb.velocity.x * factor;
|
||
if (Mathf.Abs(newX) < 0.05f) newX = 0f;
|
||
_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;
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
// ── 重力 ──────────────────────────────────────────────────────────────
|
||
public void SetGravityScale(float scale) => _rb.gravityScale = scale;
|
||
|
||
// ── 击退 ──────────────────────────────────────────────────────────────
|
||
public void ApplyKnockback(Vector2 direction, float force)
|
||
=> _rb.velocity = direction.normalized * force;
|
||
|
||
// ── 速度控制 ──────────────────────────────────────────────────────────
|
||
public void ZeroVelocity() => _rb.velocity = Vector2.zero;
|
||
public void ZeroHorizontalVelocity()
|
||
{
|
||
_rb.velocity = new Vector2(0f, _rb.velocity.y);
|
||
// 设置标记:下一个 FixedUpdate 开头再次强制清零,
|
||
// 防止因读到旧输入而把速度重新写成非零值。
|
||
_pendingHorizontalZero = true;
|
||
}
|
||
|
||
// ── 朝向 ──────────────────────────────────────────────────────────────
|
||
public void UpdateFacing()
|
||
{
|
||
float vx = _rb.velocity.x;
|
||
if (Mathf.Abs(vx) < 0.1f) return;
|
||
int dir = vx > 0f ? 1 : -1;
|
||
if (dir == _facingDirection) return;
|
||
_facingDirection = dir;
|
||
if (_spriteRenderer != null)
|
||
_spriteRenderer.flipX = dir < 0;
|
||
}
|
||
|
||
// ── 取消窗口 ──────────────────────────────────────────────────────────
|
||
public void SetCancelWindowOpen(bool open) => _cancelWindowOpen = open;
|
||
|
||
// ── 冲刺 ──────────────────────────────────────────────────────────────
|
||
/// <summary>
|
||
/// 施加冲刺速度(DashState/AerialDashState 调用)。
|
||
/// direction 应为归一化水平向量(±1, 0)。
|
||
/// 冲刺期间调用 SetGravityScale(0) 可关闭重力;结束后恢复默认重力。
|
||
/// </summary>
|
||
public void Dash(Vector2 direction, float speed)
|
||
{
|
||
_rb.velocity = new Vector2(direction.x * speed, 0f);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 壁滑:将垂直速度限制为 -WallSlideSpeed(向下缓慢滑动)。
|
||
/// WallSlideState.OnStateFixedUpdate 每帧调用。
|
||
/// </summary>
|
||
public void ApplyWallSlide()
|
||
{
|
||
float targetY = -_config.WallSlideSpeed;
|
||
if (_rb.velocity.y < targetY)
|
||
_rb.velocity = new Vector2(_rb.velocity.x, targetY);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 蹬墙跳:对墙方向施加相反水平力 + 向上力。
|
||
/// wallDir = +1 (右墙) 或 -1 (左墙),跳跃方向与之相反。
|
||
/// </summary>
|
||
public void WallJump(int wallDir)
|
||
{
|
||
float forceX = -wallDir * _config.WallJumpForceX;
|
||
float forceY = _config.WallJumpForceY;
|
||
_rb.velocity = new Vector2(forceX, forceY);
|
||
_coyoteTimer = 0f;
|
||
}
|
||
|
||
/// <summary>单向平台穿透(输入下行 + 跳跃键时触发)。</summary>
|
||
public void DropThroughPlatform() { }
|
||
|
||
// ── Physics 检测 ──────────────────────────────────────────────────────
|
||
private void CheckGrounded()
|
||
{
|
||
bool wasGrounded = _isGrounded;
|
||
Vector2 origin = _groundCheck != null
|
||
? (Vector2)_groundCheck.position
|
||
: (Vector2)transform.position + Vector2.down * 0.5f;
|
||
|
||
_isGrounded = Physics2D.OverlapBoxNonAlloc(origin, _groundCheckSize, 0f, _groundBuffer, _groundLayer) > 0;
|
||
|
||
if (_isGrounded && !wasGrounded)
|
||
_coyoteTimer = _config.CoyoteTime;
|
||
}
|
||
|
||
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);
|
||
}
|
||
|
||
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. 站立检测框(落地亮绿 / 未落地淡绿)──────────────────────
|
||
Vector2 gOrigin = _groundCheck != null
|
||
? (Vector2)_groundCheck.position
|
||
: (Vector2)transform.position + Vector2.down * 0.5f;
|
||
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);
|
||
BaseGames.Combat.HitBox.DrawWireRect2D(gOrigin, _groundCheckSize);
|
||
|
||
// ── 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);
|
||
BaseGames.Combat.HitBox.DrawWireCircle2D((Vector3)(wallPos + Vector2.left * wLen), 0.04f, 8);
|
||
|
||
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);
|
||
BaseGames.Combat.HitBox.DrawWireCircle2D((Vector3)(wallPos + Vector2.right * wLen), 0.04f, 8);
|
||
#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,
|
||
}
|
||
}
|