using UnityEngine; using System.Collections; [RequireComponent(typeof(CharacterController))] // 이 스크립트를 넣으면 CharacterController도 자동으로 추가됨 (실수 방지) public class PlayerMovement : MonoBehaviour { [Header("=== 참조 ===")] // 외부 스크립트들을 상속 시키기 위한 변수들 // 'private'에 [SerializeField]를 붙여 에디터 인스펙터 창에서 설정 가능 [SerializeField] private Stats stats; // 이동 속도(Stat)를 가져오기 위해 필요 [SerializeField] private PlayerHealth health; // 죽거나 맞았을 때 이동을 멈추기 위해 필요 [SerializeField] private PlayerAnimator pAnim; // 이동에 맞춰 뛰는 모션을 재생하기 위해 필요 [SerializeField] private PlayerAttack attackScript; // 공격 중일 때 이동을 막기 위해 필요 [Header("=== CharacterController ===")] //CharacterControlle는 유니티가 제공하는 인간형 이동 컴포넌트 // Rigidbody와 달리 경사면, 계단 처리가 자동으로 됨. 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)] // 슬라이더로 조절하게 만듦 (1이면 감속 없음, 0.1이면 엄청 느려짐) [SerializeField] private float minSpeedMultiplier = 0.3f; // 차징 중일 때 속도를 30%로 줄임 [Header("=== 중력 설정 ===")] [SerializeField] private float gravity = -20f; // 지구 중력(-9.81)보다 세게 해서 점프 후 빨리 떨어지게 함 (조작감 향상) [Header("=== 충돌 설정 ===")] [Tooltip("무기 레이어 (이 레이어와는 충돌하지 않음)")] [SerializeField] private LayerMask weaponLayer; // 무기를 밟고 올라가는 버그를 막기 위해 무기 레이어를 지정 //[Tooltip("플레이어가 서 있어야 하는 최소 높이 (버그 방지)")] //[SerializeField] private float minGroundHeight = 0.1f; // 땅 보정 시 바닥에 파묻히지 않게 살짝 띄워주는 값 // 내부 상태 변수 (로직 계산용) private Vector3 _moveDir; // 키보드 입력(WASD)으로 결정된 이동 방향 private bool _isSprinting; // Shift 키를 눌렀는지 여부 private bool _isDashing; // 지금 대시 중인가? (중복 대시 방지) private float _lastDashTime; // 마지막으로 대시를 쓴 시간 (쿨타임 계산용) private float _verticalVelocity; // 수직 속도 (중력 가속도 계산용) // 디버그용 (초기 위치 저장) private float _initialYPosition; private void Awake() { // 내 몸에 붙어있는 CharacterController를 가져옴 _controller = GetComponent(); // 만약 없다면 콘솔창에 에러를 띄움 if (_controller == null) { Debug.LogError("[PlayerMovement] CharacterController가 필요합니다!"); } _initialYPosition = transform.position.y; } private void Start() { // 게임 시작 시 딱 한 번 실행: 무기와 플레이어끼리 충돌 끄기 SetupLayerCollision(); } /// 레이어 충돌 무시 설정 (플레이어 자살 버그 방지) private void SetupLayerCollision() { int playerLayer = gameObject.layer; // 내 레이어 번호 가져오기 // 무기 레이어가 설정되어 있다면 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)} 간 충돌 무시 설정 완료"); } } } // 비트 마스크(이진수)를 정수 인덱스로 변환하는 수학 함수 private int GetLayerFromMask(LayerMask mask) { int layerNumber = 0; int layer = mask.value; while (layer > 1) { layer = layer >> 1; // 비트를 오른쪽으로 밀면서 횟수를 셉니다. layerNumber++; } return layerNumber; } // 외부(InputHandler)에서 키보드 입력을 넣어주는 함수 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; // 쿨타임 끝남 && 대시 중 아님 && 살아있음 -> true 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. 이동 속도 계산 // Shift 눌렀으면 달리기 속도, 아니면 걷기 속도 float speed = _isSprinting ? stats.CurrentRunSpeed : stats.CurrentMoveSpeed; // 공격 차징 중이라면 속도를 느리게 만듦 (긴장감 조성) if (attackScript != null && attackScript.IsCharging) { // Lerp를 써서 차징 단계에 따라 부드럽게 감속 float speedReduction = Mathf.Lerp(1.0f, minSpeedMultiplier, attackScript.ChargeProgress); speed *= speedReduction; } // 4. 최종 이동 벡터 계산 // 방향 * 속도 * 시간(프레임 보정) = 이번 프레임에 움직일 거리 Vector3 motion = _moveDir * speed * Time.deltaTime; // 중력 계산 (Y축) ApplyGravity(); motion.y = _verticalVelocity * Time.deltaTime; // ⭐ 실제 이동 실행 (여기서 벽 충돌 처리가 자동 수행됨) _controller.Move(motion); // 5. 안전장치 가동 (승천 버그 체크) //CheckAbnormalHeight(); // 6. 애니메이션 업데이트 (걷기/뛰기 모션) UpdateAnimation(); } private void ApplyGravity() { if (_controller.isGrounded) { // 땅에 닿아있어도 -2f 정도로 계속 눌러줘야 경사면에서 붕 뜨지 않음 _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; // // 땅에서 3미터 이상 떠있는데, 시스템상으론 점프 상태가 아니라면? (버그!) // if (heightAboveGround > 3f && !_controller.isGrounded) // { // // 강제로 땅바닥 위치로 좌표를 계산 // Vector3 correctedPos = transform.position; // correctedPos.y = hit.point.y + _controller.height / 2f + minGroundHeight; // // 순간이동 시킬 땐 CharacterController를 잠시 꺼야 안전함 // _controller.enabled = false; // transform.position = correctedPos; // _controller.enabled = true; // _verticalVelocity = 0f; // 낙하 속도 초기화 // Debug.LogWarning("[PlayerMovement] 비정상적인 높이 감지! 위치 보정함"); // } // } //} private void UpdateAnimation() { if (pAnim == null) return; // 이동 입력이 있으면 1(달리기) 또는 0.5(걷기), 없으면 0(대기) float animVal = _moveDir.magnitude > 0.1f ? (_isSprinting ? 1.0f : 0.5f) : 0f; // 차징 중이면 무조건 걷기 모션(0.5) 이하로 만듦 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; // 정해진 시간(0.08초) 동안 반복 while (Time.time < startTime + dashDuration) { // 속도 = 거리 / 시간 float speed = dashDistance / dashDuration; Vector3 dashMotion = dashDir * speed * Time.deltaTime; // ⭐ transform.position += ... 대신 Move()를 쓰는 이유: // Move를 써야 대시 도중 벽을 만나면 뚫지 않고 멈줌 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; }