Glossar

Parent = Elternelement

Child = Kindelement

Aspect Ratio = Seitenverhältnis

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

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:


Lösungen: Was kann man dagegen tun?

Der Animator

Kurz gesagt: Benutzt keinen Animator für die 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!

Device Simulator

[Beispiel - wie schaltet man ihn an?]

Mit dem Device Simulator kann man sehen, wie sich unter anderem die UI auf verschiedenen Geräten verhält. Es ist auch möglich das Gerät zu drehen und die Safe Zone anzeigen zu lassen.

[Beispiel]

Man sollte stets verschiedenste gängige Geräte testen - mindestens Android bzw. iOS jeweils als Phone und Tablet. Insbesondere bei der Safe Zone unterscheiden sich die Geräte sehr:

[Beispiel]

Apple macht die Safe Zone gerne symmetrisch und hat außerdem eine vertikale Safe Zone für den Home-Button.

[Beispiel]

Android ist hingegen oft asymmetrisch.

[Beispiel]

Tablets haben oft genug gar keine Safe Zone.


Um all diese Dinge sehen zu können und entsprechend mit Code oder Design zu reagieren, ist der Device Simulator unabdingbar.

UI Design Tools

Tools für UI- oder UX-Design, je nach dem wie genau man mit dem Namen ist, helfen massiv bei der Arbeit an UI. Von Wireframes bis hin zu Click-Prototypen kann man damit alles erstellen und das meistens sogar kollaborativ. Entwickler können später als Betrachter hinzugefügt werden, damit sie alle Layouts, Abstände und Größen genauestens ins Spiel bringen können.

Figma ist ein cloudbasiertes UI-Tool, was ich in den meisten meiner Projekte verwendet habe.

[Beispiele]

-zeigen: Basic layout in figma, inspector

Komponenten

Komponenten sind wiederverwendbare Elemente im eigenen Design. Sie können alles Mögliche sein, von einem simplen Button bis hin zu einer kompletten Navigationsleiste. Ein paar der geläufigsten Kandidaten für Komponenten sind:

[Beispiel]

Sobald man eine Komponente erstellt hat, kann man Kopien von dieser im Projekt verwenden. Die Kopien der sog. Master-Komponente sind mit ihr verbunden, das heißt wenn sich die Master-Komponente ändert, werden sich auch alle Kopien entsprechend verändern. Die Kopien selbst können aber immer noch lokal geändert werden - so kann ein und derselbe Button an verschiedenen Stellen in unterschiedlicher Farbe erscheinen, vielleicht auch nur um rumzuprobieren. Man kann dann später die lokalen Änderungen auf die Master-Komponente anwenden, wenn man sie für alle Kopien übernehmen will.

Ein weiterer Vorteil ist, dass man Komponenten auch schachteln kann.

[Beispiel]

Im Grunde genommen kann man sich so eine eigene Bibliothek an UI-Elementen aufbauen, die dann in vielen Designs verwendet werden.

Mit Komponenten kann man sich viel Arbeit sparen, indem man Designs und strukturieren wiederverwendbar macht, während Änderungen am gesamten Design mit wenig Aufwand möglich sind.

Styles

Styles sind ähnlich wie Komponenten, nur das mit ihnen das Aussehen bzw. Eigenschaften von Elementen festgelegt werden. Dazu gehören Dinge wie Farben, Textgröße, Schlagschatten oder gar Einstellungen von Layout-Grids.

[Beispiel]

Der Vorteil ist dann, dass man durch Änderungen am Style alle Elemente verändert, die diesen Style verwenden. So könnte man beispielsweise den im Projekt verwendeten Font für Seitentitel austauschen.

[Beispiel]

Neben der erhöhten Iterationsgeschwindigkeit kann man so auch bestimmte Teile des eigenen Designs benennen, um z.B. die Verwendung einer Farbe deutlich zu machen, kann man einen Style mit dem Namen "Highlight Color" erstellen. Jedes neue Teammitglied, dass am UI-Design arbeitet kann dann über die Bibliothek auf diesen Style zugreifen und seine Verwendung verstehen.

Prefabs

Workflow

Bei der Arbeit mit Prefabs orientiere ich mich an dem Workflow in Figma. So kann man stumpf für jede Master-Komponente aus Figma ein eigenes Prefab erstellen. Solche Prefabs nenne ich "Templates". Templates sind idR. nicht dafür gedacht wirklich selbst verwendet zu werden, sondern um als Ausganspunkt für Prefab-Varianten zu dienen. Beispielsweise würde ich einen fertigen Button als Template erstellen - jedoch Dinge wie z.B. Grafiken auf einer Variante ausdefinieren. Auf diese Weise erhält man die selbe Funktionalität, wie die Master-Komponenten in Figma: Größere Änderungen am Design des Buttons sind schnell einzupflegen, weil sie sich auf alle Varianten auswirken, die keine im Konflikt stehenden Änderungen aufweisen.

