パーシスタントレベルの自動生成を行う手法につきまして

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

大量にパーシスタントレベルを作成する必要があるため

任意のテンプレートからパーシスタントレベルを自動生成するような処理を検討しています。

その過程でPlayerStartなどを含んだサブレベルを追加すると

保存時にコンポーネント登録エラーが発生しエディタがフリーズしてしまいました。

そこで確認させていただきたいのですが

「パーシスタントレベルの自動生成を行う」

といった対応は現実的でしょうか。

再現プロジェクトを共有いたしますので

こちらの実装に不備があればご指摘いただけますと幸いです。

  1. /Game/EUW_Sample.uaaset を実行
  2. 「Execute」ボタンを選択
  3. 生成された /Game/duplicate_P を保存
  4. コンポーネント登録エラーの無限ループによりフリーズが発生

以上、よろしくお願いいたします。

[Attachment Removed]

再現手順

  1. /Game/EUW_Sample.uaaset を実行
  2. 「Execute」ボタンを選択
  3. 生成された /Game/duplicate_P を保存
  4. コンポーネント登録エラーの無限ループによりフリーズが発生

[Attachment Removed]

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

プロジェクトを共有いただきありがとうございます。

duplicate_P.umapを確認したところ、LevelStreamingAlwaysLoadedのエントリが元のSource_Sをそのまま参照しているように見えました。Source_Sが既に読み込まれている状態でduplicate_PのSublevelとして追加されると、同じUWorldが2つのパーシスタントレベルから参照されることになり、保存時の登録ループにつながっているのではないかと思います。OpenWorldテンプレートをお使いのため、World Partition側の制約も関係している可能性があります。

念のため確認させてください。

EUWを実行する際、エディタに開かれているレベルはどちらでしょうか?

Source_Sもあわせて複製されるご予定でしょうか、それとも元のものを使い回す想定でしょうか?

複製されるご予定であれば、先にSource_Sを複製してからduplicate_PのULevelStreamingエントリを書き換える方法か、あるいはULevelEditorSubsystem::NewLevelで空のレベルを作り、UEditorLevelUtils::AddLevelsToWorldでSublevelを追加する方法が考えられます。

Persistant Levelの自動生成自体は十分実現できる方向だと思いますが、Sublevelの扱いを明示的にコントロールする必要がありそうです。

お手数ですが、よろしくお願いします。​

[Attachment Removed]

ご確認ありがとうございます。

ご認識の通り、UEditorLevelUtils::AddLevelToWorldはエディタで現在開かれているワールドを前提とした処理を行っているようでして、内部的にストリーミング状態のリフレッシュや、レベルの可視性の設定(これに伴いアクター上のコンポーネントが再登録されます)が走ります。複製されたばかりのワールドに対してこれを呼び出すと、ご報告いただいているコンポーネント登録ループにつながるのではないかと思われます。回避の方向性としては妥当だと感じます。

ひとつだけ気になりましたのが、GetStreamingLevels()の戻り値に対するconst_castの部分です。UWorldには公開メソッドとしてAddStreamingLevel(ULevelStreaming*)とRemoveStreamingLevel(ULevelStreaming*)が用意されておりまして、同じ配列操作に加えて、StreamingLevelsToConsiderの同期やULevelStreamingのランタイム状態の更新といったメモリ上の処理も合わせて行ってくれます。「アクティブなエディタワールド前提」の副作用は伴わないように見えますので、newWorldに対して呼び出しても問題ないかと思います。ただ、こちらで完全には再現できておりませんので、念のためお手元で動作をご確認いただけますと幸いです。

念のため補足させていただきますと、現状の実装でも保存後のアセット自体は正しく読み込まれるはずです。上記のメモリ上のフィールドはディスクには保存されないためです。気になりますのは、変更から保存までの間のnewWorldのメモリ上の状態でして、もしこの間にGCやレベルブラウザのリフレッシュなどがワールドに触れますと、不整合が予期しない形で表面化する可能性があります。公開APIを使えば、この点は自然に回避できるかと思います。あわせて、変更後にnewWorld->MarkPackageDirty()を呼んでいただくのも、エンジン側の処理に合わせる意味でよろしいかと思います。

差し支えなければ、一点お伺いさせてください。Source_PはWorld Partition有効なマップでしょうか?通常レベルであれば上記の方針でそのまま問題ないかと思いますが、WPマップにULevelStreamingAlwaysLoadedのサブレベルを追加されている場合は、ご意図を確認させていただいた上で改めてご返信できればと思います。

もしお手元で異なる挙動が見られましたら、ご共有いただけますと幸いです。

お手数ですが、よろしくお願いします。

[Attachment Removed]

ご確認ありがとうございます。

説明が不足していたようで申し訳ありません。

