32일차) [Unity] 코루틴(Coroutine)을 활용한 FSM 리팩토링

엄기태·2026년 2월 13일

Unity

목록 보기
7/13

1. 구현 목표

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

  • 과제 내용: Init/Update 구조의 FSM 코드를 코루틴 기반으로 변경
  • 제출 항목: 소스코드, 구동 영상

2. 주요 구현 내용

🛠 추가된 필드 (Field)

코루틴의 중복 실행을 방지하고 상태별 타이머를 관리하기 위해 Coroutine 변수를 개별적으로 선언했다.

private float _updateInterval = 0.2f; // 타겟 스캔 주기

Coroutine _scanTarget;
Coroutine _idleTimer;
Coroutine _combatCooldown;
Coroutine _damagedStun;

📂 주요 상태 함수 (State Functions)

각 상태의 Init 시점에서 필요한 코루틴을 실행하고, 기존에 돌고 있던 코루틴을 안전하게 해제하는 로직을 추가했다.

  • Idle: IdleTimer를 실행하여 일정 시간 후 자동으로 Patrol 전환
  • Patrol/Chase: ScanTarget 코루틴을 통해 주기적으로 플레이어 탐색
  • Combat/Damaged: 공격 쿨타임 및 스턴 시간을 코루틴으로 제어

캡슐화된 코루틴 해제 함수

private void DeleteCoroutine(Coroutine coroutine)
{
    if (coroutine != null)
    {
        StopCoroutine(coroutine);
    }
}

3. 핵심 코루틴 로직

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;
		}
	}

4. 문제 해결 및 경험 (Troubleshooting)

⚠️ 코루틴 중복 실행 및 좀비 현상

  • 문제: 몬스터가 죽은 후에도 코루틴이 살아있어 타겟을 계속 탐지하거나 공격하는 버그 발생.
  • 해결: 코루틴 시작 전 DeleteCoroutine을 통해 기존 루틴을 제거하고, 각 코루틴 내부에서 if (!_isAlive) yield break; 조건을 추가하여 생존 여부를 최우선으로 체크함.

⚠️ 이동 로직의 코루틴화 실패

  • 문제: 이동(Chase, Patrol) 로직을 _updateInterval에 맞춰 코루틴화했더니 몬스터가 끊기면서 움직임.
  • 결과: 이동은 매 프레임 부드러운 좌표 계산이 필요하므로 Update 영역에 남겨두는 것이 맞다는 것을 학습함.

⚠️ Combat 무한 재실행 버그

  • 문제: ScanTarget 코루틴이 0.2초마다 Chase나 Combat 상태를 강제로 재호출하여 공격 애니메이션이 씹히거나 빠르게 작동함.
  • 해결: 코루틴 내부에 현재 상태(_state)를 체크하는 조건문을 추가하여 불필요한 상태 전환 호출을 차단함.

5. 회고 및 개선 계획

무작정 모든 Update 코드를 코루틴으로 옮기는 것이 정답은 아니라는 것을 깨달았다.

  • 코루틴이 유리한 경우: 타이머, 주기적인 탐색, 1회성 대기 로직
  • Update가 유리한 경우: 연속적인 이동, 물리 연산, 실시간 입력 반응

앞으로 로직의 성격에 따라 '프레임 단위 연산'과 '시간 단위 연산'을 명확히 구분하여 설계할 계획이다.

profile
코딩 학습 공간

0개의 댓글