Projext/Assets/Scripts/Enemy/AI/ChargeMonster.cs

312 lines
19 KiB
C#
Raw Normal View History

2026-02-20 16:39:14 +00:00
using UnityEngine; // 유니티 기능을 불러올거에요 -> UnityEngine을
2026-02-13 09:11:54 +00:00
using System.Collections; // 코루틴을 사용할거에요 -> System.Collections를
2026-02-04 14:06:25 +00:00
2026-02-13 09:11:54 +00:00
public class ChargeMonster : MonsterClass // 클래스를 선언할거에요 -> 돌진 몬스터를
2026-02-04 14:06:25 +00:00
{
2026-02-13 09:11:54 +00:00
[Header("=== 돌진 설정 ===")] // 인스펙터 제목을 달거에요 -> 돌진 설정을
2026-02-19 18:14:55 +00:00
[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; // 변수를 선언할거에요 -> 돌진 준비 시간을
2026-02-25 10:39:20 +00:00
[Tooltip("돌진 중 플레이어 추적 회전 속도 (낮을수록 둔하게 추적, 높을수록 예리하게 추적)")]
[SerializeField] private float chargeTrackingSpeed = 5f; // 변수를 선언할거에요 -> 돌진 중 실시간 추적 회전 속도를
2026-02-13 09:11:54 +00:00
2026-02-19 18:14:55 +00:00
[Header("애니메이션")] // 인스펙터 제목을 달거에요 -> 애니메이션 State 이름을
[SerializeField] private string chargeAnim = "Run-run"; // 변수를 선언할거에요 -> 돌진 State 이름을
[SerializeField] private string prepareAnim = "Run-wait"; // 변수를 선언할거에요 -> 준비 State 이름을
[SerializeField] private string walkAnim = "Run-walk"; // 변수를 선언할거에요 -> 걷기 State 이름을
2026-02-13 09:11:54 +00:00
2026-02-19 18:14:55 +00:00
private float lastChargeTime; // 변수를 선언할거에요 -> 마지막 돌진 시간을
private bool isCharging; // 변수를 선언할거에요 -> 돌진 중 여부를
private bool isPreparing; // 변수를 선언할거에요 -> 돌진 준비 중 여부를
private Rigidbody _rb; // 변수를 선언할거에요 -> 리지드바디를
private string _lastLoggedState = ""; // 변수를 선언할거에요 -> 직전 로그 State 이름을
// ─────────────────────────────────────────────────────────
// 초기화
// ─────────────────────────────────────────────────────────
2026-02-13 09:11:54 +00:00
protected override void Awake() // 함수를 실행할거에요 -> 초기화 Awake를
2026-02-04 14:06:25 +00:00
{
2026-02-19 18:14:55 +00:00
base.Awake(); // 실행할거에요 -> 부모 Awake를
2026-02-13 09:11:54 +00:00
_rb = GetComponent<Rigidbody>(); // 가져올거에요 -> 리지드바디를
if (_rb == null) _rb = gameObject.AddComponent<Rigidbody>(); // 추가할거에요 -> 없으면 새로
_rb.isKinematic = true; // 끌거에요 -> 물리 연산을
2026-02-04 14:06:25 +00:00
}
2026-02-19 18:14:55 +00:00
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; // 변수를 선언할거에요 -> 돌진 코루틴 핸들을
2026-02-20 16:39:14 +00:00
[Header("=== 디버그 ===")] // 인스펙터 제목을 달거에요 -> 디버그 설정을
[SerializeField] private bool showChargeDebugLog = true; // 변수를 선언할거에요 -> 디버그 로그 on/off를
2026-02-19 18:14:55 +00:00
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 로직
// ─────────────────────────────────────────────────────────
2026-02-04 14:06:25 +00:00
2026-02-13 09:11:54 +00:00
protected override void ExecuteAILogic() // 함수를 실행할거에요 -> AI 로직을
2026-02-04 14:06:25 +00:00
{
2026-02-19 18:14:55 +00:00
if (isHit || isCharging || isPreparing || isAttacking || isResting) return; // 중단할거에요 -> 행동 불가 상태면
2026-02-13 09:11:54 +00:00
float dist = Vector3.Distance(transform.position, playerTransform.position); // 계산할거에요 -> 거리를
2026-02-19 18:14:55 +00:00
if (dist <= chargeRange && Time.time >= lastChargeTime + chargeDelay) // 조건이 맞으면 실행할거에요 -> 돌진 가능하면
{
_chargeCoroutine = StartCoroutine(ChargeRoutine()); // 실행할거에요 -> 돌진 코루틴을 (핸들 저장)
}
else // 조건이 틀리면 실행할거에요 -> 일반 추격
{
ChasePlayer(); // 실행할거에요 -> 추격을
}
2026-02-04 14:06:25 +00:00
}
2026-02-13 09:11:54 +00:00
private void ChasePlayer() // 함수를 선언할거에요 -> 추격 로직을
2026-02-04 14:06:25 +00:00
{
2026-02-19 18:14:55 +00:00
if (agent != null && agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> 에이전트가 정상이면
{
agent.isStopped = false; // 켤거에요 -> 이동을
agent.SetDestination(playerTransform.position); // 설정할거에요 -> 목적지를
}
if (!animator.GetCurrentAnimatorStateInfo(0).IsName(walkAnim)) // 조건이 맞으면 실행할거에요 -> 걷기 아니면
animator.Play(walkAnim, 0, 0f); // 재생할거에요 -> 걷기 애니를
2026-02-04 14:06:25 +00:00
}
2026-02-19 18:14:55 +00:00
// ─────────────────────────────────────────────────────────
// 돌진 코루틴
// ─────────────────────────────────────────────────────────
private IEnumerator ChargeRoutine() // 코루틴을 정의할거에요 -> 돌진 전체 과정을
2026-02-04 14:06:25 +00:00
{
2026-02-19 18:14:55 +00:00
// ── 준비 단계 ──────────────────────────────────────────
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] ⏳ 돌진 준비 시작"); // 출력할거에요
2026-02-04 14:06:25 +00:00
2026-02-19 18:14:55 +00:00
animator.Play(prepareAnim, 0, 0f); // 재생할거에요 -> 준비 애니를
2026-02-12 15:23:25 +00:00
yield return new WaitForSeconds(prepareTime); // 기다릴거에요 -> 준비 시간만큼
2026-02-04 14:06:25 +00:00
2026-02-19 18:14:55 +00:00
// 준비 중 피격으로 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); // 재생할거에요 -> 돌진 애니를
2026-02-05 15:42:48 +00:00
2026-02-13 09:11:54 +00:00
float elapsed = 0f; // 초기화할거에요 -> 경과 시간을
while (elapsed < chargeDuration) // 반복할거에요 -> 지속 시간 동안
2026-02-04 14:06:25 +00:00
{
2026-02-25 10:39:20 +00:00
// ⭐ 실시간 추적 — 매 프레임 플레이어 방향으로 재계산
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; // 적용할거에요 -> 현재 방향으로 돌진 속도를
2026-02-19 18:14:55 +00:00
elapsed += Time.deltaTime; // 더할거에요 -> 경과 시간을
2026-02-12 15:23:25 +00:00
yield return null; // 대기할거에요 -> 다음 프레임까지
2026-02-04 14:06:25 +00:00
}
2026-02-19 18:14:55 +00:00
// ── 정상 종료 ──────────────────────────────────────────
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 + 휴식)
2026-02-04 14:06:25 +00:00
}
2026-02-19 18:14:55 +00:00
// ─────────────────────────────────────────────────────────
// 충돌 판정
// ─────────────────────────────────────────────────────────
2026-02-13 09:11:54 +00:00
private void OnCollisionEnter(Collision col) // 함수를 실행할거에요 -> 충돌 시
2026-02-04 14:06:25 +00:00
{
2026-02-19 18:14:55 +00:00
if (!isCharging) return; // 중단할거에요 -> 돌진 중이 아니면
if (!col.gameObject.CompareTag("Player")) return; // 중단할거에요 -> 플레이어가 아니면
IDamageable t = col.gameObject.GetComponent<IDamageable>(); // 가져올거에요 -> 데미지 인터페이스를
if (t != null) t.TakeDamage(attackDamage); // 입힐거에요 -> 데미지를
2026-02-25 10:39:20 +00:00
// ⭐ 경직 적용 — 돌진 충돌 시 플레이어를 n초 동안 움직이지 못하게
PlayerMovement pm = col.gameObject.GetComponent<PlayerMovement>(); // 가져올거에요 -> 플레이어 이동 스크립트를
if (pm != null) pm.ApplyStagger(); // 적용할거에요 -> 경직을 (PlayerMovement.staggerDuration 값 사용)
}
2026-02-19 18:14:55 +00:00
// ─────────────────────────────────────────────────────────
// 디버그 기즈모
// ─────────────────────────────────────────────────────────
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) // 조건이 맞으면 실행할거에요 -> 플레이 중이면
2026-02-06 09:27:08 +00:00
{
2026-02-19 18:14:55 +00:00
string label = isCharging ? "⚡ 돌진! (슈퍼아머)" : (isPreparing ? "⏳ 준비..." : ""); // 결정할거에요
if (!string.IsNullOrEmpty(label))
UnityEditor.Handles.Label(transform.position + Vector3.up * 0.5f, label); // 표시할거에요 -> 씬 뷰에
2026-02-04 14:06:25 +00:00
}
2026-02-19 18:14:55 +00:00
#endif
2026-02-04 14:06:25 +00:00
}
2026-02-05 15:42:48 +00:00
}