[UE] BehaviorTree の再利用について考える

:::note info
BehaviorTree を BT、Blackboard を BB と略します
:::

# この記事で伝えたいこと
- BT 上で定数が定義できるよ
- BT がデータドリブンな設計になるよ、サブツリーの再利用性が上がるよ

![BT_Test_1-Behavior Tree.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41105/4a261954-3fd0-1d07-84de-991816fb144b.png)
まず用意したのはこれ。Target に向かって移動を続ける、単純な思考ツリー。
Acceptable Distance | Acceptable Radius どちらも 300.0 という数値が入ってますが、これを変数化して共用にできないか、というのが動機の始まりです。

<details open>
<summary>グラフで使用した BT ノードの説明</summary>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)</details>

## Blackboard で変数化する
![79d41bb2df076e38e321a8775a91bc96.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41105/7d9082c5-e314-b2e5-2665-2e1fc2b29752.png)
変数定義なら Blackboard があるじゃない？
でもデフォルト値はセットできないんだよな。BP ではセットできるんだけどな～～～。
＿人人人人人人人人人人人人人人人人人＿
＞　そうだ、Service でセットしよう　 ＜
￣Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y ￣

## Service ノードを作成する
![BTS_ScopedBBEntry-EventGraph.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41105/a8cea0c4-642d-5ccf-56cb-00b058bb5dec.png)
Activation AI で値をセットして、Deactivation AI でリセットする。処理がノードを通ると子ノード全域で値が有効になります。
![BT_Test-Behavior Tree.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41105/677945b6-d1de-e446-8d87-5900f6669cf4.png)
親ノードにサービスをくっつけて、Distance キーに値をセットするように変更。
![BTT_MoveTo-EventGraph_0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41105/22860f13-e19b-6237-d4c5-df91f680f978.png)
既存の実装も Blackboard Key Selector を使うように変更します。

### Task や Decorator でもよくない？
- ノードの処理開始/終了時で値を管理したいので、タスクで初期化するだけだと NG（兄弟、同じ階層ノードで運用する、とした時に値がリセットされてないと使いにくい）。
- デコレータとはノードの見た目を分けたかった。
- サブツリー化したときに内包ノードを表示したくない（デコレータはサブツリーのトップ階層が親階層に表示される性質がある）。
![68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f34313130352f34386432363733302d626433342d386164652d366432352d3233643834663431633539362e706e67.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41105/f2705ebb-bda2-5802-dc49-d63ed40109bc.png)

## BT のサブツリー化
処理が多くなってきたときに一つのグラフで管理してると、果てしなく長くなり大変です。
そんな時はまとめてサブツリー化とかよくやります。バトルとパトロールとかね、もっと細かい単位でも。
![BT_Test-Behavior Tree_0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41105/d6eafd88-b46a-a3e0-31d3-60a5424a7c0e.png)
先ほどのツリーを修正しましょう。中身をサブツリー化して RunBehavior で実行する。サービスは親タスクにアタッチします。ついでに Distance を 500 に変更しました、これで間合いを遠くにとるように。

### サブツリーの共通化、再利用
![ReferenceViewer_BT_Child_0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41105/c2ff42ab-8c05-5421-ed8d-9316e8d92ce6.png)
サブツリーは他のツリーでも再利用、BT ごとに変数を設定することができるようになりました。**セットできる変数は float に限りません。bool や GameplayTag などセットして AI 思考のスイッチも可能になります。**なかなか夢が広がったのでは？

### 値入力のインターフェースが使いにくい問題
上で実装した例だと、Service と値定義が対になっていて、複数をセットするには複数アタッチする必要があります。型も予め Service 側で固定する必要があり、Enum などは用途ごとに Service が増えていきそうです。
![856724cf7abc874f91ffb67d2bacaab1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41105/751197b1-bfa2-deed-7c68-d635b131fff9.png)
汎用的に配列で作った例。BlackboardComponent の SetValueAs~ と同様に用意する感じ。

# C++ で BTNode を作るときの TIPS
ここからはオマケで、C++ で FBlackboardKeySelector を扱うときの TIPS をいくつか紹介。まぁエンジンで用意されてる BTNode たちを見るのが一番確実ですけど。。

## KeySelector の型制限
BP で KeySelector を定義すると、BT 側で BBKey を選択するとき全てのキーが表示されてしまいます。キーの種類が増えると結構不便ですが、C++ で追加した KeySelector であれば型を制限することができます。

```C++
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 以外が自動でアサインされます）。
利点は、キーによる指定とデフォルト値の二通りの運用ができることです。

```C++
// キーがセットされていれば BB から値を取得、そうでなければデフォルト値を使う
float Distance = AcceptableDistanceKey.IsSet() ?
	BlackboardComp->GetValueAsFloat( AcceptableDistanceKey.SelectedKeyName ) :
	AcceptableDistance;
```

## BlackboardKeySelector 変数の初期化
C++ で定義した KeySelector は InitializeFromAsset で全て初期化する必要があります。
ResolveSelectedKey を処理しないと値が取得できない。

```C++
void UBTDecorator_CloseEnough::InitializeFromAsset( UBehaviorTree& Asset )
{
	Super::InitializeFromAsset( Asset );

	UBlackboardData* BBAsset = GetBlackboardAsset();
	if ( ensure( BBAsset ) )
	{
		ActorToCheck.ResolveSelectedKey( *BBAsset );
		AcceptableDistanceKey.ResolveSelectedKey( *BBAsset );
	}
}
```