こんにちは、情熱開発部プログラマの青柳です。
お盆休みで一息ついたのですがまだまだ暑い日が続きそうで溶けてしまいそうです。
お盆前にはCEDECがあり色々勉強や刺激を受けました。
その中でUnity Object 原論という講演があり、講演を見てから私もUnityの内部動作を知りたくなりました。
今回はUnityの基本であるSceneLoadについて改めて調べてみます。
目次
初めに
Unityの基本でもあるSceneLoadですが、基本過ぎて中で何が行われているか知らないままです。
今回は、FantasyKingdomをAndroid端末P710で読み込んだ場合にかかった時間を調べ、Profilerから動作を推測しながら、Load時間の軽減、ヒッチの軽減が出来ないか検討します。
今回の環境ですが家に転がっていたネットにはつながないP710です。
スペックは以下になります。
2020/12/08 発売
Snapdragon™ 720G / オクタコア 2.3GHz + 1.8GHz
Ram 4GB
OS Android10
5年くらい前のミドルレンジスマートフォンの認識です。
こちらでDevelopmentBuildにてProfilerにつなぎながら確認します。
Unityのバージョンは
6000.1.11f1
になります
注意
この文章の中で度々重いという言葉が出てきますが
これはP710にとって重いというだけです。
M2.SSDのPCにて実行した場合はロード時間はほぼなくすぐに実行されます。
調べるとはいいつつ結論から言ってしまえば、ロードが長いという場合は結局、その機器で一度に読み込む量が多い、ということに他なりません。
ロード時間を短くするには事前に読み込んでおく、必要最小限のものを読み込む。という対策になります。
ただそれはそれとして何か発見があるかもしれませんので見ていきましょう。
FantasyKingdom Sampleと今回の計測方法について
FantasyKingdomはUnity社が公開した公式サンプルになります。
Mobileの限界に挑んだ、というコピーに偽りない凄いサンプルになっています。
GameObjectの数が大体25000個になる巨大なSceneで作られています。

LoaderSceneをはじめに起動し、そこからDemoSceneが読み込まれます。
計測時間はDemoSceneの読み込み開始から、DemoSceneにあるMonobehaviourのStartが開始できるようになるまでを測定します。
LoaderSceneには負荷をかけるためにMask君を置いておきます。
Mask君のAI系のScriptは外しておきました。
測定結果
大体、4.9秒ほどかかっていました。
Profilerのスクリーンショットは以下になります

SceneLoadの処理内容
AssetLoaderのProfilerと合わせるとリソースを読んでいる以外に処理がありそうな事が分かります。
Unity内部の動作なので詳細はもちろん分かりませんが、Profilerで出てくる関数名が「Component.setGameObject」なのを見ると
ComponentとGameObjectを結び付けているようです、これはC++世界とC#世界が分かれているのを紐づけているのではないかと思います。
つまりSceneの構築をしていそうです。

しばらくその状態が続いた後、CPUに強烈なスパイクが入っています。これはComponentなどがAwake等を行っていそうです。
また、このタイミングでも読み込まれるAssetがあるようです。今回はAudioを読み込んでいました。
- Assetを読む
- Mesh, Textureを読み込む(おそらく.unityファイルに書かれているAsset)
- Sceneを構築する
- Awake処理
- 必要なAssetがあればここでも読み込む
がSceneLoadで行われる処理のようです。
言われてみれば当たり前の事をされていますね。

これだけだとあまり内容が無いのでもう少しだけ調べてLoadingを軽くするにはどうしたらいいかについても調べてみます。
Loading負荷軽減に関する疑問をあれこれ
0. LoadSceneAsyncとLoadSceneでLoad時間は変わるか?
変わりませんでした。
1.GameObject数は関係があるか?
試しにGameObjectを25000個だけ作ったSceneを読み込んでみましょう。
1.49秒かかりました。

次にMeshRenderをアタッチさせた場合を試してみます、Capsuleを描画させますが、一部のみがカメラに映るようにしています
3.29秒かかりました。

処理負荷増加はGameObject数というよりはComponent数といっていいのかもしれません。
1.1 非ActiveなGameObjectはLoadに影響するか?
Sceneの構築処理は変わらず実行されるようです。DemoSceneのGameObjectを全て非Activeにしても同じような時間がかかりました。
2. TextureのRead/WriteEnableはLoad時間に影響するか?
MeshやTextureは公式Blogに書かれている通り、ある条件を満たせばStreamingで読み込まれます。
この条件に、Read/WriteEnableでない事、が含まれています。
またRead/WriteEnableを入れるとC#側のメモリも必要とされるのでこちらは影響があるでしょうか。
FantasyKingdomのTextureを全てRead/Write Enableに変えて試してみます
大体5.1秒ほどに伸びました。
またRenderThreadへの負荷でスパイクが発生しています。

