Unity 공부

[Unity] FSM 유한 상태 기계

때류기 2023. 11. 21. 22:14

 

FSM: 유한 상태 기계(Finite-State Machine)

유한 상태 기계는 주어지는 모든 시간에서 처해 있을 수 있는 유한개의 상태를 가지고 주어진 입력에 따라 어떤 상태에서 다른 상태로 전환하거나 출력이나 액션이 일어나게 하는 장치 또는 장치를 나타낸 모델

 

상태(State): 게임에 정의된 여러 동작을 낸다

  • Idle, Walk, Run...
  • 한 상태에서 다른 상태로 전이할 수 있고 동시에 여러 상태를 실행할 수 없다.

 

전이(Transition): 한 상태에서 다른 상태로 변화 하는것

  • 각 상태 로직 또는 외부에서 전이 조건에 의해 전이될 수 있다.

 

 

FSM을 간단하게 사용해 플레이어를 움직일 수 있게 하는 코드를 작성했습니다.

구현 코드

 

BaseState

모든 상태의 부모 클래스

public abstract class BaseState
{
    /// <summary> 다른상태에서 이 상태로 변화될때 한번 실행  </summary>
    public abstract void OnEnter();

    /// <summary> Update()에 추가하여 반복 실행 시키는 함수 </summary>
    public abstract void OnUpdate();

    /// <summary> FixedUpdate()에 추가하여 반복 실행 시키는 함수 </summary>
    public abstract void OnFixedUpdate();

    /// <summary> 이 상태에서 다른 상태로 변화할때 한번 실행  </summary>
    public abstract void OnExit();
}

 

 

PlayerState

BaseState를 상속받으며 플레이어의 상태 클래스의 부모 클래스

만약 Monster의상태를 만들고자 한다면 BaseState에서 상속받은 MonsterState를 아래와 같은 방식으로 작성하면된다.

/// <summary> 플레이어 상태의 부모 클래스 </summary>
public abstract class PlayerState : BaseState
{
    protected Player _player;

    protected PlayerStateMachine _machine;

    public PlayerState(Player player, PlayerStateMachine machine)
    {
        _player = player;
        _machine = machine;
    }
}

 

 

P_IdleState

플레이어가 대기 상태일때 실행되는 함수를 정리한 클래스

/// <summary> 플레이어 대기 상태 정보를 가지고 있는 클래스 </summary>
public class P_IdleState : PlayerState
{
    public P_IdleState(Player player, PlayerStateMachine machine) : base(player, machine)
    {
    }

    /// <summary> 현재 상태로 전이시 한번 실행 </summary>
    public override void OnEnter()
    {
        //만약 애니메이션이 있다면 이쪽에 애니메이션 전환 코드를 작성
    }


    /// <summary> 다음 상태로 전이시 한번 실행 </summary>
    public override void OnExit()
    {
        //만약 애니메이션이 있다면 이쪽에 애니메이션 전환 코드를 작성
    }


    /// <summary> FixedUpdate()에 추가하여 반복 실행 시키는 함수 </summary>
    public override void OnFixedUpdate()
    {

    }


    /// <summary> Update()에 추가하여 반복 실행 시키는 함수 </summary>
    public override void OnUpdate()
    {
        //플레이어를 움직이게하는 함수를 실행하고
        //만약 플레이어가 키를 누르고 있으면 참, 아니면 거짓 반환
        //움직이고 있는 상태이면 이동 상태로 전환 한다.
        if (_player.Move())
            _machine.OnStateChange(_machine.RunState);
    }
}

 

 

P_RunState

플레이어가 이동 상태일때 실행되는 함수를 정리한 클래스

public class P_RunState : PlayerState
{
    public P_RunState(Player player, PlayerStateMachine machine) : base(player, machine)
    {
    }


    /// <summary> 현재 상태로 전이시 한번 실행 </summary>
    public override void OnEnter()
    {
        //만약 애니메이션이 있다면 이쪽에 애니메이션 전환 코드를 작성
    }


    /// <summary> 다음 상태로 전이시 한번 실행 </summary>
    public override void OnExit()
    {
        //만약 애니메이션이 있다면 이쪽에 애니메이션 전환 코드를 작성
    }


  /// <summary> FixedUpdate()에 추가하여 반복 실행 시키는 함수 </summary>
    public override void OnFixedUpdate()
    {

    }


