LoginSignup
3
1

UE5 StateTree についてのメモ

Posted at

概要

UnrealEngine5 から追加された StateTree のメモです。ビヘイビアツリーのようにAIに使用したりできます。
タスクなどは毎フレーム処理があるのでC++で作成すると処理負荷の軽減につながります。

更新履歴

日付 内容
2023/06/22 初版

参考

以下の記事を参考にいたしました、ありがとうございます。
UE公式:StateTree概要
[UE5] StateTreeで状態管理

環境

Windows10
Visual Studio 2022
UnrealEngine 5.1.1

関連ソース

"Engine\Plugins\Runtime\StateTree\Source\StateTreeModule\Public\StateTree.h"
"Engine\Plugins\Runtime\StateTree\Source\StateTreeModule\Public\StateTreeTaskBase.h"
"Engine\Plugins\Runtime\StateTree\Source\StateTreeModule\Public\StateTreeEvaluatorBase.h"
"Engine\Plugins\Runtime\StateTree\Source\StateTreeModule\Public\StateTreeConditionBase.h"
"Engine\Plugins\Runtime\GameplayStateTree\Source\GameplayStateTreeModule\Public\Components\StateTreeComponent.h"
"Engine\Plugins\Runtime\StateTree\Source\StateTreeModule\Public\StateTreeExecutionContext.h"

準備

プラグインから GameStateTreeStateTree の2つを有効にします。
Plugin.png

アセットブラウザ右クリック [AI] -> [StateTree] からアセットが作成可能になります。

C++で扱う場合は、StateTreeModule モジュールの追加を行います。

MyProject.Build.cs
using UnrealBuildTool;

public class StateTreeTest : ModuleRules
{
	public StateTreeTest(ReadOnlyTargetRules Target) : base(Target)
	{
		PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;

		PublicDependencyModuleNames.AddRange(new string[] {
			"Core", 
			"CoreUObject", 
			"Engine", 
			"InputCore", 
			"HeadMountedDisplay", 
			"EnhancedInput",
			"StateTreeModule"      // ←追加
			});
	}
}

ステートツリーの条件チェックの流れ

基本的に[Root]から始まって子ステートの条件チェックを行い条件に合わない場合、次の兄弟ステートの条件を見ていきます。

下のスクショのようなステートツリーの場合、[State1]の条件チェック -> 条件に合わない場合[State2]の条件チェック... となります。

条件に合ったステートの子ステートが更にあった場合、その条件を上から順に辿っていき、合致したものがあればそのステートをアクティブとするようです。子ステートの条件が全て合致しなかった場合、親ステートの条件が合致していない場合と同様に次の判定へ移ります。

下のスクショの場合、[State1]に条件が合致した場合、子ステートの[State1-1]の条件を見ていきます。ここで[State1-1][State1-2]ともに条件を満たさない場合、次の[State2]の条件チェックへ移ります。

StateTree.png

起動時ステートの注意点

ステートツリー起動時に条件に合うステートがない場合、以下のようなエラーログが出力されます。

エラーログ
LogStateTree: Error: TextObject: FStateTreeExecutionContext::Start: Failed to select initial state on 'TestObject' using StateTree 'StateTree /Game/TestStateTree.TestStateTree'. This should not happen, check that the StateTree logic can always select a state at start.

この場合、ステートツリーの動作が止まります。なので最低限[Root]ステートの子の一番下は条件なしのステートを用意するといいと思います。

StateTreeComponentについて

ビヘイビアツリーと同様に BrainComponentを継承しているので同じメソッドが使えます。

StateTreeComponent.h
UCLASS(ClassGroup = AI, HideCategories = (Activation, Collision), meta = (BlueprintSpawnableComponent))
class GAMEPLAYSTATETREEMODULE_API UStateTreeComponent : public UBrainComponent, public IGameplayTaskOwnerInterface
{
	GENERATED_BODY()
// ...省略...

	// BEGIN UBrainComponent overrides
	virtual void StartLogic() override;
	virtual void RestartLogic() override;
	virtual void StopLogic(const FString& Reason)  override;
	virtual void Cleanup() override;
	virtual void PauseLogic(const FString& Reason) override;
	virtual EAILogicResuming::Type ResumeLogic(const FString& Reason)  override;
	virtual bool IsRunning() const override;
	virtual bool IsPaused() const override;
	// END UBrainComponent overrides

// ...省略...
};

特にStopLogicRestartLogicで適宜処理停止/再開をすると負荷軽減になるので常駐するような利用の場合はやっておくべきかと思います。
BrainComponent_StopLogic.png

