unityパーフォマンス最適化シリーズーーUIモジュール

4年前、Unityの各主要なモジュールのパフォーマンス最適化知識（初心者向け版）を1つずつ説明しました。近年、エンジン自体、ハードウェアデバイス、製作基準などのアップグレードに伴って、UWAは引き続き規則や方法を更新して、各開発者に提供しつつあります。「アップグレード版」のパフォーマンス最適化マニュアルとして、**【Unityパフォーマンス最適化シリーズ】**はより多くの開発者が利用できるように、シンプルでわかりやすい表現を心がけています。今回、UIモジュールに関連する知識を共有します。
エンジンでは、主流のUIフレームワークには、UGUI、NGUI、およびますます使用されるFairyGUIが含まれます。 この記事では、主に最もよく使用されるUGUIから説明します。 この記事では、CPU時間のメモリ割り当てやGPUへの影響から、UIの最適化について説明します。

---
##一、CPU時間について　　　　　　　　　　　　　　　　　

次の図は、一般的なケースとして、GOT Onlineでテストを行なったプロジェクトにおけるUGUI時間配布スタックです。Unity 2019バージョンより前は、Canvas.SendWillRenderCanvasesのあらゆる時間コストがLayoutにカウントされていました。
![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/11674a86-e367-5612-8af1-6b52f30bb7b2.png)
Unity 2019のバージョンになると、少し異なって、時間コストは以下のように、LayoutとRenderと二つの部分に分けられます。
![2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/28204e28-5cda-b5ee-7e23-02b998f3dd90.png)
次に、これらのスタックのCanvas.SendWillRendererCanvses、Canvas.BuildBatchおよびSyncTransformについて話しましょう。
###1.Canvas.SendWillRenderCanvases

この関数の時間コストは、UI要素自体の変更によって更新された時間コストを表し、これはCanvas.BuildBatch（以下を参照）のメッシュ再構築の時間と区別しており、UI更新時間として理解できます。
![3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/9f5b3374-70f0-80c3-53bf-80eaa3bcc317.png)
UI要素自体の更新には、画像の置き換え、テキストや色の変更などが含まれます。UI要素の変位、回転、またはスケーリングによって、この関数にオーバーヘッドが発生することはありません。この関数の時間コストは、やはりUI要素の更新数とUI要素の複雑さによって異なります。したがって、この関数のコストを最適化するには、通常、次の点から取り組みます。
####1）頻繁に更新されるUI要素の頻度を減らす

たとえば、ミニマップのモンスターマーク、キャラクターや生命値などは、ロジックの変化が特定のしきい値を超えたときにUI表示を更新することを制御します。また、スキルCD効果、ダメージフローティング文字などのような　フレームおきに更新することを制御します。
####2）複雑なUIが変更されないようにしてください

たとえば、一部の文字列は非常に多くて、さらにRich Text、OutlineまたはShadow効果のText、Image TypeがTiledであるImageなどを使用するようなUI要素には非常に多くの頂点があるため、一度更新すると時間がかかります。一部の効果にOutlineまたはShadowmapを使用する必要があるが、頻繁に変更する必要もあります。その場合に、例として、フローティングのダメージ数字を固定の芸術的文字にすることで、頂点の数がN倍になることを避けられます。
####3）Font.CacheFontForTextに注目してください

この関数は、時間コストの最大値を引き起こす傾向があります。このAPIは、主に動的フォントFont Textureのコストを引き起こします。実行中の突発な高時間コストは、一度に多くの新しい文字が書き込まれることにより、Font Textureのテクスチャ拡張を引き起こすことに由来するかもしれません。それで、フォントの種類を減らしたり、フォントサイズを小さくしたり、常用文字を事前に表示して動的フォントのFontTextureを拡張したりすることでこの項の時間コストを最適化できます。
![4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/d9c0474b-396e-2c29-ec14-4f21d389f972.png)


###2.BuildBatch＆EmitWorldScreenspaceCameraGeometry