[Beispiel]

Styles lassen sich leider nicht so einfach umsetzen, ohne eigenen Code zu schreiben. Einzig bei TextMeshPro liegt schon eine Implementierung seitens Unity vor. Grob gesehen reichen Prefab-Varianten für einige Anwendungsfälle von Styles aus - aber mit einem Klick die Highlight-Farbe im gesamten Projekt zu ändern bleibt leider aus.

Oft lohnt es sich lieber noch ein Prefab oder eine Variante mehr zu erstellen, anstatt zu viel in einem einzelnen vereinigen zu wollen. Versionen für links und rechts sind ein gutes Beispiel dafür:
Es scheint zunächst verlockend, die Grafiken und das Layout innerhalb des gleichen Prefabs einzubauen, man spart sich so eine weitere Datei, die es zu verwalten gilt, aber beim Animieren sind rechts und links oft genug zu unterschiedlich - deshalb lieber eine Variante links und eine Variante rechts.

[Beispiel - speechbubble?]

Ich versuche innerhalb meines Projektes möglichst wenige lokale Änderungen zu haben, da diese eingehende globale Änderungen von Templates blockieren. Nach Möglichkeit sollten Änderungen lieber eine eigene Variante oder ein neues Prefab werden - wie immer gilt, dass von Fall zu Fall zu entscheiden ist. Änderungen innerhalb von Szenen sind besonders nervig für Versionskontrolle, weshalb ich die gesamte UI innerhalb von eigenen Prefabs produziere. Es gibt also z.B. keinen Button, der nur in einer Szene eine andere Farbe annimmt. Einzig die fertige Hierarchie bzw. das Layering findet am Ende in der Szene statt. 

Struktur

Im Grunde genommen habe ich gelernt, UI-Elemente in drei verschiedene Klassen aufzuteilen:

Beginnen wir mit der View:

Eine Gruppe von Views ist ein Element:

Ganz oben in der Hierarchie steht das Panel:

Diese Aufteilung soll Ordnung in die ganzen Elemente und Prefabs bringen, indem sie:

  1. Bei der Benennung hilft,
  2. Hierarchien und damit auch,
  3. Zuständigkeiten vorgibt.

TODO: Idk beispiele oder genauer sagen, warum das gut ist?


Flexibles Spacing

Ich verwende drei verschiedene Wege um in meinen Layouts Abstände einzubauen:

Auf die Anwedungsgebiete, sowie Vor- und Nachteile möchte ich im Folgenden eingehen.

Spacing über die Gruppe

Der einfachste Weg ist es einen Wert für Spacing auf der Gruppe zu setzen.
Vorteil:

Nachteil:

Spacing als fixer Wert funktioniert am besten, wenn zuvor bereits absehbar ist, dass es problemlos eingehalten werden kann. Um dem Problem der fehlenden Flexibilität entgegenzuwirken, könnten die Childs der Gruppe flexible Größen haben, um eventuell zu schrumpfen oder zu wachsen - allerdings stellt sich hier die Frage, ob man festen Abständen so viel Priorität einräumen will.

Wichtig: Diese Spacing Variante unterstützt als einzige negative Werte, wodurch man Elemente überlappen lassen kann.

Spacing über Force Expand

Zur Wiederholung: Wenn auf einer Layoutgruppe die Option Child Force Expand angewählt ist, werden die Childs so verteilt, dass sie die Gruppe maximal ausfüllen. Wird die Größe der Childs nicht zusätzlich von der Gruppe kontrolliert, kann Leerraum entstehen, wenn die Gruppe größer als die Summe ihrer Childs ist.
Vorteil:

Nachteil:

Auf diese Art und Weise lässt sich unkompliziert dynamisches Spacing erzeugen - allerdings nur bis zu einer gewissen Mindestgröße der Gruppe. Ausschlaggebend für die Richtung des Spacings ist die Option Child Alignment:

[Beispiele]

Anzuwenden ist diese Technik, wenn auf der Achse des Spacings keine dynamische Größe existiert - zum Beispiel eine Gruppe von Bildern mit fixer Höhe, welche dynamischen Abstand auf der horizontalen hat. Die Nachteile könnte man beispielsweise ausbessern, indem die Gruppe selbst eine ausreichende Minimum Size hat, durch die dann ein Mindestmaß an Spacing gewährleistet ist.

