Projext/Assets/Scripts/Enemy/AI/MonsterClass.cs

366 lines
12 KiB
C#
Raw Normal View History

2026-02-02 08:30:23 +00:00
using UnityEngine;
using UnityEngine.AI;
using System.Collections;
using System;
2026-02-09 14:49:44 +00:00
2026-02-04 14:06:25 +00:00
/// <summary>
/// 몬스터 기본 클래스 (공통 기능만)
/// - 공격 방식은 자식 클래스에서 구현
/// </summary>
2026-02-02 08:30:23 +00:00
public abstract class MonsterClass : MonoBehaviour, IDamageable
{
[Header("--- 최적화 설정 ---")]
protected Renderer mobRenderer;
protected Transform playerTransform;
[SerializeField] protected float optimizationDistance = 40f;
2026-02-03 14:41:49 +00:00
[Header("몬스터 기본 스탯")]
2026-02-02 08:30:23 +00:00
[SerializeField] protected float maxHP = 100f;
2026-02-04 14:06:25 +00:00
[SerializeField] protected float attackDamage = 10f;
2026-02-03 14:41:49 +00:00
[SerializeField] protected int expReward = 10;
[SerializeField] protected float moveSpeed = 3.5f;
2026-02-02 15:02:12 +00:00
2026-02-02 08:30:23 +00:00
protected float currentHP;
public event Action<float, float> OnHealthChanged;
2026-02-04 14:06:25 +00:00
[Header("전투 / 무기 (선택사항)")]
// ⭐ 근접 몬스터가 사용할 무기 (원거리 몬스터는 비워둬도 됨)
[SerializeField] protected MonsterWeapon myWeapon;
2026-02-02 15:02:12 +00:00
[Header("피격 / 사망 / 대기 애니메이션")]
2026-02-02 08:30:23 +00:00
[SerializeField] protected string Monster_Idle = "Monster_Idle";
[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, isDead, isAttacking;
2026-02-03 14:41:49 +00:00
public bool IsAggroed { get; protected set; }
2026-02-04 14:06:25 +00:00
[Header("AI 설정")]
2026-02-02 08:30:23 +00:00
[SerializeField] protected float attackRestDuration = 1.5f;
protected bool isResting;
public static System.Action<int> OnMonsterKilled;
[Header("공통 사운드/이펙트")]
[SerializeField] protected AudioClip hitSound, deathSound;
[SerializeField] protected GameObject deathEffectPrefab;
[SerializeField] protected ParticleSystem hitEffect;
[SerializeField] protected Transform impactSpawnPoint;
2026-02-04 14:06:25 +00:00
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 생명주기
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2026-02-03 14:41:49 +00:00
2026-02-02 08:30:23 +00:00
protected virtual void Awake()
{
mobRenderer = GetComponentInChildren<Renderer>();
animator = GetComponent<Animator>();
agent = GetComponent<NavMeshAgent>();
audioSource = GetComponent<AudioSource>();
2026-02-02 15:02:12 +00:00
if (agent != null) agent.speed = moveSpeed;
2026-02-02 08:30:23 +00:00
}
protected virtual void OnEnable()
{
playerTransform = GameObject.FindGameObjectWithTag("Player")?.transform;
if (mobRenderer != null) mobRenderer.enabled = true;
Init();
if (MobUpdateManager.Instance != null) MobUpdateManager.Instance.RegisterMob(this);
}
2026-02-04 14:06:25 +00:00
protected virtual void OnDisable()
{
if (MobUpdateManager.Instance != null) MobUpdateManager.Instance.UnregisterMob(this);
}
2026-02-02 15:02:12 +00:00
public void ResetStats()
{
isDead = false;
2026-02-03 14:41:49 +00:00
IsAggroed = false;
2026-02-02 15:02:12 +00:00
currentHP = maxHP;
OnHealthChanged?.Invoke(currentHP, maxHP);
2026-02-03 14:41:49 +00:00
2026-02-02 15:02:12 +00:00
Collider col = GetComponent<Collider>();
if (col != null) col.enabled = true;
2026-02-03 14:41:49 +00:00
2026-02-09 14:49:44 +00:00
if (agent != null) agent.speed = moveSpeed; // 스피드 초기화
2026-02-04 14:06:25 +00:00
OnResetStats();
2026-02-02 15:02:12 +00:00
}
2026-02-04 14:06:25 +00:00
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 추상 메서드 (자식이 반드시 구현)
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2026-02-02 08:30:23 +00:00
protected virtual void Init() { }
protected abstract void ExecuteAILogic();
2026-02-04 14:06:25 +00:00
protected virtual void OnResetStats() { }
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 공통 기능
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2026-02-02 08:30:23 +00:00
2026-02-03 14:41:49 +00:00
public virtual void Reactivate()
{
if (agent != null && agent.isOnNavMesh)
{
agent.isStopped = false;
if (IsAggroed && playerTransform != null)
{
agent.SetDestination(playerTransform.position);
}
}
if (mobRenderer != null) mobRenderer.enabled = true;
}
2026-02-02 08:30:23 +00:00
public void OnManagedUpdate()
{
if (isDead || playerTransform == null || !gameObject.activeInHierarchy) return;
2026-02-03 14:41:49 +00:00
2026-02-02 08:30:23 +00:00
float distance = Vector3.Distance(transform.position, playerTransform.position);
2026-02-03 14:41:49 +00:00
if (distance > optimizationDistance && !IsAggroed)
2026-02-02 08:30:23 +00:00
{
2026-02-02 15:02:12 +00:00
StopMovement();
2026-02-02 08:30:23 +00:00
if (mobRenderer != null && mobRenderer.enabled) mobRenderer.enabled = false;
return;
}
2026-02-03 14:41:49 +00:00
2026-02-02 08:30:23 +00:00
if (mobRenderer != null && !mobRenderer.enabled) mobRenderer.enabled = true;
2026-02-02 15:02:12 +00:00
if (mobRenderer != null && !mobRenderer.isVisible) { StopMovement(); return; }
if (agent != null && agent.isOnNavMesh && agent.isStopped) agent.isStopped = false;
2026-02-02 08:30:23 +00:00
ExecuteAILogic();
}
protected void StopMovement()
{
2026-02-04 14:06:25 +00:00
if (agent != null && agent.isOnNavMesh)
{
agent.isStopped = true;
agent.velocity = Vector3.zero;
}
if (animator != null)
{
animator.SetFloat("Speed", 0f);
animator.Play(Monster_Idle);
}
2026-02-02 08:30:23 +00:00
}
2026-02-04 14:06:25 +00:00
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 피격 / 사망
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
public virtual void TakeDamage(float amount)
{
OnDamaged(amount);
}
2026-02-03 14:41:49 +00:00
2026-02-02 08:30:23 +00:00
public virtual void OnDamaged(float damage)
{
if (isDead) return;
2026-02-03 14:41:49 +00:00
IsAggroed = true;
2026-02-02 08:30:23 +00:00
currentHP -= damage;
OnHealthChanged?.Invoke(currentHP, maxHP);
if (currentHP <= 0) { Die(); return; }
if (!isHit) StartHit();
}
protected virtual void StartHit()
{
2026-02-03 14:41:49 +00:00
isHit = true;
isAttacking = false;
isResting = false;
2026-02-02 08:30:23 +00:00
StopAllCoroutines();
2026-02-03 14:41:49 +00:00
2026-02-04 14:06:25 +00:00
OnStartHit();
2026-02-03 14:41:49 +00:00
if (agent && agent.isOnNavMesh)
{
agent.isStopped = true;
agent.velocity = Vector3.zero;
}
2026-02-02 08:30:23 +00:00
animator.Play(Monster_GetDamage, 0, 0f);
if (hitEffect) hitEffect.Play();
if (hitSound) audioSource.PlayOneShot(hitSound);
}
2026-02-04 14:06:25 +00:00
protected virtual void OnStartHit() { }
2026-02-03 14:41:49 +00:00
public virtual void OnHitEnd()
{
isHit = false;
if (agent && agent.isOnNavMesh) agent.isStopped = false;
}
2026-02-02 08:30:23 +00:00
protected virtual void Die()
{
if (isDead) return;
isDead = true;
2026-02-03 14:41:49 +00:00
IsAggroed = false;
2026-02-04 14:06:25 +00:00
OnDie();
2026-02-03 14:41:49 +00:00
OnMonsterKilled?.Invoke(expReward);
2026-02-02 08:30:23 +00:00
Collider col = GetComponent<Collider>();
if (col != null) col.enabled = false;
2026-02-03 14:41:49 +00:00
if (agent && agent.isOnNavMesh)
{
agent.isStopped = true;
agent.velocity = Vector3.zero;
}
2026-02-02 08:30:23 +00:00
animator.Play(Monster_Die, 0, 0f);
if (deathSound) audioSource.PlayOneShot(deathSound);
Invoke("ReturnToPool", 1.5f);
}
2026-02-03 14:41:49 +00:00
2026-02-04 14:06:25 +00:00
protected virtual void OnDie() { }
2026-02-02 08:30:23 +00:00
public bool IsDead => isDead;
protected void ReturnToPool() => gameObject.SetActive(false);
2026-02-04 14:06:25 +00:00
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2026-02-09 14:49:44 +00:00
// ⭐ 공격 이벤트
2026-02-04 14:06:25 +00:00
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
public virtual void OnAttackStart()
{
isAttacking = true;
isResting = false;
if (myWeapon != null)
{
myWeapon.EnableHitBox();
}
}
public virtual void OnAttackEnd()
{
if (myWeapon != null)
{
myWeapon.DisableHitBox();
}
isAttacking = false;
if (!isDead && !isHit) StartCoroutine(RestAfterAttack());
}
protected virtual IEnumerator RestAfterAttack()
{
isResting = true;
yield return new WaitForSeconds(attackRestDuration);
isResting = false;
}
2026-02-09 14:49:44 +00:00
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// ⭐ 상태 이상 시스템 (추가된 부분)
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
/// <summary>
/// 상태이상 적용 (PlayerArrow에서 호출)
/// </summary>
public void ApplyStatusEffect(StatusEffectType type, float damage, float duration)
{
if (isDead) return;
switch (type)
{
case StatusEffectType.Burn:
StartCoroutine(BurnCoroutine(damage, duration));
break;
case StatusEffectType.Slow:
StartCoroutine(SlowCoroutine(damage, duration));
break;
case StatusEffectType.Poison:
StartCoroutine(PoisonCoroutine(damage, duration));
break;
case StatusEffectType.Shock:
TakeDamage(damage); // 충격 데미지 즉시 적용
StartCoroutine(StunCoroutine(0.5f)); // 0.5초 스턴
break;
}
}
/// <summary>
/// 화상: 일정 시간 동안 0.5초마다 틱 데미지
/// </summary>
private IEnumerator BurnCoroutine(float tickDamage, float duration)
{
float elapsed = 0f;
float tickInterval = 0.5f;
while (elapsed < duration)
{
TakeDamage(tickDamage * tickInterval);
yield return new WaitForSeconds(tickInterval);
elapsed += tickInterval;
}
}
/// <summary>
/// 슬로우: 이동속도를 일시적으로 감소
/// </summary>
private IEnumerator SlowCoroutine(float amount, float duration)
{
// NavMeshAgent가 있는 경우 속도 조절
if (agent != null)
{
float originalSpeed = agent.speed;
agent.speed *= (1f - Mathf.Clamp01(amount / 100f));
yield return new WaitForSeconds(duration);
agent.speed = originalSpeed; // 원래 속도로 복구
}
else
{
// NavMeshAgent가 없으면 그냥 대기
yield return new WaitForSeconds(duration);
}
}
/// <summary>
/// 독: 지속 데미지 (1초마다)
/// </summary>
private IEnumerator PoisonCoroutine(float tickDamage, float duration)
{
float elapsed = 0f;
float tickInterval = 1f;
while (elapsed < duration)
{
TakeDamage(tickDamage * tickInterval);
yield return new WaitForSeconds(tickInterval);
elapsed += tickInterval;
}
}
/// <summary>
/// 스턴: 짧은 시간 동안 행동 정지
/// </summary>
private IEnumerator StunCoroutine(float duration)
{
if (agent != null)
{
agent.isStopped = true;
if (animator != null) animator.speed = 0; // 애니메이션도 멈춤 (선택사항)
yield return new WaitForSeconds(duration);
if (!isDead)
{
agent.isStopped = false;
if (animator != null) animator.speed = 1;
}
}
else
{
yield return new WaitForSeconds(duration);
}
}
}