본문 바로가기
C#

대리자를 이용한 FSM 만들기

by Oz Driver 2025. 4. 24.

 

게임 개발을 하다 보면 캐릭터의 상태를 관리해야 하는 상황이 자주 생깁니다.

플레이어가 Idle → Walk → Die 같은 상태를 오가며 행동해야 할 때, 우리는 흔히 FSM (Finite State Machine) 을 사용하게 됩니다.

처음에는 대부분 switch-case 방식으로 구현하게 됩니다. 상태 수가 적고, 단순히 Update()만 필요할 때는 이 방식이 가장 쉽고 직관적입니다. 하지만 상태가 점점 늘어나고, 각 상태마다 진입(Enter), 수행(Update), 종료(Exit) 같은 단계가 나뉘기 시작하면 코드는 금세 복잡해지고, 유지보수가 어려워집니다.

이럴 때 대리자(delegate)를 활용하면 FSM 구조를 더 유연하고 간결하게 만들 수 있습니다.

 

FSM 구조, 이렇게 구성합니다

이번 예제에서는 다음과 같은 방식으로 FSM을 구성했습니다.

* 상태마다 Enter / Update / Exit 동작을 갖는 DelegateState 클래스를 만들고,

* FSM 클래스는 현재 상태만 추적하며 해당 동작을 실행합니다.

* Player 클래스에서는 실제 상태를 정의하고 각 행동을 함수로 연결합니다.

* enum을 사용해 명확하게 상태를 구분하고 switch-case 없이 상태 객체만 넘겨줍니다.

* 이 구조의 가장 큰 장점은 상태가 많아져도 전체 코드 구조는 거의 변하지 않는다는 것입니다.

 

FSM 구현 방식별 비교

FSM을 구현하는 방법은 여러 가지가 있고, 각 방식마다 장단점이 분명합니다. 상황에 따라 어떤 방식을 쓸지 선택하는 것이 중요합니다.

switch ~ case

상태 전이에 따른 Update() 분기만 필요한 단순 FSM에 적합합니다. 빠르게 구현할 수 있으며 상태 수가 적을 때 가장 직관적입니다.

delegate 기반 FSM

상태마다 진입, 수행, 종료 단계가 나뉘는 경우에 적합합니다. switch-case보다 유연하고, 상태별로 여러 단계의 처리를 깔끔하게 구성할 수 있습니다.

IState 기반 FSM

각 상태의 처리 로직이 많고 복잡한 경우, 상태를 별도의 클래스로 분리하는 방식입니다. IState 인터페이스를 만들고 상태별 클래스를 따로 구성하면 코드 구조가 명확해지고, 확장 시에도 각 상태를 독립적으로 관리할 수 있어 안정성이 높습니다. 다만 구조가 커지고 코드량이 많아지므로 간단한 FSM에는 부담이 될 수 있습니다. 이 방식은 다음에 기회가 된다면 따로 정리해보겠습니다.

 

마무리

FSM 구현 방식은 딱 떨어지는 정답이 있는 것이 아니라, 상태 수와 상태의 복잡도에 따라 선택이 달라져야 합니다.

* 상태 수가 적고 단순한 Update()만 필요하다면 → switch-case

* 각 상태가 Enter / Update / Exit으로 나뉘기 시작한다면 → delegate 기반 FSM

* 상태별 로직이 복잡하고 구조적 관리가 필요하다면 → IState 패턴

그리고 한 가지 기준을 추가하자면,

상태 수가 5개 이상이 되거나, 상태마다 단계가 나뉘기 시작하면 switch-case 구조는 빠르게 한계에 다다릅니다.

그 시점이 바로, FSM 구조화를 고민해볼 타이밍입니다. 그리고 오늘 소개한 delegate 방식은 그 첫걸음으로 정말 좋은 선택이 될 수 있습니다.

 

예제 코드

#nullable enable
using System;
using UnityEngine;

