diff --git a/Assets/Scripts/Player/Controller/PlayerMovement.cs b/Assets/Scripts/Player/Controller/PlayerMovement.cs index 462fd8e4..63d681d8 100644 --- a/Assets/Scripts/Player/Controller/PlayerMovement.cs +++ b/Assets/Scripts/Player/Controller/PlayerMovement.cs @@ -1,33 +1,74 @@ using UnityEngine; using System.Collections; +/// +/// CharacterController 기반 플레이어 이동 (완전판) +/// - 벽 뚫림 방지 +/// - 물리 오브젝트 밀기 +/// - 중력 처리 +/// - 대시 충돌 감지 +/// +[RequireComponent(typeof(CharacterController))] public class PlayerMovement : MonoBehaviour { - [Header("--- 참조 ---")] + [Header("=== 참조 ===")] [SerializeField] private Stats stats; [SerializeField] private PlayerHealth health; [SerializeField] private PlayerAnimator pAnim; [SerializeField] private PlayerAttack attackScript; - [Header("--- 대시 설정 ---")] - // ⭐ 유저님 이미지(image_42ccbd.png) 수치를 기본값으로 적용했습니다. + [Header("=== CharacterController ===")] + private CharacterController _controller; + + [Header("=== 대시 설정 ===")] [SerializeField] private float dashDistance = 3f; [SerializeField] private float dashDuration = 0.08f; [SerializeField] private float dashCooldown = 1.5f; - [Header("--- 차징 감속 설정 ---")] + [Header("=== 차징 감속 설정 ===")] [Range(0.1f, 1f)] [SerializeField] private float minSpeedMultiplier = 0.3f; + [Header("=== 중력 설정 ===")] + [SerializeField] private float gravity = -20f; + + [Header("=== 물리 상호작용 설정 ===")] + [Tooltip("일반 이동 시 물체 밀기 힘")] + [SerializeField] private float pushPower = 2f; + + [Tooltip("대시 중 물체 밀기 힘 (더 강함)")] + [SerializeField] private float dashPushPower = 5f; + + [Tooltip("밀 수 있는 최대 질량 (kg)")] + [SerializeField] private float maxPushMass = 10f; + + // 이동 상태 private Vector3 _moveDir; private bool _isSprinting; private bool _isDashing; private float _lastDashTime; + private float _verticalVelocity; - // ⭐ PlayerInput에서 호출하는 입력 세팅 - public void SetMoveInput(Vector3 dir, bool sprint) { _moveDir = dir; _isSprinting = sprint; } + private void Awake() + { + _controller = GetComponent(); + + if (_controller == null) + { + Debug.LogError("[PlayerMovement] CharacterController가 필요합니다!"); + } + } + + // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + // 입력 처리 + // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + public void SetMoveInput(Vector3 dir, bool sprint) + { + _moveDir = dir; + _isSprinting = sprint; + } - // ⭐ PlayerInput에서 Space를 누를 때 호출하는 대시 시도 함수 public void AttemptDash() { if (CanDash()) StartCoroutine(DashRoutine()); @@ -35,29 +76,34 @@ public class PlayerMovement : MonoBehaviour private bool CanDash() { - // 쿨타임 체크 + 대시 중 아님 + 사망 아님 bool isCooldownOver = Time.time >= _lastDashTime + dashCooldown; bool isAlive = health != null && !health.IsDead; return isCooldownOver && !_isDashing && isAlive; } + // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + // 메인 이동 + // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + private void Update() { - // 1. 💀 사망, ⚡ 대시 중, 🤕 혹은 피격(isHit) 중일 때 이동 차단 + // 1. 사망/대시/피격 중 이동 차단 if (health != null && (health.IsDead || health.isHit || _isDashing)) { if (pAnim != null && !health.IsDead) pAnim.UpdateMove(0f); + ApplyGravityOnly(); return; } - // 2. ⚔️ 공격 중(콤보 및 후딜레이)일 때 이동 차단 + // 2. 공격 중 이동 차단 if (attackScript != null && attackScript.IsAttacking) { if (pAnim != null) pAnim.UpdateMove(0f); + ApplyGravityOnly(); return; } - // 3. 이동 속도 계산 및 차징 감속 적용 + // 3. 이동 속도 계산 float speed = _isSprinting ? stats.CurrentRunSpeed : stats.CurrentMoveSpeed; if (attackScript != null && attackScript.IsCharging) @@ -66,41 +112,115 @@ public class PlayerMovement : MonoBehaviour speed *= speedReduction; } - // 4. 실제 이동 처리 - transform.Translate(_moveDir * speed * Time.deltaTime, Space.World); + // 4. ✅ CharacterController로 이동 (벽 충돌 처리!) + Vector3 motion = _moveDir * speed * Time.deltaTime; - // 5. 애니메이션 연동 - if (pAnim != null) + ApplyGravity(); + motion.y = _verticalVelocity * Time.deltaTime; + + _controller.Move(motion); + + // 5. 애니메이션 + UpdateAnimation(); + } + + private void ApplyGravity() + { + if (_controller.isGrounded) { - float animVal = _moveDir.magnitude > 0.1f ? (_isSprinting ? 1.0f : 0.5f) : 0f; - if (attackScript != null && attackScript.IsCharging) animVal *= 0.5f; - pAnim.UpdateMove(animVal); + _verticalVelocity = -2f; + } + else + { + _verticalVelocity += gravity * Time.deltaTime; } } - // ⭐ [핵심] 방향성 대시 및 무적 처리 로직 + private void ApplyGravityOnly() + { + ApplyGravity(); + _controller.Move(new Vector3(0, _verticalVelocity * Time.deltaTime, 0)); + } + + private void UpdateAnimation() + { + if (pAnim == null) return; + + float animVal = _moveDir.magnitude > 0.1f ? (_isSprinting ? 1.0f : 0.5f) : 0f; + if (attackScript != null && attackScript.IsCharging) animVal *= 0.5f; + + pAnim.UpdateMove(animVal); + } + + // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + // 대시 + // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + private IEnumerator DashRoutine() { _isDashing = true; _lastDashTime = Time.time; - // 입력 방향이 있으면 그쪽으로, 없으면 캐릭터의 정면이 아닌 후방(회피)으로 대시 Vector3 dashDir = _moveDir.sqrMagnitude > 0.001f ? _moveDir : -transform.forward; - // 🛡️ [무적] 대시 시작 시 무적 상태 활성화 if (health != null) health.isInvincible = true; float startTime = Time.time; while (Time.time < startTime + dashDuration) { - // 속도 = 거리 / 시간 float speed = dashDistance / dashDuration; - transform.Translate(dashDir * speed * Time.deltaTime, Space.World); + Vector3 dashMotion = dashDir * speed * Time.deltaTime; + + // ✅ 벽 충돌 감지 + CollisionFlags flags = _controller.Move(dashMotion); + + // 벽에 부딪히면 대시 조기 종료 (선택사항) + // if ((flags & CollisionFlags.Sides) != 0) break; + yield return null; } - // 🛡️ [무적] 대시 종료 시 무적 상태 해제 if (health != null) health.isInvincible = false; _isDashing = false; } + + // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + // ⭐ 물리 상호작용 (핵심!) + // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + /// + /// CharacterController가 물체와 충돌할 때 호출됨 + /// + private void OnControllerColliderHit(ControllerColliderHit hit) + { + // Rigidbody 확인 + Rigidbody body = hit.collider.attachedRigidbody; + + // Rigidbody 없거나 Kinematic이면 밀 수 없음 + if (body == null || body.isKinematic) return; + + // 너무 무거운 물체는 밀 수 없음 + if (body.mass > maxPushMass) return; + + // 바닥/천장 충돌은 무시 + if (hit.moveDirection.y < -0.3f || hit.moveDirection.y > 0.3f) return; + + // 💪 밀기 방향 계산 + Vector3 pushDir = new Vector3(hit.moveDirection.x, 0, hit.moveDirection.z); + pushDir.Normalize(); + + // 대시 중이면 더 강한 힘 + float power = _isDashing ? dashPushPower : pushPower; + + // ✅ 힘 적용! + body.AddForce(pushDir * power, ForceMode.Impulse); + } + + // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + // 유틸리티 + // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + public bool IsGrounded() => _controller.isGrounded; + public bool IsDashing() => _isDashing; + public float GetCurrentSpeed() => _contzroller.velocity.magnitude; } \ No newline at end of file