ブラックボード
はじめに
ブラックボードは、Bot SDKに組み込まれた非常に便利なツールです。
これは、任意のQuantumロジックで読み書きできるデータストレージ機能を持つQuantumコンポーネントで、主に3つの要素で構成されています。
AIBlackboardComponent:エンティティに追加できるQuantumコンポーネントです。ゲームシミュレーション実行時に変更可能なデータを保持するストレージを持ちます。AIBlackboard:Unity側で作成されるアセットです。コンポーネントに適用されるデータレイアウト(エンティティの型とキー)を定義します。AIBlackboardInitializer:ブラックボードコンポーネントが初期化される際の、各エントリの初期値を保存するアセットです。これの使用はオプションです。
なぜブラックボードを使うのか?
ブラックボードへのデータ格納は、通常のQuantumコンポーネントへのデータ格納と非常に似ていますが、Bot SDKと併用することで以下のような利点があります。
- Bot SDKのビジュアルエディターは、ブラックボードアセットと統合されています。エディターウィンドウ上でエントリを初期化して、柔軟に変更したり、ゲームデザイナー側で直接利用したりすることができます。
- エディターでは、ブラックボードノードをAIグラフにドラッグ&ドロップして、HFSMのアクションやBTのリーフノードなどの他のノードと接続する機能を備えています。これによって、エディターからエントリを参照することが非常に容易になります。
ブラックボードの欠点
ブラックボードを使用する際には、次の点を考慮する必要があります。
- ブラックボードの各エントリは
union(共用体)であるため、実行時のメモリサイズは、共用体がサポートする最大の型(AssetRef型の8バイト)に相当します。これは、エントリがブール型(サイズ1バイト)で、実際に1バイトしか使用していないとしても変わりません。フレームサイズがボトルネックになる場合は、一部の変数をブラックボード外に出すことを検討してください。 - ブラックボードの主な用途は、実行時に変更される変数です。変更が想定されない変数は、他のQuantumアセットに格納する方が適切です。これによって、フレーム内の実行時のデータが、ブラックボードの一部として作成されることを回避できます。
カーディナリティ
ブラックボードのアセットは読み取り専用であり、ブラックボードコンポーネントに適用されるレイアウトを定義するために、任意の数のエンティティで使用されます。
アセット自体はエンティティから参照されて、各エンティティのコンポーネント初期化後に、エンティティ自身の変数データを作成するために使用されます。
各ブラックボードアセットについて、アセットを参照するエージェント数nの時、カーディナリティは[1..n]となります。
簡単な使用例
「マップ内を走り回って、コインを3つ集めたら待機状態になる」エンティティを考えてみましょう。このエンティティのAIでは、収集したアイテムの数を追跡すると便利なことは明らかです。
そのため、ブラックボードにはCollectedCoinsAmountという名前のint型エントリを新しく作成します。
その後、エンティティが新しいコインを収集するたびに、ブラックボード上の値を1増加し、値が3になったかどうかを判定します。ブラックボードからのデータ取得は辞書検索に基づく高速な操作になるため、AIの意思決定プロセスの一環として、判定を毎フレーム実行しても問題ありません。
各エンティティのブラックボードコンポーネントは、自身のデータを保持するため、あるエンティティが別のエンティティのブラックボードを読み取ったり、独立したシステムがすべてのエージェントの値を読み取ったりすることも可能です。
ビジュアルエディター上のブラックボード
ビジュアルエディターには、ブラックボード用のサブメニューが備わっていて、左サイドパネルに「Blackboard Variables」があります。
新しいブラックボード変数を作成するには「+」ボタンを押してください。
エントリをダブルクリックすると編集が可能です。
右クリックメニューからも同様の操作が可能です。
新しいエントリを作成/編集したら、以下の要素を定義してください。
Name:内部的なキーの生成に使用され、辞書の変数の作成/保存時に使用されます。Type:サポートされている事前定義された型を、ドロップダウンメニューから選択できます。HasDefault:セットアップ時に変数をデフォルト値で初期化するかどうかを示すチェックボックスです。Default:デフォルト値です。
エンティティのAIにはどのブラックボードエントリが必要なのかは、自由に決められます。
補足:ブラックボードエントリを右クリックすると、コンテキストメニューを開くことができます。
ブラックボードが現在サポートしている型は次の通りです。
BooleanByteIntegerFPVector2Vector3EntityRefAssetRef
ブラックボードが定義されると、AIドキュメントコンパイル時に2つのQuantumアセットが生成されます。デフォルトでは、Assets/Resources/DB/CircuitExport/Blackboard_Assetsフォルダーに出力されます。
AIBlackboardAsset:型とキーのレイアウトを持ちます。AIBlackboardInitializer:(適用可能なら)エントリの初期値を格納します。
補足:両アセットは相互参照しているため、コンポーネントを初期化するコードが簡単になります(例は後述)。
Blackboard Node(ブラックボードノード)
定義した変数をグラフにドラッグ&ドロップすると、ブラックボードノードを作成できます。このノードには2つの出力スロット(KeyとValue)があります。
keyスロットは、AIBlackboardValueKey型のフィールドをリンクするために使用されます。これによって、値を取得/設定する際、ハードコードされた文字列をキーに指定せずに済むため、コードの柔軟性と信頼性が向上します。
Quantumコードの観点から、値を取得/設定するメソッドについて、ハードコードされた文字列のキーを使用する場合と、ブラックボードノードのキーを使用する場合の違いを見てみましょう。
C#
// -- ハードコードされたキーを使用する --
var bbComponent = f.Unsafe.GetPointer<AIBlackboardComponent>(entityRef);
// 読み取り
var value = bbComponent->GetInteger(frame, "someKey");
// 書き込み
bbComponent->Set(frame, "someKey", value);
// -- ブラックボードノードのキーを使用する --
public AIBlackboardValueKey PickupsKey;
// 読み取り
var value = bbComponent->GetInteger(frame, PickupsKey.Key);
// 書き込み
bbComponent->Set(frame, PickupsKey.Key, value);
新しいフィールドを宣言すると、ビジュアルエディター上のブラックボードノードのKeyスロットを直接リンクすることができます。
前述のKeyスロットに加えて、同じ型のフィールドを定義する(例:整数型のブラックボード変数を、アクション・BTのリーフノード・反応曲線の整数型フィールドにリンクする)ために、Valueスロットをリンクすることができます。左パネルで定義されたDefault値が値として使用(対象ノードアセットにベイク)されます。
ブラックボードのコード実装
ブラックボードコンポーネントの初期化
ブラックボードコンポーネントを初期化する上で重要なことは、ビジュアルエディターでAIドキュメントをコンパイルした後に作成されるAIBlackboardInitializerアセットの参照を持つことです。
シミュレーションのコードでは、このアセットを使用してコンポーネントを初期化します。
C#
// -- ブラックボードのセットアップ
// まず、ブラックボードコンポーネントを作成する(または、エンティティプロトタイプで作成する)
var blackboardComponent = new AIBlackboardComponent();
// BlackboardInitializerアセットを取得する
var bbInitializerAsset = f.FindAsset<AIBlackboardInitializer>(blackboardAsset.BlackboardInitializer.Id);
// 静的な初期化メソッドを呼び出して、ブラックボードコンポーネントとアセットを渡す
AIBlackboardInitializer.InitializeBlackboard(f, &blackboardComponent, bbInitializerAsset);
// エンティティにブラックボードを設定する
f.Set(littleGuyEntity, blackboardComponent);
これで初期化は完了です。ここから、APIを使用してブラックボードの値の読み書きが可能になります。
C#
// 各ブラックボードの型(int, byte, FP, boolean, FPVector, EntityRef)に対してメソッドが存在する
blackboardComponent->GetInteger(frame, key);
// 値として渡されるデータ型に応じて、異なるセッターメソッドが存在する
blackboardComponent->Set(frame, key, value);
エンティティプロトタイプの使用
エンティティプロトタイプのコンポーネントを使用して、Unityエディターからブラックボードアセットを参照することもできます。AIBlackboardComponentを追加してBoardフィールドを定義し、シミュレーションの初期化ステップで必要に応じて使用します。
ブラックボードコンポーネントの破棄
ブラックボードコンポーネントを使用するエンティティを破棄する際は、メモリリークを避けるためにコンポーネントを解放することが重要です。
ブラックボードのメモリを破棄するには、blackboardComponent->Free(frame);を使用します。
コンポーネントのコールバックによる初期化/破棄
SDK付属のBotSDKSystemには、 OnComponentAdded/OnComponentRemovedコールバックを使用して、AIBlackboardComponentを初期化/破棄する例が含まれています。
補足:Bot SDKにはブラックボードを読み書きするサンプルコードが含まれています。BotSDK/Samplesに置かれているIncreaseBlackboardInt.cs・SetBlackboardInt.cs・HFSM.CheckBlackboardInt.csをご覧ください。