BehaviorTree を BT、Blackboard を BB と略します
この記事で伝えたいこと
- BT 上で定数が定義できるよ
- BT がデータドリブンな設計になるよ、サブツリーの再利用性が上がるよ
まず用意したのはこれ。Target に向かって移動を続ける、単純な思考ツリー。
Acceptable Distance | Acceptable Radius どちらも 300.0 という数値が入ってますが、これを変数化して共用にできないか、というのが動機の始まりです。
グラフで使用した BT ノードの説明
BTT_MoveTo ... 目標に対して AI 移動する。 ![BTT_MoveTo-EventGraph.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41105/4268b803-9fd7-4f8f-0121-007142713de4.png) BTD_CloseEnough ... 対象との水平距離が N 以下であるか判定する。 ![BTD_CloseEnough-PerformConditionCheckAI.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41105/b8f15f8f-6775-aa73-a58b-0be6ffa1dc0d.png)Blackboard で変数化する
変数定義なら Blackboard があるじゃない?
でもデフォルト値はセットできないんだよな。BP ではセットできるんだけどな~~~。
_人人人人人人人人人人人人人人人人人_
> そうだ、Service でセットしよう <
 ̄Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y  ̄
Service ノードを作成する
Activation AI で値をセットして、Deactivation AI でリセットする。処理がノードを通ると子ノード全域で値が有効になります。
親ノードにサービスをくっつけて、Distance キーに値をセットするように変更。
既存の実装も Blackboard Key Selector を使うように変更します。
Task や Decorator でもよくない?
- ノードの処理開始/終了時で値を管理したいので、タスクで初期化するだけだと NG(兄弟、同じ階層ノードで運用する、とした時に値がリセットされてないと使いにくい)。
- デコレータとはノードの見た目を分けたかった。
- サブツリー化したときに内包ノードを表示したくない(デコレータはサブツリーのトップ階層が親階層に表示される性質がある)。
BT のサブツリー化
処理が多くなってきたときに一つのグラフで管理してると、果てしなく長くなり大変です。
そんな時はまとめてサブツリー化とかよくやります。バトルとパトロールとかね、もっと細かい単位でも。
先ほどのツリーを修正しましょう。中身をサブツリー化して RunBehavior で実行する。サービスは親タスクにアタッチします。ついでに Distance を 500 に変更しました、これで間合いを遠くにとるように。
サブツリーの共通化、再利用
サブツリーは他のツリーでも再利用、BT ごとに変数を設定することができるようになりました。**セットできる変数は float に限りません。bool や GameplayTag などセットして AI 思考のスイッチも可能になります。**なかなか夢が広がったのでは?
値入力のインターフェースが使いにくい問題
上で実装した例だと、Service と値定義が対になっていて、複数をセットするには複数アタッチする必要があります。型も予め Service 側で固定する必要があり、Enum などは用途ごとに Service が増えていきそうです。
汎用的に配列で作った例。BlackboardComponent の SetValueAs~ と同様に用意する感じ。
C++ で BTNode を作るときの TIPS
ここからはオマケで、C++ で FBlackboardKeySelector を扱うときの TIPS をいくつか紹介。まぁエンジンで用意されてる BTNode たちを見るのが一番確実ですけど。。
KeySelector の型制限
BP で KeySelector を定義すると、BT 側で BBKey を選択するとき全てのキーが表示されてしまいます。キーの種類が増えると結構不便ですが、C++ で追加した KeySelector であれば型を制限することができます。
ActorToCheck.AddObjectFilter( // Actorのみ
this, GET_MEMBER_NAME_CHECKED( UBTDecorator_CloseEnough, ActorToCheck ), AActor::StaticClass() );
AcceptableDistanceKey.AddFloatFilter( // floatのみ
this, GET_MEMBER_NAME_CHECKED( UBTDecorator_CloseEnough, AcceptableDistanceKey ) );
KeySelector の None を許可
こちらも同じく C++ のみで、BBKey に None を指定できるようになります(指定しない場合でもプロパティ欄のリセットボタンを押せば None にできますが、カーソルが外れたときに None 以外が自動でアサインされます)。
利点は、キーによる指定とデフォルト値の二通りの運用ができることです。
// キーがセットされていれば BB から値を取得、そうでなければデフォルト値を使う
float Distance = AcceptableDistanceKey.IsSet() ?
BlackboardComp->GetValueAsFloat( AcceptableDistanceKey.SelectedKeyName ) :
AcceptableDistance;
BlackboardKeySelector 変数の初期化
C++ で定義した KeySelector は InitializeFromAsset で全て初期化する必要があります。
ResolveSelectedKey を処理しないと値が取得できない。
void UBTDecorator_CloseEnough::InitializeFromAsset( UBehaviorTree& Asset )
{
Super::InitializeFromAsset( Asset );
UBlackboardData* BBAsset = GetBlackboardAsset();
if ( ensure( BBAsset ) )
{
ActorToCheck.ResolveSelectedKey( *BBAsset );
AcceptableDistanceKey.ResolveSelectedKey( *BBAsset );
}
}