[TIL] Unity - FSM

MINO·2024년 6월 26일
0
post-thumbnail

2024-06-26


FSM

FSM (Finite State Machine) 는 상태를 기반으로 동작을 제어하는 방식의 디자인 패턴이다.


주요 구성 요소

  • State (상태) : 객체는 오직 하나의 특정한 상태만을 가진다.
    • Enemy : Idle (대기) , Patrol (정찰) , Chase (추적) , Attack (공격) ...
  • Transition (전이) : 한 상태에서 다른 상태로의 변화를 나타낸다.
    • 특정 조건을 충족할 때, 전이를 통해 상태가 바뀐다.
    • Enemy : Chase -> Attack
  • Event (이벤트) : 상태 전이를 트리거하는 사건 또는 조건이다.
    • Enemy 의 공격 범위 안에 플레이어가 들어왔다.
  • Action (행동) : 특정 상태에서 수행되는 동작이다.
    • Enemy 가 플레이어를 공격하는 행동을 수행한다.

장점

  • 단순성 : 상태와 전이가 명확하게 정의되어 있어 코드가 직관적이고 이해하기 쉽다.
  • 유연성 : 상태와 전이를 추가하거나 수정하기가 비교적 쉽다.
  • 재사용성 : FSM 구조를 다른 캐릭터나 객체에 재사용할 수 있다.

FSM 예시

구글링과 유튜브를 통해, 크게 2가지의 FSM 활용 방법을 찾아보았다.

Idle, Chase , Attack 3가지 상태가 존재하는 Enemy FSM 으로 예시를 들어보자.

public enum State
{
	Idle,
    Chase,
    Attack
}

public State curState = State.Idle;

코루틴을 활용한 FSM

IEnumerator FSM()
{
	yield return null;
    
    while(hp > 0)
    {
		yield return StartCoroutine(curState.ToString());
    }
}

IEnumerator Idle()
{
	yield return null;
    
    Anim.SetBool("idle",true);
    
    if ( 조건1 )// Enemy 가 플레이어를 발견했을 경우
    {
    	if( 조건2 ) // 공격 가능한 경우
		curState = State.Attack;
        
        else // 공격 불가능한 경우
			curState = State.Chase;
	}
    else // Enemy 가 플레이어를 발견하지 못한 경우
    	curState = State.Idle;
}

IEnumerator Chase()
{
	yield return null;
    
    Anim.SetBool("chase",true);
    
    // Enemy 가 플레이어 쪽으로 이동하는 로직
    
    if( 조건1 ) // Enemy 가 플레이어를 공격 가능한 경우
    	curState = State.Attack;
        
    else // Enemy 가 플레이어를 공격할 수 없는 경우 
    	curState = State.Chase;
    
    if ( 조건2 ) // Enemy 가 플레이어를 놓친 경우 
    	curState = State.Idle;
    
}

IEnumerator Attack()
{
	yield return null;
    
    Anim.SetTrigger("attack");
    
    // Enemy 가 플레이어를 공격하는 로직
    
    if( 조건1 ) // 플레이어가 여전히 Enemy 의 탐지 범위에 있는 경우
    	curState = State.Chase;
	
    else // 플레이어가 Enemy 의 탐지 범위에서 벗어난 경우
    	curState = State.Idle;
}

위에서 언급한 4가지 구성요소가 모두 적절히 사용되었다.
Idle, Chase, Attack 의 3가지의 State ,
Event 에 따른 Transition , 상태마다 수행되는 Action.


추상 클래스를 활용한 FSM

// BaseState 라는 추상 클래스를 정의하고, 
// 각 상태를 구현하기 위한 필수적인 내용을 미리 정의한다.

public abstract class BaseState
{
	protected Enemy enemy;
    
	protected BaseState(Enemy enemy)
    {
    	this.enemy = enemy;
    }
    
    public abstract void EnterState(); // 상태 진입 시, 초기화
    public abstract void Update(); // 매 프레임마다 호출
    public abstract void ExitState(); // 상태 변경 시, 초기화
}

public class IdleState : BaseState
{
	public IdleState(Enemy enemy) : base(enemy) { }
	
    public override void EnterState() 
    {
    	// Idle 상태 진입 시, 초기화 로직
    }
    
    public override void Update()
    {
    	// Ex : 플레이어 탐지 로직
    }
    
    public override void ExitState()
    {
    
    }
}

public class ChaseState : BaseState
{
	public ChaseState(Enemy enemy) : base(enemy) { }
	
    public override void EnterState() 
    {
    	// Chase 상태 진입 시, 초기화 로직
    }
    
    public override void Update()
    {
    	// Ex : 플레이어 방향으로 이동 , 공격 범위 안 타겟 탐지
    }
    
    public override void ExitState()
    {
    
    }
}

public class AttackState : BaseState
{
	public AttackState(Enemy enemy) : base(enemy) { }
	
    public override void EnterState() 
    {
    	// Attack 상태 진입 시, 초기화 로직
    }
    
    public override void Update()
    {
    	// Ex : 플레이어 공격 로직
    }
    
    public override void ExitState()
    {
    
    }
}

장점 비교

Coroutine FSM

  • 비동기 작업이 많거나, 상태 전환 로직이 비교적 간단한 경우에 유리
  • 간결한 코드와 자연스러운 흐름 제어가 필요한 상황에서 효과적

BaseState FSM

  • 상태 전환이 명확하고, 각 상태가 독립적으로 동작해야 하는 경우에 유리
  • 구조적인 명확성과 디버깅 용이성을 중시하는 상황에서 효과적

TIL 마무리

FSM 의 두 가지 구현 방법에 대해 알아보며, 각각의 장점에 대해 알아보았다.

다양한 방법으로 FSM 을 구현해보며, 상황에 따라 적절한 구현 방식을 활용해야겠다.

profile
안녕하세요 게임 개발하는 MINO 입니다.

0개의 댓글