EditorPaths.EnabledをONにしているにもかかわらず、パッケージでシーケンサーの静的バインドができない場合がある

お世話になっております。

以前、御社のご担当者様と質問会を開催させていただいた際に、シーケンサー上からレベルインスタンス以下のオブジェクトを静的バインドしたい場合は、EditorPaths.EnabledをONにすることで対応可能と伺っておりました。

その上で現在、以下の症状を確認しております。

  1. EditorPaths.EnabledをONにする
  2. WorldPartitionをONにする
  3. レベルインスタンス以下に存在するオブジェクトをバインドする
    1. バインド方法:シーケンサーエディタ左上の追加ボタンからアクタトラックの追加で該当オブジェクトを選択、もしくはオブジェクトが存在するレベルインスタンスを開いて、オブジェクトをシーケンサーエディタにドラッグアンドドロップ
  4. 特定のフレームにキーを追加(トランスフォームトラックからの回転値変更や、可視性の切り替えなど)
  5. パッケージを作成
  6. 動作するケースと動作しないケースがある

なお、シーケンサーエディタ上でバインドされないトラックをコピーペーストしたテキストを添付いたします。

さらに、こちら側で解決を試みたものを羅列させていただきます。いずれも解決には至りませんでした。

  • 再バインド
  • キーを再生区間全域に掛かるように、配置する(100フレームであれば、0と100にキーを配置)
  • シーケンサー上でタグをつける
  • アウトライナー上のレベルインスタンスの名前を「LI_St_VS_CHN_WallArt_A」として、ObjectPathと合わせる

お忙しいところ恐縮ですが、何卒よろしくお願いいたします。

[Attachment Removed]

再現手順

  1. EditorPaths.EnabledをONにする
  2. WorldPartitionをONにする
  3. レベルインスタンス以下に存在するオブジェクトをバインドする
    1. バインド方法:シーケンサーエディタ左上の追加ボタンからアクタトラックの追加で該当オブジェクトを選択、もしくはオブジェクトが存在するレベルインスタンスを開いて、オブジェクトをシーケンサーエディタにドラッグアンドドロップ
  4. 特定のフレームにキーを追加(トランスフォームトラックからの回転値変更や、可視性の切り替えなど)
  5. パッケージを作成
  6. 動作するケースと動作しないケースがある

[Attachment Removed]

既知のバグとしてue5.6以降のエンジンでシーケンサーの内のEditorPathsを使ったパス解決が失敗する問題を見つけることはできませんでした。

恐らくパス解決に失敗した場合でもランタイムで特別なログが出ていないのではないかと思います。

ランタイムでのパス解決は以下の様なコールスタックで呼び出されます。

UWorldPartitionLevelStreamingPolicy::GetSubObject(const wchar_t * SubObjectPath) Line 286	C++
UWorldPartition::ResolveSubobject(const wchar_t * SubObjectPath, UObject * & OutObject, bool bLoadIfExists) Line 1687	C++
ULevel::ResolveSubobject(const wchar_t * SubObjectPath, UObject * & OutObject, bool bLoadIfExists) Line 4795	C++
UWorld::ResolveSubobject(const wchar_t * SubObjectPath, UObject * & OutObject, bool bLoadIfExists) Line 10054	C++
FSoftObjectPath::ResolveObjectInternal() Line 903	C++
FActorLocatorFragment::Resolve(const UE::UniversalObjectLocator::FResolveParams & Params) Line 165	C++
UE::UniversalObjectLocator::IUniversalObjectLocatorModule::RegisterFragmentType::__l2::<lambda>(const void *) Line 87	C++
`UE::UniversalObjectLocator::IUniversalObjectLocatorModule::RegisterFragmentType<FActorLocatorFragment>'::`2'::void <lambda>(const TArray<FString,TSizedDefaultAllocator<32>> &, UWorld *, FOutputDevice &)::<lambda_invoker_cdecl>(const void * Payload, const UE::UniversalObjectLocator::FResolveParams & Params) Line 88	C++
UE::UniversalObjectLocator::FFragmentType::ResolvePayload(const void * Payload, const UE::UniversalObjectLocator::FResolveParams &) Line 14	C++
FUniversalObjectLocatorFragment::Resolve(const UE::UniversalObjectLocator::FResolveParams & Params) Line 627	C++
FUniversalObjectLocator::ResolveSyncImpl(const UE::UniversalObjectLocator::FResolveParams & Params) Line 118	C++
FUniversalObjectLocator::Resolve(const UE::UniversalObjectLocator::FResolveParams & Params) Line 68	C++

確実に失敗する再現方法があれば、UWorldPartitionLevelStreamingPolicy::GetSubObject関数内にブレイクポイントを張っていただいてステップしてもらうことで原因を特定できると思います。またはこの周辺関数に失敗時のログを仕込んでいただくのも有用かと思います。

可能性としてはセルがロードされていないことがありえるのではないかと推測しています。