> EUWを実行する際、エディタに開かれているレベルはどちらでしょうか?

エディタ起動後に開かれている “Untitled” での実行になります。

USampleWidget::OnExecute() を見ていただくと分かるのですが

“/Game/duplicate_P” はEUW実行時に古いものがあれば削除しており

実際に行いたいことは下記です。

  1. Source_P を複製して duplicate_P を生成
  2. duplicate_P に対し Source_S をサブレベルとして追加
  3. しかし duplicate_P を保存すると無限ループが生じる

補足として無限ループに陥った際にエディタを終了させると

duplicate_P に Source_S は追加された状態で存在しており

開くことも出来るようでした。

検討内容としては生成対象のパーシスタントレベルを開かずに

サブレベルの削除、追加を行えるかということになります。

こちらでも調査を進めた結果「UEditorLevelUtils::~」は

レベルが開かれた状態でのみ正しく動作するようでしたので

それぞれ追加/削除を下記のようなコードに置き換えると

こちらが期待した挙動になったのですが

懸念やリスクなどが無いかご意見いただきたく思います。

■サブレベルの追加

TArray<ULevelStreaming*>& streamingLevels = 
	const_cast<TArray<ULevelStreaming*>&>(newWorld->GetStreamingLevels());
 
ULevelStreaming* levelStreaming = NewObject<ULevelStreaming>(newWorld, ULevelStreamingAlwaysLoaded::StaticClass(), NAME_None, RF_NoFlags, NULL);
if (!levelStreaming)
{
	continue;
}
levelStreaming->SetWorldAssetByPackageName(【追加サブレベル】.ToSoftObjectPath().GetLongPackageFName());
levelStreaming->LevelColor = FLinearColor::MakeRandomColor();
//
streamingLevels.Emplace(levelStreaming);

■サブレベルの削除

TArray<ULevelStreaming*>& streamingLevels = 
	const_cast<TArray<ULevelStreaming*>&>(newWorld->GetStreamingLevels());
 
for (auto it = streamingLevels.CreateIterator(); it; ++it)
{
	if (!(*it))
	{
		continue;
	}
	if (!【削除対象か?】)
	{
		continue;
	}
	//
	it.RemoveCurrent();
}

以上、よろしくお願いいたします。

[Attachment Removed]

詳細な説明大変助かります。

> ひとつだけ気になりましたのが、GetStreamingLevels()の戻り値に対するconst_castの部分です。

AddStreamingLevelRemoveStreamingLevel の存在は把握していましたが

内部挙動に理解が追いついておらず副作用も不明だったため

まずはコンテナを直接操作することで検証を行った形です。

差し替えて確認してみたところ特に副作用もなく期待した挙動を得られました。

■サブレベルの削除

TArray<ULevelStreaming*> removeLevelStreamings;
for (auto&& levelStreaming : newWorld->GetStreamingLevels())
{
	if (!levelStreaming)
	{
		continue;
	}
	if (【削除対象?】)
	{
		removeLevelStreamings.Add(levelStreaming);
	}
}
const int32 removeNum = newWorld->RemoveStreamingLevels(removeLevelStreamings);
if (removeNum != removeLevelStreamings.Num())
{
	//	削除処理が不完全
}
newWorld->MarkPackageDirty();

■サブレベルの追加

TArray<ULevelStreaming*> addLevelStreamings;
for(auto&& worldAsset : WorldAssets)
{
	//	重複チェック
	if (newWorld->GetStreamingLevels().ContainsByPredicate(
		[&worldAsset](const ULevelStreaming* In)->bool
		{
			return In && In->GetWorldAssetPackageFName().IsEqual(worldAsset.GetLongPackageFName());
		}))
	{
		continue;
	}
	ULevelStreaming* levelStreaming = NewObject<ULevelStreaming>(newWorld, ULevelStreamingAlwaysLoaded::StaticClass(), NAME_None, RF_NoFlags, NULL);
	if (!levelStreaming)
	{
		continue;
	}
	levelStreaming->SetWorldAssetByPackageName(worldAsset.GetLongPackageFName());
	levelStreaming->LevelColor = FLinearColor::MakeRandomColor();
	//
	addLevelStreamings.Add(levelStreaming);
}
newWorld->AddStreamingLevels(addLevelStreamings);
newWorld->MarkPackageDirty();

> Source_PはWorld Partition有効なマップでしょうか?

Souce_P は WorldPartation を利用しておりません。

現在追加サブレベルにはPlayerStartや外観などの常駐物のみが想定されているため

ULevelStreamingAlwaysLoaded を指定しております。

こちらは今後必要に応じて適切な実装を行う予定です。

​​

今回ご教示いただいた内容を元に実装を進めてみたいと思います。

ご対応ありがとうございました。

以上、よろしくお願いいたします。

[Attachment Removed]