기존의 Update 문에서 매 프레임 체크하던 FSM(Finite State Machine) 로직을 코루틴 방식으로 리팩토링하여 최적화 및 가독성을 높이는 것을 목표로 한다.

코루틴의 중복 실행을 방지하고 상태별 타이머를 관리하기 위해 Coroutine 변수를 개별적으로 선언했다.
private float _updateInterval = 0.2f; // 타겟 스캔 주기
Coroutine _scanTarget;
Coroutine _idleTimer;
Coroutine _combatCooldown;
Coroutine _damagedStun;
각 상태의 Init 시점에서 필요한 코루틴을 실행하고, 기존에 돌고 있던 코루틴을 안전하게 해제하는 로직을 추가했다.
IdleTimer를 실행하여 일정 시간 후 자동으로 Patrol 전환ScanTarget 코루틴을 통해 주기적으로 플레이어 탐색private void DeleteCoroutine(Coroutine coroutine)
{
if (coroutine != null)
{
StopCoroutine(coroutine);
}
}
WaitForSeconds를 활용하여 불필요한 매 프레임 연산을 줄였다. 특히 타겟 스캔 로직을 주기적으로 실행하게 변경한 점이 핵심이다.
IEnumerator IdleTimer()
{
yield return new WaitForSeconds(_idleTime);
if (!_isAlive) yield break;
ChangeState(MonsterState.PATROL);
yield break;
}
IEnumerator ScanTarget()
{
while (true)
{
yield return new WaitForSeconds(_updateInterval);
if (!_isAlive) yield break;
if (_state == MonsterState.COMBAT || _state == MonsterState.DAMAGED) continue;
if (IsFindTarget(_chaseRange))
{
if (_state != MonsterState.CHASE)
{
ChangeState(MonsterState.CHASE);
yield break;
}
}
}
}
IEnumerator CombatCooldown()
{
// 공격 대상의 상태 - 살아있는지 -> Idle 로 전환
if (_target == null || _playerCombat.IsAlive == false)
{
ChangeState(MonsterState.IDLE);
yield break;
}
// 딜레이
yield return new WaitForSeconds(_attackInterval);
if (!_isAlive) yield break;
// 공격 범위 내에 있는지 -> Combat / Chase 로 전환
float distTarget = Vector3.Distance(transform.position, _target.transform.position);
if (distTarget <= _attackRange)
{
ChangeState(MonsterState.COMBAT);
yield break;
}
else if (distTarget <= _chaseRange)
{
ChangeState(MonsterState.CHASE);
yield break;
}
else
{
ChangeState(MonsterState.IDLE);
yield break;
}
// 공격을 받았는지 -> 애니메이션 전환 (GotHit)
// 본인 상태 확인 -> Dead 전환
}
IEnumerator DamagedStun()
{
// 애니메이션 종류 후 -> Combat 으로 전환
yield return new WaitForSeconds(_damageStunTime);
if (!_isAlive) yield break;
if (IsFindTarget(_chaseRange))
{
if (IsFindTarget(_attackRange))
{
ChangeState(MonsterState.COMBAT);
yield break;
}
else
{
ChangeState(MonsterState.CHASE);
yield break;
}
}
else
{
ChangeState(MonsterState.IDLE);
yield break;
}
}
DeleteCoroutine을 통해 기존 루틴을 제거하고, 각 코루틴 내부에서 if (!_isAlive) yield break; 조건을 추가하여 생존 여부를 최우선으로 체크함._updateInterval에 맞춰 코루틴화했더니 몬스터가 끊기면서 움직임.Update 영역에 남겨두는 것이 맞다는 것을 학습함.ScanTarget 코루틴이 0.2초마다 Chase나 Combat 상태를 강제로 재호출하여 공격 애니메이션이 씹히거나 빠르게 작동함._state)를 체크하는 조건문을 추가하여 불필요한 상태 전환 호출을 차단함.무작정 모든 Update 코드를 코루틴으로 옮기는 것이 정답은 아니라는 것을 깨달았다.
앞으로 로직의 성격에 따라 '프레임 단위 연산'과 '시간 단위 연산'을 명확히 구분하여 설계할 계획이다.