using UnityEngine; // 유니티 기능을 불러올거에요 -> UnityEngine을 using UnityEngine.AI; // 길찾기 기능을 불러올거에요 -> UnityEngine.AI를 using System.Collections; // 코루틴을 사용할거에요 -> System.Collections를 using System; // 시스템 기능을 사용할거에요 -> System을 public abstract class MonsterClass : MonoBehaviour, IDamageable // 추상 클래스를 선언할거에요 -> MonsterClass를 { [Header("--- 최적화 ---")] // 인스펙터 제목을 달거에요 -> 최적화 설정을 protected Renderer mobRenderer; // 변수를 선언할거에요 -> 렌더러를 protected Transform playerTransform; // 변수를 선언할거에요 -> 플레이어 위치를 [SerializeField] protected float optimizationDistance = 40f; // 변수를 선언할거에요 -> 최적화 거리를 [Header("스탯")] // 인스펙터 제목을 달거에요 -> 스탯 설정을 [SerializeField] protected float maxHP = 100f; // 변수를 선언할거에요 -> 최대 체력을 [SerializeField] protected float attackDamage = 10f; // 변수를 선언할거에요 -> 공격력을 [SerializeField] protected int expReward = 10; // 변수를 선언할거에요 -> 경험치 보상을 [SerializeField] protected float moveSpeed = 3.5f; // 변수를 선언할거에요 -> 이동 속도를 protected float currentHP; // 변수를 선언할거에요 -> 현재 체력을 public float GetCurrentHP() => currentHP; // 함수를 선언할거에요 -> 현재 체력을 반환하는 GetCurrentHP를 public float GetMaxHP() => maxHP; // 함수를 선언할거에요 -> 최대 체력을 반환하는 GetMaxHP를 public event Action OnHealthChanged; // 이벤트를 선언할거에요 -> 체력 변경 알림을 [Header("전투")] // 인스펙터 제목을 달거에요 -> 전투 설정을 [SerializeField] protected MonsterWeapon myWeapon; // 변수를 선언할거에요 -> 무기를 [Header("애니메이션")] // 인스펙터 제목을 달거에요 -> 애니메이션 State 이름을 [SerializeField] protected string Monster_Idle = "Monster_Idle"; // 변수를 선언할거에요 -> 대기 애니 이름을 [SerializeField] protected string Monster_GetDamage = "Monster_GetDamage"; // 변수를 선언할거에요 -> 피격 애니 이름을 [SerializeField] protected string Monster_Die = "Monster_Die"; // 변수를 선언할거에요 -> 사망 애니 이름을 [Tooltip("피격 애니 길이를 못 읽을 때 이 시간 후 자동으로 피격 상태 해제")] [SerializeField] protected float hitRecoverFallback = 1f; // 변수를 선언할거에요 -> 피격 최대 유지 시간을 protected Animator animator; // 변수를 선언할거에요 -> 애니메이터를 protected NavMeshAgent agent; // 변수를 선언할거에요 -> 에이전트를 protected AudioSource audioSource; // 변수를 선언할거에요 -> 오디오 소스를 protected StatusEffectProcessor statusProcessor; // 변수를 선언할거에요 -> 상태이상 처리기를 protected bool isHit, isDead, isAttacking; // 상태 변수들을 선언할거에요 public bool IsDead => isDead; // 프로퍼티를 선언할거에요 -> 외부에서 사망 여부를 읽을 수 있는 IsDead를 public bool IsAggroed { get; protected set; } // 프로퍼티를 선언할거에요 -> 어그로 여부를 [Header("AI")] // 인스펙터 제목을 달거에요 -> AI 설정을 [SerializeField] protected float attackRestDuration = 1.5f; // 변수를 선언할거에요 -> 공격 후 휴식 시간을 protected bool isResting; // 변수를 선언할거에요 -> 휴식 여부를 [Header("감지")] // 인스펙터 제목을 달거에요 -> 감지 설정을 [SerializeField] protected float detectionRange = 10f; // 변수를 선언할거에요 -> 플레이어 자동 감지 거리를 [Header("디버그")] // 인스펙터 제목을 달거에요 -> 디버그 설정을 [SerializeField] protected bool showDebugGizmos = false; // 변수를 선언할거에요 -> Scene 뷰 기즈모 on/off를 public static Action OnMonsterKilled; // 이벤트를 선언할거에요 -> 사망 전역 알림을 [Header("사운드/이펙트")] // 인스펙터 제목을 달거에요 -> 효과 설정을 [SerializeField] protected AudioClip hitSound, deathSound; // 변수를 선언할거에요 -> 효과음들을 [SerializeField] protected GameObject deathEffectPrefab; // 변수를 선언할거에요 -> 사망 이펙트를 [SerializeField] protected ParticleSystem hitEffect; // 변수를 선언할거에요 -> 피격 이펙트를 [SerializeField] protected Transform impactSpawnPoint; // 변수를 선언할거에요 -> 이펙트 위치를 // ─── 피격 전용 코루틴 핸들 ─────────────────────────────── private Coroutine _hitCoroutine; // 변수를 선언할거에요 -> 피격 코루틴 핸들을 (개별 취소용) // ─── 시각 효과 ─────────────────────────────────────────── protected MonsterVisualFX visualFX; // 변수를 선언할거에요 -> 피격 깜빡임/사망 페이드 처리기를 // ───────────────────────────────────────────────────────── // 초기화 // ───────────────────────────────────────────────────────── protected virtual void Awake() // 함수를 실행할거에요 -> 컴포넌트 캐싱을 { animator = GetComponent(); // 가져올거에요 -> 애니메이터를 agent = GetComponent(); // 가져올거에요 -> 에이전트를 audioSource = GetComponent(); // 가져올거에요 -> 오디오 소스를 mobRenderer = GetComponentInChildren(); // 가져올거에요 -> 렌더러를 visualFX = GetComponent(); // 가져올거에요 -> 시각 효과 스크립트를 statusProcessor = new StatusEffectProcessor(this, this, agent, animator); // 생성할거에요 -> 상태이상 처리기를 (this = 코루틴 주체 + IDamageable) if (agent != null) agent.speed = moveSpeed; // 설정할거에요 -> 이동 속도를 Init(); // 실행할거에요 -> 자식 클래스 추가 초기화를 } protected virtual void OnEnable() // 함수를 실행할거에요 -> 활성화/스폰될 때마다 { playerTransform = GameObject.FindGameObjectWithTag("Player")?.transform; // 찾을거에요 -> 플레이어를 ResetStats(); // 초기화할거에요 -> 체력/상태를 if (mobRenderer != null) mobRenderer.enabled = true; // 켤거에요 -> 렌더러를 if (MobUpdateManager.Instance != null) MobUpdateManager.Instance.RegisterMob(this); // 등록할거에요 -> 업데이트 매니저에 } protected virtual void Init() { } // 가상 함수를 선언할거에요 -> 자식 클래스 전용 추가 초기화를 protected virtual void OnDisable() // 함수를 실행할거에요 -> 비활성화 시 { if (MobUpdateManager.Instance != null) MobUpdateManager.Instance.UnregisterMob(this); // 해제할거에요 -> 매니저에서 } // ───────────────────────────────────────────────────────── // 리셋 // ───────────────────────────────────────────────────────── public void ResetStats() // 함수를 선언할거에요 -> 스탯/상태 완전 초기화를 { isDead = false; // 초기화할거에요 -> 사망 상태를 IsAggroed = false; // 초기화할거에요 -> 어그로 상태를 isHit = false; // 초기화할거에요 -> 피격 상태를 isAttacking = false; // 초기화할거에요 -> 공격 상태를 isResting = false; // 초기화할거에요 -> 휴식 상태를 _hitCoroutine = null; // 초기화할거에요 -> 피격 코루틴 핸들을 currentHP = maxHP; // 채울거에요 -> 체력을 최대로 OnHealthChanged?.Invoke(currentHP, maxHP); // 알릴거에요 -> UI에 체력 변경을 Collider col = GetComponent(); // 가져올거에요 -> 콜라이더를 if (col != null) col.enabled = true; // 켤거에요 -> 콜라이더를 if (agent != null) { agent.speed = moveSpeed; // 복구할거에요 -> 이동 속도를 agent.isStopped = false; // 켤거에요 -> 이동을 } StopAllCoroutines(); // 중단할거에요 -> 이전 코루틴들을 (리셋 시에만 전체 중단) if (visualFX != null) visualFX.ResetVisuals(); // [NEW] 실행할거에요 -> 머티리얼/색상 원래대로 복원 OnResetStats(); // 실행할거에요 -> 자식 클래스 추가 리셋을 } protected virtual void Update() => OnManagedUpdate(); // 실행할거에요 -> 관리형 업데이트를 public virtual void Reactivate() // 함수를 선언할거에요 -> 재활성화 추가 처리를 { if (agent != null && agent.isOnNavMesh) { agent.isStopped = false; // 재개할거에요 -> 이동을 if (IsAggroed && playerTransform != null) agent.SetDestination(playerTransform.position); // 이동할거에요 -> 플레이어에게 } } protected abstract void ExecuteAILogic(); // 추상 함수를 선언할거에요 -> 자식이 반드시 구현해야 하는 AI를 protected virtual void OnResetStats() { } // 가상 함수를 선언할거에요 -> 자식 클래스 추가 리셋을 // ───────────────────────────────────────────────────────── // MobUpdateManager 전용 업데이트 // ───────────────────────────────────────────────────────── public virtual void OnManagedUpdate() // 함수를 선언할거에요 -> 매니저가 매 프레임 호출하는 업데이트를 { if (isDead || playerTransform == null || !gameObject.activeInHierarchy) return; // 중단할거에요 -> 작동 불가 상태면 float dist = Vector3.Distance(transform.position, playerTransform.position); // 계산할거에요 -> 플레이어와의 거리를 if (!IsAggroed && dist <= detectionRange) // 조건이 맞으면 실행할거에요 -> 감지 범위 안이면 IsAggroed = true; // 설정할거에요 -> 어그로 상태로 if (dist > optimizationDistance && !IsAggroed) // 조건이 맞으면 실행할거에요 -> 최적화 대상이면 { StopMovement(); // 멈출거에요 -> 이동을 if (mobRenderer != null) mobRenderer.enabled = false; // 끌거에요 -> 렌더러를 return; } if (mobRenderer != null) mobRenderer.enabled = true; // 켤거에요 -> 렌더러를 if (!IsAggroed) // 조건이 맞으면 실행할거에요 -> 어그로가 없으면 대기 { if (agent != null && agent.isOnNavMesh) { agent.isStopped = true; agent.velocity = Vector3.zero; } if (animator != null && animator.isInitialized && !animator.GetCurrentAnimatorStateInfo(0).IsName(Monster_Idle)) animator.Play(Monster_Idle, 0, 0f); // 재생할거에요 -> 대기 애니를 return; } if (agent != null && agent.isOnNavMesh && agent.isStopped) // 조건이 맞으면 실행할거에요 -> 멈춰있으면 agent.isStopped = false; // 켤거에요 -> 이동을 ExecuteAILogic(); // 실행할거에요 -> AI 로직을 } // ───────────────────────────────────────────────────────── // 이동 정지 // ───────────────────────────────────────────────────────── protected void StopMovement() // 함수를 선언할거에요 -> 이동 정지를 { if (agent != null && agent.isOnNavMesh) { agent.isStopped = true; agent.velocity = Vector3.zero; } if (animator != null) { animator.SetFloat("Speed", 0f); // 설정할거에요 -> 속도 파라미터를 0으로 if (!animator.GetCurrentAnimatorStateInfo(0).IsName(Monster_Idle)) animator.Play(Monster_Idle, 0, 0f); // 재생할거에요 -> 대기 애니를 } } // ← 빠져있던 닫는 중괄호 복원 // ───────────────────────────────────────────────────────── // 피격 처리 // // ⭐ 핵심 구조: // StopAllCoroutines() 절대 사용 안 함 // _hitCoroutine 핸들로 피격 코루틴만 개별 관리 // HitRoutine이 클립 길이만큼 기다린 뒤 OnHitEnd() 자동 호출 // → Animation Event 등록 여부와 무관하게 항상 복구됨 // ───────────────────────────────────────────────────────── public virtual void TakeDamage(float amount) // 함수를 선언할거에요 -> 피격 처리를 { if (isDead) return; // 중단할거에요 -> 이미 죽었으면 IsAggroed = true; // 설정할거에요 -> 피격 시 즉시 어그로를 currentHP -= amount; // 줄일거에요 -> 체력을 OnHealthChanged?.Invoke(currentHP, maxHP); // 알릴거에요 -> UI에 체력 변경을 if (currentHP <= 0) // 조건이 맞으면 실행할거에요 -> 체력이 바닥났으면 { Die(); // 실행할거에요 -> 사망 처리를 return; } if (!isHit) // 조건이 맞으면 실행할거에요 -> 아직 피격 상태가 아니면 StartHit(); // 실행할거에요 -> 피격 처리를 } // InvokeHitRecovery — 자식에서 직접 OnHealthChanged 없이 데미지+UI만 처리할 때 사용 protected void ApplyDamageOnly(float amount) // 함수를 선언할거에요 -> 체력 감소+UI만 처리를 (StartHit 없이) { IsAggroed = true; // 설정할거에요 -> 어그로를 currentHP -= amount; // 줄일거에요 -> 체력을 OnHealthChanged?.Invoke(currentHP, maxHP); // 알릴거에요 -> UI에 } private void StartHit() // ← 빠져있던 메서드 선언부 복원 { isHit = true; // 설정할거에요 -> 피격 상태를 isAttacking = false; // 해제할거에요 -> 공격 상태를 isResting = false; // 해제할거에요 -> 휴식 상태를 // 이전 피격 코루틴만 취소 (다른 코루틴은 건드리지 않음) if (_hitCoroutine != null) // 조건이 맞으면 실행할거에요 -> 이전 피격 코루틴이 있으면 { StopCoroutine(_hitCoroutine); // 취소할거에요 -> 이전 피격 코루틴만 _hitCoroutine = null; // 초기화할거에요 -> 핸들을 } OnStartHit(); // 실행할거에요 -> 자식 추가 처리를 if (agent != null && agent.isOnNavMesh) { agent.isStopped = true; agent.velocity = Vector3.zero; } // 멈출거에요 -> 이동을 if (hitEffect != null) hitEffect.Play(); // 재생할거에요 -> 이펙트를 if (hitSound != null && audioSource != null) audioSource.PlayOneShot(hitSound); // 재생할거에요 -> 피격 소리를 if (visualFX != null) visualFX.Flash(); // [NEW] 실행할거에요 -> 피격 깜빡임 효과를 // 피격 코루틴 시작 — 애니 재생 + 클립 길이 후 자동 OnHitEnd() _hitCoroutine = StartCoroutine(HitRoutine()); // 시작할거에요 -> 피격 코루틴을 } private IEnumerator HitRoutine() // 코루틴을 정의할거에요 -> 피격 애니 재생 후 자동 복구를 { float waitTime = hitRecoverFallback; // 초기화할거에요 -> 대기 시간을 기본값으로 int hash = Animator.StringToHash(Monster_GetDamage); // 계산할거에요 -> 피격 State 해시를 if (animator != null && animator.HasState(0, hash)) // 조건이 맞으면 실행할거에요 -> State가 있으면 { animator.Play(Monster_GetDamage, 0, 0f); // 재생할거에요 -> 피격 애니를 yield return null; // 대기할거에요 -> 1프레임 (State 전환 대기) AnimatorClipInfo[] clips = animator.GetCurrentAnimatorClipInfo(0); // 가져올거에요 -> 클립 정보를 if (clips.Length > 0 && clips[0].clip != null) // 조건이 맞으면 실행할거에요 -> 클립이 있으면 waitTime = clips[0].clip.length; // 저장할거에요 -> 실제 클립 길이를 } else // 조건이 틀리면 실행할거에요 -> State가 없으면 { Debug.LogWarning($"[MonsterClass] '{Monster_GetDamage}' State 없음! {hitRecoverFallback}초 후 자동 복구"); // 경고를 찍을거에요 } yield return new WaitForSeconds(waitTime); // 기다릴거에요 -> 클립 길이 or 기본값만큼 if (isHit && !isDead) // 조건이 맞으면 실행할거에요 -> 아직 피격 상태이고 살아있으면 OnHitEnd(); // 실행할거에요 -> 피격 종료를 } protected virtual void OnStartHit() // 가상 함수를 선언할거에요 -> 자식 추가 피격 처리를 { if (myWeapon != null) myWeapon.DisableHitBox(); // 끌거에요 -> 무기 판정을 } public virtual void OnHitEnd() // ← 빠져있던 메서드 선언부 복원 { isHit = false; // 해제할거에요 -> 피격 상태를 _hitCoroutine = null; // 초기화할거에요 -> 코루틴 핸들을 if (agent != null && agent.isOnNavMesh) agent.isStopped = false; // 재개할거에요 -> 이동을 } // ───────────────────────────────────────────────────────── // 넉백 // ───────────────────────────────────────────────────────── private Coroutine _knockbackCoroutine; // 변수를 선언할거에요 -> 넉백 코루틴 핸들을 /// /// [NEW] 외부에서 호출 — 화살 명중 시 넉백 적용 /// public void ApplyKnockback(Vector3 direction, float force, float duration) // 함수를 선언할거에요 -> 넉백을 적용하는 ApplyKnockback을 { if (isDead) return; // 조건이 맞으면 중단할거에요 -> 죽었으면 if (_knockbackCoroutine != null) StopCoroutine(_knockbackCoroutine); // 조건이 맞으면 취소할거에요 -> 이전 넉백 코루틴을 _knockbackCoroutine = StartCoroutine(KnockbackRoutine(direction, force, duration)); // 실행할거에요 -> 넉백 코루틴을 } private IEnumerator KnockbackRoutine(Vector3 direction, float force, float duration) // 코루틴을 정의할거에요 -> 넉백 이동을 처리하는 KnockbackRoutine을 { if (agent != null && agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> 에이전트가 있고 NavMesh 위라면 { agent.isStopped = true; // 멈출거에요 -> 이동을 agent.velocity = Vector3.zero; // 초기화할거에요 -> 현재 속도를 } float elapsed = 0f; // 변수를 초기화할거에요 -> 경과 시간을 0으로 Vector3 startPos = transform.position; // 값을 저장할거에요 -> 시작 위치를 Vector3 targetPos = startPos + direction * force; // 값을 계산할거에요 -> 넉백 목표 위치를 // NavMesh 위의 유효한 위치인지 확인 UnityEngine.AI.NavMeshHit navHit; // 변수를 선언할거에요 -> NavMesh 히트 정보를 if (UnityEngine.AI.NavMesh.SamplePosition(targetPos, out navHit, force, UnityEngine.AI.NavMesh.AllAreas)) // 조건이 맞으면 실행할거에요 -> NavMesh 위 유효 위치를 찾았다면 { targetPos = navHit.position; // 값을 보정할거에요 -> NavMesh 위의 가장 가까운 유효 위치로 } while (elapsed < duration) // 반복할거에요 -> 지속 시간 동안 { elapsed += Time.unscaledDeltaTime; // 값을 더할거에요 -> 히트스톱 중에도 작동하게 unscaled 시간을 float t = elapsed / duration; // 비율을 계산할거에요 -> 진행도를 float easeOut = 1f - (1f - t) * (1f - t); // 이징을 계산할거에요 -> 처음 빠르고 끝에 감속하는 easeOut을 transform.position = Vector3.Lerp(startPos, targetPos, easeOut); // 위치를 보간할거에요 -> 시작에서 목표로 부드럽게 yield return null; // 다음 프레임까지 기다릴거에요 } _knockbackCoroutine = null; // 초기화할거에요 -> 코루틴 핸들을 if (agent != null && agent.isOnNavMesh && !isDead && !isHit) // 조건이 맞으면 실행할거에요 -> 에이전트가 유효하고 살아있다면 { agent.Warp(transform.position); // 워프할거에요 -> 현재 위치로 에이전트를 동기화 agent.isStopped = false; // 재개할거에요 -> 이동을 } } // ───────────────────────────────────────────────────────── // 사망 // ───────────────────────────────────────────────────────── protected virtual void Die() // 함수를 선언할거에요 -> 사망 처리를 { isDead = true; // 설정할거에요 -> 사망 상태를 IsAggroed = false; // 해제할거에요 -> 어그로를 // 피격 코루틴만 정리 if (_hitCoroutine != null) { StopCoroutine(_hitCoroutine); _hitCoroutine = null; } // 취소할거에요 -> 피격 코루틴을 OnDie(); // 실행할거에요 -> 자식 추가 처리를 OnMonsterKilled?.Invoke(expReward); // 알릴거에요 -> 경험치 지급을 Collider col = GetComponent(); // 가져올거에요 -> 콜라이더를 if (col != null) col.enabled = false; // 끌거에요 -> 충돌을 if (agent != null && agent.isOnNavMesh) { agent.isStopped = true; agent.velocity = Vector3.zero; } // 멈출거에요 -> 이동을 if (animator != null) animator.Play(Monster_Die, 0, 0f); // 재생할거에요 -> 사망 애니를 if (deathSound != null && audioSource != null) audioSource.PlayOneShot(deathSound); // 재생할거에요 -> 사망 소리를 // [NEW] 시각 효과가 있으면 페이드아웃으로 처리, 없으면 기존 방식 if (visualFX != null) // 조건이 맞으면 실행할거에요 -> 시각 효과 스크립트가 있다면 { visualFX.StartDeathFade(); // 실행할거에요 -> 사망 페이드 (투명해지면서 가라앉기) // StartDeathFade가 완료 후 SetActive(false) 처리함 } else // 조건이 틀리면 실행할거에요 -> 시각 효과가 없다면 { Invoke(nameof(ReturnToPool), 1.5f); // 예약할거에요 -> 1.5초 후 비활성화를 } } protected virtual void OnDie() // 가상 함수를 선언할거에요 -> 자식 추가 사망 처리를 { if (myWeapon != null) myWeapon.DisableHitBox(); // 끌거에요 -> 무기 판정을 } protected void ReturnToPool() => gameObject.SetActive(false); // 함수를 선언할거에요 -> 비활성화를 // ───────────────────────────────────────────────────────── // 공격 // ───────────────────────────────────────────────────────── public virtual void OnAttackStart() // 함수를 선언할거에요 -> 공격 시작을 { isAttacking = true; // 설정할거에요 -> 공격 중으로 isResting = false; // 해제할거에요 -> 휴식 상태를 if (myWeapon != null) myWeapon.EnableHitBox(); // 켤거에요 -> 무기 판정을 } public virtual void OnAttackEnd() // ← 빠져있던 메서드 선언부 복원 { if (myWeapon != null) myWeapon.DisableHitBox(); // 끌거에요 -> 무기 판정을 isAttacking = false; // 해제할거에요 -> 공격 상태를 if (!isDead && !isHit) StartCoroutine(RestAfterAttack()); // 시작할거에요 -> 공격 후 휴식을 } protected virtual IEnumerator RestAfterAttack() // 코루틴을 선언할거에요 -> 공격 후 휴식 로직을 { isResting = true; // 설정할거에요 -> 휴식 상태를 yield return new WaitForSeconds(attackRestDuration); // 기다릴거에요 -> 휴식 시간만큼 isResting = false; // 해제할거에요 -> 휴식 상태를 } // ───────────────────────────────────────────────────────── // 상태이상 // ───────────────────────────────────────────────────────── /// /// [기존] 상태이상 적용 — 3-파라미터 (하위 호환) /// public void ApplyStatusEffect(StatusEffectType type, float dmg, float dur) // 함수를 선언할거에요 -> 상태이상 적용을 (3-파라미터 버전) { ApplyStatusEffect(type, dmg, dur, 0.5f, 1f); // 실행할거에요 -> 5-파라미터 버전을 기본 틱간격 0.5초, 강도 1로 } /// /// [NEW] 상태이상 적용 — 5-파라미터 (Arrow 인스펙터 연동) /// tickInterval: 도트 데미지 틱 간격 (초) /// strength: 특수 효과 강도 (슬로우 비율, 스턴 시간 등) /// public void ApplyStatusEffect(StatusEffectType type, float dotDmg, float dur, float tickInterval, float strength) // 함수를 선언할거에요 -> 상태이상 적용을 (5-파라미터 버전) { if (isDead) return; // 중단할거에요 -> 죽었으면 if (statusProcessor == null) // 조건이 맞으면 실행할거에요 -> 상태이상 처리기가 없으면 { Debug.LogWarning($"[MonsterClass] {gameObject.name}에 StatusEffectProcessor가 없습니다!"); // 경고를 출력할거에요 -> 누락 안내를 return; // 중단할거에요 -> 함수를 } switch (type) // 분기할거에요 -> 상태이상 타입에 따라 { case StatusEffectType.Burn: // 조건이 맞으면 실행할거에요 -> 화상이라면 statusProcessor.ApplyBurn(dotDmg, dur, tickInterval); // 적용할거에요 -> 화상을 (틱 간격 포함) break; // 분기를 종료할거에요 case StatusEffectType.Slow: // 조건이 맞으면 실행할거에요 -> 슬로우라면 statusProcessor.ApplySlow(strength, dur); // 적용할거에요 -> 슬로우를 (strength = 감속 비율) break; // 분기를 종료할거에요 case StatusEffectType.Poison: // 조건이 맞으면 실행할거에요 -> 독이라면 statusProcessor.ApplyPoison(dotDmg, dur, tickInterval); // 적용할거에요 -> 독을 (틱 간격 포함) break; // 분기를 종료할거에요 case StatusEffectType.Shock: // 조건이 맞으면 실행할거에요 -> 감전이라면 statusProcessor.ApplyShock(dotDmg, dur); // 적용할거에요 -> 감전을 (스턴) break; // 분기를 종료할거에요 } } // ───────────────────────────────────────────────────────── // 디버그 기즈모 // ───────────────────────────────────────────────────────── protected virtual void OnDrawGizmosSelected() // 함수를 선언할거에요 -> Scene 뷰 기즈모를 { if (!showDebugGizmos) return; // 중단할거에요 -> 기즈모가 꺼져 있으면 bool aggroed = Application.isPlaying && IsAggroed; // 판단할거에요 -> 런타임 어그로 여부를 Gizmos.color = aggroed ? new Color(1f, 0.2f, 0.1f, 0.18f) : new Color(1f, 0.9f, 0f, 0.10f); Gizmos.DrawSphere(transform.position, detectionRange); // 그릴거에요 -> 감지 범위 채우기를 Gizmos.color = aggroed ? Color.red : Color.yellow; // 설정할거에요 -> 외곽선 색을 Gizmos.DrawWireSphere(transform.position, detectionRange); // 그릴거에요 -> 감지 범위 외곽선을 #if UNITY_EDITOR if (Application.isPlaying) // 조건이 맞으면 실행할거에요 -> 플레이 중이면 { string stateLabel = aggroed ? "[ 추격 중 ]" : "[ 대기 ]"; // 결정할거에요 -> 표시할 상태 문자열을 string hpText = $"HP {currentHP:F0} / {maxHP:F0}"; // 만들거에요 -> 체력 문자열을 UnityEditor.Handles.Label( transform.position + Vector3.up * (detectionRange + 0.4f), $"{gameObject.name}\n{stateLabel}\n{hpText}" ); // 표시할거에요 -> Scene 뷰 레이블을 } #endif } }