Behaviour Tree
イントロダクション
こちらは、Behaviour Treeの挙動と、我々の実装についての詳細がいくつか説明された動画です。
開始から1:15: イントロダクション;
1:15から21:40: Behaviour Treeの基本概念について;
21:40から最後: Decorator/Leaf/Serviceのサンプルノードコードを実際に見てみましょう
新規のBehaviour Treeを作成する
エディターの上部バーで(+)ボタンをクリックし、Behaviour Treeオプションを選択します。
BTファイルを保存するように促されます。 好きな場所に保存してください。 これにより、**ビジュアルエディター上で** 完了した作業を持続するのに使用するデータアセットが作成されます。注意: ここで選択する名前は、のちにビジュアルエディターで作業したものをコンパイルする際に生成されるもう一つのデータアセットの名前になります。
これは、Quantumシミュレーションから実際にボットを動作させるのに使用するデータアセットとなります。そのため、この段階でわかりやすい名前を選んでください。
ファイルを保存する際に、メインのBot SDKウィンドウがシングルノードで表示されますが、これが作業を開始するルートノードです。
ルートノード
名前が示す通り、ここがツリーの開始地点となります。BT Agantコンポーネントで参照されるメインのアセットで、何が初めに実行すべきコンポジットまたはリーフノードなのかを定義します。
ルートノードは、子を1つしか持てません。ルートノードを他のノードにリンクさせるには、マウスのカーソルをルートノードの下部に合わせて、表示される「+」ボタンをクリックします。リンクのプロセスが始まります。
まだ他のノードを作成していない場合は、空白のスペースをクリックするとノード作成パネルが表示されます。
ノードのステータス
Behaviour Treeのほぼすべてのノードにはそれぞれのステートがあります。これでBehaviour Treeのフローがほぼ定義されるのでとても重要です。
ステータスは以下の様になります:
- Success(成功): ノードが行うべきタスクを問題なく実行する場合。ノードがSuccessを返すと、親ノードへのツリーでコントロールが上に上がります。これをもって子のタスクが成功したことがわかり、その情報に基づいてフローを定義することができます。;
- Failure(失敗): ノードがタスク実行に失敗した場合。これが発生した場合のコントロールは、結果がSuccessの時と同じようにコントロールが上に上がります。;
- Running(実行中): 特定のフレーム上でノードがタスクの実行に成功も失敗もしなかった場合、ノードには、実行を親ノードに返す前により多くの実行フレームが必要です。そして一度に実行中ノードは1つのみはBTエージェント上でキャッシュされ、継続的なフレームで結果が変わるまで繰り返し実行されるからです。
- Inactive(無効): 内部的な使用のみのため、気にしないでください。
自分のゲーム特有のノードをコーディングする場合、成功、失敗、実行中のタイミングを自分で決めることになります。これはBehaviour Treeのどのブランチをとるかに直接影響します。
ノードのタイプについてと新規ノード作成の方法については、後で詳しくお話しします。
新規ノードを作成する
新規ノードを作成する方法は2通りあります。
- エディタウィンドウの空白のスペース上でマウスを右クリックし、表示されるコンテキストメニューから作成
- ノードの新しいリンクを初期化し、空白のスペースをクリックして作成
それでは、作成できるノードのタイプを見ていきましょう。
コンポジットノード
Behaviour Treeのフローコントロールのメインソースです。次に考えられる実行すべきノードを定義し、それぞれ異なる挙動をします。
コンポジットノードは子ノードを左から右へ実行しようとします。つまり、優先順位がこのように定義される ということです。
コンポジットノードは、0から多く の子ノードをリンクできます。リンク先の子ノードはコンポジットノードまたはリーフノードです。
セレクタノード
OR 演算子に相当します。
- 子が成功するとすぐに成功します。その瞬間、実行が停止されコントロールがセレクタの親ノードに向かい、結果は「Success」となります。実行されるべき子が残っている場合はこのイテレーションでは実行されません。;
- 失敗するのは全ての子が失敗した場合のみです。セレクタが「Failure」の結果を親ノードに返します。
シーケンスノード
AND 演算子に相当します。
- 子が失敗するとすぐに失敗します。「Failure」の結果を親ノードに返し、残っている子はこの時点であ実行されません。
; - 全ての子が成功したときのみ成功します。その場合にはシーケンスノードが「Success」の結果を親ノードに返します。
デコレータでインタラプトする
ノードのステータス部分で説明した通り、LeafノードのステータスがRUNNINGである場合、特定のLeafノードがキャッシュされ次のフレームで再実行されることになります。ただし、これはある条件に基づいてそのLeafノードの実行をインタラプトする方法がいくつか必要であることも意味します。例えば、FPSキのャラクターが目標を撃っていて、突然自分の方向に手りゅう弾が投げ込まれたことに気づいたときなど、撃つのをやめて手りゅう弾を浴びないように避難しなければなりません。
既に 確認されたデコレータは、全てのフレームで再確認されるわけでは ありません。ただし、上記で説明したようにデコレータが再度実行されLeafノードをインタラプトできるような特別な状況が必要な場面があります。
インタラプトの確認には下記の2通りの方法があります。:
動的コンポジットノード
お気づきかもしれませんが、全てのコンポジットノードにはIsDynamic
フィールドがあります。これはブーリアンで編集時にトグルすることができます。:
コンポジットノードが動的である場合、この特定のコンポジットが実行中の現在のサブツリーの一部である間、全てのDecoratorsは各フレームごとに再チェックされます。デコレータのうちのどれかが失敗すると、現在のリーフノードが中止しそのコンポジットノードはFailureという結果となります。
これによるメリットは、各ティックで再評価されるべきコンポジットノードを自分で選択できる点です。そのため自分のツリーを最適化を管理しやすくなります。
リアクティブデコレータ
Blackboardに依拠するデコレータノードがある場合は、それらのデコレータが特定のBlackboardエントリでの変更を監視するようにできます。
このメリットは、再度になりますが、デコレータ確認を全てのティックで行わず、そのBlackboardエントリに何かしらの新しいものを設定した際にのみ行う点です。
例えば、Blackboardの整数「A」がBlackboardの整数「B」よりも大きいか確認する「CompareIntegersDecorator」があります。リアクティブデコレータを使用すると、確認が適用されるのはコードのどこか一部でBlackboardのエントリ「A」または「B」に変更を加えた時のみになります。
インタラプトが発生すると、現在の実行をどのようにアボートするか選択できます。アボート方法は以下の3通りあります。
- Self: 現在のノードの実行を停止しアボーションノードに到達するまですべてを中止する;
- Lower Priority: 現在のノードは実行をし続けるが、兄弟ノードの実行を中止する;
- Both: 上記の両ロジックを適用
デコレータノード自体にアボートの種類を定義することもできます:
リアクティブデコレータはシミュレーションコードに設定するものなので、詳細についてはBehaviour Treeのコーディングセッションを参照してください。
リーフノード
Behaviour Treeの最下層ノード。
ほとんどのゲーム特有ロジックの処理をつかさどります。実行時に返されるステータスに大きく依拠しています。
リーフノードの簡単な例:
- Wait Node: 一定の時間が経過するまで実行し続けます。タイマーが終了するとSUCCESSを返します。;
- Chase Node:
実行している間、ターゲットのエンティティに向けてBTエージェントを動かし続けます。Agentがターゲットに到達できた場合、SUCCESSを返します。何かしらでAgentがターゲットに到達するのが阻まれると(例:ターゲットが破壊されたり、別のnav meshリージョンにあるなど)、FAILUREとなります。; - Debug Node: コンソールにメッセージを表示し、常にSUCCESSを返します。;
つまり、返すべきStatusは完全に何をしたいのかによって異なります。
**PS:**SDKで提供されるリーフノードの一つはWaitLeaf
です。このノードが適切に動作するようにするには、BotSDKTimerSystem
を有効化してください。これは経過時間をカウントするのにWaitLeaf
ノードで使用されます。
デコレータノード
デコレータはコンディショナルノードです。どのブランチを実行すべきかの定義を支えるものです。
Statusを返すことができるだけでなく、このタイプのノードはブーリアンも返します。ブーリアンの結果がTruwであればSuccessに繋がり、結果がFalseであればFailureとなります。
デコレータノードはアタッチしているサブツリーの実行をブロックまたは許可できます。それらはコンポジットノードとリーフノードの中に追加されるため、実行前に何かしらの条件を考慮に入れるノードを選択して表現することができます。
デコレータノードの例:
- Has Ammunitionノード: BTエージェントの武器に弾が1つ以上ある場合、TRUEを返す;
- Has Targetノード: BTエージェントのメモリ(またはそのBlackboard)に定義された目標がある場合、TRUEを返す;
- Cooldownノード:最後の「T秒間」で特定のノードが実行されなかった場合のみ、TRUEを返す。
つまり、デコレータはブランチを許可したりブロックしたりするのに使用します。例えば、「Has Ammunitionノード」は「撃つ」ブランチを実行させたり、またはそれをブロックして「武器をリロードする」ブランチにツリーを繋げるのに使用できます。
デコレータを定義するには、いずれかのコンポジットノードかリーフノード をダブルクリックします。するとサブグラフが表示されます。ここに指定された順番で実行されるデコレータのリストがあります。マウスの右ボタンで新規のデコレータを作成し、デコレータルートノードにリンクします。下記の画像を参照ください。:
ノードのサブグラフで定義されたデコレータはデコレータリストのトップレベルビューで確認できます:
サービスノード
主に直接Behaviour Treeのフローに影響しないヘルパーノードとして使用します。サービスノードは通常、何も返さずにゲームステートを変更するのに使用します。
サービスノードはステータスを返さない唯一のノードです。
Decoratorと同じように、サービスノードもコンポジットノードやリーフノードに追加することができます。ノードのサブグラフに移動してサービスルートノードに作成・リンクしてください。
サービスノードの重要な特徴は、時間のインターバル内で実行される ということです。全てのサービスノードにはIntervalInSec
フィールドがあり、編集時にそのサービスをどれほど頻繁に実行するか定義できます。パフォーマンスを管理しやすくなるので便利です。
PS: サービスノードを使用するには、 BotSDKTimerSystem を有効化してください。これはノードの時間カウント方法です。
サービスノードの例:
- Update Target Position Node: 時に応じて、Agentが移動する位置がアップデートされます。これはNavMesh()に関連させることも、特定のエンティティを追いかけるのに関連させることなどもできます。;
- Perform Jump Node: 時に応じて、Agentはジャンプを実行します。;
サービスノードは、ステータスを返す必要がなく、要求されるどのようなノードにもアタッチでき、デカップリングのヘルプもできるためヘルパーとして使用されます。
サービスノードは実行中のサブツリーの一部として保存される 点にご注意ください。サービスノードを持つセレクタノードがあり、現在特定のリーフノードの実行によって「スタック」している場合、セレクターノードに含まれるノードはそのサブツリーが最新でなくなるまで実行を続けます。つまり、ツリー全体は目標の位置が最新になっているかどうかできまります。それから持っている初めのセレクタノード・シーケンスノードにUpdateTargetPosition
ノードを追加できます。または目標の位置を必要とするのがツリーの一部のみである場合はちょうどその部分にサービスノードを追加できます。とても柔軟です。
Decoratorsと同じように、トップのグラフビューでサービスノードリストを見ることができます。:
## Behaviour Treeをコンパイルする作成したBehaviour Treeを実際に使用するために、作業をコンパイルする必要があります。
コンパイルには2通りの方法があります。
- 左のボタン:現在開いているドキュメントのみコンパイルする;
- 右のボタン:プロジェクトにある全てのAIドキュメントをコンパイルする
Behaviour Treeのファイルは「Assets/Resources/DB/CircuitExport/BT_Assets」に格納されます。
AIを設定してBotで使用できるようにする
いよいよ作成したAIを使用するのに、必要なのはコンパイルしたアセットを参照することです。
アセットベースのGUIDを読み込むか、 AssetRefBTRoot
を作成して希望するAIアセットにポイントします。
Behaviour Treeのコーディング
エンティティにコンポーネントを追加する
自分のエージェントをセットアップするため、BTAgent
コンポーネントをエンティティプロトタイプに直接追加するか、またはvar btAgent = new BTAgent()
経由でコンポーネントを作成してコードから追加します。それから、選択したエンティティにframe.Set(myEntity, btAgent)
でセットします。
BTAgentを初期化する
自分のアプリケーションに合わせたタイミングで以下を呼び出します。
C#
var btRootAsset = f.FindAsset<BTRoot>(btReference.Id);
BTManager.Init(f, myEntity, btRoot);
パラメータは以下の通りです。
- フレーム;
- BTAgentコンポーネントを含むエンティティ;
- ビジュアルエディタから作成したBTRootアセット;
これでエンティティのBTAgentが初期化されました。システムのアップデートでアップデートメソッドを呼び出すだけです。
C#
BTManager.Update(f, myEntity);
これをもって、AIがビジュアルエディタ作成したフローを実行します。
ノードのコーディング
ノードタイプの多くは同じクラスから継承されているため、上書きして自分のカスタムコードを作成できる、とても似ているAPIを共有しています。
Behaviour Tree開発初期段階では、リーフノード、デコレータノード、サービスノードのみを実装するように 推奨します。そして新規のコンポジットノードの実装が必要な場合は、特に注意してください。このタイプのノードはBehaviour Treeの動作について少々知識が必要とされるからです。新規のコンポジットノードの追加を依頼することもでき、ゲーム特有ではなく通常使用されるノードとして成り立つようであれば我々の方で実装してデフォルトのSDKの一部として提供します。
自分のデコレータノード・リーフノードを開始する
- 新規のデコレータノードを作成するには、
BTDecorator
から継承する新しいクラスを作成します; - 新規のリーフノードを作成するには、
BTLeaf
から継承する新しいクラスを作成します; - 新規のサービスノードを作成するには、
BTService
から継承する新しいクラスを作成します;
;
**重要:**上記のクラスはすべて、[System.Serializable]
としてマークする必要があります。
デコレータノードおよびリーフノード用のAPI
Init
は、BTManager.Init
が呼び出されたときに一度呼び出されます。特定のノードデータにスペースを配布するのに使用します。これについての詳細は下にあるノードデータの章をご確認ください。
;OnEnter
は、特定のノードがノードのアップデートが実行される前に訪問された瞬間に呼び出されます。タイマーFPの保管などデータのセットアップに役立ちます。WaitLeaf
クラスには、エージェントへのタイマー情報保管方法についての例があります。ただし、これについてもノードデータの章で詳しく説明されています。
;OnUpdate
はリーフノードが実行されているティックごとに呼び出されます。BTStatus
を返すので、どのような時に、そしていつSuccess/Failure/Running
を返すようにするか選択することができます。常にSuccess
を返すとても簡単な例をDebugLeaf
クラスでみることができます。WaitLeaf
クラスにあるサンプルはもう少し複雑で、結果はRunning
またはSuccess
になります。OnExit
はノードがジョブを完了した際、またはアボートされた場合は実行がツリーの上部へ上がったときに呼び出されます。必要に応じてデータの初期化を取り下げるのに使用できます。;
デコレータノードについて
デコレータノードにはオーバーライド可能なメソッドがあります。
DryRun
はノードのアップデート中に呼び出されます。自分のゲーム特有のニーズによってBoolean
を返します。
デコレータを実装する際、通常DryRun
メソッドの実装により関心が向くのが普通です。通常デコレータが返すのはSuccessまたはFailure
で、これはそのままDryRun
が返したのがTrueかFalseか
次第だからです。そのため、OnUpdate
メソッドの実装によってこれを変更する必要が生じるのは稀なケースではありますが、必要なものだといえます。
サービスノード用のAPI
OnUpdate
はサービスノードが実行されたときに毎回呼び出します。これはビジあるエディタで定義されたインターバルに依拠します。
ノードデータ
独自の整数またはFPデータが必要なノードがあります。これらのデータはゲームステートに追加するものでノード自身によってのみアップデートされます。
いくつかの重要なノードに共通していますが、BTAgent
コンポーネントには既にこれらのノード特有のデータ用のストレージがあります。
例:
- コンポジットノードは、実行されている現在の子インデックスを保管する必要があります;
- The
WaitLeaf
ノードは待機が終了となる時間の値となるものを保管する必要があります;
同じように、実行中に変更する必要のある自分のノードにデータが必要な場合があります。
ただし、ノードはデータアセットであり、実行中にフィールドを変更することはできない 点に留意してください。フレームデータとして保管されているデータを持ち、そこから変更する必要があります。
整数とFPフィールドでは、BTDataIndex
タイプを使用することで簡単に取得できます。この構造はビジュアルエディタでコンパイルプロセスの間に事前にベイクされ、ノードアセットにある全てのBTDataIndex
が一意のインデックス値を持つことを保証します。
自分のノードにこのような揮発性データが必要だと 場合は、手順通り以下に従って処理してください。:
BTDataIndex
タイプの新しいフィールドを、そのインデックスが何を表しているのかわかるようにわかりやすい名前で作成します。例えば、WaitLeaf
コードでのフィールド宣言はpublic BTDataIndex EndTimeIndex;
です。このノードは実行中にEndTimeを読み取り・書き込みする必要があるためです。;Init
メソッドでは、btAgent->AddFPData()
またはbtAgent->AddIntData();
を実行することでデータをBTAgentに配賦します。保存したい初期値をパラメータで通知します。;- BTAgentからデータを読み込むには、
p.BtAgent->GetFPData(frame, EndTimeIndex.Index)
を実行します。ここではEndTimeIndexはWaitLeaf
ノードを形成したサンプルです。; - BTAgentにデータを書き込むには、
p.BtAgent->SetFPData(frame, endTimeValue, EndTimeIndex.Index);
を実行します。
リアクティブデコレータコーディング
前のトピックの説明にある通り、Blackboardエントリを監視するデコレータがある場合、リアクティブデコレータとして登録することができます。そのため、監視されたエントリに変更が適用されると、毎回デコレータが再確認され現在の実行が途中で停止することになります。
コードの観点から、リアクティブデコレータを使用するには以下のステップに従ってください。
- デコレータクラスの
OnEnter
メソッドで、監視したいエントリに応じて必要なすべてのエントリ にデコレータを登録します。:
// --> Sample from BTBlackboardCompare
// We let the user define, on the Visual Editor, which Blackboard entries
// shall be observed by this Decorator
public AIBlackboardValueKey BlackboardKeyA;
public AIBlackboardValueKey BlackboardKeyB;
public override void OnEnter(BTParams p)
{
base.OnEnter(p);
// Whenever we enter this Decorator...
// We register it as a Reactive Decorator so, whenever the entries are changed,
// the DryRun is executed again, possibly aborting the current execution
p.Blackboard->RegisterReactiveDecorator(p.Frame, BlackboardKeyA.Key, this);
p.Blackboard->RegisterReactiveDecorator(p.Frame, BlackboardKeyB.Key, this);
}
OnExit
で、デコレータの登録を解除します。
// --> Sample from BTBlackboardCompare
public override void OnExit(BTParams p)
{
base.OnExit(p);
// Whenever the execution goes higher, it means that this Decorator isn't in the current subtree anymore
// So we unregister this Decorator from the Reactive list. This means that if the Blackboard entries
// get changed, this Decorator will not react anymore
p.Blackboard->UnregisterReactiveDecorator(p.Frame, BlackboardKeyA.Key, this);
p.Blackboard->UnregisterReactiveDecorator(p.Frame, BlackboardKeyB.Key, this);
}
- リアクティブデコレータのリアクションをトリガーしたいBlackboardエントリを変更すると毎回以下の様になります。:
blackboard->Set(f, "SomeKey", someValue)->TriggerDecorators(p);
フィールド値を定義
Actions/Decisionフィールドに値を設定する際の別の方法については、フィールド値を定義でさらに情報を参照いただけます。
AIParam
手動で設定したりBlackboardノード/Constantノード/Configノードから設定したりできる様々な方法で定義できるより柔軟なフィールドにしたい場合に便利な、AIParamの使用についての詳細はAIParamを参照ください。
AIContext
エージェントコンテキストの情報をパラメータとして渡す方法については、AIContextを参照してください。
BotSDKSystem
Blackboardメモリの割り当て解除などのプロセスの自動化に使用するクラスがあります。詳細はBotSDKSystemを参照してください。
デバッガ
Bot SDKは独自のデバッグツールを搭載しています。そのため、開発者はランタイムでBTAgentを選択しVisual Editorで注目されている最新のエージェントフローを確認することができます。Bot SDKプロジェクトで使用しているデバッグツールの一例をご紹介します。
:
- ブルー = 現在実行中のサブツリー。ブルーのリンクはこれまでに辿られたパスを示しており、最も濃いブルーのノードは現在実行中のものです。
- グリーン =アプリケーションのその時点において成功している各サブツリー。Compositeノードは、子ノードが成功した場合のみグリーンになる点に注意してください () (Sequenceでは全ての子ノードが成功すること、Selectorでは少なくとも1つの子ノードが成功していることが必要です)。
- レッド = アプリケーションのその時点において成功していない各サブツリー。
- グレー = 未到達で、後から到達される可能性のあるブランチ。
デバッガの使用
自分のプロジェクトでデバッガを使用するための手順です:
- 自分の
SystemSetup.cs
ファイルでBotSDKDebuggerSystem
を有効にする。この特定のシステムの使用は任意で、どこかにデバッギングロジックを持っておきたい場合は自分のカスタムシステム内の認証済みフレームでBotSDKDebuggerSystem.OnVerifiedFrame?.Invoke(f);
を呼び出しを行う。; - ビジュアルエディタで一番上のパネルにある虫のアイコンをクリックする。アイコンが緑色の時は、デバッギングが有効。
どのエンティティをデバッグするか選択するには2通りの方法があります。選択済みのGame Objectと関連付けることもできるし、インスペクターウィンドウで選択することもできます。このうち1つを選んでもいいし、両方組み合わせることもできます。:
デバッグ用インスペクターウィンドウからデバッグする:
- シミュレーション側では、Agentエンティティをデバッガ・ウィンドウに登録する必要があります。以下のように呼び出して行うことができます。
BotSDKDebuggerSystem.AddToDebugger(entitiRef, (optional) customLabel)
デバッグされるエンティティに表示されるデフォルトの名前は、Entity XX | AIAssetName
というパターンに従います。デバッグエントリにカスタムの名前を付けたい場合には、customLabel
パラメータを使用できます。好きな名前を付けることができます。
階層を作成することも可能です。カスタムラベルにセパレータ /
を使用するだけで、デバッガウィンドウ上に階層が作成され、折りたたんだり展開したりすることができます。
- Unity上で、デバッガ起動ボタンの右側にあるボタンをクリックします。 登録されているすべてのエンティティを表示する新しいウィンドウが開きます。デバッグしたいものを選択します。
例として、上記のサンプルGIFに使用されたカスタムラベルの一部を紹介します:Monster 1, Monster 2, Blue Team/Commander, Blue Team/Warriors/Foo, Blue Team/Warriors/Fuz and Blue Team/Wizards/Bar
**重要:デバッガを起動すると、デバッグに必要なデータを格納するためのメモリが割り当てられ、エディタからプレイしている場合はゲームの動作が遅くなる可能性があります。**そのため、Unity内でアプリケーションのプロファイリングを行う場合、プロファイリングの一部がデバッガに関連している可能性がありますので、プロファイリング中はデバッガを無効にすることを検討してください。
PS: デバッガウィンドウには、エンティティビューを持たない エンティティも表示されるため、それらのBTをデバッグするにはこの方法を用います。;
PS2: 現在、エンティティにリンクされていない、DSLグローバルにあるようなエージェントのデバッグはできません。以降のバージョンで追加される予定です。
Visual Editorのコメント
Visual Editorでのコメント作成方法についての詳細は Visual Editorのコメントを参照ください。
コンパイル出力フォルダの変更
デフォルトで、Bot SDKのコンパイルで生成されたアセットはAssets/Resources/DB/CircuitExport
フォルダにおかれます。出力フォルダの変更方法については、出力フォルダの変更を参照してください。
保存される履歴サイズの選択
Bot SDKファイルにいくつ履歴エントリ保存するか変更することができます。こちらについての詳細は保存される履歴サイズの選択を参照してください。
Back to top