Unityパフォーマンス最適化シリーズ—— ローディングとアセット管理

![0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/0806ca63-3f95-994e-a530-ed1c33766997.png)
4年前、Unityの各主要なモジュールのパフォーマンス最適化知識（初心者向け版）を1つずつ説明しました。近年、エンジン自体、ハードウェアデバイス、製作基準などのアップグレードに伴って、UWAは引き続き規則や方法を更新して、各デベロッパーに提供しつつあります。「アップグレード版」のパフォーマンス最適化マニュアルとして、**「Unityパフォーマンス最適化シリーズ」**はより多くのデベロッパーが利用できるように、シンプルでわかりやすい表現を心がけています。今回、ローディングとアセット管理に関連する知識を共有します。

このような問題はよく聞かれます：なぜゲームはこんなに遅いの？***ゲームのように数秒でシーンを切り替えるのかな？発熱するのはどういう原因だろう？みんなが一生懸命頑張って作ったゲームを実機に数秒で強制終了させたり、発熱すぎてまるで懐炉になったりするのを防ぐために、UWAレポートのローディングとアセット管理モジュールをよく利用しましょう。それでは、ステップバイステップで分析しましょう。

---

##一、ローディングモジュールの注目点

ここではまずローディングに関する注目指標について紹介しましょう。下図はOverviewレポートにおけるローディングモジュール的ブックマークです。左側にはいくつかのパラメータ指標が見えます：
![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/d8a9a27e-4f02-ae2e-15b1-166a27eea7ee.png)

###1、Loading.UpdatePreloading

これはUnityエンジンにおいて最も主要なローディング関数です。この項はシーンを切り替える時や非同期的にアセットをローディングする時に、コストが多くなります。一般的には、ローディングされたアセットは多いほど複雑になります。対応するLoad.UpdatePreloadingの処理時間も長くなります。
![2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/b72fd51c-0811-74d7-6919-c67034c2a20e.png)
当該関数を最適化する前、まずその時間使用ボトルネックを特定することをお勧めします。レポートにあるCPUの呼び出されたスタックを通して、この関数が実行プロセスにおける詳細なスタックトレンドを確認できます。これによって、この関数の処理時間の割り当ては非常に明確になって、ちゃんとしたねらいがあってから最適化します。
![3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/c1aaba65-1a1c-5cd4-3c8d-52d28b036e00.png)
###2、Resources.UnloadUnusedAssets
当該関数はまだ使用されていないアセットをアンインストールすることです。コストは主にシーンにおけるAssetsとObjectの数量で決められます。数量が多いほど、処理時間が多くなる。パフォーマンスを最適化する時、最大処理時間以外、当該関数の呼び出された回数にも注目する必要があります。
![4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/4db0a356-0731-b214-a38f-f86c6255097f.png)
一般的には、シーンが切り替わるプロセスには、エンジンが自動的に一回に呼び出されます。これに対し、UWAは10～15分の時に手動で一回に呼び出すことをお勧めします。

同時に、開発チームはResources.UnloadAssetを使用して、ゲームの実行中に使用されなくなったと判断された特定のアセットを削除することができます。このAPIは、単一のアセットを削除するのに非常に効率的です。また Resources.UnloadUnusedAssetsの統一処理時のプレッシャーを軽減することもできます。

下図はレポートにある相関関数をローディングするスタック情報を示しています。スタック内のGarbageCollectAssetsProfileはResources.UnloadAssetsUnusedの呼び出されによって引き起こされたのです。もしこの項の占用は高すぎると、頻繁にResrouces.UnloadUnusedAssetsを積極的に呼び出すかどうかことに注意する必要があります。
![5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/211070ed-e729-82f8-2cf8-43d3a288e8fa.png)
###3、GC.Collect
GCの呼び出された頻度は、主にMONOメモリからの影響を受けます。関数のMONOメモリ割り当て量は多いほど、頻繁になるほど，GCは早くなります。したがって、GC.Collect関数がより頻繁に呼び出される場合（下の図に示すように）、特にゲームの実行時間が長くなり、頻繁になるにつれて、高い割り当てと頻繁な割り当てを行う関数があるかどうかに注意する必要があります。このように、Mono割り当てが早すぎるかまたは高すぎるかという現象は、GOT OnlineのMonoモードを利用してトラブルシューティングすることができます。
![6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/33e6c3aa-3309-5e85-0600-b085a10b726c.png)
###4、Instantiate
![7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/30050831-9ae0-4cf0-1675-0634f3066f37.png)
ここでの統計は、アセットがインスタンス化処理にかかる時間です。プロジェクトのアセットがより複雑であるほど、インスタンス化された数が多いほど、フリーズの感覚はより明白になりますが、この部分は見落としがちです。UWAはこの部分をどのように処理しますか？それでは、UWAの実人実機テストレポートの[アセット管理]モジュールを組み合わせて、具体的に解説します。

