【UE4】Behavior Treeノードを自作してUtility Based AIを作った

はじめに

以前【UE4】Utility Based AI(プロジェクト付)という記事を書きました。
以前の記事ではキャラクターBPにユーティリティベースでAIを動かす処理を定義していましたが、UE4でAIを作る上で多くの方が使われているであろう「Behavior Treeエディタ」を使わないのは不便で勿体無いと感じたので、「じゃあBehavior TreeでユーティリティベースのAIを作れるようにしよ」と思い立ち実際に作ってみた次第です。

今回の記事はBehavior Treeノードを自作する際に「ん?なんだこれ?」となった部分や「何故このように書いたか」という点について解説していきます。

Utility Based AIそのものについては上で貼った記事を参照していただければと思います。ここでは特に解説はしません。

プロジェクトはこちらから

以下のURLからプロジェクトをダウンロードしてください。
https://1drv.ms/u/s!Au-8FqgREBKZhTJLhxO9kgUrj5LX

エンジンのバージョンはUE4.19.0となります。

Behavior Treeノードを自作する

プロジェクト名_Build.cs

エディタからBehavior Treeのいずれかのノードを継承して新たにC++クラスを作成する際はBuild.cs「AIModule」「GameplayTasks」の2つを**PublicDependencyModuleNamesに追加する必要があります。
この2つを追加しなければリンカエラーとなります。

        // 「AIModule」と「GameplayTasks」を追加
        PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "AIModule", "GameplayTasks" });

UBTComposite_Utility

このクラスでは「Utilityスコアに基づいた行動の選択」を行っています。
子ノードが求めたUtilityスコアを収集し「重み付けランダム」もしくは「最もスコアの高いアクション」のいずれかの方法で子ノードを選択し実行します。

関数GetNextChildHandler

この関数は実行する子ノードのインデックスを返すCompositeノードには必須の関数です。

if文の理由

遷移するノードのインデックスを決める処理は以下のif文で囲っています。

    // BTSpecialChild::NotInitialized = このノードに遷移したばかり。つまり子ノードを一度も実行していない状態.
    if (PrevChild == BTSpecialChild::NotInitialized)

UBTComposite_Utilityは「次の子ノードへ遷移する必要の無いノード」です。
ユーティリティベースではUtilityスコアを見て最も良いアクションを取るべきなので、SequenceノードやSelectorノードの様に子ノードの成功/失敗によって「次のノードへ遷移」してしまうとUtilityスコアを収集する意味が全く無くなってしまいます。

そこで親ノードからUtility Compositeノードへ遷移した直後のみ動作するようにし、子ノードからUtility Compositeノードへ遷移した場合はそのまま親ノードへ戻るようにしました。

