ロード時間を短くするために効率よくチューニングする

本記事は、[サムザップ Advent Calendar 2022](https://qiita.com/advent-calendar/2022/sumzap) の12/18の記事です。

例えばよくあるゲームでローディング画面を開いて次のゲームシーンを待っている時間、絶対短くしたいですよね。

しかしながら、開発を続けていると、うっかりロードが長くなってしまったりすることもあると思います。
この記事では、ロード処理を改善することで、ロード時間を短くするためのチューニングテクニックを紹介したいと思います。

※ この記事で出てくるprofileのサンプルは、Unity2021.3.11f1を使用しています


# ロード処理を無駄なく最大効率で行う
ロード時間はアセットの容量に比例して伸びます。
アセット量がボトルネックなら、処理をいくら工夫してもロード時間を早くする術はありません。
ところが、CPUがロード処理に対して十分に仕事をできておらず、結果としてロード時間が延びるということもあり得ます。
そこで、まずは **本当に毎フレーム時間いっぱいロード処理をやれているのか** という観点でProfiler ([CPU Usage Profiler Module](https://docs.unity3d.com/ja/2021.3/Manual/ProfilerCPU.html)) を眺めてみるのがよいと思います。

## 計測をする

さて、まずは理想の状態をイメージしてもらうために、たくさんのアセットバンドルを並列ロードしてる良さげなフレームを撮ってみました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/98553/6b983a79-7a3b-f7f7-95e7-03af8028d4de.png)

アセットバンドルのロード処理はPreload Manager というスレッドで行われているようです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/98553/1555ca3b-9c5d-8ae1-8770-eccb33c3e8bd.png)


このPreloadManagerスレッドの欄にたくさん処理が書かれていれば、アセットバンドルのロード処理が休みなく行われていることになります。

逆に、十分なアセットのロード処理が発行していないフレーム (下図) をみてみると、PreloadManagerの処理のほとんどは `Semaphore.WaitForSignal`となっていて、ただ待機してるだけになっています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/98553/9fe47bff-bbb1-37e6-1427-f705292192ef.png)


ロード画面においては、とにかくロード処理を短時間で行うことが望ましいので、PreloadManagerスレッドがたくさん働いている状態を作ると良いと言えます。




## AssetBundleのロード処理を効率よく並列化する

ロードによる待機時間の短縮の大方針は、とにかく並列にロード命令を出してPreloadManagerが常に働いている状態を作る　です。

具体的には
```csharp
await LoadItem1Async();
await LoadItem2Async();
```
これを
```csharp
await UniTask.WhenAll(LoadItem1Async(), LoadItem2Async());
```
こうしていく作業をひたすらやり切る です。

ロード開始直後に全部のロード命令を一気に出してロード自体を並列処理させることができれば、無駄なくロードができてかなり速くなると思います。

とはいえ、実際に開発中のプロジェクトでロード処理を改善したい場面に直面した時には、全てをがむしゃらに書き換えていくのはなかなか難しいかもしれません。
シンプルに工数がかかりますし、場合によってはデータの持ち方を変えたり初期化順を調整したりと、バグにつながるような変更もたくさん必要になると思います。
さらに、アセット一つ大変な思いで対応したとしても、ボトルネックが他のアセットだったりすれば効果を体感しづらいこともありえます。

ボトルネックになってるアセットを見つけ出して並列化することで効率的にチューニングができるといいですよね。
そこで、[Asset Loading Module](https://docs.unity3d.com/ja/2021.3/Manual/profiler-asset-loading-module.html)を活用します。

### Asset Loading Module でどのフレームでどのアセットがロードされているかを知る

Asset Loading Moduleは Unity2021.2以降で使える機能でして、どのフレームでどのアセットが読み込まれたのかがわかります。
画像は、あるフレームでLoad中のアセットデータの一覧が表示されている様子です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/98553/ac162fb4-97ab-9b91-2853-b2d73e2848a4.png)

また、ひと手間かかるのですが、該当アセットのアセットバンドルがどのタイミングでロードされているのかもわかります。
こんな感じに注目したいアセットを開いて、謎のファイル名を覚えておく。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/98553/ef253343-a700-b1bd-adda-c4310680a96d.png)

表示モードを変更して、All Framesに変更
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/98553/bea180ab-9114-1b9c-62eb-193c7e2288a8.png" width=40%>

上部の表示マーカーを右クリックして、First Frame Indexを表示する項目に加えてあげて、
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/98553/5883e2bb-2750-e0ca-ad86-c0d547af86d4.png" width=25%>

検索窓に先程の謎のFile名を入力すると、
![スクリーンショット 2022-12-16 15.33.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/98553/bfebc1fa-e4f0-f8d1-b4e8-021b0bed886c.png)

該当ファイルのアセットバンドルがどのフレームでロードされて、いつアセットが取り出されたのかがわかります。
Typeの欄を確認すると、AssetBundleをロードしたのか、アセットをロードしたのかがわかり、該当処理のフレームはFirstFrameIndexをみるとわかります。
このスクショだと、Index 45でアセットバンドルがロードされて、65でstatus_410というアセットがロードされていることがわかります。

