288 lines
18 KiB
C#
288 lines
18 KiB
C#
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; // 변수를 선언할거에요 -> 돌진 준비 시간을
|
|
|
|
[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<Rigidbody>(); // 가져올거에요 -> 리지드바디를
|
|
if (_rb == null) _rb = gameObject.AddComponent<Rigidbody>(); // 추가할거에요 -> 없으면 새로
|
|
_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) // 반복할거에요 -> 지속 시간 동안
|
|
{
|
|
_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<IDamageable>(); // 가져올거에요 -> 데미지 인터페이스를
|
|
if (t != null) t.TakeDamage(attackDamage); // 입힐거에요 -> 데미지를
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────
|
|
// 디버그 기즈모
|
|
// ─────────────────────────────────────────────────────────
|
|
|
|
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
|
|
}
|
|
} |