✍️

【UE5】BTDecorator_Cooldownを参考にしながらC++で自作Decoratorを作る

2024/12/05に公開

概要

この記事は一人アドベントカレンダー by ダリアの5日目の記事です。

今回は自作のDecoratorをC++で作る方法について、元々用意してあるCooldownというDecoratorを参考にしながら作ることが出来たので、それについてまとめた記事になります。

環境

UE5.4.4

説明

まずクラスの全体を書きます。

#pragma once

#include "CoreMinimal.h"
#include "BehaviorTree/BTDecorator.h"
#include "BTDecorator_ExampleCooldown.generated.h"


struct FBTExampleCooldownDecoratorMemory
{
    double LastUseTimestamp;
    uint8 bRequestedRestart : 1;
};
UCLASS()
class EXAMPLEAISYSTEMS_API UBTDecorator_ExampleCooldown : public UBTDecorator
{
    GENERATED_UCLASS_BODY()
public:
    UPROPERTY(Category=Decorator, EditAnywhere)
    float CoolDownTime;
    
    virtual bool CalculateRawConditionValue(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) const override;
    virtual void InitializeMemory(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, EBTMemoryInit::Type InitType) const override;
    virtual void CleanupMemory(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, EBTMemoryClear::Type CleanupType) const override;
    virtual uint16 GetInstanceMemorySize() const override;
    virtual void DescribeRuntimeValues(const UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, EBTDescriptionVerbosity::Type Verbosity, TArray<FString>& Values) const override;
virtual FString GetStaticDescription() const override;
#if WITH_EDITOR
    virtual FName GetNodeIconName() const override;
#endif
protected:
    virtual void OnNodeDeactivation(FBehaviorTreeSearchData& SearchData, EBTNodeResult::Type NodeResult) override;
    virtual void TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;
private:
};
#include "BTDecorator_ExampleCooldown.h"
UBTDecorator_ExampleCooldown::UBTDecorator_ExampleCooldown(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer)
{
    NodeName = "Example Cooldown";
    INIT_DECORATOR_NODE_NOTIFY_FLAGS();
    CoolDownTime = 5.0f;
    
    bAllowAbortChildNodes = false;
}
bool UBTDecorator_ExampleCooldown::CalculateRawConditionValue(UBehaviorTreeComponent& OwnerComp,
	uint8* NodeMemory) const
{
    FBTExampleCooldownDecoratorMemory* DecoratorMemory = CastInstanceNodeMemory<FBTExampleCooldownDecoratorMemory>(NodeMemory);
    const double RecalcTime = (OwnerComp.GetWorld()->GetTimeSeconds() - CoolDownTime);
    return RecalcTime >= DecoratorMemory->LastUseTimestamp;
}

void UBTDecorator_ExampleCooldown::InitializeMemory(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory,
	EBTMemoryInit::Type InitType) const
{
    FBTExampleCooldownDecoratorMemory* DecoratorMemory = InitializeNodeMemory<FBTExampleCooldownDecoratorMemory>(NodeMemory, InitType);
    if (InitType == EBTMemoryInit::Initialize)
    {
        DecoratorMemory->LastUseTimestamp = TNumericLimits<double>::Lowest();
    }
}

void UBTDecorator_ExampleCooldown::CleanupMemory(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory,
	EBTMemoryClear::Type CleanupType) const
{
    CleanupNodeMemory<FBTExampleCooldownDecoratorMemory>(NodeMemory,CleanupType);
}

uint16 UBTDecorator_ExampleCooldown::GetInstanceMemorySize() const
{
    return sizeof(FBTExampleCooldownDecoratorMemory);
}

void UBTDecorator_ExampleCooldown::DescribeRuntimeValues(const UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory,
	EBTDescriptionVerbosity::Type Verbosity, TArray<FString>& Values) const
{
    Super::DescribeRuntimeValues(OwnerComp, NodeMemory, Verbosity, Values);
    
    FBTExampleCooldownDecoratorMemory* DecoratorMemory = CastInstanceNodeMemory<FBTExampleCooldownDecoratorMemory>(NodeMemory);
    const double TimePassed = OwnerComp.GetWorld()->GetTimeSeconds() - DecoratorMemory->LastUseTimestamp;
    
    if (TimePassed < CoolDownTime)
    {
        Values.Add(FString::Printf(TEXT("%s in %ss"),
            (FlowAbortMode == EBTFlowAbortMode::None) ? TEXT("unlock") : TEXT("restart"),
            *FString::SanitizeFloat(CoolDownTime - TimePassed)));
    }
}

