今回はビヘイビアツリーの基礎について説明します。
Contents
ビヘイビアツリーとは
ビヘイビアツリー(Behaviour Tree)とは、実行する挙動の優先度に着目した木構造のグラフです。
ルートから優先度順に子ノードを実行していき、子ノードの実行が成功したか失敗したかで分岐する機能を備えています。
子ノードの実行順序を制御するコンポジットノードと、ノードの末端に位置しアクションを行うアクションノードがあります。
この文章だけではイメージしづらいと思いますので、今回は以下について実践していきましょう。
- ビヘイビアツリーを使用する準備
- アクションを実行する
- Sequencerによってノードを順番に実行する
- Selectorによって子ノードの実行を選択する
チュートリアルプロジェクトの準備
今回のチュートリアルを実践するプロジェクトを準備しましょう。
動作環境
このチュートリアルは、以下の環境で作成しております。
Unity | 2019.4.40f1 |
Arbor | 3.9.0 |
Arbor3の準備
まだ購入していない場合はアセットストアで購入する必要があります。
購入する場合は以下リンクからアセットの商品ページが開けます。
チュートリアル用プロジェクト作成
チュートリアル用にプロジェクトを作成します。
プロジェクト名 | Arbor_BT_Basic |
テンプレート | 3D |
インポートするアセット | Arbor 3: FSM & BT Graph Editor |
プロジェクトの作成やArborのインポートについては「Arborを使用するための準備」を参照してください。
ビヘイビアツリーを使用する準備
ビヘイビアツリーを使用するには、GameObjectにBehaviourTreeコンポーネントを追加する必要があります。
BehaviourTreeコンポーネント付きGameObjectの作成
ここでは、BehaviourTreeコンポーネント付きのGameObjectを作成します。
- Hierarchyの+ボタンをクリック。
- メニューから「Arbor > BehaviourTree」を選択。
BehaviourTreeコンポーネントをArborEditorで開く
BehaviourTreeコンポーネントのグラフの編集はArbor Editorウィンドウで行います。
- Hierarchyから作成したBehaviourTreeオブジェクトを選択。
- InspectorウィンドウのBehaviourTreeコンポーネントにある「Open Editor」ボタンをクリック。
ルートノードについて
作られたばかりのビヘイビアツリーを確認すると、Rootという名前のルートノードが最初から作成されています。
ルートノードは一番最初に実行されるノードで、1つの子ノードのみを持てます。
ルートノードが実行されると接続している子ノードをすぐに実行し結果を待ちます。
アクションを実行する
ルートノードの子ノードに、一定時間待つアクションノードを作成してみましょう。
一定時間待つにはWaitアクションを使用します。
アクションノードについて
アクションノードはビヘイビアツリーの末端のノードであり、挙動を実行する役目を持ちます。
例えばNPCの移動や攻撃はアクションノードで組むことになります。
アクションの処理はActionBehaviourで行います。
ActionBehaviourについて
ActionBehaviourはアクションノードに紐づけられるC#スクリプトで、挙動の処理内容を実装します。
アクションノードには1つのActionBehaviourが紐づけられ、アクションノードが実行されると紐づけられたActionBehaviourに実装された処理を行います。
Waitについて
指定した秒数経過後に、親ノード(今回の場合はRootノード)へ実行結果を成功として通達します。
詳しくはWaitを参照してください。
Waitアクションノードを作成
- Rootノード下部のスロットをドラッグ。
- Rootノードの下のほうでドロップ。
- 表示されたメニューから「アクション作成」を選択。
- アクション選択ウィンドウから「Wait」を選択。
- 名前を「Wait1」に変更してEnterキーで確定。
Waitアクションの設定
今回は、1秒経過を待つこととします。
以下のようにWaitを設定してください。
Seconds | 1 |
動作を確認してみよう
一度プレイして動作を確認してみましょう。
プレイするとRootノードとWaitアクションが実行中を示すオレンジ色になります。
Waitアクションで1秒経過すると再度Waitアクションが実行されます。
Rootノードが完了したときの動作
Waitアクションが繰り返し実行されるのは、BehaviourTreeコンポーネントの「Restart On Finish」のデフォルト値がオンになっているためです。
「Restart On Finish」がオンの場合、Rootノードの実行が完了したら再スタートします。
試しにプレイ中に「Restart On Finish」をオフにしてみましょう。
- プレイ開始
- HierarchyからBehaviourTreeオブジェクトを選択。
- Inspectorウィンドウで「Restart On Finish」をオフにする。
「Restart On Finish」をオフにした後のWaitアクション完了時にグラフが再生停止になります。
このように「Restart On Finish」によってグラフが繰り返し実行されるかが決定します。
今回は試しにプレイ中にオフにしたため、プレイ終了すれば元に戻ります。
Sequencerによってノードを順番に実行する
ここまででルートノードに接続したアクションが実行されるまでを確認できました。
しかしアクションノードがひとつだけではこれ以上の複雑な処理はできません。
そこでルートノードとアクションノードの間にコンポジットノードを介すことで、より複雑な実行制御を行えるようにします。
ここでは、Sequencerコンポジットを使用して、複数のアクションノードを順番に実行してみましょう。
コンポジットノードについて
コンポジットノードはルートノードとアクションノードの間に組み込むノードであり、子ノードの実行を制御する役目を持ちます。
例えば、NPCがプレイヤーに近づくアクションを実行し、近くに来たら攻撃するアクションを実行するように制御します。
子ノードの実行順制御はCompositeBehaviourで行います。
CompositeBehaviourについて
CompositeBehaviourはコンポジットノードに紐づけられるスクリプトです。
コンポジットノードには1つのCompositeBehaviourが紐づけられ、コンポジットノードが実行されると紐づけられたCompositeBehaviourが子ノードの実行順を制御します。
Sequencerコンポジットについて
Sequencerは子ノードの実行順を制御するコンポジットの一種です。
複数持てる子ノードのうち、最初に一番左のノードを実行します。
子ノードの実行結果が成功だった場合は右隣のノードを実行し、以降成功するたびに右に向かって実行していきます。
右端の子ノードまで実行した結果すべて成功であれば、自身も親ノードに成功を返します。
実行した子ノードが失敗した場合は右隣の子ノードを実行することなく、自身も親ノードに失敗を返します。
Sequencerは子ノードを左から順番に実行しすべて成功したい場合に使用できます。
Wait1ノードの移動
今あるWait1ノードは後で使用しますので、邪魔にならない位置に移動しておきましょう。
- Wait1ノードのノードヘッダ部をドラッグして下のほうへ移動。
Sequencerコンポジットノードを作成
Rootノードの子ノードにSequencerコンポジットを作成します。
- Rootノード下部のスロットをドラッグ。
- Rootノードの下のほうでドロップ。
- メニューから「コンポジット作成」を選択。
- コンポジット選択ウィンドウから「Sequencer」を選択。
- 名前はそのままでEnterキーを押して確定。
Sequencerノードの子にWait1ノードを接続
もともとあるWaitノードをSequencerノードの子ノードとして接続しなおします。
- Sequencerノード下部のスロットをドラッグ。
- Wait1ノード上部のスロットにドロップ。
Wait1ノードを複製
Waitアクション一つのみですと動作にさほど違いがありませんので、もう一つWaitアクションを追加してみましょう。
- Wait1ノードの設定アイコンをクリック。
- メニューから「複製」を選択。
- 新しく追加されたWaitノードの名前部分をダブルクリック。
- 「Wait2」に名前を変更してEnterキーで確定。
Sequencerの子ノードに2つのWaitノードができました。
ノードの優先度について
ノードはRootノードから下方向に、左から右方向に向かって優先度が設定されています。
原則として優先度が小さいほど先に実行され、大きいほど後に実行されるようになります。
各ノードの優先度は、ノード右上に数値として表示されます。
今回の例では以下のようになっています。
- 0 : Root
- 1 : Sequencer
- 2: Wait1
- 3: Wait2
Sequencerは子ノードを左から順に実行するため、優先度2のWait1ノードを先に実行し、優先度3のWait2ノードをその次に実行します。
プレイ確認しよう
ここでプレイ確認してみましょう。
優先度2のWait1ノードが先に実行され、次に優先度3のWait2ノードが実行されるのが確認できます。
「Restart On Finish」はオンのため、二つのWaitノードが交互に実行され続けます。
Selectorによって子ノードの実行を選択する
子ノードの実行結果によって、次の優先度のノードを実行するかどうかを選択できます。
個別のノードの実行可否の判断はデコレータで行います。
ここではSelectorコンポジットとCalculatorCheckデコレータを使用して、実行するノードの選択を行ってみましょう。
Selectorについて
Selectorは子ノードの実行順を制御するコンポジットの一種です。
複数持てる子ノードのうち、最初に一番左のノードを実行します。
子ノードの実行結果が失敗だった場合は右隣のノードを実行し、以降失敗するたびに右に向かって実行していきます。
右端の子ノードまで実行した結果すべて失敗であれば、自身も親ノードに失敗を返します。
実行した子ノードが成功した場合は右隣の子ノードを実行することなく、自身も親ノードに成功を返します。
Selectorは子ノードのうち一つだけ実行を成功したい場合に使用できます。
デコレータについて
デコレータ(Decorator)はコンポジットノードとアクションノードに付加機能を持たせられるスクリプトです。
デコレータには多数の機能がありますが、ここではノードが実行可能かどうかの判定に使用します。
ノードが実行される際に、すべてのデコレータで条件判定が行われ結果がtrueになればノードが実行されます。
条件判定結果がfalseになった場合は、ノードが実行できないと判断され、親ノードに失敗を返します。
どのノードを実行するかを決めるのがデコレータの役目となります。
例えば、プレイヤーとの距離が設置値より近い場合に判定結果をtrueとすれば、プレイヤーとの距離が近い場合にのみ実行するノードが作れます。
CalculatorCheckデコレータについて
CalculatorCheckは演算ノードの結果を判定してノードの実行可否を決めるデコレータです。
判定に必要な計算を演算ノードで行い結果判定にCalculatorCheckを用いれば、デコレータ用スクリプトを作成することなくある程度複雑な条件判定が行えます。
詳しくはCalculatorCheckを参照してください。
Wait2ノードを移動する
SequencerとWait2ノードの間にSelectorノードを追加したいと思います。
まずは邪魔にならないようWait2ノードを移動させます。
- Wait2ノードのヘッダ部をドラッグし、下のほうに移動。
Selectorコンポジットノードを作成
- Wait2ノード上部のスロットをドラッグ
- SequencerノードとWait2ノードとの間付近でドロップ
- メニューから「コンポジット作成」を選択。
- コンポジット選択ウィンドウから「Selector」を選択。
- 名前はそのままでEnterキーを押して確定。
- ノードが重なるようなら適宜位置を調整。
SequencerノードとSelectorノードを接続
- Sequencerノード下部のスロットをドラッグ
- Selectorノード上部のスロットにドロップ
Wait2ノードを複製する
Selectorの子ノードにも2つのアクションが実行されるよう、もう一つWaitアクションを追加してみましょう。
- Wait2ノードの設定ボタンをクリック。
- メニューから「複製」を選択。
- 新しく追加されたWaitノードの名前部分をダブルクリック。
- 「Wait3」に名前を変更してEnterキーで確定。
Wait2ノードにCalculatorCheckデコレータを追加
- Wait2ノードの設定ボタンをクリック。
- メニューから「デコレータ追加」を選択。
- デコレータ選択ウィンドウから「CalculatorCheck」を選択。
CalculatorCheckの設定
前述のとおり、チュートリアルでの簡単な動作確認のために、演算ノードは用いずBool値のチェックボックスを直接クリックして動作確認します。
- Condition Listの+ボタンをクリック。
- メニューから「Bool」を選択。
- Bool Value 1のチェックをオンにする。
最終確認
最後にもう一度プレイ開始して動作確認してみましょう。
まず、何もせずに確認すると、Wait1アクションとWait2アクションが交互に実行されます。
この動作は以下のようになります
- 最初にRootノードが実行される。
Rootノードは一つしかない子ノードを強制的に実行する。 - Sequencerノードが実行される。
Sequencerノードは子ノードのうち一番左から順に実行する。 - Wait1ノードが実行される。
Secondsに設定した1秒が経過すると、親ノードに成功を返す。 - SequencerノードはWait1ノードの結果が成功だったため、右隣のノードを実行する。
- Selectorノードが実行される。
Selectorノードは子ノードのうち一番左から順に実行する。 - Wait2ノードのCalculatorCheckで実行の条件判定を行う。
Bool値が一致しているため、Wait2ノードのアクションを実行する。 - Wait2ノードが実行される。
Secondsに設定した1秒が経過すると、親ノードに成功を返す。 - SelectorノードはWait2ノードの結果が成功だったため、それ以上子ノードを実行せず、親ノードに成功を返す。
- SequencerノードはSelectorノードの結果が成功であり、右端のノードであったため、親ノードに成功を返す。
- Rootノードは子ノードの実行が完了したので、いったん終了する。
Restart On Finishがオンであるため、再度実行する。
では、プレイしたままでCalculator CheckのBool Value 1のチェックをオフにしてみましょう。
Wait1ノードとWait3ノードが交互に実行されるように変わりました。
この動作は以下のようになります
- RootノードからSelectorノードの実行までは前述のとおり。
- Wait2ノードのCalculatorCheckで実行の条件判定を行う。
Bool値が一致していないため、Wait2ノードのアクションを実行せずそのまま失敗として返す。 - SelectorノードはWait2ノードの結果が失敗だったため、右隣のノードを実行する。
- Wait3ノードが実行される。
Secondsに設定した1秒が経過すると、親ノードに成功を返す。 - SelectorノードはWait3ノードの結果が成功だったため、それ以上子ノードを実行せず、親ノードに成功を返す。
- SequencerノードはSelectorノードの結果が成功であり右端のノードであったため、親ノードに成功を返す。
- Rootノードは子ノードの実行が完了したので、いったん終了する。
Restart On Finishがオンであるため、再度実行する。
おわり
以上でArborチュートリアル「ビヘイビアツリーの基礎」は完了です。
Arborチュートリアル「ビヘイビアツリーの基礎」完了をツイート
次のステップ
AbortFlags(中断フラグ)については補足で多少触れましたが、このフラグを駆使することでより柔軟な挙動が組めるようになります。
このAbortFlagsを設定した場合にどのような動作になるかを次回「デコレータによる中断と割り込み」で確認してみましょう。