Erfahren Sie in diesem ausführlichen Handbuch, wie Sie die UI-Leistung in Unity optimieren können, und sehen Sie sich die Ergebnisse mit zahlreichen Experimenten, praktischen Ratschlägen und Leistungstests an.
Hallo! Ich bin Sergey Begichev, Client Developer bei Pixonic (MY.GAMES). In diesem Beitrag bespreche ich die UI-Optimierung in Unity3D. Das Rendern einer Reihe von Texturen mag zwar einfach erscheinen, kann aber zu erheblichen Leistungsproblemen führen. In unserem War Robots-Projekt beispielsweise machten nicht optimierte UI-Versionen bis zu 30 % der gesamten CPU-Last aus – eine erstaunliche Zahl!
Normalerweise tritt dieses Problem unter zwei Bedingungen auf: Erstens, wenn zahlreiche dynamische Objekte vorhanden sind, und zweitens, wenn Designer Layouts erstellen, bei denen zuverlässige Skalierung über verschiedene Auflösungen hinweg Priorität hat. Selbst eine kleine Benutzeroberfläche kann unter diesen Umständen eine spürbare Belastung erzeugen. Lassen Sie uns untersuchen, wie dies funktioniert, die Ursachen der Belastung ermitteln und mögliche Lösungen besprechen.
Lassen Sie uns zunächst überprüfen
Während die Punkte 2 und 3 intuitiv klar sind, kann es schwierig sein, sich die restlichen Empfehlungen in der Praxis vorzustellen. Der Ratschlag, „Ihre Canvases in Sub-Canvases aufzuteilen“, ist beispielsweise sicherlich wertvoll, aber Unity bietet keine klaren Richtlinien zu den Prinzipien hinter dieser Aufteilung. Ich persönlich möchte in praktischer Hinsicht wissen, wo es am sinnvollsten ist, Sub-Canvases zu implementieren.
Beachten Sie den Ratschlag, „Layoutgruppen zu vermeiden“. Obwohl sie zu einer hohen UI-Last beitragen können, verfügen viele große UIs über mehrere Layoutgruppen, und die Überarbeitung aller Elemente kann zeitaufwändig sein. Darüber hinaus verbringen Layoutdesigner, die Layoutgruppen vermeiden, möglicherweise deutlich mehr Zeit mit ihren Aufgaben. Daher wäre es hilfreich zu verstehen, wann solche Gruppen vermieden werden sollten, wann sie von Vorteil sein können und welche Maßnahmen zu ergreifen sind, wenn wir sie nicht eliminieren können.
Diese Mehrdeutigkeit der Empfehlungen von Unity ist ein Kernproblem – es ist oft unklar, welche Grundsätze wir auf diese Vorschläge anwenden sollten.
Um die UI-Leistung zu optimieren, ist es wichtig zu verstehen, wie Unity die UI erstellt. Das Verständnis dieser Phasen ist für eine effektive UI-Optimierung in Unity von entscheidender Bedeutung. Wir können in diesem Prozess im Großen und Ganzen drei Schlüsselphasen identifizieren:
Layout . Zunächst ordnet Unity alle UI-Elemente anhand ihrer Größe und zugewiesenen Position an. Diese Positionen werden im Verhältnis zu Bildschirmrändern und anderen Elementen berechnet und bilden eine Kette von Abhängigkeiten.
Batching . Als nächstes gruppiert Unity einzelne Elemente in Batches, um ein effizienteres Rendering zu ermöglichen. Das Zeichnen eines großen Elements ist immer effizienter als das Rendern mehrerer kleinerer. (Weitere Informationen zum Batching finden Sie unter
Rendering . Zum Schluss zeichnet Unity die gesammelten Batches. Je weniger Batches vorhanden sind, desto schneller ist der Rendering-Prozess.
Zwar sind auch noch andere Elemente in den Prozess eingebunden, doch diese drei Phasen sind für die Mehrzahl der Probleme verantwortlich. Konzentrieren wir uns daher zunächst auf diese.
Im Idealfall können wir das Layout einmal erstellen, einen einzigen großen Stapel erstellen und ihn effizient rendern, wenn unsere Benutzeroberfläche statisch bleibt, d. h. sich nichts bewegt oder ändert.
Wenn wir jedoch die Position auch nur eines Elements ändern, müssen wir dessen Position neu berechnen und den betroffenen Batch neu erstellen. Wenn andere Elemente von dieser Position abhängen, müssen wir auch deren Positionen neu berechnen, was einen Kaskadeneffekt in der gesamten Hierarchie verursacht. Und je mehr Elemente angepasst werden müssen, desto höher wird die Batch-Last.
Änderungen am Layout können also einen Welleneffekt in der gesamten Benutzeroberfläche auslösen, und unser Ziel ist es, die Anzahl der Änderungen zu minimieren. (Alternativ können wir versuchen, Änderungen zu isolieren, um eine Kettenreaktion zu verhindern.)
In der Praxis ist dieses Problem besonders ausgeprägt, wenn Layoutgruppen verwendet werden. Bei jedem Neuaufbau eines Layouts führt jedes LayoutElement eine GetComponent-Operation aus, was sehr ressourcenintensiv sein kann.
Sehen wir uns eine Reihe von Beispielen an, um die Leistungsergebnisse zu vergleichen. (Alle Tests wurden mit Unity Version 2022.3.24f1 auf einem Google Pixel 1-Gerät durchgeführt.)
In diesem Test erstellen wir eine Layoutgruppe mit einem einzelnen Element und analysieren zwei Szenarien: eines, in dem wir die Größe des Elements ändern, und ein anderes, in dem wir die Eigenschaft FillAmount verwenden.
Änderungen bei RectTransform:
FlllBetragsänderungen:
Im zweiten Beispiel versuchen wir dasselbe, aber in einer Layoutgruppe mit 8 Elementen. In diesem Fall ändern wir immer noch nur ein Element.
Änderungen bei RectTransform:
FlllBetragsänderungen:
Wenn im vorherigen Beispiel Änderungen an RectTransform zu einer Belastung des Layouts von 0,2 ms führten, erhöht sich die Belastung dieses Mal auf 0,7 ms. Ebenso steigt die Belastung durch Batch-Updates von 0,65 ms auf 1,10 ms.
Obwohl es sich immer noch nur um eine Änderung an einem Element handelt, hat die erhöhte Größe des Layouts erhebliche Auswirkungen auf die Belastung während des Neuaufbaus.
Wenn wir dagegen den FillAmount eines Elements anpassen, beobachten wir selbst bei einer größeren Anzahl von Elementen keine erhöhte Last. Dies liegt daran, dass die Änderung des FillAmount keinen Layoutneuaufbau auslöst, was nur zu einer geringfügigen Erhöhung der Batch-Update-Last führt.
In diesem Szenario ist die Verwendung von FillAmount eindeutig die effizientere Wahl. Die Situation wird jedoch komplexer, wenn wir die Skalierung oder Position eines Elements ändern. In diesen Fällen ist es schwierig, die integrierten Mechanismen von Unity zu ersetzen, die keinen Layoutneuaufbau auslösen.
Hier kommen SubCanvases ins Spiel. Sehen wir uns die Ergebnisse an, wenn wir ein veränderbares Element in einem SubCanvas einkapseln.
Wir erstellen eine Layoutgruppe mit 8 Elementen, von denen eines in einem SubCanvas untergebracht wird, und ändern dann seine Transformation.
RectTransform-Änderungen in SubCanvas:
Wie die Ergebnisse zeigen, wird durch die Kapselung eines einzelnen Elements in einem SubCanvas die Belastung des Layouts nahezu eliminiert. Dies liegt daran, dass SubCanvas alle Änderungen isoliert und so einen Neuaufbau in den höheren Ebenen der Hierarchie verhindert.
Es ist jedoch wichtig zu beachten, dass Änderungen innerhalb der Leinwand keinen Einfluss auf die Positionierung von Elementen außerhalb der Leinwand haben. Wenn wir die Elemente daher zu stark erweitern, besteht das Risiko, dass sie sich mit benachbarten Elementen überlappen.
Fahren wir fort, indem wir 8 Layout-Elemente in ein SubCanvas einbinden:
Das vorherige Beispiel zeigt, dass sich die Batch-Aktualisierung verdoppelt hat, obwohl die Belastung des Layouts gering bleibt. Dies bedeutet, dass die Aufteilung der Elemente in mehrere SubCanvases zwar zur Reduzierung der Belastung des Layoutaufbaus beiträgt, die Belastung der Batch-Assemblierung jedoch zunimmt. Dies könnte insgesamt zu einem negativen Nettoeffekt führen.
Lassen Sie uns nun ein weiteres Experiment durchführen. Zuerst erstellen wir eine Layoutgruppe mit 8 Elementen und ändern dann eines der Layoutelemente mit dem Animator.
Der Animator passt RectTransform auf einen neuen Wert an:
Hier sehen wir das gleiche Ergebnis wie im zweiten Beispiel, wo wir alles manuell geändert haben. Das ist logisch, da es keinen Unterschied macht, womit wir RectTransform ändern.
Der Animator ändert RectTransform in einen ähnlichen Wert:
Animatoren hatten zuvor das Problem, dass sie in jedem Frame kontinuierlich denselben Wert überschrieben, selbst wenn dieser Wert unverändert blieb. Dies löste versehentlich einen Layout-Neuaufbau aus. Glücklicherweise haben neuere Versionen von Unity dieses Problem behoben, sodass es nicht mehr nötig ist, auf alternative
Schauen wir uns nun an, wie sich die Änderung des Textwertes innerhalb einer Layoutgruppe mit 8 Elementen verhält und ob dadurch ein Layoutneuaufbau ausgelöst wird:
Wir sehen, dass auch der Neuaufbau ausgelöst wird.
Jetzt ändern wir den Wert von TextMechPro in der Layoutgruppe mit 8 Elementen:
TextMechPro löst außerdem einen Layout-Neuaufbau aus und es sieht sogar so aus, als würde es die Stapelverarbeitung und das Rendering stärker belasten als normaler Text.
Ändern des TextMechPro-Wertes in SubCanvas in einer Layoutgruppe mit 8 Elementen:
SubCanvas hat die Änderungen effektiv isoliert und so einen Neuaufbau des Layouts verhindert. Obwohl die Belastung durch Batch-Updates gesunken ist, bleibt sie relativ hoch. Dies wird bei der Arbeit mit Text zu einem Problem, da jeder Buchstabe als separate Textur behandelt wird. Eine Änderung des Textes wirkt sich folglich auf mehrere Texturen aus.
Lassen Sie uns nun die Belastung auswerten, die beim Ein- und Ausschalten eines GameObjects (GO) innerhalb der Layoutgruppe entsteht.
Ein- und Ausschalten eines GameObjects innerhalb einer Layoutgruppe mit 8 Elementen:
Wie wir sehen, löst das Ein- oder Ausschalten eines GO auch einen Layout-Neuaufbau aus.
Aktivieren eines GO innerhalb eines SubCanvas mit einer Layoutgruppe aus 8 Elementen:
Auch hier hilft SubCanvas zur Entlastung.
Nun prüfen wir, wie hoch die Belastung ist, wenn wir das gesamte GO mit einer Layout-Gruppe ein- oder ausschalten:
Wie die Ergebnisse zeigen, hat die Belastung ihren bisher höchsten Stand erreicht. Das Aktivieren des Stammelements löst einen Layoutneuaufbau für die untergeordneten Elemente aus, was wiederum zu einer erheblichen Belastung sowohl beim Batching als auch beim Rendering führt.
Was können wir also tun, wenn wir ganze UI-Elemente aktivieren oder deaktivieren müssen, ohne eine übermäßige Belastung zu verursachen? Anstatt das GO selbst zu aktivieren und zu deaktivieren, können Sie einfach die Canvas- oder Canvas-Gruppenkomponente deaktivieren. Darüber hinaus kann das Setzen des Alphakanals der Canvas-Gruppe auf 0 den gleichen Effekt erzielen und gleichzeitig Leistungsprobleme vermeiden.
Folgendes passiert mit der Last, wenn wir die Canvas Group-Komponente deaktivieren. Da die GO aktiviert bleibt, während die Canvas deaktiviert ist, bleibt das Layout erhalten, wird aber einfach nicht angezeigt. Dieser Ansatz führt nicht nur zu einer geringen Layoutlast, sondern reduziert auch die Belastung beim Batching und Rendering erheblich.
Als Nächstes untersuchen wir die Auswirkungen einer Änderung des SiblingIndex innerhalb der Layoutgruppe.
Ändern des SiblingIndex innerhalb einer Layoutgruppe mit 8 Elementen:
Wie beobachtet, bleibt die Belastung mit 0,7 ms für die Aktualisierung des Layouts erheblich. Dies zeigt deutlich, dass Änderungen am SiblingIndex auch einen Layout-Neuaufbau auslösen.
Lassen Sie uns nun mit einem anderen Ansatz experimentieren. Anstatt den SiblingIndex zu ändern, tauschen wir die Texturen zweier Elemente innerhalb der Layoutgruppe aus.
Austauschen der Texturen zweier Elemente in einer Layoutgruppe aus 8 Elementen:
Wie wir sehen, hat sich die Situation nicht verbessert, sondern sogar verschlechtert. Das Ersetzen der Textur löst auch einen Neuaufbau aus.
Lassen Sie uns nun eine benutzerdefinierte Layoutgruppe erstellen. Wir konstruieren 8 Elemente und tauschen einfach die Positionen von zwei davon.
Benutzerdefinierte Layoutgruppe mit 8 Elementen:
Die Belastung hat tatsächlich deutlich abgenommen – und das war zu erwarten. In diesem Beispiel tauscht das Skript einfach die Positionen zweier Elemente aus, wodurch aufwändige GetComponent-Operationen und die Notwendigkeit, die Positionen aller Elemente neu zu berechnen, entfallen. Dadurch sind weniger Aktualisierungen für die Stapelverarbeitung erforderlich. Obwohl dieser Ansatz wie ein Allheilmittel erscheint, ist es wichtig zu beachten, dass die Durchführung von Berechnungen in Skripten ebenfalls zur Gesamtbelastung beiträgt.
Wenn wir mehr Komplexität in unsere Layoutgruppe einführen, wird die Belastung zwangsläufig zunehmen, aber das wird sich nicht unbedingt im Layoutabschnitt widerspiegeln, da die Berechnungen in Skripten erfolgen. Daher ist es wichtig, die Effizienz des Codes selbst zu überwachen. Für einfache Layoutgruppen können jedoch benutzerdefinierte Lösungen eine hervorragende Option sein.
Der Neuaufbau des Layouts stellt eine große Herausforderung dar. Um dieses Problem zu lösen, müssen wir die Grundursachen ermitteln, die unterschiedlich sein können. Hier sind die Hauptfaktoren, die zu Layoutneuaufbauten führen:
Es ist wichtig, einige Aspekte hervorzuheben, die in neueren Versionen von Unity keine Probleme mehr darstellen, in früheren jedoch schon: das Überschreiben desselben Textes und das wiederholte Festlegen desselben Werts mit einem Animator.
Nachdem wir nun die Faktoren identifiziert haben, die einen Layout-Neuaufbau auslösen, fassen wir unsere Lösungsoptionen zusammen:
Packen Sie ein GameObject (GO), das einen Neuaufbau auslöst, in ein SubCanvas. Dieser Ansatz isoliert Änderungen und verhindert, dass sie andere Elemente in der Hierarchie beeinflussen. Seien Sie jedoch vorsichtig – zu viele SubCanvases können die Belastung beim Batching erheblich erhöhen.
Schalten Sie SubCanvas oder Canvas Group anstelle von GO ein und aus. Verwenden Sie einen Objektpool, anstatt neue GOs zu erstellen. Diese Methode behält das Layout im Speicher bei und ermöglicht eine schnelle Aktivierung von Elementen, ohne dass ein Neuaufbau erforderlich ist.
Nutzen Sie Shaderanimationen. Das Ändern der Textur mithilfe eines Shaders löst keinen Neuaufbau des Layouts aus. Bedenken Sie jedoch, dass sich Texturen mit anderen Elementen überlappen können. Diese Methode dient effektiv einem ähnlichen Zweck wie die Verwendung von SubCanvases, erfordert jedoch das Schreiben eines Shaders.
Ersetzen Sie die Layoutgruppe von Unity durch eine benutzerdefinierte Layoutgruppe. Eines der Hauptprobleme bei den Layoutgruppen von Unity besteht darin, dass jedes LayoutElement beim Neuaufbau GetComponent aufruft, was ressourcenintensiv ist. Das Erstellen einer benutzerdefinierten Layoutgruppe kann dieses Problem lösen, bringt aber seine eigenen Herausforderungen mit sich. Benutzerdefinierte Komponenten haben möglicherweise bestimmte Betriebsanforderungen, die Sie für eine effektive Verwendung verstehen müssen. Dennoch kann dieser Ansatz effizienter sein, insbesondere bei einfacheren Layoutgruppenszenarien.