Spacer Objekte mit Layout Element

Man kann ebenfalls ganze, quasi leere (sprich ohne Grafik) Objekte für Spacing benutzen. Diese enthalten dann nur ein Layoutelement, was der Gruppe ihre Size mitteilt.
Vorteil:

Nachteil:

[Beispiele]

Diese Variante gibt einem die meiste Flexibilität und Kontrolle, bringt aber auch mehr Komplexität mit sich. Diese Komplexität kann sich auf das gesamte Projekt ausweiten, wenn man Spacer als Prefabs verwendet. In der Theorie könnte man ähnlich wie in UX-Tools das Spacing in mehreren Elementen der UI mit nur einer Änderung auf dem Prefab anpassen. Das klingt erstmal sehr praktisch, kann aber auch gleichermaßen tückisch sein - es wird viel Achtsamkeit im Umgang mit solchen Strukturen gebraucht. Genau deswegen ist es in den meisten Fällen leichter auf die zuvor genannten einfacheren Optionen zurückzugreifen.

Eine wirklich gute Anwendung ist das synchronisieren der Abstände über mehrere Panels. Nehmen wir an, dass unser Spiel ein HUD besitzt, was stehts am oberen Bildschirmrand ist. Andere Elemente auf der gleichen Ebene sollten einen Abstand zum HUD wahren, um dieses nicht zu überlappen. Natürlich kann jedes weitere Element nun schlicht einen festen Abstand nach oben aufweisen, welcher dann die Höhe des HUDs und eventuelles Padding ist - was passiert aber, wenn das HUD nun durch ein neues Design seine Höhe ändert? Man muss loslaufen und diesen Wert auf jedem anderen Element aktualisieren. Um diese Arbeit zu sparen, kann schlicht ein Spacer Prefab erstellt werden, welches die selben Maße wie das HUD aufweist. Jetzt muss bei einer Änderung des Designs nur noch das Spacer Prefab angepasst werden und nicht jedes einzelne Objekt. Diese Technik hilft ebenfalls, wenn das HUD eine flexible Höhe hat, z.B. für den Porträt-Modus.

[Beispiele]

Flexibles Padding

Ähnlich wie beim Spacing kann manchmal auch beim Padding ein fixer Wert nicht ausreichend sein. Hierfür gibt es folgende Lösungen:

Flexibles Padding über die Anker

Vorteil:

Nachteil:

Indem man die Ankerpositionen korrekt wählt, kann man ebenfalls ein Padding relativ zur Größe des Parents erreichen.

Eine beispielhafte Konfiguration wäre Min (0.25,0) und Max (0.75, 1). Auf diese Art und Weise erhält man ein Padding von 25% auf der X-Achse, jeweils links und rechts.

[Beispiel]

Da das Padding immer relativ zum Parent ist, kann man leider keinen maximalen Wert für die Menge an Padding festlegen. Es bedeutet ebenfalls, dass das Padding nach Innen ist.

Size mit Layoutelement überschreiben

Zur Erinnerung: Bei Layoutcontrollern gibt es Priorisierung. Das Layoutelement hat die höchste Priorität und kann somit die Size der Layoutgruppe überschreiben. Wenn man dann die Layoutgruppe größer werden lässt, als deren Children das benötigen, entsteht Leerraum um die Gruppe herum - vorausgesetzt das die Option Child Force Expand auf der entsprechenden Achse nicht genutzt wird. Wo der Leerraum entsteht ist vom Alignment der Childs abhängig

[Beispiele]


Vorteil:

Nachteil:

Der eine Vorteil dieser Methode mag unscheinbar klingen, es ist jedoch nicht zu verachten, dass in dieser Variante die Padding-Einstellung noch gut nachvollziehbar ist und nicht so versteckt (wie bei der folgenden Variante).

Size der Childs manipulieren

Diese Methode funktioniert nur mit Images oder wenn Spacer Objekte innerhalb der Gruppe genutzt werden. Die Idee ist, dass wir Objekte größer werden lassen, ohne das optisch mehr Raum mit Grafik gefüllt wird. Wenn wir z.B. Spacern in einer Horizontalen Gruppe eine andere Höhe als die restlichen Elemente geben entsteht Padding auf der vertikalen Achse. Umgekehrtes funktioniert für die Vertikale Layoutgruppe - andere Breite für Spacer führt zu Padding auf der Horizontalen. Wenn wir Padding auf der kontrollierten Achse erzeugen wollen, fügen wir einfach am Anfang und Ende der Gruppe ein Spacer Objekt mit der Size des Paddings ein.

