2026-02-02 08:30:23 +00:00
|
|
|
|
using UnityEngine;
|
|
|
|
|
|
using UnityEngine.AI;
|
|
|
|
|
|
using System.Collections;
|
|
|
|
|
|
using System;
|
|
|
|
|
|
|
|
|
|
|
|
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-03 14:41:49 +00:00
|
|
|
|
[SerializeField] protected float attackDamage = 10f; // 몬스터의 힘 (예: 10)
|
|
|
|
|
|
[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-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-02 08:30:23 +00:00
|
|
|
|
[Header("AI 설정 (공격 후 휴식)")]
|
|
|
|
|
|
[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-03 14:41:49 +00:00
|
|
|
|
[Header("=== 전투(무기) 설정 ===")]
|
|
|
|
|
|
[SerializeField] private MonsterWeapon myWeapon;
|
|
|
|
|
|
|
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-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
|
|
|
|
|
|
|
|
|
|
// ⭐ 여기서 몬스터의 힘(attackDamage)을 무기에 보냄
|
|
|
|
|
|
if (myWeapon != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
myWeapon.SetDamage(attackDamage);
|
|
|
|
|
|
}
|
2026-02-02 15:02:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-02 08:30:23 +00:00
|
|
|
|
protected virtual void OnDisable()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (MobUpdateManager.Instance != null) MobUpdateManager.Instance.UnregisterMob(this);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
protected virtual void Init() { }
|
|
|
|
|
|
protected abstract void ExecuteAILogic();
|
|
|
|
|
|
|
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()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (agent != null && agent.isOnNavMesh) { agent.isStopped = true; agent.velocity = Vector3.zero; }
|
2026-02-02 15:02:12 +00:00
|
|
|
|
if (animator != null) { animator.SetFloat("Speed", 0f); animator.Play(Monster_Idle); }
|
2026-02-02 08:30:23 +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
|
|
|
|
|
|
|
|
|
|
if (myWeapon != null) myWeapon.DisableHitBox();
|
|
|
|
|
|
|
|
|
|
|
|
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-03 14:41:49 +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 IEnumerator RestAfterAttack()
|
|
|
|
|
|
{
|
|
|
|
|
|
isResting = true;
|
|
|
|
|
|
yield return new WaitForSeconds(attackRestDuration);
|
|
|
|
|
|
isResting = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
|
|
if (myWeapon != null) myWeapon.DisableHitBox();
|
|
|
|
|
|
|
|
|
|
|
|
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-02 08:30:23 +00:00
|
|
|
|
public bool IsDead => isDead;
|
|
|
|
|
|
protected void ReturnToPool() => gameObject.SetActive(false);
|
|
|
|
|
|
}
|