Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.
Comment: Kapitel über Performance

...

World Space UI kann vor allem hilfreich sein, um sog. diegetische UI in Spielen mit variabler Tiefe zu erstellen.

Nested Canvasses

Einen Beispiel Artikel von Unity findet man hier.

Die grundsätzliche Idee ist es UI in statische bzw. dynamische Elemente und damit auch Canvasse zu unterteilen. Dadurch sollen Elemente die sich manchmal bis sehr häufig verändern von denen, die das gar nicht tun, abgeschottet werden, damit sie nicht in Neu-Berechnungen eingeschlossen werden.

Im Artikel von Unity geht es um einen Timer. Dieser besteht aus einem Text und einer Zeitanzeige. Es ist klar, dass sich der Text "Current time" nie ändern wird, während die Anzeige der Zeit sich kontinuierlich updated. Entsprechend bringen wird die Zeitanzeige in ein eigenes Subcanvas, unter dem Canvas auf dem der Text liegt. Wenn sich jetzt die Zeit verändert, wird nur das Subcanvas neu berechnet - der Text bleibt unberührt.

Performance Tipps

Um bessere Performance aus der UI herauszuholen müssen wir uns vor allem zweier Dinge bewusst sein:

  1. Sämtliche UI ist transparent.
  2. Wie funktionieren Canvas und Layouts oder genauer gesagt, wann müssen sie erneuert werden?

Bevor es jetzt weiter geht noch ein Hinweis: Wie bei allem was mit Performance zu tun hat, gibt es kaum einen universell besten Weg Dinge anzugehen, sondern spezifische Lösungen für Probleme mit der eigenen UI. Diese Lösungen können dann wieder an anderen Stellen Dinge kaputt machen, beispielsweise sind mehrere Canvasses eine gute Idee, jedoch erhöhen sie auch die Anzahl an Batches; Spritemesh wird Overdraw verringern, allerdings gibt es jetzt mehr Polygone und man muss die Importeinstellung "Tight" verwenden. Es ist also ein Ausprobieren und Abschätzen, was für einen den meisten Mehrwert bringt. Am Ende kann wahrscheinlich nur der Profiler wirklich bei der Entscheidung helfen.

Overdraw verringern

Gerade auf Mobile ist Overdraw ein Performance-Killer. Er entsteht, wenn sehr viele transparente Objekte über einander liegen. Es werden also oft Pixel gerendered, die später im Rendervorgang überschrieben werden, weil sie tatsächlich verdeckt sind.

Nochmal: Die gesamte UI ist transparent. 

Es ist also allgemein ratsam beim Bau von Elementen darauf zu achten, dass alle statischen Elemente zu einer Textur zusammengeführt werden. Hierzu ein Beispiel:
[Beispiel vorher nachher]

Man kann bereits beim Export von Grafiken darauf achten, dass sie möglichst wenig leere Pixel enthalten und beispielsweise nicht unvorteilhaft rotiert sind.

[Beispiel]

Bei sehr detailierten Sprites kann es sich lohnen, die Funktion "Use Sprite Mesh" der Image-Komponente zu verwenden.

[Beispiel vorher nachher]

Man kann sehen, dass nun ein mehr oder minder genaues Mesh des Sprites verwendet wird, anstelle eines einzelnen Rechtecks. Dafür wird allerdings die Einstellung "Mesh Type - Tight" beim Import benötigt.

Unity bietet Möglichkeiten an Overdraw zu visualisieren - welche es gibt ist abhängig von der verwendeten Renderpipeline. Der Modus für die Built-in-Renderpipeline sieht beispielsweise so aus:

[Beispiel]

Effiziente Nutzung des Canvas

Allgemein wird der Inhalt von Canvasses über ein generiertes dynamisches Mesh gerendered. Dieses Mesh wird jedes mal neu generiert wenn sich etwas innerhalb des Canvas verändert. Wenn sich also ein einzelnes Element verändert, muss das ganze Canvas neu generiert werden. Zusätzlich dazu muss auch noch das Layout neu generiert werden - warum das ebenfalls schlecht ist kommt später im Abschnitt Layouts.

Eine erste Faustregel ist es also mehrere Canvasses zu benutzen und nicht die gesamte UI in ein riesiges Canvas zu Packen.

Im nächsten Schritt kann man dann ein jedes Canvas noch unterteilen, indem man Sub - bzw. Nested Canvasses hinzufügt.

Nested Canvasses

Einen Beispiel Artikel von Unity findet man hier.

