게임 개발을 하다 보면 캐릭터의 상태를 관리해야 하는 상황이 자주 생깁니다.
플레이어가 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.");
}
}
'C#' 카테고리의 다른 글
병합 할당 연산자(??=) 이해하기 (0) | 2025.04.28 |
---|---|
Enum을 활용한 Dictionary 이해하기 (0) | 2025.04.28 |
delegate 를 더 간결하게 선언하는 방법 (0) | 2025.04.23 |
bool isValue = (조건식) : 삼항 연산자를 보다 단순하게 (0) | 2025.04.22 |
구조체를 배열로 만들면 메모리 저장 위치는 어디일까? (0) | 2025.04.22 |