C++でのタスク作成

C++でStateTree用タスクを作成するには以下2パターンあります。
どちらも EnterStateがタスク開始時、ExitStateがタスク終了時、Tickがタスク処理時の毎フレーム処理、StateCompletedがタスク完了時(完了せずに遷移した場合呼ばれない)となっています。
またTick処理は返値によってタスク実行の継続or完了が決まります。

UStateTreeTaskBlueprintBase を継承してタスクを作る

BPでタスクを作るのと同様に UStateTreeTaskBlueprintBase を継承してタスククラスを作成できます。
以下コード例。

STT_Test.h
#include "CoreMinimal.h"
#include "StateTreeTaskBase.h"
#include "Blueprint/StateTreeTaskBlueprintBase.h"
#include "STT_Test.generated.h"

UCLASS(meta = (DisplayName = "MySTT_Test"))
class MYPROJECT_API USTT_Test : public UStateTreeTaskBlueprintBase
{
	GENERATED_BODY()

public:
	virtual EStateTreeRunStatus Tick(FStateTreeExecutionContext& Context, const float DeltaTime) override;

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

	UPROPERTY(EditAnywhere, Category = Parameter, meta = (ToolTip = "Test"))
		int32  ParamInt = 0;
};
STT_Test.cpp
#include "STT_Test.h"
#include "GameFramework/Actor.h"
#include "StateTreeExecutionContext.h"

// Tick処理
EStateTreeRunStatus USTT_Test::Tick(FStateTreeExecutionContext& Context, const float DeltaTime)
{
	if (bHasTick)
	{
		return ReceiveTick(DeltaTime);
	}
	return EStateTreeRunStatus::Running;
}

// タスク開始時処理
EStateTreeRunStatus USTT_Test::EnterState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition)
{
	Super::EnterState(Context, Transition);

	return EStateTreeRunStatus::Running;
}

// タスク終了時処理
void USTT_Test::ExitState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition)
{
	Super::ExitState(Context, Transition);

	auto _Owner = Context.GetOwner();
	if (_Owner) {
		auto _Actor = Cast<AActor>(_Owner);
		// 何らかの処理を行う
	}

}

結果は以下のようなタスクが作られます。
STT_Test.png

FStateTreeTaskCommonBase を継承してタスクを作る

FStateTreeTaskCommonBase を継承してタスクを作成することもできます。注意点としてはUObjectではないことと、公開パラメータやワークのためのデータを別に用意する必要があることです。
以下コード例。

STT_Test2.h
USTRUCT()
struct MYPROJECT_API FSTT_Test2InstanceData
{
	GENERATED_BODY()
	
	UPROPERTY(EditAnywhere, Category = Parameter)
	bool bBoolParam = false;

	float RemainingTime = 1.f;
};

USTRUCT(meta = (DisplayName = "MySTT_Test2"))
struct MYPROJECT_API FSTT_Test2 : public FStateTreeTaskCommonBase
{
	GENERATED_BODY()

	typedef FSTT_Test2InstanceData InstanceDataType;
	
	FSTT_Test2() = default;

	virtual const UStruct* GetInstanceDataType() const override { return InstanceDataType::StaticStruct(); }

	virtual EStateTreeRunStatus EnterState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const override;
	virtual EStateTreeRunStatus Tick(FStateTreeExecutionContext& Context, const float DeltaTime) const override;
	virtual void ExitState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const override;

};
STT_Test2.cpp
// タスク開始時処理
EStateTreeRunStatus FSTT_Test2::EnterState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const
{
	InstanceDataType& InstanceData = Context.GetInstanceData<InstanceDataType>(*this);

	return EStateTreeRunStatus::Running;
}

// Tick処理
EStateTreeRunStatus FSTT_Test2::Tick(FStateTreeExecutionContext& Context, const float DeltaTime) const
{
	InstanceDataType& InstanceData = Context.GetInstanceData<InstanceDataType>(*this);

	InstanceData.RemainingTime -= DeltaTime;
	if (InstanceData.RemainingTime <= 0.f)
	{
		// 何らかの処理、完了時に Succeeded か Failed を返す
		return EStateTreeRunStatus::Succeeded;
	}
	
	return EStateTreeRunStatus::Running;
}

// タスク終了時処理
void FSTT_Test2::ExitState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const
{
	InstanceDataType& InstanceData = Context.GetInstanceData<InstanceDataType>(*this);
}

結果以下のようなタスクが作られます。
STT_Test2.png

C++でのエバリュエーター作成