Die grundsätzliche Idee ist es UI in statische bzw. dynamische Elemente und damit auch Canvasse zu unterteilen. Dadurch sollen Elemente die sich manchmal bis sehr häufig verändern von denen, die das gar nicht tun, abgeschottet werden, damit sie nicht in Neu-Berechnungen eingeschlossen werden.

Im Artikel von Unity geht es um einen Timer. Dieser besteht aus einem Text und einer Zeitanzeige. Es ist klar, dass sich der Text "Current time" nie ändern wird, während die Anzeige der Zeit sich kontinuierlich updated. Entsprechend bringen wird die Zeitanzeige in ein eigenes Subcanvas, unter dem Canvas auf dem der Text liegt. Wenn sich jetzt die Zeit verändert, wird nur das Subcanvas neu berechnet - der Text bleibt unberührt.

[Beispiel]

Layouts
Anchor
PerformanceLayouts
PerformanceLayouts

Auto Layout funktioniert über ein sog. "dirty flag"-System. Wenn sich ein Layoutelement verändert und es damit einen umgebenden Layoutcontroller ungültig macht (z.B. Size oder Scale verändert), wird es als "dirty" markiert, worauf das Layoutsystem dann reagieren kann.

Problem: Layoutelemente sind Komponenten, also kann auf jedem Element oder auch Parent eins oder mehrere sein.

Um die Neu-Berechnung des Layouts korrekt auszuführen, wird nach dem Layoutcontroller gesucht, der am weitesten oben in der Hierarchie steht. Das ganze passiert natürlich über GetComponent() auf jedem Objekt. So wird jedes Element, dass sein Layout auf dirty setzen will, minimal einen Aufruf von GetComponent() nach sich ziehen. Jeder geschaltete Layoutcontroller vervielfältigt dieses Problem.

Wodurch wird ein Layout als dirty markiert?

Kurzform: Durch fast alles ...

Nur ein paar Beispiele:

  • OnEnable() und OnDisable(),
  • Reparenting,
    Doppelt; einmal für den alten und neuen Parent
  • OnDidApplyAnimationProperties(),
    Wenn ein Animator mit dem Element interagiert
  • OnRectTransformDimensionsChanged()
    z.B. Skalieren oder Resizing, Änderung von Position oder Kamera


Lösungen: Was kann man dagegen tun?

  • Vermeidet Layoutelemente wo es geht,
    Oft genug reicht es nur mit festen oder relativen Ankern zu arbeiten.
  • Deaktiviert Layoutelemente nachdem sie ihre Arbeit getan haben,
    Viele Layouts müssen sich nur einmal nach dem Laden des Spiels aufbauen und verändern sich dann nicht mehr. Das gilt insbesondere für statische Anzeigen. Auf all diesen Objekten kann man die Layoutelemente deaktivieren, um sich das dirtying zu sparen.
  • Deaktiviert die Canvas Komponente an Stelle des GameObjects, wenn ihr ein ganzes Canvas ausblenden wollt,
    Dadurch wird nichts mehr angezeigt, aber man spart sich OnDisable() und OnEnable() + dirtying.
  • Suche nach zuständigem Layoutcontroller über leeres Objekt unterbrechen.
    Die Suche nach oben wird beendet, sobald kein Layoutcontroller mehr gefunden wird. Das ist vor allem hilfreich, wenn man Objekte über eine Animation skalieren will, dies aber nicht das Layout beeinflussen soll (z.B. Button drücken mit leichtem bounce).

Der Animator

Kurz gesagt: Benutzt keinen Animator für eure UI. Stattdessen solltet ihr Code und/oder Tweens verwenden um eure UI zu animieren. Für Tweens gibt es das sehr gute Package DotweenPro.

[Beispiel]

Warum ist ein Animator Setup, wie auf dem Standard-Button von Unity, schlecht?

Ein Animator wendet jeden Frame seine Werte auf die animierten Objekte an, egal ob sich Werte in der Animation verändert haben oder nicht. Das bedeutet, dass jeden Frame alle animierten Objekte als dirty markiert werden, was wie bereits beschrieben diverse Aktionen im Layout bzw. Canvas Code nach sich zieht.

Entsprechend ist der Animator nicht dafür geeignet States abzubilden - wie z.B. hover, selected im Standard-Button Beispiel - sondern sollte nur zum Abspielen von Animationen verwendet werden, da sich in diesem meist sowieso jeden Frame etwas verändert, was dann dirtying nach sich zieht.

Graphics Raycaster

