182 lines
13 KiB
C#
182 lines
13 KiB
C#
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<GameObject> 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)으로
|
|
}
|
|
} |