
UnityでUIパフォーマンスを向上させる8つのTips【Advent Calendar 12/1】
はじめに
この記事は Colorful Palette アドベントカレンダー 12/1 の記事です。
Colorful Palette初のアドベントカレンダーとなります!
ぜひ、読んで頂ける方には普段の実装のヒントや、知見になるものを発信できればと思います!
それでは早速簡単な自己紹介のあと、本編に入ります!
こんにちは、株式会社Colorful Paletteでクライアントエンジニアをやっているマミーです。
普段はUI実装や、リアルタイム通信を使った機能のクライアントサイド実装を担当しています。
本記事では「UnityのUIパフォーマンス改善」にフォーカスした内容になります。
ゲームにおけるUIは「ゲームの世界観を表現し、プレイヤーの没入感を高めること」や、「プレイヤーが迷わず到達したい画面、行いたい操作が出来るようにする」といった役割があります。
そのため「リッチな表現」なども場合によっては必要となり、一方でゲーム全体を通して「スムーズに動く」ことも重要になります。
特に、「スムーズに動く」ことを担保するためには、Import Settings、バッチ処理、リビルドやLayout Groupの実行時負荷などを考慮する必要があります。ただし、全自動最適化ボタンのような魔法はありません。そのため「何をすればパフォーマンスを向上させることができるのか」を知っておく必要があります。
そこで、本記事では、UnityでUIパフォーマンスを向上させる8つのTipsを紹介します。
検証環境
●Unity 2022.1.21f1
●Windows 10 Pro 11th Gen IIntel(R) Core(TM) i9-11900K
●Android SONY XPERIA XQ-BC72
Import Settingsについて
まずは、SpriteとしてTextureをインポートする際のImport Settingsの項目でポイントとなる部分を紹介します。
1. 適切なMesh Typeを設定する
Mesh TypeではFull RectとTightの2種類を選択することが可能です。


Mesh Typeでは「板ポリゴンとしてSpriteを描画する」か、「実際のTextureの不透明領域に沿ったMeshを作成して描画する」かを指定することができます。
実際にFull RectとTightそれぞれを設定してImageとして描画した際のGameviewと、Wireframeが次のようになります。
上がFull Rect、下がTightを設定したものになります。


さらに、Overdrawを確認すると下のTightを設定したImageの場合はTextureの透明領域以外が描画されていることがわかります。

まとめると、Mesh Typeは「頂点数」と「オーバードロー」のトレードオフを考慮して適切な設定を指定すると描画効率の改善が見込めます。
2. Read/WriteはOffにする
Read/WriteはOn/Offの2種類を選択することが可能です。

Read/WriteのOn/Offのみを変えた同一画像をImageで表示して比較してみます。

この二つの画像が占めるメモリサイズをMemoryProfilerで確認した結果が下記になります。

Read/Writeを有効化すると、CPUとGPUの両方からアクセス可能にするためのコピーが作成されるためメモリサイズが2倍になります。そのため、実行時にTextureに対して読み書き処理を行わない場合はOffにしておくことでメモリサイズを削減することができます。
3. Generate MipmapsはOffにする
Generate MipmapsはOn/Offの2種類を選択することが可能です。



Generate MipmapsをOnにすると内部的にMip Mapを生成するため、若干データサイズが増加します。
上がGenerate Mip MapsをOffにした場合、下がOnにした場合になります。
同じ画像、同じ圧縮設定ですが若干データサイズが増大していることがわかります。
UI画像においてはMip Mapは基本的には不要なため、デフォルト設定のOffにしておくことでデータサイズやメモリサイズを削減することができます。
4. 適切なCompressionを設定する
Compressionではプラットフォームごとに圧縮形式や圧縮時の設定パラメータなどを決めることができます。

基本的に「プラットフォームごとに適切な値を選ぶ」ことが答えにはなりますが、下記の画像のように圧縮形式や設定によって見た目に大きな劣化が生まれる場合があります。

そのため「品質」と「パフォーマンス」のトレードオフになりますが、プラットフォームごとに実機で適時確認しつつ、アセット制作者とすり合わせながら適切な値を選ぶことをお勧めします。
バッチについて
次に、描画効率化の仕組みとして、1度のドローコールで複数のオブジェクトをまとめて描画するためのバッチ処理がありますが、これはUIでも同様に機能するため、バッチを効かせてUI要素のドローコールを削減するためのポイントを紹介します。
※UIバッチの条件は「Unityでパフォーマンスの良いUIを作るためのTips」が参考になります。
5. SpriteAtlasを利用する
ProfilerのUI Detailsの項目から、どの単位でバッチされているかや、なぜバッチが剥がれているかを確認することができます。

