[UE4] Behavior Tree の Composite Node を自作する
今回は Behavior Tree の Composite Node の自作方法について解説していきます。
Composite Node を自作しようと思ったのは、Unreal Fest 2016 West にて行われた講演 『スクウェア・エニックスにおける UNREAL ENGINE 4 を用いた人工知能技術の開発事例』 の中で、UE4 にはデフォルトで用意されていない 『Weighted Random (重み付きランダム)』 という Composite Node が使われているのを見つけたのがきっかけです。確率分岐フローを入れたいことは結構多いので…。1
作成方法
非常に簡単で、UBTCompositeNode
を継承したクラスを作成するだけです。
エンジンコードに手を入れる必要はありません。また、Editor 上で使用できるようにするための登録的な作業も必要なく、クラスを作れば自動で Behavior Tree の Node 追加メニューに表示されるようになります。
スクウェア・エニックスさんの実装とは異なる点が多々ありますが、Weighted Random Node を簡易実装してみましたので、コードをご覧ください。
(BTComposite_WeightedRandom.h
)
#pragma once #include "CoreMinimal.h" #include "BehaviorTree/BTCompositeNode.h" #include "BTComposite_WeightedRandom.generated.h" UCLASS() class T_BTCOMPNODESELFMADE_API UBTComposite_WeightedRandom : public UBTCompositeNode { GENERATED_UCLASS_BODY() int32 GetNextChildHandler(struct FBehaviorTreeSearchData& SearchData, int32 PrevChild, EBTNodeResult::Type LastResult) const; virtual FString GetStaticDescription() const override; #if WITH_EDITOR virtual bool CanAbortLowerPriority() const override; virtual FName GetNodeIconName() const override; #endif public: UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Weighted Random") float LeftChildSelectingRate; };
(BTComposite_WeightedRandom.cpp
)
#include "BTComposite_WeightedRandom.h" UBTComposite_WeightedRandom::UBTComposite_WeightedRandom(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) , LeftChildSelectingRate(1.0f) { NodeName = "Weighted Random"; OnNextChild.BindUObject(this, &UBTComposite_WeightedRandom::GetNextChildHandler); } int32 UBTComposite_WeightedRandom::GetNextChildHandler(FBehaviorTreeSearchData& SearchData, int32 PrevChild, EBTNodeResult::Type LastResult) const { int32 NextChildIndex = BTSpecialChild::ReturnToParent; // 子 Node の数が 2 で、かつこの Node に遷移してきた直後であれば if (GetChildrenNum() == 2 && PrevChild == BTSpecialChild::NotInitialized) { const int32 LeftChildIndex = 0; const int32 RightChildIndex = 1; NextChildIndex = (FMath::FRand() <= LeftChildSelectingRate) ? LeftChildIndex : RightChildIndex; } return NextChildIndex; } FString UBTComposite_WeightedRandom::GetStaticDescription() const { int32 ChildrenNum = GetChildrenNum(); // 子 Node の数が 2 ならそれぞれの分岐確率を表示 if (ChildrenNum == 2) { float LeftPercentage = LeftChildSelectingRate * 100; float RightPercentage = 100 - LeftPercentage; return FString::Printf(TEXT("Left : %.2f / Right : %.2f"), LeftPercentage, RightPercentage); } // 子 Node の数が 2 でないなら警告を表示 return FString::Printf(TEXT("Warning : Connect Just 2 Children Nodes (Currently %d Node(s))"), ChildrenNum); } #if WITH_EDITOR bool UBTComposite_WeightedRandom::CanAbortLowerPriority() const { // Sequence Node と同様に、子 Node が優先度の低い Node から処理を奪うことができないようにする return false; } FName UBTComposite_WeightedRandom::GetNodeIconName() const { // Selector Node と同じアイコンを使用 // 独自のアイコン設定は恐らくエンジン改造が必要 return FName("BTEditor.Graph.BTNode.Composite.Selector.Icon"); } #endif
以下、実装している重要なメソッドについて解説します。
GetNextChildHandler
int32 GetNextChildHandler(FBehaviorTreeSearchData& SearchData, int32 PrevChild, EBTNodeResult::Type LastResult) const
次に遷移すべき Node の Index を返すメソッドです。Node の遷移の仕方を規定する最も重要なメソッドとなります。
第 2 引数 PrevChild
で直前に実行された Node の Index が、第 3 引数 LastResult
で直前に実行された Node の結果 (成功・失敗・中断) が得られます。
ここでいう “Node の Index” とは相対的な値で、以下のように定められています。
BTSpecialChild::ReturnToParent
(-2)- 親 Node を指す
BTSpecialChild::NotInitialized
(-1)- 特定の Node を指す値ではない
PrevChild
がこの値の場合、まだこの Node に遷移してきた直後で、子 Node に一度も遷移していないことを示す
- 非負の整数
- 子 Node を指す
- 優先度が高い方から順に (Behavior Tree 上で左に配置されている方から順に) 0, 1, 2… と振られる
Node の右上に付いている、Behavior Tree 全体での実行順を示す数字とは関係ないので注意してください。
Weighted Random のコードは、子 Node の数が 2 で、かつ この Node に遷移してきた直後であれば、ランダムで左右の子 Node のどちらかの Index を返し、それ以外の場合は親 Node の Index を返します。子 Node 数 2 の場合でのみ機能する割り切ったコードになっています。
CanAbortLowerPriority
bool CanAbortLowerPriority() const
子 Node に付加された Decorator の Observer aborts
に Lower Priority
を設定できるかどうかを返すメソッドです。つまり、このメソッドが false を返す場合、子 Node が優先度の低い Node の実行を中断して処理を奪うような動作ができなくなります。
デフォルトで用意されている Composite Node の中では、Sequence Node が false を返す実装となっています。優先順位の低い Node から処理を奪うような動作を許可すると、子 Node の実行順が乱れて “Sequence” でなくなりますし、そのような動作に意味があるとは言えないためです。2
Weighted Random においても、子 Node 間で処理を奪うような動作を許可すると 「指定した確率での分岐」 ではなくなるため、禁止しています。
GetStaticDescription
FString GetStaticDescription() const
Node の Description (Node 名の下に小さく表示されている文字列) を返すメソッドです。
Weighted Random のコードでは、子 Node の数が 2 であればそれぞれの分岐確率を表示し、そうでなければ警告を表示するようにしてあります。
作成した Node を使用する
通常の Composite Node と同様に、Behavior Tree Graph 上で右クリックすると出てくるメニューから選択して配置します。
試しに PrintString を行うだけの Task Node を子として接続し、確率を指定して動作確認してみます。
左の Node に遷移する確率が 30% という設定ですが、上手くいっているようです。
まとめ
- Behavior Tree の Composite Node は
UBTCompositeNode
継承クラスを書くだけで自作できる - Node の遷移の仕方を規定する
GetNextChildHandler
メソッドがキモ
- 確率分岐フロー自体は、指定した確率で成功する Decorator と Selector Node の組み合わせで実現できます。ただし、その場合は子 Node に対して Decorator を追加することになり、確率分岐であることが一見してわかりにくいです。確率分岐フローが複数必要な場合は、利便性や視認性向上のために専用の Composite Node を用意する方がよいかと思われます。 ↩
-
UBTComposite_Sequence::CanAbortLowerPriority()
に “don’t allow aborting lower priorities, as it breaks sequence order and doesn’t makes sense” とコメントされています。 ↩