using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 using UnityEngine.AI; // 길찾기(내비게이션) 기능을 불러올거에요 -> UnityEngine.AI를 using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Collections를 using System.Collections.Generic; // 리스트 기능을 사용할거에요 -> System.Collections.Generic을 /// /// 돌진 공격 몬스터 /// public class ChargeMonster : MonsterClass // 클래스를 선언할거에요 -> MonsterClass를 상속받는 ChargeMonster를 { [Header("=== 돌진 공격 설정 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 돌진 공격 설정 === 을 [SerializeField] private float chargeSpeed = 15f; // 변수를 선언할거에요 -> 돌진 속도(15.0)를 chargeSpeed에 [SerializeField] private float chargeRange = 10f; // 변수를 선언할거에요 -> 돌진 시작 거리(10.0)를 chargeRange에 [SerializeField] private float chargeDuration = 2f; // 변수를 선언할거에요 -> 돌진 지속 시간(2.0초)을 chargeDuration에 [SerializeField] private float chargeDelay = 3f; // 변수를 선언할거에요 -> 돌진 쿨타임(3.0초)을 chargeDelay에 [SerializeField] private float prepareTime = 0.5f; // 변수를 선언할거에요 -> 돌진 준비 시간(0.5초)을 prepareTime에 // 🎁 [추가] 드랍 아이템 리스트 [Header("=== 드랍 아이템 설정 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 드랍 아이템 설정 === 을 [Tooltip("드랍 가능한 아이템 리스트 (랜덤 1개)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을 [SerializeField] private List dropItemPrefabs; // 리스트를 선언할거에요 -> 드랍 아이템 프리팹 목록을 dropItemPrefabs에 [Tooltip("아이템이 나올 확률 (0 ~ 100%)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을 [Range(0, 100)][SerializeField] private float dropChance = 30f; // 변수를 선언할거에요 -> 드랍 확률(30%)을 dropChance에 private float lastChargeTime; // 변수를 선언할거에요 -> 마지막 돌진 시간을 lastChargeTime에 private bool isCharging = false; // 변수를 선언할거에요 -> 돌진 중 여부를 isCharging에 private bool isPreparing = false; // 변수를 선언할거에요 -> 준비 중 여부를 isPreparing에 private Vector3 chargeDirection; // 변수를 선언할거에요 -> 돌진 방향 벡터를 chargeDirection에 [Header("공격 / 이동 애니메이션")] // 인스펙터 창에 제목을 표시할거에요 -> 공격 / 이동 애니메이션 을 [SerializeField] private string chargeAnimation = "Monster_Charge"; // 변수를 선언할거에요 -> 돌진 애니메이션 이름을 chargeAnimation에 [SerializeField] private string prepareAnimation = "Monster_ChargePrepare"; // 변수를 선언할거에요 -> 준비 애니메이션 이름을 prepareAnimation에 [SerializeField] private string Monster_Walk = "Monster_Walk"; // 변수를 선언할거에요 -> 걷기 애니메이션 이름을 Monster_Walk에 [Header("AI 상세 설정")] // 인스펙터 창에 제목을 표시할거에요 -> AI 상세 설정 을 [SerializeField] private float patrolRadius = 5f; // 변수를 선언할거에요 -> 순찰 반경(5.0)을 patrolRadius에 [SerializeField] private float patrolInterval = 2f; // 변수를 선언할거에요 -> 순찰 간격(2.0초)을 patrolInterval에 private float nextPatrolTime; // 변수를 선언할거에요 -> 다음 순찰 시간을 nextPatrolTime에 private bool isPlayerInZone; // 변수를 선언할거에요 -> 플레이어 감지 여부를 isPlayerInZone에 private Rigidbody _rigidbody; // 변수를 선언할거에요 -> 물리 컴포넌트를 담을 _rigidbody를 protected override void Awake() // 함수를 덮어씌워 실행할거에요 -> Awake 초기화 로직을 { base.Awake(); // 부모의 함수를 실행할거에요 -> MonsterClass의 Awake를 _rigidbody = GetComponent(); // 컴포넌트를 가져올거에요 -> Rigidbody를 if (_rigidbody == null) _rigidbody = gameObject.AddComponent(); // 조건이 맞으면 실행할거에요 -> 없으면 새로 추가해서 _rigidbody에 _rigidbody.isKinematic = true; // 설정을 바꿀거에요 -> 물리 연산을 꺼두기(Kinematic)로 } protected override void Init() // 함수를 덮어씌워 실행할거에요 -> 초기 설정을 Init에서 { if (agent != null) agent.stoppingDistance = 1f; // 조건이 맞으면 설정할거에요 -> 에이전트 정지 거리를 1.0으로 if (animator != null) animator.applyRootMotion = false; // 조건이 맞으면 설정할거에요 -> 애니메이션 이동을 끄기로 } protected override void ExecuteAILogic() // 함수를 덮어씌워 실행할거에요 -> AI 행동 로직인 ExecuteAILogic을 { if (isHit || isCharging || isPreparing) return; // 조건이 맞으면 중단할거에요 -> 피격, 돌진, 준비 중이라면 float distance = Vector3.Distance(transform.position, playerTransform.position); // 거리를 계산할거에요 -> 플레이어와의 거리를 if (isPlayerInZone || distance <= chargeRange * 1.5f) // 조건이 맞으면 실행할거에요 -> 감지 구역 안이거나 거리가 가까우면 { HandleChargeCombat(distance); // 함수를 실행할거에요 -> 전투 로직인 HandleChargeCombat을 } else // 조건이 틀리면 실행할거에요 -> 멀리 있다면 { Patrol(); // 함수를 실행할거에요 -> 순찰을 도는 Patrol을 } UpdateMovementAnimation(); // 함수를 실행할거에요 -> 이동 애니메이션을 갱신하는 UpdateMovementAnimation을 } void HandleChargeCombat(float distance) // 함수를 선언할거에요 -> 거리 기반 전투 판단 로직 HandleChargeCombat을 { if (distance <= chargeRange && Time.time >= lastChargeTime + chargeDelay) // 조건이 맞으면 실행할거에요 -> 사거리 안이고 쿨타임이 지났다면 { StartCoroutine(PrepareCharge()); // 코루틴을 실행할거에요 -> 돌진 준비인 PrepareCharge를 } else if (!isCharging && !isPreparing) // 조건이 맞으면 실행할거에요 -> 돌진이나 준비 중이 아니라면 (추격) { if (agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> NavMesh 위에 있다면 { if (agent.isStopped) agent.isStopped = false; // 명령을 내릴거에요 -> 이동 정지를 풀라고 agent.SetDestination(playerTransform.position); // 명령을 내릴거에요 -> 목적지를 플레이어 위치로 } } } IEnumerator PrepareCharge() // 코루틴 함수를 선언할거에요 -> 돌진 준비 과정인 PrepareCharge를 { isPreparing = true; // 상태를 바꿀거에요 -> 준비 중 상태를 참(true)으로 if (agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> NavMesh 위에 있다면 { agent.isStopped = true; // 명령을 내릴거에요 -> 이동을 멈추라고 agent.ResetPath(); // 명령을 내릴거에요 -> 경로를 초기화하라고 agent.velocity = Vector3.zero; // 값을 넣을거에요 -> 속도를 0으로 } chargeDirection = (playerTransform.position - transform.position).normalized; // 벡터를 계산할거에요 -> 플레이어를 향하는 방향을 chargeDirection.y = 0; // 값을 바꿀거에요 -> 높이 차이를 무시하게 y를 0으로 if (chargeDirection != Vector3.zero) transform.rotation = Quaternion.LookRotation(chargeDirection); // 회전시킬거에요 -> 돌진 방향을 바라보게 if (!string.IsNullOrEmpty(prepareAnimation)) animator.Play(prepareAnimation, 0, 0f); // 재생할거에요 -> 준비 애니메이션을 Debug.Log("[ChargeMonster] 돌진 준비..."); // 로그를 출력할거에요 -> 준비 메시지를 yield return new WaitForSeconds(prepareTime); // 기다릴거에요 -> 준비 시간만큼 StartCoroutine(Charge()); // 코루틴을 실행할거에요 -> 실제 돌진인 Charge를 } IEnumerator Charge() // 코루틴 함수를 선언할거에요 -> 실제 돌진 동작인 Charge를 { isPreparing = false; // 상태를 바꿀거에요 -> 준비 상태를 거짓(false)으로 isCharging = true; // 상태를 바꿀거에요 -> 돌진 중 상태를 참(true)으로 lastChargeTime = Time.time; // 값을 저장할거에요 -> 현재 시간을 마지막 돌진 시간으로 if (agent != null) agent.enabled = false; // 기능을 끌거에요 -> 길찾기 에이전트를 (물리 이동을 위해) if (_rigidbody != null) _rigidbody.isKinematic = false; // 기능을 켤거에요 -> 물리 연산을 (충돌 감지를 위해) animator.Play(chargeAnimation, 0, 0f); // 재생할거에요 -> 돌진 애니메이션을 Debug.Log("[ChargeMonster] 돌진 시작!"); // 로그를 출력할거에요 -> 돌진 시작 메시지를 float chargeStartTime = Time.time; // 값을 저장할거에요 -> 돌진 시작 시간을 while (Time.time < chargeStartTime + chargeDuration) // 반복할거에요 -> 지속 시간이 끝날 때까지 { if (_rigidbody != null) // 조건이 맞으면 실행할거에요 -> 리지드바디가 있다면 _rigidbody.velocity = chargeDirection * chargeSpeed; // 값을 넣을거에요 -> 속도를 돌진 방향과 속도로 else // 조건이 틀리면 실행할거에요 -> 리지드바디가 없다면 (비상용) transform.position += chargeDirection * chargeSpeed * Time.deltaTime; // 이동시킬거에요 -> 위치를 직접 수정해서 yield return null; // 대기할거에요 -> 다음 프레임까지 } if (_rigidbody != null) // 조건이 맞으면 실행할거에요 -> 리지드바디가 있다면 { _rigidbody.velocity = Vector3.zero; // 값을 넣을거에요 -> 속도를 0으로 _rigidbody.isKinematic = true; // 기능을 끌거에요 -> 물리 연산을 (다시 NavMesh 이동을 위해) } if (agent != null) // 조건이 맞으면 실행할거에요 -> 에이전트가 있다면 { agent.enabled = true; // 기능을 켤거에요 -> 길찾기 에이전트를 agent.ResetPath(); // 초기화할거에요 -> 경로를 agent.velocity = Vector3.zero; // 값을 넣을거에요 -> 내부 속도를 0으로 } isCharging = false; // 상태를 바꿀거에요 -> 돌진 중 상태를 거짓(false)으로 Debug.Log("[ChargeMonster] 돌진 종료, 쿨타임 시작"); // 로그를 출력할거에요 -> 종료 메시지를 } void UpdateMovementAnimation() // 함수를 선언할거에요 -> 애니메이션 갱신 함수 UpdateMovementAnimation을 { if (isCharging || isPreparing || isHit || isResting) return; // 조건이 맞으면 중단할거에요 -> 다른 행동 중이라면 if (agent.enabled && agent.velocity.magnitude > 0.1f) // 조건이 맞으면 실행할거에요 -> 움직이고 있다면 animator.Play(Monster_Walk); // 재생할거에요 -> 걷기 애니메이션을 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)) // 조건이 맞으면 실행할거에요 -> 유효한 NavMesh 위치라면 if (agent.isOnNavMesh) agent.SetDestination(hit.position); // 명령을 내릴거에요 -> 그곳으로 이동하라고 nextPatrolTime = Time.time + patrolInterval; // 값을 갱신할거에요 -> 다음 순찰 시간을 } private void OnCollisionEnter(Collision collision) // 함수를 실행할거에요 -> 물리 충돌이 발생했을 때 OnCollisionEnter를 { if (!isCharging) return; // 조건이 맞으면 중단할거에요 -> 돌진 중이 아니라면 if (collision.gameObject.CompareTag("Player")) // 조건이 맞으면 실행할거에요 -> 충돌한 대상이 플레이어라면 { if (collision.gameObject.TryGetComponent(out var playerHealth)) // 컴포넌트를 가져올거에요 -> 플레이어 체력 스크립트를 { if (!playerHealth.isInvincible) // 조건이 맞으면 실행할거에요 -> 무적 상태가 아니라면 playerHealth.TakeDamage(attackDamage); // 함수를 실행할거에요 -> 데미지를 입히는 TakeDamage를 } } } // ☠️ [추가] 죽을 때 아이템 드랍 protected override void OnDie() // 함수를 덮어씌워 실행할거에요 -> 사망 시 아이템 드랍 로직인 OnDie를 { if (_rigidbody != null) _rigidbody.velocity = Vector3.zero; // 값을 넣을거에요 -> 죽을 때 미끄러지지 않게 속도를 0으로 if (dropItemPrefabs != null && dropItemPrefabs.Count > 0) // 조건이 맞으면 실행할거에요 -> 아이템 리스트가 있다면 { float randomValue = Random.Range(0f, 100f); // 값을 뽑을거에요 -> 0~100 사이 랜덤값을 if (randomValue <= dropChance) // 조건이 맞으면 실행할거에요 -> 확률에 당첨되었다면 { int randomIndex = Random.Range(0, dropItemPrefabs.Count); // 값을 뽑을거에요 -> 랜덤 인덱스를 if (dropItemPrefabs[randomIndex] != null) // 조건이 맞으면 실행할거에요 -> 아이템이 유효하다면 { Instantiate(dropItemPrefabs[randomIndex], transform.position + Vector3.up * 0.5f, Quaternion.identity); // 생성할거에요 -> 아이템을 몬스터 위치에 } } } } private void OnTriggerEnter(Collider other) { if (other.CompareTag("Player")) isPlayerInZone = true; } // 상태를 바꿀거에요 -> 플레이어 감지 시 참(true)으로 private void OnTriggerExit(Collider other) { if (other.CompareTag("Player")) isPlayerInZone = false; } // 상태를 바꿀거에요 -> 플레이어가 나가면 거짓(false)으로 }