エバリュエーター作成も2パターンの作成方法があります。
どちらもステートツリー開始時処理TreeStart、ステートツリー終了時処理TreeStop、毎フレーム処理Tickが継承可能です。

UStateTreeEvaluatorBlueprintBase を継承する

UStateTreeEvaluatorBlueprintBaseを継承してエバリュエーター作成することができます。
以下コード例。

STE_Test.h
#include "CoreMinimal.h"
#include "Blueprint/StateTreeEvaluatorBlueprintBase.h"
#include "StateTreeEvaluatorBase.h"
#include "STE_Test.generated.h"

UCLASS()
class MYPROJECT_API USTE_Test : public UStateTreeEvaluatorBlueprintBase
{
	GENERATED_BODY()

public:
	virtual void TreeStart(FStateTreeExecutionContext& Context) override;
	virtual void TreeStop(FStateTreeExecutionContext& Context) override;
	virtual void Tick(FStateTreeExecutionContext& Context, const float DeltaTime) override;

	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = OutPut)
	FVector OwnerLocation;

	AActor* Owner;
};

STE_Test.cpp
#include "STE_Test.h"
#include "Kismet/GameplayStatics.h"
#include "GameFramework/Actor.h"
#include "GameFramework/Character.h"
#include "StateTreeExecutionContext.h"

// ステートツリー開始時処理
void USTE_Test::TreeStart(FStateTreeExecutionContext& Context)
{
	Super::TreeStart(Context);
	// 毎回キャストが無駄なので予めオーナーを取得しておく
	Owner = Cast<AActor>(UGameplayStatics::GetPlayerCharacter(GetWorld(), 0));
}

// ステートツリー終了時処理
void USTE_Test::TreeStop(FStateTreeExecutionContext& Context)
{
	Super::TreeStop(Context);
}

// Tick処理
void USTE_Test::Tick(FStateTreeExecutionContext& Context, const float DeltaTime)
{
	Super::Tick(Context, DeltaTime);
	// オーナーから値を取得
	if (Owner) {
		OwnerLocation = Owner->GetActorLocation();
	}
}

結果は以下のようにVector型プロパティを取得できるようになります。
STE_Test.png

FStateTreeEvaluatorCommonBase を継承する

FStateTreeEvaluatorCommonBase を継承してエバリュエーター作成することができます。この場合はインスタンスデータは別途定義が必要です。
以下コード例。

STE_Test2.h
// インスタンスデータ
USTRUCT()
struct MYPROJECT_API FSTE_Test2InstanceData
{
	GENERATED_BODY()
	
	UPROPERTY(EditAnywhere, Category = Parameter)
	int32 IntParam = 0;

	UPROPERTY(EditAnywhere, Category = Parameter)
	float FloatParam = 0.0f;

	AActor* Owner;
};

USTRUCT(meta = (DisplayName="MyEva2"))
struct MYPROJECT_API FSTE_Test2 : public FStateTreeEvaluatorCommonBase
{
	GENERATED_BODY()

	typedef FSTE_Test2InstanceData InstanceDataType;
	virtual const UStruct* GetInstanceDataType() const override { return InstanceDataType::StaticStruct(); }

	FSTE_Test2() = default;
	
	virtual void TreeStart(FStateTreeExecutionContext& Context) const override;
	virtual void TreeStop(FStateTreeExecutionContext& Context) const override;
	virtual void Tick(FStateTreeExecutionContext& Context, const float DeltaTime) const override;
};
STE_Test2.cpp
// 開始時処理
void FSTE_Test2::TreeStart(FStateTreeExecutionContext& Context) const
{
	InstanceDataType& InstanceData = Context.GetInstanceData<InstanceDataType>(*this);

	Super::TreeStart(Context);
	
	auto _World = Context.GetWorld();
	// 予めキャストして保持
	InstanceData.Owner = Cast<AActor>(UGameplayStatics::GetPlayerCharacter(_World, 0));

}

// 終了時処理
void FSTE_Test2::TreeStop(FStateTreeExecutionContext& Context) const
{
	Super::TreeStop(Context);

}

// Tick処理
void FSTE_Test2::Tick(FStateTreeExecutionContext& Context, const float DeltaTime) const
{
	InstanceDataType& InstanceData = Context.GetInstanceData<InstanceDataType>(*this);

	Super::Tick(Context, DeltaTime);

	// オーナーから値を取得する例
	InstanceData.IntParam = Owner->GetActorLocation().X;
	InstanceData.FloatParam = Owner->GetActorLocation().Y;
}

