
Unity UI の最適化
Tutorial
advanced
+0XP
60 mins
(9)
Unity Technologies

Unity UI によって駆動されるユーザーインターフェースを最適化することは、芸術的なことです。このチュートリアルでは、Unity UI の基盤となる基本的なコンセプト、アルゴリズム、コードについて説明するとともに、よくある問題とその解決策についても触れています。
Languages available:
1. Unity UI の最適化ガイド
Unity UI によって実装されたユーザーインターフェースの最適化はもはや芸術です。厳しいルールは殆どないですが、代わりにシステムの挙動を念頭に置いて、各状況を慎重に判断する必要があります。 Unity UI を最適化する際に最も重要なのは、ドローコールとバッチングコストのバランスをとることです。 一方または両方を減らす為には一般的なテクニックを活用する事が出来ますが、複雑な UI の場合はトレードオフを行わなければなりません。
ただし、他のトピックでのベストプラクティスと同様に、Unity UI を最適化する際もプロファイリングから始めるべきです。Unity UI システムを最適化しようとする際は、まず、現在発生している問題の正確な原因を突き止めることが必要です。Unity UI のユーザーが遭遇する問題には、共通する以下の4つがあります:
- 過大な GPU フラグメントシェーダの利用率 (フィルレート制限超えなど)
- 過大な Canvas バッチのリビルドに費やされたCPU時間
- 過大な Canvas バッチのリビルド数 (頻繁なキャッシュ更新、over-dirtying)
- 過大な 頂点生成に費やされた CPU 時間 (ほとんど原因はテキスト)
原則として、GPUへのドローコール数によってパフォーマンスが低下しても、Unity UI を生成する事は可能です。ただし、実際にはドローコールが増え GPU に過負荷が掛かるプロジェクトの場合、フィルレートの制限を超えてしまう可能性が高くなります。
このガイドでは、Unity UI の根底にある基本的な概念・アルゴリズム・コードについて触れ、またよくある問題や解決策について、5 つのチャプターに分けて説明します。
- Unity UI の基礎のチャプター:Unity UI 固有の用語を定義し、バッチジオメトリの構築など、UI をレンダリングするために実行される多くの基本的なプロセスの詳細について説明します。このチャプターから読み始めることを強くおススメします。
- Unity UI のプロファイリングツールのチャプター:開発者が利用できる様々なツールを使ってプロファイリングデータを収集する方法について説明します。
- フィルレート (Fill Rate)、キャンバス (Cavas)、入力 (input) のチャプター:Unity UI の Canvas と Input コンポーネントのパフォーマンスを向上させる方法について説明します。
- UI コントロールの最適化のチャプター:UI Text、Scroll Views、その他のコンポーネント固有の最適化について説明し、これまでの項目で紹介できなかったテクニックも紹介しています。
- その他の UI 最適化のテクニックとヒントのチャプター:UI システムでの「落とし穴」に関する基本的なヒントと回避策など、これまでのチャプターでは説明出来なかった問題について説明しています。
UI ソースコード
Unity UI の Graphic コンポーネントと Layout コンポーネントは完全にオープンソースであることを常に忘れないでください。これらのソースコードは、 Unity の Bitbucket リポジトリの UI で確認することができます。
2. Unity UI の基礎
Unity UI システムを構成するさまざまなパーツを理解することは重要です。いくつかの基本的なクラスとコンポーネントがあり、それらが組み合わさりシステムを構成しています。このチャプターでは、まずこのシリーズを通して使用される用語を定義し、次に Unity UI のキーとなるシステムについて、低レベル層での動作について説明します。
用語
Canvas(キャンバス):ネイティブコードの Unity コンポーネントで、Unity のレンダリングシステムで階層化されたジオメトリを提供する為に使用されます。そしてゲームのワールド空間の中、又は前面に描画されます。
Canvases は、それらの構成ジオメトリをバッチにまとめ、適切なレンダリングコマンドを生成し、Unity の Graphics システムに送信する役割を担っています。これらの作業はすべて C++ で書かれたネイティブコードで行われ、リバッチ (rebatch) またはバッチ (batch) ビルドと呼ばれます。Canvas に再配置が必要なジオメトリが含まれているとマークされているとき、その Canvas はダーティ (dirty) であると表現します。
ジオメトリは Canvas Renderer コンポーネントによって Canvas に提供されます。
Sub-canvas:単純に Canvas コンポーネント内にネストされた別の Canvas コンポーネントの事をいいます。Sub-canvas は 親 Canvas とは分離されているため、ダーティな Sub-canvas は親 Canvas にジオメトリを再構築させることはありません。逆もまた同様です。ただし、親 Canvas を変更すると Sub-canvas のサイズが変更されるなど、これが当てはまらないケースもあります。
Graphic:Unity UI C#ライブラリによって提供されるクラスであり、Canvas システムに描画可能なジオメトリを提供する全ての Unity UI C# クラスの基底クラスになっています。ビルトインの Unity UI Graphics の殆どは、 MaskableGraphic のサブクラスを介して実装されています。このサブクラスを使用すると、IMaskable インターフェイスを介してマスクすることができます。Drawable の主なサブクラスとして Image と Text があり、それぞれ同名のコンポーネントを提供します。
Layout コンポーネント:RectTransform のサイズと位置の制御を担っており、相対的なサイズ変更や位置変更を必要とする複雑なレイアウトを作成する為に使用されます。Layout コンポーネントは、RectTransform にのみ依存し、関連する RectTransform プロパティにのみ影響を与えます。それらは Graphic クラスには依存せず、Unity UI の Graphic コンポーネントとは独立して使用できます。
Graphic コンポーネントと Layout コンポーネント:このクラスは Unity エディターのインターフェースでは公開されていない CanvasUpdateRegistry クラスに依存しています。このクラスは更新が必要な Layout コンポーネントと Graphic コンポーネントのセットを追跡し、関連付けられた Canvas が willRenderCanvases イベントを呼び出すときに、必要に応じて更新をトリガーします。
Layout コンポーネントと Graphic コンポーネントの更新は、リビルド と呼ばれます。リビルドプロセスについては、このドキュメントの後半でさらに詳しく説明します。
レンダリングの詳細
Unity UI でユーザーインターフェースを作成する際には、Canvas で描画されたジオメトリはすべて Transparent キューに描画されることに留意してください。つまり、Unity UI によって生成されたジオメトリは、常に Alpha Blending (アルファブレンディング)が行われ奥から手前に順番に描画されます。パフォーマンスの観点から覚えておくべき重要なことは、ポリゴンからラスタライズされた各ピクセルが、たとえそれが他の不透明なポリゴンで完全に覆われていてもサンプリングされるということです。モバイルデバイスでは、このような高レベルのオーバードローは、GPU のフィルレート容量を急速に上回る可能性があります。
バッチビルドプロセス (Canvas)
バッチビルドプロセス (Batch building process) とは、Canvas が UI 要素を表すメッシュを結合し、Unity のグラフィックスパイプラインに送る適切なレンダリングコマンドを生成するプロセスです。この処理の結果はキャッシュされ、Canvas がダーティであるとマークされるまで再利用されます。これは、キャンバスを構成するメッシュの1つにでも変更がある度に発生します。
Canvas で使用されるメッシュは、Canvas にアタッチされた Canvas Renderer コンポーネントのセットから取得されますが、Sub-canvas は含まれません。
バッチを計算するにはメッシュを深さ (depth) でソートし、重なりや共有されているマテリアルなどを調べる必要があります。この処理はマルチスレッドで行われるため、CPU アーキテクチャによってパフォーマンスが大きく異なります。特に、CPU コア数の少ないモバイル SoC と 4 コア以上の最新のデスクトップ CPU との間には大きな違いがあります。
リビルドプロセス (Graphics)
リビルドプロセスでは、Unity UI の C# Graphic コンポーネントの Layout とメッシュが再計算されます。これは CanvasUpdateRegistry クラスで実行されます。これは C# のクラスであり、そのソースは Unity の Bitbucket で公開されています。
具体的には CanvasUpdateRegistry クラスの PerformUpdate メソッドが担っており、このメソッドは Canvas コンポーネントが WillRenderCanvases イベントを発生させるたびに呼び出されます。このイベントは1フレームにつき1回呼び出されます。
PerformUpdate メソッドは3つのプロセスを実行します:
- ダーティな Layout コンポーネントは、レイアウトのリビルドを ICanvasElement.Rebuild メソッドを通して要求されます。
- 登録された Clipping コンポーネント (Mask など) は、クリップされたコンポーネントをカリングするよう要求されます。これは、ClippingRegistry.Cull メソッドによって行われます。
- ダーティな Graphic コンポーネントは、グラフィカル要素のリビルドを要求されます。
Layout と Graphic のリビルドの場合、プロセスは複数のパートに分かれています。Layout のリビルドは 3 つ(PreLayout、Layout、PostLayout)で実行され、Graphic のリビルドは 2 つ(PreRender、LatePreRender)で実行されます。
Layout のリビルド
1 つまたは複数の Layout コンポーネントに含まれるコンポーネントの適切な位置(および潜在的なサイズ)を再計算するには、適切な階層順に Layout を適用する必要があります。ゲームオブジェクト階層のルートに近い Layout は、自身が含んでいる Layout の位置とサイズを変更する場合がある為最初に計算する必要があります。
これを行うために、Unity UI はダーティな Layout コンポーネントのリストを、階層の深さでソートします。階層の上位アイテム (つまり、親の Transform コンポーネントが少ない)は、リストの先頭に移動されます。
ソートされた Layout コンポーネントリストは、次に Layout をリビルドするよう要求されます。ここは、Layoutコンポーネントによって制御されている UI 要素の位置とサイズが実際に変更される場所です。個々の要素の位置がLayout によってどの様に影響を受けているかについては UI Auto Layout を参照してください。
Graphic のリビルド
Graphic コンポーネントがリビルドされると、Unity UI は ICanvasElement インターフェースの Rebuild メソッドに制御を渡します。Graphic クラスはこれを実装し、リビルドプロセスの PreRender ステージで 2 つの異なるリビルド ステップを実行します。
- 頂点データがダーティとマークされていた場合(例:コンポーネントの RectTransform のサイズが変更されたとき)は、メッシュがリビルドされます。
- マテリアルデータがダーティとマークされていた場合(コンポーネントのマテリアルやテクスチャが変更された場合など)、アタッチされた Canvas Renderer のマテリアルが更新されます。
Graphic のリビルドは、Graphic コンポーネントのリストを特定の順序で処理しないため、ソート操作を必要としません。
3. Unity UI プロファイリングツール
のパフォーマンスを分析するために使用できる便利なプロファイリングツールがいくつかあります。主なツールは次のとおりです:
- Unity プロファイラー
- Unity フレームデバッガー
- Xcode のツールまたは Intel VTune
- Xcode のフレームデバッガーまたは Intel GPA
外部ツールは、ミリ秒(またはそれ以上)の解像度のメソッドレベルの CPU プロファイリング、および詳細なドローコールやシェーダーの詳細なプロファイリングも可能です。上記のツールを設定して使用する手順は、このガイドの範囲を超えています。Xcode フレームワークデバッガーとツールは Apple プラットフォームでの IL2CPP ビルドにのみ使用できるため、現時点では IOS ビルドのプロファイリングにのみ使用できることに注意してください。
Unity プロファイラー
Unity プロファイラー (Profiler) の主な目的は、比較プロファイリングを実行することです。Unity プロファイラーの実行中に UI 要素を有効にしたり無効にしたりすることで、パフォーマンスの問題に最も関与する UI 階層の部分を素早く絞り込むことができます。
これを分析するには、 アナライザー出力の Canvas.BuildBatch と Canvas.SendWillRenderCanvases の行を確認します。
前述のように、Canvas.BuildBatch は、Canvas バッチビルドプロセスを実行するネイティブコード計算です。
Canvas.SendWillRenderCanvases には、サブスクライブされた Canvas コンポーネントの willRenderCanvases イベントを呼び出す C# スクリプトが含まれています。Unity UI の CanvasUpdateRegistry クラスはこのイベントを受け取り、前述のようにそれを使ってリビルドプロセスを実行します。ダーティな UI コンポーネントは、この時点で Canvas Renderers を更新することが予想されます。
注:UI パフォーマンスの違いをより簡単に確認するには、通常、「レンダリング」、「スクリプト」、「UI」を除くすべてのトラッキングカテゴリーを無効にすることをお勧めします。これは、CPU Usage プロファイラーの左側にある追跡カテゴリー名の横にある色付きのボックスをクリックすることで実行できます。また、カテゴリー名をクリックして上下にドラッグすることで、CPU プロファイラー内でカテゴリーの順序を変更することができます。
UI カテゴリーは、Unity 2017.1 以降で新しくなりました。残念ながら、UI の更新プロセスの一部は適切に分類されていないため、UI 曲線を見るときは注意してください。これには、UI 関連の呼び出しがすべて含まれているとは限りません。たとえば、Canvas.endWillRenderCanvases は「UI」に分類され、Canvas.BuildBatch は「その他」と「レンダリング」に分類されます。
2017.1 以降では、新しい UI Profiler も登場しています。デフォルトでは、このプロファイラーは Profiler ウィンドウの最後に表示されます。これは、2 つのタイムラインとバッチビューアで構成されています:
最初のタイムラインは、計算レイアウトとレンダリングという 2 つのカテゴリーに使用される CPU 時間を示しています。前述と同じような問題が発生し、一部の UI 機能が考慮されていない可能性があることに注意してください。
2 番目のタイムラインには、バッチ、頂点、およびイベントマーカーの総数が表示されます。前のスクリーンショットでは、いくつかのボタンクリックイベントを見ることができます。これらのマーカーは、CPU 使用率の急上昇の原因を特定するのに役立ちます。
最後に、UI プロファイラーの最も便利な機能は、下部にある Batch ビューアです。左側にはすべての Canvas のツリービューがあり、各 Canvas の下には、それらが生成した Batch (バッチ) のリストが表示されています。これらの列には、各 Canvas や Batch に関する興味深い詳細を提供しますが、中でも UI の最適化方法をよりよく理解するために欠かせないものがあります。それが Batch Breaking Reason です。
この列には、選択したバッチが前のバッチにマージできなかった理由が表示されます。バッチの数を減らすことは、UI のパフォーマンスを向上させる最も効果的な方法の 1 つであるため、バッチ処理ができない原因を理解することが重要です。
スクリーンショットに示されているように、最も頻繁に起こる理由の一つは、さまざまなテクスチャまたはマテリアルを持つ UI 要素の使用です。多くの場合、これはスプライトアトラスを使用することで簡単に修正することができます。最後の列には、そのバッチに関連付けられているゲームオブジェクトの名前が表示されます。名前をダブルクリックすると、そのゲームオブジェクトをエディターで選択することができます(同じ名前のオブジェクトが複数ある場合には特に便利です)。
Unity 2017.3 以降、バッチビューアーはエディターでしか使えません。バッチ処理は通常、デバイス上で同じであるべきなので、これは今でもとても便利です。もし、デバイス上でバッチが違うのではないかという疑問がある場合は、次に説明するフレームデバッガー (Frame Debugger) を使うことができます。
Unity フレームデバッガー
Unity フレームデバッガー (Frame Debugger) は、Unity の UI によって生成されるドローコールの数を減らすための便利なツールです。この組み込みツールは、Unity エディター内の Window メニューからアクセスできます。有効にすると、Unity UI で生成されたものを含め、Unity で生成されたすべてのドローコールが表示されます。
また、フレームデバッガーは、Unity エディターの Game ビューを表示するために生成されたドローコールで更新されるので、Play モードに入らなくても、様々な UI 設定を試すことができます。
Unity UI のドローコールの位置は、描画される Canvas コンポーネントで選択されている Render モードによって異なります:
- Screen Space – Overlay は Canvas.RenderOverlays グループに表示されます。
- Screen Space – Camera は、選択した Render Camera の Camera.Render グループ内に、Render.TransparentGeometry のサブグループとして表示されます。
- World Spaceは、Canvas が表示されている World Space カメラごとに、Render.TransparentGeometry のサブグループとして表示されます。
すべての UI は、グループまたはドローコールの詳細にある「Shader: UI/Default」の行で識別できます(UI シェーダーがカスタムシェーダーに置き換えられていないことが前提です)。下のスクリーンショットでは、強調表示されている赤いボックスをご覧ください。
この一連の行を見ながら UI を微調整すると、比較的簡単に Canvas の能力を最大限に発揮して UI 要素をバッチにまとめることができます。バッチが壊れてしまう設計上の最も一般的な原因は、意図せずに重なってしまうことです。
Unity のすべての UI コンポーネントは、そのジオメトリを一連の Quad (クアッド) として生成します。ただし、多くの UI スプライトや UI テキストグリフは、それらを表現するために使用されるクアッドのごく一部しか占めておらず、残りは空のスペースとなっています。その結果、UI デザイナーが誤ってテクスチャの素材が異なる複数のクアッドを重ねてしまい、バッチ処理ができなくなってしまうことがよくあります。
Unity UI は完全に透過キューで動作するため、その上にオーバーレイされた不安定なクアッドは、壊れないクアッドの前に描画する必要があります。したがって壊れないクアッドに配置された他のクアッドとバッチ処理することはできません。
A、B、C の 3 つのクアッドの場合を考えてみましょう。これらの 3 つのクアッドが互いに重なり合っていると仮定し、クアッド A と C が同じマテリアルを使用し、クアッド B が別のマテリアルを使用していると仮定します。そのため、クアッド B はA や C とバッチ処理することはできません。
階層の順番が上から順に A、B、Cとなっている場合、Bは A の上、C の下に描画する必要があるため、A と C をバッチ処理することはできません。ただし、B がバッチ可能なクアッドの前または後に配置されている場合、バッチ可能なクアッドは実際にバッチ処理できます。B はバッチ可能なクアッドの前か後に描画するだけでよく、挿入する必要はありません。
この問題については、Canvas の章、Child order セクションを参照してください。
Instruments & VTune
Xcode の Instruments と Intel の VTune により、それぞれ Apple や Intel CPU で Unity UI のリビルドや Canvas のバッチ計算を極めて詳細にプロファイリングすることができます。このメソッド名は、前述の Unity Profiler セクションで説明したプロファイラーのラベルとほぼ同じです:
- Canvas::SendWillRenderCanvases は、Canvas.SendWillRenderCanvases C# メソッドを呼び出し、Unity プロファイラーでその行を管理する C ++ の親です。このコードには、前のステップで説明した Rebuild プロセスを実行するためのコードが含まれています。
- Canvas::UpdateBatches は Canvas.BuildBatch と同じですが、Unity プロファイラーラベルではカバーされていない追加の定型コードが含まれています。これは、上記の実際の Canvas Batch Building プロセスを実行します。
IL2CPP を介してビルドされた Unity アプリと組み合わせて使用すると、これらのツールを使って、Canvas::SendWillRenderCanvases のトランスパイルされた C# コードをより深く掘り下げることができます。主な関心事は、以下の方法のコストです。(注:トランスパイルされたメソッド名は概算です。)
- IndexedSet_Sort および CanvasUpdateRegistry_SortLayoutList は、レイアウトが再計算される前に、ダーティな Layout コンポーネントのリストをソートするために使用されます。上記のように、これには各 Layout コンポーネントの上にある親トランスフォームの数を計算します。
- ClipperRegistry_Cull は、IClipRegion インターフェースの登録済み実装者すべてを呼び出します。組み込みの Implementers には,IClippable インターフェースを使用する RectMask2D が含まれます。ClipperRegistry.Cull の呼び出し中、RectMask2D コンポーネントは、その階層内に含まれるすべてのクリッ可能な要素をループし、それらのカリング情報を更新するように要求します。
- Graphic_Rebuild には、Image、Text、その他の Graphic から派生したコンポーネントを表現するために必要なメッシュを実際に計算するコストが含まれます。この下には、Graphic_UpdateGeometry や、最も注目すべき Text_OnPopulateMesh など、いくつかのメソッドがあります。
- Text_OnPopulateMesh は、通常 Best Fit モードが有効な場合、ホットスポットになります。これについては、このガイドの後半で詳しく説明します。
- Shadow_ModifyMesh や Outline_ModifyMesh などのメッシュ修飾子もここで実行されます。コンポーネントのドロップシャドウ、アウトライン、その他の特殊効果を計算するコストは、これらの方法で確認できます。
Xcode フレームデバッガー & Intel GPA
低レベルのフレームデバッグツールは、バッチ処理された UI の個々の部分のコストをプロファイリングしたり、UI のオーバードローのコストを監視したりするために不可欠です。UI のオーバードローについては、このガイドの後半で詳しく説明します。
Xcode フレームデバッガーの使用
特定の UI が GPU に過度の負担をかけているかどうかをテストするには、Xcode の組み込み GPU 診断ツールを使用することができます。まず、対象となるプロジェクトを Metal または OpenGLES3 を使用するように設定し、ビルドを作成してできた Xcode プロジェクトを開きます。Xcode のバージョンやデバイスの組み合わせによっては、OpenGLES 2 のフレームキャプチャーをサポートしている場合がありますが、それが動作するという保証はありません。
注:Xcode の一部のバージョンでは、Graphics プロファイラーを動作させるために、Build Scheme で適切なグラフィックス API を選択する必要があります。これを行うには、Xcode の Product メニューで Scheme メニュー項目を展開し、Edit Scheme.... を選択します。Run ターゲットを選択し、Options タブに移動します。GPU Frame Capture オプションを、プロジェクトで使用されている API に合わせて変更します。Unity プロジェクトがグラフィックス API を自動的に選択するように設定されている場合、ほとんどの最新の iPad ではデフォルトで Metal が使用されます。疑わしい場合は、プロジェクトを起動して、Xcode のデバッグログを確認してください。初期の行の 1 つに、どのレンダリングパス(Metal、GLES3、GLES2)が初期化されているかが示されているはずです。
iOS デバイスでプロジェクトをビルドして実行します。GPUプロファイラーは、Xcode の Navigator サイドバーにある Debug ペインを表示し、FPS の Entry をクリックすることで確認できます。
GPU プロファイラーの最初の注目点は、画面中央にある「Tiler」、「Renderer」、「Device」と書かれた 3 本のバーのセットです。このうちの2つは:
- 「Tiler」は通常、ジオメトリを処理する際の GPU の負荷を示す指標で、頂点シェーダーにかかる時間も含まれます。一般的に、「Tiler」の使用率が高いということは、頂点シェーダーの処理速度が極端に遅いか、描画される頂点の数が多すぎることを示しています。
- 「Renderer」は通常、GPU のピクセルパイプラインにかかる負荷を示す指標です。一般に、「Renderer」の使用率が高い場合、アプリケーションが GPU の最大フィルレートを超えているか、フラグメントシェーダが非効率的であることを示しています。
- 「Device 」は、「Tiler」と「Renderer」の両方のパフォーマンスを含む、GPU 全体の使用量の複合尺度です。通常は、「Tiler」と「Renderer」のどちらか測定値の高い方を大まかに追跡するため、無視しても構いません。
Xcode の GPU プロファイラーの詳細については、こちらのドキュメント記事をご覧ください。
Xcode のフレームデバッガー (Frame Debugger) は、GPU プロファイラーの下部に隠れている小さな「Camera」アイコンをクリックすることでトリガーできます。次のスクリーンショットでは、矢印と赤いボックスで強調表示されています。
少し間を置くと、フレームデバッガーの概要ビューが以下のように表示されます:
デフォルトの UI シェーダーを使用する場合、Unity UI システムで生成されたジオメトリのレンダリングコストは、デフォルトの UI シェーダーがカスタムシェーダーで置き換えられていないことを前提に、「UI/Default」シェーダーパスの下に表示されます。上のスクリーンショットでは、Render Pipeline "UI/Default"として、このデフォルトの UI シェーダーを見ることができます。
Unity UI はクアッドのみを生成するため、頂点シェーダーが GPU の Tiler パイプラインに負荷を与えることはありません。このシェーダーパスで問題が発生した場合は、フィルレートの問題が原因と考えられます。
プロファイラー結果の分析
プロファイリングデータを収集した後、いくつかの結論を導き出すことができます。もし、Canvas.BuildBatch や Canvas::UpdateBatches が過剰に CPU 時間を消費しているようであれば、単一の Canvas 上にある Canvas Renderer コンポーネントの数が多すぎることが問題であると考えられます。Canvas の章の「the Splitting Canvases」セクションを参照してください。
GPU で UI を描画するのに過度の時間が費やされ、フレームデバッガーでフラグメントシェーダーパイプラインがボトルネックであることを示している場合、UI は GPU が可能なピクセルフィルレートを超えている可能性があります。最も考えられる原因は、過剰な UI のオーバードローです。フィルレート、キャンバス、入力の章の「the Remediating fill-rate issues」セクションを参照してください。
Canvas.SendWillRenderCanvases または Canvas :: SendWillRenderCanvases に行く CPU 時間の大部分に見られるように、Graphic Rebuilds が過剰なCPUを使用している場合は、より詳細な分析が必要です。Graphic Rebuilds プロセスの一部が原因である可能性があります。
WillRenderCanvas の大部分が IndexedSet_Sort や CanvasUpdateRegistry_SortLayoutList の中で使われている場合、ダーティな Layout コンポーネントのリストのソートに時間が費やされています。 Canvas 上の Layout コンポーネントの数を減らすことを検討してください。可能な修正方法については、「Replacing layouts with RectTransforms and Splitting Canvases」と「Splitting Canvases 」セクションを参照してください。
Text_OnPopulateMesh に過剰な時間がかかっているようであれば、原因は単純にテキストメッシュの生成にあると考えられます。考えられる修正方法については、「Best Fit 」および「Disabling Canvases」セクションを参照してください。また、リビルドされるテキストの多くが実際にはその基礎となる文字列データを変更していない場合は、「Splitting Canvases」のアドバイスを検討してください。
Shadow_ModifyMesh や Outline_ModifyMesh(または ModifyMesh の他の実装)内で時間が費やされている場合、問題はメッシュモディファイアの計算に過剰な時間がかかっていることです。これらのコンポーネントを削除して、静止画像で視覚効果を得ることを検討してください。
Canvas.SendWillRenderCanvases 内に特定のホットスポットがない場合や、毎フレーム実行されているように見える場合は、動的要素が静的要素と一緒にグループ化され、Canvas 全体を頻繁にリビルドさせていることが問題であると考えられます。「Splitting Canvases」のセクションを参照してください。
4. フィルレート / キャンバス / 入力
この章では、Unity UI を構築する上での幅広い問題について説明します。
フィルレートに関する問題の解決
GPUのフラグメント処理のパイプラインにかかる負荷を軽減するには、2 つの方法があります:
- フラグメントシェーダーの複雑さを軽減します。詳細は「UI shaders and low-spec devices」のセクションを参照してください。
- サンプリングする必要のあるピクセルの数を減らします。
UI シェーダーは一般的に標準化されているので、最も一般的な問題は、単純にフィルレートの過度の使用です。これの最も一般的な理由は、画面の重要な部分を占める多数の重複する UI 要素、または複数の UI 要素です。これらの問題は、どちらも非常に高いレベルのオーバードローにつながります。
フィルレートの過度の利用を緩和し、オーバードローを減らすために、以下のような改善策を検討してください。
非表示の UI の排除
既存の UI 要素の再設計が最も少ない方法は、プレイヤーに見えない要素を単純に無効化することです。 これが適用される最も一般的なケースは、背景が不透明なフルスクリーン UI を開く場合です。この場合、フルスクリーン UI の下に配置された UI 要素を無効にすることができます。
これの最も簡単な方法は、非表示の UI 要素を含むルートゲームオブジェクトか、ゲームオブジェクトを無効にすることです。代替ソリューションについては、「Disabling Canvases」セクションを参照してください。
最後に、UI 要素の alpha を 0 に設定して、UI 要素が非表示になっていないことを確認します。要素は引き続き GPU に送信され、貴重なレンダリング時間がかかる可能性があるためです。UI 要素が Graphic コンポーネントを必要としない場合は、単にそれを削除してもレイキャスティングは機能します。
UI ストラクチャー の簡素化
UI のリビルドとレンダリングにかかる時間を短縮するためには、UI オブジェクトの数をできるだけ少なくすることが重要です。なるべくベイクするようにしましょう。例えば、要素の色相を変更するためだけにブレンドされたゲームオブジェクトを使用しないで、代わりにマテリアルのプロパティを使ってください。また、フォルダーのようなゲームオブジェクトを作成して、シーンを整理する以外の目的を持たないようにしましょう。
非表示のカメラ出力の無効化
背景が不透明なフルスクリーンの UI を開いた場合でも、ワールドスペースカメラは UI の背後にある標準的な 3D シーンをレンダリングします。レンダラーは、フルスクリーンの Unity UI が 3D シーン全体を覆い隠してしまうことを認識していません。
そのため、完全にフルスクリーンの UI が開かれている場合は、隠されたワールド空間のカメラをすべて無効にすることで、3D ワールドのレンダリングという無駄な作業がなくなり、GPU への負荷が軽減されます。
UI が 3D シーン全体をカバーしていない場合は、連続してレンダリングするのではなく、一度シーンをテクスチャにレンダリングしてそれを使用するとよいでしょう。3D シーン内のアニメーションコンテンツを見ることができなくなりますが、ほとんどの場合はこれで問題ないでしょう。
注:Canvas が「Screen Space – Overlay」に設定されている場合、シーン内でアクティブなカメラの数に関わらず描画されます。
大部分が隠されたカメラ
多くの「フルスクリーン」UI は、実際には 3D ワールド全体が見えなくなるわけではなく、ワールドのごく一部が見えるようになっています。このような場合には、見えている部分だけをレンダリングテクスチャにキャプチャする方が最適な場合があります。ワールドの表示部分がレンダリングテクスチャに「キャッシュ」されていれば、実際のワールド空間のカメラを無効にして、キャッシュされたレンダリングテクスチャを UI 画面の背後に描画することで、3D ワールドの実例となるバージョンを提供することができます。
コンポジションベースの UI
デザイナーは、標準的な背景や要素を組み合わせたり、重ねたりして、最終的な UI を作成するコンポジションを用いて UI を作成することがよくあります。この方法は比較的簡単で、反復作業には非常に適していますが、Unity UI では透過レンダリングキューを使用しているため、パフォーマンスが低下します。
背景、ボタン、ボタン上のテキストというシンプルな UI を考えてみましょう。透過キュー内のオブジェクトは後ろから前に向かってソートされるため、ピクセルがテキストグリフ内にある場合、GPU は背景のテクスチャ、ボタンのテクスチャ、最後にテキストアトラスのテクスチャをサンプリングする必要があります。UI が複雑になり、背景に重ねる装飾要素が増えてくると、サンプル数が急激に増えてしまいます。
大きな UI がフィルレートの制限を受けていることが判明した場合、最良の手段は、UI の装飾的/不変的な要素の多くを背景テクスチャに統合する、特殊な UI スプライトを作成することです。これにより、目的のデザインを実現するために重ねなければならない要素の数を減らすことができますが、手間がかかり、プロジェクトのテクスチャアトラスのサイズも大きくなります。
ある UI を作るために必要なレイヤー要素の数を、専用の UI スプライトに凝縮するというこの原理は、サブ要素にも使えます。製品のスクロールペインを持つストアの UI について考えてみましょう。各製品の UI 要素には、境界、背景、そして価格や名前などの情報を示すアイコンがあります。
ストア UI には背景が必要ですが、製品が背景を横切るようにスクロールするため、製品要素をストア UI の背景テクスチャにマージすることはできません。ただし、製品の UI 要素の境界、価格、名前などの要素は、製品の背景にマージすることができます。アイコンのサイズや数にもよりますが、フィルレートの削減効果はかなり大きいです。
レイヤー要素を組み合わせるには、いくつかの欠点があります。特殊な要素は再利用できなくなり、作成にはアーティストの追加リソースが必要になります。特に UI テクスチャがオンデマンドでロード/アンロードされない場合、大きな新しいテクスチャを追加すると、UI テクスチャを保持するために必要なメモリ量が大幅に増加する可能性があります。
UI シェーダーとロースペックデバイス
Unity UI で使用されているビルトインのシェーダーは、マスキング、クリッピング、その他多くの複雑な操作をサポートしています。この複雑さのために、iPhone 4 などのローエンドデバイスでは、UI シェーダーは、よりシンプルな Unity 2D シェーダーに比べてパフォーマンスが低下します。
ローエンドデバイスを対象としたアプリケーションで、マスキングやクリッピングなどの「派手な」機能が不要な場合は、下記のような最小限の UI シェーダのように、不要な処理を省いたカスタムシェーダを作成することも可能です:
<pre> Shader "UI/Fast-Default" { Properties { [PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {} _Color ("Tint", Color) = (1,1,1,1) }
SubShader
{
Tags
{
"Queue"="Transparent"
"IgnoreProjector"="True"
"RenderType"="Transparent"
"PreviewType"="Plane"
"CanUseSpriteAtlas"="True"
}
Cull Off
Lighting Off
ZWrite Off
ZTest [unity_GUIZTestMode]
Blend SrcAlpha OneMinusSrcAlpha
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "UnityUI.cginc"
struct appdata_t
{
float4 vertex : POSITION;
float4 color : COLOR;
float2 texcoord : TEXCOORD0;
};
struct v2f
{
float4 vertex : SV_POSITION;
fixed4 color : COLOR;
half2 texcoord : TEXCOORD0;
float4 worldPosition : TEXCOORD1;
};
fixed4 _Color;
fixed4 _TextureSampleAdd;
v2f vert(appdata_t IN)
{
v2f OUT;
OUT.worldPosition = IN.vertex;
OUT.vertex = mul(UNITY_MATRIX_MVP, OUT.worldPosition);
OUT.texcoord = IN.texcoord;
#ifdef UNITY_HALF_TEXEL_OFFSET
OUT.vertex.xy += (_ScreenParams.zw-1.0)*float2(-1,1);
#endif
OUT.color = IN.color * _Color;
return OUT;
}
sampler2D _MainTex;
fixed4 frag(v2f IN) : SV_Target
{
return (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd) * IN.color;
}
ENDCG
}
}UI Canvas のリビルド
任意の UI を表示するために、UI システムは、画面上に表現される各 UI コンポーネントのジオメトリを構築する必要があります。これには、動的レイアウトコードの実行、UI テキスト文字列内の文字を表現するポリゴンの生成、ドローコールを最小限にするために可能な限り多くのジオメトリを単一のメッシュへのマージなどが含まれます。これは複数のステップで構成されており、本ガイドの冒頭にある Fundamentals セクションで詳しく説明しています。
Canvas のリビルドがパフォーマンスの問題になるのは、主に 2つの理由があります:
- Canvas 上の描画可能な UI 要素の数が多い場合、バッチの計算自体が非常に高価になります。これは、要素をソートして分析するコストが、Canvas 上の描画可能な UI 要素の数に比べて線形的に大きくなるためです。
- Canvas が頻繁にダーティとしてマークされると、比較的変更の少ない Canvas のリフレッシュに過剰な時間がかかってしまうことがあります。
いずれの問題も、Canvas上の要素数が増えれば増えるほど顕著になる傾向があります。
重要な注意:キャンバス上の描画可能な UI 要素が変更された場合、そのキャンバスはバッチビルドプロセスを再実行する必要があります。このプロセスでは、Canvas 上のすべての描画可能な UI 要素が変更されたかどうかにかかわらず、再分析されます。スプライトレンダラーに割り当てられたスプライト、トランスフォームの位置やスケール、テキストメッシュに含まれるテキストなど、UI オブジェクトの外観に影響を与えるあらゆる変更を「変更」と呼ぶことに注意してください。
子要素の順番
Unity UI は後ろから前に描画され、ヒエラルキーでの表示順と同じ順に描画されます。ヒエラルキーの前にあるオブジェクトは、オブジェクトの背面にあるオブジェクトと考えられます。2 つの UI 要素が重なっている場合、後から描画した UI 要素は先に描画した UI 要素の上に表示されます。バッチは、ヒエラルキーを上から下に向かって移動し、同じマテリアル、同じテクスチャを使用し、中間レイヤーを持たないすべてのオブジェクトを収集することで構築されます。「中間レイヤー」とは、異なるマテリアルを持つグラフィックオブジェクトで、そのバウンディングボックスは他の 2 つのバッチ可能なオブジェクトと重なり、2 つのバッチ可能なオブジェクトの間のヒエラルキーに配置されます。中間レイヤーがあると、バッチを強制的に中断します。
Unity UI Profiling Tools のステップで説明したように、UI プロファイラーとフレームデバッガを使って、UI の中間レイヤーを検査することができます。これは、ある描画可能なオブジェクトが、他のバッチ可能な 2 つの描画可能なオブジェクトの間に介在する状況です。
この問題は、テキストとスプライトが近くに配置されている場合によく発生します。テキストグリフのポリゴンの多くは透明であるため、テキストのバウンディングボックスが近くのスプライトに見えないように重なってしまうのです。この問題は 2つの方法で解決できます:
- バッチ可能なオブジェクトがバッチ不可能なオブジェクトに邪魔されないように、つまり、バッチ不可能なオブジェクトをバッチ可能なオブジェクトの上または下に移動させるように、描画可能なオブジェクトを並べ替えます。
- 目に見えない重なり合う空間をなくすために、オブジェクトの位置を調整します。
これらの操作はいずれも、Unity フレームデバッガーを開いて有効にした状態で、Unity エディターで行うことができます。Unity フレームデバッガーで見えるドローコールの数を観察するだけで、UI 要素が重なることで無駄になるドローコールの数を最小限に抑えることができる順番や位置を見つけることができます。
キャンバスの分割
非常に些細なケースを除いて、一般的には Canvas (キャンバス) を分割して、要素を Sub-canvas (サブキャンバス) や兄弟 Canvas に移動させるのが良い方法です。
兄弟 Canvas は、UI の特定の部分の描画の深さを他の部分とは別に制御し、常に他のレイヤーの上または下に配置する必要がある場合に最適です(チュートリアルの矢印など)。
それ以外のほとんどの場合、Sub-canvas は親 Canvas の表示設定を継承するため、より便利です。
一見すると、UI を多くの Sub-canvas に分割することがベストプラクティスのように思えますが、Canvas システムでは、別々の Canvas でバッチを結合することはできないことを覚えておいてください。パフォーマンスの高い UI 設計には、リビルドのコストを最小限に抑えることと、無駄なドローコールを最小限に抑えることのバランスが必要です。
一般的なガイドライン
キャンバス (Canvas) は、その構成要素である Drawable コンポーネントが変更されるたびにリバッチされるので、一般的には、重要なキャンバスは少なくとも 2つの部分に分割するのがベストです。また、要素が同時に変更されることが予想される場合は、同じキャンバス上に要素を配置することをお勧めします。例えば、プログレスバーとカウントダウンタイマーがあります。これらはどちらも同じ基礎データに依存しており、同時に更新が必要となるため、同じキャンバスに配置する必要があります。
1 つ目のキャンバスに、背景やラベルなど、静的で不変の要素をすべて配置します。これらの要素は、Canvas が最初に表示されたときに一度バッチ処理され、その後はリバッチ処理の必要がなくなります。
2 つ目のキャンバスには、頻繁に変化する「動的」な要素をすべて配置します。これにより、このキャンバスでは主にダーティな要素がリバッチされることになります。動的要素の数が非常に多くなった場合は、動的要素を、常に変化する要素(プログレスバー、タイマーの表示、アニメーションなど)と、時々しか変化しない要素にさらに細分化する必要があるかもしれません。
これは実際にはかなり難しいことで、特に UI コントロールをプレハブにカプセル化している場合はなおさらです。多くの UI では、コストのかかるコントロールをサブキャンバスに分割してキャンバスを細分化しています。
Unity 5.2 バッチングで最適化
Unity 5.2では、バッチコードが大幅に書き直され、Unity 4.6、5.0、5.1 に比べてかなりパフォーマンスが向上しています。さらに、1つ以上のコアを持つデバイスでは、Unity UI システムは処理のほとんどをワーカースレッドに移します。一般的に、Unity 5.2 は、UI を多数のサブキャンバスに積極的に分割する必要性を減らします。モバイルデバイス上の多くの UI は、2つまたは 3つのキャンバスだけでパフォーマンスを発揮できるようになりました。
Unity 5.2 の最適化に関する詳細は、こちらのブログ記事をご覧ください。
Unity UI での入力とレイキャスティング
デフォルトでは、Unity UI は Graphic Raycaster コンポーネントを使用して、タッチイベントやポインターイベント (ホバー) などの入力イベントを処理します。これは通常、Standalone Input Manager コンポーネントによって処理されます。Standalone Input Manager は、その名前に反して、「ユニバーサル」な入力マネージャーシステムであり、ポインターとタッチの両方を扱うことができます。
モバイルでの誤ったマウス入力検知 (5.3)
Unity 5.4 以前では、Graphic Raycaster がアタッチされた各アクティブな Canvas は、現在利用可能なタッチ入力がない限り、フレームごとに 1 回レイキャストを実行してポインターの位置を確認していました。これは、プラットフォームに関係なく発生します。マウスを持たない iOS および Android デバイスは、マウスの位置を query し、その位置の下にある UI 要素を検出してホバーイベントを送信する必要があるかどうかを判断します。
これは CPU 時間の浪費であり、Unity アプリケーションの CPU フレーム時間の 5 %以上を消費していることが確認されています。
この問題は、Unity 5.4 で解決されています。5.4 以降、マウスを搭載していないデバイスでは、マウスの位置を query したり、不要なレイキャストを実行しないようになりました。
5.4 以前のバージョンの Unity を使用している場合、モバイル開発者は独自の Input Manager クラスを作成することを強く推奨されます。これは、Unity UI ソースから Unity の Standard Input Manager をコピーして、ProcessMouseEvent メソッドとそのメソッドへのすべての呼び出しをコメントアウトするのと同じくらい簡単に出来ます。
Raycast の最適化
Graphic Raycaster は比較的簡単な実装で、「Raycast Target」設定が true に設定されているすべての Graphic コンポーネントについて繰り返し処理を行います。各 Raycast ターゲットに対して、Raycaster は一連のテストを実行します。Raycast ターゲットがすべてのテストに合格すると、ヒットリストに追加されます。
Raycast の実装の詳細
テストは次のとおりです:
- Raycast ターゲットがアクティブな(つまりジオメトリがある)場合は有効にして描画します
- 入力ポイントが Raycast ターゲットにアタッチされている RectTransform 内にある場合
- Raycast ターゲットが ICanvasRaycastFilter コンポーネントを持っているか、その子要素(任意の深さ)であり、Raycast Filter コンポーネントが Raycast を許可する場合
ヒットした Raycast Target のリストは、深さごとにソート (深さ優先探索) され、反転したターゲットのフィルタリングが行われ、カメラの背後にレンダリングされた(つまり画面に表示されない)要素が除外されるようにフィルタリングされます。
また、Graphic Raycaster の「Blocking Objects」プロパティにそれぞれのフラグが設定されている場合、Graphic Raycaster は 3D または 2D の物理システムにレイをキャストすることができます (スクリプトでは、このプロパティの名前は blockingObjects です)。
2D または 3D ブロッキングオブジェクトが有効になっている場合、レイキャストをブロックする Physics Layer の 2D か、3D オブジェクトの下に描画されている Raycast ターゲットは、ヒットリストからも除外されます。
次に、最終的にヒットしたリストが返されます。
レイキャスティングに関する最適化のヒント
すべての Raycast ターゲットは Graphic Raycaster によってテストする必要があるため、ポインターイベントを受信しなければならない UI コンポーネントでのみ「Raycast Target」設定を有効にすることが最善の方法です。Raycast ターゲットのリストが小さければ小さいほど、またトラバースしなければならない階層が浅ければ浅いほど、各 Raycast テストは高速になります。
背景とテキストの両方の色を変えたいボタンのように、ポインターイベントに応答しなければならない複数の描画可能な UI オブジェクトを持つ複合 UI コントロールの場合、一般的には複合 UI コントロールのルートに単一の Raycast ターゲットを配置するのが良いでしょう。その単一の Raycast ターゲットがポインターイベントを受信すると、そのイベントを複合コントロール内の関係する各コンポーネントに転送することができます。
階層の深さと Raycast フィルター
各 Graphic Raycast は、Raycast フィルターを検索する際に、Transform 階層をルートまでさかのぼって検索します。Graphic Raycast 操作のコストは、階層の深さに比例して直線的に増加します。階層内の各 Transform にアタッチされているすべてのコンポーネントをテストして、それらが ICanvasRaycastFilter を実装されているかどうかを判断する必要があるため、これは安価な操作ではありません。
CanvasGroup、Image、Mask、RectMask2D など、ICanvasRaycastFilter を使用する標準の Unity の UI コンポーネントがいくつかあるため、このトラバーサルを単純に排除することはできません。
Sub-canvas と OverrideSorting プロパティ
Sub-canvas の overrideSorting プロパティを使用すると、Graphic Raycast テストでトランスフォーム階層の昇順が停止します。ソートやレイキャスト検出の問題を起こさずに有効にできる場合は、レイキャスト階層をトラバースするコストを削減するために使用する必要があります。
5. UI コントロールの最適化
このセクションでは 「Unity UI の最適化」ガイドのうち、特定のタイプの UI コントロールに特有の問題を取り上げます。ほとんどの UI コントロールは、パフォーマンスの面では比較的似ていますが、出荷可能な状態に近いゲームで発生するパフォーマンス問題の原因として、2 つのコントロールが注目されています。
UI 要素のテキスト
Unity ビルトインの Text コンポーネントは、ラスタライズされたテキストグリフを UI に表示するのに便利なコンポーネントです。ただし、一般的には知られていませんが、パフォーマンスのホットスポットとして頻繁に登場する動作がいくつかあります。UI にテキストを追加する際には、テキストグリフは実際には 1 文字につき 1 つのクアッドとしてレンダリングされることを常に念頭に置いてください。これらのクアッドは、グリフの形状によってはグリフの周囲にかなりの空きスペースがある傾向があり、意図せずに他の UI 要素のバッチングを崩してしまうようなテキストの配置が非常に簡単にできてしまいます。
Text Mesh のリビルド
大きな問題の一つは、UI Text Mesh のリビルドです。UI Text コンポーネントが変更されるたびに、Text コンポーネントは実際のテキストを表示するためのポリゴンを再計算しなければなりません。この再計算は、Text コンポーネントやその親のゲームオブジェクトが、テキストに変更を加えずに単に無効化され、再び有効化された場合にも発生します。
この挙動は、リーダーボードや統計画面など、大量のテキストラベルを表示するあらゆる UI で問題となります。Unity の UI を非表示にしたり表示させたりする最も一般的な方法は、UI を含むゲームオブジェクトを有効にしたり無効にしたりすることですが、大量の Text コンポーネントを含む UI は、それらが表示されるたびに望ましくないフレームレートのヒカップを引き起こすことがよくあります。
この問題を回避する方法としては、次のステップの「Disabling Canvases」のセクションを参照してください。
ダイナミックフォントとフォントアトラス
ダイナミックフォントは、表示可能な完全な文字セットが非常に大きいか、ランタイム前にわからない場合に、テキストを表示する便利な方法です。Unity の実装では、これらのフォントは、UI Text コンポーネント内で使用される文字に基づいて、ランタイムにグリフアトラスを生成します。
ロードされた個別の Font オブジェクトは、他のフォントと同じフォントファミリーであっても、それぞれのテクスチャアトラスを維持します。例えば、あるコントロールで Arial の太字テキストを使用し、別のコントロールで Arial Bold を使用すると、出力は同じですが、Unity では Aria l用と Arial Bold 用の 2つの異なるテクスチャアトラスが維持されます。
パフォーマンスの観点から理解すべき最も重要なことは、Unity UI のダイナミックフォントは、サイズ、スタイル、文字の異なる組み合わせごとに、フォントのテクスチャアトラスに 1つのグリフを保持するということです。つまり、ある UI に 2つのテキストコンポーネントがあり、どちらも「A」という文字を表示している場合、次のようになります:
- 2 つの Text コンポーネントが同じサイズを共有している場合、フォントアトラスには 1つのグリフが含まれます。
- 2 つのテキストコンポーネントのサイズが同じでない場合(一方が 16 ポイント、他方が 24 ポイントなど)、フォントアトラスには文字「A」の異なるサイズのコピーが 2つ含まれることになります。
- 一方のテキストコンポーネントがボールド体 (太字) で、もう一方がボールド体でない場合、フォントアトラスにはボールド体の「A」とレギュラーの「A」が含まれます。
ダイナミックフォントを持つ UI Text オブジェクトが、フォントのテクスチャアトラスにまだラスタライズされていないグリフに遭遇するたびに、フォントのテクスチャアトラスをリビルドする必要があります。新しいグリフが現在のアトラスに収まる場合は、グリフが追加され、アトラスはグラフィックデバイスに再アップロードされます。ただし、現在のアトラスが小さすぎる場合は、システムはアトラスのリビルドを試みます。これには 2 つの段階があります。
まず、アクティブな UI Text コンポーネントが現在表示しているグリフのみを使用して、アトラスを同じサイズでリビルドします。これには、親の Canvas が有効で、Canvas Renderer が無効になっている UI Text コンポーネントも含まれます。システムが現在使用しているすべてのグリフを新しいアトラスに収めることに成功した場合は、そのアトラスをラスタライズし、2 番目のステップには進みません。
次に、現在使用しているグリフのセットが、現在のアトラスと同じサイズのアトラスに収まらない場合、アトラスの短い方の寸法を 2 倍にして、より大きなアトラスを作成します。例えば、512x512 のアトラスは 512x1024 のアトラスに拡張されます。
上記のアルゴリズムにより、ダイナミックフォントのアトラスは、一度作成されるとサイズが大きくなるだけです。テクスチャアトラスのリビルドにかかるコストを考えると、リビルド時のコストを最小限に抑えることが不可欠です。これには 2 つの方法があります。
可能な限り、非ダイナミックフォントを使用し、必要なグリフセットのサポートを事前に設定します。これは通常、Latin/ASCII 文字だけなど、制約の多い文字セットを使用し、サイズの範囲が小さい UI に有効です。
Unicode セット全体のような非常に大きな範囲の文字に対応する必要がある場合は、フォントを Dynamic に設定する必要があります。予期されるパフォーマンスの問題を避けるために、起動時にフォントのグリフアトラスに Font.RequestCharactersInTexture 経由で適切な文字セットを準備します。
なお、フォントアトラスのリビルドは、変更された各 UI Text コンポーネントに対して個別に行われます。非常に多くの Text コンポーネントを入力する場合は、Text コンポーネントのコンテンツに含まれるすべてのユニークな文字を収集し、フォントアトラスを用意するとよいでしょう。これにより、新しいグリフに出会うたびにグリフアトラスをリビルドするのではなく、1 回だけリビルドすればよいようになります。
また、フォントアトラスのリビルドがトリガーされると、アクティブな UI Text コンポーネントに現在含まれていない文字は、たとえ Font.RequestCharactersInTexture の呼び出しの結果として元々アトラスに追加されていたとしても、新しいアトラスには存在しないことに注意してください。この制限を回避するには、Font.textureRebuilt をデリゲート経由でサブスクライブし、Font.characterInfo へクエリを実行して、必要な文字がすべて用意されていることを確認します。
Font.textureRebuilt デリゲートは、現在文書化されていません。これは単一の引数を持つ Unity イベントです。引数は、テクスチャがリビルドされたフォントです。このイベントのサブスクライバーは、以下のシグネチャに従うべきです:
public void TextureRebuiltCallback(Font rebuiltFont) { /* ... */ }特殊なグリフレンダラー
グリフがよく知られていて、各グリフ間の位置が比較的固定されている場合は、そのグリフを表示するスプライトを表示するカスタムコンポーネントを書いた方が圧倒的に有利です。例えば、スコアの表示などがその例です。
スコアの場合、表示可能な文字はよく知られたグリフセット(0〜9 の数字)から描画されており、地域によって変わることはなく、互いに一定の距離を置いて表示されています。整数をその桁に分解して、適切な桁のスプライトを表示することは比較的簡単です。このような特殊な数字表示システムは、Canvas 駆動の UI Text コンポーネントと比較して、計算、アニメーション、表示が非常に高速で、かつ、割り当てのない方法で構築することができます。
フォールバックフォントとメモリ使用量について
大きな文字セットに対応しなければならないアプリケーションでは、フォントインポーターの「Font Names」フィールドに多数のフォントをリストアップしたくなるものです。「Font Names」フィールドに記載されているフォントは、グリフが主なフォント内に見つからない場合のフォールバックとして使用されます。フォールバックの順序は、「Font Names」フィールドにフォントがリストアップされている順序によって決まります。
ただし、この動作をサポートするために、Unity は「Font Names」フィールドにリストされているすべてのフォントをメモリにロードし続けます。フォントの文字セットが非常に大きい場合、フォールバックフォントが消費するメモリ量が過大になる可能性があります。これは、日本語の漢字や中国語の文字、絵文字フォントなどを含む場合によく見られます。
Best Fit とパフォーマンス
一般的に、UI Text コンポーネントの Best Fit 設定は絶対に使用しないでください。
「Best Fit」では、Text コンポーネントのバウンディングボックス内でオーバーフローせずに表示できる最大の整数ポイントサイズにフォントサイズを動的に調整し、設定可能な最小/最大ポイントサイズにクランプします。ただし、Unity では、表示されている文字のサイズごとに異なるグリフをフォントアトラスにレンダリングするため、Best Fit を使用すると、さまざまなグリフサイズのアトラスが急速に対処できなくなります。
Unity 2017.3 の時点で、Best Fit が使用するサイズ検出は最適ではありません。テストされたサイズの増分ごとにフォントアトラスにグリフが生成されるため、フォントアトラスの生成に必要な時間がさらに長くなります。また、アトラスのオーバーフローを引き起こす傾向があり、古いグリフがアトラスから除外されてしまいます。Best Fit の計算に膨大な量のテストが必要なため、他の Test コンポーネントで使用されているグリフが削除されることが多く、適切なフォントサイズが計算された後に、少なくとも 1 回はフォントアトラスをリビルドしなければなりません。この問題は Unity 5.4 で修正され、Best Fit はフォントのテクスチャアトラスを不必要に拡張しませんが、それでも静的サイズのテキストよりはかなり遅くなります。
フォントアトラスのリビルドが頻繁に行われると、ランタイムのパフォーマンスが急激に低下し、メモリの断片化を引き起こします。Best Fit に設定されている Text コンポーネントの数が多いほど、この問題は悪化します。
TextMeshPro Text
TextMesh Pro (TMP) は、Unity の既存の Text コンポーネントである Text Mesh や UI Text に代わるものです。TextMesh Pro は、主要なテキストのレンダリングパイプラインとしてSDF(Signed Distance Field)を使用しており、あらゆるポイントサイズと解像度でテキストをきれいにレンダリングすることができます。TextMesh Pro は、SDF テキストレンダリングのパワーを活用するために設計されたカスタムシェーダーのセットを使用して、マテリアルのプロパティを変更するだけで、テキストの視覚的外観を動的に変更することができます。また、Dilation、Outline、Soft Shadow、Beveling、Textures、Glow などのビジュアルスタイルを追加し、Material Preset (マテリアル プリセット) を作成・使用することで、これらのビジュアルスタイルを保存し、呼び出しすることができます。
2018.1 のリリースまで、TextMesh Pro は Asset Store パッケージとして自分のプロジェクトに含まれていました。2018.1 からは TextMesh Pro は Package Manager のパッケージとして提供されます。
Text Mesh のリビルド
Unity のビルトイン UIText コンポーネントのように、コンポーネントが表示するテキストに変更を加えると、Canvas.SendWillRendererCanvases や Canvas.BuildBatch の呼び出しが発生して、コストがかかることがあります。TextMeshProUGUI コンポーネントの Text フィールドへの変更は最小限に抑え、テキストが頻繁に変更される TextMeshProUGUI コンポーネントを独自の Canvas コンポーネントを持つ親ゲームオブジェクトと必ずペアレント化して、Canvas のリビルドの呼び出しができるだけ効率的になるようにします。
なお、ワールド空間にテキストを表示する場合は、ワールド空間で Canvases を使用すると効率が悪くなるので、TextMeshProUGUI ではなく、normal の TextMeshPro コンポーネントを使用することをお勧めします。TextMeshPro を直接使用すると、Canvas システムのオーバーヘッドが発生しないため、より効率的です。
フォントとメモリ使用量について
TMP にはダイナミックフォント機能がないため、フォールバックフォントに頼らざるを得ません。フォールバックフォントがどのように読み込まれ、使用されるかを理解することは、TMP を使用する際にメモリを最適化する上で非常に重要です。
TMP におけるグリフの検出は、再帰的に行われます。つまり、TMP Font Asset からグリフが欠落している場合、TMP は現在割り当てられている、またはアクティブなフォールバック Font アセットのリストを、リストの最初のフォールバックから順に、自分のフォールバックまで繰り返し処理します。それでもグリフが見つからない場合、TMP はテキストオブジェクトに割り当てられている可能性のあるすべての Sprite アセットと、この Sprite アセットに割り当てられているフォールバックを検索します。それでも目的のグリフが見つからない場合、TMP は、TMP 設定ファイルで割り当てられた一般的なフォールバックのリストを再帰的に検索し、次にデフォルトの Sprite アセットを検索します。それでもこのグリフが見つからない場合は、TMP 設定で割り当てられているデフォルトの Font アセットを検索します。最後の手段として、TMP 設定ファイルに定義されている Missing Glyph Replacement の文字を使用して表示します。
TextMesh Pro の Font アセットは、シーンやプロジェクトで参照されるとロードされます。これらは主に、TextMeshPro の Text コンポーネント、TMP 設定、およびフォールバックフォントとしての Font アセット自体によって参照されます。TMP Settings アセットで Font アセットを参照している場合、TMP Text コンポーネントがある最初のシーンがアクティブになったときに、それらの Font アセットとそのフォールバックフォントアセットが再帰的に読み込まれます。デフォルトのスプライトシートアセットが参照されている場合は、それも読み込まれます。
また、シーン内の TextMeshPro コンポーネントで Font アセットが参照されていて、TMP Settings でロードされていない場合は、コンポーネントがアクティブになると、参照されている Font アセットとそのフォールバック Font アセットのすべてが再帰的にロードされます。多くのフォントを使用するプロジェクトでは、このプロセスを念頭に置いておくことが重要で、特に使用可能なメモリに問題がある場合は注意が必要です。
上記の理由により、TMP を使用してプロジェクトをローカライズする場合、ローカライズされたすべての言語の Font アセットを TMP 設定を介して前もってロードしておくと、メモリの負荷に悪影響を及ぼすことが懸念されます。ローカライズが必要な場合は、必要なときだけ(さまざまなシーンが読み込まれるときに)これらのフォントアセットやフォールバックを割り当てるか、 Asset Bundles (アセットバンドル) を使用してモジュール式で Font アセットをロードするという潜在的戦略をお勧めします。
アプリケーションの起動時には、ユーザーのロケールを確認し、各フォントアセットのフォールバックを設定するブートストラップのステップを入れる必要があります:
- ベースとなる TMP Font アセット(各フォントに対して最小の Latin 文字のグリフなど)のアセットバンドルの作成
- 言語ごとに必要なフォールバック TMP Font アセットのアセットバンドルを作成する(例:日本語に必要なフォントごとに TMP Font アセットのアセットバンドルを 1 つ作成する)
- ベースとなるアセットバンドルをブートストラップステップで読み込む
- ロケールに応じて、必要なアセットバンドルをフォールバックフォントとともに読み込む
- ベースとなる Asset Bundle 内の各フォントに対して、ローカライズされたフォントアセットバンドルからフォールバックのフォントアセットを割り当てる
- ゲームのブートストラップを続行する
また、画像を使用しない場合は、TMP 設定からデフォルトの Sprite アセットの参照を削除することで、メモリを大幅に節約することもできます。
Best Fit とパフォーマンスについて
繰り返しになりますが、TextMesh Pro にはダイナミックフォント機能がないため、上記の UGI UIText セクションで説明した Best Fit に関する問題は発生しません。TextMesh Pro コンポーネントで Best Fit を使用する際に考慮すべき唯一の点は、正しいサイズを見つけるためにバイナリサーチが使用されることです。テキストの自動サイズ変更を使用する場合は、最長または最大のテキストブロックの最適なポイントサイズをテストすることをお勧めします。最適なサイズが決まったら、そのテキストオブジェクトの自動サイズ変更を無効にして、他のテキストオブジェクトに手動で最適なポイントサイズを設定します。これにより、パフォーマンスが向上し、視覚的にもタイポグラフィ的にも良くないとされる、異なるポイントサイズを使用したテキストオブジェクトのグループを回避することができます。
Scroll View
Unity UI の Scroll View (スクロールビュー) は、フィルレートの問題に次いで、ランタイムのパフォーマンス問題の原因としてよく見られます。スクロールビューは通常、コンテンツを表現するためにかなりの数の UI 要素を必要とします。スクロールビューにコンテンツを追加するには、次の 2つの基本的なアプローチがあります:
- スクロールビューですべてのコンテンツを表示するために必要なすべての要素を追加する
- 要素をプールし、必要に応じて位置を変えて、目に見えるコンテンツを表示する
これらのソリューションはどちらも問題があります。
1 つ目のソリューションでは、表現するアイテムの数が増えると、すべての UI 要素をインスタンス化するのに必要な時間が増え、さらにスクロールビューのリビルドに必要な時間も増えてしまいます。スクロールビュー内に必要な要素数が少ない場合、例えば、いくつかの Text コンポーネントを表示するだけのスクロールビューの場合は、シンプルなこの方法が好まれます。
2 つ目のソリューションは、現在の UI とレイアウトシステムの下で正しく実装するために、かなりの量のコードを必要とします。考えられる 2つの方法について、以下でさらに詳しく説明します。非常に複雑なスクロール UI では、パフォーマンスの問題を回避するために、何らかのプーリング手法が必要となります。
これらの問題にもかかわらず、RectMask2D コンポーネントを Scroll View に追加することで、すべてのアプローチが改善されます。このコンポーネントは、Scroll View のビューポートの外側にある Scroll View 要素が、Canvas のリビルド時にジオメトリの生成、ソート、分析が必要な描画可能な要素のリストに含まれないようにします。
シンプルな Scroll View 要素のプーリング
スクロールビュー (Scroll View) でオブジェクトプーリングを実装しつつ、Unity に組み込まれた Scroll View コンポーネントのネイティブな利便性を維持するには、ハイブリッドなアプローチをとるのが一番簡単です。
レイアウトシステムがスクロールビューのコンテンツのサイズを適切に計算し、スクロールバーが適切に機能するように、UI の要素をレイアウトするには、Layout Element コンポーネントを持つゲームオブジェクトを、表示される UI 要素の「プレースホルダー」として使用します。
次に、スクロールビューの可視領域を埋めるのに十分な表示されている UI 要素のプールをインスタンス化し、これらの要素を位置決めプレースホルダーの親とします。スクロールビューがスクロールすると、UI 要素を再利用してスクロールしてきたコンテンツを表示します。
バッチ処理のコストは、Rect Transform の数ではなく、Canvas 内の Canvas Renderer の数に応じて増加するだけなので、これによりバッチ処理が必要な UI 要素の数が大幅に削減されます。
シンプルなアプローチの問題点
現在、任意の UI 要素の親が変更されたり、兄弟の順序が変更されたりすると、その要素とそのサブ要素はすべて「ダーティ」としてマークされ、Canvas のリビルドを余儀なくされます。
これは、Unity がトランスフォームの親を変更し、その兄弟内で順序を変更するためのコールバックを分離していないためです。これらのイベントは、いずれも OnTransformParentChanged コールバックを起動します。Unity UI の Graphic クラスのソース(ソース内の Graphic.cs を参照)では、そのコールバックが実装されており、SetAllDirty というメソッドを呼び出しています。Graphic をダーティにすることで、次のフレームがレンダリングされる前に、Graphic のレイアウトと頂点がリビルドされるようになっています。
スクロールビュー内の各要素のルート RectTransform にキャンバスを割り当てることは可能です。これにより、リビルドはスクロールビューのコンテンツ全体ではなく、親に変更された要素のみに制限されます。ただし、この場合、スクロールビューのレンダリングに必要なドローコールの数が増える傾向にあります。さらに、スクロールビュー内の個々の要素が複雑で、十数個の Graphic コンポーネントで構成されている場合、特に各要素に相当数の Layout コンポーネントがある場合は、リビルドのコストが高くなり、ローエンドデバイスではフレームレートの低下が顕著になることが多いです。
スクロールビューの UI 要素のサイズが可変でない場合は、このようなレイアウトと頂点の完全な再計算は必要ありません。ただし、この動作を避けるためには、親や兄弟の順序の変更ではなく、位置の変更に基づいてオブジェクトプーリングソリューションを実装する必要があります。
位置情報ベースの Scroll View のプール
上記の問題を回避するために、含まれる UI 要素の RectTransform を動かすだけで、オブジェクトをプールするスクロールビューを作成することができます。これにより,移動された RectTransform の寸法が変更されない場合に,そのコンテンツをリビルドする必要がなくなり,スクロールビューのパフォーマンスが大幅に向上しました。
これを実現するには、スクロールビューのカスタムサブクラスを作成するか、カスタムの Layout Group コンポーネントを作成するのが通常の最適です。後者は一般的にシンプルなソリューションであり、Unity UI の LayoutGroup 抽象クラス (abstract) のサブクラスを実装することで実現できます。
カスタムの Layout Group を使うと、基礎となるソースデータを分析して、表示すべきデータ要素の数を調べ、スクロールビューの Content RectTransform のサイズを適切に変更することができます。そして、Scroll View の変更イベントをサブスクライブし、それに応じて表示されている要素を再配置することができます。
6. UI の最適化に関するその他のヒントとテクニック
World Space または Screen Space – Camera ョンでは、UI のパフォーマンスを向上させるのに役立ついくつかの提案を紹介しますが、中には構造的に「クリーンでない」とか、メンテナンスが困難だったり、他の影響が生じてしまいやっかいなものもあります。また、初期の開発を簡素化するために UI の挙動を回避している場合もありますが、パフォーマンス上の問題を比較的容易に発生させている場合もあります。
RectTransform ベースのレイアウト
Layout コンポーネントは、ダーティとマークされるたびに子要素のサイズと位置を再計算しなければならないため、比較的コストがかかります (詳細は基礎編の「Graphic のリビルド」セクションを参照してください)。特定のレイアウト内に比較的少数の固定数の要素があり、レイアウトの構造が比較的単純な場合、レイアウトを RectTransform ベースのレイアウトに置き換えることができる場合があります。
RectTransform のアンカーを割り当てることにより、RectTransform の位置とサイズを、その親に基づいてスケーリングすることができます。 たとえば、単純な 2 列のレイアウトは 2つの RectTransforms で実現できます:
- 左列のアンカーは X にする:(0, 0.5) と Y: (0, 1)
- 右列のアンカーは X にする:(0.5, 1) と Y: (0, 1)
RectTransform のサイズと位置の計算は、Transform システム自体によってネイティブコードで実行されます。これは通常、レイアウトシステムに依存するよりもパフォーマンスが高くなります。RectTransform ベースのレイアウトを設定する MonoBehaviours を作成することもできます。 ただし、これは比較的複雑なタスクであり、このガイドの範囲を超えています。
Canvas の無効化
UI の個別の部分を表示したり隠したりする場合、UI のルートにあるゲームオブジェクトを有効にしたり無効にしたりするのが一般的です。これにより、無効化された UI のコンポーネントが入力や Unity のコールバックを受け取らないようになります。
ただし、この場合、Canvas は VBO データを破棄することになります。Canvas を再度有効にすると、Canvas(およびSub-canvas)がリビルドおよびリバッチの処理を行う必要があります。この処理が頻繁に行われると、CPU 使用率の増加により、アプリケーションのフレームレートが不安定になることがあります。
これを回避するには、表示/非表示の対象となる UI を独自の Canvas または Sub-canvas に配置し、このオブジェクトの Canvas コンポーネントを単に有効/無効にすることが考えられますが、現実的ではありません。
これにより、UI のメッシュは描画されませんが、メモリ内に常駐し、元のバッチングが保持されます。また、UI の階層で OnEnable や OnDisable のコールバックが呼び出されることはありません。
ただし、非表示の UI 内にある MonoBehaviours は無効化されないため、これらの MonoBehaviours は Update などの Unity ライフサイクルコールバックを受け取ることになりますのでご注意ください。
この問題を回避するには、この方法で無効化される UI 上の MonoBehaviour は、Unity のライフサイクルコールバックを直接実装せず、UI のルートゲームオブジェクト上の「Callback Manager」MonoBehaviour からコールバックを受け取るようにしてください。この「Callback Manager」は、UI が表示/非表示されるたびに通知され、必要に応じてライフサイクルイベントが伝搬されたり、伝搬されなかったりするようにすることができます。この「Callback Manager」パターンについての詳しい説明は、このガイドの範囲を超えています。
Event Camera の割り当て
World Space または Screen Space – Camera モードでレンダリングするように設定された Canvases と一緒に Unity のビルトイン Input Manager を使用する場合は、必ず Event Camera または Render Camera プロパティをそれぞれ設定しなければなりません。スクリプトでは、これは常に worldCamera プロパティとして公開されます。
このプロパティが設定されていない場合、Unity UI は Main Camera タグを持つゲームオブジェクトにアタッチされた Camera コンポーネントを探してメインカメラを検索します。この検索は、World Space または Camera Space の Canvas ごとに少なくとも 1 回実行されます。GameObject.FindWithTag は負荷が高く時間がかかることが知られているので、設計時または初期化時に、すべての World Space および Camera Space Canvases に Camera プロパティを割り当てることを強く推奨します。
この問題は Overlay モードの Canvases では発生しません。
UI ソースコードのカスタマイズ
UI システムは、数多くのユースケースをサポートするように設計されています。この柔軟性は素晴らしいものですが、他の機能を壊すことなく簡単に最適化できない部分があることも事実です。C# の UI ソースコードを変更することで CPU サイクル数を増やすことができる状況になった場合、UI DLL を再コンパイルして Unity に付属しているものを上書きすることが可能です。この手順は、Bitbucket リポジトリの readme ファイルに記載されています。お使いの Unity バージョンに対応したソースコードを必ず入手してください。
ただし、この方法はいくつかの重要な欠点があるため、最後の手段としてのみ実行するべきです。まず、この新しい DLL を開発者に配布してマシンを構築する方法を見つけなければなりません。その後、Unity をアップグレードするたびに、あなたの変更を新しい UI ソースコードとマージする必要があります。その方向に進む前に、既存のクラスを拡張したり、コンポーネントの独自のバージョンを作成したりできないことを確認してください。