また、このFirstFrameIndexの値は、プロファイラの持つフレームのIndexを示しており、下図の `Frame: 52/300`の部分とリンクしています。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/98553/9bcfa7e5-ddc4-e6ae-18b2-c72fa7ce5d29.png">

ただし、例えばIndexなので(?)、Index45なら、Frame46を見ないと該当データが存在しないので注意してください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/98553/a5cf1c01-f19c-2dfc-b021-e5d57f4b316c.png)

### Asset Loading Module を活用して効率的に並列化する

並列化したロード処理は、ソースコード上では、(ものすごく簡単化すると)こんなイメージです。
```csharp
async UniTask LoadAsset(string assetBundleName, string assetName)
{
    var filePath = GetAssetPath(assetBundleName);
    var assetBundle = await AssetBundle.LoadFromFileAsync(filePath);
    await assetBundle.LoadAssetAsync(assetName);
}

// 並列化したロード処理はこんなイメージ
// UniTask.WhenAll(LoadAsset("hoge", "hoge"), LoadAsset("fuga", "fuga"), ...);
```

まずはAssetBundleLoadの命令がUnity側の伝えられ、それらが終わって初めてAssetをAssetBundleからロードする処理がよばれることになります。
実際、Asset Loading Moduleを眺めていると、アセットバンドルロード命令を同じフレームで並列実行したものについては、すべてのAssetBundleロード処理が走ってから、アセットのロード処理が動いていそうでした。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/98553/42a2d46d-30da-0cf3-7910-a6deecadef41.png)
例えば、このフレームまではアセットバンドル Typeばかりですが、

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/98553/57a43908-bcbc-8f60-d835-ee447428b9f0.png)
次のフレームからはAsset In AssetBundle Typeも発生しており、アセットバンドルからアセットをロードする処理が動いていることがわかります。


この特性を利用すると、アセットバンドルからアセットをロードする処理が動き始めてから、途中でAssetBundleロードが差し込まれるようなら、そのアセットは並列ロードできていないかもしれない　と疑うことができます。

さらに、このアセットバンドルロードが断続的に起きてるようなら、もしかしたらこんな感じになってるかもせず、ロード処理を長引かせがちなので要注意です。
```csharp
for(int i = 0; i < count; i++)
{
   await items[count].AssetLoadAsync();
}
```

また、ロード処理の最後の方には特に注意を払うべきです。
特に、最後の方にアセットロード量が少ないケース。終盤にアセットバンドルロードが飛んでいる可能性が高く、もしそれだけを待っているなら、並列化によってロードタイミングを早めることでロード待機時間を減らすことができます。

明らかに後半に少しずつロード命令が飛んでいるProfile結果の例を出しておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/98553/d428be56-a7e4-e6f7-4601-a4a9b22cf820.png)
Asset Loading Module Windowのグラフ上のロード後半付近に着目すると、僅かなロード量しか計上されていません。
詳細をみると、ロード後半にもかかわらずアセットバンドル Typeのロードが入っていることがわかります。


# バックグラウンドスレッドの設定を変えてロード速度をアップ
PreloadManagerスレッドにギチギチに処理を詰め込んだところで、バックグラウンドスレッドの優先度をあげてロード速度をアップします。

[Application.backgroundLoadingPriority](https://docs.unity3d.com/ScriptReference/Application-backgroundLoadingPriority.html)で設定を変更することで実現できます。
こちらの設定のみを変えて、同じシーンファイルのロード時間を比較してみました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/98553/8d018c54-20c3-38b9-8c85-f798aedf2779.png)

`Application.backgroundLoadingPriority = ThreadPriority.High`

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/98553/08f71537-e468-118e-8e48-eb969db74d48.png)

`Application.backgroundLoadingPriority = ThreadPriority.Low`

同じシーンファイルをロードしているフレームにフォーカスしていますが、**Low 257ms** に対して、**High 116ms** と、２倍以上の速度でロードができていることがわかります。
効果が高いので是非試してみてください。


# おわりに

ロード時間を短くするために効率よくチューニングするテクニックについて紹介しました。
- ロード並列化を効率よく行うためのProfilerの利用方法
- Application.backgroundLoadingPriorityによるロード処理の高速化

本記事で紹介した手法は多くのプロジェクトで使える知見だと思うので、ロード時間を短縮したいと思ったら是非試してみてください。

※ 本記事ではロード処理の最適化にフォーカスした話をしましたが、ロード時間の短縮にはアセット自体の最適化も重要です。
これらについては[UnityPerformanceTuningBible](https://github.com/CyberAgentGameEntertainment/UnityPerformanceTuningBible)が参考になると思います。興味があればこちらもぜひどうぞ。

明日は @norimatsu_yusuke さんの記事になります. お楽しみに。