[Unity] FSM 분석하기

Connected Brain·2025년 3월 21일

FSM(Finite State Machine) 분석하기

FSM(Finite State Machine)이란?

  • FSM = Finite State Machine = 유한상태 머신
  • FSM이란 유한한 개수의 정해진 상태를 두고 상태의 전환을 관리하여, 실질적인 기능의 실행은 각각의 상태에 맡기는 형태로 기능을 구현하는 것을 말한다.
  • 기능을 실행할 때 각각의 상태에서의 해야할 일을 하나에서 관리하는 것이 아니라 어떤 상태에서 다음 상태로 넘겨주기만 하면 해당 상태에서 해야할 일을 해당 상태에서 알아서 처리하므로 처리해야할 상태가 여러 개일 경우 새로운 상태를 추가하기에 용이하며, 각각의 상태에서 처리하는 기능을 수정하기에도 유리하다.
  • 지금까지는 플레이어의 걷기, 달리기, 공격 등의 상태를 관리할 때 조건문(if 내지는 Switch 문)으로 관리해야 했다. 다만 이런 방법은 상태가 많아질 때마다 조건문이 길어져, 확장성이 떨어진다는 단점이 있다. 이를 해결하기 위해 FSM 기법을 적용할 수 있다.

  • 이처럼 각각의 상태들을 클래스로 만들어 StateMachine 클래스에서 한 상태에서 다른 상태로 바꿔주면 각각의 상태에서 구현된 세부 기능을 사용하는 것이다.

1. IState

  • 각각의 State들이 가질 요소들을 IState라는 인터페이스를 활용해 명시해 놓도록 하였다.
public interface IState
{
    public void Enter();
    public void Exit();
    public void HandleInput();
    public void Update();
    public void PhysicsUpdate();
}

Enter()

  • 해당 상태로 진입시 실행할 로직

Exit()

  • 해당 상태에서 빠져나갈 시 실행할 로직

HandleInput()

  • 키 입력을 처리하는 부분

Update()

  • 해당 상태일 때 정보 갱신을 위해 매 프레임마다 실행해야 하는 기능

PhysicsUpdate()

  • 물리 현상에 대한 정보 갱신을 위해 실행해야 하는 기능
  • 이를 State들에서 구현하도록 하나 State에 따라 해당하는 기능이 필요하거나 필요하지 않을 수 있다. 또는 여러 State들이 공통적인 기능을 가질 수도 있다. 따라서 상속 구조로 만들어 이런 부분을 간단하게 할 수 있다.

2. BaseState

public class PlayerBaseState : IState
{
	protected PlayerStateMachine stateMachine;
    
    public PlayerBaseState(PlayerStateMachine stateMachine)
    {
        this.stateMachine = stateMachine;
    }

    public virtual void Enter()
    {
        AddInputActionCallbacks();
    }

    public virtual void Exit()
    {
        RemoveInputActionCallbacks();
    }

    public virtual void AddInputActionCallbacks()
    {
        PlayerController input = stateMachine.Player.Input;
        input.playerActions.Movement.canceled += OnMovementCanceled;
        input.playerActions.Run.started += OnRunStarted;
        input.playerActions.Jump.started += OnJumpStarted;
        input.playerActions.Attack.performed += OnAttackPerformed;
        input.playerActions.Attack.canceled += OnAttackCanceled;
    }

    public virtual void RemoveInputActionCallbacks()
    {
        PlayerController input = stateMachine.Player.Input;
        input.playerActions.Movement.canceled -= OnMovementCanceled;
        input.playerActions.Run.started -= OnRunStarted;
        input.playerActions.Jump.started -= OnJumpStarted;
        input.playerActions.Attack.performed -= OnAttackPerformed;
        input.playerActions.Attack.canceled -= OnAttackCanceled;
    }

    public virtual void HandleInput()
    {
        ReadMovementInput();
    }

    public virtual void PhysicsUpdate()
    {

    }

    public virtual void Update()
    {
        Move();
    }
    
    ...각종 키 입력 이벤트를 위함 함수...

    private void ReadMovementInput()
    {
        ... 키 입력에 따른 이동 입력을 받는 함수...
    }

    private void Move()
    {
        ...이동 기능을 처리...
    }

}
  • 간략화한 BaseState를 살펴보도록 하겠다. 먼저 생성자에서 stateMachine을 캐싱한다. PlayerStateMachine에서 각각의 상태에서 필요로 하는 정보를 가지고 있으므로 이를 활용하기 위함이다.
  • Enter와 Exit에서는 키 입력에 따른 이벤트를 연결하고 있다. BaseState에서 키 입력 이벤트를 위한 함수를 만들어 두고 각각의 상태에서 이를 상속받아 기능을 구현한다.
  • HandleInput에서는 키 입력을 통한 이동 입력을 받는 함수를 실행하도록 하였고, Update에서는 이를 통한 이동을 실행하도록 한다.
  • 다만 어디까지나 State들은(Base뿐만 아니라 어떤 State든) 기능을 직접 실행하지 않는다. 실행할 기능을 정의해 두었을 뿐이다. 이를 실행하는 것은 StateMachine에서 이루어 진다.

3. StateMachine

public abstract class StateMachine
{
    protected IState currentState;

    public void ChangeState(IState state)
    {
        currentState?.Exit();
        currentState = state;
        currentState?.Enter();
    }

    public void HandleInput()
    {
        currentState?.HandleInput();
    }

    public void Update()
    {
        currentState?.Update();
    }

    public void PhaysicsUpdate()
    {
        currentState?.PhysicsUpdate();
    }
}
  • StateMachine을 구현하도록 하는 추상클래스이지만 기능을 알아보는 데에는 지장이 없을 것이다.
  • ChangeState는 State를 입력받아 기존에 실행 중이던 상태를 종료하고 이를 입력받은 상태로 전환한 다음 실행하도록 한다. currentState의 기능들은 StateMachine에 의해 호출받는다.
public class Player : MonoBehaviour
{
    ...

    private PlayerStateMachine stateMachine;

    private void Awake()
    {
        stateMachine = new PlayerStateMachine(this);
    }

    // Start is called before the first frame update
    void Start()
    {
        Cursor.lockState = CursorLockMode.Locked;
        stateMachine.ChangeState(stateMachine.IdleState);
    }

    private void Update()
    {
        stateMachine.HandleInput();
        stateMachine.Update();
    }

    private void FixedUpdate()
    {
        stateMachine.PhaysicsUpdate();
    }
}
  • 최종적으로 Monobehaviour를 상속받는 Player 클래스에서 StateMachine을 생성해 매 프레임마다 현재 상태에서 사용할 기능을 호출하고 있음을 볼 수 있다.

총정리

  • 각각의 상태에서는 어떤 것을 실시할지에 관한 명령만 가지고 있고, 이에 따라 실행하는 것은 Player 클래스를 통해 이루어지는 구조를 분석하며 FSM의 전반적인 개념과 사용 이유에 대해 파악할 수 있었다.
  • 다만 실제로 이를 사용할 때는 상속에 의해 다층적으로 이루어진 클래스와 실제로 이를 실행하는 부분이 분리되어 있다는 점에서 이해하는데 다소 어려움이 있음을 느꼈다. 따라서 이를 사용하기 위해서는 명확한 계층 구조를 잡고 각각의 상태에서 실행할 기능에 대한 확실한 구분을 할 필요가 있을 것이라고 느낄 수 있었다.

0개의 댓글