数多くの実験、実用的なアドバイス、パフォーマンス テストを裏付ける詳細なガイドを使用して、Unity で UI パフォーマンスを最適化する方法を確認してください。
こんにちは!Pixonic (MY.GAMES) のクライアント開発者、Sergey Begichev です。この記事では、Unity3D での UI の最適化について説明します。テクスチャ セットのレンダリングは単純に思えるかもしれませんが、パフォーマンスに重大な問題を引き起こす可能性があります。たとえば、War Robots プロジェクトでは、最適化されていない UI バージョンが CPU 負荷全体の最大 30% を占めていました。これは驚くべき数字です。
通常、この問題は 2 つの状況で発生します。1 つは、多数の動的オブジェクトがある場合、もう 1 つは、デザイナーがさまざまな解像度間での信頼性の高いスケーリングを優先するレイアウトを作成する場合です。このような状況では、小さな UI でも顕著な負荷が発生する可能性があります。これがどのように機能するかを調べ、負荷の原因を特定し、考えられる解決策について説明しましょう。
まず、復習しましょう
2 番目と 3 番目のポイントは直感的に明らかですが、残りの推奨事項は実際に想像するのが難しい場合があります。たとえば、「キャンバスをサブキャンバスに分割する」というアドバイスは確かに価値がありますが、Unity はこの分割の背後にある原則について明確なガイドラインを提供していません。私個人としては、実際的な観点から、サブキャンバスを実装するのに最も適切な場所を知りたいのです。
「レイアウト グループを避ける」というアドバイスを検討してください。レイアウト グループは UI の負荷を高める原因となる可能性がありますが、多くの大規模な UI には複数のレイアウト グループが付属しており、すべてを作り直すのに時間がかかる可能性があります。さらに、レイアウト グループを避けるレイアウト デザイナーは、タスクにかなり多くの時間を費やすことになります。したがって、このようなグループを避けるべき場合、グループが有益な場合、グループを排除できない場合に取るべきアクションを理解しておくと役立ちます。
Unity の推奨事項のこの曖昧さは根本的な問題であり、これらの提案にどのような原則を適用すべきかが不明瞭な場合がよくあります。
UI パフォーマンスを最適化するには、Unity が UI を構築する方法を理解することが不可欠です。これらの段階を理解することは、Unity で UI を効果的に最適化するために不可欠です。このプロセスには、大きく分けて 3 つの主要な段階があります。
レイアウト。最初に、Unity はすべての UI 要素をサイズと指定された位置に基づいて配置します。これらの位置は、画面の端や他の要素を基準に計算され、依存関係のチェーンを形成します。
バッチ処理。次に、Unity は個々の要素をバッチにグループ化して、より効率的にレンダリングします。1 つの大きな要素を描画する方が、複数の小さな要素をレンダリングするよりも効率的です。(バッチ処理の詳細については、
レンダリング。最後に、Unity は収集したバッチを描画します。バッチの数が少ないほど、レンダリング プロセスは速くなります。
プロセスには他の要素も関係していますが、問題の大部分はこれら 3 つの段階に関係するため、ここではこれらに焦点を当てます。
理想的には、UI が静的である場合 (つまり、何も移動または変更されない場合)、レイアウトを 1 回構築し、単一の大きなバッチを作成して、効率的にレンダリングできます。
ただし、1 つの要素の位置を変更するだけでも、その位置を再計算し、影響を受けるバッチを再構築する必要があります。他の要素がこの位置に依存している場合は、それらの位置も再計算する必要があり、階層全体に連鎖的な影響が生じます。調整が必要な要素が増えるほど、バッチ処理の負荷は高くなります。
したがって、レイアウトの変更は UI 全体に波及効果を引き起こす可能性があり、私たちの目標は変更の数を最小限に抑えることです。(あるいは、連鎖反応を防ぐために変更を分離することを目指すこともできます。)
実際の例として、この問題はレイアウト グループを使用する場合に特に顕著になります。レイアウトが再構築されるたびに、すべての LayoutElement が GetComponent 操作を実行しますが、これはリソースを大量に消費する可能性があります。
パフォーマンス結果を比較するために、一連の例を調べてみましょう。(すべてのテストは、Google Pixel 1 デバイスで Unity バージョン 2022.3.24f1 を使用して実施されました。)
このテストでは、単一の要素を含むレイアウト グループを作成し、要素のサイズを変更するシナリオと FillAmount プロパティを利用するシナリオの 2 つのシナリオを分析します。
RectTransform の変更:
FlllAmount の変更:
2 番目の例では、8 つの要素を持つレイアウト グループで同じことを実行します。この場合も、変更するのは 1 つの要素だけです。
RectTransform の変更:
FlllAmount の変更:
前の例では、RectTransform の変更によってレイアウトに 0.2 ミリ秒の負荷が発生しましたが、今回は負荷が 0.7 ミリ秒に増加します。同様に、バッチ更新による負荷は 0.65 ミリ秒から 1.10 ミリ秒に増加します。
まだ 1 つの要素のみを変更していますが、レイアウトのサイズが大きくなると、再構築中の負荷に大きな影響を与えます。
対照的に、要素の FillAmount を調整すると、要素の数が増えても負荷の増加は見られません。これは、FillAmount を変更してもレイアウトの再構築がトリガーされず、バッチ更新の負荷がわずかに増加するだけだからです。
このシナリオでは、FillAmount を使用する方が明らかに効率的です。ただし、要素のスケールや位置を変更すると、状況はより複雑になります。このような場合、レイアウトの再構築をトリガーしない Unity の組み込みメカニズムを置き換えるのは困難です。
ここで SubCanvas が役立ちます。変更可能な要素を SubCanvas 内にカプセル化した場合の結果を調べてみましょう。
8 つの要素を持つレイアウト グループを作成し、そのうちの 1 つを SubCanvas 内に配置して、その変換を変更します。
SubCanvas での RectTransform の変更:
結果が示すように、SubCanvas 内に 1 つの要素をカプセル化すると、レイアウトの負荷がほぼなくなります。これは、SubCanvas がすべての変更を分離し、階層の上位レベルでの再構築を防ぐためです。
ただし、キャンバス内の変更はキャンバス外の要素の配置には影響しないことに注意してください。したがって、要素を拡大しすぎると、隣接する要素と重なるリスクがあります。
8 つのレイアウト要素を SubCanvas にラップして進めていきましょう。
前の例では、レイアウトの負荷は低いままですが、バッチ更新が 2 倍になっていることが示されています。つまり、要素を複数の SubCanvas に分割すると、レイアウト ビルドの負荷は軽減されますが、バッチ アセンブリの負荷は増加します。その結果、全体としてはマイナスの影響が出る可能性があります。
さて、別の実験をしてみましょう。まず、8 つの要素を持つレイアウト グループを作成し、次にアニメーターを使用してレイアウト要素の 1 つを変更します。
アニメーターは RectTransform を新しい値に調整します。
ここでは、すべてを手動で変更した 2 番目の例と同じ結果が表示されます。これは論理的であり、RectTransform を変更するために何を使用しても違いはありません。
アニメーターは RectTransform を同様の値に変更します。
アニメーターは以前、フレームごとに同じ値を上書きし続けるという問題に直面していました。その値が変わらない場合でも、レイアウトの再構築が誤ってトリガーされていました。幸いなことに、Unityの新しいバージョンではこの問題は解決されており、代替手段に切り替える必要がなくなりました。
ここで、8 つの要素を持つレイアウト グループ内でテキスト値を変更するとどのように動作するか、またレイアウトの再構築がトリガーされるかどうかを確認してみましょう。
再構築もトリガーされていることがわかります。
ここで、8 つの要素のレイアウト グループ内の TextMechPro の値を変更します。
TextMechPro もレイアウトの再構築をトリガーし、通常のテキストよりもバッチ処理とレンダリングに負荷がかかるように見えます。
8 つの要素のレイアウト グループ内の SubCanvas の TextMechPro 値を変更します。
SubCanvas は変更を効果的に分離し、レイアウトの再構築を防止します。ただし、バッチ更新の負荷は減少したとはいえ、比較的高いままです。これは、各文字が個別のテクスチャとして扱われるため、テキストを操作するときに問題になります。結果として、テキストを変更すると、複数のテクスチャに影響します。
ここで、レイアウト グループ内で GameObject (GO) のオン/オフを切り替えるときに発生する負荷を評価してみましょう。
8 つの要素のレイアウト グループ内の GameObject のオン/オフを切り替えます。
ご覧のとおり、GO をオンまたはオフにすると、レイアウトの再構築もトリガーされます。
8 つの要素のレイアウト グループを持つ SubCanvas 内で GO をオンにします。
この場合、SubCanvas も負荷の軽減に役立ちます。
ここで、レイアウト グループを使用して GO 全体をオンまたはオフにした場合の負荷を確認してみましょう。
結果が示すように、負荷はこれまでで最高レベルに達しました。ルート要素を有効にすると、子要素のレイアウト再構築がトリガーされ、その結果、バッチ処理とレンダリングの両方に大きな負荷がかかります。
では、過度の負荷をかけずに UI 要素全体を有効または無効にする必要がある場合はどうすればよいでしょうか。GO 自体を有効または無効にする代わりに、Canvas または Canvas Group コンポーネントを無効にするだけで済みます。さらに、Canvas Group のアルファ チャネルを 0 に設定すると、パフォーマンスの問題を回避しながら同じ効果が得られます。
Canvas Group コンポーネントを無効にすると、負荷は次のように変化します。キャンバスが無効になっている間も GO は有効なままなので、レイアウトは保持されますが、単に表示されなくなります。このアプローチにより、レイアウトの負荷が低くなるだけでなく、バッチ処理とレンダリングの負荷も大幅に軽減されます。
次に、レイアウト グループ内で SiblingIndex を変更した場合の影響を調べてみましょう。
8 つの要素のレイアウト グループ内で SiblingIndex を変更する:
ご覧のとおり、レイアウトの更新には 0.7 ミリ秒かかり、負荷は依然として大きいままです。これは、SiblingIndex の変更によってレイアウトの再構築もトリガーされることを明確に示しています。
ここで、別のアプローチを試してみましょう。SiblingIndex を変更する代わりに、レイアウト グループ内の 2 つの要素のテクスチャを交換します。
8 つの要素のレイアウト グループ内の 2 つの要素のテクスチャを交換します。
ご覧のとおり、状況は改善されておらず、むしろ悪化しています。テクスチャを置き換えると、再構築もトリガーされます。
それでは、カスタム レイアウト グループを作成しましょう。8 つの要素を作成し、そのうちの 2 つの位置を入れ替えるだけです。
8 つの要素を持つカスタム レイアウト グループ:
負荷は確かに大幅に減少しました。これは予想どおりです。この例では、スクリプトは 2 つの要素の位置を交換するだけで、重い GetComponent 操作とすべての要素の位置の再計算の必要性がなくなります。その結果、バッチ処理に必要な更新が少なくなります。このアプローチは万能薬のように思えますが、スクリプトで計算を実行すると全体的な負荷も増加することに注意することが重要です。
レイアウト グループに複雑さが増すと、必然的に負荷が増加しますが、計算はスクリプト内で行われるため、レイアウト セクションに必ずしも反映されるわけではありません。そのため、コードの効率を自分で監視することが重要です。ただし、シンプルなレイアウト グループの場合は、カスタム ソリューションが最適な選択肢となります。
レイアウトの再構築は大きな課題です。この問題に対処するには、さまざまな根本原因を特定する必要があります。レイアウトの再構築につながる主な要因は次のとおりです。
Unity の新しいバージョンでは問題がなくなったが、以前のバージョンでは問題となっていた、同じテキストを上書きしたり、アニメーターで同じ値を繰り返し設定したりするといった点について強調しておくことが重要です。
レイアウトの再構築を引き起こす要因を特定したので、解決策のオプションをまとめてみましょう。
SubCanvas で再構築をトリガーする GameObject (GO) をラップします。このアプローチでは変更が分離され、階層の上位にある他の要素に影響が及ぶのを防ぎます。ただし、注意が必要です。SubCanvas が多すぎると、バッチ処理の負荷が大幅に増加する可能性があります。
GO の代わりに、SubCanvas または Canvas Group をオン/オフにします。新しい GO を作成するのではなく、オブジェクト プールを使用します。この方法では、レイアウトがメモリ内に保持されるため、再構築せずに要素をすばやくアクティブ化できます。
シェーダー アニメーションを活用します。シェーダーを使用してテクスチャを変更しても、レイアウトの再構築はトリガーされません。ただし、テクスチャが他の要素と重なる可能性があることに注意してください。この方法は、実質的に SubCanvases を使用するのと同様の目的を果たしますが、シェーダーを記述する必要があります。
Unity のレイアウト グループをカスタム レイアウト グループに置き換えます。Unityのレイアウト グループの主な問題の 1 つは、各 LayoutElement が再構築中に GetComponent を呼び出すことです。これはリソースを大量に消費します。カスタム レイアウト グループを作成するとこの問題に対処できますが、独自の課題があります。カスタム コンポーネントには、効果的に使用するために理解する必要がある特定の操作要件がある場合があります。それでも、このアプローチは、特に単純なレイアウト グループのシナリオではより効率的です。