Canvas.BuildBatchは、UI要素によって合併されたMeshを変更する必要があるときに発生したコールです。通常、前述のCanvas.SendWillRenderCanvases()をコールすると、Canvas.BuildBatchがのコールを引き起こします。さらに、Canvas内のUI要素の移動により、Canvas.BuildBatchもコールされます。
![5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/c58116a4-0cd6-2400-5677-0d707ac9f048.png)
Canvas.BuildBatchは、メインスレッドでUIメッシュのマージを開始して、具体的なマージプロセスは子スレッドで処理されたのです。子スレッドのプレッシャーが高すぎる場合、またはマージされたUIメッシュが複雑すぎる場合、メインスレッドで待機が発生します。待機時間はEmitWorldScreenspaceCameraGeometryにカウントします。
 
これらの2つの関数は高い時間コストが発生するのは、Canvasの再構築が非常に複雑であることを示しています。このとき、Canvasを細分化する必要があります。通常、静的要素は一つのCanvasに配置され、更新されたUI要素は一つのCanvasに配置されます。このように、静的Canvasはキャッシュのためにメッシュを更新しないため、メッシュ更新の複雑さが軽減され、メッシュ再構築の時間が削減されます。
###3.SyncTransform

CanvasRenderer.SyncTransformは、一部のプロジェクトの一部のフレームで頻繁にコールされることに注意してください。下図では、CanvasRenderer.SyncTransformが1017回までにコールされます。Canvas.SyncTransformが非常に頻繁にトリガーされると、その親ノードUGUI.Rendering.UpdateBathesに非常に高い時間コストを引き起こします。
Unity 2018及びその以降のバージョンでは、Canvasの下のUI要素に対してSetActive（falseをtrueに変更）をコールすると、Canvasの下の他のUI要素がSyncTransformをトリガーし、UI更新の全体的なコストが増加します。Unity2017では、UI要素自体がSyncTransformをトリガーするだけになります。
![6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/6d0d586f-acdd-c5a3-908b-057263b506d1.png)


したがって、UI要素（ImageやTextなど）の多くのCanvasに対し、いくつかのUI要素が頻繁にSetActiveであるかどうかに注意する必要があります。この状況では、SetActive（falseまたはtrue）の代わりにSetScale（0または1）を使用することをお勧めします。または、Canvasを適切に分割して、SetActive（true）である必要のある要素と他の要素が同じCanvasの下にないようにして、SyncTransformが頻繁にコールされないようにすることもできます。

###4.EventSystem.Update

####1）トリガーコールの時間コストが高い
この関数は、タッチを離したときにトリガーされます。関数自体に高いCPUコストがあるのは、通常、他のより時間のかかる関数をコールすることが原因です。したがって、トリガーされたロジックをさらに検出するには、Profiler.BeginSample / EndSampleドットまたはGOT Onlineサービス+UWA APIドットを追加する必要があります。
![7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/723a5cce-0dce-1aac-27a1-405c8d4cbb70.png)


####2）ポーリングの時間コストが高い
すべてのUGUIコンポーネントでは、作成時にデフォルトでRaycast Targetオプションがオンになっており、実際にはイベント応答を受け入れる準備ができています。実際、Image、TextなどのUIコンポーネントはほとんどイベント応答に参加しませんが、マウス/指をスワイプまたはホバーしたときにポーリングに参加します。だから、アナログ光線検出を通して、UIコンポーネントがホバーやスワイプされたかどうかを判断して、不必要な時間コストを引き起こします。特にプロジェクトのUIコンポーネント多くある場合、イベント応答に参加していないコンポーネントのRaycast Target設定をオフにすると、EventSystem.Update()の時間コストを効果的に減らすことができます。
![9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/cf972511-7fd5-d987-6d9a-089525ded6d7.png)
###5.UI DrawCall
通常、戦闘シーンの他のモジュールは時間コストのプレッシャーが大きいので、そしてUIモジュールにはパフォーマンスのコストを注意深く制御する必要があります。一般的に、戦闘シーンのUIDrawCallを約40〜50に制御するのが最適です。
![10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/8bcab419-3d4d-9356-5ef3-2c50830172c5.png)
UI要素を削減しないという前提で、DrawCallを制御する問題は、つまりUI要素を可能な限りバッチ化させる方法でもあります。一般的なバッチ処理には同じマテリアルが必要ですが、UIでは、同じマテリアル、同じアトラスで作成されたUI要素でもバッチ処理できないことがよくあります。これは、UGUIDrawCallの計算原理に関連しています。関連コースは以下を参照。「UGUIDrawCall計算とRebuild操作の最適化」（中国語注意）
 
