using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 using UnityEngine.AI; // 길찾기(내비게이션) 기능을 불러올거에요 -> UnityEngine.AI를 using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Collections를 /// /// 자폭 몬스터 (Kamikaze) /// - 플레이어에게 전력 질주 /// - 범위 내 진입 시 제자리에서 카운트다운 후 폭발 /// public class ExplodeMonster : MonsterClass // 클래스를 선언할거에요 -> MonsterClass를 상속받는 ExplodeMonster를 { [Header("=== 자폭 설정 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 자폭 설정 === 을 [SerializeField] private float explodeRange = 4f; // 변수를 선언할거에요 -> 폭발 데미지 반경(4.0)을 explodeRange에 [SerializeField] private float triggerRange = 2.5f; // 변수를 선언할거에요 -> 자폭 시퀀스 시작 거리(2.5)를 triggerRange에 [SerializeField] private float fuseTime = 1.5f; // 변수를 선언할거에요 -> 폭발 지연 시간(1.5초)을 fuseTime에 [SerializeField] private float explosionDamage = 50f; // 변수를 선언할거에요 -> 폭발 데미지(50.0)를 explosionDamage에 [Header("폭발 효과")] // 인스펙터 창에 제목을 표시할거에요 -> 폭발 효과 를 [SerializeField] private GameObject explosionEffectPrefab; // 변수를 선언할거에요 -> 폭발 이펙트 프리팹을 explosionEffectPrefab에 [SerializeField] private ParticleSystem fuseEffect; // 변수를 선언할거에요 -> 도화선(준비) 이펙트를 fuseEffect에 [SerializeField] private AudioClip fuseSound; // 변수를 선언할거에요 -> 도화선 소리를 fuseSound에 [SerializeField] private AudioClip explosionSound; // 변수를 선언할거에요 -> 폭발 소리를 explosionSound에 [Header("애니메이션")] // 인스펙터 창에 제목을 표시할거에요 -> 애니메이션 을 [SerializeField] private string runAnimation = "Monster_Run"; // 변수를 선언할거에요 -> 달리기 애니메이션 이름을 runAnimation에 [SerializeField] private string fuseAnimation = "Monster_Fuse"; // 변수를 선언할거에요 -> 자폭 준비 애니메이션 이름을 fuseAnimation에 [Header("AI 설정")] // 인스펙터 창에 제목을 표시할거에요 -> AI 설정 을 [SerializeField] private float chaseSpeed = 6f; // 변수를 선언할거에요 -> 추격 속도(6.0)를 chaseSpeed에 [SerializeField] private float patrolRadius = 5f; // 변수를 선언할거에요 -> 순찰 반경(5.0)을 patrolRadius에 [SerializeField] private float patrolInterval = 2f; // 변수를 선언할거에요 -> 순찰 간격(2.0초)을 patrolInterval에 private bool isExploding = false; // 변수를 초기화할거에요 -> 폭발 진행 중 여부를 거짓(false)으로 private bool hasExploded = false; // 변수를 초기화할거에요 -> 이미 터졌는지 여부를 거짓(false)으로 private float nextPatrolTime; // 변수를 선언할거에요 -> 다음 순찰 시간을 nextPatrolTime에 private bool isPlayerInZone; // 변수를 선언할거에요 -> 플레이어 감지 여부를 isPlayerInZone에 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // 초기화 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ protected override void Init() // 함수를 덮어씌워 실행할거에요 -> 초기화 로직인 Init을 { if (agent != null) { agent.speed = chaseSpeed; // 값을 설정할거에요 -> 이동 속도를 추격 속도로 agent.stoppingDistance = 0.5f; // 값을 설정할거에요 -> 정지 거리를 아주 짧게(0.5) } if (animator != null) animator.applyRootMotion = false; // 설정을 바꿀거에요 -> 애니메이션 이동을 끄기로 if (fuseEffect != null) fuseEffect.Stop(); // 기능을 실행할거에요 -> 도화선 이펙트가 켜져있다면 끄기를 } protected override void OnResetStats() // 함수를 덮어씌워 실행할거에요 -> 스탯 초기화 로직인 OnResetStats를 { isExploding = false; // 상태를 바꿀거에요 -> 폭발 진행 상태를 거짓(false)으로 hasExploded = false; // 상태를 바꿀거에요 -> 폭발 완료 상태를 거짓(false)으로 if (fuseEffect != null) fuseEffect.Stop(); // 기능을 실행할거에요 -> 도화선 이펙트를 끄기를 } // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // AI 로직 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ protected override void ExecuteAILogic() // 함수를 덮어씌워 실행할거에요 -> AI 행동 로직인 ExecuteAILogic을 { if (isHit || isExploding || hasExploded) return; // 조건이 맞으면 중단할거에요 -> 피격, 자폭 중, 이미 폭발 상태라면 float distance = Vector3.Distance(transform.position, playerTransform.position); // 거리를 계산할거에요 -> 플레이어와의 거리를 // 플레이어가 감지 범위(15m) 안에 있거나 트리거에 닿았으면 추격 if (isPlayerInZone || distance <= 15f) // 조건이 맞으면 실행할거에요 -> 추격 조건이 만족되면 { ChasePlayer(distance); // 함수를 실행할거에요 -> 추격 및 자폭 처리를 하는 ChasePlayer를 } else // 조건이 틀리면 실행할거에요 -> 멀리 있다면 { Patrol(); // 함수를 실행할거에요 -> 순찰을 도는 Patrol을 UpdateMovementAnimation(); // 함수를 실행할거에요 -> 애니메이션을 갱신하는 UpdateMovementAnimation을 } } void ChasePlayer(float distance) // 함수를 선언할거에요 -> 거리별 추격 행동을 정하는 ChasePlayer를 { // 1. 폭발 시작 거리 안으로 들어왔다? -> 점화! if (distance <= triggerRange) // 조건이 맞으면 실행할거에요 -> 자폭 거리 이내라면 { StartCoroutine(ExplodeRoutine()); // 코루틴을 실행할거에요 -> 자폭 시퀀스인 ExplodeRoutine을 return; // 중단할거에요 -> 더 이상 이동하지 않도록 } // 2. 아직 멀었다? -> 전력 질주 if (agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> NavMesh 위에 있다면 { agent.isStopped = false; // 명령을 내릴거에요 -> 이동 정지를 풀라고 agent.SetDestination(playerTransform.position); // 명령을 내릴거에요 -> 목적지를 플레이어 위치로 // 달리기 애니메이션 animator.Play(runAnimation); // 재생할거에요 -> 달리기 애니메이션을 } } void UpdateMovementAnimation() // 함수를 선언할거에요 -> 애니메이션 갱신 함수 UpdateMovementAnimation을 { if (isExploding || isHit) return; // 조건이 맞으면 중단할거에요 -> 폭발 중이거나 맞고 있다면 if (agent.velocity.magnitude > 0.1f) // 조건이 맞으면 실행할거에요 -> 움직이고 있다면 animator.Play(runAnimation); // 재생할거에요 -> 달리기 애니메이션을 else // 조건이 틀리면 실행할거에요 -> 멈춰 있다면 animator.Play(Monster_Idle); // 재생할거에요 -> 대기 애니메이션을 } void Patrol() // 함수를 선언할거에요 -> 순찰 로직 Patrol을 { if (Time.time < nextPatrolTime) return; // 조건이 맞으면 중단할거에요 -> 대기 시간이라면 Vector3 randomPoint = transform.position + new Vector3( // 벡터를 계산할거에요 -> 랜덤 순찰 지점을 Random.Range(-patrolRadius, patrolRadius), 0, Random.Range(-patrolRadius, patrolRadius) ); if (NavMesh.SamplePosition(randomPoint, out NavMeshHit hit, 3f, NavMesh.AllAreas)) // 조건이 맞으면 실행할거에요 -> 유효한 위치라면 if (agent.isOnNavMesh) agent.SetDestination(hit.position); // 명령을 내릴거에요 -> 이동하라고 nextPatrolTime = Time.time + patrolInterval; // 값을 갱신할거에요 -> 다음 순찰 시간을 } // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // 💣 폭발 시퀀스 (점화 -> 대기 -> 쾅!) // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ IEnumerator ExplodeRoutine() // 코루틴 함수를 선언할거에요 -> 자폭 진행 과정인 ExplodeRoutine을 { if (hasExploded) yield break; // 조건이 맞으면 종료할거에요 -> 이미 터졌다면 isExploding = true; // 상태를 바꿀거에요 -> 폭발 진행 상태를 참(true)으로 hasExploded = true; // 상태를 바꿀거에요 -> 폭발 완료 상태를 참(true)으로 (중복 방지) isAttacking = true; // 상태를 바꿀거에요 -> 공격 중 상태를 참(true)으로 (부모 간섭 방지) // ⭐ [핵심] 급브레이크! (문워크 방지) if (agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> NavMesh 위에 있다면 { agent.isStopped = true; // 명령을 내릴거에요 -> 멈추라고 agent.ResetPath(); // 명령을 내릴거에요 -> 경로를 지우라고 agent.velocity = Vector3.zero; // 값을 넣을거에요 -> 속도를 0으로 } Debug.Log($"💣 자폭 카운트다운! ({fuseTime}초 뒤 폭발)"); // 로그를 출력할거에요 -> 자폭 예고 메시지를 // 1. 부들부들 떨기 (폭발 준비 모션) if (!string.IsNullOrEmpty(fuseAnimation)) animator.Play(fuseAnimation); // 재생할거에요 -> 준비 애니메이션을 // 2. 치익~ 소리 & 이펙트 if (fuseEffect != null) fuseEffect.Play(); // 실행할거에요 -> 도화선 이펙트를 if (fuseSound != null && audioSource != null) audioSource.PlayOneShot(fuseSound); // 재생할거에요 -> 도화선 소리를 // 3. 도망갈 시간 주기 yield return new WaitForSeconds(fuseTime); // 기다릴거에요 -> 폭발 지연 시간만큼 // 4. 쾅! PerformExplosion(); // 함수를 실행할거에요 -> 실제 폭발 처리를 } void PerformExplosion() // 함수를 선언할거에요 -> 폭발 데미지와 이펙트를 처리하는 PerformExplosion을 { Debug.Log("💥💥💥 쾅!!!"); // 로그를 출력할거에요 -> 폭발 메시지를 // 폭발 이펙트 생성 (Particle System) if (explosionEffectPrefab != null) // 조건이 맞으면 실행할거에요 -> 폭발 프리팹이 있다면 { Instantiate(explosionEffectPrefab, transform.position, Quaternion.identity); // 생성할거에요 -> 폭발 이펙트를 현재 위치에 } // 폭발음 if (explosionSound != null) // 조건이 맞으면 실행할거에요 -> 폭발음이 있다면 { AudioSource.PlayClipAtPoint(explosionSound, transform.position, 1f); // 재생할거에요 -> 폭발 소리를 해당 위치에서 } // 주변 데미지 처리 DamageNearbyTargets(); // 함수를 실행할거에요 -> 주변 대상에게 데미지를 주는 DamageNearbyTargets를 // 몬스터 사망 처리 (MonsterClass 기능 사용) Die(); // 함수를 실행할거에요 -> 몬스터를 죽게 만드는 Die를 } void DamageNearbyTargets() // 함수를 선언할거에요 -> 폭발 범위 내 데미지를 입히는 DamageNearbyTargets를 { // 폭발 범위(Sphere) 안에 있는 모든 물체 검사 Collider[] hitColliders = Physics.OverlapSphere(transform.position, explodeRange); // 배열에 담을거에요 -> 폭발 범위 내의 충돌체들을 foreach (Collider hit in hitColliders) // 반복할거에요 -> 감지된 모든 충돌체에 대해 { // 플레이어 데미지 if (hit.CompareTag("Player")) // 조건이 맞으면 실행할거에요 -> 대상이 플레이어라면 { if (hit.TryGetComponent(out var playerHealth)) // 컴포넌트를 가져올거에요 -> 플레이어 체력 스크립트를 { if (!playerHealth.isInvincible) // 조건이 맞으면 실행할거에요 -> 무적 상태가 아니라면 { playerHealth.TakeDamage(explosionDamage); // 함수를 실행할거에요 -> 폭발 데미지를 입히는 TakeDamage를 Debug.Log($"💥 플레이어 폭사! 데미지: {explosionDamage}"); // 로그를 출력할거에요 -> 피격 메시지를 } } } // (선택사항) 주변 몬스터도 팀킬? else if (hit.CompareTag("Enemy") && hit.gameObject != gameObject) // 조건이 맞으면 실행할거에요 -> 대상이 다른 몬스터라면 { // 팀킬 로직이 필요하면 여기에 추가 } } } // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // 유틸리티 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // 자폭병은 때려도 폭발 안 멈춤 (취향에 따라 수정 가능) protected override void OnStartHit() { } // 함수를 비워둘거에요 -> 피격 시 아무 행동도 안 하게 (폭발 캔슬 방지) // 죽을 때 퓨즈 끄기 protected override void OnDie() // 함수를 덮어씌워 실행할거에요 -> 사망 시 정리를 위해 { if (fuseEffect != null && fuseEffect.isPlaying) fuseEffect.Stop(); // 기능을 실행할거에요 -> 도화선 이펙트가 켜져있다면 끄기를 } private void OnTriggerEnter(Collider other) { if (other.CompareTag("Player")) isPlayerInZone = true; } // 상태를 바꿀거에요 -> 플레이어 진입 시 감지 상태를 참으로 private void OnTriggerExit(Collider other) { if (other.CompareTag("Player")) isPlayerInZone = false; } // 상태를 바꿀거에요 -> 플레이어 이탈 시 감지 상태를 거짓으로 // 에디터에서 범위 보여주기 private void OnDrawGizmosSelected() // 함수를 실행할거에요 -> 에디터에서 범위를 그리는 OnDrawGizmosSelected를 { Gizmos.color = Color.red; // 색상을 설정할거에요 -> 감지 범위 색을 빨간색으로 Gizmos.DrawWireSphere(transform.position, triggerRange); // 그림을 그릴거에요 -> 자폭 감지 범위를 Gizmos.color = new Color(1f, 0.5f, 0f, 0.3f); // 색상을 설정할거에요 -> 폭발 범위 색을 주황색 반투명으로 Gizmos.DrawSphere(transform.position, explodeRange); // 그림을 그릴거에요 -> 폭발 데미지 범위를 } }