ぼっちプログラマのメモ

Unreal Engineについて書いたりしてます

【UE5】StateTreeで文字列から関数・イベントを呼び出すタスクの作り方

はじめに

StateTreeから制御対象のActorの関数・イベントを呼びたい、というのは多々あるかと思いますが、そのたびに専用のStateTree Taskを作るのは少々めんどうです。

基底クラスで関数を用意したり、(BP)Interfaceなどを用いることで1つのTaskを流用することはできますが、プロト制作時などスピード感が必要な段階では…その準備もサボりたい所…

ということで、文字列から関数・イベントを呼び出せる CallFunctionByNameWithArguments を使ったStateTree Taskを作ってみました!

事前準備不要でStateTreeからActor(UObject)が持つ処理を呼び出せますし、

呼び出す関数名・引数をStateTreeのParameterと紐づけておけば、Actor側のStateTree Component で設定することが可能になります。

その結果、実装が多少違うBP・Actorであっても、StateTreeを超汎用的に使用することが可能になります!これは便利!(なはず)

ということで、今回はそんなStateTree Taskを実現する C++コードを紹介します。なお、もしBPのみで実現したい場合はKEコマンドを使用することになるはずです。

【UE4】ConsoleCommand「CE,KE」について【★★☆】 | キンアジのブログ

StateTreeで文字列から関数・イベントを呼び出す C++コード

…といっても、CallFunctionByNameWithArguments 自体はむかーしからある関数で、解説している記事もいくつかあったりしますが…

UE4 UnrealC++リフレクションについてのメモ #UnrealEngine4 - Qiita

UObject の任意関数の検索と実行 | 棚からねぎもち

今回はユースケースということで何卒何卒( 自作StateTree Taskに関する記事はまだまだ少ないですし!)

では余談はさておき、C++コードをぺたり

#pragma once

#include "StateTreeTaskBase.h"
#include "StateTreeTask_CallFunction.generated.h"

USTRUCT(BlueprintType)
struct XXX_API FStateTreeTask_CallFunction_InstanceData
{
    GENERATED_BODY()

    UPROPERTY(EditAnywhere, Category = Context)
    TObjectPtr<AActor> Actor;
    
    // 呼び出す関数・イベント名
    UPROPERTY(EditAnywhere, Category = Parameter)
    FName FunctionName;

    // 引数
    UPROPERTY(EditAnywhere, Category = Parameter)
    TArray<FName> Arguments;

    // FinishTaskを呼ぶか否か。同時に複数のTaskを呼ばない場合はtrueに
    UPROPERTY(EditAnywhere, Category = Parameter)
    bool bShouldCallFinishTask = false;
};

// 文字列から指定Actorの関数・イベントを引数付きで実行
USTRUCT(meta = (DisplayName = "Call Function"))
struct XXX_API FStateTreeTask_CallFunction : public FStateTreeTaskCommonBase
{
    GENERATED_BODY()

    using FInstanceDataType = FStateTreeTask_CallFunction_InstanceData;
    virtual const UStruct* GetInstanceDataType() const override { return FInstanceDataType::StaticStruct(); }

    virtual EStateTreeRunStatus EnterState(FStateTreeExecutionContext& Context, 
        const FStateTreeTransitionResult& Transition) const override;

    // StateTreeのTask上での表示名
#if WITH_EDITOR
    virtual FText GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, 
        const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting = EStateTreeNodeFormatting::Text) const override;
#endif
};
#include "StateTreeTask_CallFunction.h"
#include "StateTreeExecutionContext.h"

#define LOCTEXT_NAMESPACE "StateTreeTask_CallFunction"

EStateTreeRunStatus FStateTreeTask_CallFunction::EnterState(FStateTreeExecutionContext& Context,
                                                            const FStateTreeTransitionResult& Transition) const
{
    const FInstanceDataType* InstanceData = Context.GetInstanceDataPtr<FInstanceDataType>(*this);
    check(InstanceData);
    
    if(!InstanceData->Actor)
    {
        UE_VLOG_UELOG(Context.GetOwner(), LogStateTree, Warning, TEXT("%hs Failed. Reason: Invalid Actor"), __FUNCTION__);
        return EStateTreeRunStatus::Failed;
    }

    bool bResult = false;
    // 指定の関数・イベント名があるかを確認し、
    // 存在したら、引数も合成した文字列を元に関数・イベントを実行
    if (const UFunction* Function = InstanceData->Actor->FindFunction(InstanceData->FunctionName))
    {
        FString Cmd = Function->GetName();
        for (const FName& Arg : InstanceData->Arguments)
        {
            Cmd += FString::Printf(TEXT(" %s"), *Arg.ToString());
        }
        bResult = InstanceData->Actor->CallFunctionByNameWithArguments(*Cmd, *GLog, InstanceData->Actor, true);
    }
    else
    {
        UE_VLOG_UELOG(Context.GetOwner(), LogStateTree, Warning, 
            TEXT("%hs Failed. Reason: Function not found"), __FUNCTION__);
        return EStateTreeRunStatus::Failed;
    }

    if(InstanceData->bShouldCallFinishTask)
    {
        return bResult ? EStateTreeRunStatus::Succeeded : EStateTreeRunStatus::Failed;
    }
    
    return EStateTreeRunStatus::Running;
}

#if WITH_EDITOR
FText FStateTreeTask_CallFunction::GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView,
    const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting) const
{
    const FInstanceDataType* InstanceData = InstanceDataView.GetPtr<FInstanceDataType>();
    check(InstanceData);
    
    return FText::Format(LOCTEXT("StateTreeTask_CallFunction", "CallFunction : {0}"), 
        FText::FromName(InstanceData->FunctionName));
}
#endif

さいごに

かんたんな内容ではありましたが、StateTreeのTaskはまだまだ数が十分ではないので、こういう小粒な便利Taskを自前で用意しておくとState Tree実装担当者が少ししあわせになると思います!この記事がきっかけとなって、そんな少し便利Taskが色々出てきたら嬉しいです(そして、記事書いてください!)。

おしまい