public class DelegateState
{
    public Action? Enter;
    public Action? Update;
    public Action? Exit;
}

public class FSM
{
    private DelegateState? currentState;

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

    public void ChangeState(DelegateState? newState)
    {
        if( newState == null )
        {
            Debug.LogWarning("[FSM] Attempted to change to a null state.");
            return;
        }

        if(currentState == newState)
        {
            Debug.Log("[FSM] Already in the requested state. Skipping.");
            return;
        }

        currentState?.Exit?.Invoke();
        currentState = newState;
        currentState?.Enter?.Invoke();
    }
}

public enum PlayerState
{
    Idle,
    Walk,
    Die
}

public class Player : MonoBehaviour
{
    private FSM fsmController = new FSM();
    private PlayerState currentState;

    private DelegateState idleState;
    private DelegateState walkState;
    private DelegateState dieState;

    // 게임 시작 전 상태들을 초기화합니다.
    private void Awake()
    {
        InitializeStates();
    }

    // 초기 상태를 idleState로 설정합니다.
    private void Start()
    {
        ChangeState(PlayerState.Idle);
    }

    // 매 프레임마다 현재 상태의 Update 함수를 실행합니다.
    private void Update()
    {
        fsmController.Update();
    }

    // 상태 인스턴스를 생성하고 각 상태별 함수들을 연결합니다.
    private void InitializeStates()
    {
        // Idle 상태 객체 생성 및 함수 연결
        idleState = new DelegateState();
        idleState.Enter += OnEnterIdle;
        idleState.Update += OnUpdateIdle;
        idleState.Exit += OnExitIdle;

        // Walk 상태 객체 생성 및 함수 연결
        walkState = new DelegateState();
        walkState.Enter += OnEnterWalk;
        walkState.Update += OnUpdateWalk;
        walkState.Exit += OnExitWalk;

        // Die 상태 객체 생성 및 함수 연결
        dieState = new DelegateState();
        dieState.Enter += OnEnterDie;
        dieState.Update += OnUpdateDie;
        dieState.Exit += OnExitDie;
    }

    // 상태 변경을 처리하는 함수
    private void ChangeState(PlayerState newState)
    {
        currentState = newState;

        switch (newState)
        {
            case PlayerState.Idle:
                fsmController.ChangeState(idleState);
                break;
            case PlayerState.Walk:
                fsmController.ChangeState(walkState);
                break;
            case PlayerState.Die:
                fsmController.ChangeState(dieState);
                break;
        }
    }

    // Idle State
    private void OnEnterIdle()
    {
        // 플레이어가 Idle 상태에 진입했습니다.
        Debug.Log("[Idle] Player has entered idle state.");
    }

    private void OnUpdateIdle()
    {
        // 플레이어가 Idle 상태입니다.
        Debug.Log("[Idle] Player is idling...");
    }

    private void OnExitIdle()
    {
        // 플레이어가 Idle 상태를 종료합니다.
        Debug.Log("[Idle] Player is leaving idle state.");
    }

    // Walk State
    private void OnEnterWalk()
    {
        // 플레이어가 Walk 상태에 진입했습니다.
        Debug.Log("[Walk] Player has started walking.");
    }

    private void OnUpdateWalk()
    {
        // 플레이어가 Walk 상태입니다.
        Debug.Log("[Walk] Player is walking...");
    }

    private void OnExitWalk()
    {
        // 플레이어가 Walk 상태를 종료합니다.
        Debug.Log("[Walk] Player has stopped walking.");
    }

    // Die State
    private void OnEnterDie()
    {
        // 플레이어가 Die 상태에 진입했습니다.
        Debug.Log("[Die] Player has died.");
    }

    private void OnUpdateDie()
    {
        // 플레이어가 Die 상태입니다.
        Debug.Log("[Die] Player remains dead.");
    }

    private void OnExitDie()
    {
        // 플레이어가 Die 상태를 종료합니다.
        Debug.Log("[Die] Player should not be exiting die state.");
    }
}