using UnityEngine;
namespace BaseGames.Player
{
///
/// 玩家物理移动组件。封装 Rigidbody2D 操作,提供跑动、跳跃、击退等接口。
///
[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;
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;
/// 垂直速度大于零(处于上升弧段)。供 WallJumpState 判断起跳是否结束。
public bool IsRising => _rb.velocity.y > 0f;
private void Awake()
{
Debug.Assert(_config != null, "[PlayerMovement] _config 未赋值,请在 Inspector 中指定 PlayerMovementConfigSO。", this);
_rb = GetComponent();
if (_spriteRenderer == null)
_spriteRenderer = GetComponentInChildren();
}
private void FixedUpdate()
{
CheckGrounded();
CheckWalls();
if (_isGrounded)
_coyoteTimer = _config.CoyoteTime;
else
_coyoteTimer = Mathf.Max(0f, _coyoteTimer - Time.fixedDeltaTime);
}
// ── 移动 ──────────────────────────────────────────────────────────────
public void Move(float speedX)
{
float target = speedX;
float current = _rb.velocity.x;
float accel = Mathf.Abs(speedX) > 0.01f ? _config.Acceleration : _config.Deceleration;
float newX = Mathf.MoveTowards(current, target, accel * Time.fixedDeltaTime);
_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 * 0.5f);
}
// ── 重力 ──────────────────────────────────────────────────────────────
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);
// ── 朝向 ──────────────────────────────────────────────────────────────
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;
// ── 冲刺 ──────────────────────────────────────────────────────────────
///
/// 施加冲刺速度(DashState/AerialDashState 调用)。
/// direction 应为归一化水平向量(±1, 0)。
/// 冲刺期间调用 SetGravityScale(0) 可关闭重力;结束后恢复默认重力。
///
public void Dash(Vector2 direction, float speed)
{
_rb.velocity = new Vector2(direction.x * speed, 0f);
}
///
/// 壁滑:将垂直速度限制为 -WallSlideSpeed(向下缓慢滑动)。
/// WallSlideState.OnStateFixedUpdate 每帧调用。
///
public void ApplyWallSlide()
{
float targetY = -_config.WallSlideSpeed;
if (_rb.velocity.y < targetY)
_rb.velocity = new Vector2(_rb.velocity.x, targetY);
}
///
/// 蹬墙跳:对墙方向施加相反水平力 + 向上力。
/// wallDir = +1 (右墙) 或 -1 (左墙),跳跃方向与之相反。
///
public void WallJump(int wallDir)
{
float forceX = -wallDir * _config.WallJumpForceX;
float forceY = _config.WallJumpForceY;
_rb.velocity = new Vector2(forceX, forceY);
_coyoteTimer = 0f;
}
/// 单向平台穿透(输入下行 + 跳跃键时触发)。
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 OnDrawGizmosSelected()
{
if (_groundCheck == null) return;
Gizmos.color = Color.green;
Gizmos.DrawWireCube(_groundCheck.position, _groundCheckSize);
}
}
/// 当前所在地面类型(用于脚步声等反馈)。
public enum SurfaceType
{
Ground,
OneWayPlatform,
Slope,
Ice,
}
}