Den Graphics Raycaster benötigt man auf jedem Canvas (und Sub-Canvas!) das (Touch-)Input erhalten soll. Tatsächlich ist er nicht wirklich ein Raycaster, sondern prüft ob sich ein Punkt innerhalb eines Rechtecks befindet und das für jedes RectTransform unter dem Canvas, dass als interaktiv markiert ist. Als interaktiv markiert sind alle Komponenten mit dem Toggle "Raycast Target" auf an (beispielsweise Image).

[Beispiel]

Faustregel: Achte darauf den Toggle auszuschalten, wo es Sinn macht![Beispiel]

Sorting Group

Mit der Sorting Group gruppiert man ein GameObject und seine Childs als Einheit für das Rendern - ähnlich wie es das Canvas tut. Das bedeutet, dass außerhalb der Gruppe die Sortieroptionen und die Z-Position des Gruppen-Parents für alle Childs gelten.

...

Ein Child ist standardmäßig zum Zentrum geankert. Häufige andere Konfigurationen können als Presets gefunden werden, indem man auf das Anchor Preset Symbol links oben im Rect Transform klickt.

[Beispiel]

Auto Layout

Layout Elemente

Ein Layoutelement ist grundsätzlich erst einmal jedes GameObject mit einem Rect Transform. Es weiß im Grunde genommen, welche Size es haben sollte, setzt diese aber nicht selbst - lediglich die Information wird an die sog. Layout Controller weitergegeben, damit diese dem Element eine Size zuweisen können.

...

In diesem Fall wachsen beide Elemente gleichmäßig, bis der Raum vollständig aufgebraucht ist. Element A endet hier bei in etwa 44 units. Element B bei 56. Beide Elemente sind ähnlich weit von ihrer Preferred Size entfernt - Element B ist näher dran, weil die Differenz zwischen Minimum Size und Preferred Size bei ihm geringer ausfiel.

Layout Controller

Layout Controller kontrollieren die Size und/oder Position von einem oder mehreren Layoutelement(en). Es gibt zwei Sorten von Layout Controllern:

  • Fitter,
  • Gruppen.

Fitter Komponenten

Fitter kontrollieren immer ihr eigenes Layout Element, also das Objekt, auf dem sie platziert sind.

Aspect Ratio Fitter

Der Aspect Ratio Fitter verändert das Element so, dass es eine bestimmte Aspect Ratio einhält. Dafür orientiert er sich an den Maßen seines Parents. Für diese Orientierung gibt es verschiedene Modi:

...

Größtes Problem des Aspect Ratio Fitters ist, dass man ihn nur nutzen kann, wenn das Element nicht von einer Layoutgruppe gesteuert wird. Ein Problem was sich auch bei der nächsten Komponente fortsetzt.

Content Size Fitter

Der Content Size Fitter setzt die Maße des Elements basierend auf einem Layout Component auf dem gleichen Objekt. Das können beispielsweise Layoutgruppen, Images oder Text sein. Er verfügt über folgende Optionen, die jeweils auf die horizontale oder vertikale Achse angewendet werden können:

...

Für mich leistet der Content Size Fitter seine Arbeit vor allem auf Root Elementen von Panels, um automatisch die richtige Größe einzustellen. Eine weitere gute Anwendung ist in Kombination mit Text, wo der Content Size Fitter den Text Container wachsen lässt um den Text bei gleicher Schriftgröße einzufassen. Er ist ebenfalls sehr nützlich für die Gridlayoutgruppe.

Gruppen Komponenten

Gruppen kontrollieren Größe und Position ihrer Childs.

...

Es ist wichtig, dass eine Gruppe groß genug ist, um all ihre Childs einzufassen. Sollte eine Achse zu klein sein, beginnen Childs aus der Gruppe herauszuragen. Am einfachsten kann man das über Fitter Komponenten wie den Content Size Fitter verhindern, welche die Layout-Informationen der Gruppe nutzen, um ihre Maße entsprechend ihrer Childs anzupassen.

Horizontal/Vertikal

Horizontale bzw. vertikale Gruppen platzieren ihre Childs nebeneinander auf der entsprechenden Achse.

...

Wenn ja - Die Elemente werden so verändert, dass sie den Raum voll ausfüllen.
Wenn auf der entsprechenden Achse auch die Size kontrolliert wird wachsen alle Elemente, bis die Gruppe ausgefüllt ist.
Falls die Size nicht kontrolliert wird, entsteht Spacing zwischen den Elementen.

Grid

Eine Gridlayoutgruppe platziert Childs auf einem Grid. Sie verfügt über folgende zusätzliche Optionen:

...