FString UBTDecorator_ExampleCooldown::GetStaticDescription() const
{
    return FString::Printf(TEXT("%s: lock for %.1fs after execution and return %s"), *Super::GetStaticDescription(),
        CoolDownTime, *UBehaviorTreeTypes::DescribeNodeResult(EBTNodeResult::Failed));
}
#if WITH_EDITOR
FName UBTDecorator_ExampleCooldown::GetNodeIconName() const
{
    return FName("BTEditor.Graph.BTNode.Decorator.Cooldown.Icon");
}
#endif

void UBTDecorator_ExampleCooldown::OnNodeDeactivation(FBehaviorTreeSearchData& SearchData,
	EBTNodeResult::Type NodeResult)
{
    FBTExampleCooldownDecoratorMemory* DecoratorMemory = GetNodeMemory<FBTExampleCooldownDecoratorMemory>(SearchData);
    DecoratorMemory->LastUseTimestamp = SearchData.OwnerComp.GetWorld()->GetTimeSeconds();
    DecoratorMemory->bRequestedRestart = false;
}

void UBTDecorator_ExampleCooldown::TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
{
    FBTExampleCooldownDecoratorMemory* DecoratorMemory = CastInstanceNodeMemory<FBTExampleCooldownDecoratorMemory>(NodeMemory);
    if (!DecoratorMemory->bRequestedRestart)
    {
        const double RecalcTime = (OwnerComp.GetWorld()->GetTimeSeconds() - CoolDownTime);
        if (RecalcTime >= DecoratorMemory->LastUseTimestamp)
        {
            DecoratorMemory->bRequestedRestart = true;
            OwnerComp.RequestExecution(this);
        }
    }
}

ここから主要な各関数や構造体の説明をしようと思います。

FBTExampleCooldownDecoratorMemory構造体

これは参考にしたCooldownなどC++で作られたDecoratorは構造体を作ってそれを通してやりとりするみたいでした。

Cooldownの場合はTimeStampを設定して現在時刻から引いた時間を経過時間として算出してCoolDownTimeを超えていたらDecoratorの終了条件に設定してるみたいです。

NodeName

Nodeのタイトル名。BPの方に設定されたらそちらに上書きされます。

InitializeMemory

BehaviourTree起動時に1回だけ呼ばれます。

CleanupMemory

PIE終了時に呼ばれたので、BehaviourTreeが停止されたら呼ばれる関数みたいでした。

TickNode

スタックトレースで確認しましたが呼ばれなかったため詳細は不明です。

CalculateRawConditionValue

毎フレーム呼ばれて、コンディションの確認に使われてました。
trueを返すようにするとNodeが実行できるようになります。

GetInstanceMemorySize

ノード上でやり取りするデータ構造体のサイズを返す機能です。
これはsizeofで自作した構造体FBTExampleCooldownDecoratorMemoryを返すようにします。

uint16 UBTDecorator_ExampleCooldown::GetInstanceMemorySize() const
{
    return sizeof(FBTExampleCooldownDecoratorMemory);
}

DescribeRuntimeValues

この関数はノード実行時にカーソルを合わせると残り時間を表示してくれるから、ランタイム時に表示されるツールチップみたいな役割の関数なのかなと思いました。

Valuesに文字列を追加すれば表示できるみたいでした。

GetStaticDescription

NodeNameの下にある説明を作る機能です。

結果

冒頭のソースコードのようにすれば、自作のDecoratorが選択できるようになります。
以下の画像のように自作したDecoratorが候補に出てれば成功です。

参考

Engine\Source\Runtime\AIModule\Classes\BehaviorTree\Decorators\BTDecorator_Cooldown.h
Engine\Source\Runtime\AIModule\Classes\BehaviorTree\Decorators\BTDecorator_Cooldown.cpp

Discussion