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, } }