製作中、以下の点に注意しましょう。
（1）同じCanvasにあるUI要素のみがバッチ処理できます。異なるCanvasには、Order in Layerが同じであっても、バッチ処理することはできません。そのゆえ、UIの合理的な計画と作成は非常に重要です。
（2）異なるUI要素のマテリアルアトラスが一貫するように、アトラスを統合して作成するようにしてください。アトラスのボタンやアイコンなどは、画像の比較的小さなUI要素を使用したらそれができます。それらが同時に密に表示される場合、DrawCallを効果的に削減します。
（3）同じCanvas、同じマテリアル・アトラスを前提として、階層的なインターリーブを避けしてください。一言にいえば、バッチ処理条件を満たすUI要素の「レベルの深さ」は同じである必要があります。
（4）該当するUIのPos Zを一律に0に設定してみてください。 Z値が0でないUI要素は、Hierarchy内の隣接する要素とのみバッチ処理できるため、バッチ処理は中断されやすくなります。
（5）アルファが0の画像の場合、CanvasRenderコンポーネントの[透明メッシュのカル]オプションをチェックする必要があります。チェックしないと、DrawCallが生成されたままになり、バッチ処理が簡単に中断されます。
Alphaが0であるImageに対して、CanvasRenderコンポーネントのCull Transparent Meshを選択して、そうしないとDrawCallが発生し、バッチ処理も簡単に中断されます。

---
##二、メモリ
1）一般に、UGUI自体によって割り当てられるmonoメモリは非常に小さいため、サードパーティのプラグインや、自分で書いたUIコンポーネントに注目してください。例えば、人気のあるUIParticlesはUIと特殊効果に階層的な管理を実現させるために作ったプラグインです。しかし、それはParticleSystemのMaxParticlesの数で初期化されるため、その数値に特に注意してください。
 
下図のように、開発者によって作成されたUIコンポーネントMeshImageは、大量のmonoメモリを生成しているため、それに対する最適化が必要になるます。
![12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/90014d24-f76b-2a7e-fc47-ce39d5a6b5ec.png)
2）アトラスをマージし、同じアトラスを2つ以下のAtlasにしようとします。そうしないと、このアトラス内のすべてのSprite子アトラスがメモリにロードされ、最後的に、全てのAtlasがメモリにロードされます。
 
3）UGUIのGC最適化
この点について、「UnityのGC最適化の原理と実践」（中国語注意）を参照してください。
 
他の記事で詳しく説明されていますから、ここでは見落としやすい点だけを提示します。つまり、Prefabには大量の空Textがある場合、初期化中に深刻なGC Allocが発生します。これは、初期化時にTextGeneratorが最初に初期化されるためです。Textが空であると、50文字、つまり50文字のUIVertexと50文字のUICharInfoによって空にならないように初期化されます。または空欄に記入して整理してもいいです。

---
##三、GPU

1）フルスクリーンUIを開いているときは、バックグラウンドでブロックされている他のUIを閉じることをお勧めします。
![14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/51c1e946-9273-4682-25a3-aac6a601acb2.png)

2）Alphaが0のUIの場合、Canvas RendererコンポーネントのCullTransparent Meshを選択することをお勧めします。これなら、レンダリングせずにUIイベントの応答することが保障されます。
![15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/d73d9614-1a4f-2e41-3a06-d3b55a56974c.png)
3）Maskコンポーネントの使用を可能な限り最小限に抑えます。これより、描画のコストが増加するだけでなく、DrawCallも増加します。Overdrawが高い場合は、代わりにRectMask2Dの使用を検討できます。
 
4）URPでは、不要なCopy ColorまたはCopy Depthがないかに注意する必要があります。特に、UIと戦闘シーンのカメラが同じRendererPipelineAssetを使用している場合、不要なレンダリング時間と帯域幅の浪費が発生しやすく、GPUに不要なコスとが発生します。通常、UIカメラとシーンカメラに異なるRendererDataを使用することをお勧めします。

---
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