結果は以下のようにInt型とFloat型プロパティを取得することができます。
STE_Test2.png

C++でのコンディション作成

コンディション作成も2パターンあり、TestConditionで条件を判定するコードを書くことで作成ができます。

UStateTreeConditionBlueprintBase を継承する

UStateTreeConditionBlueprintBaseを継承してコンディションを作成することができます。
以下コード例。

STC_Test.h
#include "CoreMinimal.h"
#include "Blueprint/StateTreeConditionBlueprintBase.h"
#include "STC_Test.generated.h"

UCLASS()
class MYPROJECT_API USTC_Test : public UStateTreeConditionBlueprintBase
{
	GENERATED_BODY()
	
protected:
	virtual bool TestCondition(FStateTreeExecutionContext& Context) const override;

	UPROPERTY(EditAnywhere, Category = Parameter)
	bool bInvert = false;
};
STC_Test.cpp
#include "STC_Test.h"
#include "Kismet/GameplayStatics.h"
#include "GameFramework/Actor.h"
#include "StateTreeExecutionContext.h"
#include "StateTreePropertyBindings.h"

bool USTC_Test::TestCondition(FStateTreeExecutionContext& Context) const
{
	auto _Owner = Context.GetOwner();
	auto _Actor = Cast<AActor>(_Owner);
	bool _bResult = false;

	// 適当な条件
	if (_Actor && _Actor->GetActorScale().X < 1.0f){
		_bResult = true;
	}

	return _bResult ^ bInvert;
}

以下のようなコンディションになります。
STC_Test.png

FStateTreeConditionCommonBase を継承する

FStateTreeConditionCommonBaseを継承することで作成ができます。例のごとくインスタンスデータは別途用意する必要があります。
以下コード例。

STC_Test2.h
USTRUCT()
struct MYPROJECT_API FSTC_Test2InstanceData
{
	GENERATED_BODY()
	
	UPROPERTY(EditAnywhere, Category = Input)
	TObjectPtr<UObject> Object = nullptr;
};

USTRUCT(DisplayName="STC_Test2")
struct MYPROJECT_API FSTC_Test2 : public FStateTreeConditionCommonBase
{
	GENERATED_BODY()

	using FInstanceDataType = FSTC_Test2InstanceData;

	FSTC_Test2() = default;
	
	explicit FSTC_Test2(const EStateTreeCompare InInverts)
		: bInvert(InInverts == EStateTreeCompare::Invert)
	{}

	virtual const UStruct* GetInstanceDataType() const override { return FInstanceDataType::StaticStruct(); }
	virtual bool TestCondition(FStateTreeExecutionContext& Context) const override;

	UPROPERTY(EditAnywhere, Category = Parameter)
	bool bInvert = false;
};
STC_Test2.cpp
bool FSTC_Test2::TestCondition(FStateTreeExecutionContext& Context) const
{
	const FInstanceDataType& InstanceData = Context.GetInstanceData(*this);
	bool _bResult = false;

	// 適当な条件
	if(IsValid(InstanceData.Object) && InstanceData.Object.IsA(AActor::StaticClass()) ){
		_bResult = true;
	}

	return _bResult ^ bInvert;
}

以下のようなコンディションになります。
STC_Test2.png

その他

デバッグ用メソッドについて

デバッグ用のメソッドはFStateTreeExecutionContext にあるようです。

stateTreeExecutionContext.h
struct STATETREEMODULE_API FStateTreeExecutionContext
{
public:
// ..省略..

#if WITH_GAMEPLAY_DEBUGGER
	/** @return 現在の実行状態を説明するデバッグ文字列 */
	FString GetDebugInfoString() const;
#endif // WITH_GAMEPLAY_DEBUGGER

	/** @return アクティブな状態のステート名を返します */
	FString GetActiveStateName() const;
	
	/** @return 全てのアクティブな状態のステート名を返します */
	TArray<FName> GetActiveStateNames() const;
};

UE5.2での変更について

5.1でプロダクション対応になったはずですが、変更点が大きいの注意が必要かと思われます。

グローバルタスクの追加

グローバルタスクが追加されました。ROOTステートよりも速く処理が開始されるようです。ステートに関わらない永続的な処理が必要な場合に使うようです。

トランジッションの変更

Transition Toから「遷移のブロック」がなくなり、Priorityが追加されました。

まとめ

ビヘイビアツリーよりもシンプルかと思うのでAIに使うのもアリかと思いますが、AI以外にも使えそうな感じです。具体的にはイベントフロー管理などがいいのではないでしょうか。

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1