[Attachment Removed]

ご回答ありがとうございます。返信にお時間を頂いてしまい申し訳ございません。

頂いたコールスタックなどの情報を元に、追加調査を行いました。

まず、今回問題になっている状態の説明の為にレベル構成について改めて記載致します。

・LevelInstanceActor_A(親階層)

・LevelInstanceActor\_B(子階層)

    ・バインドされないActor(孫階層)

上記が、現状のレベル構成となります。

以下の文では、上記の名称を利用させていただきます。

今回問題になっている箇所では、WorldPartitionを利用しており、バインドされないActorもLevelInstance以下に存在しておりますが、こちらでCellのロード制御などは行っておらず、常に全てのロードを行う状態にしている認識です。

ここでいう常に全てロードを行う状態というのは、セルサイズをほぼ最大化して、ロード範囲も同等のサイズにしており、さらに、LevelInstanceActor_Aの「Is Spatially Loaded」をfalseとしている状態です。

バインドされないActorとLevelInstanceActor_Bの「Is Spatially Loaded」はtrueとなっております。

念のため、バインドされないActorの「Is Spatially Loaded」をfalseとしてみたのですが、問題は解決されませんでした。

これらを踏まえて、スタックトレースを確認したところ、失敗しているケースでは、UWorldPartitionLevelStreamingPolicy::GetSubObjectの

if (const FName* CellName = FindCellNameForSubObject(SrcPath, /*bInResolveContainers=*/true, OutSubObjectString, OutLevelMountPointContext, OutCellOuter))

でtrueにならず、そこから先に進まずに失敗しており、成功例では正しくこの先に入っており、StaticFindObjectに成功していました。

そこで、FindCellNameForSubObjectの中身を確認したところ、失敗ケースではSubObjectsToCellRemappingの中身に、今回バインドを行いたいActorが登録されていないことがわかりました。

改めて成功例を見ると、SubObjectsToCellRemappingの中身にバインドを行いたいActorが登録されております。

なお、上記の実験時に参照解決を行おうとしていたObjectのUAIDは成功時、失敗時を問わず同一の文字列が利用されていることを確認済みです。

そこで実験として、バインド解決ができていないActorをInitialRuntimeStateがActivatedになっているDataLayerに登録したところ、パッケージのSubObjectsToCellRemappingの中身に登録され、バインドの解決が行われる状態を確認しました。

しかし、このActorには元々DataLayerが登録されておらず、このActorが配置されているLevelInstanceActorに設定されているDataLayerは、今回問題が発生しているWorldPartitionのレベル、LevelInstanceActorに登録されているレベルのどちらにも登録されていない(インスタンス化されていない)ものです。

その為、このレベルでは該当のActorのON/OFFは発生せず、DataLayerには本来登録の必要が無いものであるという認識です。

ここで改めて3点、質問させてください。

  1. DataLayerに登録することでSubObjectsToCellRemappingに登録されたのは何故でしょうか?
  2. バインド解決を行う場合は、SubObjectsToCellRemappingに登録されることが必要なのでしょうか?
  3. もし必要なのであれば、SubObjectsToCellRemappingに登録する為には、どのようなことを行えばよいでしょうか?今回はDataLayerに登録するというフローを行いましたが、他の方法があればご教授いただければ幸いです。

参考資料として、登録したDataLayerと、WorldSettingsの画像を添付いたします。

不足情報がありましたら、改めて添付いたします。

よろしくお願いします。

[Attachment Removed]

[Attachment Removed]

ご無沙汰しております。

引き続き調査を進める中で、新たに判明した内容がありましたので共有いたします。

以下の内容を試しました。

  • 「Is Spatially Loaded」は、LevelInstanceActor_A、LevelInstanceActor_B、バインドされないActor全てにおいてfalseにする
    • 結果:バインド失敗
  • LevelInstanceActor_Bに登録されていたDataLayerを全て削除
    • 結果:バインド失敗
  • LevelInstanceActor_Bに登録されているDataLayerをパーシスタントレベルに配置
    • 結果:バインド成功

いずれも、失敗している場合はSubObjectsToCellRemappingに登録されておらず、成功した場合はSubObjectsToCellRemappingに登録されているという状況です。

お手数をお掛けして恐縮ですが、ご確認いただけますと幸いです。

引き続きよろしくお願いいたします。

[Attachment Removed]

以下の手順でパッケージビルドでバインドが外れてしまう現象を再現できました。

  • LevelInstanceをパーシスタントレベルに配置し、 IsSpatiallyLoadedをfalseに設定する
  • Levelinstance内のアクターをシーケンサーでバインディングする
  • パッケージビルドを行い、実行する

原因としてはレベルインスタンス内のアクターのEditorPathsを使ったバインディングは、どこかのセルに属している属している必要があります。