  /// <summary> Update()에 추가하여 반복 실행 시키는 함수 </summary>
    public override void OnUpdate()
    {
        //플레이어를 움직이게하는 함수를 실행하고
        //만약 플레이어가 키를 누르고 있으면 참, 아니면 거짓 반환
        //움직이지 않는 상태이면 대기 상태로 전환 한다.
        if (!_player.Move())
            _machine.OnStateChange(_machine.IdleState);
    }
}

 

 

StateMachine

모든 상태 머신의 부모 클래스

public abstract class StateMachine
{
   /// <summary> FixedUpdate()에 추가하여 반복 실행 시키는 함수 </summary>
    public abstract void OnStateUpdate();

    /// <summary> Update()에 추가하여 반복 실행 시키는 함수 </summary>
    public abstract void OnStateFixedUpdate();
}

 

 

PlayerStateMachine

StateMachine를 상속받으며 플레이어의 상태 확인 및 전이시키는 역할을 하는 클래스

만약 Monster상태머신을 만들고자 한다면 아래와 비슷한 방식으로 코드를 작성하면 되겠다.

using UnityEngine;

public class PlayerStateMachine : StateMachine
{

    protected Player _player;


    //유한상태머신 변수들
    //==========================================================================

    /// <summary> 현재 상태를 저장 </summary>
    public BaseState CurrentState { get; private set; }
     
    /// <summary> 대기 상태 </summary>
    public BaseState IdleState { get; private set; }

    /// <summary> 대기 상태 </summary>
    public BaseState RunState { get; private set; }


    //==========================================================================

    public PlayerStateMachine(Player player)
    {
        _player = player;
        StateInit();
    }


    private void StateInit()
    {
        IdleState = new P_IdleState(_player, this);
        RunState = new P_RunState(_player, this);
        //아래에 추가 상태가 더 생길시 기본 세팅을 해야합니다.

        CurrentState = IdleState;
    }


	//현재 상태의 Update함수를 주기적으로 실행(PlayerClass Update()에 추가)
    public override void OnStateUpdate()
    {
        
        CurrentState.OnUpdate();
    }


	//현재 상태의 FixedUpdate함수를 주기적으로 실행(PlayerClass FixedUpdate()에 추가)
    public override void OnStateFixedUpdate()
    {
        
        CurrentState.OnFixedUpdate();
    }


    /// <summary> 현재 상태를 다른 상태로 전이하는 함수 </summary>
    public void OnStateChange(BaseState nextState)
    {
        if (CurrentState == nextState)
        {
            Debug.Log("현재 상태와 같은 상태로 전이할 수 없습니다.");
            return;
        }

        CurrentState.OnExit();
        CurrentState = nextState;
        CurrentState.OnEnter();
    }

}

 

 

Player

플레이어의 기본적인 정보를 가지고 있는 클래스

using UnityEngine;

/// <summary> 플레이어 클래스 </summary>
public class Player : MonoBehaviour
{
    [Tooltip("이동 속도")]
    [SerializeField] private float _moveSpeed;

    /// <summary> 플레이어 상태 머신 </summary>
    private PlayerStateMachine _machine;

    private void Awake()
    {
        Init();
    }


    private void Init()
    {
        _machine = new PlayerStateMachine(this);
    }


    private void Update()
    {
        //상태 머신의 Update함수를 주기적으로 실행
        _machine?.OnStateUpdate();
    }


    private void FixedUpdate()
    {
        //상태 머신의 FixedUpdate함수를 주기적으로 실행
        _machine?.OnStateFixedUpdate();
    }


    /// <summary> 키를 입력받아 오브젝트를 움직이게하는 함수 </summary>
    public bool Move()
    {
        float horizontal = Input.GetAxis("Horizontal");
        float vertical = Input.GetAxis("Vertical");
        Vector3 movePos = new Vector3(horizontal, 0, vertical);

        transform.Translate(movePos * Time.deltaTime * _moveSpeed);

        if (horizontal != 0 || vertical != 0)
            return true;

        return false;
    }

}

 

 

이상으로 유한 상태 기계에 대해 간략하게 알아봤습니다.

하지만 각각 상태들을 뚜렷하게 정의할 수 없거나 여러 상태를 가져야 하는 경우, 상태의 종류가 많아지게되면 코드의 가독성이 떨어지기 때문에 이러한 특징을 고민해보고 사용해야 할 것 같습니다.