##二、アセット管理
ここでのアセット管理はアセットの呼び出された頻度、処理時間などの策略を指します。所詮、ローディング体験を影響するのは二つの視点がある：ローディングの頻度とローディングたびに所要な処理時間です。実機のテストレポートには、「アセット管理」というラベルの後に以下のトラブルシューティング項目が含まれます：
![8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/7a0d6eb3-b963-d95b-c36a-ba52da3ba5cd.png)
こんなに多くの機能の中に、注目に値する詳細はどれでしょう？では、いくつかのキーポイントを説明しましょう：
###1、処理時間が長いローディングに注目する
AssetBundleであろうとアセットローディングであろうと、処理時間が長い方に注目するすべきです。ここではあるアセットローディングのブックマークをオープンして、全体の実行プロセスにアセットの呼び出された詳細が表示されます。ちなみに、最後の列は処理時間です。
![9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/6548f5bb-6e1c-993d-9984-6826a47a7080.png)

アセットの具体的な情報に、あるアセットを選択すると、実行プロセスにおける呼び出された詳細が表示されます。上のスクリーンショットと対応すると、このAssetBundleローディングはそんなに長い処理時間が必要かどうかを検討します。
![10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/931d0ba1-6afe-7fb8-f50a-c76644533764.png)
###2、短時間の密集的な呼び出された回数への要注意
AssetBundleであろうとアセットローディングであろうと、ローディング的頻度に注目する必要があります。普通に频繁的なローディングの対象に対して、バッファプールを立てる方法で、一回にローディングしてからキャッシュに入れて、それでは後のローディングは必要がなくなります。

下図のように、頻繁にローディングされたAssetBundleは、元々毎回5ミリ秒または50ミリ秒の処理時間があるかもしれませんが、その後は直接0になっています。
![11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/54cd762d-cd84-1731-84cc-bd699c01c689.png)
もう一度、一つのフレームに同じアセットは何度もローディングされることも注意してください。

下図のように、この一つのフレームに５回に呼び出されることは間違いです。
![12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/2049f218-5e20-c88e-1261-38d30ed8cd89.png)

###3、存在しないアセットに注意する
![13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/6918580e-a1ad-11e4-8b41-33bb78e35c15.png)
アセットローディングのリストに時にはアセットが「存在しません」という現象が現れます。これらはロードを指定しないことによってローディングに失敗したアセットであります。一般的に、この種類のアセットはバージョンの更新に伴って、削除や移動する後、対応するコードを修正や注釈しないことによって引き起こされたのです。

これらの「存在しません」というアセットをローディングするのは、一部だけのCPUコストを引き起こします。さらに重要なのは、これらの「存在しません」アセットをトラブルシューティングすると、ロジック上の問題で強制終了やフリーズなどの問題が避けられます。
###4、頻繁なインスタンス化/Destroy
オペレーション回数はより多いまたは処理時間はより長いアセットです。頻繁的なInstantiateはある程度のMONOメモリ分配を引き起こします。それはGCがシステムに呼び出された頻度を加速させます。さらに重要なのは、頻繁的なインスタンス化はCPU使用時間に一定的な最大値を持ち起こします。自然にゲームの流暢性は影響されしまいます。だから、この部分に注意する必要もあります。
![14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/6d2f77a4-6a01-3bac-4f25-8f84b0c4cd55.png)
このような頻繁的にインスタンス化されたアセットに対し、バッファプールを通して、インスタンス化回数が多いGameObjectを再利用することでGameObjectインスタンス化時間を減らします。
###5、ActivateとDeactivate
このトラブルシューティング方法はインスタンス化と類似で、主に呼び出された頻度と処理時間に注意してください。
![15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/1bf1ca4a-d281-0cca-acb0-ea1dc368d1f2.png)

両者の差が大きければ、無駄なActivate/Deactivateオペレーションが存在することを説明しています。そのため、ActivateとDeactivateの呼び出された回数を比較する必要があります。
![16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/c0a36ec9-9a34-0846-b3d5-d04aa07bcb1b.png)

例えば、あるアセットのActivateオペレーション回数は非常に多い（下図のGold_2とGold_4）、どうして回数はそんなに多いなのか？必要があるかどうか？当該アセットの名称をコッピして、Deactivateアセットのリストには検索してみて、こんなに多くな状態のアクティベーションは必要かどうかを検討します。
![17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/f27b487f-1233-2f15-69ed-4cec00ed6e3f.png)

Gold_2のDeactivate
![18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/2c7c8678-f241-58a5-2313-bb701d028b0f.png)
Gold_4のDeactivate
![19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/4b07a8f2-fcfe-8a5c-2d73-563c00422407.png)

**これは１千回以上のDeactivateオペレーションは無意味であることを示しています。**

