階層型有限ステートマシン (HFSM)
はじめに
ステートマシンによって、エージェントが取り得る状態を簡単に定義できます。
すべての状態は、以下の要素で構成されます。
- Action(アクション):エージェントが状態を「開始(Enter)」「更新(Update)」「終了(Exit)」する際に実行されるロジックの種類を表します。
- Transition(遷移):ある状態から状態へのリンクを表し、エージェントが変更可能な他の状態を定義します。
Bot SDKのステートマシンは階層型(hierarchical)であるため、各状態が複数のサブステート(子状態)を持つことが可能です。これによって、ステートマシンの構築方法は大きく変わります。階層構造の使用は必須ではありませんが、より複雑なAIを動作させるためには非常に重要です。
ステートマシンでは、すべての状態・アクション・遷移など、AIエージェントのあらゆる要素を開発者が自由に定義できます。ステートマシンのアルゴリズムは通常、開発者が設定した範囲外でプランを計画したり解決策を考案したりすることはありません。
現時点では、すべての要素が実行時に計画/作成されずに固定されているため、固定構造のAIモデルと呼びましょう。
利点と欠点
Bot SDK HFSMを使用する際の利点と欠点は次の通りです。
- 利点:
- パフォーマンス:固定構造の性質と簡潔な内部メカニズムにより、HFSMは非常に高速です。そのため、パフォーマンスの大部分は、アクションやディシジョンなどの特定のAIロジックをどのように実装するかに依存します。
- メモリ使用量:
HFSMAgent
コンポーネントは非常にシンプルなので、動作に必要な少量のデータをキャッシュするだけで十分です。そのため、多くのHFSMエージェントを動作させてもメモリ使用量はそれほど増加しません。 - 表現しやすさ:状態・アクション・遷移の概念は非常にわかりやすく、ステートマシンはゲーム開発分野でよく利用されます。コーダーもゲームデザイナーも概念をすぐに理解して、開発を始めることができます。
- 厳密な制御:固定構造によって、ある状態で何が起こるのか?遷移で何が起こり得るのか?を正確に把握できます。
- 欠点:
- 厳密な制御:これは長所でもあり短所でもあります。すべての可能性を定義する必要があるため、保守が必須になります。状態やロジックを追加する際には、既存の状態の再調査や、調整が頻繁に発生する可能性があります。
- スパゲッティ状態:複雑なHFSMは、状態と遷移の多さから理解困難になりがちです。階層構造を適切に使用したりコメントを追加したりすることは、AIグラフの理解と保守性を高めるために非常に重要です。
- 柔軟性の欠如:すべての要素を手動で定義するより、AI自身に計画を立てさせるアプローチの方が有益な場合もあります。これは、Bot SDKの効用理論で実現可能です。
Bot SDK使用時、HFSMは一般的に採用しやすいアプローチで、特にAI設計についてチームメンバーの個人的な好みに合う場合に有効です。単純~複雑なエージェントに幅広く使用可能で、Bot SDKの他モデルと比較してCPUやメモリ効率が良いため、大量のエージェントを動作させる際のスケーラビリティに優れます。
ドキュメントの作成
Bot SDKウィンドウでNew Document
ボタンをクリックして、Hierarchical Finite State Machine (HFSM)
を選択してください。

まず、AIドキュメントの名前を設定してください。このドキュメントはScriptableObject
で、エディター側でのみで使用されるXMLを保持し、Quantumシミュレーションには関連しません。そのため、ビルドに含める必要はありません。
このAIドキュメントに設定した名前は、生成されるQuantumアセット名にもなります。アセットは、シミュレーション内でエンティティのAIを更新するために使用されるので、意味のある適切な名前を選ぶと良いでしょう。

新しく作成したHFSMドキュメントには、基本的な(何のアクションも実行せず、遷移を持たない)状態が置かれています。

「状態(State)」の特徴を以下に示します。