[Beispiele]

Ohne Spacer Objekte kann man das Ganze auch mit Images und der Option "Preserve Aspect Ratio" erreichen, da diese nicht mit Auto Layout kommuniziert. Allerdings ist man so auf eine Achse beschränkt, da die Images ansonsten wieder im Sinne der Aspect Ratio wachsen würden. Das ist aber eher ein Hack, als das ich es wirklich für gut empfinden würde.

[Beispiel]


Vorteil:

Nachteil:

Ich glaube eigentlich nicht, dass diese Methode empfehlenswert ist. Was man sich hier potenziell an Iterationszeit sparen kann wird durch die gestiegene Komplexität zunichte gemacht.

Spacer Objekte um die Layoutgruppe herum

Bedenke: Wir können Layoutgruppen schachteln. Eine Layoutgruppe mit flexiblem Padding wäre dann also drei Objekte unterhalb einer weiteren Layoutgruppe - enstprechend ein Spacer, Layoutgruppe und ein weiterer Spacer.

[Beispiel]

Vorteil:

Nachteil:

Diese Variante bietet sich vor allem an, wenn die Layoutgruppe sowieso schon unterhalb einer anderen Gruppe liegt (Beispiel: Mehrere Zeilen Inhalt innerhalb Panels, also horizontale Gruppen unter einer vertikalen). Vielleicht will man auch andere Alignment Optionen nehmen oder Force Expand nutzen. Leider kann man wie gesagt nur auf der kontrollierten Achse Padding erzeugen - auf der anderen geht das nur mit einer der beiden zuvor beschriebenen Methoden.

Hierarchische Benennung von Assets

Für die Benennung von meinen UI-Prefabs benutze ich lose das von Masahiro Sakurai vorgeschlagene Schema. Im Grunde genommen geht es darum selbst sortierende Namen zu erzeugen, indem man eine Hierarchie innerhalb der Namen einhält. Ein Beispiel: Nehmen wir an, dass wir den Button zum Schließen des Optionsmenü benennen wollen - ein möglicher Name wäre "Menu Options Button Close" mit Leerzeichen oder einfach Camel-Case "MenuOptionsButtonClose". Der Name entsteht durch eine Verkettung von Kategorien, die den Kontext der Verwendung immer weiter eingrenzen:

  1. Menu - verwendet innerhalb eines Menüs
  2. Options - genauer gesagt im Optionsmenü
  3. Button - es handelt sich um einen Button
  4. Close - genauer gesagt um den Close-Button

Der direkte Vorteil an dieser Benennung ist, dass sie innerhalb eines Ordners die Sortierung vorgibt. Beispielsweise werden alle Elemente des Optionsmenüs nebeneinander sein.

[Beispiel]

Bei Anzahl bzw. Umfang von Kategorien kann man nicht pauschal sagen, wie viele Kategorien benötigt werden bzw. wie groß der Umfang sein sollte. Beispielswiese könnte man die erste Kategorie "Menü" auch durch einen Ordner ersetzten. Die Entscheidung ist also ein wenig persönliche Präferenz, aber auch vom Projektumfang abhängig.

Ein weiterer Faktor ist die globale Suche: Wenn man nun z.B. "Options" in die Suche eingibt, erhält man alle zu dem Optionsmenü gehörigen Elemente - wenn man die Kategorie "Options" in einen Ordner verwandelt hat, findet man jetzt nur diesen. Es kann ebenfalls vorkommen, dass es Kollisionen gibt, wenn es beispielsweise mehrere Close Buttons in den zugehörigen Ordnern (z.B. Options, Shop) gibt - wird hier nach "Close Button" gesucht, erscheinen zwei Assets und man kann nur noch durch Blick auf Pfad oder Inspektor feststellen, welcher der richtige ist.

[Beispiel]

Sakurai verwendet in dem Video Abkürzungen, diese sparen Platz bei den immer länger werdenden Namen. Allerdings haben Abkürzungen immer den Nachteil, dass man sie erklären muss. Sobald also mehrere Leute an dem Projekt arbeiten müsste man eine Art Glossar für die Abkürzungen aufsetzen. Deswegen ist es meist unkomplizierter die Sachen einfach auszuschreiben.

Für Varianten oder fortlaufende Teile einer Animation kann man Buchstaben bzw. Nummerierung anfügen: Close Button a-c oder Attack Forward 01 - 05.