using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Collections를 [RequireComponent(typeof(CharacterController))] // 컴포넌트를 강제로 추가할거에요 -> CharacterController를 public class PlayerMovement : MonoBehaviour // 클래스를 선언할거에요 -> PlayerMovement를 { [Header("=== 참조 ===")] // 인스펙터 창에 제목을 표시할거에요 -> 참조를 [SerializeField] private Stats stats; // 변수를 선언할거에요 -> 이동 속도 Stats를 [SerializeField] private PlayerHealth health; // 변수를 선언할거에요 -> 체력/피격 상태 PlayerHealth를 [SerializeField] private PlayerAnimator pAnim; // 변수를 선언할거에요 -> 애니메이션 제어 PlayerAnimator를 [SerializeField] private PlayerAttack attackScript; // 변수를 선언할거에요 -> 공격 상태 PlayerAttack를 [SerializeField] private PlayerSoundFX soundFX; // 변수를 선언할거에요 -> 사운드 스크립트를 [Header("=== CharacterController ===")] // 인스펙터 창에 제목을 표시할거에요 private CharacterController _controller; // 변수를 선언할거에요 -> 이동 담당 CharacterController를 [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; // 변수를 선언할거에요 -> 충돌 무시할 무기 레이어를 // ───────────────────────────────────────────────────────── // ⭐ 경직 시스템 // ───────────────────────────────────────────────────────── [Header("=== 경직 설정 ===")] // 인스펙터 창에 제목을 표시할거에요 -> 경직 설정을 [Tooltip("돌진 몬스터 피격 시 경직 지속 시간 (초)")] [SerializeField] private float staggerDuration = 0.8f; // 변수를 선언할거에요 -> 경직 지속 시간을 (Inspector에서 조절 가능) private bool _isStaggered = false; // 변수를 선언할거에요 -> 현재 경직 상태인지 여부를 private Coroutine _staggerCoroutine = null; // 변수를 선언할거에요 -> 경직 코루틴 핸들을 (중복 방지용) public bool IsStaggered => _isStaggered; // 프로퍼티를 선언할거에요 -> 경직 상태를 외부에서 읽을 수 있도록 public float StaggerDuration => staggerDuration; // 프로퍼티를 선언할거에요 -> 경직 지속 시간을 외부에서 읽을 수 있도록 [SerializeField] private StaggerIndicator staggerIndicator; // 변수를 선언할거에요 -> 경직 UI 연동용을 // ───────────────────────────────────────────────────────── private Vector3 _moveDir; // 변수를 선언할거에요 -> 입력 이동 방향을 private bool _isSprinting; // 변수를 선언할거에요 -> 달리기 여부를 private bool _isDashing; // 변수를 선언할거에요 -> 대시 여부를 private float _lastDashTime; // 변수를 선언할거에요 -> 마지막 대시 시간을 private float _verticalVelocity; // 변수를 선언할거에요 -> 수직 낙하 속도를 [Header("=== 애니메이션 보간 ===")] // 인스펙터 창에 제목을 표시할거에요 [SerializeField] private float animSmoothSpeed = 8f; // 변수를 선언할거에요 -> 애니메이션 보간 속도를 private float _currentAnimVal = 0f; // 변수를 초기화할거에요 -> 현재 보간 애니메이션 값을 private float _initialYPosition; // 변수를 선언할거에요 -> 시작 높이를 private void Awake() // 함수를 실행할거에요 -> 초기화를 { _controller = GetComponent(); // 가져올거에요 -> CharacterController를 if (_controller == null) // 조건이 맞으면 실행할거에요 -> 없으면 Debug.LogError("[PlayerMovement] CharacterController가 필요합니다!"); // 에러를 출력할거에요 _initialYPosition = transform.position.y; // 저장할거에요 -> 초기 Y 위치를 } private void Start() // 함수를 실행할거에요 -> 시작 시 { SetupLayerCollision(); // 실행할거에요 -> 레이어 충돌 설정을 } // ───────────────────────────────────────────────────────── // ⭐ 경직 적용 함수 — ChargeMonster 무기에서 호출 // // 사용법: // hit.GetComponent()?.ApplyStagger(); // 또는 duration 직접 지정: // hit.GetComponent()?.ApplyStagger(1.2f); // ───────────────────────────────────────────────────────── public void ApplyStagger(float duration = -1f) // 함수를 선언할거에요 -> 외부에서 경직을 걸 때 호출할 { float actualDuration = duration > 0f ? duration : staggerDuration; // 결정할거에요 -> 인자가 있으면 그 값, 없으면 Inspector 기본값을 if (_staggerCoroutine != null) // 조건이 맞으면 실행할거에요 -> 이미 경직 중이면 StopCoroutine(_staggerCoroutine); // 멈출거에요 -> 이전 경직 코루틴을 (덮어쓰기) _staggerCoroutine = StartCoroutine(StaggerRoutine(actualDuration)); // 시작할거에요 -> 새 경직 코루틴을 if (staggerIndicator != null) staggerIndicator.NotifyStagger(actualDuration); // 알릴거에요 -> UI에 경직 시작을 } private IEnumerator StaggerRoutine(float duration) // 코루틴을 정의할거에요 -> 경직 지속 로직을 { _isStaggered = true; // 설정할거에요 -> 경직 상태를 true로 if (pAnim != null) pAnim.UpdateMove(0f); // 멈출거에요 -> 이동 애니메이션을 yield return new WaitForSeconds(duration); // 기다릴거에요 -> 경직 시간만큼 _isStaggered = false; // 해제할거에요 -> 경직 상태를 _staggerCoroutine = null; // 초기화할거에요 -> 코루틴 핸들을 } private void SetupLayerCollision() // 함수를 선언할거에요 -> 레이어 충돌 설정을 { int playerLayer = gameObject.layer; // 가져올거에요 -> 내 레이어 번호를 if (weaponLayer != 0) // 조건이 맞으면 실행할거에요 -> 무기 레이어가 설정됐으면 { 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; // 초기화할거에요 -> 레이어 번호를 0으로 int layer = mask.value; // 가져올거에요 -> 마스크 정수값을 while (layer > 1) // 반복할거에요 -> 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; // 반환할거에요 -> 모두 만족할 때만 true를 } private void Update() // 함수를 실행할거에요 -> 매 프레임마다 { // 1. 행동 불가 상태 체크 // ⭐ _isStaggered 추가 — 경직 중에도 이동 차단 if (health != null && (health.IsDead || health.isHit || _isDashing || _isStaggered)) // 조건이 맞으면 실행할거에요 -> 죽었거나, 피격이거나, 대시 중이거나, 경직 중이면 { // ⭐ 경직 중 공격 강제 취소 — 차징 포함 if (_isStaggered && attackScript != null) // 조건이 맞으면 실행할거에요 -> 경직 상태이고 공격 스크립트가 있으면 { attackScript.CancelCharging(); // 취소할거에요 -> 차징을 attackScript.IsAttacking = false; // 해제할거에요 -> 공격 상태를 } 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. 최종 이동 벡터 계산 Vector3 motion = _moveDir * speed * Time.deltaTime; // 계산할거에요 -> 이번 프레임 이동량을 ApplyGravity(); // 계산할거에요 -> 수직 속도를 motion.y = _verticalVelocity * Time.deltaTime; // 넣을거에요 -> 수직 이동량을 _controller.Move(motion); // 이동할거에요 -> 캐릭터 컨트롤러를 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)); // 이동할거에요 -> 수직 방향만 } private void UpdateAnimation() // 함수를 선언할거에요 -> 이동 애니메이션을 갱신할 { if (pAnim == null || stats == null) return; // 중단할거에요 -> 필수 참조 없으면 Vector3 horizontalVel = _controller.velocity; // 가져올거에요 -> 현재 속도를 horizontalVel.y = 0f; // 무시할거에요 -> 수직 속도를 float actualSpeed = horizontalVel.magnitude; // 계산할거에요 -> 수평 속도 크기를 float walkSpeed = stats.CurrentMoveSpeed; // 가져올거에요 -> 걷기 속도를 float runSpeed = stats.CurrentRunSpeed; // 가져올거에요 -> 달리기 속도를 float targetAnimVal = 0f; // 초기화할거에요 -> 목표 애니메이션 값을 if (actualSpeed > 0.1f) // 조건이 맞으면 실행할거에요 -> 움직이고 있으면 { if (actualSpeed <= walkSpeed) // 조건이 맞으면 실행할거에요 -> 걷기 이하면 targetAnimVal = Mathf.Lerp(0f, 0.5f, actualSpeed / walkSpeed); // 보간할거에요 -> 0~0.5로 else // 조건이 틀리면 실행할거에요 -> 달리기면 targetAnimVal = Mathf.Lerp(0.5f, 1.0f, (actualSpeed - walkSpeed) / (runSpeed - walkSpeed)); // 보간할거에요 -> 0.5~1.0으로 } if (attackScript != null && attackScript.IsCharging) // 조건이 맞으면 실행할거에요 -> 차징 중이면 targetAnimVal *= 0.5f; // 줄일거에요 -> 절반으로 _currentAnimVal = Mathf.Lerp(_currentAnimVal, targetAnimVal, Time.deltaTime * animSmoothSpeed); // 보간할거에요 -> 부드럽게 pAnim.UpdateMove(_currentAnimVal); // 전달할거에요 -> 보간된 애니메이션 값을 } private IEnumerator DashRoutine() // 코루틴을 정의할거에요 -> 대시 로직을 { _isDashing = true; // 설정할거에요 -> 대시 상태를 _lastDashTime = Time.time; // 저장할거에요 -> 대시 시작 시간을 if (soundFX != null) soundFX.PlayDash(); // 재생할거에요 -> 대시 소리를 if (PlayerBehaviorTracker.Instance != null) // 조건이 맞으면 실행할거에요 -> 트래커가 있으면 PlayerBehaviorTracker.Instance.RecordDodge(); // 기록할거에요 -> 회피를 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; // 계산할거에요 -> 이동량을 _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; // 반환할거에요 -> 현재 이동 속도를 }