- この階層レベルにおける初期状態を示す
- 状態の名前
- 子状態の数を表示(デフォルトは0)
- 折りたたみ/展開ボタン
- この状態が持つ遷移
- 遷移の追加ボタン(マウスホバー時のみ表示)
新しい状態の作成
新しい状態を作成するには、エディターウィンドウの何もない場所を右クリックし「Create New State」を選択してください。

状態の編集
状態を編集するには、対象の状態を右クリックし「Edit This State」を選択するか、対象の状態を選択してF2を押してください。

- 状態名を定義する
- 遷移を削除する
- 遷移の順序を変更する(変更はビジュアルのみで、遷移の優先順位を定義するわけではありません)
Enterを押して変更を適用するか、Escを押して変更を破棄します。
状態のビューの折りたたみ
HFSMは状態と遷移の数が多くなりやすいため、実際の流れを理解するのが困難になる場合があります。
任意の状態ノードで折りたたみボタンをクリックすると、遷移スロットが非表示になり、ノードから伸びる線の描画を変更されて、簡略化されたビューになります。

折りたたみ前と後を見比べてみてください。
折りたたみ前:

折りたたみ後:

2つの状態間の遷移の作成
2つの状態間の遷移を作成するには、まず状態の左右端にある小さな円をクリックしてください。
その次に別の状態をクリックすると、新しい遷移が作成されます。
別の状態のかわりに何もない場所をクリックすると、エディターのノード作成パネルが表示され、遷移先の新しい状態をすぐに作成できます。

作成直後の遷移は、暗い色で表示されます。これは、遷移条件がまだ定義されていないことを示します。
遷移にはいくつかのインタラクションが存在します。
- 遷移の上にマウスカーソルがあると、線が太く強調されます。
- マウス左ボタンで遷移を選択すると、小さな点によって遷移の方向と移動先が示されます。ここでDeleteを押すと、遷移を削除できます。
- 遷移をダブルクリックすると、サブグラフに移動します。
- 右クリックメニューから他の操作も可能です。Muteオプションなどは非常に便利でしょう。
各遷移のサブグラフには固定ノードが存在します。これを見てみましょう。

これは遷移を定義するノードで、4つの重要な要素を持ちます。
- ノード名で、遷移元と遷移先の状態を示します。右クリックメニューから名前を変更可能で、遷移条件が理解しやすい意味のある名前にすることができます。
- この遷移を評価する際に考慮されるEvent(イベント)を定義します。
- 遷移を構成するDecision(ディシジョン)の集合を定義します。
- 同一ノードから伸びるすべての遷移間の実行順序を定義します。(詳細は後述)
ここで、遷移に対して簡単なディシジョンを定義してみましょう。
何もない場所を右クリックすると、作成可能なすべてのノードが並んだパネルが表示されます。

説明を簡単にするため、常にTrue
を返す(常に遷移が発生する)「TrueDecision」を選択しましょう。

出力スロットから、このディシジョンの結果を渡す先を定義します。
出力スロットを左クリックして、Decisionスロットに接続してください。

このセットアップでは、NewState
状態のBotのHFSMが更新されると、ステートマシンはNewState1
へ遷移します。
ほとんどのスロットの値は、値のフィールドをクリックすることで編集可能です。

Enterを押して変更を適用するか、Escを押して変更を破棄します。
上位階層に戻るには、上部バーのパンくずリストボタンを押すか、Escを押してください。

あるいは、左サイドパネルのStates
セクションから、状態間を移動することも可能です。

遷移が定義されると、明るい色で表示され、ディシジョンの条件によって遷移が実行されるようになります。
ディシジョンもイベントも指定されない場合、遷移は常に実行されます。

遷移の優先度の定義
複数の遷移を持つ状態は、どの遷移を最初に評価するかを定義できます。
この順序を定義するには、遷移ノードのPriorityスロットを使用します。

