공용 스크립트 제작기🎈 - 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로 도배된 상태머신은 이제 안녕.