unity-state-machines について
ゲームフロー、AI、UI ナビゲーション向けの状態・振る舞いシステムを選ぶ Unity 開発者向けスキルです。FSM、階層型 FSM、ビヘイビアツリー、スタック方式を扱い、テスト可能なプレーン C# の状態ロジックを重視します。
主な機能
- 01Animator FSM、コード FSM、HFSM、ビヘイビアツリー、状態スタックの比較
- 02状態数と遷移形式に応じた選択指針
- 03クラスベース FSM と階層型 FSM の設計
- 04ビヘイビアツリーとスタック方式の実装パターン
- 05シーンを使わない状態ロジックのテスト
活用シーン
- 01アニメーション以外のゲームロジックに使う状態方式の選択
- 02サブ状態をまとめた戦闘状態マシンの構築
- 03割り込み可能な優先判断を行う複雑な AI の設計
- 04前の画面に戻れる UI ナビゲーションの実装
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ nice-wolf-studio/ unity-claude-skills --skill unity-state-machinesインストール後、Claude Code で/unity-state-machinesを入力して読み込まれたか確認します。
同梱ファイルreferences/state-system-scaffolds.md
詳細
状態・振る舞いシステム ― 意思決定パターン
前提となるスキル:
unity-game-architecture(MonoBehaviour とプレーンな C# の違い、コンポーネントの合成)、unity-animation(Animator FSM、StateMachineBehaviour)、unity-scripting(MonoBehaviour のライフサイクル)
これらのパターンは、状態管理で最もよくある失敗に対処します。Claude が構造のない場当たり的な if/else の連鎖や巨大な switch 文を書き、3~4 個を超える状態を管理できなくなるという問題です。
PATTERN:状態システムの選択
WHEN:AI の振る舞い、ゲームの進行、キャラクターの状態、UI ナビゲーションを実装するとき
DECISION:
- Animator FSM ― 状態がアニメーションに関連付けられている場合。キャラクターの移動、攻撃コンボ、死亡アニメーションなどです。デザイナーが扱いやすいビジュアルグラフです。詳しくは
unity-animationを参照してください。アニメーションを動かすためだけにコード FSM を構築しないでください。 - コード FSM(enum またはクラスベース) ― ゲームの進行(MainMenu/Playing/Paused/GameOver)、アビリティシステム、ターンフェーズなどです。完全にテスト可能で、Animator のオーバーヘッドもありません。状態に単なるアニメーションの切り替えではなく、複雑なロジックがある場合に使用します。
- 階層型 FSM(HFSM) ― 状態の中に状態がある場合です。Combat に {Melee, Ranged, Blocking} が含まれる場合などです。関連する状態間の遷移が多すぎて、フラットな FSM では扱いにくい場合に使用します。
- ビヘイビアツリー(BT) ― 優先順位付け、中断可能なシーケンス、並列処理が必要な複雑な AI に適しています。FSM の状態数が 8~10 を超え、遷移が増えすぎてグラフが読みづらくなる場合に使用します。
- スタックベース(プッシュダウン) ― UI 画面、一時停止メニュー、モーダルダイアログに適しています。状態をプッシュ/ポップし、前の状態を保持して再開します。
How many states? What kind of transitions?
|
+-- 2-3 states, simple toggling?
| --> if/else or bool flags (don't over-engineer)
|
+-- 4-8 states, clear transitions?
| --> Code FSM (enum or IState)
|
+-- States group naturally into clusters?
| --> HFSM (Locomotion > {Idle,Walk,Run}, Combat > {Melee,Ranged})
|
+-- AI with priorities, interrupts, parallel tasks?
| --> Behavior Tree
|
+-- Need to return to previous state (back button, unpause)?
| --> Stack-Based State Machine
|
+-- States tied to animations?
--> Animator FSM (use unity-animation skill)
注意点:Animator FSM は、アニメーションが再生されていない場合でも、フレームごとの評価オーバーヘッドが発生します。純粋なロジックの状態マシン(ゲームの進行、ターンシステムなど)に Animator を使用しないでください。代わりにコード FSM を構築してください。
PATTERN:クラスベース FSM アーキテクチャ
WHEN:3 つを超える状態を持つコード FSM を構築するとき
DECISION:
- Enum + switch ― 遷移ロジックが最小限の、最大 5 状態程度の場合です。シンプルに保ちます。
- IState インターフェース + dictionary ― 5 状態以上で、各状態に複雑な Enter/Tick/Exit ロジックがある場合です。状態はプレーンな C# クラスにすることで、MonoBehaviour なしでテストできます。
SCAFFOLD(Enum FSM ― シンプル):
public enum GameState { MainMenu, Playing, Paused, GameOver }
public class GameFlowManager : MonoBehaviour
{
private GameState _state = GameState.MainMenu;
public void ChangeState(GameState newState)
{
ExitState(_state);
_state = newState;
EnterState(_state);
}
void Update()
{
switch (_state)
{
case GameState.Playing:
// game logic
break;
case GameState.Paused:
// pause logic
break;
}
}
void EnterState(GameState state) { /* ... */ }
void ExitState(GameState state) { /* ... */ }
}
SCAFFOLD(IState FSM ― 拡張性あり):
// See references/state-system-scaffolds.md for complete implementation
public interface IState
{
void Enter();
void Tick(float deltaTime);
void Exit();
}
public class StateMachine
{
private IState _currentState;
public void ChangeState(IState newState)
{
_currentState?.Exit();
_currentState = newState;
_currentState?.Enter();
}
public void Tick(float deltaTime) => _currentState?.Tick(deltaTime);
}
注意点:状態はプレーンな C# クラスにしてください。MonoBehaviour にしてはいけません。所有する MonoBehaviour から、コンストラクターまたはインターフェースを介して、共有コンテキストオブジェクト(transform、rigidbody、参照)を状態に渡します。これにより状態をテスト可能に保ち、空の MonoBehaviour によるパフォーマンスオーバーヘッドを避けられます。
PATTERN:階層型 FSM(HFSM)
WHEN:状態を自然にスーパー状態にまとめられるとき(Locomotion に Idle/Walk/Run/Sprint が含まれる場合など)
DECISION:
- ネストされた StateMachine ― スーパー状態が独自の StateMachine を持ちます。明確に分離できます。階層が 2 レベルの場合に最適です。
- ガード条件でフラット化 ― 階層が 3 レベル以上になる場合は、HFSM の複雑さがビヘイビアツリーを上回る可能性があります。BT への切り替えを検討してください。
SCAFFOLD:
// A super-state that contains a child state machine
public class HierarchicalState : IState
{
private readonly StateMachine _subMachine;
private readonly IState _defaultSubState;
public HierarchicalState(StateMachine subMachine, IState defaultSubState)
{
_subMachine = subMachine;
_defaultSubState = defaultSubState;
}
public void Enter() => _subMachine.ChangeState(_defaultSubState);
public void Tick(float deltaTime) => _subMachine.Tick(deltaTime);
public void Exit() => _subMachine.ChangeState(null); // Exit current sub-state
}
// Usage:
// var idleState = new IdleState(context);
// var walkState = new WalkState(context);
// var locomotionSubMachine = new StateMachine();
// var locomotionState = new HierarchicalState(locomotionSubMachine, idleState);
// mainMachine.ChangeState(locomotionState);
注意点:スーパー状態の Exit は、アクティブなサブ状態にも伝播させる必要があります。Exit の順序は、sub-state.Exit() -> super-state.Exit() です。遷移は両方のレベルに設定できます。サブ状態の遷移(Idle -> Walk)はスーパー状態内にとどまり、スーパー状態の遷移(Locomotion -> Combat)は階層全体を終了します。
PATTERN:ビヘイビアツリーのアーキテクチャ
WHEN:AI に割り込み可能なシーケンスを伴う、優先順位ベースの意思決定が必要なとき
DECISION:
- 最小限のカスタム BT ― ノード数が 20 未満で、完全な制御を求め、外部依存関係を避けたい場合です。
Selector、Sequence、ActionLeafを構築します。 - サードパーティ製 BT ライブラリ(NodeCanvas、Behavior Designer、Fluid BT)― ビジュアル編集、デザイナー向けの操作性、50 種類以上のノードが必要な場合です。プログラマー以外が AI を設計するなら、依存関係を追加する価値があります。
SCAFFOLD(最小限の BT):
// See references/state-system-scaffolds.md for complete implementation
public enum BTStatus { Success, Failure, Running }
public abstract class BTNode
{
public abstract BTStatus Tick(Blackboard bb);
}
// Selector: tries children in order, succeeds on first success
// Sequence: runs children in order, fails on first failure
// ActionLeaf: executes a single action
注意点:ビヘイビアツリーは毎フレーム、ルートからティックします。パフォーマンス向上のため、ツリー全体を再評価するのではなく、「実行中」のノードをキャッシュして、そこから再開します。大規模な BT(ノード数 100 以上)では、クロージャーではなく共有データ用の Blackboard を使用してください。Blackboard は、型付きアクセサーを備えた Dictionary<string, object> にすぎません。
パターン:スタックベースのステートマシン
WHEN:UI ナビゲーション、一時停止メニュー、モーダルダイアログ、元に戻せる状態遷移
DECISION:
- スタックマシン -- 前の状態に戻る必要がある場合です。ゲームプレイ -> 一時停止 -> ゲームプレイ。ゲームプレイ -> インベントリ -> クラフト -> インベントリ -> ゲームプレイ。
- フラット FSM -- 遷移がスタック形式ではない場合です(「元いた場所に戻る」ことがありません)。
SCAFFOLD:
public class StateStack
{
private readonly Stack<IState> _stack = new();
public IState Current => _stack.Count > 0 ? _stack.Peek() : null;
public void Push(IState state)
{
Current?.Pause(); // Pause current (not Exit -- it stays on the stack)
_stack.Push(state);
state.Enter();
}
public void Pop()
{
if (_stack.Count == 0) return;
var popped = _stack.Pop();
popped.Exit();
Current?.Resume(); // Resume the state underneath
}
public void Tick(float deltaTime) => Current?.Tick(deltaTime);
}
// Extended state interface for stack machines
public interface IStackableState : IState
{
void Pause(); // Called when a new state is pushed on top
void Resume(); // Called when the state above is popped
}
GOTCHA:スタックからのポップを忘れると、スタックが際限なく大きくなります。最大深度(例:10)を設定し、警告をログに記録してください。スタックをクリアする場合(例:メインメニューに戻る場合)は、各状態で Exit が呼び出されるよう、順番にすべてポップしてください。同じ状態インスタンスを二度プッシュしないでください。新しいインスタンスを作成するか、フラグを使って二重プッシュを防いでください。
パターン:ステートマシンのテスト
WHEN:FSM または BT のロジックをテストする場合
DECISION:状態は通常の C# クラスです -> NUnit Edit Mode テストでテストします。MonoBehaviour、Play Mode、シーンは不要です。
SCAFFOLD:
// State that returns a transition signal
public class IdleState : IState
{
private readonly EnemyContext _ctx;
public bool ShouldTransitionToChase { get; private set; }
public IdleState(EnemyContext ctx) => _ctx = ctx;
public void Enter() => ShouldTransitionToChase = false;
public void Tick(float deltaTime)
{
if (_ctx.DistanceToPlayer < _ctx.DetectionRange)
ShouldTransitionToChase = true;
}
public void Exit() { }
}
// Test (Edit Mode, no Unity runtime needed)
[Test]
public void IdleState_TransitionsToChase_WhenPlayerInRange()
{
var ctx = new EnemyContext { DistanceToPlayer = 5f, DetectionRange = 10f };
var idle = new IdleState(ctx);
idle.Enter();
idle.Tick(0.016f);
Assert.IsTrue(idle.ShouldTransitionToChase);
}
[Test]
public void IdleState_StaysIdle_WhenPlayerOutOfRange()
{
var ctx = new EnemyContext { DistanceToPlayer = 15f, DetectionRange = 10f };
var idle = new IdleState(ctx);
idle.Enter();
idle.Tick(0.016f);
Assert.IsFalse(idle.ShouldTransitionToChase);
}
GOTCHA:状態が Time.deltaTime に依存する場合は、代わりに float deltaTime を Tick() のパラメーターとして渡してください。状態で Unity API(Physics.Raycast)が必要な場合は、インターフェースの背後にラップして、テストでスタブを渡せるようにしてください。Test Framework のセットアップについては unity-testing を参照してください。
比較表
| 特徴 | Enum FSM | IState FSM | HFSM | ビヘイビアツリー | スタックマシン |
|---|---|---|---|---|---|
| 複雑さ | 低 | 中 | 中~高 | 高 | 中 |
| 最適な用途 | 2~5 個の単純な状態 | 5~15 個の状態 | グループ化された状態 | 複雑な AI | UI ナビゲーション |
| テスト容易性 | switch のテスト | 状態ごとのテスト | 状態ごと+階層のテスト | ノードごとのテスト | プッシュ/ポップのテスト |
| デザイナー向け | いいえ | いいえ | いいえ | ビジュアルエディターを使用 | いいえ |
| 遷移管理 | switch 内で手動 | Dictionary またはシグナル | レベルごとの遷移 | 優先度ベース | プッシュ/ポップ |
| メモリ | 最小限 | 状態ごとのインスタンス | ネストされたマシン | ツリー全体をメモリに保持 | スタックの深さ |
| パフォーマンス | O(1) の switch | O(1) のデリゲート | ティックごとに O(深度) | ティックごとに O(ツリー) | O(1) |
| 避けるべき場合 | 状態が 5 個を超える場合 | アニメーションに結び付いた状態 | 階層が 2 レベルを超える場合 | 状態が 8 個未満の場合 | スタック形式ではない遷移 |
関連スキル
- unity-animation -- Animator FSM、StateMachineBehaviour、ブレンドツリー(状態がアニメーションを制御する場合に使用)
- unity-game-architecture -- MonoBehaviour と通常の C# の比較(状態は通常の C# にする)、Service Locator
- unity-ai-navigation -- AI の移動における FSM/BT と NavMeshAgent の統合
- unity-testing -- 状態ロジックの NUnit Edit Mode テスト