月別アーカイブ: 12月 2017

[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” とコメントされています。 

[UE4] Event Graph に最初からある半透明の BeginPlay アレ何なの?

(UE 4.18.1)

この記事は、Unreal Engine 4 (UE4) Advent Calendar 2017 の 10 日目の記事です。

「Blog にはある程度まとまった内容の記事を書こう」と思っていたら億劫になって更新しなくなってしまったので、これからは短めの記事を頻度高めで書いていきたいと思います…。

今回はコレ (↓) について書いていきます。

お名前は何ていうの?

『Ghost Node』 と呼ぶようです。無駄にカッコイイ。

どういう Node が Ghost Node なの?

Node を Ghost Node に変換するメソッド UEdGraphNode::MakeAutomaticallyPlacedGhostNode の中身は以下のとおりです。

(Runtime/Engine/Private/EdGraph/EdGraphNode.cpp)

void UEdGraphNode::MakeAutomaticallyPlacedGhostNode()
{
 EnabledState = ENodeEnabledState::Disabled;
 NodeComment = LOCTEXT("DisabledNodeComment", "This node is disabled and will not be called.\nDrag off pins to build functionality.").ToString();
 bUserSetEnabledState = false;
}

また、Node が Ghost Node であれば true を返すメソッド UEdGraphNode::IsAutomaticallyPlacedGhostNode の中身は以下のとおりです。

(Runtime/Engine/Private/EdGraph/EdGraphNode.cpp)

bool UEdGraphNode::IsAutomaticallyPlacedGhostNode() const
{
    return !bUserSetEnabledState && (EnabledState == ENodeEnabledState::Disabled);
}

以上より、

  • EnabledStateENodeEnabledState::Disabled である
  • bUserSetEnabledStatefalse である

という 2 点を両方満たせば、その Node は Ghost Node であると言えるようです。

ENodeEnabledState::Disabled って何なの?

Node には Compile Option というものが存在しており、これによって Node がコンパイル・実行されるかどうかが決まります。内部的には ENodeEnabledState 型のメンバ変数 EnabledState として保持されます。

Compile Option ENodeEnableState 実行の有無
Enable Compile (Always) Enabled 常に実行される
Enable Compile (Development Only) DevelopmentOnly Shipping Build では実行されない
Disable (Do Not Compile) Disabled 常に実行されない

なお、

Editor Preferences → Content Editors → Blueprint Editor → Experimental → Allow Explicit Impure Node Disabling

のチェックをオンにすることで、Inpure Node  (実行ピンが付いているもの) の右クリックメニューにて Compile Option の変更ができるようになります。

Ghost Node は EnabledStateENodeEnabledState::Disabled なので、常にコンパイル・実行されず、無いもの同然として扱われるということです。

Ghost Node には Disabled と書かれたしましまが付いてないけど何故なの?

Compile Option を Disable に設定した Node には “Disabled” と書かれたしましまの Border が付いていますが、Ghost Node には付いていません。

このしましまの Slate を描画する処理は、SGraphNode::UpdateGraphNode の中にある以下の if ブロック内に書かれています。

(Editor/GraphEditor/Private/SGraphNode.cpp)

if ((GraphNode->GetDesiredEnabledState() != ENodeEnabledState::Enabled) && !GraphNode->IsAutomaticallyPlacedGhostNode())

先に挙げた UEdGraphNode::IsAutomaticallyPlacedGhostNode の内容と合わせると、Ghost Node でしましまが描画されないのは、bUserSetEnabledStatefalse であることに依るものとわかります。

この他にも、bUserSetEnabledState の値によって挙動が変化する処理がいくつかあります。1

不要な Node が化けて出てくるんだけど除霊できないの?

EditorPerProjectUseSettings.ini[DefaultEventNodes] セクションで Ghost Node の追加・削除ができます。

例えば、

  • Actor の ActorBeginOverlap はあまり使わないので Ghost Node として出てほしくない
  • Behavior Tree の Task を書く時に、最初から ReceiveExecuteAI が出ていてほしい

という場合は、

《Project Directory》/Config/DefaultEditorPerProjectUserSettings.ini に以下のように記述します。

[DefaultEventNodes]
-Node=(TargetClass=Actor TargetEvent="ReceiveActorBeginOverlap")
+Node=(TargetClass=BTTask_BlueprintBase TargetEvent="ReceiveExecuteAI")

TargetClass には A や U などの接頭辞を除いた Class 名を、TargetEvent には元のメソッド名 (Display Name ではない) を指定する必要があるようです。

まとめ

  • 最初から配置されている半透明の Node は Ghost Node と呼ぶ
  • Ghost Node は基本的に Disable な Node と同じだが、表示や操作時の挙動など異なる点もある
  • デフォルトで配置される Ghost Node は .ini を編集することで追加・削除できる

***

明日は ほげたつ (@HogeTatu) 大先生による 『UE4でゲームを作る時に考えていること2選』 です。


  1. 例えば、作成しようとした Node が既に配置済だった場合、配置済 Node が Disable の場合はフォーカスが合うだけですが、Ghost Node の場合は Ghost Node が削除されて新規 Node が作成されます。Editor/BlueprintGraph/Private/BlueprintEventNodeSpawner.cppUEdGraphNode UBlueprintEventNodeSpawner::Invoke を参照。 

あわせて読みたい