공용 스크립트 제작기🎈 - StateMachine 편

공용 스크립트 제작기🎈 - StateMachine 편

Unity 프로젝트의 공용으로 사용할 수 있는 스크립트를 제작하면서 어떻게 만들었는지, 어떤 점에 신경썼는지 설명하는 글입니다.

원래 그냥 간단한 Enum-State문의 StateMachine을 버릇처럼 사용하다가, 이제는 Generic한 상태머신을 만들어야겠다는 생각이 들어서 작성해본다. 이전에 만들었던 GameFlowManager를 활용하는 면이 있으니 이전 편을 읽고오면 좋다.

💡예시

간단하게 먼저 사용 예시부터 소개해보자면 다음과 같다.

먼저 컨텍스트를 정의한다. 컨텍스트 안에는 상태가 사용할 데이터를 담는다.

public class ExampleContext
{
    public int Value { get; set; }
}

State는 이렇게 만든다.

public class State : IState<ExampleContext>
{
    public void Enter(ExampleContext context)
    {
        Debug.Log("State 진입");
        context.Value = 2;
    }

    public void Update(ExampleContext context, float deltaTime)
    {
        // 업데이트 로직
    }

    public void Exit(ExampleContext context)
    {
        Debug.Log("State 종료");
    }
}

StateMachine은 이렇게 사용한다.

public class StateMachineExample : MonoBehaviour
{
    private StateMachine<ExampleContext> stateMachine;

    private void Awake()
    {
        var context = new ExampleContext { Value = 0 };
        stateMachine = new StateMachine<ExampleContext>(context);
        stateMachine.ChangeState(new State());
    }

    private void OnDestroy()
    {
        stateMachine?.Dispose();
    }
}

📊 클래스 다이어그램

IUpdatable (GameFlowManager와 통합)
    ↑
    │ implements
    │
StateMachine<TContext>
    │
    ├─ TContext (참조)
    │   └─ 상태가 조작할 데이터
    │
    ├─ IState<TContext> currentState
    ├─ IState<TContext> previousState
    │
    ├─ StateTransitionValidator (델리게이트)
    └─ OnStateChanged (이벤트)

IState<TContext>
    ↑
    │ implements
    │
    ├─ IdleState
    ├─ WalkState
    ├─ AttackState
    └─ ...

📁 시스템 아키텍처

핵심 컴포넌트

IState 인터페이스

Generic 상태 인터페이스로, 모든 상태가 구현해야 하는 생명주기 메서드를 정의한다.

public interface IState<TContext> where TContext : class
{
    void Enter(TContext context);   // 상태 진입 시
    void Update(TContext context, float deltaTime);  // 매 프레임
    void Exit(TContext context);    // 상태 종료 시
}
StateMachine 클래스

상태 머신의 핵심 로직을 담당하며, IUpdatable을 구현하여 GameFlowManager와 자동 통합된다.

  • Context 관리: 상태가 조작할 데이터 객체 보관
  • 상태 전환: ChangeState(), RevertToPreviousState()
  • 상태 검증: StateTransitionValidator 델리게이트로 잘못된 전환 방지
  • 이벤트: OnStateChanged로 상태 변화 추적
  • 생명주기: GameFlowManager 자동 등록/해제
Context 패턴

상태가 조작할 데이터를 Context 객체로 분리한다.

  • 상태는 Context만 알면 되고, 나머지는 몰라도 됨
  • 상태마다 필요한 데이터를 Context로 전달
  • class 타입만 사용 가능 (where TContext : class 제약)

🔄 데이터 흐름

  • StateMachine 생성 시 Context 전달
  • 생성자에서 GameFlowManager에 자동 등록
  • ChangeState() 호출 → 상태 전환
    • TransitionValidator 검증 (있으면)
    • currentState.Exit()
    • previousState 저장
    • currentState 교체
    • newState.Enter()
    • OnStateChanged 이벤트 발생
  • GameFlowManager가 매 프레임 OnUpdate() 호출
    • currentState.Update(context, deltaTime) 실행
  • Dispose() 호출 → GameFlowManager에서 해제
  • 소멸

🎯 신경 쓴 부분

GameFlowManager 자동 통합

StateMachine은 IUpdatable을 구현하여 GameFlowManager에 자동으로 등록된다. 따라서 MonoBehaviour의 Update()에서 직접 호출할 필요가 없다.

public StateMachine(TContext context)
{
    this.context = context;
    // 생성과 동시에 자동 등록!
    GameFlowManager.Instance.RegisterUpdatable(this);
}
상태 전환 검증 시스템

TransitionValidator를 통해 잘못된 상태 전환을 방지한다.

stateMachine.TransitionValidator = (from, to) =>
{
    // 체력이 0 이하면 죽음 상태로만 전환 가능
    if (context.Health <= 0 && !(to is DeadState))
    {
        Debug.LogWarning("체력이 0입니다. 죽음 상태로만 전환 가능합니다.");
        return false;
    }
    return true;
};
이전 상태 추적

previousState를 저장하여 RevertToPreviousState()로 되돌릴 수 있다.

// 피격 후 원래 상태로 복귀
public class HitState : IState<PlayerContext>
{
    public void Update(PlayerContext context, float deltaTime)
    {
        if (IsHitAnimationFinished())
        {
            stateMachine.RevertToPreviousState();  // 이전 상태로!
        }
    }
}
메모리 누수 방지

Dispose 패턴을 구현하여 명확한 리소스 해제를 보장한다. Dispose()를 호출하지 않으면 소멸자에서 경고 메시지를 출력한다.

~StateMachine()
{
    if (!isDisposed)
    {
        Debug.LogWarning($"[메모리 누수 경고] StateMachine<{typeof(TContext).Name}>이(가) Dispose()되지 않고 소멸되었습니다.");
    }
}

결론

Switch-Case로 도배된 상태머신은 이제 안녕.


© 2022. All rights reserved.