遷移の優先度(Priority)は、状態ノード上で確認できます。

遷移の評価順序は降順(優先度が高い値から低い値への順)です。
新しい遷移の作成
新しい遷移を作成するには、状態の下部にカーソルを移動して表示される「+」ボタンをクリックします。

特殊な遷移タイプ
Transition Set(遷移セット)
このノードは、複数の遷移をグループ化するために使用可能で、再利用や整理に便利です。
新しい遷移セットを作成するには、何もない場所を右クリックし、Create New Transition Set
を選択してください。このノードも、名前を変更可能です。
これによって、状態ノードに似たノードが作成されます。最初は未定義の遷移が一つだけ存在しますが、下部ボタンから複数の遷移を追加することができます。

その後、遷移セットと他の状態間のリンクを作成します。
以下はその例です。

右上のボタンを使用して、遷移セットを折りたたむこともできます。

ANY Transition(ANY遷移)
この遷移タイプは、対象の状態への遷移を個別に追加することなく、他のすべての状態から対象の状態への遷移を素早く作成できます。
これは、同じ階層レベル(階層構造については後述)の状態のみが考慮されます。
右クリックメニューから、新しいANY遷移を作成してください。
そして、対象の状態を定義します。

上記の例では、特定の階層レベルにあるすべての状態が、ANY遷移ノードを考慮します。
ANY遷移について、除外すべき状態リスト、または考慮すべき状態リストを定義することが可能です。
これらは「除外リスト(Excluded List
)」「包含リスト(Included List
)」と呼ばれます。ダイヤモンド型のボタンからリストを切り替えて、「+
」ボタンから状態を選択してください。

追記:対象のノードもANY遷移に含まれるので、対象自身への状態遷移も可能です。
Portal Transition(ポータル遷移)
この遷移タイプは、HFSMのある状態から(階層レベルが異なる状態を含め)他の任意の状態へ強制的に遷移させます。
右クリックメニューから新しいポータル遷移を作成し、ドロップダウンメニューから対象の状態を定義してください。

そして、どの状態がポータルを考慮すべきかを定義します。

追記:左パネルの階層から任意の状態を右クリックすることで、現在のグラフビュー上にその状態への新しいポータル遷移を作成できます。
ディシジョンの合成
単一のディシジョンで遷移を定義することも、複合的なディシジョンを作成することも可能です。
Bot SDKには、3つの論理的なディシジョンノードが用意されています。
以下はAND・OR・NOTに基づく複合的なディシジョンの例です。

イベント
遷移は、ディシジョンを経由せずに、名前で定義されたイベントのような方法でトリガーすることもできます。
イベントは、シミュレーションの任意のロジック(システムのロジックなど)からトリガー可能で、HFSMのパイプライン外から遷移を実行できるため、非常に便利です。
イベントの動作は非常にシンプルです。イベントがトリガーされると、現在の状態の遷移がイベントを購読しているかをチェックします。
現在の遷移のいずれか(現在の状態の遷移、または上位階層の状態の遷移)がイベントを購読している場合、イベントのチェックが成功します。
HFSMイベントを(シミュレーションコードから)トリガーする方法は次の通りです。
C#
HFSMManager.TriggerEvent(frame, entityRef, "FooEvent");
これはBot SDK関連のクラスに限らず、開発者独自のシステムやロジックにも追加できます。
新しいイベントを作成するには、左サイドパネルのEventの「+」ボタンをクリックしてください。

ここでイベント名を入力します。イベントをダブルクリックすると、イベント名の編集やイベントの削除も可能です。
イベントをドラッグ&ドロップして、遷移のサブグラフに配置できます。
そして、Eventのスロットと遷移のEventスロットをリンクしてください。

備考: ディシジョンとは異なり、複合的なイベントは存在しません。遷移に、複数のイベントを接続することはできません。
イベントのみで定義された遷移は、有効な遷移になります。
イベントとディシジョンの両方を設定して遷移を定義することも可能です。
その場合は、同一フレームにおいて、イベントがトリガーされ、かつディシジョンの条件が満たされた場合にのみ、遷移が発生します。

アクションの定義
ステートマシン(状態と遷移)の流れを定義することと同様に、ステートマシンが実際に何を実行するのかを定めるAIアクション(ゲームステートの変更など)を実装することが非常に重要です。
詳細な情報は、アクションの定義をご覧ください。
階層構造
任意の状態のサブグラフにおいて、新しく複数の状態と遷移を作成することができます。これによって、状態の親子関係が生成されます。
HFSMが更新されると、現在の状態の階層構造全体が、親状態から子状態、子状態から孫状態の順で実行されます。
この仕組みによって、サブステートマシンを別のステートマシン内にカプセル化することができます。
複雑な挙動を単一の階層レベルで構成するのは非常に難しいため、HFSMの構成は非常に便利です。
一例として、2つの異なるルート状態を持つ場合を考えてみましょう。一つは「パトロールと探索」ロジック、もう一つは「追跡と攻撃」ロジックを処理します。
それぞれのルート状態が多くの子孫の状態を持ち、特定の状況に対する処理を個別に実行できます。
子状態を作成するには、状態のサブグラフに移動して、そこで新しい状態を作成してください。
状態の階層構造は左サイドメニューから確認できます。

備考: これらのボタンをクリックすると、階層を移動できます。
重要: HFSMの各階層レベルで、デフォルト状態を定義することもできます。これは、親状態が遷移する際の、初期の子状態を定義します。デフォルト状態を定義するには、状態ノードを右クリックし「Make Default State」を選択してください。
ドキュメントのコンパイル
HFSMを実際にシミュレーションで使用するには、AIドキュメントをコンパイルする必要があります。これは、ドキュメントを変更するたびに行う必要があります。
コンパイルには、2つのオプションがあります。

- 左ボタン:現在開いているドキュメントのみをコンパイルする
- 右ボタン:プロジェクトのドキュメントすべてをコンパイルする
デフォルトでは、HFSMファイルはAssets/QuantumUser/Resources/DB/CircuitExport/HFSM_Assets
に置かれます。
このコンパイルプロセスで、HFSMRoot
アセットが作成されます。