RenderThreadに負荷が起きる場合は、Read/WriteEnable以外に以下があります。
- Texture
- テクスチャは読み取り/書き込み可能ではない。
- テクスチャは Resources フォルダーに入っていない。
- ビルドターゲットが Android の場合、プロジェクトのビルド設定で LZ4 圧縮が有効になっている。
- Mesh
- メッシュは読み取り/書き込み可能ではない。
- メッシュは Resources フォルダーに入っていない。
- メッシュは BlendShape ではない。
- Unity がメッシュに動的バッチ処理を適用していない。
- メッシュの頂点/インデックスデータが、パーティクルシステム、Terrain (地形)、メッシュコライダーで必要とされていない。
- メッシュに ボーンウェイト がない。
- メッシュ トポロジー が クワッド ではない。
- メッシュアセットの meshCompression が Off に設定されている。 ビルドターゲットが Android の場合、プロジェクトのビルド設定で LZ4 圧縮が有効になっている。
2-1. 非同期読み込みなのにスパイクが起きる場合があるか?
基本的には上のように非同期で読み込まれます。
非同期でない場合でも、RenderThreadに余裕があればわからないかもしれません。
それ以外には今回、SpeedTreeのAssetを読み込む際に、Meshが作られて盛大にスパイクが起きました。
Unityの内部動作でどうしようもなさそうだったのでそのままにしています。

3. Assetを前のSceneで読み込めばLoad時間は短くなるか?
当たり前ですがなりました。
ただ今回はSceneの構築にも時間がかかっているので一瞬にはなりません。
また読み込みを事前に行う場合、複雑になりがちです。
事前にどういう仕組みにしておくか決めてから実装するのがよさそうです。
それにはどういうゲームなのか知っておく必要があります。
4. Application.backgroundLoadingPriority, Async Upload Pipelineの設定は関係があるか?
backgroundLoadingPriorityをHightにしない場合は6.0秒ほどに伸びました。
お手軽に効果はありそうです。
AUP
の設定変更なのですが
QualitySettings.asyncUploadTimeSlice = 8;
QualitySettings.asyncUploadBufferSize = 1024 * 1024 * 64;
にしてみたところ、結果のLoad時間はあまり変わりませんでした。
ただProfilerで動きを見るに、Assetの読み込み時間はたしかに短くなってそうなのですが、Sceneの構築がその分のびてしまっているように感じられました。
ちょっと極端な設定にはしづらそうです。

Load時間短縮まとめ
事前にAssetを読み込むのは有効ですが、ただ、これらは問題になったときに行うのには結構つらいものがあります。
OpenWorldのゲームでなくても事前読み込み(とその途中中断、破棄)はしない場合に比べて複雑です。
Load戦略は事前に決めて実装する、そもそも巨大なSceneを作らない、不要な大きなAssetを作らない、が重要ですね。
また実際のゲーム制作では通信が必要だったりなど絡む要素が多く、ロードをごまかす演出を含めれたりで出来ることもまた多くあるはずです。
クライアントプログラマだけでなく、サーバーサイド、プランナー、デザイナーにも注意してもらいながら進めたいですね。
その他のAsset設定をProject Auditor packageという便利なツールもあるので確認してみます。
SpeedTreeのMeshがRead/WriteEnableになってるのは変えられないので置いといて、、、。
Audioが軒並み「Decompress on Load」になってたり「Load Background」になってなかったりしました、これを変えて再度測定してみましょう。
Load時間が1秒くらいちょっとだけ軽くなりました。Project Auditor便利です。
Extra. DOTSで読み込むとどうか
今回試したこと
本当はGameObjectをGridで分けてSubSceneにして一度に読み込む量をへらしてー、と思ったのですが力及ばず、、、。
そこまではやらずにSceneAssetにある要素をそのままEntityとして読み込んでもらいます。
やり方は公式のDOTSSampleにてBakeしたSceneAssetからEntityを作る形にしました。
※DemoSceneでGameObjectからSubSceneにする方法は手軽だったんですが、読み込みステータスをとるのに手間取ってしまいまして、、、。
画像のような形にしています。
また今回の環境ではAudioMixerでErrorが起きてしまったため、AudioMixerは削除しています。
(Editorでは読み込み時のログでMaterialでWarningやLODGroupでErrorが出たのでかなり無理をさせているようでした)

Script
EntitiesSamples/Assets/Streaming/SceneManagement/2. SceneState
のSceneReferenceAuthorings.csを参考にしました。
結果
全て同じ条件ではありませんが11秒という結果でした。
無茶をさせ過ぎてるので正当な結果とは言えませんが、ECSに変換すれば読み込みも早くなる。
というのは私の勝手な幻想のようですね、、、。

終わりに
CEDECのUnityObject原論を見て、内部動作に少し興味が出てきたので調べてみました。
大した結果ではありませんでしたが何かの参考になれば幸いです。
Unity Object 原論の公開待ってます。
参考にさせて頂いた資料
ロード時間を短くするために効率よくチューニングする
Async Upload Pipeline(AUP)でローディングのパフォーマンスを最適化する
テクスチャとメッシュデータのロード
Project Auditor package
GitHub Unity-Technologies/EntityComponentSystemSamples
Unity 6 の『Fantasy Kingdom』
Time Ghost
【免責事項】
本サイトでの情報を利用することによる損害等に対し、
株式会社ロジカルビートは一切の責任を負いません。