This document is about: QUANTUM 3
SWITCH TO

階層型有限ステートマシン (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)を選択してください。

Create new HFSM Document
Create new HFSM Document

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

HFSM file
HFSM file

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

Initial State
Initial State

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

State analysis
State analysis
  1. この階層レベルにおける初期状態を示す
  2. 状態の名前
  3. 子状態の数を表示(デフォルトは0)
  4. 折りたたみ/展開ボタン
  5. この状態が持つ遷移
  6. 遷移の追加ボタン(マウスホバー時のみ表示)

新しい状態の作成

新しい状態を作成するには、エディターウィンドウの何もない場所を右クリックし「Create New State」を選択してください。

Create New State
Create New State

状態の編集

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

Edit State
Edit State
  1. 状態名を定義する
  2. 遷移を削除する
  3. 遷移の順序を変更する(変更はビジュアルのみで、遷移の優先順位を定義するわけではありません

Enterを押して変更を適用するか、Escを押して変更を破棄します。

状態のビューの折りたたみ

HFSMは状態と遷移の数が多くなりやすいため、実際の流れを理解するのが困難になる場合があります。
任意の状態ノードで折りたたみボタンをクリックすると、遷移スロットが非表示になり、ノードから伸びる線の描画を変更されて、簡略化されたビューになります。

Minimized State View
Minimized State View

折りたたみを見比べてみてください。

折りたたみ前:

Maximized Sample
Maximized Sample

折りたたみ後:

Minimized Sample
Minimized Sample

2つの状態間の遷移の作成

2つの状態間の遷移を作成するには、まず状態の左右端にある小さな円をクリックしてください。
その次に別の状態をクリックすると、新しい遷移が作成されます。

別の状態のかわりに何もない場所をクリックすると、エディターのノード作成パネルが表示され、遷移先の新しい状態をすぐに作成できます。

New Transition
New Transition

作成直後の遷移は、暗い色で表示されます。これは、遷移条件がまだ定義されていないことを示します。

遷移にはいくつかのインタラクションが存在します。

  1. 遷移の上にマウスカーソルがあると、線が太く強調されます。
  2. マウス左ボタンで遷移を選択すると、小さな点によって遷移の方向と移動先が示されます。ここでDeleteを押すと、遷移を削除できます。
  3. 遷移をダブルクリックすると、サブグラフに移動します。
  4. 右クリックメニューから他の操作も可能です。Muteオプションなどは非常に便利でしょう。

各遷移のサブグラフには固定ノードが存在します。これを見てみましょう。

Transition Node
Transition Node

これは遷移を定義するノードで、4つの重要な要素を持ちます。

  1. ノード名で、遷移元と遷移先の状態を示します。右クリックメニューから名前を変更可能で、遷移条件が理解しやすい意味のある名前にすることができます。
  2. この遷移を評価する際に考慮されるEvent(イベント)を定義します。
  3. 遷移を構成するDecision(ディシジョン)の集合を定義します。
  4. 同一ノードから伸びるすべての遷移間の実行順序を定義します。(詳細は後述)

ここで、遷移に対して簡単なディシジョンを定義してみましょう。
何もない場所を右クリックすると、作成可能なすべてのノードが並んだパネルが表示されます。

Initial Decisions
Initial Decisions

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

New Decision
New Decision

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

Connected Decision
Connected Decision

このセットアップでは、NewState状態のBotのHFSMが更新されると、ステートマシンはNewState1へ遷移します。

ほとんどのスロットの値は、値のフィールドをクリックすることで編集可能です。

Decision Fields
Decision Fields

Enterを押して変更を適用するか、Escを押して変更を破棄します。

上位階層に戻るには、上部バーのパンくずリストボタンを押すか、Escを押してください

Root on Breadcrumb
Root on Breadcrumb

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

Root on Hierarchy
Root on Hierarchy

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

Valid Transition
Valid Transition

遷移の優先度の定義

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

Transition Priority
Transition Priority

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

Transition Priority on State node
Transition Priority on State node

遷移の評価順序は降順(優先度が高い値から低い値への順)です。

新しい遷移の作成

新しい遷移を作成するには、状態の下部にカーソルを移動して表示される「+」ボタンをクリックします。

New Transition
New Transition

特殊な遷移タイプ

Transition Set(遷移セット)

このノードは、複数の遷移をグループ化するために使用可能で、再利用や整理に便利です。

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

Transition Set
Transition Set

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

Maximized Transition Set
Maximized Transition Set

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

Minimized Transition Set
Minimized Transition Set

ANY Transition(ANY遷移)

この遷移タイプは、対象の状態への遷移を個別に追加することなく、他のすべての状態から対象の状態への遷移を素早く作成できます。
これは、同じ階層レベル(階層構造については後述)の状態のみが考慮されます。

右クリックメニューから、新しいANY遷移を作成してください。

そして、対象の状態を定義します。

Any Transition
Any Transition

上記の例では、特定の階層レベルにあるすべての状態が、ANY遷移ノードを考慮します。

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

Any Transition Excluded List
Any Transition Excluded List

補足:対象のノードもANY遷移に含まれるので、対象自身への状態遷移も可能です。

Portal Transition(ポータル遷移)

この遷移タイプは、HFSMのある状態から(階層レベルが異なる状態を含め)他の任意の状態へ強制的に遷移させます。

右クリックメニューから新しいポータル遷移を作成し、ドロップダウンメニューから対象の状態を定義してください。

Portal Transition
Portal Transition

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

Transition to Portal
Transition to Portal

補足:左パネルの階層から任意の状態を右クリックすることで、現在のグラフビュー上にその状態への新しいポータル遷移を作成できます。

ディシジョンの合成

単一のディシジョンで遷移を定義することも、複合的なディシジョンを作成することも可能です。
Bot SDKには、3つの論理的なディシジョンノードが用意されています。

以下はANDORNOTに基づく複合的なディシジョンの例です。

Composed Decision 1
Composed Decision 1

イベント

遷移は、ディシジョンを経由せずに、名前で定義されたイベントのような方法でトリガーすることもできます。
イベントは、シミュレーションの任意のロジック(システムのロジックなど)からトリガー可能で、HFSMのパイプライン外から遷移を実行できるため、非常に便利です。

イベントの動作は非常にシンプルです。イベントがトリガーされると、現在の状態の遷移がイベントを購読しているかをチェックします。
現在の遷移のいずれか(現在の状態の遷移、または上位階層の状態の遷移)がイベントを購読している場合、イベントのチェックが成功します。

HFSMイベントを(シミュレーションコードから)トリガーする方法は次の通りです。

C#

HFSMManager.TriggerEvent(frame, entityRef, "FooEvent");

これはBot SDK関連のクラスに限らず、開発者独自のシステムやロジックにも追加できます。

新しいイベントを作成するには、左サイドパネルのEventの「+」ボタンをクリックしてください。

Create Event
Create Event

ここでイベント名を入力します。イベントをダブルクリックすると、イベント名の編集やイベントの削除も可能です。

イベントをドラッグ&ドロップして、遷移のサブグラフに配置できます。
そして、Eventのスロットと遷移のEventスロットをリンクしてください。

Linked Event
Linked Event

備考: ディシジョンとは異なり、複合的なイベントは存在しません。遷移に、複数のイベントを接続することはできません。

イベントのみで定義された遷移は、有効な遷移になります。

イベントとディシジョンの両方を設定して遷移を定義することも可能です。
その場合は、同一フレームにおいて、イベントがトリガーされ、かつディシジョンの条件が満たされた場合にのみ、遷移が発生します。

Event and Decision
Event and Decision

アクションの定義

ステートマシン(状態と遷移)の流れを定義することと同様に、ステートマシンが実際に何を実行するのかを定めるAIアクション(ゲームステートの変更など)を実装することが非常に重要です。

詳細な情報は、アクションの定義をご覧ください。

階層構造

任意の状態のサブグラフにおいて、新しく複数の状態と遷移を作成することができます。これによって、状態の親子関係が生成されます。

HFSMが更新されると、現在の状態の階層構造全体が、親状態から子状態、子状態から孫状態の順で実行されます。
この仕組みによって、サブステートマシンを別のステートマシン内にカプセル化することができます。

複雑な挙動を単一の階層レベルで構成するのは非常に難しいため、HFSMの構成は非常に便利です。
一例として、2つの異なるルート状態を持つ場合を考えてみましょう。一つは「パトロールと探索」ロジック、もう一つは「追跡と攻撃」ロジックを処理します。
それぞれのルート状態が多くの子孫の状態を持ち、特定の状況に対する処理を個別に実行できます。

子状態を作成するには、状態のサブグラフに移動して、そこで新しい状態を作成してください。
状態の階層構造は左サイドメニューから確認できます。

HFSM Hierarchy
HFSM Hierarchy

備考: これらのボタンをクリックすると、階層を移動できます。

重要: HFSMの各階層レベルで、デフォルト状態を定義することもできます。これは、親状態が遷移する際の、初期の子状態を定義します。デフォルト状態を定義するには、状態ノードを右クリックし「Make Default State」を選択してください。

ドキュメントのコンパイル

HFSMを実際にシミュレーションで使用するには、AIドキュメントをコンパイルする必要があります。これは、ドキュメントを変更するたびに行う必要があります。

コンパイルには、2つのオプションがあります。

Compile Buttons
Compile Buttons
  • 左ボタン:現在開いているドキュメントのみをコンパイルする
  • 右ボタン:プロジェクトのドキュメントすべてをコンパイルする

デフォルトでは、HFSMファイルはAssets/QuantumUser/Resources/DB/CircuitExport/HFSM_Assetsに置かれます。
このコンパイルプロセスで、HFSMRootアセットが作成されます。

HFSM Asset
HFSM Asset

HFSMRootアセットの使用

作成されたHFSMRootアセットを使用するには、AssetRef<HFSMRoot>型のフィールドで参照を作成し、frame.FindAsset()からロードします。

HFSMのコード実装

HFSMの主要なコンポーネントはHFSMAgentで、基本的に2つの方法で使用できます。

  • エンティティにコンポーネントを追加する(直接コードから追加する、またはUnity上のエンティティプロトタイプから追加する)
  • frame.GlobalHFSMAgentインスタンスを宣言する

最も一般的な使用方法は、エンティティにコンポーネントを追加することですが、エンティティから分離させて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を返します。

重要AIActionHFSMDecisionクラスには、必ず[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サンプルプロジェクトにおけるデバッグツールの例です。

Debugger Graph
Debugger Graph

上記画像が示すように、エージェントの現在の状態は何か?その状態に入るまでの直近3つの遷移は何か?が確認できます。青色の遷移が最も直近の遷移で、それ以前の黒色の遷移よりも強調して表示されます。

さらに、階層構造ビューから現在の状態を確認することも可能です。矢印が付いた状態はHFSMの現在の状態を示し、ノードから探さずに素早く現在の状態を確認するのに便利です。

Debugger Hierarchy
Debugger Hierarchy

デバッガーの使用方法

デバッガーを使用するために必要な手順は次の通りです。

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

どのエンティティをデバッグするかを選択する方法は2つあります。

ゲームオブジェクトを使用したデバッグ:

  1. HFSMAgentコンポーネントを持つQuantumエンティティを表す、プレハブ/エンティティプロトタイプを選択する
  2. UnityのBotSDKDebuggerコンポーネントを追加する
  3. 実行時にBot SDKウィンドウを開きデバッガーを有効にした上で、BotSDKDebuggerを持つゲームオブジェクトを選択して動作を確認する

デバッガーウィンドウを使用したデバッグ:

  1. シミュレーション側でBotSDKDebuggerSystem.AddToDebugger(frame, collectorEntity, hfsmAgent, (optional) customLabel);を呼び出して、エージェントのエンティティをデバッガーウィンドウに登録します。デバッグ対象のエンティティのデフォルト名は「Entity XX | AI Document Name」形式になりますが、customLabelパラメーターを使用して特定のラベルを割り当てることが可能です。階層構造名を作成することも可能で、カスタムラベルに区切り文字「/」を使用すると、デバッガーウィンドウに階層構造が作成されて、折りたたみ/展開できるようになります。
  2. Unity側でデバッガー起動ボタンの隣のボタンをクリックすると、登録済みエージェントすべてを表示するウィンドウが新しく開きます。そこからデバッグ対象のエージェントを選択してください。
Debug Window
Debug Window
Debug Hierarchy
Debug Hierarchy

重要:デバッガーを有効にすると、エージェントデータを処理するためにCPUとメモリ使用量が増加します。
これによって、ゲームのパフォーマンスが低下する可能性があるため、エージェントの挙動をデバッグする際のみデバッガーを有効にし、パフォーマンステスト時は必ずデバッガーを無効にしてください。デバッガーをあまり使用していなくても、バッググラウンドではデータ処理が継続しています。

補足:現在、エンティティにリンクしていないエージェント(例:グローバルに存在するエージェント)はデバッグできません。

ミュート

AIテスト時にノードをミュートして、一部のロジックを一時的に無効にすると便利です。ノードのミュート方法について、詳細な情報はミュートをご覧ください。

ビジュアルエディターのコメント

ビジュアルエディターでコメントを作成する方法について、詳細な情報はビジュアルエディターのコメントをご覧ください。

コンパイル出力フォルダーの変更

デフォルトでは、Bot SDKのコンパイルプロセスで生成されたアセットはAssets/Resources/DB/CircuitExportフォルダーに置かれます。出力フォルダーを変更する方法については、出力フォルダーの変更をご覧ください。

フレーム内の動作

Bot SDKの主なエントリーポイントは次の通りです。

  • HFSMManager.Init:エージェントを初期化し、デフォルトの状態の初期アクションを実行します。
  • HFSMManager.Update:エージェントを更新するために継続的に呼び出す必要があります。
  • HFSMManager.TriggerEvent:イベント特有の遷移チェックを強制します。

以下のフロー図は、これらのメソッドが実行されるフレーム内の動作を可視化したものです。

HFSM In A Frame
HFSM In A Frame
Back to top