ゲームが開発における一定の段階に到達すると、具体的に何がメモリーに読み込まれ、それがなぜ読み込まれるのかを把握する事が重要となります。新しいアセットが作られていくにつれてゲームのサイズが肥大化し、ロード時間が極端に遅くなり、ゲームもメモリーが不足しがちです。ですが UE4 にはこれを想定して、メモリーに何が含まれ、それがなぜ含まれているのかを追跡するための役立つツールがいくつか組み込まれています。エピックにいる私もこれらツールやテクニックを使い、「Fortnite」のメモリー使用とロード時間を最適化してみました。
Memreport を使用した問題把握
私の最初のステップとしてまず行うのが、memreport コマンドの実行です。これはコンソールを開いて「memreport」を実行すると簡易レポートが生成され、「memreport -full」を実行するとより詳細なレポートが生成されます。以上のいずれかを行ったら、次は YourGame/Saved/Profiling/MemReports のフォルダ内にマップ名とタイムスタンプがタグ付けされた .memreport ファイルが生成されています。BaseEngine.ini の [MemReportCommands] セクション(-full を付けた場合は [MemReportFullCommands] セクション) 内の全コマンド実行による全ての出力データが含まれた、普通のテキストファイルとなっています。一応、ゲームの engine.ini ファイルからどのコマンドが実行されるかをゲーム毎に変更できますが、まずはデフォルトのコマンドで始めてみることをお勧めします。
memreport ファイルの冒頭には、プラットフォーム特有のメモリー使用と記録されたすべてのメモリー数値を含めた、全体的なメモリーの使用が一覧表示されています。「STAT_PixelShaderMemory」などのメモリー数値の名前をソース内から検索することで、具体的に何がそのメモリーを使用しているのかを確認する事ができます。その次のセクションは、UObject クラスのすべてのオブジェクトを一覧表示し、それらがどれほどのメモリーを使用しているかを示す、「obj list -alphasort」コマンドの出力データです。その後にはレンダリング用メモリー、読み込まれたレベル ストリーミングの状態、スポーンされたアクターなどのセクションが続きます。「-full」コマンドを実行した場合は、静的メッシュやテクスチャといった個別のアセット用のセクションがあるので、高コストな個別のアセット探しに役立ちます。
尚、「obj list」の出力項目を少しご説明いたします。Fortnite 用に生成されたレポートから抜粋した行はこちら:
Class |
Count |
NumKBytes |
MaxKBytes |
ResKBytes |
ExclusiveResKBytes |
---|---|---|---|---|---|
AIPerceptionSystem |
1 |
0K |
0K |
0K |
0K |
AnimSequence |
19 |
1018K |
986K |
986K |
986K |
Material |
263 |
767K |
800K |
888892K |
1080K |
「memreport」コマンドを利用することで、メモリーが一体何に使われているのか手早く確認する事が可能です。また前後の memreport を取っておいて、テキスト比較ツールを使って比較するのも大変役に立ちます。項目は順番付けられているので、比較は見やすい形で行う事ができます。それでは Fortnite を例に、エンジンのツールを使って特定のアセットがなぜ読み込まれているのかを調べる方法をお見せいたします。まずは「memreport -full」の出力データを見て、大型アセットの中におかしな物が混じってないかを確認します。私の場合、memreport 内にこの行は次のようになっていました。一つ目の列にはクラスの名前、次にクラスのインスタンス数が示されています。メモリーの列において重要なのは、各クラスの全インスタンスの累積メモリー量を示す最初と最後の列のみです。「NumKBytes」はメモリー内における UObject のボディによって消費されるメモリー量を示し、「ExclusiveResKBytes」はその UObject に属した非 UObject リソース (サウンド バッファなど) が使用するメモリー量を示しています。オブジェクトのリソース サイズを決定する関数が「UObject::GetResourceSize」です。このケースでは共有リソースが含まれるため、ResKBytes はあまり役に立ちません。そこで Material の場合、マテリアル毎に同じ共有テクスチャを 1 度含むことで、わざと「ResKBytes」の合計値を水増ししています。特定のクラスによって使用されているメモリー量を見るには、1つ目と4つ目の列の値を足します。
StaticMesh .../S_Hex_Urban_Standard_03.S_Hex_Urban_Standard_03 1K 1K 14306K 3220K
ここでは、1 つの静的メッシュがメモリーを 3 MB 消費しています。ただ計測時のゲームプレイ中には必要としていないメッシュだったため、読み込まれているのは予定外でした。つまり何かがこのアセットを参照し、不要な時でも読み込ませていることを意味しています。よって何故このアセットが参照されているのか、これを調べます。
不要な参照を追跡するにあたって、2 つの役立つテクニックが使えます。1 つ目はエディタを起動して、コンテンツ ブラウザーから問題のアセットを見つけ出します。次にそのアセットを右クリックして Reference Viewer (参照ビューア) を選択します。これによって次のような表示になるかと思います:
参照ビューアを使えば、すべての参照を素早く確認する事ができ、何が特定のオブジェクトを参照しているのかを見ることができます。この場合、ブループリントが当該のアセットを参照しており、そのブループリントをダブルクリックすれば、今度はブループリントを参照している物を確認できます。この機能を利用して、私は高コストなメッシュの読み込みに寄与している候補をいくつか手っ取り早くリストアップできました。
利用する 2 つ目のツールが「obj refs」コマンドです。プロフィールを取得したゲームのインスタンス内から、コンソール上で「obj refs name= S_Hex_Urban_Standard_03 shortest」を実行します。数秒後、対象オブジェクトへの GC のルートからなる参照チェーンを含めたログ ダンプが出力されます。この例から出力されたダンプのサンプルはこちら:
(root) World /Game/Maps/Zones/Zone_Temperate_Urban.TheWorld->CurrentLevel
Level .../Zone_Temperate_Urban.TheWorld:PersistentLevel->ULevel::AddReferencedObjects()
FortWorldManager .../PersistentLevel.FortWorldManager_0->CurrentWorldRecord
FortWorldRecord .../PersistentLevel.FortWorldManager_0.FortWorldRecord_1->ZoneTheme
(standalone) FortZoneTheme .../ZoneTheme_Urban.ZoneTheme_Urban->HexTileClass
BlueprintGeneratedClass .../HexTile_Urban01.HexTile_Urban01_C->UClass::AddReferencedObjects()
HexTile_Urban01_C .../HexTile_Urban01.Default__HexTile_Urban01_C->Hex Deco Meshes
(target) StaticMesh .../S_Hex_Urban_Standard_03.S_Hex_Urban_Standard_0
以上の出力データは、対象オブジェクトへ決して GC されることのないルート オブジェクトからの参照チェーンとなります。尚、パスはあまり長くならないように私が省略させています。出力内容の各行は任意のノート (例: (root) など) で始まり、次にオブジェクトのクラス、オブジェクトのフルパス、そして最後にそのオブジェクトのどの要素が参照しているのかを示しています。今回の例では、一部の参照が「AddReferencedObjects()」のカスタム関数に含まれている他、編集可能な UObject プロパティにも含まれています。
出力内容をすべて確認してみたところ、ブループリントが「FortZoneTheme」によって参照され、それがさらに Fortnite のセーブゲーム システムによって参照されているようでした。このケースにおいては、FortZoneTheme の HexTileClass プロパティを「TSubclassOf<>」から「TAssetSubclassOf<>」に変更することで、私が参照されるブループリントを読み込ませるように指定しない限り、読み込まれないようにしようと思います。
もちろん、この解決方法は複数ある中のひとつでしかありません。ですが私個人の経験から言いますと、メモリー使用量を軽減してロード時間を改善する最も簡単で効果的な方法が、ゲーム内での不要なアセットの読み込みを防ぐことです。メモリーの最適化についてまだまだ語れることはありますので、ご質問などがありましたら、こちらまでお越しください。