#1.はじめに
エディターでのイテレーションを早くすることは開発においてとても重要です。ゲームエンジンを使用する開発では、デザイナ、アーティスト、エンジニアと開発に携わる殆どの人がエディタ上で作業を行い、多くの時間をエディタのオペレーションに充てます。
例えば、PIEでの起動が遅くエディタでの反復操作が遅い場合、作業効率が良いとは言えません。開発規模が大きい場合は更に多くの時間をエディタの操作に充てることになるため、工数の殆どをコンテンツとは別の時間に割くことになります。せっかくゲームエンジンを使うことで効率が上がっても、このような見えない時間への消費が多ければコンテンツの品質は一向に上がらないと言えます。
エディタのイテレーションを早くすることで見えない時間に払うコストを減らし、コンテンツの品質向上に注力するということを意識しておく必要があります。
本記事はUE4.20.0で検証しています。
#2. Editor起動
この章では、UE4Editorの起動を早くする方法について記載します。
Editorを起動すると上記のスプラッシュが表示されますが、このプロセスで実行される処理と改善方法を以下に記載します。UE4Editorを起動すると上記のようなスプラッシュにはLoadingの"%"を表示し100%になると起動完了となり操作が可能となります。Editorを頻繁に再起動するようなケースにおいてはこのプロセスを短くすることが重要となってきます。
##2.1. Editor起動を早くするには?
① 不要なPluginをOFFにする
② Editor起動時のマップを軽量なものにする
先に結論を述べますと上記の通りとなります。基本的にEngineでEditor起動に必要な処理を実行するため、あまりEngineの起動処理自体を高速化することはできません。以降に起動時の処理とその内容を記載します。
##2.2. Editor起動処理とその内容
####① Initializing
Pluginのマウント
Editorを起動時にエンジンの起動前にPluginのマウント処理を行います。
不要なPluginをOFFにすることで微小ですが起動時間を短縮できます。
####② Loading... 0-79%
事前初期化処理 FEngineLoop::PreInit()
Editor起動の事前初期化処理が行われます。"Loading..."表記の0-80%は以下の処理を実行します。
基本的にEngineの初期化と起動に必要なオブジェクトのロードが行われるため、大きな改善点はありません。
Projectで定義するオブジェクトが増加するほど処理時間は増加しますが、大半がEngineの起動処理のため数秒程度です。
# | Progress | Work |
---|---|---|
1 | 00-09 | TargetPlatformModuleのロード ターゲットプラットフォームのモジュールをロード |
2 | 10-39 | GlobalShaderMapのコンパイル Enigneコンテンツに含まれるシェーダー(.usf)をロード |
3 | 40-44 | AssetRegistryのロード コンパイル済みのすべてのプロパティ(UStruct等)をロード |
4 | 45-49 | DisregardForGC対象の常駐オブジェクトのロード |
5 | 50-59 | StartupCoreModuleのロード Engine定義のオブジェクトの生成 |
6 | 60-69 | LoadingScreenModuleのロード |
7 | 70-74 | StartupModuleのロード プロジェクト定義のオブジェクトの生成 |
8 | 75-79 | HighResScreenshotMaterialの初期化 |
####③ Loading... 80-100%
初期化処理 FEngineLoop::Init()
Editor起動の事前初期化処理が行われます。"Loading..."表記の80-100%は以下の処理を実行します。
プロジェクトではEditor起動時のマップを指定することで起動処理の時間を短縮することができます。
# | Progress | Work |
---|---|---|
9 | 80-89 | EngineLoopの初期化 コンフィグファイルのロード等 |
10 | 90-100 | その他の初期化 起動マップのロード |
##2.3. Editor起動処理の詳細
以下にEditor起動時の処理について、主要な箇所のみを抜粋して流れを記載しています。以下はWindows/DevelopmentEditorの起動時のケースについてですが、他のケースにおいても大まかな流れは変わりません。また、Editor起動ではないゲーム起動時には処理が別になりますが、ここで記載した流れから大きくは変わらないため、あるNoneEditorでの起動ケースにおいてもある程度の参考になるかと思います。
// Editor起動処理の流れ(抜粋)
WinMain
GuardedMain
FEngineLoop::PreInit
FEngineLoop::LoadCoreModules // CoreModuleのロード
FEngineLoop::LoadPreInitModules // PreInitModuleのロード
FEngineLoop::AppInit
UDeviceProfileManager::InitializeCVarsForActiveDeviceProfile // DeviceProfile Cvarのロード
InitEngineTextLocalization // Engineローカライズ処理
ApplyDefaultCultureSettings
FInternationalization::SetCurrentLanguageAndLocale // Language/Locale設定
FTextLocalizationManager::LoadLocalizationResourcesForCulture // ローカライズリソースロード
FTextLocalizationManager::LoadLocalizationResourcesForPrioritizedCultures
FTextLocalizationManager::UpdateFromLocalizations
FWindowsPlatformSplash::Show // Splash表示
FInternationalization::LoadAllCultureData
FICUInternationalization::LoadAllCultureData
FShaderCompilingManager::FShaderCompilingManager // (1) ShaderCompileWorker起動
CompileGlobalShaderMap // (2) GlobalShaderMapのコンパイル
CreateMoviePlayer // 起動用MoviePlayer生成
InitGameTextLocalization // Gameローカライズ処理
FModuleManager::LoadModule // (3) AssetRegistryのロード
ProcessNewlyLoadedUObjects // (4) UObject/Enum/Struct/Propertyの初期化と登録
LoadStartupCoreModules // (5) StartupCoreModuleのロード
FProjectManager::LoadModulesForProject // (6) LoadingScreenModuleのロード
FPluginManager::LoadModulesForEnabledPlugins
FPlatformMisc::PlatformHandleSplashScreen // SplashScreen表示
FEngineLoop::LoadStartupModules // (7) StartupModuleのロード
FUObjectArray::CloseDisregardForGC // DisregardForGCのClose
FHighResScreenshotConfig::Init // (8) HighResScreenshotMaterialの初期化
EditorInit
FEngineLoop::Init // (9) EngineLoopの初期化
UUnrealEdEngine::Init
UEditorEngine::Init
UEditorEngine::InitEditor
UEngine::Init
UEngine::InitializeHMDDevice // Device初期化
UEngine::InitializeEyeTrackingDevice
UEditorEngine::InitializeObjectReferences
UEngine::InitializeAudioDeviceManager
UGameUserSettings::LoadSettings
UGameUserSettings::ApplySettings
LoadPackage // EditorResourceロード
FUnrealEdMisc::OnInit // (10) その他の初期化
FEditorFileUtils::LoadDefaultMapAtStartup
FEditorFileUtils::LoadMap // DefaultMapロード (詳細は3.章参照)
UEngine::Start
FWindowsPlatformSplash::Hide // Splash非表示
##2.4. Editor起動時間比較
.uprojectをダブルクリックした時の起動についてオプション等で調整可能な項目を比較した結果を示します。
####●「Third Person Template」のEditor起動Plugin設定差分による起動時間比較
Editor起動Level | Work Time(s) |
---|---|
Pluginデフォルト | 7.43 |
不要Pluginを外す | 6.56 |
####●「Infiltrator Demo」のEditor起動Level差分による起動時間比較
Editor起動Level | Work Time(s) |
---|---|
VIS_ArtDemo_P | 28.11 |
Blank | 7.32 |
上記比較結果が示すようにEditorの起動レベルによって起動までの時間も異なります。 | |
4.18では上記のおよそ2倍(VIS_ArtDemo_Pで約56s程度)を要していましたが、4.19/4.20のロード改善で更に早く起動できるようになっています。Editorの起動においてユーザーが関与可能で最も効果的な設定はここになります。 |
##2.5. その他Editor起動を早くするためにできること
その他、用途に応じて処理をスキップすることで起動時間を短縮することができます。
・HMDモジュールのロード
HMDを使用しない場合は起動引数に"-nohmd"または"-emulatestereo"を付与することでモジュールのロードをスキップします。
・スプラッシュスクリーンの更新
起動時にスプラッシュスクリーンを表示しない場合は起動時引数に"-NOSPLASH"を付与することでウィンドウの生成、及びProgressの更新処理をスキップします。
・起動時プロジェクトのコンパイル
Editor起動時にProjectのコンパイルを実行しない場合は起動時引数に"-SKIPCOMPILE"を付与することでスキップします。VisualStudioから起動する場合、デフォルトでは付与された状態となっています。
#3. Levelオープン
この章では、Levelオープンを早くする方法について記載します。
ContentBrowserからLevelを開くと上記のスプラッシュが表示されますが、このプロセスで実行される処理と改善方法を記載します。スプラッシュにはLoadingの"%"を表示しますが、以下の表から何に時間が掛かっているか目途を付けることができます。
##3.1. Levelオープン時間を早くするには?
① Level上の不要なObjectを減らす
② Level上の不要なObjectのロードを減らす
③ 不要な初期化処理を減らす
先に結論を述べますと上記の通りとなります。Levelのオープン処理は主にLevel、Objectのロード、登録処理が行われます。これらの詳細は「3.3.Levelオープン処理の詳細」にあります。しかし、基本的にSublevelやLevelに配置されるActorなどは、コンテンツで必要な都合により配置されているObjectであるためパッケージ後のゲームプレイにおいては必要最低限のものしか無いはずです。そのため、各コンテンツの中でこれらの処理を用途に応じて適切に最適化する必要があります。
①については、「Unreal Engine 4を使って地球を衛る方法」の中でも述べられていますが、Levelを展開する過程において、ActorやComponentに対しての登録/追加/削除の操作が行われることによります。②については、「Nintendo Switch『OCTOPATH TRAVELER』はこうして作られた」で述べられているように、ActorやComponentを展開する過程で参照するObjectのロードが行われるため、参照関係を最低限に維持する必要があります。
参照関係については「アセットの参照」を意識する必要がありますが、特に注意すべき点としてGameInstanceやGameModeなどで多くの参照を持つことです。GameInstanceは特にゲーム起動時に作成されるものであり(Editor起動の場合は都度PIEInstanceを作成しますが)、ゲーム共通で使用するものを何でも入れてしまうと、ゲームの初回起動時間に影響しますのでご注意を。例えば、「DataTableで言語によって切り替えるパターンを定義してGameInstanceで保持しておく」ようなケースにおいては「Tableに登録されたAssetが初回起動時に全てロードされて初回起動時間が長くなる」ことが想定されるので、必要なアセットを都度読む/不要なアセットが読まれていないかに注意する必要があります(個人的にはDataAssetで管理する方が好きですが用途に応じて使い分けてください)。
##3.2. Levelオープン処理とその内容
Progress | Work |
---|---|
00-75 | Levelファイルの検索 |
75-100 | LevelロードとMapチェック |
選択したファイルを検索する処理で75%まで進行するため、基本的にはLevelのロードやMapチェックを行う75%以降で大抵は進行が停滞します。よって、Levelのロードを如何に早く完了させるかがLevelオープンを完了させるために必要な事項です。
##3.3. Levelオープン処理の詳細
以下にEditorでのLevelオープン処理について、主要な箇所のみを抜粋して流れを記載しています。この処理自体は「Editor起動時の初期マップロード」及び「PIE開始」と殆ど同じであり、ここで記載した流れから大きくは変わらないためある程度の参考になるかと思います。
// ContentBrowserからLevelアセットをダブルクリックで選択した際の処理の流れ(抜粋)
SContentBrowser::OnAssetsActivated
FAssetTypeActions_Base::AssetsActivated
FAssetEditorManager::OpenEditorForAsset
FEditorFileUtils::LoadMap // マップのロード開始
UUnrealEdEngine::Exec
UEditorEngine::Exec
UEditorEngine::HandleMapCommand
UEditorEngine::Map_Load
UEditorEngine::EditorDestroyWorld // 先に現在のWorldを破棄
UWorld::ClearWorldComponents
ULevel::ClearLevelComponents
AActor::UnregisterAllComponents // 現在のWorldの全てのComponentを登録解除
UWorld::DestroyWorld
UWorld::CleanupWorld
UWorld::ClearWorldComponents
ULevel::ClearLevelComponents
AActor::UnregisterAllComponents // 遷移先のWorldのComponentのリセット
UEngine::WorldDestroyed
UWorld::UpdateWorldComponents // 新しいWorldのComponent更新
ULevel::UpdateLevelComponents
ULevel::IncrementalUpdateComponents
AActor::PreRegisterAllComponents
AActor::RerunConstructionScripts
AActor::IncrementalRegisterComponents
AActor::RegisterAllActorTickFunctions
UActorComponent::RegisterComponentWithWorld
UWorld::UpdateCullDistanceVolumes
UWorld::FlushLevelStreaming // 現在のWorld情報の更新
UWorld::UpdateLevelStreaming
ULevelStreaming::UpdateStreamingState
UWorld::AddToWorld // 新しいWorldのSubLevelを追加
ULevel::IncrementalUpdateComponents
SortActorsHierarchy
AActor::PreRegisterAllComponents
AActor::RerunConstructionScripts
AActor::IncrementalRegisterComponents
AActor::RegisterAllActorTickFunctions
UActorComponent::RegisterComponentWithWorld
UEngine::WorldAdded
CollectGarbage // クリーンアップ
UUnrealEdEngine::Exec
UEditorEngine::Exec
UEditorEngine::HandleMapCommand
UEditorEngine::Map_Check
ContentBrowserUtils::OpenEditorForAsset
##3.4. Levelオープン処理の解析
####●LoadTimes.DumpReportコマンド
パッケージ単位でロードに掛かった時間を出力することができ、ここからどのオブジェクト、リソースのロードに時間が掛かっているのかを調べることができます。コンソール向けの場合はCookされたコンテンツやハードウェアの性能によって時間の差はあれどその傾向は殆ど一貫性を持つものになるはずなので、Editor上で確認して傾向を把握し早めに対処することもできます(思わぬ箇所で4kTextureが設定されているなど)。注意点としては、ロード済みのパッケージをリストアップするので事前にロードされるEditorとは異なり、パッケージ版で動作した時にロードされていない場合はリストアップされません。逆に表示されていればロード済みということになります。
####●Obj listコマンド
以前の記事「Objコマンドによるオブジェクト解析」でも説明していますが、オブジェクトのカウント数やリソースのサイズを出力できます。こちらは上記のパッケージ単位とは異なり、Object単位での結果を出力します。例えば、Obj Listコマンドではオブジェクト数やリソースサイズを確認して、Objectやリソースのボトルネックを探すことができたり、Obj RefsコマンドではObjectが参照されている箇所を探すことができます。BlueprintやBrowserから確認できるアセットはReferenceViewerで確認することができますが、NativeObjectはこのコマンドで確認することができます。
####●ProjectSettings GCの設定
Levelファイルを開いた時など、遷移前のレベルのObjectを破棄するためにGCを行います。GCの設定を予め行うことでEditor操作でのGCにかかる時間を短縮します。パフォーマンスのテストはどうしても開発の後半に実施しがちであるため、出来るだけ初期から設定を行っておくべきです。これはEditorで動作する場合もパッケージで動作する場合にも言えますが、レベルのオープン、Visibleの変化、PIE終了時などGCが頻繁に実行されるためです。
#4. PIE起動
この章ではPIEのイテレーションを早くする方法について記載します。
##4.1. PIE起動を早くするには?
① Level上の不要なObjectを減らす
② Level上の不要なObjectのロードを減らす
③ 不要な初期化処理を減らす
基本的にはLevelオープンと同じ考え方です。PIEは現在開いているLevelを複製してPIE用の別ワールドとして展開します。Levelオープンと違い、全てのActiveなSubLevelを展開した後に存在するActorのBeginPlayが実行されます。また、EditorではGameInstanceをPIE用として都度生成し、ゲームに必要な情報を展開します。
##4.2. PIE起動処理の詳細
以下にPIEを実行した際の流れを示します。
// PIE起動した際の処理の流れ(抜粋)
UEditorEngine::PlayInEditor
UEditorEngine::CreatePIEGameInstance
UGameInstance::InitializeForPlayInEditor
UEditorEngine::CreatePIEWorldByDuplication
UEditorEngine::PostCreatePIEWorld
UWorld::InitWorld
UGameInstance::StartPlayInEditorGameInstance
UWorld::FlushLevelStreaming // AlwaysLoadedのSubLevelを起動
UWorld::UpdateLevelStreaming
ULevelStreaming::UpdateStreamingState
UWorld::AddToWorld // SubLevel追加
ULevel::IncrementalUpdateComponents
AActor::PreRegisterAllComponents
AActor::RerunConstructionScripts
AActor::IncrementalRegisterComponents
AActor::RegisterAllActorTickFunctions
UActorComponent::RegisterComponentWithWorld
UWorld::InitializeActorsForPlay
ULevel::RouteActorInitialize
AActor::PreInitializeComponents // World上の全Component登録
AActor::InitializeComponents
UActorComponent::Activate
UActorComponent::InitializeComponent
AActor::PostInitializeComponents
AActor::UpdateOverlaps
ULocalPlayer::SpawnPlayActor
UWorld::SpawnPlayActor // Player生成
AGameModeBase::Login
AGameModeBase::SpawnPlayerController
AGameModeBase::SpawnPlayerController
AGameModeBase::SpawnPlayerControllerCommon
UGameplayStatics::FinishSpawningActor
AActor::FinishSpawning
AActor::ExecuteConstruction
AActor::ProcessUserConstructionScript
AActor::UserConstructionScript // BP Construction Script
UWorld::BeginPlay
AGameStateBase::HandleBeginPlay
AWorldSettings::NotifyBeginPlay // Worldに存在する全Actorの起動
AActor::DispatchBeginPlay
AActor::BeginPlay // Tick起動とComponent起動
AActor::RegisterAllActorTickFunctions
UActorComponent::RegisterAllComponentTickFunctions
UActorComponent::BeginPlay
AActor::ReceiveBeginPlay
##4.3. PIE起動を早くするための工夫
###4.3.1. WorldのInstance化と展開しないWorld
Editorでイテレーションを回す過程において全てが要素が必要というわけではありません。例えば、レベルデザインのためにテストプレイをしたり、エンジニアが軽微な修正を確認する際には、ゲームに必要な全てのアセットをロードする必要はないため、PIE時は全てのアセットやObjectを展開せずにイテレーションを回す例について紹介します。
例えば「大量のモジュラーアセットを含む建物」がレベル上に必要な場合、Sublevelにそれら全てのアセットを配置することで構築するケースがあります。しかし「StaticMeshを大量に配置すること」は「レベル上にStaticMeshActorやStaticMeshComponentが存在して大量のアセットロードが発生」します。そのような場合には、大量のActor群を1つのWorldに包括し、配置してそのWorldのロード/アンロードを制御することによって、PIE実行時には大量のアセットやオブジェクトを生成しないようにします。これは"ゲームロジックだけをPIEで確認したいようなケース"において特に役に立ちます。
・PIE実行時(レベル展開前イメージ)
例えば大量のビル群を構築する場合、仮配置(ホワイトボクシング)として灰色のStaticMeshを配置します。レベル展開前に状態では仮のObjectが存在している状態です。
・PIE実行時(レベル展開後イメージ)
そしてこちらはレベル展開後のイメージです。図は単純な黒色のStaticMeshを配置していますが、実際にはビルが展開されるイメージです。そしれこれをWorld単位で構築できるようにします。ゲームを繰り返しプレイする際にはビルを全て構築する必要がありません。そこでPIEの場合はホワイトボクシングで作成した適当なStaticMeshで代用し、必要に応じてビルを構築できるようにします。
・展開の様子
このようにPIEのようなデバッグでは適当なStaticMeshをビルの代用として利用し、ビルを構築しないようにします。そして必要な場合はActorと同じように配置したWorldを展開します。
・展開の様子 (allow stats)
Stat Levelsを実行している状態での展開の様子です。SubLevelではない200以上のWorldが展開されていることが分かります。
・Worldを展開するActor
PIEで使用するStaticMeshと展開するWorldを用意します。そしてLoadLevelInstanceを利用して展開/破棄を行います。LoadLevelInstanceについては、「UE4でSubLevelを動的生成・削除する」を参照下さい。以前の記事ではEngineに直接手を入れていますが、こちらはActorとして作成する方法になります。また、4.20までにアクセス制限の変更があった箇所を一部修正しています。
#include "MyBuildActor.h"
// Worldを構築
void AMyBuildActor::TryBuildingWorld(bool& bOutSuccess)
{
bOutSuccess = false;
if (!BuildingWorld.IsNull())
{
UWorld* MyWorld = GetWorld();
if (MyWorld)
{
StreamingKismet = ULevelStreamingKismet::LoadLevelInstanceBySoftObjectPtr(MyWorld, BuildingWorld, GetActorLocation(), GetActorRotation(), bOutSuccess);
}
}
}
// Worldを破棄
void AMyBuildActor::UnLoadBuildingWorld(bool& bOutSuccess)
{
bOutSuccess = false;
if (StreamingKismet != nullptr)
{
UWorld* MyWorld = GetWorld();
if (MyWorld)
{
ULevel* level = StreamingKismet->GetLoadedLevel();
if (level)
{
MyWorld->RemoveFromWorld(level);
}
MyWorld->RemoveStreamingLevel(StreamingKismet);
bOutSuccess = true;
}
}
}
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Engine/LevelStreamingKismet.h"
#include "MyBuildActor.generated.h"
UCLASS()
class TP420_API AMyBuildActor : public AActor
{
GENERATED_BODY()
public:
// World構築
UFUNCTION(BlueprintCallable)
void TryBuildingWorld(bool& bOutSuccess);
// World破棄
UFUNCTION(BlueprintCallable)
void UnLoadBuildingWorld(bool& bOutSuccess);
public:
// 展開するレベル
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "World")
TSoftObjectPtr<UWorld> BuildingWorld;
private:
ULevelStreamingKismet* StreamingKismet;
};
上記はレベルのロードのコールバックなどを省略したものですが、OnLeveXXXなどのイベントが用意されています。
・Actorの配置とWorldの設定
上記で作成したActorをWorldに配置して、展開するWorldを設定しておきます。そして必要に応じてレベルをロード/アンロードを実行します。以上のことから、SubLevelとして登録されないWorldを展開することができるようになりイテレーションを早くすることに役に立ちます。これはロードやObject数の定常コストを抑えるだけでなく、World単位で作業をできるという点や、位置の修正が容易にできるという点でのメリットもあります。