using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 using UnityEngine.AI; // 길찾기(내비게이션) 기능을 불러올거에요 -> UnityEngine.AI를 using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Collections를 using System.Collections.Generic; // 리스트 기능을 사용할거에요 -> System.Collections.Generic을 public class MeleeMonster : MonsterClass // 클래스를 선언할거에요 -> MonsterClass를 상속받는 MeleeMonster를 { [Header("=== 근접 공격 설정 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 근접 공격 설정 === 을 [SerializeField] private float attackRange = 2f; // 변수를 선언할거에요 -> 공격 사거리(2.0)를 attackRange에 [SerializeField] private float attackDelay = 1.5f; // 변수를 선언할거에요 -> 공격 딜레이(1.5초)를 attackDelay에 [Header("=== 드랍 아이템 설정 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 드랍 아이템 설정 === 을 [Tooltip("드랍 가능한 아이템들을 여기에 여러 개 등록하세요. (랜덤 1개 드랍)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을 [SerializeField] private List dropItemPrefabs; // 리스트를 선언할거에요 -> 드랍할 아이템 프리팹 목록을 dropItemPrefabs에 [Tooltip("아이템이 나올 확률 (0 ~ 100%)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을 [Range(0, 100)][SerializeField] private float dropChance = 30f; // 변수를 선언할거에요 -> 드랍 확률(30%)을 dropChance에 private float lastAttackTime; // 변수를 선언할거에요 -> 마지막 공격 시간을 lastAttackTime에 [Header("공격 / 이동 애니메이션")] // 인스펙터 창에 제목을 표시할거에요 -> 공격 / 이동 애니메이션 을 [SerializeField] private string[] attackAnimations = { "Monster_Attack_1" }; // 배열을 선언할거에요 -> 공격 애니메이션 이름들을 attackAnimations에 [SerializeField] private string Monster_Walk = "Monster_Walk"; // 변수를 선언할거에요 -> 걷기 애니메이션 이름을 Monster_Walk에 [Header("AI 상세 설정")] // 인스펙터 창에 제목을 표시할거에요 -> AI 상세 설정 을 [SerializeField] private float stopBuffer = 0.3f; // 변수를 선언할거에요 -> 정지 여유 거리(0.3)를 stopBuffer에 [SerializeField] private float patrolRadius = 5f; // 변수를 선언할거에요 -> 순찰 반경(5.0)을 patrolRadius에 [SerializeField] private float patrolInterval = 2f; // 변수를 선언할거에요 -> 순찰 대기 시간(2.0)을 patrolInterval에 private float nextPatrolTime; // 변수를 선언할거에요 -> 다음 순찰 시간을 nextPatrolTime에 private float repathInterval = 0.3f; // 변수를 선언할거에요 -> 경로 갱신 주기(0.3초)를 repathInterval에 private float nextRepathTime; // 변수를 선언할거에요 -> 다음 경로 갱신 시간을 nextRepathTime에 private int attackIndex; // 변수를 선언할거에요 -> 현재 공격 애니메이션 인덱스를 attackIndex에 private bool isPlayerInZone; // 변수를 선언할거에요 -> 플레이어가 감지 구역에 있는지 여부를 isPlayerInZone에 protected override void Init() // 함수를 덮어씌워 실행할거에요 -> 초기화 로직인 Init을 { if (agent != null) agent.stoppingDistance = attackRange - 0.4f; // 조건이 맞으면 설정할거에요 -> 에이전트 정지 거리를 사거리보다 약간 짧게 if (animator != null) animator.applyRootMotion = false; // 조건이 맞으면 설정할거에요 -> 애니메이션 이동(Root Motion)을 끄기로 } protected override void OnResetStats() // 함수를 덮어씌워 실행할거에요 -> 스탯 초기화 로직인 OnResetStats를 { if (myWeapon != null) // 조건이 맞으면 실행할거에요 -> 무기가 있다면 { myWeapon.SetDamage(attackDamage); // 함수를 실행할거에요 -> 무기 공격력을 몬스터 공격력으로 설정을 } } protected override void ExecuteAILogic() // 함수를 덮어씌워 실행할거에요 -> AI 행동 로직인 ExecuteAILogic을 { if (isHit || isAttacking || isResting) return; // 조건이 맞으면 중단할거에요 -> 피격, 공격, 휴식 중이라면 float distance = Vector3.Distance(transform.position, playerTransform.position); // 거리를 계산할거에요 -> 나와 플레이어 사이의 거리를 distance에 if (isPlayerInZone || distance <= attackRange * 2f) // 조건이 맞으면 실행할거에요 -> 감지 구역 안이거나 거리가 가까우면 HandlePlayerTarget(); // 함수를 실행할거에요 -> 추격 및 공격 처리를 하는 HandlePlayerTarget을 else // 조건이 틀리면 실행할거에요 -> 멀리 있다면 Patrol(); // 함수를 실행할거에요 -> 주변을 배회하는 Patrol을 UpdateMovementAnimation(); // 함수를 실행할거에요 -> 애니메이션 상태를 갱신하는 UpdateMovementAnimation을 } void HandlePlayerTarget() // 함수를 선언할거에요 -> 타겟 처리 로직인 HandlePlayerTarget을 { float distance = Vector3.Distance(transform.position, playerTransform.position); // 거리를 계산할거에요 -> 플레이어와의 거리를 if (distance <= attackRange - stopBuffer) // 조건이 맞으면 실행할거에요 -> 사거리 안쪽으로 충분히 들어왔다면 TryAttack(); // 함수를 실행할거에요 -> 공격을 시도하는 TryAttack을 else if (Time.time >= nextRepathTime) // 조건이 맞으면 실행할거에요 -> 경로 갱신 시간이 되었다면 { if (agent.isOnNavMesh) agent.SetDestination(playerTransform.position); // 명령을 내릴거에요 -> 목적지를 플레이어 위치로 nextRepathTime = Time.time + repathInterval; // 값을 갱신할거에요 -> 다음 경로 갱신 시간을 } } void TryAttack() // 함수를 선언할거에요 -> 공격을 수행하는 TryAttack을 { if (Time.time < lastAttackTime + attackDelay) return; // 조건이 맞으면 중단할거에요 -> 아직 쿨타임이 안 지났다면 lastAttackTime = Time.time; // 값을 저장할거에요 -> 현재 시간을 마지막 공격 시간으로 string attackName = attackAnimations[attackIndex]; // 값을 가져올거에요 -> 현재 인덱스의 공격 애니메이션 이름을 attackIndex = (attackIndex + 1) % attackAnimations.Length; // 값을 갱신할거에요 -> 다음 공격 인덱스로 (순환) isAttacking = true; // 상태를 바꿀거에요 -> 공격 중 상태를 참(true)으로 if (agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> 에이전트가 바닥에 있다면 { agent.isStopped = true; // 명령을 내릴거에요 -> 이동을 멈추라고 agent.velocity = Vector3.zero; // 값을 넣을거에요 -> 속도를 0으로 } animator.Play(attackName, 0, 0f); // 재생할거에요 -> 공격 애니메이션을 처음부터 } void UpdateMovementAnimation() // 함수를 선언할거에요 -> 이동 애니메이션을 제어하는 UpdateMovementAnimation을 { if (isAttacking || isHit || isResting) return; // 조건이 맞으면 중단할거에요 -> 다른 행동 중이라면 if (agent.velocity.magnitude < 0.1f) // 조건이 맞으면 실행할거에요 -> 거의 멈춰 있다면 animator.Play(Monster_Idle); // 재생할거에요 -> 대기 애니메이션을 else // 조건이 틀리면 실행할거에요 -> 움직이고 있다면 animator.Play(Monster_Walk); // 재생할거에요 -> 걷기 애니메이션을 } 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; // 값을 갱신할거에요 -> 다음 순찰 시간을 } public override void OnAttackStart() // 함수를 덮어씌워 실행할거에요 -> 공격 시작 시 호출되는 OnAttackStart를 { isAttacking = true; // 상태를 바꿀거에요 -> 공격 중 상태를 참(true)으로 isResting = false; // 상태를 바꿀거에요 -> 휴식 중 상태를 거짓(false)으로 if (myWeapon != null) myWeapon.EnableHitBox(); // 조건이 맞으면 실행할거에요 -> 무기 공격 판정을 켜기를 } public override void OnAttackEnd() // 함수를 덮어씌워 실행할거에요 -> 공격 종료 시 호출되는 OnAttackEnd를 { if (myWeapon != null) myWeapon.DisableHitBox(); // 조건이 맞으면 실행할거에요 -> 무기 공격 판정을 끄기를 isAttacking = false; // 상태를 바꿀거에요 -> 공격 중 상태를 거짓(false)으로 if (!isDead && !isHit) StartCoroutine(RestAfterAttack()); // 조건이 맞으면 실행할거에요 -> 살아있고 안 맞았다면 휴식 코루틴을 } protected override IEnumerator RestAfterAttack() // 코루틴 함수를 덮어씌워 실행할거에요 -> 공격 후 휴식하는 RestAfterAttack을 { isResting = true; // 상태를 바꿀거에요 -> 휴식 중 상태를 참(true)으로 yield return new WaitForSeconds(attackRestDuration); // 기다릴거에요 -> 휴식 시간만큼 isResting = false; // 상태를 바꿀거에요 -> 휴식 중 상태를 거짓(false)으로 } protected override void OnStartHit() // 함수를 덮어씌워 실행할거에요 -> 피격 시작 시 호출되는 OnStartHit을 { if (myWeapon != null) myWeapon.DisableHitBox(); // 조건이 맞으면 실행할거에요 -> 피격 당했으니 공격 판정을 끄기를 } // 🎲 [핵심 수정] 리스트에서 랜덤 뽑기 + 확률 체크 protected override void OnDie() // 함수를 덮어씌워 실행할거에요 -> 사망 시 호출되는 OnDie를 { if (myWeapon != null) myWeapon.DisableHitBox(); // 조건이 맞으면 실행할거에요 -> 공격 판정을 끄기를 // 1. 리스트에 아이템이 하나라도 있는지 확인 if (dropItemPrefabs != null && dropItemPrefabs.Count > 0) // 조건이 맞으면 실행할거에요 -> 드랍 테이블이 비어있지 않다면 { // 2. 확률 체크 (0 ~ 100) float randomValue = Random.Range(0f, 100f); // 값을 뽑을거에요 -> 0부터 100 사이의 랜덤값을 if (randomValue <= dropChance) // 당첨! // 조건이 맞으면 실행할거에요 -> 랜덤값이 드랍 확률 이하라면 { // 3. 리스트에서 랜덤하게 하나 뽑기 (0번 ~ 마지막 번호 중 하나) int randomIndex = Random.Range(0, dropItemPrefabs.Count); // 값을 뽑을거에요 -> 아이템 리스트 인덱스를 랜덤으로 GameObject selectedItem = dropItemPrefabs[randomIndex]; // 오브젝트를 가져올거에요 -> 선택된 아이템 프리팹을 if (selectedItem != null) // 조건이 맞으면 실행할거에요 -> 아이템이 유효하다면 { Instantiate(selectedItem, transform.position + Vector3.up * 0.5f, Quaternion.identity); // 생성할거에요 -> 아이템을 몬스터 위치에 } } } } private void OnTriggerEnter(Collider other) // 함수를 실행할거에요 -> 트리거에 들어왔을 때 OnTriggerEnter를 { if (other.CompareTag("Player")) isPlayerInZone = true; // 조건이 맞으면 실행할거에요 -> 플레이어라면 감지 구역 진입 상태를 참(true)으로 } private void OnTriggerExit(Collider other) // 함수를 실행할거에요 -> 트리거에서 나갔을 때 OnTriggerExit을 { if (other.CompareTag("Player")) isPlayerInZone = false; // 조건이 맞으면 실행할거에요 -> 플레이어라면 감지 구역 진입 상태를 거짓(false)으로 } }