Help us understand the problem. What is going on with this article?

[UE4] Editorのイテレーションを早くするTips (Editor起動/LevelOpen/PIE)

1.はじめに

 エディターでのイテレーションを早くすることは開発においてとても重要です。ゲームエンジンを使用するような開発においては、デザイナ、アーティスト、エンジニアと、開発に携わる殆どの人がエディタ上で作業を行い、多くの時間をエディタのオペレーションに充てます。
 例えば、PIEでの起動が遅くエディタでの反復操作が遅い場合、これは作業効率が良いとは言えません。開発の規模が大きい場合は、更に多くの時間をエディタの操作に充てることになるため、開発工数の殆どをコンテンツとは別の時間に割くことになります。せっかくゲームエンジンを使うことで効率的に開発が出来るようになったとしても、結局このような見えない時間を支払う必要があるのであれば、コンテンツの品質は一向に上がらないと言えます。エディタのイテレーションを早くすることで、見えない時間に払うコストを減らし、コンテンツの品質向上に注力するということを意識しておく必要があります。
本記事はUE4.20.0で検証しています。

2. Editor起動

この章では、UE4Editorの起動を早くする方法について記載します。
2018-09-23_10h17_29.png
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起動処理
// 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オープンを早くする方法について記載します。
2018-09-23_10h14_44.png
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開始」と殆ど同じであり、ここで記載した流れから大きくは変わらないためある程度の参考になるかと思います。

LevelOpen処理
// 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とは異なり、パッケージ版で動作した時にロードされていない場合はリストアップされません。逆に表示されていればロード済みということになります。
2018-11-04_00h15_39.png

●Obj listコマンド

以前の記事「Objコマンドによるオブジェクト解析」でも説明していますが、オブジェクトのカウント数やリソースのサイズを出力できます。こちらは上記のパッケージ単位とは異なり、Object単位での結果を出力します。例えば、Obj Listコマンドではオブジェクト数やリソースサイズを確認して、Objectやリソースのボトルネックを探すことができたり、Obj RefsコマンドではObjectが参照されている箇所を探すことができます。BlueprintやBrowserから確認できるアセットはReferenceViewerで確認することができますが、NativeObjectはこのコマンドで確認することができます。
2018-11-04_00h56_53.png

●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起動処理
// 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実行時(レベル展開前イメージ)
BeforBuilding.png
例えば大量のビル群を構築する場合、仮配置(ホワイトボクシング)として灰色のStaticMeshを配置します。レベル展開前に状態では仮のObjectが存在している状態です。

・PIE実行時(レベル展開後イメージ)
AfterBuilding.png
そしてこちらはレベル展開後のイメージです。図は単純な黒色のStaticMeshを配置していますが、実際にはビルが展開されるイメージです。そしれこれをWorld単位で構築できるようにします。ゲームを繰り返しプレイする際にはビルを全て構築する必要がありません。そこでPIEの場合はホワイトボクシングで作成した適当なStaticMeshで代用し、必要に応じてビルを構築できるようにします。

・展開の様子
video_02.gif
このようにPIEのようなデバッグでは適当なStaticMeshをビルの代用として利用し、ビルを構築しないようにします。そして必要な場合はActorと同じように配置したWorldを展開します。

・展開の様子 (allow stats)
video_01.gif
Stat Levelsを実行している状態での展開の様子です。SubLevelではない200以上のWorldが展開されていることが分かります。

・Worldを展開するActor
PIEで使用するStaticMeshと展開するWorldを用意します。そしてLoadLevelInstanceを利用して展開/破棄を行います。LoadLevelInstanceについては、「UE4でSubLevelを動的生成・削除する」を参照下さい。以前の記事ではEngineに直接手を入れていますが、こちらはActorとして作成する方法になります。また、4.20までにアクセス制限の変更があった箇所を一部修正しています。

MyBuildActor.cpp
#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;
        }
    }
}

MyBuildActor.h
#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の設定
Level.png
BP.png
上記で作成したActorをWorldに配置して、展開するWorldを設定しておきます。そして必要に応じてレベルをロード/アンロードを実行します。以上のことから、SubLevelとして登録されないWorldを展開することができるようになりイテレーションを早くすることに役に立ちます。これはロードやObject数の定常コストを抑えるだけでなく、World単位で作業をできるという点や、位置の修正が容易にできるという点でのメリットもあります。

donbutsu17
主にUnreal Engineに関する記事を書きます。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした