using UnityEngine; using System.Collections; /// /// CharacterController 기반 플레이어 이동 (최종 버전) /// - 벽 뚫림 방지 /// - 중력 처리 /// - 대시 충돌 감지 /// - 무기 충돌 문제 해결 /// - 물리 상호작용 제거 (몬스터 안 밀림) /// [RequireComponent(typeof(CharacterController))] public class PlayerMovement : MonoBehaviour { [Header("=== 참조 ===")] [SerializeField] private Stats stats; [SerializeField] private PlayerHealth health; [SerializeField] private PlayerAnimator pAnim; [SerializeField] private PlayerAttack attackScript; [Header("=== CharacterController ===")] private CharacterController _controller; [Header("=== 대시 설정 ===")] [SerializeField] private float dashDistance = 3f; [SerializeField] private float dashDuration = 0.08f; [SerializeField] private float dashCooldown = 1.5f; [Header("=== 차징 감속 설정 ===")] [Range(0.1f, 1f)] [SerializeField] private float minSpeedMultiplier = 0.3f; [Header("=== 중력 설정 ===")] [SerializeField] private float gravity = -20f; [Header("=== 충돌 설정 ===")] [Tooltip("무기 레이어 (이 레이어와는 충돌하지 않음)")] [SerializeField] private LayerMask weaponLayer; [Tooltip("플레이어가 서 있어야 하는 최소 높이 (버그 방지)")] [SerializeField] private float minGroundHeight = 0.1f; // 이동 상태 private Vector3 _moveDir; private bool _isSprinting; private bool _isDashing; private float _lastDashTime; private float _verticalVelocity; // 디버그용 private float _initialYPosition; private void Awake() { _controller = GetComponent(); if (_controller == null) { Debug.LogError("[PlayerMovement] CharacterController가 필요합니다!"); } // 초기 높이 저장 _initialYPosition = transform.position.y; } private void Start() { // ⭐ 무기와 플레이어 레이어 간 충돌 무시 설정 SetupLayerCollision(); } /// /// 레이어 충돌 설정 (무기는 플레이어와 충돌하지 않음) /// private void SetupLayerCollision() { // "Player" 레이어와 "Weapon" 레이어 간 충돌 무시 int playerLayer = gameObject.layer; // weaponLayer가 설정되어 있으면 충돌 무시 if (weaponLayer != 0) { // LayerMask에서 실제 레이어 번호 추출 int weaponLayerIndex = GetLayerFromMask(weaponLayer); if (weaponLayerIndex >= 0) { Physics.IgnoreLayerCollision(playerLayer, weaponLayerIndex, true); Debug.Log($"[PlayerMovement] {LayerMask.LayerToName(playerLayer)}와 {LayerMask.LayerToName(weaponLayerIndex)} 간 충돌 무시 설정 완료"); } } } /// /// LayerMask에서 레이어 인덱스 추출 /// private int GetLayerFromMask(LayerMask mask) { int layerNumber = 0; int layer = mask.value; while (layer > 1) { layer = layer >> 1; layerNumber++; } return layerNumber; } // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // 입력 처리 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ public void SetMoveInput(Vector3 dir, bool sprint) { _moveDir = dir; _isSprinting = sprint; } public void AttemptDash() { if (CanDash()) StartCoroutine(DashRoutine()); } private bool CanDash() { bool isCooldownOver = Time.time >= _lastDashTime + dashCooldown; bool isAlive = health != null && !health.IsDead; return isCooldownOver && !_isDashing && isAlive; } // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // 메인 이동 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ private void Update() { // 1. 사망/대시/피격 중 이동 차단 if (health != null && (health.IsDead || health.isHit || _isDashing)) { if (pAnim != null && !health.IsDead) pAnim.UpdateMove(0f); ApplyGravityOnly(); return; } // 2. 공격 중 이동 차단 if (attackScript != null && attackScript.IsAttacking) { if (pAnim != null) pAnim.UpdateMove(0f); ApplyGravityOnly(); return; } // 3. 이동 속도 계산 float speed = _isSprinting ? stats.CurrentRunSpeed : stats.CurrentMoveSpeed; if (attackScript != null && attackScript.IsCharging) { float speedReduction = Mathf.Lerp(1.0f, minSpeedMultiplier, attackScript.ChargeProgress); speed *= speedReduction; } // 4. ✅ CharacterController로 이동 (벽 충돌 처리!) Vector3 motion = _moveDir * speed * Time.deltaTime; ApplyGravity(); motion.y = _verticalVelocity * Time.deltaTime; _controller.Move(motion); // ⭐ 높이 제한 체크 (버그 방지) CheckAbnormalHeight(); // 5. 애니메이션 UpdateAnimation(); } private void ApplyGravity() { if (_controller.isGrounded) { _verticalVelocity = -2f; } else { _verticalVelocity += gravity * Time.deltaTime; } } private void ApplyGravityOnly() { ApplyGravity(); _controller.Move(new Vector3(0, _verticalVelocity * Time.deltaTime, 0)); // ⭐ 높이 제한 체크 CheckAbnormalHeight(); } /// /// ⭐ 비정상적으로 높이 올라갔는지 체크 (무기 충돌 버그 방지) /// private void CheckAbnormalHeight() { // 땅에서 너무 높이 떠있으면 강제로 내림 RaycastHit hit; if (Physics.Raycast(transform.position, Vector3.down, out hit, 100f)) { float heightAboveGround = transform.position.y - hit.point.y; // 땅에서 일정 높이 이상 떠있고, 땅에 닿지 않았다면 if (heightAboveGround > 3f && !_controller.isGrounded) { // 강제로 땅 근처로 이동 Vector3 correctedPos = transform.position; correctedPos.y = hit.point.y + _controller.height / 2f + minGroundHeight; // CharacterController는 enabled 끄고 위치 변경 _controller.enabled = false; transform.position = correctedPos; _controller.enabled = true; _verticalVelocity = 0f; Debug.LogWarning("[PlayerMovement] 비정상적인 높이 감지! 위치 보정함"); } } } 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; Vector3 dashMotion = dashDir * speed * Time.deltaTime; // ✅ 벽 충돌 감지 CollisionFlags flags = _controller.Move(dashMotion); yield return null; } if (health != null) health.isInvincible = false; _isDashing = false; } // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // 유틸리티 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ public bool IsGrounded() => _controller.isGrounded; public bool IsDashing() => _isDashing; public float GetCurrentSpeed() => _controller.velocity.magnitude; }