본문 바로가기

프로그래밍/Unity

Unity StateMachine으로 FSM 관리

이번 시간에는 StateMachine에 대해 알아보고 FSM에 대해 이해해보는 시간을 가지겠다.

 

 

 

 

 

내가 구현하고 싶었던 기능은 캐릭터가 상태에 따라 다른 동작을 취하게 하는 것이였다.

평소 같았으면 enum 타입을 사용하여 처리를 할 수 있지만, 이 프로젝트 내에서는 다양한 기믹이 존재하기 때문에

여러 상태가 필요한 케이스였다. 따라서 이런 다양한 상태에 따른 처리를 도와줄 수 있는 클래스가 필요해보였다.

 

 

 

1. 상태 기계 (State Machnie, FSM)

 

상태 기계(StateMachine)이라기도 불리고 "유한 상태 기계(Finite State Machine, FSM)"라 불리는 이 것은 유한한 상태 개수 중 오르지 하나의 상태에만 있게 하는 계산 모델이다.

 

 

 

FSM을 정의하는 세가지 요소는 다음과 같다.

 

1. 상태(State)

 ⦁ 개체가 현재 처해 있는 조건이나 수행하고 있는 행동을 나타냅니다.

 ⦁ 개체는 특정 시점에 오직 하나의 상태에만 있을 수 있습니다(배타성).

 ⦁ 예시 (게임 캐릭터): Idle (대기), Moving (이동), Jumping (점프), Attacking (공격).

 

2. 이벤트/조건(Event/Condition)

 ⦁ 상태 전환(Transition)을 유발하는 외부 또는 내부의 입력입니다.

 ⦁ 예시: 키 입력, 타이머 만료, 충돌 발생, 체력 감소.

 

3. 전환(Transitions)

 ⦁ 특정 이벤트나 조건이 충족될 때 한 상태에서 다른 상태로 넘어가는 과정입니다.

 ⦁ 예시: Idle 상태에서 '이동 키 입력'이라는 이벤트가 발생하면 Moving 상태로 전환합니다.

 

 

 

2. StateMachine 적용

 

 

내가 관리할 State들은 다음과 같다. 프로젝트를 진행하면서 더 늘어나겠지만 일단 5가지의 상태를 가지고 처리를 하고 싶었다.

 

 

public interface IState 
{
    // FSM에서 관리하려는 상태 인터페이스
    public void Enter();
    public void Do();
    public void Exit();
}

 

우선 State 속성을 나타낼 인터페이스를 만들고 각각 상태 진입/지속/탈출 했을 때 기능을 구현할 수 있도록 설계를 하였다.

 

 

public class StateMachine : MonoBehaviour
{
    // Player를 FSM을 관리하는 스크립트

    private IState curState;        // 현재 상태
    public IState CurState { get { return curState; } set { curState = value; } }

    public void Init(IState initState)
    {
        // 초기 상태 결정
        curState = initState;
        curState.Enter();
    }

    public void ChangeState(IState newState)
    {
        // 상태 변경

        if(curState != null)
        {
            // 이전 상태가 있다면 Exit 실행
            curState.Exit();
        }

        // 새로운 상태 변경 및 Enter 호출
        curState = newState;
        curState.Enter();
    }

    public void Update()
    {
        // Update에서 Do 실행
        curState.Do();
    }
}

 

StateMachine에서는 curState 변수를 통해 하나의 상태에만 동작할 수 있게 설정을 해주고 ChangeState 변경 시 이전 State의 Exit를 호출하고 새로운 State의 Enter를 호출하여 FSM 클래스를 만들게 되었다.

 

 

 

3. 상태 전환 시 규약? FSM의 관리 스케일

 

 

하지만 2가지 문제가 있었다.

 

첫번째는 나중에 Buff 같은 독립적으로 수행하는 것들도 FSM으로 관리를 해야하는가에 대한 고민이였고

두번째는 무분별하게 전환(Transition)이 이뤄지면 (점프 -> 달리기) 상태로 아무런 규약없이 처리 되는 거에 대한 고민이였다.

 

 

#1. State에 대한 정의

 

StateMachine을 사용할 때에는 State의 정의가 가장 중요하였다.

 

Player를 예를 들면 기본(Idle) / 이동(Move) / 죽음(Die) 같은 상태가 직관적이지만, 이 외에 Buff 걸린 상태를 정의하려면 이전 3가지 상태와 어떤 상관관계가 있는지 판별을 해야 한다. 왜냐하면 Buff 걸린 상태로 이동할 수 있기 때문에 하나의 상태로만 존재할 수 없는 경우가 있기 때문이다.

 

따라서 기본 5가지의 상태 (Idle, Run, Jump, JumpingPad, Climb) 모두 플레이어 움직임에 관련된 상태로 상관관계를 묶고 버프는 플레이의 Condition이나 State에 관계하기 때문에 독립적 요소라고 판단하고 State에서 배제하였다.

 

 

#2. 전환(Transition) 규약

 

Shift 누르면 Run 상태, Space 누르면 Jump 상태가 되게 전환한다고 할때 Space->Shift로 누르면 공중에서 Run 상태가 되버리게 된다. 따라서 Jump 상태 중일 때는 Run 상태가 되면 안된다. 따라서 이런 상태 전환에 대한 규약이 필요하였다.

 

 

    // 전환 규약을 나타내는 Dictionary
    private Dictionary<IState, List<IState>> allowedTransitions;
    
    public void MakeTransitionRule(IState fromState, IState toState)
    {
        if (!allowedTransitions.ContainsKey(fromState))
        {
            // 새로운 Transition이면 새로 생성
            allowedTransitions[fromState] = new List<IState>();
        }
        allowedTransitions[fromState].Add(toState);
    }

 

따라서 Animation BlenTree처럼 Transition 규약을 만들 수 있는 메서드를 다음과 같이 작성하였다.

 

 

    private void SetUpTransition()
    {
        stateMachine.MakeTransitionRule(IdleState, MoveState);
        stateMachine.MakeTransitionRule(IdleState, RunState);
        stateMachine.MakeTransitionRule(IdleState, JumpState);
        stateMachine.MakeTransitionRule(IdleState, JumpingPadState);

        stateMachine.MakeTransitionRule(RunState, JumpState);
        stateMachine.MakeTransitionRule(RunState, JumpingPadState);

        stateMachine.MakeTransitionRule(JumpState, IdleState);
        stateMachine.MakeTransitionRule(JumpState, JumpingPadState);

        stateMachine.MakeTransitionRule(JumpingPadState, IdleState);
    }

 

위 그림과 같이 Jump 상태에서는 Run 상태로 전환하는 Transition 줄이 없게 구현하였다.

 

    public void ChangeState(IState newState)
    {
        // 상태 변경
        if (curState != null &&
            allowedTransitions.ContainsKey(curState) &&
            !allowedTransitions[curState].Contains(newState))
        {
            Debug.Log($"규약 위반: {curState}에서 {newState}으로 전환 불가");
            return; 
        }

        if (curState != null)
        {
            // 이전 상태가 있다면 Exit 실행
            curState.Exit();
        }

        // 새로운 상태 변경 및 Enter 호출
        curState = newState;
        curState.Enter();
    }

 

마지막으로 ChangeState에서 규약 검사만 하면 상태 전환 시 불필요한 예외처리를 할 수 있게 된다.