以上のようなアセットに対し、C#端末で特例キャッシュを作って、この対象のActiveの状態（True or False）を記録します。，SetActiveを呼び出す前、まず目前も状態は切り返したい状態になっているかどうかを判断し、なっていなかったら呼び出します。これはSetActiveのオペレーションはC#からC++層へと移動するからです。C#で状態判断する時、このような言語間のオペレーションを減少し、無駄な処理時間を避けます。

###6、AssetBundle持続最適化
このパラメータに注目するのは、プロジェクト実行プロセスのメモリを影響するためであります。Unityメモリの一部は由AssetBundle持続で引き起こしたSerializedfileとかかわっています。一般的に、AssetBundleアセット数量を1000以下に制御することをお勧めします。この指標はプロジェクト自身の複雑さと関連することを考えて、自分でいくつかの実験を行い、CPUとメモリ間のバランスをよく把握する必要があります。
![20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/fb9721c4-dfd2-b503-501f-85b651616ac4.png)
アセットのローディングはバッファプールの方法で最適化でき、AssetBundleのローディングも類似です。同じAssetBundleを頻繁的にローディングするのは常に不合理であります。（下図のように）、頻繁的にローディング・アンインストールするAssetBundleに対し、キャッシュに入れて、メモリに常駐することをお勧めします。
![21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/f20ab6dd-7a5a-80fb-4784-04d18dfb16ca.png)

##三、Shader.Parse/CreateGPUProgram

Shaderアセットはローディングを解析する策略は不適切な場合、高いCPUコストを引き起こします。Shaderのメモリ占用は少ないけれども、ローディング時間は長いです。そのため、理想的には実行開始時に、すべてのShaderアセットをローディングしてから、キャッシュすることをお勧めします。

###1、Shader.Parse
![22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/2fdd4ab1-e7b7-7c2b-a599-f683a55ed23d.png)
この関数の処理時間はShaderのローディングと解析によって、特にShaderの繰り返したローディングによって引き起こされます。最適化する際、具体的なShaderローディングの状況を見て、以下の三点から取り組みます：

（1）Standardの使用を避けます。Standard Shaderの代わりにほかのShaderを使用してください。モデルのインポートでStandard ShaderがAssetBundleにローディングされることを注意してください。
![23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/768a15af-3e97-9865-17eb-f1a9ad8e02c1.png)

（2）Shaderの冗長問題を解決します。冗長の問題は、Shaderのメモリトレンドと一緒に考えられます。下図のように。
![24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/caae48d5-8b16-d7b2-1303-08f97b300077.png)
もしShaderアセットはメモリにキャッシュされなかったら、シーンを切り取るとShaderが解放され、シーンに切り込むとShaderが読み込まれるため、大量な重複ローディングが引き起こされました。この問題を解決するには、Shaderを取り除き、依存関係を介して別のABにし、ローディングした後にアンインストールしないでキャッシュしてください。それで後にShaderをローディング必要がなくなります。

（3）ShaderのKeywordを検証します

開発チームは以下の資料を参考しください：
[「Shaderバリアントコレクションとパッケージコンパイルの最適化のアイデア」](https://blog.jp.uwa4d.com/2021/09/24/shader%e3%83%90%e3%83%aa%e3%82%a2%e3%83%b3%e3%83%88%e3%82%b3%e3%83%ac%e3%82%af%e3%82%b7%e3%83%a7%e3%83%b3%e3%81%a8%e3%83%91%e3%83%83%e3%82%b1%e3%83%bc%e3%82%b8%e3%82%b3%e3%83%b3%e3%83%91%e3%82%a4/)

[https://answer.uwa4d.com/question/5da86670e84db43d6efbda72](https://answer.uwa4d.com/question/5da86670e84db43d6efbda72)
###2、Shader.CreateGPUProgram
![25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/dd1739c8-48cb-b358-f123-812d11686cb1.png)

このAPIのCPU占用はShaderが初めてレンダリングされる時に造られたGPUプログラムの使用時間です。この時間はShaderの複雑さと相関します。それに対し、開発チームは通过ShaderVariantCollectionを通してShaderをローディングします。それにローディングした後、Warmupして、Shaderが実行中に造られたShader.CreateGPUProgramの処理時間を避けます。

---

以上はローディングは最適化する際に注意すべき点であります。具体的には、プロジェクトの実際な状況に応じて操作する必要があります。UWAのオンラインテストサービスを組み合わせて、素早くパフォーマンスのボトルネックを特定することができます。
UWA Technologyは、モバイル/VRなど様々なゲーム開発者向け、パフォーマンス分析と最適化ソリューション及びコンサルティングサービスを提供している会社でございます。

今なら、UWA GOTローカルツールが15日間に**無償試用**できます！！
よければ、ぜひ！

UWA公式サイト：https://jp.uwa4d.com
UWA GOT OnlineレポートDemo：https://jp.uwa4d.com/u/got/demo.html
UWA公式ブログ：https://blog.jp.uwa4d.com