[Unity] 유한 상태 머신 (FSM, finite-state-machine)

a-a·2025년 4월 26일

알쓸신잡

목록 보기
22/26
post-thumbnail

이 문서는 FSM 머신 패턴을 사용하다가 생긴 문제점을 해결함을 기록한 회고록에 가깝습니다.

⭐FSM이란?

상태(State)를 기반으로 오브젝트의 동작을 제어하며, 특정 트리거(Trigger)나 조건(Condition)에 의해 명확하게 정의된 상태 간 전이(State Transition)가 발생함.

장점

  1. 상태 기반 제어 흐름의 명확성 확보
    각 상태는 독립적인 책임을 가지며, 전이 조건과 함께 관리됨으로써
    게임 오브젝트의 행위 흐름을 구조적으로 이해하고 디버깅하기 용이함.

  2. 복잡도 분산 및 코드 유지보수성 향상
    상태별 로직을 분리함으로써 거대하고 모놀리식한 조건 분기를 제거하고,
    유지보수 및 확장 시 변경 범위를 최소화할 수 있음.

  3. 유기적인 동작 조합을 통한 행위 다양성 구현
    전이 조건과 상태 간 상호작용을 기반으로 복잡한 게임 플레이 흐름을 유연하게 설계할 수 있으며,
    상태 재사용이 가능하여 설계 효율 또한 높아짐.

작동방식 설명

FSM의 작동 FlowChart를 간단하게 그려보았다.

Attack State은 공격 기능만, Move State는 객체의 이동만 구현하면 되니, 만약 어떤 지점에서 문제가 생긴다면 해당 State만 수정하면 된다.

위 도식에서는 외부에서의 트리거로 Die 상태가 된다.
이러면 뭔가 스크립트가 꼬이지 않을까?

각 State는, Enter(), Update(), Exit()를 반드시 구현해야 한다.
(Update()는 실질적으로 상태의 '동작'을 담은 함수이다.)
SetState()라는 함수로 상태를 설정하게 된다. 이때 Enter()~Exit()를 모두 거치게 되므로, 모든 상태가 정상적으로 종료됨이 보장된다.

만약 Attack State에서 Die State로 SetState()될 경우 반드시 Exit()를 거치므로 안전하다.

⭐구현부

😎 클래스 다이어그램

🧩FSM 관련 기본 인터페이스 및 클래스 IState.cs

// 상태 인터페이스 정의 - 상태 전이 및 상태 동작 메서드를 명시
public interface IState<TEnum, TOwner>
{
    TEnum StateType { get; } // 상태 식별용 Enum
    void Enter(TOwner owner);  // 상태 진입 시 호출
    void Exit(TOwner owner);   // 상태 종료 시 호출
    void Update(TOwner owner); // 상태 중 로직 (매 프레임 등)
}

⚙️ 상태 기계 클래스 StateMachine.cs

using System.Collections.Generic;

public class StateMachine<TEnum, TOwner>
{
    private Dictionary<TEnum, IState<TEnum, TOwner>> _states = new();
    private IState<TEnum, TOwner> _currentState;
    private TOwner _owner;

    public TEnum CurrentStateType => _currentState.StateType;

    public StateMachine(TOwner owner)
    {
        _owner = owner;
    }

    // 상태 등록
    public void AddState(IState<TEnum, TOwner> state)
    {
        if (!_states.ContainsKey(state.StateType))
            _states.Add(state.StateType, state);
    }

    // 상태 전이
    public void ChangeState(TEnum newStateType)
    {
        if (_states.TryGetValue(newStateType, out var newState))
        {
            _currentState?.Exit(_owner);
            _currentState = newState;
            _currentState.Enter(_owner);
        }
    }

    // 매 프레임 호출 등에서 상태 업데이트
    public void Update()
    {
        _currentState?.Update(_owner);
    }
}

🎯상태 Enum 정의, Enum.cs

public enum EnemyStateType
{
    Idle,
    Chase,
    Attack,
    Die
}

🧠 실제 상태 클래스 예시 (Idle 상태)

public class EnemyIdleState : IState<EnemyStateType, EnemyFSM>
{
    public EnemyStateType StateType => EnemyStateType.Idle;

    public void Enter(EnemyFSM enemy)
    {
        // Idle 상태 진입 시 초기화 로직
        UnityEngine.Debug.Log("Enter Idle");
    }

    public void Exit(EnemyFSM enemy)
    {
        // Idle 상태 종료 시 처리
        UnityEngine.Debug.Log("Exit Idle");
    }

    public void Update(EnemyFSM enemy)
    {
        // Idle 중 처리 (예: 시야 감지)
    }
}

🧬 FSM Owner 클래스 EnemyFSM.cs

public class EnemyFSM : UnityEngine.MonoBehaviour
{
    private StateMachine<EnemyStateType, EnemyFSM> _stateMachine;

    private void Awake()
    {
        _stateMachine = new StateMachine<EnemyStateType, EnemyFSM>(this);

        // 상태 등록
        _stateMachine.AddState(new EnemyIdleState());
        _stateMachine.AddState(new EnemyChaseState());
        _stateMachine.AddState(new EnemyAttackState());

        // 초기 상태 설정
        _stateMachine.ChangeState(EnemyStateType.Idle);
    }

    private void Update()
    {
        _stateMachine.Update();
    }

    public void SetState(EnemyStateType state)
    {
        _stateMachine.ChangeState(state);
    }
}

😊마무리 회고

기존의 상태 시스템은 설계 초기에 본체 클래스에 대한 직접 접근을 제한하여 응집도를 높이려는 의도에서 출발했습니다.
그러나 이로 인해 각 상태 클래스의 생성자가 점점 복잡해졌고, 상태 간 전이 또한 일관성이 부족해지며 전이 유효성 검증을 테스트 코드에 의존하는 비효율적인 구조로 발전하게 되었습니다.
결과적으로 디버깅 및 유지보수 비용이 증가하고, 시스템 신뢰성이 떨어지는 문제를 직면하게 되었습니다.

이를 해결하기 위해 제네릭 기반의 FSM 구조로 리팩토링하면서,

  • StateMachine<TEnum, TOwner>를 통한 타입 안정성과 재사용성 확보,
  • Owner에 대한 직접 참조를 허용함으로써 상태 구현의 간결화,
  • Dictionary 기반 상태 등록으로 전이의 명시성과 안전성 강화
    등의 효과를 얻을 수 있었습니다.

이번 개선 과정을 통해, 설계 철학이 지나치게 엄격하거나 이상적일 경우 오히려 유지보수성과 실용성을 해칠 수 있음을 체감했습니다. 궁극적으로는 구조적 완성도와 실제 운영 효율성 사이의 균형을 맞추는 것이 클라이언트 아키텍처 설계에 있어 가장 중요한 요소임을 다시금 확인할 수 있었습니다.

profile
"게임 개발자가 되고 싶어요."를 이뤄버린 주니어 0년차

2개의 댓글