유니티에서 상태(State) 패턴을 제대로 활용해보기

2025. 3. 29. 10:12유니티 unity/디자인패턴

유니티로 게임을 만들다 보면, 반드시 마주하게 되는 구조가 하나 있습니다. 바로 상태에 따라 행동이 달라지는 오브젝트입니다.

예를 들어 적 캐릭터는 대기하다가, 플레이어가 가까워지면 추적하고, 일정 거리 이내로 다가가면 공격하다가, 체력이 다 떨어지면 죽습니다. UI도 마찬가지예요. 메인 메뉴 → 설정 화면 → 인게임 → 일시정지 → 게임 오버까지 전환이 이루어지죠.

이 모든 흐름의 공통점은 상태에 따라 행동이 바뀐다는 점입니다.

처음엔 대부분 이렇게 코드를 짭니다:

void Update()
{
    if (isDead)
        PlayDeathAnimation();
    else if (isAttacking)
        Attack();
    else if (isChasing)
        ChasePlayer();
    else
        Idle();
}
 

괜찮아 보이죠. 그런데 상태가 늘어나고 조건이 꼬이기 시작하면?
바로 조건문 지옥이 펼쳐집니다.
이럴 때 필요한 게 바로 **상태 패턴(State Pattern)**입니다.


상태 패턴이란?

상태 패턴은 말 그대로, 객체의 상태를 클래스로 분리해서 관리하는 디자인 패턴입니다. 상태마다 클래스를 만들고, 그 안에 해당 상태의 로직을 넣습니다. 그리고 상태 전환은 명확하게 하나의 관리자가 처리합니다.

즉, 상태 = 객체화된 로직 단위입니다.

이렇게 하면 다음과 같은 이점이 생깁니다:

  • ✅ 상태별 로직이 한 곳에 모여 있어서 가독성이 좋아지고
  • ✅ 새로운 상태를 추가할 때 기존 코드 수정 없이 확장 가능하고
  • ✅ 각 상태를 독립적으로 테스트하거나 유지보수하기도 쉬워집니다.

왜 유니티에서 특히 잘 맞을까?

유니티는 프레임 단위로 Update가 도는 구조고, 게임 오브젝트마다 동작이 나뉘기 때문에 상태 기반 로직이 매우 흔하게 등장합니다. 그런데 MonoBehaviour 기반으로 작성하다 보면 로직이 여기저기 흩어져서, 유지보수가 매우 힘들어집니다.

예를 들어, 적 캐릭터가 추적하다가 공격하거나 도망치는 등의 로직이 섞여 있으면 각 조건을 추적하기도 어렵고, 나중에 새로운 상태를 추가하는 것도 부담스럽습니다.

 

핵심 개념은 이렇습니다:

  • StateMachine<T>: 상태를 관리하는 컨트롤러
  • FsmState<T>: 상태의 추상 클래스
  • IdleState, ChaseState, AttackState 등: 상태를 구현한 클래스들
  • FsmMessage: 상태 간 전달할 수 있는 메시지 구조체

예제 코드로 살펴보기

1. 상태 메시지 구조

public struct FsmMessage
{
    public int Type;
    public FsmMessage(int type) { Type = type; }
}

2. 상태 추상 클래스

public abstract class FsmState<T> where T : Enum
{
    public T StateType { get; private set; }

    protected FsmState(T stateType)
    {
        StateType = stateType;
    }

    public virtual void OnEnter(T fromState, FsmMessage msg) { }
    public virtual void OnUpdate(float deltaTime) { }
    public virtual void OnExit(T toState) { }
    public virtual void OnMessage(FsmMessage msg) { }
}
 

3. 상태 머신 클래스

public class StateMachine<T> where T : Enum
{
    private Dictionary<T, FsmState<T>> _states = new();
    private FsmState<T> _currentState;

    public void AddState(FsmState<T> state)
    {
        _states[state.StateType] = state;
    }

    public void ChangeState(T newState, FsmMessage msg = default)
    {
        _currentState?.OnExit(newState);
        _currentState = _states[newState];
        _currentState.OnEnter(newState, msg);
    }

    public void Update(float deltaTime)
    {
        _currentState?.OnUpdate(deltaTime);
    }
}
 

상태 구현 예시: Idle, Chase

 
public enum EnemyState
{
    Idle,
    Chase
}
 
 
public class IdleState : FsmState<EnemyState>
{
    private Enemy enemy;

    public IdleState(Enemy enemy) : base(EnemyState.Idle)
    {
        this.enemy = enemy;
    }

    public override void OnEnter(EnemyState from, FsmMessage msg)
    {
        Debug.Log("Idle 상태 진입");
    }

    public override void OnUpdate(float deltaTime)
    {
        if (enemy.IsPlayerInRange())
            enemy.FSM.ChangeState(EnemyState.Chase);
    }
}
 
 
public class ChaseState : FsmState<EnemyState>
{
    private Enemy enemy;

    public ChaseState(Enemy enemy) : base(EnemyState.Chase)
    {
        this.enemy = enemy;
    }

    public override void OnEnter(EnemyState from, FsmMessage msg)
    {
        Debug.Log("Chase 상태 진입");
    }

    public override void OnUpdate(float deltaTime)
    {
        enemy.MoveTowardsPlayer();
        if (!enemy.IsPlayerInRange())
            enemy.FSM.ChangeState(EnemyState.Idle);
    }
}

이렇게 바뀝니다

Before

  • 상태가 늘어날수록 if, else if, switch가 늘어남
  • 코드가 중복되고 수정이 어려움
  • 테스트 불가능

After (상태 패턴 적용)

  • 각 상태가 독립적으로 존재
  • 상태 간 전환 명확
  • 테스트 가능 / 확장 용이

 

상태 패턴은 단순히 “코드를 예쁘게 만들자”는 것이 아닙니다.
“유지보수 가능한 구조를 만들자”,
**“상태가 많아질수록 더 유리한 구조로 가자”**라는 철학에 가깝습니다.

이 패턴은 특히 유니티처럼 상태 기반 동작이 잦은 환경에 정말 잘 어울립니다.
직접 상태 클래스를 나눠보면 생각보다 훨씬 깔끔하고,
“아 이래서 다들 상태 패턴을 쓰는구나” 하는 감이 오게 됩니다.