StateTreeでCharacterAIを作るときに使いそうなこと備忘録
前回記事の続きoneday-memorandum.hateblo.jp
LinkedStateTreeOverrideの注意点
LinkedStateTreeOverrideを使用する際には、コンテキストを一致させておく必要あり。添付画像の赤線の部分は一致している必要がありますが、緑の部分は異なっていても問題ありません。

動的な値の設定
StateTreeで設定したTreeのパラメータは、キャラクターの初期化時にDataTableなどから読み込んで動的に設定することが可能。
StateTreeComponent::SetStateTreeReferenceから設定。
LinkedStateOverrideを使用する場合も同様。
タスクの逐次実行(シーケンシャルなタスク実行)
BehaviorTreeのように、前のタスクが完了したら次のタスクを実行したい場合の解説です。
問題点
タスクを子のノードとして追加すると、それらのタスクは同時に実行されてしまいます。

解決策
子として追加するのではなく、兄弟としてタスクを追加し、「タスク完了時に次のタスクへ移行する」という遷移設定を行うことで、逐次実行を実現できます。

タスクによるパラメータの動的設定
タスク内でパラメータを設定する方法について解説します。
1. Moduleの追加
まず、プロジェクトの .Build.cs ファイルに以下のモジュールを追加する必要があります。
"PropertyBindingUtils",
2. FStateTreePropertyRefの使用
FStateTreePropertyRef を使用して、StateTree内のプロパティへの参照を保持します。今回はEnumでStateを設定するTaskを作成する例です。
注意点: EnumやStructを使用する場合、meta指定子のRefTypeの設定に注意が必要です。
実装例:AIStateを設定するTask
StateTreeTask_SetState.h
// Fill out your copyright notice in the Description page of Project Settings. #pragma once #include "StateTreePropertyRef.h" #include "StateTreeTaskBase.h" #include "StateTreeTask_SetState.generated.h" UENUM(BlueprintType) enum class EAIState : uint8 { None, Normal, Suspicious, Battle, Dead }; /*********************************************************************************** * EnumSet ***********************************************************************************/ USTRUCT() struct STATETREELEARNING_56_API FStateTreeTask_SetStateInstanceData { GENERATED_BODY() // EnumのProperty UPROPERTY(EditAnywhere, meta = (RefType = "/Script/StateTreeLearning_56.EAIState", Required), Category = "") FStateTreePropertyRef StateToSet; // 次のステート UPROPERTY(EditAnywhere, meta = (Required), Category = "") EAIState NewState; }; USTRUCT(BlueprintType, meta = (DisplayName = "Set Param (AIState)")) struct STATETREELEARNING_56_API FStateTreeTask_SetState : public FStateTreeTaskCommonBase { GENERATED_BODY() public: using FInstanceDataType = FStateTreeTask_SetStateInstanceData; virtual const UStruct* GetInstanceDataType() const override { return FStateTreeTask_SetStateInstanceData::StaticStruct(); } // StateIn時にCall virtual EStateTreeRunStatus EnterState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const override; };
StateTreeTask_SetState.cpp
#include "StateTreeTask_SetState.h" EStateTreeRunStatus FStateTreeTask_SetState::EnterState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const { // FStateTreeTask_SetStateInstanceData取得 FInstanceDataType* InstanceData = Context.GetInstanceDataPtr<FInstanceDataType>(*this); check(InstanceData); // Property取得 if (EAIState* StateProperty = InstanceData->StateToSet.GetMutablePtr<EAIState>(Context)) { // InstanceDataに設定されている値をPropertyに反映 *StateProperty = InstanceData->NewState; } // このタスクではFinishTaskしない return EStateTreeRunStatus::Running; }
StateTreeのEvent発火タスク

