[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 abortsLower 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 メソッドがキモ

  1. 確率分岐フロー自体は、指定した確率で成功する Decorator と Selector Node の組み合わせで実現できます。ただし、その場合は子 Node に対して Decorator を追加することになり、確率分岐であることが一見してわかりにくいです。確率分岐フローが複数必要な場合は、利便性や視認性向上のために専用の Composite Node を用意する方がよいかと思われます。 
  2. UBTComposite_Sequence::CanAbortLowerPriority() に “don’t allow aborting lower priorities, as it breaks sequence order and doesn’t makes sense” とコメントされています。 

あわせて読みたい