오늘은 Unity 3D 심화 강의를 통해 유한 상태 머신이라는 것을 알게 되었는데, 잘 사용하면 이후 프로젝트에서 유용하게 쓸 수 있을 것 같아서 정리해보려고 한다.
FSM은 Finite-state machine의 약자로 유한한 개수의 상태를 가지며, 각 상태와 상태 간의 전환을 기반으로 동작하도록 설계하는 것이다.
FSM의 구성요소는 다음과 같다.
FSM의 핵심은 항상 하나의 상태를 가지고 있다 는 것이다. 항상 상태에 맞는 동작을 수행하고 전환 조건에 의해 상태가 전환되면 전환된 상태에 맞는 동작을 수행하기 때문에 어떤 동작을 구현할 때 어떤 상태일때 수행하는 동작인지 명확하게 정의할 수 있어서 개발에 용이하다.
플레이어의 상태를 FSM으로 구현한다고 가정하면 다음과 같다.
이 예시를 간단하게 enum형으로 상태를 정의하고 switch문을 통해 FSM을 구현해보자.
using UnityEngine; public Enum State { Idle, Move, Jump, } public class Player : Monobehaviour { private State state; void Start() { stage = State.Idle; } void Update() { switch(state) { case State.Idle: // Idle 동작 구현부 break; case State.Move: // Move 동작 구현부 break; case State.Attack: // Attack 동작 구현부 break; } } }
이런 느낌으로 간단하게 FSM을 구현할 수 있다.
하지만 switch 문으로 FSM를 구현하면 다음과 같은 단점이 존재한다.
*그래서 FSM은 State Pattern(상태패턴)을 활용해서 구현해야 한다.

위 이미지를 토대로 예시 코드를 작성해보면 다음과 같다.
public interface IState { public void Enter(); public void Exit(); public void HandleInput(); public void Update(); public void PhysicsUpdate(); } 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 PhysicsUpdate() { currentState?.PhysicsUpdate(); } }
먼저 가장 기초가 되는 StateMachine 이라는 추상클래스와 IState 인터페이스를 만든다. StateMachine 클래스는 상태에 따른 처리를 하거나 상태를 전환 시키는 데 사용하고, IState는 각 상태를 구현하기 위해 필요한 것들을 모아놓은 것이다.
IState의 함수들에 대해 간단하게 설명하면 다음과 같다.
이제 StateMachine을 상속받아 PlayerStateMachine을 만들고, PlayerStateMacine에서 Player의 상태를 관리해주면 된다.
using UnityEngine; public class PlayerStateMachine : StateMachine { public Player Player { get; } public Vector2 MovementInput { get; set; } public float MovementSpeed { get; private set; } public float RotationDamping { get; private set; } public float MovementSpeedModifier { get; set; } = 1f; public float JumpForce { get; set; } public bool IsAttacking { get; set; } public int ComboIndex { get; set; } public Transform MainCamTransform { get; set; } public PlayerIdleState IdleState { get; private set; } public PlayerWalkState WalkState { get; private set; } public PlayerRunState RunState { get; private set; } public PlayerJumpState JumpState { get; private set; } public PlayerFallState FallState { get; private set; } public PlayerComboAttackState ComboAttackState { get; private set; } public PlayerStateMachine(Player player) { this.Player = player; MainCamTransform = Camera.main.transform; IdleState = new PlayerIdleState(this); WalkState = new PlayerWalkState(this); RunState = new PlayerRunState(this); JumpState = new PlayerJumpState(this); FallState = new PlayerFallState(this); ComboAttackState = new PlayerComboAttackState(this); MovementSpeed = player.Data.GroundData.BaseSpeed; RotationDamping = player.Data.GroundData.BaseRotationDamping; } }
PlayerStateMachine의 코드는 플레이어를 조작하기 위해 필요한 변수들과 그것을 초기화하는 내용이므로 설명은 생략한다.
중간 부분을 건너 뛰고 간단하게 PlayerGroundState에 속해있는 PlayerIdleState, PlayerWalkState, PlayerRunState들만 보면 다음과 같다.
// PlayerIdleState.cs using UnityEngine; public class PlayerIdleState : PlayerGroundState { public PlayerIdleState(PlayerStateMachine stateMachine) : base(stateMachine) { } public override void Enter() { base.Enter(); // 구현부 } public override void Exit() { base.Exit(); // 구현부 } public override void Update() { base.Update(); // 구현부 } } // PlayerWalkState.cs using UnityEngine.InputSystem; public class PlayerWalkState : PlayerGroundState { public PlayerWalkState(PlayerStateMachine stateMachine) : base(stateMachine) { } public override void Enter() { base.Enter(); // 구현부 } public override void Exit() { base.Exit(); // 구현부 } protected override void OnRundStarted(InputAction.CallbackContext context) { base.OnRundStarted(context); stateMachine.ChangeState(stateMachine.RunState); } } // PlayerRunState.cs using UnityEngine; public class PlayerRunState : PlayerGroundState { public PlayerRunState(PlayerStateMachine stateMachine) : base(stateMachine) { } public override void Enter() { base.Enter(); // 구현부 } public override void Exit() { base.Exit(); // 구현부 } }
이런 느낌으로 각 상태에 따라 스크립트(클래스)를 나눠서 각 상태에 따라 어떤 동작을 해야되는 지 구현하면 된다.
이렇게 상태패턴을 활용해서 FSM을 구현하는 이유는 다음과 같은 장점이 있기 때문이다.
FSM이 항상 옳은 것은 아니다. 각각의 상태를 뚜렷하게 정의할 수 없거나 상태의 종류가 매우 많아지면 가독성이 떨어지며 디버깅이 어려워진다는 단점을 가지고 있다.
따라서 FSM은 상태의 수가 적고 상태별 동작이 명확하게 구별될 때 가장 빛을 발한다. 무작정 사용하지 말고 이런 장단점을 명확하게 인지하고 상황에 맞게 사용해보자.
참고 링크 : https://dodobug.tistory.com/16