IsSpatiallyLoaded:falseの場合パッケージビルドではパーシスタントレベル下のAlwaysLoadedのアクターとして登録されるため、どのセルにも属しません。そのためSubObjectsToCellRemappingに登録されず、解決に失敗してしまいます。

データレイヤーにアサインした場合、AlwaysLoadedの判定を通過せずデータレイヤーに属した一つのセルとして展開されるため解決に成功しているということなりそうです。

問題を解決するためには IsSpatiallyLoadedをtrueに設定してどこかのセルに属させるか、常にロードされるデータレイヤーを使ってセルに分類させるかのどちらかになります。

この方法で解決がみられるかご確認いただけますでしょうか。

[Attachment Removed]

ありがとうございます。

ご提案いただいた方法について確認したところ、LevelInstance をパーシスタントレベルに配置し、IsSpatiallyLoaded = trueとすることで、今回の問題を回避できることを確認できました。

一方で、今回LevelInstanceActorのIsSpatiallyLoaded = falseに設定していた理由があります。

該当のレベルはゲーム中のカットシーン用レベルであり、常時ロードされていても問題がないため、セルロードをワークフローに組み込まずに運用したいと考えていました。また、デザイナーがレベルを開いた際に、最初からレベルインスタンス内のアクタがロードされた状態となり、すぐに編集・カスタマイズできるようにしたいという意図もあります。

そこで、追加で4点質問させてください。

  1. こちらの挙動は仕様となるのでしょうか?
  2. Always Loaded な Level Instance は静的バインドできないとのことでしたが、パーシスタントレベルに直接配置されたアクタはどのような扱いになりますでしょうか。
  3. レベルを開いた際に、Level Instance 内のアクタが最初からロード済み(エディタ上でオブジェクトが表示)となる状態を実現する方法として、IsSpatiallyLoaded = false以外に推奨される方法はありますでしょうか。
  4. もし推奨方法がある場合、静的バインドとの関係も含めてご教示いただけますと幸いです。

重ねての質問になり恐縮ですが、ご回答よろしくお願いいたします。

[Attachment Removed]

現時点では仕様となりますが、バグとして扱うべき問題だと考えられます。

UE-389193 Sequencer binding resolve fails in packaged builds for actors in non spatially loaded Level Instances

としてバグ登録しました。

対策としてパス解決に失敗した場合にパーシスタントレベル上のアクターを検索する処理を追加する方法が機能します。

Object* UWorldPartitionLevelStreamingPolicy::GetSubObject(const TCHAR* SubObjectPath)
{
	TRACE_CPUPROFILER_EVENT_SCOPE(UWorldPartitionLevelStreamingPolicy::GetSubObject);
 
	const FString SrcPath = UWorld::RemovePIEPrefix(SubObjectPath);
 
	FString OutSubObjectString;
	const UObject* OutLevelMountPointContext = nullptr;
	UObject* OutCellOuter = nullptr;
	if (const FName* CellName = FindCellNameForSubObject(SrcPath, /*bInResolveContainers=*/true, OutSubObjectString, OutLevelMountPointContext, OutCellOuter))
	{
		if (const UWorldPartitionRuntimeLevelStreamingCell* Cell = (UWorldPartitionRuntimeLevelStreamingCell*)StaticFindObject(UWorldPartitionRuntimeLevelStreamingCell::StaticClass(), OutCellOuter, *(CellName->ToString())))
		{
			if (UWorldPartitionLevelStreamingDynamic* LevelStreaming = Cell->GetLevelStreaming())
			{
				if (LevelStreaming->GetLoadedLevel())
				{
					return StaticFindObject(UObject::StaticClass(), LevelStreaming->GetLoadedLevel(), *OutSubObjectString);
				}
			}
		}
	}
#if 1 // workaround : try to resolve the actor from the persitant level
	else if (OutSubObjectString != SrcPath)
	{
		if (const UWorld* OuterWorld = WorldPartition->GetTypedOuter<UWorld>())
		{
			if (ULevel* PersistentLevel = OuterWorld->PersistentLevel)
			{
				return StaticFindObject(UObject::StaticClass(), PersistentLevel, *OutSubObjectString);
			}
		}
	}
#endif
 
	return nullptr;
}

個別の質問についてです。

(2) 直接配置されたアクターはEditorPaths機能を使う必要がないため問題無く解決されます。

(3&4) データレイヤーを割り当てて、初期ステートをActivatedにする方法は比較的インパクトが小さいと思います。

DynamicBindingでアクタータグなどでアクターを検索して割り当てる方法も検討できると思いますが、少々運用コストが上がります。

[Attachment Removed]

お世話になっております。

バグとして登録していただいた旨、承知いたしました。

ひとまず今回は「IsSpatiallyLoaded = false」として静的バインドが行える状態にすることでの解決を図りたいと思います。

度重なる質問にも関わらずご回答いただきありがとうございました。

今後ともよろしくお願いいたします。

[Attachment Removed]