HFSMRootアセットの使用
作成されたHFSMRoot
アセットを使用するには、AssetRef<HFSMRoot>
型のフィールドで参照を作成し、frame.FindAsset()
からロードします。
HFSMのコード実装
HFSMの主要なコンポーネントはHFSMAgent
で、基本的に2つの方法で使用できます。
- エンティティにコンポーネントを追加する(直接コードから追加する、またはUnity上のエンティティプロトタイプから追加する)
frame.Global
にHFSMAgent
インスタンスを宣言する
最も一般的な使用方法は、エンティティにコンポーネントを追加することですが、エンティティから分離させてframe.Global
に配置することで、HFSMで動作するGameManager
(ゲームマッチの開始・ゲームルールの更新・ゲームマッチの終了などのロジックを持つ)のようなものを作成するのに便利です。
HFSMAgentの初期化
HFSMAgent
をエンティティプロトタイプに追加していない場合は、コードからエンティティに追加することができます。これは、プレイヤーが切断した際など、実行時にエンティティをAIエージェントに切り替える場合に便利です。
以下は、コンポーネントを追加するコードスニペットです。(エンティティプロトタイプに追加していない場合のみ)
C#
var hfsmAgent = new HFSMAgent();
f.Set(myEntity, hfsmAgent);
HFSMManager
クラスには、HFSMエージェントの初期化や更新を行う様々なメソッドが用意されています。
エージェントを初期化するにはHFSMManager.Init()
を呼び出して、(エディターで定義した)初期状態を格納し、その状態とデフォルトの子孫状態すべてに対してOnEnter
を呼び出します。
以下の初期化ステップは、エンティティプロトタイプを使用するしないにかかわらず実行する必要があります。
C#
var hfsmRootAsset = f.FindAsset<HFSMRoot>(hfsmRoot.Id);
HFSMManager.Init(frame, entityRef, hfsmRootAsset);
OnComponentAddedコールバックによる初期化
HFSMRoot
アセットの参照をエンティティプロトタイプに直接設定し、その情報でエージェントを初期化するのにOnComponentAdded
シグナルを使用することも可能です。
これはBotSDKSystem
でも行っている方法です。以下がその例です。
C#
// 任意のシステム
public unsafe class AISystem : SystemMainThread, ISignalOnComponentAdded<HFSMAgent>
{
public void OnAdded(Frame frame, EntityRef entity, HFSMAgent* component)
{
// エンティティプロトタイプに設定されたコンポーネントからHFSMRootを取得する
HFSMRoot hfsmRoot = frame.FindAsset<HFSMRoot>(component->Data.Root.Id);
// 初期化
HFSMManager.Init(frame, entityRef, hfsmRoot);
}
// ...
}
HFSMAgentの更新
エージェント初期化後は、次のように更新できます。
C#
HFSMManager.Update(frame, frame.DeltaTime, entityRef);
これによって、HFSMメカニズム全体が開始されます。現在の状態が更新されて、そのアクションが実行されたり、遷移がチェックされたりなどが行われます。
エージェントを初期化/更新するシステムのサンプル
C#
namespace Quantum
{
public unsafe class AISystem : SystemMainThreadFilter<AISystem.Filter>, ISignalOnComponentAdded<HFSMAgent>
{
public struct Filter
{
public EntityRef Entity;
public HFSMAgent* HFSMAgent;
}
public void OnAdded(Frame frame, EntityRef entity, HFSMAgent* component)
{
HFSMRoot hfsmRoot = frame.FindAsset<HFSMRoot>(component->Data.Root.Id);
HFSMManager.Init(frame, entity, hfsmRoot);
}
public override void Update(Frame frame, ref Filter filter)
{
HFSMManager.Update(frame, frame.DeltaTime, filter.Entity);
}
}
}
アクションとディシジョンのコード実装
新しいAIアクションを作成するには、アクションのコード実装をご覧ください。
新しいHFSMディシジョンの作成は、非常に似た方法で行えます。
HFSMDecision
を継承したクラスを作成し、Decide
メソッドをオーバーライドして、ディシジョンを通すかどうかの条件によってtrue
/false
を返します。
重要:AIAction
とHFSMDecision
クラスには、必ず[System.Serializable]
属性を追加してください。
SDKから提供される最も基本的なHFSMディシジョンの例は次の通りです。
C#
namespace Quantum
{
[System.Serializable]
public partial class TrueDecision : HFSMDecision
{
public override unsafe bool Decide(Frame frame, EntityRef entity)
{
return true;
}
}
}
フィールド値の定義
ノードのフィールドに値を設定する様々な方法について、詳細な情報はフィールド値の定義をご覧ください。
AIParam
AIParam
型の使用方法について、詳細な情報はAIParamをご覧ください。この型は、手動設定やBlackboard
/Constant
/Config
ノードからの設定など、柔軟に定義可能なフィールドを実現するのに便利です。
AIContext
エージェントのコンテキスト情報をパラメーターとして渡す方法について、詳細な情報はAIContextをご覧ください。
BotSDKSystem
Bot SDKコンポーネントの追加/削除コールバック時、コンポーネントのメモリ初期化/解放などのプロセスを自動化するクラスです。詳細な情報はBotSDKSystemをご覧ください。
デバッガー
Bot SDKには独自のデバッグツールが付属しています。これによって、実行時に任意のHFSMAgent
を選択すると、エージェントの直近フローをビジュアルエディター上でハイライト表示できます。以下はBot SDKサンプルプロジェクトにおけるデバッグツールの例です。

