using UnityEngine; // 유니티 기능을 불러올거에요 -> UnityEngine을 using System.Collections; // 코루틴을 사용할거에요 -> System.Collections를 public class ChargeMonster : MonsterClass // 클래스를 선언할거에요 -> 돌진 몬스터를 { [Header("=== 돌진 설정 ===")] // 인스펙터 제목을 달거에요 -> 돌진 설정을 [SerializeField] private float chargeSpeed = 15f; // 변수를 선언할거에요 -> 돌진 속도를 [SerializeField] private float chargeRange = 10f; // 변수를 선언할거에요 -> 돌진 감지 거리를 [SerializeField] private float chargeDuration = 2f; // 변수를 선언할거에요 -> 돌진 지속 시간을 [SerializeField] private float chargeDelay = 3f; // 변수를 선언할거에요 -> 돌진 쿨타임을 [SerializeField] private float prepareTime = 0.5f; // 변수를 선언할거에요 -> 돌진 준비 시간을 [Tooltip("돌진 중 플레이어 추적 회전 속도 (낮을수록 둔하게 추적, 높을수록 예리하게 추적)")] [SerializeField] private float chargeTrackingSpeed = 5f; // 변수를 선언할거에요 -> 돌진 중 실시간 추적 회전 속도를 [Header("애니메이션")] // 인스펙터 제목을 달거에요 -> 애니메이션 State 이름을 [SerializeField] private string chargeAnim = "Run-run"; // 변수를 선언할거에요 -> 돌진 State 이름을 [SerializeField] private string prepareAnim = "Run-wait"; // 변수를 선언할거에요 -> 준비 State 이름을 [SerializeField] private string walkAnim = "Run-walk"; // 변수를 선언할거에요 -> 걷기 State 이름을 private float lastChargeTime; // 변수를 선언할거에요 -> 마지막 돌진 시간을 private bool isCharging; // 변수를 선언할거에요 -> 돌진 중 여부를 private bool isPreparing; // 변수를 선언할거에요 -> 돌진 준비 중 여부를 private Rigidbody _rb; // 변수를 선언할거에요 -> 리지드바디를 private string _lastLoggedState = ""; // 변수를 선언할거에요 -> 직전 로그 State 이름을 // ───────────────────────────────────────────────────────── // 초기화 // ───────────────────────────────────────────────────────── protected override void Awake() // 함수를 실행할거에요 -> 초기화 Awake를 { base.Awake(); // 실행할거에요 -> 부모 Awake를 _rb = GetComponent(); // 가져올거에요 -> 리지드바디를 if (_rb == null) _rb = gameObject.AddComponent(); // 추가할거에요 -> 없으면 새로 _rb.isKinematic = true; // 끌거에요 -> 물리 연산을 } protected override void Init() // 함수를 실행할거에요 -> 추가 초기화를 { if (agent != null) agent.stoppingDistance = 1f; // 설정할거에요 -> 정지 거리를 isCharging = false; // 초기화할거에요 -> 돌진 상태를 isPreparing = false; // 초기화할거에요 -> 준비 상태를 } // ───────────────────────────────────────────────────────── // 디버그 State 감시 // ───────────────────────────────────────────────────────── public override void OnManagedUpdate() // 함수를 정의할거에요 -> 매 프레임 상태 감시를 { base.OnManagedUpdate(); // 실행할거에요 -> 부모 업데이트를 if (!showChargeDebugLog || animator == null) return; // 중단할거에요 -> 디버그 꺼져있으면 AnimatorStateInfo info = animator.GetCurrentAnimatorStateInfo(0); // 가져올거에요 -> 현재 State 정보를 string cur; if (info.IsName(prepareAnim)) cur = prepareAnim; else if (info.IsName(chargeAnim)) cur = chargeAnim; else if (info.IsName(walkAnim)) cur = walkAnim; else if (info.IsName(Monster_Idle)) cur = Monster_Idle; else if (info.IsName(Monster_GetDamage)) cur = Monster_GetDamage; else if (info.IsName(Monster_Die)) cur = Monster_Die; else cur = $"UNKNOWN({info.fullPathHash})"; if (cur == _lastLoggedState) return; // 중단할거에요 -> 같은 State면 중복 출력 안 함 _lastLoggedState = cur; // 저장할거에요 -> 현재 State를 Debug.Log( $"[ChargeMonster] 🎬 {cur}\n" + $" isHit={isHit} | isPreparing={isPreparing} | isCharging={isCharging} | isAttacking={isAttacking} | isResting={isResting}\n" + $" agent.enabled={agent?.enabled} | rb.isKinematic={_rb?.isKinematic}" ); // 출력할거에요 -> State + 상태 변수들을 } // ───────────────────────────────────────────────────────── // ⭐ 피격 처리 오버라이드 // // 돌진 준비(isPreparing) 중 피격 // → StopChargingRoutine()으로 코루틴 종료 + 상태 정리 // → base.TakeDamage()로 체력 감소 + 히트 애니 처리 // // 돌진 중(isCharging) 피격 (슈퍼아머) // → ApplyDamageOnly()로 체력 감소 + UI만 // → 히트 애니/멈춤 없음 // // 일반 상태 피격 // → base.TakeDamage() 그대로 // ───────────────────────────────────────────────────────── public override void TakeDamage(float amount) // 함수를 정의할거에요 -> 피격 처리 오버라이드를 { if (isDead) return; // 중단할거에요 -> 이미 죽었으면 // ── 돌진 중 슈퍼아머 ────────────────────────────────── if (isCharging) // 조건이 맞으면 실행할거에요 -> 돌진 중이면 { if (showChargeDebugLog) Debug.Log($"[ChargeMonster] 🛡️ 슈퍼아머 — 데미지만 ({amount})"); // 출력할거에요 // ⭐ ApplyDamageOnly: 체력 감소 + UI만, StartHit() 호출 없음 ApplyDamageOnly(amount); // 실행할거에요 -> 체력 감소와 UI만 처리를 if (currentHP <= 0) // 조건이 맞으면 실행할거에요 -> 사망했으면 { StopChargingRoutine(); // 실행할거에요 -> 돌진 정리를 Die(); // 실행할거에요 -> 사망 처리를 return; } // 이펙트/소리만 (애니/상태 변화 없음) if (hitEffect != null) hitEffect.Play(); // 재생할거에요 -> 피격 이펙트를 if (hitSound != null && audioSource != null) audioSource.PlayOneShot(hitSound); // 재생할거에요 -> 소리를 return; } // ── 돌진 준비 중 피격 → 돌진 취소 + 일반 히트 ───────── if (isPreparing) // 조건이 맞으면 실행할거에요 -> 준비 중이면 { if (showChargeDebugLog) Debug.Log($"[ChargeMonster] ⛔ 준비 중 피격 — 돌진 취소"); // 출력할거에요 StopChargingRoutine(); // 실행할거에요 -> 준비 코루틴 종료 + 상태 정리를 // 이후 base.TakeDamage()로 일반 피격 처리 } // ── 일반 피격 ───────────────────────────────────────── base.TakeDamage(amount); // 실행할거에요 -> 체력 감소 + StartHit() 호출을 } // ───────────────────────────────────────────────────────── // 돌진 코루틴만 종료 (StopAllCoroutines 사용 안 함) // ───────────────────────────────────────────────────────── private Coroutine _chargeCoroutine; // 변수를 선언할거에요 -> 돌진 코루틴 핸들을 [Header("=== 디버그 ===")] // 인스펙터 제목을 달거에요 -> 디버그 설정을 [SerializeField] private bool showChargeDebugLog = true; // 변수를 선언할거에요 -> 디버그 로그 on/off를 private void StopChargingRoutine() // 함수를 선언할거에요 -> 돌진 코루틴만 종료를 { // ⭐ StopAllCoroutines() 사용 안 함 // HitRoutine 등 다른 코루틴은 건드리지 않음 if (_chargeCoroutine != null) // 조건이 맞으면 실행할거에요 -> 돌진 코루틴이 있으면 { StopCoroutine(_chargeCoroutine); // 취소할거에요 -> 돌진 코루틴만 _chargeCoroutine = null; // 초기화할거에요 -> 핸들을 } if (_rb != null) // 조건이 맞으면 실행할거에요 -> 리지드바디가 있으면 { if (!_rb.isKinematic) _rb.velocity = Vector3.zero; // 멈출거에요 -> 돌진 중일 때만 속도를 _rb.isKinematic = true; // 끌거에요 -> 물리 연산을 } if (agent != null && !agent.enabled) // 조건이 맞으면 실행할거에요 -> 에이전트가 꺼져있으면 agent.enabled = true; // 켤거에요 -> 에이전트를 isPreparing = false; // 해제할거에요 -> 준비 상태를 isCharging = false; // 해제할거에요 -> 돌진 상태를 isAttacking = false; // 해제할거에요 -> 공격 상태를 } // ───────────────────────────────────────────────────────── // AI 로직 // ───────────────────────────────────────────────────────── protected override void ExecuteAILogic() // 함수를 실행할거에요 -> AI 로직을 { if (isHit || isCharging || isPreparing || isAttacking || isResting) return; // 중단할거에요 -> 행동 불가 상태면 float dist = Vector3.Distance(transform.position, playerTransform.position); // 계산할거에요 -> 거리를 if (dist <= chargeRange && Time.time >= lastChargeTime + chargeDelay) // 조건이 맞으면 실행할거에요 -> 돌진 가능하면 { _chargeCoroutine = StartCoroutine(ChargeRoutine()); // 실행할거에요 -> 돌진 코루틴을 (핸들 저장) } else // 조건이 틀리면 실행할거에요 -> 일반 추격 { ChasePlayer(); // 실행할거에요 -> 추격을 } } private void ChasePlayer() // 함수를 선언할거에요 -> 추격 로직을 { if (agent != null && agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> 에이전트가 정상이면 { agent.isStopped = false; // 켤거에요 -> 이동을 agent.SetDestination(playerTransform.position); // 설정할거에요 -> 목적지를 } if (!animator.GetCurrentAnimatorStateInfo(0).IsName(walkAnim)) // 조건이 맞으면 실행할거에요 -> 걷기 아니면 animator.Play(walkAnim, 0, 0f); // 재생할거에요 -> 걷기 애니를 } // ───────────────────────────────────────────────────────── // 돌진 코루틴 // ───────────────────────────────────────────────────────── private IEnumerator ChargeRoutine() // 코루틴을 정의할거에요 -> 돌진 전체 과정을 { // ── 준비 단계 ────────────────────────────────────────── isPreparing = true; // 설정할거에요 -> 준비 중으로 isAttacking = true; // 설정할거에요 -> 공격 중으로 agent.isStopped = true; // 멈출거에요 -> 이동을 Vector3 dir = (playerTransform.position - transform.position).normalized; // 계산할거에요 -> 방향을 dir.y = 0f; // 무시할거에요 -> 수직 성분을 transform.rotation = Quaternion.LookRotation(dir); // 회전할거에요 -> 플레이어 방향으로 if (showChargeDebugLog) Debug.Log($"[ChargeMonster] ⏳ 돌진 준비 시작"); // 출력할거에요 animator.Play(prepareAnim, 0, 0f); // 재생할거에요 -> 준비 애니를 yield return new WaitForSeconds(prepareTime); // 기다릴거에요 -> 준비 시간만큼 // 준비 중 피격으로 isPreparing이 false가 됐으면 종료 // (TakeDamage → StopChargingRoutine에서 이미 상태 정리됨) if (!isPreparing) // 조건이 맞으면 실행할거에요 -> 준비가 취소됐으면 { if (showChargeDebugLog) Debug.Log($"[ChargeMonster] ⛔ 준비 취소 감지 — 코루틴 종료"); // 출력할거에요 _chargeCoroutine = null; // 초기화할거에요 -> 핸들을 yield break; // 중단할거에요 } // ── 돌진 단계 ────────────────────────────────────────── isPreparing = false; // 해제할거에요 -> 준비 상태를 isCharging = true; // 설정할거에요 -> 돌진 중으로 lastChargeTime = Time.time; // 기록할거에요 -> 마지막 돌진 시간을 agent.enabled = false; // 끌거에요 -> NavMesh를 (물리 이동 위해) _rb.isKinematic = false; // 켤거에요 -> 물리 연산을 if (showChargeDebugLog) Debug.Log($"[ChargeMonster] ⚡ 돌진 시작"); // 출력할거에요 animator.Play(chargeAnim, 0, 0f); // 재생할거에요 -> 돌진 애니를 float elapsed = 0f; // 초기화할거에요 -> 경과 시간을 while (elapsed < chargeDuration) // 반복할거에요 -> 지속 시간 동안 { // ⭐ 실시간 추적 — 매 프레임 플레이어 방향으로 재계산 if (playerTransform != null) // 조건이 맞으면 실행할거에요 -> 플레이어가 존재하면 { Vector3 toPlayer = (playerTransform.position - transform.position); // 계산할거에요 -> 플레이어 방향 벡터를 toPlayer.y = 0f; // 무시할거에요 -> 수직 성분을 if (toPlayer.sqrMagnitude > 0.01f) // 조건이 맞으면 실행할거에요 -> 방향이 유효하면 (0벡터 방지) { // 방향 부드럽게 회전 (너무 급격한 방향 전환 방지) Quaternion targetRot = Quaternion.LookRotation(toPlayer.normalized); // 계산할거에요 -> 목표 회전을 transform.rotation = Quaternion.Slerp( // 보간할거에요 -> 현재 회전에서 목표 회전으로 부드럽게 transform.rotation, targetRot, chargeTrackingSpeed * Time.deltaTime // 추적 속도 (Inspector에서 조절) ); } } _rb.velocity = transform.forward * chargeSpeed; // 적용할거에요 -> 현재 방향으로 돌진 속도를 elapsed += Time.deltaTime; // 더할거에요 -> 경과 시간을 yield return null; // 대기할거에요 -> 다음 프레임까지 } // ── 정상 종료 ────────────────────────────────────────── if (!_rb.isKinematic) _rb.velocity = Vector3.zero; // 멈출거에요 -> 속도를 _rb.isKinematic = true; // 끌거에요 -> 물리 연산을 agent.enabled = true; // 켤거에요 -> NavMesh를 isCharging = false; // 해제할거에요 -> 돌진 상태를 _chargeCoroutine = null; // 초기화할거에요 -> 핸들을 if (showChargeDebugLog) Debug.Log($"[ChargeMonster] 🏁 돌진 정상 종료"); // 출력할거에요 OnAttackEnd(); // 실행할거에요 -> 공격 종료 처리를 (isAttacking=false + 휴식) } // ───────────────────────────────────────────────────────── // 충돌 판정 // ───────────────────────────────────────────────────────── private void OnCollisionEnter(Collision col) // 함수를 실행할거에요 -> 충돌 시 { if (!isCharging) return; // 중단할거에요 -> 돌진 중이 아니면 if (!col.gameObject.CompareTag("Player")) return; // 중단할거에요 -> 플레이어가 아니면 IDamageable t = col.gameObject.GetComponent(); // 가져올거에요 -> 데미지 인터페이스를 if (t != null) t.TakeDamage(attackDamage); // 입힐거에요 -> 데미지를 // ⭐ 경직 적용 — 돌진 충돌 시 플레이어를 n초 동안 움직이지 못하게 PlayerMovement pm = col.gameObject.GetComponent(); // 가져올거에요 -> 플레이어 이동 스크립트를 if (pm != null) pm.ApplyStagger(); // 적용할거에요 -> 경직을 (PlayerMovement.staggerDuration 값 사용) } // ───────────────────────────────────────────────────────── // 디버그 기즈모 // ───────────────────────────────────────────────────────── protected override void OnDrawGizmosSelected() // 함수를 선언할거에요 -> 기즈모 오버라이드를 { base.OnDrawGizmosSelected(); // 실행할거에요 -> 부모 기즈모를 먼저 if (!showDebugGizmos) return; // 중단할거에요 -> 기즈모 꺼져있으면 Gizmos.color = new Color(1f, 0.5f, 0f, 0.12f); // 설정할거에요 -> 주황 반투명을 Gizmos.DrawSphere(transform.position, chargeRange); // 그릴거에요 -> 돌진 범위 채우기를 Gizmos.color = new Color(1f, 0.5f, 0f, 1f); // 설정할거에요 -> 주황 외곽선을 Gizmos.DrawWireSphere(transform.position, chargeRange); // 그릴거에요 -> 돌진 범위 외곽선을 #if UNITY_EDITOR if (Application.isPlaying) // 조건이 맞으면 실행할거에요 -> 플레이 중이면 { string label = isCharging ? "⚡ 돌진! (슈퍼아머)" : (isPreparing ? "⏳ 준비..." : ""); // 결정할거에요 if (!string.IsNullOrEmpty(label)) UnityEditor.Handles.Label(transform.position + Vector3.up * 0.5f, label); // 표시할거에요 -> 씬 뷰에 } #endif } }