概要
【Unite 2017 Tokyo】最適化をする前に覚えておきたい技術 の講演動画を見て勉強用でまとめたメモ。もう3年前の講演ではあるが、2020年でも意味のある内容だったのでまとめた。
Unity 最適化の概要は本記事をざっくり流し読みして、詳細は講演動画を見るとより理解が深まると思われる。
最適化の手順
最適化の前に以下を覚えておく。
- 処理の一部分を直せば大体の問題は解決
- 直すべき処理を見極めるのが最優先事項
- いきなり修正作業を始めてはダメ (時間の浪費, 修正前後のパフォーマンス差分が取れない)
実際の手順は下記の通り。
- 処理負荷となっている箇所を特定するためにプロファイリング
- 負荷となっている処理内容が特定できたら修正の算段を立てる
- 実際に作業して直す
プロファイリングについて
- 処理に掛かった時間やメモリ使用状況を確認する作業
- Unity 標準機能の Profiler を使う
- 各プラットフォーマーのネイティブ Profiler もあるが、大まかな把握は Unity の Profiler で充分
- CPU/GPU/Rendering/Memory/Audio/Physics/uNET/Viedo Player 等の状況が確認可能 (実機での負荷も確認可能)
メモリ使用量が多い場合
- 単純にメモリ使用量が多い or メモリリーク
- C#メモリが多い or Unityメモリが多い
C#メモリでの着眼点
- C#スクリプトで利用しているメモリでGCされるが、予約済みメモリは二度と返されない
- C#メモリは Monoを確認する
- UsedよりもReserved(予約済み)に注目
- Editor上だとEditorメモリも加算されるので実機が良い
- Reservedが大きすぎる場合、一時的にC#メモリが膨らんでる可能性がある
- ファイル読み込みや通信で一時的に大量のC#メモリを使用していないか
- 指標として 300MB は大きい
- 大きかったらスクリプトの見直しを行う
Unityメモリでの着眼点
- Texture, Mesh, Animation 等のアセットのデータで利用 (GCされないので メモリリーク する)
- メモリ上にある各Assetのメモリ内の容量をチェックする
- Memory Profiler を Detailed ビューに切り替えて Take Sample を押すと表示される
- どのアセットがどのぐらいメモリを確保しているのかが分かる
- 指標として 10MB のテクスチャは大きいので圧縮する
- 余計なアセットが読み込まれないようにする, 必要以上に大きいアセットは圧縮等を行う
- 必要であれば読み込みタイミングをずらす事も視野に入れる
- 岩のテクスチャなのに「1024 x 1024」みたいにリッチなパターンもあるよ
ロードが長い場合
- 本当にロードが長いのか? => ロード済みデータを確認して、余計なデータや大きすぎるデータが無いか
- それとも初期化処理が重いのか? => C#スクリプトを見直す
- CPU グラフが跳ね上がったところがロードタイミング、ここをクリックして詳細を追う
- スクリプト名.Awake という形で C# スクリプトと対応している
- LogStringToConsole がロード時間の1/4 (Debug.Logの書き出し)
- Debug.logger.logEnabled = false でログ書き出しを無効に出来る
- Texture.AwakeFromLoad で 0.35秒、テクスチャのロードが重い
- CPU 詳細情報を Timeline を見ると、別スレッドで行っているファイル読み込み処理も確認可能
- Memory Profiler の Detailed ビューで読み込まれるはずの無いデータが読み込まれていないか?
ゲームが一瞬固まる場合
- 裏でロード処理を行っている心当たりがない場合は GC が走っている
- C# メモリが足りなくなった時に GC が発生, GC は膨大な処理時間を取られるので固まる
- CPU グラフ で Garbage Collector のみを表示
- 指標として 16ms 掛かってる場合もある
- まずはどの C# スクリプトが多く消費しているのか特定, 可能な限りメモリ使用を抑える
- 文字列操作(連結等)はメモリを都度使用するので、StringBuilderクラスを使うと良い
- 毎フレームのメモリ確保は控えるようにする
- CPUの詳細の GC Alloc にて、C#メモリが確保されたか確認できる
- 指標として 5.6kb は多い
- Deep Profile を ON にすると、更に詳細の呼び出し先の情報が確認出来る (実行中の動作は重くなる)
フレームレートが常に低い場合
- 60FPSにしたいなら1フレームの処理を16.6ミリ秒に抑える必要がある
- 30FPSでも33.3ミリ秒に抑える必要がある (30FPSは許せる, 20FPSを下回るとカクカクしてると感じる)
- ゲームロジックの負荷なのか?それとも描画負荷なのか? (描画処理の修正によるバグ発生は少ない)
- CPUグラフの「Rendering」が描画負荷、それ以外 (Vsyncを除く)ならゲームロジック
- Vsync は指定したフレームレートになるまで処理を待つ機能 (30FPSだけど10FPSで終わったら20FPS待つ)
描画負荷の場合
- PCとモバイル端末では描画性能/解像度が大きく違う (モバイルのほうが問題発生しやすい)
- モバイル端末は FullHD を超える解像度だが、描画性能は低い
- PC は大体は FullHD なので解像度が低いが、描画性能は高い
- 試しに解像度ギリギリまで下げた状態で実機で動かしてみて軽くなったら描画負荷 (Screen, Camera.Rect)
- 描画処理の確認は FrameDebugger を使うと良い、描画順や描画したMaterialの確認が可能
- 余計な描画が入っていないか
- Batch数, Set Pass数 (ドローコール) が多くないか ( 200 を超えたら見直しが必要)
- 頂点数が多すぎないか
- 描画面積が広くないか (Overdrawをしすぎていないか)
- Shader が重くないか (軽いMobile/Unlit/Texture, Mobile/VertexLit 辺りと比べて差分を確認すると分かりやすい)
ゲームロジックの負荷の場合
- 負荷の原因はC#スクリプト?Unity側の処理?
- Unity側の処理は物理処理、UI処理がよくある負荷原因
- GameObject が多すぎて全体敵に重くなることもある (モバイルで上限 3,000個)
- C#スクリプトの負荷: CPUグラフのHierarchyからBehaviourUpdateがどれくらい時間がかかっているか確認する
- 物理の負荷: Physics.Processing を確認する
- UI処理の負荷: Canvas.SendWillRenderCanvases を確認する
- GameObjectの数は Memory グラフの simple に表示されるので確認する
- C#スクリプト: ゲームによって色々
物理処理
- 余計な衝突判定を行っている → Layer Collision Matrix を見直す
- 複雑な衝突判定を行っている → MeshCollider は重いので BoxCollider等 で置き換える
- 1フレームに複数回Physics処理が回っている → TimeのFixedTimeStep設定を見直す (ありがち)
- PhisicsはUnityのUpdateとは別に回っている: ProjectSettings -> TimeManager -> Fixed Timestep が秒間の処理 (30FPSなら 0.33 で充分)
UI処理
- 原因: 重い頂点計算が走っている (canvasは動かすと再計算が走る)
- 対策: uGUIはAnimationしているものは別Canvasに分けないと重い
参考リンク
まとめ
最適化の手順や手法、負荷の原因が体系立って説明されていたので、Unity でゲームを作ったけど重いと感じたが何をしたら良いか分からない人にとっては参考になる内容だと思った。特に、Profiler で見るべき項目に加えて重い処理時間の指標も説明されていたので、今すぐ使える知識になるのでは無いだろうか。
また、上記の他に「Quality設定」や「フレームレート設定」も見ておくと良いかもしれない。
(WebGL のメモリ - Unityのようにプラットフォームごとの最適化もある)
※ 記事内容に間違いや問題が合った場合は、お手数ですがコメント等でご指摘頂けますと幸いです。