上記画像が示すように、エージェントの現在の状態は何か?その状態に入るまでの直近3つの遷移は何か?が確認できます。青色の遷移が最も直近の遷移で、それ以前の黒色の遷移よりも強調して表示されます。
さらに、階層構造ビューから現在の状態を確認することも可能です。矢印が付いた状態はHFSMの現在の状態を示し、ノードから探さずに素早く現在の状態を確認するのに便利です。

デバッガーの使用方法
デバッガーを使用するために必要な手順は次の通りです。
SystemsConfig
ファイルでBotSDKDebuggerSystem
を有効にします。システムの使用はオプションで、独自ロジックから同じAPIのBotSDKDebuggerSystemCallbacks.OnVerifiedFrame?.Invoke(frame);
を確定フレームで呼び出すこともできます。- ビジュアルエディター上で、上部パネルの小さな虫アイコンをクリックします。アイコンが緑色になれば、デバッガーが有効になります。

どのエンティティをデバッグするかを選択する方法は2つあります。
ゲームオブジェクトを使用したデバッグ:
HFSMAgent
コンポーネントを持つQuantumエンティティを表す、プレハブ/エンティティプロトタイプを選択する- Unityの
BotSDKDebugger
コンポーネントを追加する - 実行時にBot SDKウィンドウを開きデバッガーを有効にした上で、
BotSDKDebugger
を持つゲームオブジェクトを選択して動作を確認する
デバッガーウィンドウを使用したデバッグ:
- シミュレーション側で
BotSDKDebuggerSystem.AddToDebugger(frame, collectorEntity, hfsmAgent, (optional) customLabel);
を呼び出して、エージェントのエンティティをデバッガーウィンドウに登録します。デバッグ対象のエンティティのデフォルト名は「Entity XX | AI Document Name
」形式になりますが、customLabel
パラメーターを使用して特定のラベルを割り当てることが可能です。階層構造名を作成することも可能で、カスタムラベルに区切り文字「/
」を使用すると、デバッガーウィンドウに階層構造が作成されて、折りたたみ/展開できるようになります。 - Unity側でデバッガー起動ボタンの隣のボタンをクリックすると、登録済みエージェントすべてを表示するウィンドウが新しく開きます。そこからデバッグ対象のエージェントを選択してください。


重要:デバッガーを有効にすると、エージェントデータを処理するためにCPUとメモリ使用量が増加します。
これによって、ゲームのパフォーマンスが低下する可能性があるため、エージェントの挙動をデバッグする際のみデバッガーを有効にし、パフォーマンステスト時は必ずデバッガーを無効にしてください。デバッガーをあまり使用していなくても、バッググラウンドではデータ処理が継続しています。
追記:現在、エンティティにリンクしていないエージェント(例:グローバルに存在するエージェント)はデバッグできません。
ミュート
AIテスト時にノードをミュートして、一部のロジックを一時的に無効にすると便利です。ノードのミュート方法について、詳細な情報はミュートをご覧ください。
ビジュアルエディターのコメント
ビジュアルエディターでコメントを作成する方法について、詳細な情報はビジュアルエディターのコメントをご覧ください。
コンパイル出力フォルダーの変更
デフォルトでは、Bot SDKのコンパイルプロセスで生成されたアセットはAssets/Resources/DB/CircuitExport
フォルダーに置かれます。出力フォルダーを変更する方法については、出力フォルダーの変更をご覧ください。
フレーム内の動作
Bot SDKの主なエントリーポイントは次の通りです。
HFSMManager.Init
:エージェントを初期化し、デフォルトの状態の初期アクションを実行します。HFSMManager.Update
:エージェントを更新するために継続的に呼び出す必要があります。HFSMManager.TriggerEvent
:イベント特有の遷移チェックを強制します。
以下のフロー図は、これらのメソッドが実行されるフレーム内の動作を可視化したものです。
