最初に
StateTreeについて触る機会があったので、自分の中の整理を含めて記事にしてみました。
間違い等あればご指摘頂けますと幸いです。
また、今回の内容は基本的にプログラマー向けになります。
環境
Windows 11
UE 5.4.3
参考
履歴
日時 | 内容 |
---|---|
2024/08/15 | Schemaに関する情報に誤解を招く表現があったため、修正しました |
準備
エディターのメニューから「編集」>「プラグインの設定」から下記2つを有効化します
StateTreeとは
StateTreeとは、様々な場面でロジックをBehaviorTreeやStateMachineの様に管理できる
「階層型ステートマシン」です。
BehaviorTreeの代わりに思考を管理する用途に使用することもできれば、
BehaviorTree側で呼び出しのTaskも用意されているため併用も可能です。
また、UE5.4時点でExperimentalではありますが
AvalancheやMassEntity、GameplayInteractionsプラグインの中でも使用されており、
今後の活躍が期待できます。
StateTreeの構成
まず、StateTreeの機能を構成している主な要素について説明をしようと思います。
- State Tree Component
- State Tree Schema
- State
- Transition
- Node(Task, Evaluator, Condition)
1. State Tree Component
StateTreeを利用したいActorにアタッチするComponentです。
AI使用を目的として用意された派生であるUStateTreeAIComponent
の他、
C++、BPで継承して独自実装も可能です。
StateTreeComponentで行う内容は以下
- StateTreeの開始、一時停止、終了
- 使用するStateTreeとParameterの設定
- 使用するSchemaの定義
StateTreeの開始、一時停止、終了
UStateTreeComponent
はUBrainComponent
を継承しており、
各種オーバーライドした関数がStateTreeに対する実行処理になります
関数名 | 処理内容 |
---|---|
StartLogic | StateTreeの開始 |
RestartLogic | 実行中、また終了しているStateTreeの再稼働 |
StopLogic | StateTreeの停止 |
PauseLogic | StateTreeの一時停止 |
ResumeLogic | StateTreeの再開 |
IsRunning | StateTreeが実行中か |
IsPaused | StateTreeが一時停止中か |
使用するStateTreeとParameterの設定
このComponentで使用するStateTreeの指定を行います。
Parameterというのは、StateTreeのアセット側で定義して、
外部から引数として指定できる値でそれなりに自由なパラメーターの指定が可能です。
値を追加すると、StateTreeを設定している場所で追加した値の設定が可能になります。
使用するSchemaの定義
Schemaの定義は、StateTreeComponentが継承している
IStateTreeSchemaProvider
のGetSchema
を使って、
このComponentで使用したいSchemaのクラスを指定します。
// 例:AIStateTreeComponent
TSubclassOf<UStateTreeSchema> UStateTreeAIComponent::GetSchema() const
{
return UStateTreeAIComponentSchema::StaticClass();
}
この指定を行うことで、StateTreeComponentでStateTreeを設定する時に
同じSchemaを使用しているStateTreeしかリストアップされなくなります。
2. Schema
後述するNode(Task、Condition、Evaluator)の制限や、
StateTreeエディタにあるContext項目など各所で利用するデータの構造や形式を定義します。
C++限定ですが自分でUStateTreeSchema
を継承して作成や拡張することも可能です。
Schemaの基本的な役割は以下
- Contextの指定
- Nodeに対する制限
- Nodeとして使用できる型の制限
- ExternalDataの型指定、収集
Contextの指定
Schemaで定義される、StateTree内で使用可能な外部のデータです。
基本的にはStateTreeを利用しているActorやAIControllerなどが対象になっています。
ここで定義された内容はStateTree内でNodeのPropertyBindとして使用可能です。
Nodeに対する制限
Evaluator、ConditionのStateTree上での使用を制限できます
/** @return True if enter conditions are allowed. */
virtual bool AllowEnterConditions() const { return true; }
/** @return True if evaluators are allowed. */
virtual bool AllowEvaluators() const { return true; }
また、1つのStateに1つのTaskだけにすることも可能です。
/** @return True if multiple tasks are allowed. */
virtual bool AllowMultipleTasks() const { return true; }
Nodeとして使用できる型の制限
特定の型のTask、Evaluator、Conditionのエディタ上での使用自体を制限できます
UStateTreeAISchema
では追加でFStateTreeAITaskBase
を使用できるように指定しています。
/** @return True if specified struct is supported */
virtual bool IsStructAllowed(const UScriptStruct* InScriptStruct) const { return false; }
/** @return True if specified class is supported */
virtual bool IsClassAllowed(const UClass* InScriptStruct) const { return false; }
bool UStateTreeAIComponentSchema::IsStructAllowed(const UScriptStruct* InScriptStruct) const
{
return Super::IsStructAllowed(InScriptStruct) || InScriptStruct->IsChildOf(FStateTreeAITaskBase::StaticStruct());
}
ExternalDataの型指定、収集
External Dataとは、各Nodeで特定の外部データを参照したい時、
都度取得するための関数を呼ばなくて済むように予め情報をキャッシュして
Handle経由で簡単にアクセスできるようにする機能です。
Schemaではそこで使用できる型の指定と、実際のデータの収集を行います。
class STATETREEMODULE_API UStateTreeSchema : public UObject
{
// 使用できる型の判定.
virtual bool IsExternalItemAllowed(const UStruct& InStruct) const { return false; }
}
※具体的な使用方法はNodeのExternal Dataに関する項目をご確認ください。
例として、StateTreeComponentSchemaでは
- Actor
- Component
- World Subsystem
の使用が許可されています。
bool UStateTreeComponentSchema::IsExternalItemAllowed(const UStruct& InStruct) const
{
return InStruct.IsChildOf(AActor::StaticClass())
|| InStruct.IsChildOf(UActorComponent::StaticClass())
|| InStruct.IsChildOf(UWorldSubsystem::StaticClass());
}
そして、CollectExternalData関数で外部データの情報をキャッシュしています。
bool UStateTreeComponentSchema::CollectExternalData(const FStateTreeExecutionContext& Context, const UStateTree* StateTree, TArrayView<const FStateTreeExternalDataDesc> ExternalDataDescs, TArrayView<FStateTreeDataView> OutDataViews)
{
// ~略~
AAIController* AIOwner = Cast<AAIController>(Owner);
for (int32 Index = 0; Index < ExternalDataDescs.Num(); Index++)
{
const FStateTreeExternalDataDesc& ItemDesc = ExternalDataDescs[Index];
if (ItemDesc.Struct != nullptr)
{
if (ItemDesc.Struct->IsChildOf(UWorldSubsystem::StaticClass()))
{
UWorldSubsystem* Subsystem = World->GetSubsystemBase(Cast<UClass>(const_cast<UStruct*>(ItemDesc.Struct.Get())));
OutDataViews[Index] = FStateTreeDataView(Subsystem);
}
else if (ItemDesc.Struct->IsChildOf(UActorComponent::StaticClass()))
{
UActorComponent* Component = Owner->FindComponentByClass(Cast<UClass>(const_cast<UStruct*>(ItemDesc.Struct.Get())));
OutDataViews[Index] = FStateTreeDataView(Component);
}
else if (ItemDesc.Struct->IsChildOf(APawn::StaticClass()))
{
APawn* OwnerPawn = (AIOwner != nullptr) ? AIOwner->GetPawn() : Cast<APawn>(Owner);
OutDataViews[Index] = FStateTreeDataView(OwnerPawn);
}
else if (ItemDesc.Struct->IsChildOf(AAIController::StaticClass()))
{
AAIController* OwnerController = AIOwner;
OutDataViews[Index] = FStateTreeDataView(OwnerController);
}
else if (ItemDesc.Struct->IsChildOf(AActor::StaticClass()))
{
AActor* OwnerActor = (AIOwner != nullptr) ? AIOwner->GetPawn() : Owner;
OutDataViews[Index] = FStateTreeDataView(OwnerActor);
}
}
}
return true;
}
ここで注意しないといけないのが、上記処理をみるとUStateTreeComponentSchema
で
取得できるActorやComponentは全てOwnerを経由したものであるということです。
つまり、Ownerに関連していない他キャラなどの参照は行えません。
そういったデータを参照するときはEvaluaterを使用するのが良いでしょう。
3. State
状態(階層)を定義します。遷移する条件に応じてアクティブなStateを切り替えることで、
状況に応じたTaskなどの処理を行うことができます。
Stateのパラメーターを見ていきましょう
名前 | 説明 |
---|---|
Name | Stateの名前 |
Type | Stateのタイプ |
Selection Behavior | Stateがアクティブになる時の挙動 |
Parameters | このState以下でバインド可能な一時変数 |
Enter Conditions | このStateをアクティブの判定に使用されるConditionのリスト |
Tasks | このStateがアクティブな場合に処理されるTaskのリスト |
Transitions | このStateがアクティブな時に他Stateへ移動する場合の遷移先リスト |
Type
このStateがどういった挙動を行うかの設定です。
-
State
Taskや子供Stateを含む通常のState -
Group
子Stateだけを含むStateです。これ自体に処理を行う能力はありません。
※そのためTaskは設定できません、途中でタイプを切替してもCompile時に破棄されます -
Linked
StateTree内の別Stateのフローを流用したい場合に使用します。
Linked Subtreeには流用したいState(TypeはSubtree設定のものOnly)を指定します
※Group同様、このState自体にTaskを設定することはできません。 -
Subtree
Linked設定のStateから指定することができるStateです。
このStateは意図的に遷移しない限り、通常のフローで遷移することはありません -
Linked Asset
別のSubTreeアセットを使用することが可能です。
※使用できるのは、別のStateTreeのみです
※Linked と SubTreeについての補足
使用例を説明すると- 戦闘中はエネミーに向かって移動し、到着したらランダムなアクション
- 非戦闘中はプレイヤーに向かって移動し、到着したらランダムなアクション
といったすごいアレな内容のフローを用意したい時があるとします。
これを何も考えずに作ります。
今は単純な移動とランダムなアクション、と言っているので問題ないですが
これが複雑な行動になっていくと、それぞれのStateに都度タスクや
子Stateを追加していくのは正直なところ手間です。
そこでLinkedとSubTreeを活用します、
先ほどの画像の処理をSubTreeを使って変更するとこのようになります。
変更した内容としては
- それぞれのStateで行っていた処理を別のフローで用意
- 呼び出し元のStateをLinked設定に変更して移動先のStateをSubTreeに変更
これで今後拡張があっても、共通処理になっているので
SubTreeの方を変更するだけで済みます。
ちなみに、SubTreeのStateでは呼び出し元(Linked)のStateから
渡すパラメーターを引数の様に定義できます。
Selection Behavior
-
None
Transitionの遷移先候補に指定できません、また、通常の遷移の対象にもなりません。 -
TryEnterState
子Stateがいても、まずこのStateが選択されます。
子Stateに遷移したい場合は別途Transitionを指定する必要があります。 -
TrySelectChildrenInOrder
最初に遷移可能な子Stateを選択しようとします。
子Stateが存在しない場合は、このStateが選択されます -
TryFollowTransitions
Stateに遷移する時、代わりにTransitionをトリガーして有効なStateに遷移します
4. Transition
Stateなどから、他のStateに遷移したい場合の条件などを設定するための機能です、
こちらは以下のパターンの使用方法があります。
- State 経由のTransition
- Instance Data 経由のTransition
State経由のTransition
Trigger
遷移の判定タイミングです。
On State Completed | Stateが終了した時に遷移判定が行われます |
---|---|
On State Succeeded | Stateが成功終了した時に遷移判定が行われます |
On State Failed | Stateが失敗終了した時に遷移判定が行われます |
On Tick | 毎フレーム遷移判定が行われます |
On Event | 特定のEvent(Gameplay Tag)が発行された時に遷移判定が行われます |
OnEventのトリガーとなるイベントは各Nodeから呼び出し可能です。
C++はFStateTreeInstanceData(FStateTreeInstanceStorage)
のEventQueue
の関数を呼び出します。
struct FStateTreeInstanceStorage
{
/** Events */
UPROPERTY()
FStateTreeEventQueue EventQueue;
};
struct STATETREEMODULE_API FStateTreeEventQueue
{
void SendEvent(const UObject* Owner, const FGameplayTag& Tag, const FConstStructView Payload = FConstStructView(), const FName Origin = FName());
};
UStateTreeComponent
を使用している場合は下記関数でも呼び出し可能です。
class UStateTreeComponent
{
/** Sends event to the running StateTree. */
UFUNCTION(BlueprintCallable, Category = "Gameplay|StateTree")
void SendStateTreeEvent(const FStateTreeEvent& Event);
/** Sends event to the running StateTree. */
void SendStateTreeEvent(const FGameplayTag Tag, const FConstStructView Payload = FConstStructView(), const FName Origin = FName());
};
Transition To
遷移先の指定を行います。特定のStateを指定する他、遷移の仕方の指定をすることもできます
None | 何もしません |
---|---|
Next State | 同階層の次(下)にあるStateに移動します |
Next Selectable State | 同階層の次(下)にある、遷移可能なStateに移動します |
Tree Succeeded | StateTreeを成功扱いで終了させます。 |
Tree Failed | StateTreeを失敗扱いで終了させます。 |
StateTree内のState名 | 指定したStateに遷移します |
Conditions
遷移条件です。複数指定可、詳細は後述するNodeのConditionをご参照ください。
Priority
※On Tick, On Eventのみ
遷移の優先度です。同じ優先度が複数ある場合は登録順に実行されます。
Delay Transition
※On Tick, On Eventのみ
trueを指定すると、遷移実行がDelay Duration( + Delay Random Variance)の秒数だけ遅延します。
遅延中に他のTransitionが条件を満たした場合は、そちらが優先されます。
Delay Duration
※On Tick, On Eventのみ
遷移の実行を遅らせる時間(秒)
Delay Random Variance
※On Tick, On Eventのみ
Delay Durationに加算する、遅延時間のランダムな範囲の最大値(秒)です。
Instance Data経由のTransition
TransitionはState外、各Nodeからのリクエスト可能です。
C++ではFStateTreeInstanceData
(FStateTreeInstanceStorage
)経由でリクエストします。
struct FStateTreeInstanceStorage
{
void AddTransitionRequest(const UObject* Owner, const FStateTreeTransitionRequest& Request);
};
5. Node
StateTree内で使用する要素で、以下の派生があります。
- Task
基本はActiveなState内で行う「処理」を定義するものです
※Global Taskという、StateTree実行中にStateに関係なく動作するTaskもあります - Evaluator
SchemaのContextやExternal Data、StateTreeのParameterで参照できない、
外部のデータをStateTreeで参照できるようにするものです。 - Condition
StateのTransition全般で「条件」を定義するものです。
これら全てをまとめて「Node」と呼称します。
実際、各種使用される定義の派生は全てNodeBaseを継承しています。
C++ベースの継承
BPベースの継承
まずそれぞれの話に移る前に、Nodeで共通部分の説明をします。
- Instance Data
- Property Bind
- External Data(外部データの参照の設定)
Instance Data
各Nodeは、
-
FStateTreeNodeBase
を継承した実処理を記載する定義 - NodeのRuntime中のデータを保存するInstanceData用の定義
で構成されます ※BPベースの場合は考慮の必要はありません
デフォルトで用意されている、DelayTaskを例に見てみます。
USTRUCT()
struct STATETREEMODULE_API FStateTreeDelayTaskInstanceData
{
GENERATED_BODY()
/** Delay before the task ends. */
UPROPERTY(EditAnywhere, Category = Parameter, meta = (EditCondition = "!bRunForever", ClampMin="0.0"))
float Duration = 1.f;
/** Adds random range to the Duration. */
UPROPERTY(EditAnywhere, Category = Parameter, meta = (EditCondition = "!bRunForever", ClampMin="0.0"))
float RandomDeviation = 0.f;
/** If true the task will run forever until a transition stops it. */
UPROPERTY(EditAnywhere, Category = Parameter)
bool bRunForever = false;
/** Internal countdown in seconds. */
float RemainingTime = 0.f;
};
USTRUCT(meta = (DisplayName = "Delay Task"))
struct STATETREEMODULE_API FStateTreeDelayTask : public FStateTreeTaskCommonBase
{
GENERATED_BODY()
using FInstanceDataType = FStateTreeDelayTaskInstanceData;
FStateTreeDelayTask() = default;
virtual const UStruct* GetInstanceDataType() const override { return FInstanceDataType::StaticStruct(); }
virtual EStateTreeRunStatus EnterState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const override;
virtual EStateTreeRunStatus Tick(FStateTreeExecutionContext& Context, const float DeltaTime) const override;
};
コードを見るとわかりますが、
-
FStateTreeDelayTask
がFStateTreeNodeBase
を継承して、時間カウントの実処理の定義 -
FStateTreeDelayTaskInstanceData
が処理で使用するパラメーターを保持
となっています。
何故このような構成になっているのかは、
ActiveではないStateのメモリを無駄に確保しないようにするためと思われます。
InstanceDataの指定はFStateTreeNodeBase
のGetInstanceDataType関数で行います。
UStruct
を渡していますが、派生であるUClass
の指定も可能です
/** @return Struct that represents the runtime data of the node. */
virtual const UStruct* GetInstanceDataType() const { return nullptr; };
ちなみに全てのパラメーターをInstanceDataに定義する必要はなく、
設定パラメーターなどRuntime中に変化しないものは、実処理側の宣言で問題ありません
Property Bind
Evaluatorなどを作って頂くとわかりやすいですが、
StateTree内のEvaluatorやTaskなどのパラメーターを他のNodeで参照できる機能です。
この機能はNodeの種類によってバインドできる対象が異なります。
Nodeの種類 | バインド可能なデータ |
---|---|
Condition(StateのEnter条件) | Globalなデータの他、親StateのTaskのパラメーター |
Condition(Transitionの条件) | Globalなデータの他、親Stateと現在ActiveなStateのTaskのパラメーター |
Task | Globalなデータの他、親StateのTask、現在ActiveなStateの自分より前に実行されるTaskのパラメーター |
親StateのTaskで処理した結果を子StateのEnter条件に使いたい、が可能ということです。
また、Propertyのカテゴリで以下の様にPropetyBindの制限をかけることもできます。
-
Categoryに”Input”を指定
// 例:DrawDebugTextTaskのReferenceActor USTRUCT() struct STATETREEMODULE_API FStateTreeDebugTextTaskInstanceData { GENERATED_BODY() /** Optional actor where to draw the text at. */ UPROPERTY(EditAnywhere, Category = "Input", meta=(Optional)) TObjectPtr<AActor> ReferenceActor = nullptr; };
StateTree上の表示を見ると、バインドでのみ指定ができることが分かります。
DrawDebugTextTask
の場合はOptionalが設定されていますが、
通常Inputカテゴリに指定されているパラメーターはバインドされていない時は
エラーになりますのでご注意ください。
-
指定なし(or 上記以外)
例:DelayTaskUSTRUCT() struct STATETREEMODULE_API FStateTreeDelayTaskInstanceData { GENERATED_BODY() /** Delay before the task ends. */ UPROPERTY(EditAnywhere, Category = Parameter, meta = (EditCondition = "!bRunForever", ClampMin="0.0")) float Duration = 1.f; ~略~ };
External Data(外部データの参照の設定)
Schemaの説明でも記載しましたが、
StateTree外の情報を簡単に参照できるようにするための機能です。
※BPベースでは使用できません。
使用方法はFStateTreeNodeBase
のLink関数をオーバーライドして、
TStateTreeExternalDataHandle
を使ってHandleを定義するだけです。
struct FTestSTTask_Test : public FStateTreeTaskBase
{
virtual bool Link(FStateTreeLinker& Linker) override;
// ここで使用できる型はSchemaで指定した型のみ!
TStateTreeExternalDataHandle<UStateTreeComponent> CompHandle;
};
virtual bool FTestSTTask_Test::Link(FStateTreeLinker& Linker)
{
Linker.LinkExternalData(CompHandle);
return true;
}
あとはFStateTreeExecutionContext
のGetExternalData()
で参照するだけです。
ポインタも可能。
EStateTreeRunStatus FTestSTTask_Test::EnterState(FStateTreeExecutionContext& Context,
const FStateTreeTransitionResult& Transition) const
{
const UStateTreeComponent& Component = Context.GetExternalData(CompHandle);
return FStateTreeTaskBase::EnterState(Context, Transition);
}
5-1. Task
TaskはアクティブなState内で動作する通常のTaskと、
Stateの状態に関係なく動作するGlobalTaskの2種類があります。
こちらは呼び出し方が違うだけで、Task自体は共通で使用可能です。
Taskの処理のタイミング
GrobalTaskは基本的にStateTree実行中、常に実行されます。
通常のTaskでは以下のタイミング別に処理を呼び出すことができます
イベント名 | タイミング |
---|---|
EnterState | Stateの開始時に呼ばれます |
Tick | StateがActiveな時、毎Tick呼ばれます |
ExitState | Stateの終了時に呼ばれます |
StateCompleted | Stateが完了した時に呼ばれますが、 |
条件付きのTransitionによる遷移など特定の状況下では呼ばれないので | |
終了時の解放、クリア処理などを呼ぶのは避けた方が良いです。 |
また、タスクの処理タイミングは設定によっても変化するため注意が必要です。
-
bShouldStateChangeOnReselect
trueの場合、タスクは所属しているStateが前にアクティブだった場合でも、EnterState/ExitState を呼び出します。 -
bShouldCallTick
Tickを呼ぶかどうかのフラグ
※falseだとバインドされているプロパティの情報が更新されないので注意してください -
bShouldCallTickOnlyOnEvents
trueに設定すると、Tick()はイベントがあるときのみ呼び出されます。
bShouldCallTickState
がtrueの場合は効果なし。
※Tickしない場合はバインドプロパティの情報が更新されないので注意してください
TaskはStateの終了判定に関係している
TaskはStateの完了判定に関わっており、StateのTransitionにある
OnStateCompleted
OnStateSucceeded
OnStateFailed
上記タイミングはState内にあるTaskの終了に応じて発行されます
(複数ある場合はどれか1つが終了するとトリガーされます)
※GlobalTaskの場合はStateTree自体の終了判定になります
挙動としてはDelayTaskがわかりやすいです、
Durationで指定された時間が経過するとOnStateSucceeded
で終了します。
ただし、全てのTaskがこの限りではありません。
下記画像で使用しているDebugTextTaskは終了しないTaskのため、
この場合はState1に留まり続けてしまうため注意が必要です。
5-2. Evaluator
ContextやStateTreeのParameter以外の、使用したい外部データを取得するのに使用します。
データの更新は以下のタイミングで行えるようになっています。
関数名 | タイミング |
---|---|
Tick | StateTree実行中毎Tick呼ばれる |
TreeStart | StateTree開始時 |
TreeStop | StateTree終了時 |
上記好きなタイミングで取得処理を記載し、EvaluatorのInstanceDataの変数に反映して
変数のUPropertyでOutputを指定すればStateTree内で参照が可能になります
5-3. Condition
Stateの遷移に関連する場所で使用する判定を定義します。
使用箇所は以下
struct STATETREEMODULE_API FStateTreeConditionBase : public FStateTreeNodeBase
{
virtual bool TestCondition(FStateTreeExecutionContext& Context) const { return false; }
};
おまけ
StateTreeにはデバッグ機能が存在しており、StateTreeの遷移を録画することが可能です。
表示されていない場合はツールバー>Window>Debuggerで表示してください
- シュミレーションの再生、停止
- StateTreeのトレース録画の開始、停止
- 録画したトレースの削除
- トレースした対象Actorと実行番号
- 録画時のタイムラインとState表示
- 選択している4と5のシークバーのタイミングにおけるStateの状態
最後に
次回があれば、今回はみでたFStateTreeExecutionContext
と合わせて
StateTreeの進行について記事にできればと思います。