例えば上記の画像であれば「異なるTextureを参照している」ことが原因でバッチが剥がれていることがわかります。
ここからもわかる通り、UIバッチを効かせるには同じTextureを参照している必要があります。
そこで、SpriteAtlasを用いて複数の異なるUI画像を1枚のTextureにパッキングし、バッチが適用されるようにすることでドローコールの削減が見込めます。
ただし、SpriteAtlasが丸ごとメモリ上に載るため、SpriteAtlasに含まれるTextureは同じタイミングで一斉に使用されるように整備することで、無駄の少ない使用が可能になります。
6. Canvasを分割する
UnityのuGUIはCanvasごとにバッファに詰めて描画されるため、同じCanvasに所属していないとバッチは適用されません。
例えば、意図的に黄色枠で囲った部分でCanvasを分割したUIサンプルを用意します。
画面右上と右下のUIパーツは同一のSpriteAtlasにパッキングしています。

この画面発行されるドローコールをFrame Debuggerで確認した結果が次になります。

同じSpriteAtlasを使用している場合でも、Canvasが異なるとバッチが剥がれていることがわかります。
この特性を利用して、アニメーションなどを行う動的なUIが含まれるCanvasと、一切動かない静的なUIのみが含まれるCanvasに分割することでCanvasのバッファの再構築処理コストを軽減することが出来ます。
7. UIの重なりによるバッチ解除を避ける
UIのバッチ処理はバッチ対象のUIの間にバッチできないUIが含まれている場合はバッチが適用されません。
様々なパターンでバッチが剥がれてしまうのか検証するため、下記の具体例を用意しました。

上記のサンプルは
Button: 黄色のボタン画像を表示するImage
Cross: 射線画像を表示するImage
Label: 「Button」という文字を表示するText
ButtonRoot: 空のGameObjectt
で構成されています。
ButtonとCrossの画像は同一のSpriteAtlasにパッキングされているため、本来であれば1ドローコールで描画されているはずです。
実際のドローコールを確認した結果が下記になります。

このことから、UIパーツを構成する際の重なり具合などを考慮して実装することでバッチ処理をうまく適用することが出来る場合があります。
Layout Groupについて
最後に、UnityはUIのオートレイアウト実装向けに様々なLayout Groupを提供しています。
これらのコンポーネントをうまく利用することで、様々な画面サイズや解像度にも柔軟に対応出来るUIを実現しやすくなります。
ただし、Layout Groupは実行時にレイアウト計算が行われるため、大量の要素を持つなどの場合にスパイクを引き起こす可能性があります。
8. 静的なものには使用しない
Layout Group系のコンポーネントは決まったレイアウトにUIを配置する際に気軽に使ってしまいがちです。
ただし、実行時に再計算が必要でない場合は、Layout Group系のコンポーネントを付けていることはCPUを無駄に消費してしまいます。
下記のような簡単なサンプルシーンを用意しました。

GridLayoutGroupを使わずImageを900個配置した左側と、GridLayoutGroupを使用してImageを900個配置した右側を用意しました。
左側と右側のRootのGameObjectのアクティブを次のように切り替え、負荷を検証します。
※本検証はAndroid SONY XPERIAXQ-BC72を使用しました

この時のCPU Profilerの結果が下記になります。

それぞれ
GridLayoutGroup管理オブジェクトが非アクティブ化
GridLayoutGroup管理オブジェクトがアクティブ化
LayoutGroup未使用オブジェクトが非アクティブ化
LayoutGroup未使用オブジェクトがアクティブ化
を意味します。もっとも重要な個所は2と4の比較になります。
もともと大量のオブジェクトのアクティブを切り替えるため、CPU負荷は高いですが、この二つのCPU処理時間の差がLayout Groupのレイアウトの再計算にかかる時間となります。
動的に要素数が増える場合などレイアウトの再計算を実行時に行う必要がある場合を除き、Layout Groupは極力使わないことで余計なCPU負荷を避けることが出来ます。
おわりに
今回は「今日から出来るUIパフォーマンスを向上させる8つのTips」というテーマでColorful Palette初のアドベントカレンダー1日目を執筆させて頂きました。
パフォーマンス面の考慮は設計段階からリリース、運用フェーズまで常に関わってくるテーマになります。本記事では様々なフェーズで活かせるであろうTipsを紹介しました。
チューニングは「計測」するところから始まりますが、原因の究明や対策を練るための知見として本記事が役立てれば幸いです。
明日は、Diarkisという世界規模で利用実績の少ない技術について、一緒に戦ってくれたサーバエンジニアのあくびによる「ゲーム内イベントの予定に合わせたサーバのスケールアウト・インの自動化について」の記事です!
運用時に発生していた障害件数を大幅に減少させてくれた方法に関する記事なので、お楽しみに!
参考リンク