Abstract
FSM 은 if (isDead) ... else if (isStunned) ... else if (isChasing) ... 로 불어나는 Update 를 상태별 클래스로 쪼개는 도구입니다. "지금 무엇을 하고 있는가"를 클래스 하나가 책임지고, 다른 상태의 코드는 아예 실행되지 않습니다. 프레임워크의 Procedure(게임 흐름)도 내부적으로는 이 FSM 하나입니다.
실무에서 필요한 순간
•
캐릭터 스크립트의 Update 에 bool 플래그가 세 개를 넘어가고, "죽었는데 공격이 나간다" 같은 버그가 생기기 시작할 때
•
상태마다 "들어올 때 한 번 / 매 프레임 / 나갈 때 한 번" 할 일이 뚜렷할 때 (애니메이션 시작, 타이머, 정리)
•
게임 전체 흐름(로딩 → 메뉴 → 플레이 → 결과)을 나눌 때 → 이건 Procedure 가 이미 해 준다
ToyboxNightmare 에서의 실제 사용 — 적의 네 상태
stateDiagram-v2
[*] --> Chase
Chase --> Idle: 플레이어 없음 / 사망
Idle --> Chase: 플레이어 다시 등장
Chase --> Dying: 체력 0
Idle --> Dying: 체력 0
Dying --> Sinking: 침하 시작 신호 (애니메이션 이벤트)
Sinking --> [*]: 연출 시간 끝 → 회수(SafeHide)Mermaid
복사
① 상태를 만든다 — 조건을 보고 전이하는 것까지가 상태의 일입니다.
public sealed class EnemyChaseState : EnemyStateBase // EnemyStateBase : FsmState<Enemy>
{
protected override void OnStateUpdate(IFsm<Enemy> fsm, Enemy owner, float elapseSeconds)
{
if (TryChangeToDying(fsm, owner)) return;
Player player = Player.Instance;
bool isPlayerSpawned = player != null && player.Available;
if (!isPlayerSpawned)
{
owner.StopMoving();
ChangeState<EnemyIdleState>(fsm);
return;
}
owner.TickChase(player, elapseSeconds);
owner.TickAttack(player, elapseSeconds);
}
}
C#
복사
② 소유자가 FSM 을 만들고 시작한다 — 엔티티가 스폰될 때.
// Enemy.OnInit — 상태 객체는 인스턴스당 한 번만 만든다
mStates = new FsmState<Enemy>[]
{
new EnemyChaseState(), new EnemyIdleState(), new EnemyDyingState(), new EnemySinkingState(),
};
// Enemy.OnShow — 스폰마다
mFsm = fsmComponent.CreateFsm(FsmName, this, mStates);
mFsm.Start<EnemyChaseState>();
// Enemy.OnHide — 회수마다
fsmComponent.DestroyFsm(mFsm);
C#
복사
이 구조에서 지키는 원칙 세 가지
•
상태는 값을 들지 않는다. 체력·타이머·대상은 전부 소유자(fsm.Owner)에 둡니다. 상태 객체는 풀링된 엔티티와 함께 재사용되므로, 상태에 필드를 두면 이전 스폰의 값이 남습니다.
•
소유자는 "조건"과 "동작"만 공개한다. owner.IsDying, owner.TickChase() 처럼. 언제 무엇을 부를지는 상태가 정합니다.
•
전이는 상태 안에서만 한다. ChangeState 가 FsmState 의 protected 메서드라 바깥에서는 부를 수 없습니다. 외부에서 상태를 바꾸고 싶으면 소유자의 플래그를 바꾸고, 상태가 다음 프레임에 그것을 보고 전이하게 합니다.
확장하려면
•
새 행동 = FsmState<Enemy> 를 상속한 클래스 하나 추가 + 배열에 한 줄. 기존 상태 코드는 건드리지 않는다. 예: 체력이 낮으면 도망가는 EnemyFleeState
•
상태끼리 공유할 임시 값은 fsm.SetData<VarInt32>("ComboCount", 3) 처럼 FSM 에 붙일 수 있다. 다만 소유자에 필드로 두는 편이 읽기 쉬워, 예제는 쓰지 않는다
•
보스처럼 상태가 많아지면 "페이즈 FSM" 과 "행동 FSM" 두 개를 한 소유자에 붙인다. 이름이 다르면 같은 소유자 타입으로 여러 개를 만들 수 있다
자주 걸리는 곳
•
훅은 protected override 로 씁니다. 코어에서는 protected internal 이지만 게임 코드는 다른 어셈블리라 internal 을 붙이면 CS0507 입니다.
•
(소유자 타입, 이름) 조합은 유일해야 합니다. 같은 이름으로 두 번 만들면 Already exist FSM 예외입니다. 적이 여러 마리이므로 예제는 엔티티 Id 를 이름으로 씁니다.
•
DestroyFsm 을 빠뜨리면 FSM 이 계속 돕니다. 엔티티는 풀로 돌아갔는데 상태의 OnUpdate 는 계속 불려, 비활성 오브젝트를 조작하게 됩니다. CreateFsm
DestroyFsm 을 OnShow
OnHide 에 짝지어 두세요.
DestroyFsm 을 OnShow
OnHide 에 짝지어 두세요.•
Start<T>() 를 부르지 않으면 FSM 은 만들어지기만 하고 아무 상태도 돌지 않습니다. 에러도 없습니다.
•
게임 코드에서는 본문의 fsmManager 대신 GameEntry.GetComponent<FsmComponent>() 를 씁니다. 메서드 이름과 인자는 같습니다.
유한 상태 기계 (Finite State Machines)
관련 소스 파일
•
GameFramework/Base/Variable/GenericVariable.cs
•
GameFramework/Base/Variable/Variable.cs
•
GameFramework/DataNode/DataNodeManager.DataNode.cs
•
GameFramework/DataNode/DataNodeManager.cs
•
GameFramework/DataNode/IDataNode.cs
•
GameFramework/DataNode/IDataNodeManager.cs
•
GameFramework/Fsm/Fsm.cs
•
GameFramework/Fsm/FsmBase.cs
•
GameFramework/Fsm/FsmManager.cs
•
GameFramework/Fsm/IFsm.cs
•
GameFramework/Fsm/IFsmManager.cs
•
GameFramework/ObjectPool/IObjectPool.cs
•
GameFramework/ObjectPool/IObjectPoolManager.cs
•
GameFramework/ObjectPool/ObjectPoolBase.cs
•
GameFramework/ObjectPool/ObjectPoolManager.cs
GameFramework의 유한 상태 기계(FSM) 시스템은 명확히 정의된 전이를 갖춘 이산 상태(discrete states)를 통해 엔티티 동작을 모델링하기 위한 구조화된 접근 방식을 제공합니다. 이 패턴은 게임 캐릭터 동작, AI 로직, UI 네비게이션 흐름, 기타 상태 기반 게임 시스템을 구현하는 데 특히 유용합니다.
개요
FSM 시스템은 개발자가 다음을 할 수 있도록 합니다:
•
명확한 진입, 업데이트, 종료 로직을 가진 상태 정의
•
특정 소유자 타입을 가진 강력한 형식의 FSM 생성
•
게임 조건에 따라 상태 간 전이 관리
•
유연한 변수 시스템을 사용하여 상태 간 데이터 공유
•
복잡한 동작을 관리 가능한 이산 컴포넌트로 구성
FSM 라이프사이클
(다이어그램 설명 부분)
핵심 구성 요소
FsmManager
FsmManager는 애플리케이션의 모든 FSM에 대한 중앙 레지스트리 역할을 합니다. GameFrameworkModule 인터페이스를 구현하여 프레임워크 라이프사이클에 참여할 수 있습니다:
// 특정 소유자 타입과 상태들로 FSM 생성
IFsm<Player> playerFsm = fsmManager.CreateFsm("PlayerFSM", player,
new IdleState(), new MovingState(), new JumpingState());
// FSM 존재 여부 확인
bool exists = fsmManager.HasFsm<Player>("PlayerFSM");
// 기존 FSM 가져오기
IFsm<Player> fsm = fsmManager.GetFsm<Player>("PlayerFSM");
// 더 이상 필요하지 않을 때 FSM 제거
fsmManager.DestroyFsm(fsm);
C#
복사
매니저는 TypeNamePair(소유자 타입 + FSM 이름) 키로 딕셔너리를 유지하여 동일한 소유자 타입에 여러 FSM을 생성할 수 있습니다.
Fsm
Fsm 클래스는 특정 소유자 타입에 대한 FSM의 구체적인 구현체입니다. 이 클래스는 다음을 관리합니다:
•
소유자 인스턴스
•
상태 모음
•
현재 상태 및 타이밍 데이터
•
상태 간 공유 데이터(Variable 시스템 사용)
•
상태 전이
각 FSM은 상태 타입별로 딕셔너리를 유지하여 하나의 FSM 내에 동일한 상태 타입 인스턴스가 하나만 존재하도록 합니다.
FsmState
모든 상태는 FsmState<T> 클래스를 상속하며, 상태가 반드시 구현해야 하는 라이프사이클 메서드를 정의합니다:
메서드 | 설명 |
OnInit | FSM 생성 중 상태가 초기화될 때 호출 |
OnEnter | 상태가 현재 상태가 되었을 때 호출 |
OnUpdate | 상태가 활성화된 동안 매 프레임마다 호출 |
OnLeave | 다른 상태로 전환될 때 호출 |
OnDestroy | FSM이 파괴될 때 호출 |
상태 간 데이터 공유
FSM 시스템은 Variable 시스템과 통합되어 상태 간 데이터 공유가 가능합니다. 이는 전이 중에도 지속되어야 하는 상태를 유지하는 데 유용합니다:
// 한 상태에서 데이터 저장
fsm.SetData<VarInt32>("JumpCount", 0); // int → VarInt32 암시적 변환 (인자 있는 생성자는 없다)
// 다른 상태에서 데이터 가져오기
if (fsm.HasData("JumpCount"))
{
VarInt32 jumpCount = fsm.GetData<VarInt32>("JumpCount");
// 데이터 사용
}
// 더 이상 필요하지 않을 때 데이터 제거
fsm.RemoveData("JumpCount");
C#
복사
FSM은 이름으로 키를 둔 딕셔너리에 변수를 저장하며, 제네릭 메서드를 통해 타입 안정성을 보장합니다.
FSM 생성 및 사용
일반적인 워크플로우:
1.
FsmState<T>를 상속받아 상태 클래스 정의
2.
FsmManager를 사용해 FSM 생성
3.
초기 상태로 FSM 시작 — fsm.Start<PlayerIdleState>(). 이것을 부르기 전에는 어떤 상태도 돌지 않는다
4.
게임 조건에 따라 상태 전이 처리
예제:
public class PlayerIdleState : FsmState<Player>
{
protected override void OnEnter(IFsm<Player> fsm)
{
fsm.Owner.PlayAnimation("Idle");
}
protected override void OnUpdate(IFsm<Player> fsm, float elapseSeconds, float realElapseSeconds)
{
if (fsm.Owner.IsMoving)
{
// 전이는 상태 안에서만 할 수 있다 (ChangeState 는 FsmState 의 protected 메서드)
ChangeState<PlayerMovingState>(fsm);
}
}
protected override void OnLeave(IFsm<Player> fsm, bool isShutdown)
{
if (!isShutdown)
{
fsm.Owner.StopAnimation("Idle");
}
}
}
C#
복사
오브젝트 풀링과의 통합
FSM 시스템은 메모리 효율을 위해 프레임워크의 참조 풀링(reference pooling) 메커니즘을 활용합니다.
Fsm<T> 클래스는 IReference를 구현하므로 FSM 인스턴스는 재사용될 수 있습니다. FSM이 파괴되면 즉시 가비지 컬렉션되지 않고 풀로 반환되어 메모리 할당 오버헤드를 줄입니다.
모범 사례
•
명확한 상태 책임: 각 상태는 진입·종료 조건이 잘 정의된 개별 동작을 표현해야 함
•
상태 수 최소화: 상태가 너무 많으면 관리가 어려움 → 필요 시 계층적 FSM 고려
•
리소스 정리: OnLeave에서 isShutdown 여부 확인 후 정리 수행
•
데이터 공유 효율화: Variable 시스템을 통해 공유하되, 불필요해지면 제거
•
명확한 네이밍: FSM 및 변수 이름을 직관적으로 지정
•
FSM 소유자 고려: FSM의 소유자는 행동이 모델링되는 엔티티여야 하며, 매니저 클래스가 아님
일반적인 사용 사례
•
캐릭터 컨트롤러: 플레이어나 NPC의 상태(대기, 이동, 공격 등) 모델링
•
AI 시스템: 적의 의사결정 및 행동 패턴 구현
•
UI 네비게이션: 화면, 메뉴, UI 흐름 관리
•
게임 흐름: MainMenu, Loading, Gameplay, Paused 등 게임 상태 제어
•
퀘스트 시스템: 퀘스트 진행 상태 추적
FSM 시스템을 활용하면 명확한 상태 경계와 전이를 가진 유지보수 가능, 모듈화된, 테스트 가능한 게임 시스템을 구축할 수 있습니다.







