共通の概念
このページでは、一部の概念が複数のBot SDK AIモデルで共通である点を記載します。
アクションの定義
HFSMでは、各ステート内にサブグラフがあります。GOAPでは、各タスクにサブグラフがあります。これらのサブグラフは、ステート/タスクをダブルクリックすることでアクセス可能です。
これらのサブグラフでは、アクションを作成 できます。
サブグラフにアクセスすると、トップバーにが表示されます。これによって、現在階層のどこにいるかが示されます。
これらのボタンを使用して、階層の前のレベルに進むことができます。
サブグラフ上に重要なノードが定義されています:Actions Rootノードです。
HFSM上
ここでは、実行されるアクションについて3つのリストがあります:
- On Enter リストは、HFSMがこのステートに入った場合に実行されるアクションを定義します。
- On Update リストは、HFSMがアップデートされるたびに実行されるアクションを定義します(通常は、毎フレーム)。
- On Exit リストは、HFSMがこのステートを脱した場合に実行されるアクションを定義します。
GOAP上
ここでは、実行されるアクションのリストが1つあります:
- On Update リストは、GOAPがアップデートされるたびに実行されるアクションを定義します(通常は、毎フレーム)。
これらのアクションリストを定義するにはActions Rootノードの右側にある矢印をクリックし、アクションのインバウンドスロットをクリックします。または、空のスペースをクリックして
新しいアクションをただちに作成してください。
重要なのは、リンクされたアクションは必要なだけ作成できる点です。これらのアクションは同じフレームで順番に実行されます。
必要に応じて、アクションの順番を変更できます。
また、アクションを実行したくなく、かつそれらを後に利用するため削除したくない場合には、まとめてデタッチできます。
インバウンドスロットをクリックすることで、アクションフィールドを定義できる点に留意してください。
変更を適用するには Enter を押し、変更を削除するには Esc を押して ください。
すでに確認したとおり、エディタには事前に定義されたアクションやデシジョンが含まれています。
これらを使用して、ディベロッパーはすぐに作業を始めることができます。
プロジェクトでは、独自のアクションやデシジョンを実装する必要があります。
以下で、その方法を説明します。
アクションをコーディングする
新しいアクションを作成するには、quantum_code ソリューションを開いてください。
quantum.code プロジェクトでは、AIAction
抽象クラスから継承する任意のクラスを作成できます。
これをおこなうにはUpdate
メソッドを実装する必要があり、このメソッドはエージェント(HFSMまたはGOAP)がアップデートされた場合に常に呼ばれます。
HFSM向けには、定義したアクションリストで OnEnter、OnUpdate、OnExit が実行されます。たとえば、一部のHFSMステートに入る場合、このコードは初期ステートにリストされたアクションのためにも実行されます。
重要: 新しいクラスを[Serializable]
および partial
としてマークする必要があります。
C#
namespace Quantum
{
[Serializable]
public partial class IdleAction : AIAction
{
public override unsafe void Update(Frame f, EntityRef e)
{
// insert the action code here
}
}
}
フィールド値を定義
Bot SDKでは、コード上でパブリックフィールドを宣言することができ、これらのフィールドはビジュアルエディター上に表示されます。HFSMではこれをアクションおよびデシジョンコード向けに、GOAPではアクション向けにおこなうことができます。エディタ上で、ユーザーはそのフィールドの値を定義できます。
これらの値を簡単に設定するにはフィールドをクリックし、値を割り当てます。ただし、他の方法もあります:
- Blackboardノードを使用;
- Constantノードを使用;
- Configノードを使用;
- AIFunctionノードを使用.
BlackboardノードはすでにBlackboardドキュメントで説明されていますので、ここではConstant/Config/AIFunctionノードについて説明します。
定数パネル
左のサイドパネルで、定数を定義し使用できます。
定数を定義したら、定数ノードをアクションおよびデシジョンのインバウンドスロット(HFSM上)にリンクできます。単一の定数ノードをインプットとして複数回使用することが可能で、これによって同一の値で多くのフィールドを簡単に定義できます。
定数のもう1つの重要な面として、定数の値がセットアップメニューで変更されるたびに、その定数に由来するすべてのノードもアップデートされます。これによって、HFSMの様々な箇所にある値を簡単に定義でき、また後に変更することも可能です。
新しい定数を定義するには左側のメニューを使用し、Constantsタブの(+)記号をクリックしてください。
その定数のName
、Type
、Default Value
を選択し保存します。定義したこの定数をグラフビューにドラッグアンドドロップし、インバウンドスロットにリンクすることが可能です。
Configパネル
同じHFSM/GOAPを使用する多くのエージェントに対して、様々な定数の値を設定することができます。これは、たとえば様々な難易度のBotを設定し、それらの挙動ロジックを同一に設定したい場合などに便利です。初心者モードのシューターBotはリアクションタイムの値が「2秒間」で、上級者モードでは「0.5秒間」にしたい場合などに、Configパネルを使用すればこういった設定を簡単におこなえます。
Config値を作成するには左側のパネルを使用してください。作成したConfig値はその後、型AIConfigAsset
の新しいデータアセットにコンパイルされます。コンパイルされると、アセットはAIConfig_Assets
フォルダに<DocumentName>DefaultConfig
という名前のアセットで追加されています。
これらのアセットは定数値を取得するシミュレーションに使用できます。
非常に簡単なConfigレイアウトを作成してみましょう:
一から新たなconfigフィールドを作成するほか、定数をConfigに変換することもでき、またその逆も可能です。
ドキュメントのコンパイル後、結果として作成されるアセットは以下のとおりです:
このアセットにバリエーションを持たせるには、つまり様々な定数値を定義するには、UnityのProjectタブの右メニューを使用してCreate/Quantum/Assets/AIConfig
を開きます。
これによって非常に簡単なconfigアセットが作成され、このアセットはもとになるような別のconfigアセットを探します。Default Configフィールドに入力し、Update Configをクリックしてデフォルトのconfigアセットをミラーします。これによって、任意の値に変更可能となります。また、もとのconfigアセット値に戻したい場合には、Reset to Defaultをクリックしてください。
これらのconfigを使用するには、以下のような選択肢があります:
configアセットから直接configを読み込み、configキーを知らせてconfig型に応じた値を取得します:
var myBoolean = myConfig.Get("Key").Value.Boolean;
可能な型は以下のとおりです:Integer、Boolean、Byte、FP、FPVector2、FPVector3およびString
または、configをAIParam型とともに使用します。
たとえば:
まず、任意のAction/DecisionでAIParamFPフィールドを作成します。コンパイルすると、ビジュアルエディターに表示されます。
その後、左側のパネルからConfig値をドラッグアンドドロップし、AIParamフィールドにリンクします;
その後、ドキュメントをコンパイルします。コード上で、AIParam APIを使用して値を取得します。これによって、パラメータとして渡されたConfigアセットに応じてconfigの値を取得できます。
// Considering that the variable "AttackRange" is of type AIParamFP
FP rangeValue = AttackRange.Resolve(f, blackboard, myConfig);
このため基本的には、ビジュアルエディター上にあるものにもとづき、configパネルを使用してデフォルトのconfigアセットを作成する点が重要です。ここから変化したものを作成するには、AssetRefを使用して要望に応じてリンクします。
利便性を高めるため、HFSMAgentおよびGOAPAgentは特定のエージェント/エンティティのConfigアセットを参照するフィールドとともに提供されます。これを使用すれば、独自の参照を作成することが可能です。
// The config to be set can come from any source that you prefer. Some custom asset, RuntimeConfig, RuntimePlayer...it's up to you. Just set it to the component:
hfsmAgent->Config = config;
// or
goapAgent->Config = config;
// Then, when you need to GET the Config:
hfsmAgent.GetConfig(frame);
//or
goapAgent.GetConfig(frame);
AIParam
フィールド上の値のソースを定義するには、様々な方法があります。現状ではBlackboard、Constants、Configを使用して実施できます。このため、これらの値をコード上で読み込む方法も複数あります。結果的に、ディベロッパーがソース型を変更したい場合(たとえば、BlackboardノードからConstantノードに変更するなど)にはソースコードでの変更も必要となります。
なぜ、ソース型の変更が便利なのでしょうか?
- 簡単に、手動で値を定義できます。
- ランタイム時に値が変わる場合には、Blackboardに保存可能です。このため、Blackboardノードを使用して値を定義してください。
- 値は変わらないが、グラフの柔軟性を高めるために値をノードに由来するようにしたい場合には、Constantノードを使用してその値を定義してください。
- 上記に該当して、値をエージェントごとに変化させたい場合にはConfigノードを使用してください。
AIParam
は、ソースが突然変更するなどの状況をサポートするために作成された型です。ただし、使用法を習得する前に、値を読み込むコードの違いについて簡単に分析してみましょう。
なんらかのフィールドの値がビジュアルエディター上に手動で定義された場合、またはConstantノードを使用して定義された場合には、この値を読み込むコードは単純かつ簡単です:
C#
// In this case, the value is directly stored on the field itself, with no need for any extra code
public Int32 MyInteger;
値がBlackboardノードに由来する場合:
C#
// Read the value from the blackboard asset
var value = blackboardComponent->Board.GetValue("someKey");
Configノードの場合:
C#
// Read the value from the config asset
var myBoolean = myConfig.Get("Key").Value.Boolean;
このため、ビジュアルエディター上で値のソースが変更された場合にコードを変更しないで済むようにするには、フィールドにAIParam
型を使用してください。この型の主な特徴は以下のとおりです:
Resolve
メソッドがあり、blackboardおよびconfigアセットを取得します。このメソッドはフィールドの値のソースを把握し、正しい値を返します。返す方法は、値を直接返すか(フィールドが手動で定義されている場合)、またはBlackboard/Configから値を返すかのいずれかです。このため、値のソース型は必要に応じて何回でも変更でき、これを読み込むためのコードは以下のようになります:
C#
public AIParamInt MyAIParam;
var value = MyAIParam.ResolveResolve(frame, blackboard, aiConfig);
- 現在、設定可能な型は8つあります:
C#
AIParamInt, AIParamBool, AIParamByte, AIParamFP, AIParamFPVector2, AIParamFPVector3, AIParamString, AIParamEntityRef
- 内部的に、ビジュアルエディター上でAIParamがどのように定義されたかを確認します。手動で定義されたのか、または特別のノードから定義されたのか、などです。
AIFunction ノード
AIFunction ノードでは、様々なタイプの「ゲッター」ノードをあらかじめ定義することができます。主な目的は、ゲーム特有のニーズに合わせて値を返す特定のノードを作成することです。
AIFunction ノードの基本タイプは以下の通りです。
- AIFunctionByte;
- AIFunctionBool;
- AIFunctionInt;
- AIFunctionFP;
- AIFunctionFPVector2;
- AIFunctionFPVector3;
- AIFunctionEntityRef
独自のAIFunctionノードを作成するには、上記のいずれかのクラスを継承し、抽象的なExectue()
メソッドを実装するだけです。ここでは、カスタムコンポーネントに格納されているエンティティの位置を返すAIFunctionノードのサンプルを紹介します。
C#
namespace Quantum
{
[System.Serializable]
public unsafe class GetEntityPosition : AIFunctionFPVector3
{
public override FPVector3 Execute(Frame frame, EntityRef entity = default)
{
MyComponent myComponent = frame.Unsafe.GetPointer<MyComponent>(entity);
Transform3D* targetTransform = frame.Unsafe.GetPointer<Transform3D>(myComponent->TargetEntity);
return targetTransform->Position;
}
}
}
Quantumソリューションをコンパイルすると、コンテキストメニューからAIFunctionが利用できるようになります。また、AIFunctionクラスのパブリックフィールドを宣言することも可能で、ビジュアルエディタで直接入力することができます。
AIFunction ノードのリンクは、上記で説明したAIParam
タイプで行う必要があります。つまり、上記のAIFunctionクラスに基づいてエンティティの位置を取得する必要があるHFSMアクションの場合、AIParamFP
フィールドが必要になります。
namespace Quantum
{
[System.Serializable]
public unsafe partial class SampleAction : AIAction
{
public AIParamFP TargetPosition;
public override void Update(Frame f, EntityRef e)
{
// If you are not sure if your AIParam's source is a Blackboard/Config/AIFunction node, then use the general Resolve method
var position = TargetPosition.Resolve(/*args*/);
// If you are sure that the source is an AIFunction node, then you can use the specific Resolve method
var position = TargetPosition.ResolveFunction(frame, entity);
// Now, do something with the position
}
}
}
上の例で示したように、一般的なResolveメソッドを使用することもできます。このメソッドは、ビジュアルエディター(ブラックボード、config、AIFunctionノード)で定義されたSourceに応じた値を返します。しかし、AIParamがAIFunctionノードで定義されていることがわかっている場合は、特定のResolveメソッドを使用した方が、多くのパラメータを必要とせず、少し高速に処理できます。
AIFunction Nodeは、AIParam
フィールドを持つことができ、ネストしたAIFunctionsを作成することができます。
Visual Editor上
パブリックAIParam
がアクションおよびデシジョン上で宣言されるとビジュアルエディター上に表示され、その値を手動または特別なノードから定義することができます。たとえば、public AIParamInt IncreaseAmount
について検討してみましょう:
上記の型以外に、Enum用にAIParamを作成するうえでも非常に便利です。この作成をおこなうには、必要な特定のEnum向けに独自のAIParam型を作成する必要があります。コードスニペットは以下のとおりです:
C#
// Considering this enum:
public enum BotType { None, HFSM, GOAP };
// Create a new AIParam class based on that enum:
[System.Serializable]
public unsafe sealed class AIParamBotType : AIParam<BotType>
{
public static implicit operator AIParamBotType(BotType value) { return new AIParamBotType() { DefaultValue = value }; }
protected override BotType GetBlackboardValue(BlackboardValue value)
{
int enumValue = *value.IntegerValue;
return (BotType)enumValue;
}
protected override BotType GetConfigValue(AIConfig.KeyValuePair config)
{
return (BotType)config.Value.Integer;
}
}
AIContext
Bot SDKにはデータコンテナの実装があり、エージェントのアップデートルーチンでコンテキストの固有データを渡すのに役立ちます。
このようなコンテナの使用は必須ではありませんが、HFSMのAIAction.Update()
やBTの Leaf.OnUpdate()
などのユーザーエンドポイントからのデータ取得を容易にするために使用することが可能です。
これを使用する主な理由は、FrameとEntityRef以外の追加データをユーザーコードに提供するためです。こうすることで、たとえばframe.Get<MyComponent>(entityRef)
のような定型的なコードの多くを回避することができます。これらのコードは、エージェントの単一アップデートの際に、たびたび実施が必要な場合があります。
AIContextを使えば、アップデートルーチンの一番最初にデータを入れることができます(例: HFSManager.Update
を呼ぶ前など。BT、GOAP、UTも同様です)。
つまり、例えるなら、ここでのコンテキストの目的は、特定のエージェントのコンテキストに関連するデータでそれを埋めることです。おそらく、AIBlackboardコンポーネントを保存します。他のカスタムコンポーネントかもしれません。あるいは、エージェントの更新ロジックに何かを示す整数値かもしれません。
このため、実行に必要なコードスニペットをいくつか紹介します。*これらはHFSMの例ですが、BT、GOAP、UTも該当します:
AIContext構造体を拡張
AIContext.User.cs
; 新しいファイルを、任意の名前と場所で作成します。たとえばAIContext.User.cs
です。- その中で、必要なフィールドを持つAIContext構造体の
partial
宣言を作成します。
C#
namespace Quantum
{
public unsafe partial struct AIContext
{
public readonly HFSMAgent* HfsmAgent;
public readonly AIBlackboardComponent* Blackboard;
public AIContext(HFSMAgent* hfsmAgent, AIBlackboardComponent* blackboard)
{
this.HfsmAgent = hfsmAgent;
this.Blackboard = blackboard;
}
}
}
- コンテナのレイアウトの準備ができました。ここで、Agentが更新されるたびにコンテキストオブジェクトを作成し、それを
ref parameter
として渡します:
C#
AIContext aiContext = new AIContext(hfsmAgentComponent, blackboardComponent);
HFSMManager.Update(frame, frame.DeltaTime, hfsmData, entityRef, ref aiContext);
- これによって、AIActionのUpdateのようにエンドポイントがコンテキストにアクセスできるようになります:
C#
namespace Quantum
{
[System.Serializable]
public unsafe class SampleAction : AIAction
{
public override void Update(Frame frame, EntityRef entity, ref AIContext aiContext)
{
// either cash the data in local variables
var agent = aiContext.HfsmAgent;
var blackboard = aiContext.Blackboard;
// or use it right away where needed
}
}
}
重要な留意点
AIContext
を管理する方法については、十分に留意してください。他の方法もありますが、毎フレーム一から作成してデータを埋めるのが、最も安全な使用方法です。
また、コンテキストのおもな目的は、意思決定を促進するために、コンテキストに関するデータを読み取るすばらしい方法を提供することです。コンテキストにデータを保存して、それをその場で変更することは可能ですが、それをサポートするようには作られていません。好みに応じて使ってください。ただし、問題のトラッキングが困難にならないように、十分注意しておこなってください。
ミュート
各AIモデルについて、ミュート できる特定のノードがあります。これによって必然的に、何も削除されず/リンクされることなくコンパイル処理からロジックの一部が無効化されます。
様々なAIモデルで、何をミュートできるのか分析してみましょう:
HFSMに固有
ステートのミュート
ミュートされたステートへのトランジションは無視されます。また、ステート内のどのアクションも実行されません。
PS.: HFSMを、ミュートしたデフォルトのステートでコンパイルするとエラーが発生します。
ステートノードをミュートするにはステートで右クリックし、「Mute/Unmute State」を選択します。
ミュートされている間、ステートノードは透過にレンダリングされます。
トランジションをミュート
トランジションをミュートするには、トランジション行を右クリックして「Mute/Unmute Transition」を選択します。
コンパイルの間、ミュートされたトランジションは無視されます。
共通のトランジション、すべてのトランジション、トランジションセット、ポータルへのトランジションなど、任意の種類のトランジションをミュートできます。
GOAP特有
ミュートされたタスクは可能なタスクに追加されず、GOAPは計画を立てません。
タスクノードをミュートするには、ステートを右クリックして「Mute/Unmute Task」を選択します。
ミュートされている間、タスクノードは透過としてレンダリングされます。
HFSMおよびGOAP
アクションをミュート
アクションリストの位置に関わらず、アクションをミュートすることができます。
コンパイルの間、ミュートされたアクションは無視され、ミュートされたアクションに次のアクションが含まれる場合には、チェーンの次のアクションのみが実行されます。
アクションノードを右クリックして、このミュートをおこなえます。
デフォルトでBot SDKのパッケージに搭載されているクラスが2つあります:
BotSDKSystem
: エンティティプロトタイプに含まれるデータを用いてBlackboardメモリの割り当て解除、HFSM/BTエージェントの初期化などのプロセスの自動化に使用します。BotSDKDebuggerSystem
: デバッガやUnity側にとって重要な情報を収集するのに使用します。
これらを使用するには、新BotSDKSystem()、
そして新BotSDKDebuggerSystem()
を自分のSystemSetup
クラスに追加します。
PS: このシステムの使用は必須ではありません。ここでできることは全て自分独自のクラスでも行うことができます。
PS2: ここで行われたものが、既に自分の独自のコードのどこかで行われている可能性があります。このシステムの追加によって問題に繋がらないか注意してください。
システムを追加する前に、このシステムが何を行うのか、すぐに使用するか、自分のコード内でこのロジックからいくつかとって使用するのか考えてください。
Visual Editorのコメント
ビジュアルエディターに簡単にコメントを追加できます。任意のノードを選択し(States/Tasks/Actions/Decisions/Constantsなど)、「G」を押して コメントエリアを追加してください。
その後、「コメント」ヘッダーテキストをクリックし、任意のコメントに変更します。また、複数のノードにコメントを追加することも可能です。複数のノードを選択するには、Windows OSでは「Ctrl」ボタン、Mac OSでは「コマンド」ボタンを押下してください。
デフォルトでは、Bot SDKのコンパイルで生成されたアセットはフォルダAssets/Resources/DB/CircuitExport
に格納されます。これは、
フォルダAssets/Photon/BotSDK/VisualEditor/CircuitScriptables
に格納されているSettingsDatabase
という名前のアセットを選択することで変更できます。Bot SDK OutputFolder
という名前のフィールドを任意の値に変更してください。また、対象のフォルダがすでに作成済みである点を確認してください。
また、CircuitExport
という名前の親フォルダが常に作成され、そのフォルダ内にすべてのサブフォルダが作成されます。
保存される履歴サイズの選択
デフォルトでは、Bot SDKはビジュアルエディターのファイル内に履歴から5つのエントリを保存します。この機能は、AIドキュメントのクローズ/オープンを行う場合にセッション間の履歴を保持するうえで有用です。ただし、履歴エントリをいくつ保存するかにもよりますが、AIサーキットが大きくなるにつれてAIファイルのサイズも増加します。
このため、履歴エントリをいくつ保存するか選択可能です。履歴の保存が不要な場合には、ゼロと設定できます。Unityをクローズ/オープンする場合と同様に、履歴はAIファイルを開くとリロードのみされます。
履歴エントリの数を変更するには、フォルダAssets/Photon/BotSDK/VisualEditor/CircuitScriptables
に格納されたSettingsDatabase
という名前のアセットを選択してくだし。その後、Save History Count
という名前のフィールドを検索してください。