2つのfor文

        // Children = このノードが持つ子ノードの配列. 子ノードは「Taskノード」や「Compositeノード」
        for (const FBTCompositeChild& child : Children)
        {
            // child.Decorators = 子ノードにくっついている「Decoratorノード」の配列。(Decoratorノードは1つのノードにいくつもくっつけられる).
            for (const UBTDecorator* childDecorator : child.Decorators)
            {

2つのfor文で呼ばれている配列は以下のような関係になっています。
2018-03-30_23h52_08.png

IBTUtilityNodeInterface

                // 取得したDecoratorノードに「IBTUtilityNodeInterface」が実装されているかチェック
                const IBTUtilityNodeInterface* UtilityDecorator = Cast<IBTUtilityNodeInterface>(childDecorator);

                // 実装していなければ次のDecoratorノードへ
                if (UtilityDecorator == nullptr)
                    continue;

子ノードが持つDecoratorノードを取得した後はそのDecoratorノードが「IBTUtilityNodeInterfaceを備えているか?」を尋ねます。
IBTUtilityNodeInterfaceはUtilityスコアを計算するための関数が宣言されています。

このプロジェクトを作り始めた時は直接Utilityスコア計算処理を持つDecoratorノードへとキャストしていましたが「インターフェースに依存せよ」という言葉もあるくらいですし個人的にも「このクラスしか動作することを許されないのはキモい!」と妙な嫌悪感もあったので間にインターフェースを挟むようにしました。

UBTDecorator_UtilityBlueprintBase

このクラスでは「Utilityスコアの計算」を行っています。しかし、通常のDecoratorノードのように「条件チェック」は行ないません。常にTRUEを返し、ノードを実行します。
Utilityスコアの計算は思想段階では「Taskノードで行う予定」でした。しかしTaskノードは子ノードを持つ事が出来ずBehavior Treeの強みである「Aをした後Bをする」といった動作が出来ません。
攻撃アクションが選ばれた時に「①敵の近くへ移動して、②攻撃をする」という2つの動作を1つのTaskノードへ詰め込まなければならないため、デフォルトで用意されているMove Toノードなどの便利ノードを捨てて同じ処理を書かなければならないのは非常に勿体無いので、今回のプロジェクトではDecoratorノードに計算処理を定義することにしました。

コンストラクタ

コンストラクタでは以下のようにノードの各種設定をFALSEにしています。

    bAllowAbortNone = false;        // Observer abortsを「NONE」に設定出来るようにする?
    bAllowAbortLowerPri = false;    // Observer abortsを「Lower Priority」に設定できるようにする?
    bAllowAbortChildNodes = false;  // Observer abortsを「Self」や「Both」に設定できるようにする?

bAllowシリーズをfalseにすることによる効果はコメントに書いてありますがエディタでいうと以下の部分です。ここが詳細パネルに表示されなくなります。
2018-03-31_00h35_44.png

各コメント

このクラスでは多くの部分がコメントアウトされていますが、これは試行錯誤した結果を記録するためのそのままにしました。

関数SetOwner

この関数ではBehaviorTreeコンポーネントを所持するアクタを引数で渡してくれる物なのですが、どういう訳か引数の値をメンバー変数に保存した時点では値が有効なのですが関数CalculateUtilityValueで保存した変数を参照すると「NONE」になってしまっていました。

大体こういった「有効だった値が突如無効な値になっている」場合はガベージコレクションが原因だと思っているのですが、メンバー変数にUPROPERTYを付けても同様の結果になったので関数SetOwnerを使用するのは諦めて、関数CalculateUtilityValueの呼び出し元からオーナーアクタとオーナーAIコントローラーを渡すようにしました。

関数InitializeFromAsset

実はこのDecoratorノードには1つ大きな問題点があります。
「BlackboardKeySelector型の変数が使えない」という点です。

UBTDecorator_BlueprintBaseを継承してDecoratorノードをブループリントで自作する場合は以下のように
2018-03-31_01h08_06.png
2018-03-31_01h08_35.png
BlackboardKeySelector型変数を定義し「編集可能」にチェックを入れて「Get Blackboard Value as ホニャララ」のように値を取り出す場面は多くあります。(Decorator以外にもTaskやServiceノードも同様だと思います)
今回のようなC++でUBTDecoratorクラスを継承してDecoratorノードを自作した場合にはBlackboardKeySelector型変数から値を取り出す事が出来ません。
(この問題点は参考にしたBehavior Tree Utility Pluginでも同様です)

この問題が発生した原因は「なんとなくの勘」ではありますが関数InitializeFromAssetで使用している「BlueprintNodeHelpers」だと思います。
問題に気づいた時「UBTDecorator_BlueprintBaseの関数InitializeFromAssetをそのままコピペすればいいかな」と考え、実際にやってみたのですがBlueprintNodeHelpersの関数を呼び出した箇所で「関数 "symbol" で使用されている外部シンボル "function" の定義をリンカーが見つけることができませんでした。」というリンカエラーが発生してしまいました。

「ue4 BlueprintNodeHelpers」と検索してみた所以下のAnswerHUBにたどり着きました。
BlueprintNodeHelpers requires AIMODULE_API to export functions correctly.
内容としては

クライアントのC ++コードからアクセスできるようにするには、すべての関数に接頭辞AIMODULE_APIを付ける必要があるようです。

とのことでした。

試しにBlueprintNodeHelpersが持つ関数の内唯一AIMODULE_APIが接頭詞として付いている関数DescribePropertyを呼び出した所リンカエラーは起こりませんでした。
ですのでBlueprintNodeHelpersの関数群は「AIMODULE内のクラスのみでしか使用できない」もしくは「全ての関数にAIMODULE_API接頭詞を付ける」必要があります。
しかし、そのためにはエンジンを改造する必要があり、あまりにもコストが高すぎると判断したのでこの問題には目をつむることにしました。

プロジェクトでの解決策

BlackboardKeySelector型変数が使えないため今回のプロジェクトでは不便ではありますが「Blackboardに保存したかった値をBehavior Treeを所持するオーナー側から提供してもらう」という形にすることで解決としました。
2018-03-31_01h41_47.png

おわりに

Utility Based AIの構築を通じてBehavior Treeノードを自作してみました。
CompositeノードはともかくDecoratorノードをC++で自作する際にはBlackboardKeySelector型変数が使えないのは結構大きな痛手ではありますが、そもそも今回のようなDecoratorノードの使い方はかなりトリッキーな手法だと自覚はありますので「しょうがない」と思います。

特にDecoratorノードを自作することはあまりないとは思いますが、もしも作らざるを得ない状況となった場合にはこの記事が参考になれば幸いです。

参考資料

[UE4] Behavior Tree の Composite Node を自作する
Behavior Tree Utility Plugin
AnswerHUB - BlueprintNodeHelpers requires AIMODULE_API to export functions correctly.

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.