Projext/Assets/Scripts/Enemy/AI/MonsterClass.cs
2026-02-13 00:23:25 +09:00

359 lines
26 KiB
C#

using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
using UnityEngine.AI; // 길찾기(내비게이션) 기능을 불러올거에요 -> UnityEngine.AI를
using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Collections를
using System; // 기본 시스템 기능(Action 등)을 사용할거에요 -> System을
/// <summary>
/// 몬스터 기본 클래스 (공통 기능만)
/// - 공격 방식은 자식 클래스에서 구현
/// </summary>
public abstract class MonsterClass : MonoBehaviour, IDamageable // 추상 클래스를 선언할거에요 -> MonoBehaviour와 IDamageable을 상속받는 MonsterClass를
{
[Header("--- 최적화 설정 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 최적화 설정 --- 을
protected Renderer mobRenderer; // 변수를 선언할거에요 -> 몬스터의 렌더러(외형) 컴포넌트를 담을 mobRenderer를
protected Transform playerTransform; // 변수를 선언할거에요 -> 플레이어의 위치 정보를 담을 playerTransform을
[SerializeField] protected float optimizationDistance = 40f; // 변수를 선언할거에요 -> 몬스터 최적화(AI 정지) 거리(40.0)를 optimizationDistance에
[Header("몬스터 기본 스탯")] // 인스펙터 창에 제목을 표시할거에요 -> 몬스터 기본 스탯 을
[SerializeField] protected float maxHP = 100f; // 변수를 선언할거에요 -> 최대 체력(100.0)을 maxHP에
[SerializeField] protected float attackDamage = 10f; // 변수를 선언할거에요 -> 공격력(10.0)을 attackDamage에
[SerializeField] protected int expReward = 10; // 변수를 선언할거에요 -> 처치 시 경험치 보상(10)을 expReward에
[SerializeField] protected float moveSpeed = 3.5f; // 변수를 선언할거에요 -> 이동 속도(3.5)를 moveSpeed에
protected float currentHP; // 변수를 선언할거에요 -> 현재 체력을 저장할 currentHP를
public event Action<float, float> OnHealthChanged; // 이벤트를 선언할거에요 -> 체력이 변할 때 알릴 OnHealthChanged를
[Header("전투 / 무기 (선택사항)")] // 인스펙터 창에 제목을 표시할거에요 -> 전투 / 무기 (선택사항) 을
// ⭐ 근접 몬스터가 사용할 무기 (원거리 몬스터는 비워둬도 됨)
[SerializeField] protected MonsterWeapon myWeapon; // 변수를 선언할거에요 -> 몬스터가 장착한 무기 스크립트를 myWeapon에
[Header("피격 / 사망 / 대기 애니메이션")] // 인스펙터 창에 제목을 표시할거에요 -> 피격 / 사망 / 대기 애니메이션 을
[SerializeField] protected string Monster_Idle = "Monster_Idle"; // 변수를 선언할거에요 -> 대기 애니메이션 이름("Monster_Idle")을 Monster_Idle에
[SerializeField] protected string Monster_GetDamage = "Monster_GetDamage"; // 변수를 선언할거에요 -> 피격 애니메이션 이름("Monster_GetDamage")을 Monster_GetDamage에
[SerializeField] protected string Monster_Die = "Monster_Die"; // 변수를 선언할거에요 -> 사망 애니메이션 이름("Monster_Die")을 Monster_Die에
protected Animator animator; // 변수를 선언할거에요 -> 애니메이션 제어 컴포넌트를 담을 animator를
protected NavMeshAgent agent; // 변수를 선언할거에요 -> 길찾기 에이전트 컴포넌트를 담을 agent를
protected AudioSource audioSource; // 변수를 선언할거에요 -> 소리 재생 컴포넌트를 담을 audioSource를
// ⭐ [핵심] 자식 클래스에서 공유할 상태 변수들 (중복 선언 방지)
protected bool isHit;
protected bool isDead;
protected bool isAttacking;
public bool IsAggroed { get; protected set; } // 프로퍼티를 선언할거에요 -> 어그로(전투) 상태 여부를 외부에서 읽기만 가능하게 IsAggroed에
[Header("AI 설정")] // 인스펙터 창에 제목을 표시할거에요 -> AI 설정 을
[SerializeField] protected float attackRestDuration = 1.5f; // 변수를 선언할거에요 -> 공격 후 대기 시간(1.5초)을 attackRestDuration에
protected bool isResting; // 변수를 선언할거에요 -> 휴식 중인지 여부를 저장할 isResting을
public static System.Action<int> OnMonsterKilled; // 정적 이벤트를 선언할거에요 -> 몬스터 처치 시 경험치를 전달할 OnMonsterKilled를
[Header("공통 사운드/이펙트")] // 인스펙터 창에 제목을 표시할거에요 -> 공통 사운드/이펙트 를
[SerializeField] protected AudioClip hitSound, deathSound; // 변수를 선언할거에요 -> 피격음과 사망음 오디오 클립을
[SerializeField] protected GameObject deathEffectPrefab; // 변수를 선언할거에요 -> 사망 시 재생할 이펙트 프리팹을
[SerializeField] protected ParticleSystem hitEffect; // 변수를 선언할거에요 -> 피격 시 재생할 파티클 시스템을
[SerializeField] protected Transform impactSpawnPoint; // 변수를 선언할거에요 -> 이펙트가 생성될 위치를
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 생명주기
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
protected virtual void Awake() // 함수를 실행할거에요 -> 스크립트가 켜질 때 호출되는 Awake를
{
mobRenderer = GetComponentInChildren<Renderer>(); // 컴포넌트를 가져올거에요 -> 자식 오브젝트의 렌더러를 mobRenderer에
animator = GetComponent<Animator>(); // 컴포넌트를 가져올거에요 -> 내 몸의 애니메이터를 animator에
agent = GetComponent<NavMeshAgent>(); // 컴포넌트를 가져올거에요 -> 내 몸의 길찾기 에이전트를 agent에
audioSource = GetComponent<AudioSource>(); // 컴포넌트를 가져올거에요 -> 내 몸의 오디오 소스를 audioSource에
if (agent != null) agent.speed = moveSpeed; // 조건이 맞으면 설정할거에요 -> 에이전트가 있다면 이동 속도를 moveSpeed로
}
protected virtual void OnEnable() // 함수를 실행할거에요 -> 오브젝트가 활성화될 때 호출되는 OnEnable을
{
playerTransform = GameObject.FindGameObjectWithTag("Player")?.transform; // 오브젝트를 찾을거에요 -> "Player" 태그를 가진 오브젝트의 위치를 playerTransform에
if (mobRenderer != null) mobRenderer.enabled = true; // 조건이 맞으면 실행할거에요 -> 렌더러가 있다면 보이게 켜기(true)를
Init(); // 함수를 실행할거에요 -> 자식 클래스에서 정의할 초기화 함수 Init을
if (MobUpdateManager.Instance != null) MobUpdateManager.Instance.RegisterMob(this); // 조건이 맞으면 실행할거에요 -> 매니저가 있다면 나(this)를 관리 목록에 등록하기를
}
protected virtual void OnDisable() // 함수를 실행할거에요 -> 오브젝트가 비활성화될 때 호출되는 OnDisable을
{
if (MobUpdateManager.Instance != null) MobUpdateManager.Instance.UnregisterMob(this); // 조건이 맞으면 실행할거에요 -> 매니저가 있다면 나(this)를 관리 목록에서 제외하기를
}
// ⭐ [수정] Update를 virtual로 선언하여 자식이 재정의 가능하게 함
protected virtual void Update()
{
OnManagedUpdate(); // 기본적으로 매니저 업데이트 로직을 수행 (일반 몬스터용)
}
public void ResetStats() // 함수를 선언할거에요 -> 몬스터 상태를 초기화하는 ResetStats를
{
isDead = false; // 상태를 바꿀거에요 -> 사망 상태를 거짓(false)으로
IsAggroed = false; // 상태를 바꿀거에요 -> 어그로 상태를 거짓(false)으로
currentHP = maxHP; // 값을 넣을거에요 -> 현재 체력을 최대 체력으로
OnHealthChanged?.Invoke(currentHP, maxHP); // 이벤트를 알릴거에요 -> 체력이 변경되었음을 UI 등에
Collider col = GetComponent<Collider>(); // 컴포넌트를 가져올거에요 -> 내 몸의 콜라이더를 col에
if (col != null) col.enabled = true; // 조건이 맞으면 실행할거에요 -> 콜라이더가 있다면 다시 켜기(true)를
if (agent != null) agent.speed = moveSpeed; // 조건이 맞으면 실행할거에요 -> 에이전트가 있다면 속도를 초기화하기를
OnResetStats(); // 함수를 실행할거에요 -> 자식 클래스의 추가 초기화 함수 OnResetStats를
}
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 추상 메서드 (자식이 반드시 구현)
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
protected virtual void Init() { } // 가상 함수를 선언할거에요 -> 자식에서 덮어쓸 초기화 함수 Init을
protected abstract void ExecuteAILogic(); // 추상 함수를 선언할거에요 -> 자식에서 반드시 구현해야 할 AI 로직 ExecuteAILogic을
protected virtual void OnResetStats() { } // 가상 함수를 선언할거에요 -> 자식에서 덮어쓸 스탯 초기화 함수 OnResetStats를
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 공통 기능
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
public virtual void Reactivate() // 함수를 선언할거에요 -> 몬스터를 재활성화하는 Reactivate를
{
if (agent != null && agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> 에이전트가 있고 바닥에 있다면
{
agent.isStopped = false; // 명령을 내릴거에요 -> 이동 멈춤을 풀라(false)고
if (IsAggroed && playerTransform != null) // 조건이 맞으면 실행할거에요 -> 어그로가 끌렸고 플레이어가 있다면
{
agent.SetDestination(playerTransform.position); // 명령을 내릴거에요 -> 목적지를 플레이어 위치로
}
}
if (mobRenderer != null) mobRenderer.enabled = true; // 조건이 맞으면 실행할거에요 -> 렌더러가 있다면 다시 켜기(true)를
}
public virtual void OnManagedUpdate() // 함수를 선언할거에요 -> 매니저가 호출해줄 업데이트 함수 OnManagedUpdate를
{
if (isDead || playerTransform == null || !gameObject.activeInHierarchy) return; // 조건이 맞으면 중단할거에요 -> 죽었거나, 플레이어가 없거나, 꺼져 있다면
float distance = Vector3.Distance(transform.position, playerTransform.position); // 값을 계산할거에요 -> 나와 플레이어 사이의 거리를 distance에
if (distance > optimizationDistance && !IsAggroed) // 조건이 맞으면 실행할거에요 -> 거리가 멀고 어그로 상태가 아니라면
{
StopMovement(); // 함수를 실행할거에요 -> 움직임을 멈추는 StopMovement를
if (mobRenderer != null && mobRenderer.enabled) mobRenderer.enabled = false; // 조건이 맞으면 실행할거에요 -> 렌더러가 켜져 있다면 끄기(false)를 (최적화)
return; // 중단할거에요 -> AI 로직을 실행하지 않도록 함수를
}
if (mobRenderer != null && !mobRenderer.enabled) mobRenderer.enabled = true; // 조건이 맞으면 실행할거에요 -> 렌더러가 꺼져 있다면 다시 켜기(true)를
if (mobRenderer != null && !mobRenderer.isVisible) { StopMovement(); return; } // 조건이 맞으면 실행할거에요 -> 렌더러가 화면에 보이지 않는다면 멈추고 중단하기를
if (agent != null && agent.isOnNavMesh && agent.isStopped) agent.isStopped = false; // 조건이 맞으면 실행할거에요 -> 에이전트가 멈춰있다면 다시 움직이게(false) 하기를
ExecuteAILogic(); // 함수를 실행할거에요 -> 실제 몬스터의 AI 로직을
}
protected void StopMovement() // 함수를 선언할거에요 -> 이동을 멈추는 StopMovement를
{
if (agent != null && agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> 에이전트가 정상 작동 중이라면
{
agent.isStopped = true; // 명령을 내릴거에요 -> 이동을 멈추라(true)고
agent.velocity = Vector3.zero; // 값을 넣을거에요 -> 이동 속도를 0으로
}
if (animator != null) // 조건이 맞으면 실행할거에요 -> 애니메이터가 있다면
{
animator.SetFloat("Speed", 0f); // 값을 전달할거에요 -> 속도 파라미터를 0으로
animator.Play(Monster_Idle); // 재생할거에요 -> 대기 애니메이션(Monster_Idle)을
}
}
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 피격 / 사망
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
public virtual void TakeDamage(float amount) // 함수를 선언할거에요 -> 외부에서 데미지를 줄 때 호출할 TakeDamage를
{
OnDamaged(amount); // 함수를 실행할거에요 -> 실제 피격 처리를 담당하는 OnDamaged를
}
public virtual void OnDamaged(float damage) // 함수를 선언할거에요 -> 데미지 계산 로직인 OnDamaged를
{
if (isDead) return; // 조건이 맞으면 중단할거에요 -> 이미 죽었다면
IsAggroed = true; // 상태를 바꿀거에요 -> 공격받았으니 어그로 상태를 참(true)으로
currentHP -= damage; // 값을 뺄거에요 -> 체력에서 데미지(damage)만큼을
OnHealthChanged?.Invoke(currentHP, maxHP); // 이벤트를 알릴거에요 -> 체력이 깎였음을
if (currentHP <= 0) { Die(); return; } // 조건이 맞으면 실행할거에요 -> 체력이 0 이하라면 사망 함수(Die)를
if (!isHit) StartHit(); // 조건이 맞으면 실행할거에요 -> 피격 상태가 아니라면 피격 연출(StartHit)을
}
protected virtual void StartHit() // 함수를 선언할거에요 -> 피격 연출을 시작하는 StartHit을
{
isHit = true; // 상태를 바꿀거에요 -> 피격 중 상태를 참(true)으로
isAttacking = false; // 상태를 바꿀거에요 -> 공격 중 상태를 거짓(false)으로
isResting = false; // 상태를 바꿀거에요 -> 휴식 중 상태를 거짓(false)으로
StopAllCoroutines(); // 중단할거에요 -> 실행 중인 모든 코루틴을
OnStartHit(); // 함수를 실행할거에요 -> 자식 클래스의 추가 피격 처리 OnStartHit을
if (agent && agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> 에이전트가 작동 중이라면
{
agent.isStopped = true; // 명령을 내릴거에요 -> 이동을 멈추라(true)고
agent.velocity = Vector3.zero; // 값을 넣을거에요 -> 속도를 0으로
}
animator.Play(Monster_GetDamage, 0, 0f); // 재생할거에요 -> 피격 애니메이션(Monster_GetDamage)을
if (hitEffect) hitEffect.Play(); // 조건이 맞으면 실행할거에요 -> 피격 이펙트가 있다면 재생하기를
if (hitSound) audioSource.PlayOneShot(hitSound); // 조건이 맞으면 실행할거에요 -> 피격 사운드가 있다면 재생하기를
}
protected virtual void OnStartHit() { } // 가상 함수를 선언할거에요 -> 자식에서 덮어쓸 피격 시작 함수 OnStartHit을
public virtual void OnHitEnd() // 함수를 선언할거에요 -> 피격 연출이 끝났을 때 호출할 OnHitEnd를
{
isHit = false; // 상태를 바꿀거에요 -> 피격 중 상태를 거짓(false)으로
if (agent && agent.isOnNavMesh) agent.isStopped = false; // 조건이 맞으면 실행할거에요 -> 에이전트가 있다면 다시 움직이게(false) 하기를
}
protected virtual void Die() // 함수를 선언할거에요 -> 몬스터 사망을 처리하는 Die를
{
if (isDead) return; // 조건이 맞으면 중단할거에요 -> 이미 죽은 상태라면
isDead = true; // 상태를 바꿀거에요 -> 사망 상태를 참(true)으로
IsAggroed = false; // 상태를 바꿀거에요 -> 어그로 상태를 거짓(false)으로
OnDie(); // 함수를 실행할거에요 -> 자식 클래스의 추가 사망 처리 OnDie를
OnMonsterKilled?.Invoke(expReward); // 이벤트를 알릴거에요 -> 몬스터 처치와 경험치 보상(expReward)을
Collider col = GetComponent<Collider>(); // 컴포넌트를 가져올거에요 -> 내 몸의 콜라이더를 col에
if (col != null) col.enabled = false; // 조건이 맞으면 실행할거에요 -> 콜라이더가 있다면 끄기(false)를 (시체 밟기 방지)
if (agent && agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> 에이전트가 작동 중이라면
{
agent.isStopped = true; // 명령을 내릴거에요 -> 이동을 멈추라(true)고
agent.velocity = Vector3.zero; // 값을 넣을거에요 -> 속도를 0으로
}
animator.Play(Monster_Die, 0, 0f); // 재생할거에요 -> 사망 애니메이션(Monster_Die)을
if (deathSound) audioSource.PlayOneShot(deathSound); // 조건이 맞으면 실행할거에요 -> 사망 사운드가 있다면 재생하기를
Invoke("ReturnToPool", 1.5f); // 예약을 걸거에요 -> 1.5초 뒤에 풀로 반환하는 ReturnToPool 함수를
}
protected virtual void OnDie() { } // 가상 함수를 선언할거에요 -> 자식에서 덮어쓸 사망 처리 함수 OnDie를
public bool IsDead => isDead; // 프로퍼티를 선언할거에요 -> 외부에서 사망 여부를 확인할 수 있는 IsDead를
protected void ReturnToPool() => gameObject.SetActive(false); // 함수를 선언할거에요 -> 오브젝트를 비활성화해서 풀로 돌려보내는 ReturnToPool을
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// ⭐ 공격 이벤트
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// ⭐ [핵심 수정] 자식에서 재정의할 수 있도록 public virtual로 선언
public virtual void OnAttackStart() // 함수를 선언할거에요 -> 공격 시작 시 호출되는 OnAttackStart를
{
isAttacking = true; // 상태를 바꿀거에요 -> 공격 중 상태를 참(true)으로
isResting = false; // 상태를 바꿀거에요 -> 휴식 중 상태를 거짓(false)으로
if (myWeapon != null) // 조건이 맞으면 실행할거에요 -> 무기가 있다면
{
myWeapon.EnableHitBox(); // 함수를 실행할거에요 -> 무기의 공격 판정을 켜는 EnableHitBox를
}
}
// ⭐ [핵심 수정] 자식에서 재정의할 수 있도록 public virtual로 선언
public virtual void OnAttackEnd() // 함수를 선언할거에요 -> 공격 종료 시 호출되는 OnAttackEnd를
{
if (myWeapon != null) // 조건이 맞으면 실행할거에요 -> 무기가 있다면
{
myWeapon.DisableHitBox(); // 함수를 실행할거에요 -> 무기의 공격 판정을 끄는 DisableHitBox를
}
isAttacking = false; // 상태를 바꿀거에요 -> 공격 중 상태를 거짓(false)으로
if (!isDead && !isHit) StartCoroutine(RestAfterAttack()); // 조건이 맞으면 실행할거에요 -> 살아있고 안 맞았다면 휴식 코루틴(RestAfterAttack)을
}
protected virtual IEnumerator RestAfterAttack() // 코루틴 함수를 선언할거에요 -> 공격 후 잠시 쉬는 RestAfterAttack을
{
isResting = true; // 상태를 바꿀거에요 -> 휴식 중 상태를 참(true)으로
yield return new WaitForSeconds(attackRestDuration); // 기다릴거에요 -> 휴식 시간(attackRestDuration)만큼
isResting = false; // 상태를 바꿀거에요 -> 휴식 중 상태를 거짓(false)으로
}
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// ⭐ 상태 이상 시스템
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
public void ApplyStatusEffect(StatusEffectType type, float damage, float duration) // 함수를 선언할거에요 -> 상태이상을 적용하는 ApplyStatusEffect를
{
if (isDead) return; // 조건이 맞으면 중단할거에요 -> 이미 죽었다면
switch (type) // 분기할거에요 -> 상태이상 종류(type)에 따라
{
case StatusEffectType.Burn: // 조건이 맞으면 실행할거에요 -> 화상(Burn)이라면
StartCoroutine(BurnCoroutine(damage, duration)); // 코루틴을 실행할거에요 -> 화상 효과를 주는 BurnCoroutine을
break;
case StatusEffectType.Slow: // 조건이 맞으면 실행할거에요 -> 슬로우(Slow)라면
StartCoroutine(SlowCoroutine(damage, duration)); // 코루틴을 실행할거에요 -> 느리게 만드는 SlowCoroutine을
break;
case StatusEffectType.Poison: // 조건이 맞으면 실행할거에요 -> 독(Poison)이라면
StartCoroutine(PoisonCoroutine(damage, duration)); // 코루틴을 실행할거에요 -> 독 효과를 주는 PoisonCoroutine을
break;
case StatusEffectType.Shock: // 조건이 맞으면 실행할거에요 -> 충격(Shock)이라면
TakeDamage(damage); // 함수를 실행할거에요 -> 충격 데미지를 즉시 적용하기를
StartCoroutine(StunCoroutine(0.5f)); // 코루틴을 실행할거에요 -> 0.5초간 기절시키는 StunCoroutine을
break;
}
}
private IEnumerator BurnCoroutine(float tickDamage, float duration) // 코루틴 함수를 선언할거에요 -> 화상 효과를 처리할 BurnCoroutine을
{
float elapsed = 0f; // 변수를 초기화할거에요 -> 경과 시간을 0으로
float tickInterval = 0.5f; // 변수를 초기화할거에요 -> 데미지 간격을 0.5초로
while (elapsed < duration) // 반복할거에요 -> 경과 시간이 지속 시간보다 작을 동안
{
TakeDamage(tickDamage * tickInterval); // 함수를 실행할거에요 -> 틱 데미지를 입히는 TakeDamage를
yield return new WaitForSeconds(tickInterval); // 기다릴거에요 -> 0.5초의 시간을
elapsed += tickInterval; // 값을 더할거에요 -> 경과 시간에 0.5초를
}
}
private IEnumerator SlowCoroutine(float amount, float duration) // 코루틴 함수를 선언할거에요 -> 슬로우 효과를 처리할 SlowCoroutine을
{
if (agent != null) // 조건이 맞으면 실행할거에요 -> 에이전트가 있다면
{
float originalSpeed = agent.speed; // 값을 저장할거에요 -> 원래 속도를 originalSpeed에
agent.speed *= (1f - Mathf.Clamp01(amount / 100f)); // 값을 변경할거에요 -> 현재 속도를 비율(amount)만큼 줄여서
yield return new WaitForSeconds(duration); // 기다릴거에요 -> 지속 시간(duration)만큼
agent.speed = originalSpeed; // 값을 복구할거에요 -> 속도를 원래대로
}
else // 조건이 틀리면 실행할거에요 -> 에이전트가 없다면
{
yield return new WaitForSeconds(duration); // 기다릴거에요 -> 시간만 때우기를
}
}
private IEnumerator PoisonCoroutine(float tickDamage, float duration) // 코루틴 함수를 선언할거에요 -> 독 효과를 처리할 PoisonCoroutine을
{
float elapsed = 0f; // 변수를 초기화할거에요 -> 경과 시간을 0으로
float tickInterval = 1f; // 변수를 초기화할거에요 -> 데미지 간격을 1초로
while (elapsed < duration) // 반복할거에요 -> 경과 시간이 지속 시간보다 작을 동안
{
TakeDamage(tickDamage * tickInterval); // 함수를 실행할거에요 -> 틱 데미지를 입히는 TakeDamage를
yield return new WaitForSeconds(tickInterval); // 기다릴거에요 -> 1초의 시간을
elapsed += tickInterval; // 값을 더할거에요 -> 경과 시간에 1초를
}
}
private IEnumerator StunCoroutine(float duration) // 코루틴 함수를 선언할거에요 -> 기절 효과를 처리할 StunCoroutine을
{
if (agent != null) // 조건이 맞으면 실행할거에요 -> 에이전트가 있다면
{
agent.isStopped = true; // 명령을 내릴거에요 -> 이동을 멈추라(true)고
if (animator != null) animator.speed = 0; // 조건이 맞으면 실행할거에요 -> 애니메이터가 있다면 재생 속도를 0으로 (얼음 땡)
yield return new WaitForSeconds(duration); // 기다릴거에요 -> 지속 시간(duration)만큼
if (!isDead) // 조건이 맞으면 실행할거에요 -> 아직 살아있다면
{
agent.isStopped = false; // 명령을 내릴거에요 -> 이동 멈춤을 풀라(false)고
if (animator != null) animator.speed = 1; // 조건이 맞으면 실행할거에요 -> 애니메이션 속도를 정상(1)으로
}
}
else // 조건이 틀리면 실행할거에요 -> 에이전트가 없다면
{
yield return new WaitForSeconds(duration); // 기다릴거에요 -> 시간만 때우기를
}
}
}