167 lines
5.8 KiB
C#
167 lines
5.8 KiB
C#
using UnityEngine;
|
|
using UnityEngine.AI;
|
|
using System.Collections; // ⭐ IEnumerator 사용을 위해 추가
|
|
using System;
|
|
|
|
public class MonsterClass : MonoBehaviour, IDamageable
|
|
{
|
|
[Header("스탯")]
|
|
[SerializeField] protected float maxHP = 100f;
|
|
protected float currentHP;
|
|
public event Action<float, float> OnHealthChanged;
|
|
|
|
[Header("피격 / 사망 애니메이션")]
|
|
[SerializeField] protected string Monster_GetDamage = "Monster_GetDamage";
|
|
[SerializeField] protected string Monster_Die = "Monster_Die";
|
|
|
|
protected Animator animator;
|
|
protected NavMeshAgent agent;
|
|
protected AudioSource audioSource;
|
|
protected bool isHit;
|
|
protected bool isDead;
|
|
protected bool isAttacking;
|
|
|
|
[Header("AI 설정 (공격 후 휴식)")]
|
|
[SerializeField] protected float attackRestDuration = 1.5f; // ⭐ 공격 후 쉬는 시간 (초)
|
|
protected bool isResting; // ⭐ 현재 쉬고 있는 상태인지 확인
|
|
|
|
[Header("경험치")]
|
|
[SerializeField] protected int expReward = 10;
|
|
public static System.Action<int> OnMonsterKilled;
|
|
|
|
[Header("공통 사운드/이펙트")]
|
|
[SerializeField] protected AudioClip hitSound;
|
|
[SerializeField] protected AudioClip deathSound;
|
|
[SerializeField] protected GameObject deathEffectPrefab;
|
|
[SerializeField] protected ParticleSystem hitEffect;
|
|
[SerializeField] protected Transform impactSpawnPoint;
|
|
|
|
[Header("--- 드랍 설정 ---")]
|
|
[SerializeField] private GameObject potionPrefab;
|
|
[SerializeField][Range(0f, 100f)] private float potionDropChance = 30f;
|
|
[Space(10)]
|
|
[SerializeField] private GameObject[] weaponPrefabs;
|
|
[SerializeField][Range(0f, 100f)] private float weaponDropChance = 5f;
|
|
|
|
protected virtual void Start()
|
|
{
|
|
currentHP = maxHP;
|
|
animator = GetComponent<Animator>();
|
|
agent = GetComponent<NavMeshAgent>();
|
|
audioSource = GetComponent<AudioSource>();
|
|
OnHealthChanged?.Invoke(currentHP, maxHP);
|
|
}
|
|
|
|
protected virtual void Update()
|
|
{
|
|
if (isDead) return;
|
|
|
|
// ⭐ [수정] 피격(isHit), 공격(isAttacking), 또는 휴식(isResting) 중이면 이동을 멈춥니다.
|
|
bool shouldStop = isHit || isAttacking || isResting;
|
|
|
|
if (agent != null && agent.isOnNavMesh)
|
|
{
|
|
agent.isStopped = shouldStop;
|
|
if (shouldStop) agent.velocity = Vector3.zero;
|
|
}
|
|
|
|
if (animator != null)
|
|
{
|
|
// 멈췄을 때는 속도를 0으로 전달하여 Idle 애니메이션이 나오게 합니다.
|
|
float currentSpeed = (agent != null && !shouldStop) ? agent.velocity.magnitude : 0f;
|
|
animator.SetFloat("Speed", currentSpeed);
|
|
}
|
|
}
|
|
|
|
public virtual void TakeDamage(float amount) { OnDamaged(amount); }
|
|
|
|
public virtual void OnDamaged(float damage)
|
|
{
|
|
if (isDead) return;
|
|
currentHP -= damage;
|
|
OnHealthChanged?.Invoke(currentHP, maxHP);
|
|
|
|
if (currentHP <= 0) { Die(); return; }
|
|
if (!isHit) StartHit();
|
|
}
|
|
|
|
protected virtual void StartHit()
|
|
{
|
|
isHit = true;
|
|
isAttacking = false;
|
|
isResting = false; // ⭐ 맞으면 쉬는 걸 중단하고 바로 아파해야 합니다.
|
|
StopAllCoroutines(); // 진행 중인 휴식 루틴을 끕니다.
|
|
|
|
if (agent && agent.isOnNavMesh) { agent.isStopped = true; agent.velocity = Vector3.zero; }
|
|
|
|
animator.Play(Monster_GetDamage, 0, 0f);
|
|
if (hitEffect) hitEffect.Play();
|
|
if (hitSound) audioSource.PlayOneShot(hitSound);
|
|
}
|
|
|
|
public virtual void OnAttackStart() { isAttacking = true; isResting = false; }
|
|
|
|
// ⭐ [수정] 공격이 끝나면 바로 움직이는 게 아니라 '휴식' 코루틴을 실행합니다.
|
|
public virtual void OnAttackEnd()
|
|
{
|
|
isAttacking = false;
|
|
if (!isDead && !isHit)
|
|
{
|
|
StartCoroutine(RestAfterAttack());
|
|
}
|
|
}
|
|
|
|
// ⭐ 공격 후 대기 코루틴
|
|
private IEnumerator RestAfterAttack()
|
|
{
|
|
isResting = true;
|
|
yield return new WaitForSeconds(attackRestDuration); // 설정한 시간만큼 대기
|
|
isResting = false;
|
|
}
|
|
|
|
public virtual void OnHitEnd() { isHit = false; if (agent && agent.isOnNavMesh) agent.isStopped = false; }
|
|
|
|
protected virtual void Die()
|
|
{
|
|
if (isDead) return;
|
|
isDead = true;
|
|
|
|
Collider col = GetComponent<Collider>();
|
|
if (col != null) col.enabled = false;
|
|
|
|
OnMonsterKilled?.Invoke(expReward);
|
|
TryDropItems();
|
|
|
|
if (agent && agent.isOnNavMesh) { agent.isStopped = true; agent.velocity = Vector3.zero; }
|
|
animator.Play(Monster_Die, 0, 0f);
|
|
if (deathSound) audioSource.PlayOneShot(deathSound);
|
|
|
|
Destroy(gameObject, 1.5f);
|
|
}
|
|
|
|
private void TryDropItems()
|
|
{
|
|
if (potionPrefab != null && UnityEngine.Random.Range(0f, 100f) <= potionDropChance) SpawnItem(potionPrefab);
|
|
if (weaponPrefabs != null && weaponPrefabs.Length > 0 && UnityEngine.Random.Range(0f, 100f) <= weaponDropChance)
|
|
{
|
|
int randomIndex = UnityEngine.Random.Range(0, weaponPrefabs.Length);
|
|
SpawnItem(weaponPrefabs[randomIndex]);
|
|
}
|
|
}
|
|
|
|
private void SpawnItem(GameObject prefab)
|
|
{
|
|
Vector3 spawnPos = transform.position + Vector3.up * 0.5f;
|
|
GameObject item = Instantiate(prefab, spawnPos, Quaternion.identity);
|
|
item.transform.localScale = prefab.transform.localScale;
|
|
|
|
if (item.TryGetComponent<Rigidbody>(out var rb))
|
|
{
|
|
Vector3 popDir = Vector3.up * 0.02f + UnityEngine.Random.insideUnitSphere * 0.02f;
|
|
rb.AddForce(popDir, ForceMode.Impulse);
|
|
if (prefab != potionPrefab) rb.AddTorque(UnityEngine.Random.insideUnitSphere * 0.3f, ForceMode.Impulse);
|
|
}
|
|
}
|
|
|
|
public void OnDeathAnimEnd() { }
|
|
} |