StateTreeでキレた備忘録
環境 UE5.6.0-Release SourceBuild
リンカー参照エラー
StateTreeのパラメーターをStateTree実行中に操作したい!でも現状はBPだけでは実装できない・・・
のでC++書く!ってなったときに参照エラーが起きた。
具体的には
InstanceData->BoolPropertyRef.GetMutablePtr<bool>(Context)
でバインドしたパラメータのポインタを取得する処理を書くと参照エラーが起きた。
・解決方法
build.cs に PropertyBindingUtils を追加で解決した。
// MyProject.Build.cs
PublicDependencyModuleNames.AddRange(new string[] {
"Core",
"CoreUObject",
"Engine",
"InputCore",
"EnhancedInput",
"AIModule",
"PropertyBindingUtils", // ← これを追加
"StateTreeModule",
"GameplayStateTreeModule",
"UMG"
});
Linked State Tree Overrideの動的追加について
5.6リリースノートの多分この辺。 リリースノート見てるとこんな文字が。
For linked assets, test if the schema is the same. Add unit test for invalid SetLinkedStateTreeOverrides.
Add a function to set the LinkedStateTreeOverrides from BP.
そしてStateTreeComponentにはこんな関数が。
/** * Add a linked state tree override. * The override won't be set if it doesn't use the StateTreeComponentSchema schema. */ UFUNCTION(BlueprintCallable, Category = "Gameplay|StateTree") UE_API void AddLinkedStateTreeOverrides(const FGameplayTag StateTag, FStateTreeReference StateTreeReference);
ええやん!使えそう!!!!
使ったれ!!!!!!!
・・・・・・・・・
Trying to set the linked overrides 'XXX' with a wrong schema. XXXX
なんでや!!!!!!!!!!!
void UStateTreeComponent::SetLinkedStateTreeOverrides(FStateTreeReferenceOverrides Overrides) { // Validate the schema for (const FStateTreeReferenceOverrideItem& Item : Overrides.GetOverrideItems()) { if (const UStateTree* ItemStateTree = Item.GetStateTreeReference().GetStateTree()) { // ▼問題の箇所 if (ItemStateTree->GetSchema() == nullptr || ItemStateTree->GetSchema()->GetClass()->IsChildOf(UStateTreeComponentSchema::StaticClass())) { STATETREE_LOG(Warning, TEXT("%hs: Trying to set the linked overrides '%s' with a wrong schema. %s."), __FUNCTION__, *Item.GetStateTag().ToString(), *ItemStateTree->GetFullName() ); return; } } } LinkedStateTreeOverrides = MoveTemp(Overrides); }
|| ItemStateTree->GetSchema()->GetClass()->IsChildOf(UStateTreeComponentSchema::StaticClass())
泣かす。
!付けて修正。
うごいた。
泣かす。
ちなみにRestartLogicしないと反映はされなかった。StateTreeの組み方の問題かもしれないけど。
PropertyRefでEnumを使いたい!
StateTreePropertyRef.h にコメントが書いている。
(以下、機械翻訳したやつ)
StateTree のプロパティ参照 (FStateTreePropertyRef) について
FStateTreePropertyRefは、StateTree 内の選択されたプロパティへのポインタを取得するために使用されます。参照が指し示すプロパティの期待される型は、RefTypeメタ指定子で設定する必要があります。型指定のためのメタ指定子
以下のメタ指定子を使用して、
FStateTreePropertyRefの動作を制御できます。
RefType = "<type>"
- 参照するプロパティの型をコンマ区切りリストで指定します。
- サポートされている型は以下の通りです:
bool,byte,int32,int64,float,double,Name,String,Text, UObject ポインタ、および構造体。- 構造体とオブジェクトは、完全パス名を使用する必要があります。
- 複数の型が指定された場合、
GetMutablePtrTupleを使用できます。IsRefToArray
- 指定された場合、参照は
TArray<RefType>になります。CanRefToArray
- 指定された場合、参照は
RefTypeまたはTArray<RefType>のどちらにもバインドできます。Optional
- 指定された場合、参照はバインドされていない状態でも許容されます。
使用例
// float 型への参照 UPROPERTY(EditAnywhere, meta = (RefType = "float")) FStateTreePropertyRef RefToFloat; // FTestStructBase 型への参照 UPROPERTY(EditAnywhere, meta = (RefType = "/Script/ModuleName.TestStructBase")) FStateTreePropertyRef RefToTest;
Enumは・・・?
Structが /Script/ModuleName.structName (prefixなし) って書いてるしUSTRUCTとUENUMで同じrefsystemで動いてそうだしEnumも多分これで行けるだろ!
↓
だ め で し た 。
なんでやねん!!!!とキレながらコード見てると使えそうなコードが・・・
StateTreePropertyRefHelpers.cpp
if (UScriptStruct* Struct = Cast<UScriptStruct>(TargetRefField)) { PinType.PinCategory = UEdGraphSchema_K2::PC_Struct; PinType.PinSubCategoryObject = Struct; } else if (UClass* ObjectClass = Cast<UClass>(TargetRefField)) { PinType.PinCategory = UEdGraphSchema_K2::PC_Object; PinType.PinSubCategoryObject = ObjectClass; } else if (UEnum* Enum = Cast<UEnum>(TargetRefField)) { PinType.PinCategory = UEdGraphSchema_K2::PC_Enum; PinType.PinSubCategoryObject = Enum; if (Enum->GetMaxEnumValue() <= (int64)std::numeric_limits<uint8>::max()) { // Use byte for BP. It will use the correct picker and enum k2 node. PinType.PinCategory = UEdGraphSchema_K2::PC_Byte; } } else { checkf(false, TEXT("Typename in meta-data (%s) is invalid"), *TargetType); }
なんで使えないの・・・?と頭を悩ませていたら応えは単純明快。でもこんなんただの罠だろって答えだった。
/Script/Modulename.EEnumName (**prefix有り**)
で記載しないとだめだった。
まじでムカついた。ので備忘録として残す。
これが誰かの助けになってもらったら・・・
profVizあれこれ
前回の記事は2023/1か。久々だな え!?2023年!?去年丸2年更新してなかったのか・・・ということで久しぶり。 この2年の間に2回転職したよ。たいらです。
偉い人は言いました。 「GPUはパッケージ化しなくてもEditorで確認なんならスタンドアロンでの確認でいいよ^^」 と 自分はそうは思わない。 ということでEditorでProfileGPUしたときの保存形式のprofVizをどうにかしてパッケージで吐き出すように出来ないかを調査してみることに。 結論から言えばエンジン改造するしかなさそうだった。*1
GPUProfiler.cpp DumpEventTree 609~
#if !UE_BUILD_SHIPPING
// Create and display profile visualizer data
if (RHIConfig::ShouldShowProfilerAfterProfilingGPU())
{
// execute on main thread
{
struct FDisplayProfilerVisualizer
{
void Thread( TSharedPtr<FVisualizerEvent> InVisualizerData, const FText InVsyncEnabledWarningText )
{
static FName ProfileVisualizerModule(TEXT("ProfileVisualizer"));
if (FModuleManager::Get().IsModuleLoaded(ProfileVisualizerModule))
{
IProfileVisualizerModule& ProfileVisualizer = FModuleManager::GetModuleChecked<IProfileVisualizerModule>(ProfileVisualizerModule);
// Display a warning if this is a GPU profile and the GPU was profiled with v-sync enabled (otherwise InVsyncEnabledWarningText is empty)
ProfileVisualizer.DisplayProfileVisualizer( InVisualizerData, TEXT("GPU"), InVsyncEnabledWarningText, FLinearColor::Red );
}
}
} DisplayProfilerVisualizer;
TSharedPtr<FVisualizerEvent> VisualizerData = CreateVisualizerData( EventTree );
DECLARE_CYCLE_STAT(TEXT("FSimpleDelegateGraphTask.DisplayProfilerVisualizer"),
STAT_FSimpleDelegateGraphTask_DisplayProfilerVisualizer,
STATGROUP_TaskGraphTasks);
FSimpleDelegateGraphTask::CreateAndDispatchWhenReady(
FSimpleDelegateGraphTask::FDelegate::CreateRaw(&DisplayProfilerVisualizer, &FDisplayProfilerVisualizer::Thread, VisualizerData, VsyncEnabledWarningText),
GET_STATID(STAT_FSimpleDelegateGraphTask_DisplayProfilerVisualizer), nullptr, ENamedThreads::GameThread
);
}
}
#endif
なんかこのへんでVisalizerのデータ作ってそう。それをVizualizerに送ってVizualizerWindow作って読み込んでる感じ。
ProfileVisualizerModule.cpp
virtual void DisplayProfileVisualizer(TSharedPtr< FVisualizerEvent > InProfileData, const TCHAR* InProfilerType, const FText& HeaderMessageText, const FLinearColor& HeaderMessageTextColor) override
{
#if WITH_EDITOR
::DisplayProfileVisualizer( InProfileData, InProfilerType, HeaderMessageText, HeaderMessageTextColor );
#endif // WITH_EDITOR
}
なんでEditorだけやねーん! という感じでキレた。 じゃぁもういいよセーブどこ?
SProfileVisualizer.cpp
FReply SProfileVisualizer::OnSaveClicked()
{
#if PLATFORM_DESKTOP && WITH_EDITOR
// Message to display on completion
FText Message;
FString ProfileFilename = FPaths::ProjectLogDir();
ProfileFilename /= TEXT("profileViz");
ProfileFilename /= FString::Printf(
TEXT("%s-%i-%s.profViz"),
FApp::GetProjectName(),
FEngineVersion::Current().GetChangelist(),
*FDateTime::Now().ToString());
FArchive* ProfileFile = IFileManager::Get().CreateFileWriter(*ProfileFilename);
if (ProfileFile)
{
FVisualizerEvent::SaveVisualizerEventRecursively(ProfileFile, ProfileData);
// Close and delete archive.
ProfileFile->Close();
delete ProfileFile;
ProfileFile = NULL;
Message = NSLOCTEXT("ProfileVisualizer", "ExportMessage", "Wrote profile data to file");
}
else
{
Message = NSLOCTEXT("ProfileVisualizer", "FailedExportMessage", "Could not write profile data to file");
}
struct Local
{
static void NavigateToExportedFile(FString InGPUFilename, bool bInSuccessful)
{
InGPUFilename = FPaths::ConvertRelativePathToFull(InGPUFilename);
if (bInSuccessful)
{
FPlatformProcess::LaunchFileInDefaultExternalApplication(*InGPUFilename);
}
else
{
FPlatformProcess::ExploreFolder(*FPaths::GetPath(InGPUFilename));
}
}
};
FNotificationInfo Info(Message);
Info.Hyperlink = FSimpleDelegate::CreateStatic(&Local::NavigateToExportedFile, ProfileFilename, false);
Info.HyperlinkText = FText::FromString(ProfileFilename);
Info.bUseLargeFont = false;
Info.bFireAndForget = true;
Info.ExpireDuration = 8.0f;
FSlateNotificationManager::Get().AddNotification(Info);
#endif //#if PLATFORM_DESKTOP && WITH_EDITOR
return FReply::Handled();
}
VisualizerEvents.cpp
void FVisualizerEvent::SaveVisualizerEventRecursively(FArchive *Ar, TSharedPtr< FVisualizerEvent > VisualizerEvent )
{
//Write all simple members
*Ar << VisualizerEvent->Start;
*Ar << VisualizerEvent->Duration;
*Ar << VisualizerEvent->DurationMs;
*Ar << VisualizerEvent->Category;
*Ar << VisualizerEvent->EventName;
*Ar << VisualizerEvent->IsSelected;
uint32 NumChildren;
NumChildren = VisualizerEvent->Children.Num();
Ar->SerializeInt(NumChildren, MAX_uint32);
for(uint32 i = 0; i < NumChildren; ++i)
{
TSharedPtr< FVisualizerEvent > Child;
Child = VisualizerEvent->Children[i];
SaveVisualizerEventRecursively(Ar, Child);
}
}
なるほどね。 ということで結論的には
TSharedPtr
VisualizerData = CreateVisualizerData( EventTree );
ができれば良いわけだが・・・ C++自信無いニキなのでどうやってEventTree取得したら良いんだ・・・?というところで詰まってしまった。 RHIからどのレンダリングシステム使うかのViewport的なのを持ってきてそこから・・・?とか考えてるうちに10年くらいすぎそうなので、もうisualizerEventsのセーブの処理をGPUProfiler.cppのDumpEventTreeに移植してしまえば良いんじゃね?と思った。
がしかしココでたいらに雷走る
いやエンジン改造したくなくね?ということで、 プラグイン化した。 といってもEventTreeを取得するのはとてつもなく面倒そうだったのでProfileGPUで流れてくるログを解析するだけではある。
FOutputDevice
を使えば、登録して解除するまで、ログ流れてきたときにコールバックで色々処理できるらしい。うーん便利。 イメージ的には下みたいな感じ ちなみにtasksystemで別スレッドに逃がしてるのは、ProfileGPUがコマンド送信後次のフレームの画面をキャプチャする+RHIThreadで処理が走るためである。 BPLib作ったときにRHIThreadで処理する方法とかあんの?あるならもっとスマートに出来そうなんだけどなあ。
static FAutoConsoleCommand ExportProfVizCommand = FAutoConsoleCommand(
TEXT("ExportProfViz"),
TEXT("Write the results of executing ProfileGPU as simplified ProfViz data. Saved/Logs/ProfViz"),
FConsoleCommandDelegate::CreateLambda([]()
{
UProfVizExporterBPLibrary::ProfVisExport();
})
);
class UProfVisExportConsoleDevice : public FOutputDevice
{
public:
UProfVisExportConsoleDevice()
{
}
virtual void Serialize(const TCHAR* Data, ELogVerbosity::Type Verbosity, const class FName& Category, const double Time) override
{
strArray.Emplace(Data);
}
virtual void Serialize(const TCHAR* Data, ELogVerbosity::Type Verbosity, const class FName& Category) override
{
strArray.Emplace(Data);
}
};
void ProfVisExport()
{
strArray.Empty();
//GLogにProfVisExportConsoleDeviceを登録
GLog->AddOutputDevice(&device);
//ConsoleCommand[ProfileGPU]実行
const FString& Command = "ProfileGPU";
if (IConsoleManager::Get().ProcessUserConsoleInput(*Command, *GLog, GWorld) == false)
{
GEngine->Exec(GWorld, *Command);
}
Launch(TEXT("Task Launch Function Pointer"), &ProfVizExportProcess);
}
※↑はこんな感じだよみたいな感じで適当に書いたやつなので本当はBPで呼び出せるようにとかもしている。 コンソールコマンド
ExportProfViz
というコマンドを追加してみた。
ちなみに再現度はこんな感じ。かなり再現できてるのでは!?^^
ちなみにProfVizExportProcessの中身はとんでもない汚コードになってしまわれましたわ。
5.5.3で作ったので公開だ!とおもったら5.5.4が公開されてたでござる。うんち。もうこんな時間だよ(02:21)
https://drive.google.com/file/d/1m9QhBDf4vHRQQAqIZ-s_1Uh64aZRX28N/view?usp=drive_link:ProfVizExporter
*1:2025/3/9 UE5.5.3での確認
新年あけましておめでとうございます。
今年もよろしくお願いいたします。
今年の目標は、達成できるかどうかわからないとりあえず言っとけ的なところで言うとコミケで頒布する
小さな目標は体重80キロ代まで落とす
の二つの目標で行こうと思います。
ちなみになんとまだ絵は描いてます。
すごい。ただ最近目的がずれてきてるかもしれない。
ソッカの美術解剖学
本格的に人体の勉強をすべくソッカの美術解剖学ノートと言う本を購入した。
久々の勉強らしい勉強に頭を委託したと思ったが存外楽しく進められた。
とりあえず1周やり終わったので備忘録として生地を書く。
とりあえず人といえばここからということで頭の勉強。
これをそのままイラストに使うかと言われたらおそらくデフォルメすることになると思うが、どの筋肉がどう入っていてどこに穴が空いていてみたいな確認ができた。


体の骨偏
体のバランスがわからね~ってなってたところもあり、まずは骨の可動域や骨がどうついてるかなどで全体のバランスを知ることができた。なんとなくで筋肉こんな感じかなって書いてるがこのあと筋肉を勉強するときに、思ったより間違ってたということに気づく。


骨盤まわりも男女で違いがある部分も今後の絵に活かせそう。

体の筋肉編
骨の復習をしながら筋肉の付き方を勉強
この行動をするために筋肉はどう収縮するかと言うのを覚えた。そのおかげか、脇周りで困らなくなりそうだ。まだ実践してないけど。


腕・足の勉強編
身体同様筋肉がどう働くかを勉強。描きたい絵的にそんなに筋肉隆々な絵を書くわけではないけれど、腕曲げたときに上腕二頭筋が膨らむとか、そういうのは覚えておいて良さそうだなと思った。あとこのタイミングで30秒ドローイングを1回挟んだが、割りと身体を見る目が変わっていたので効果をかなり感じた。




一通り勉強したあとの全体絵

身体を見る目が養われたと感じる。ぶっちゃけこのときになると忘れている部分など結構あったのでこれから2周め、3周目とやっていき記憶の定着を目指していこう。
ということでここ2週間ほどは勉強らしい勉強をしていたので本来描きたいキャライラストはほぼ描けていない。2周め3周目をヤるときはちゃんとキャライラストも描いていこうと思います。
ちなみに以下は絵を描き始めて1ヶ月目と2ヶ月半目で描いたソッカやる前の絵。成長録として見返したときに赤面するかもしれないが一応おいておこう。


パルミー1週間
パルミー1週間試して半年間骨を埋めることを決めたのでその最初の7日の経過












パルミー前はこんな感じということでこれもはっとこ。


