using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 using UnityEngine.AI; // 길찾기(내비게이션) 기능을 불러올거에요 -> UnityEngine.AI를 using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Collections를 using System; // 기본 시스템 기능(Action 등)을 사용할거에요 -> System을 /// /// 몬스터 기본 클래스 (공통 기능만) /// - 공격 방식은 자식 클래스에서 구현 /// 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 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 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(); // 컴포넌트를 가져올거에요 -> 자식 오브젝트의 렌더러를 mobRenderer에 animator = GetComponent(); // 컴포넌트를 가져올거에요 -> 내 몸의 애니메이터를 animator에 agent = GetComponent(); // 컴포넌트를 가져올거에요 -> 내 몸의 길찾기 에이전트를 agent에 audioSource = GetComponent(); // 컴포넌트를 가져올거에요 -> 내 몸의 오디오 소스를 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(); // 컴포넌트를 가져올거에요 -> 내 몸의 콜라이더를 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(); // 컴포넌트를 가져올거에요 -> 내 몸의 콜라이더를 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); // 기다릴거에요 -> 시간만 때우기를 } } }