概要
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"
準備
プラグインから GameStateTree
と StateTree
の2つを有効にします。
アセットブラウザ右クリック [AI] -> [StateTree] からアセットが作成可能になります。
C++で扱う場合は、StateTreeModule
モジュールの追加を行います。
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]の条件チェックへ移ります。
起動時ステートの注意点
ステートツリー起動時に条件に合うステートがない場合、以下のようなエラーログが出力されます。
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
を継承しているので同じメソッドが使えます。
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
// ...省略...
};
特にStopLogic
やRestartLogic
で適宜処理停止/再開をすると負荷軽減になるので常駐するような利用の場合はやっておくべきかと思います。
C++でのタスク作成
C++でStateTree用タスクを作成するには以下2パターンあります。
どちらも EnterState
がタスク開始時、ExitState
がタスク終了時、Tick
がタスク処理時の毎フレーム処理、StateCompleted
がタスク完了時(完了せずに遷移した場合呼ばれない)となっています。
またTick
処理は返値によってタスク実行の継続or完了が決まります。
UStateTreeTaskBlueprintBase を継承してタスクを作る
BPでタスクを作るのと同様に UStateTreeTaskBlueprintBase
を継承してタスククラスを作成できます。
以下コード例。
#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;
};
#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);
// 何らかの処理を行う
}
}
FStateTreeTaskCommonBase を継承してタスクを作る
FStateTreeTaskCommonBase
を継承してタスクを作成することもできます。注意点としてはUObjectではないことと、公開パラメータやワークのためのデータを別に用意する必要があることです。
以下コード例。
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;
};
// タスク開始時処理
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);
}
C++でのエバリュエーター作成
エバリュエーター作成も2パターンの作成方法があります。
どちらもステートツリー開始時処理TreeStart
、ステートツリー終了時処理TreeStop
、毎フレーム処理Tick
が継承可能です。
UStateTreeEvaluatorBlueprintBase を継承する
UStateTreeEvaluatorBlueprintBase
を継承してエバリュエーター作成することができます。
以下コード例。
#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;
};
#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型プロパティを取得できるようになります。
FStateTreeEvaluatorCommonBase を継承する
FStateTreeEvaluatorCommonBase
を継承してエバリュエーター作成することができます。この場合はインスタンスデータは別途定義が必要です。
以下コード例。
// インスタンスデータ
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;
};
// 開始時処理
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型プロパティを取得することができます。
C++でのコンディション作成
コンディション作成も2パターンあり、TestCondition
で条件を判定するコードを書くことで作成ができます。
UStateTreeConditionBlueprintBase を継承する
UStateTreeConditionBlueprintBase
を継承してコンディションを作成することができます。
以下コード例。
#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;
};
#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;
}
FStateTreeConditionCommonBase を継承する
FStateTreeConditionCommonBase
を継承することで作成ができます。例のごとくインスタンスデータは別途用意する必要があります。
以下コード例。
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;
};
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;
}
その他
デバッグ用メソッドについて
デバッグ用のメソッドはFStateTreeExecutionContext
にあるようです。
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以外にも使えそうな感じです。具体的にはイベントフロー管理などがいいのではないでしょうか。