You are viewing an old version of this page. View the current version.

Compare with Current View Page History

« Previous Version 8 Next »

Glossar

Parent = Elternelement

Child = Kindelement

Aspect Ratio = Seitenverhältnis

UI Layering

Layering - Basics

Für einfaches Layering gilt folgende Hierarchie:

  1. Sorting Layer
  2. Sorting Wert
  3. Distanz zur Kamera (Transparenzachse; standardmäßig entlang Z)

[Beispiel sprite renderer]

Bedeutet also:

  1. Sind zwei Objekte auf dem gleichen Sorting-Layer?
    1. Ja - Sorting-Wert wird betrachtet.
    2. Nein - Das höhere Layer wird vorne angezeigt.
  2. Haben die Objekte den gleichen Sorting-Wert?
    1. Ja - Die Z-Position wird betrachtet.
    2. Nein - Der höhere Sorting-Wert ist vorne.
  3. Der geringere Z-Wert ist vorne.

Anmerkungen: 

  • Diese Hierarchie ist vereinfacht
    es gibt noch diverse andere Möglichkeiten Renderer zu sortieren. Die genaue Aufschlüsselung findet sich hier.
  • Es gibt standardmäßig 2 Render Queues "Opaque" und "Transparent", welche auch in dieser Reihenfolge sortiert werden.
    Im Folgenden wird hauptsächlich von Objekten in der Transparent Queue gesprochen.
  • Die Transparenzachse kann man umändern
    z.B. auf Y an Stelle von Z. Das ist beispielsweise hilfreich für 2D top-down Spiele-

Canvas

Jedwede Art von GUI-Element muss innerhalb eines Canvas liegen. Es kann mehrere dieser Canvasses geben. Sie haben eine rechteckige Form und sind ein GameObject mit der Canvas-Komponente darauf. Alle Objekte im Canvas werden in der "Transparent"-Render Queue gerendert.

Innerhalb eines Canvas wird das unterste Objekt im Vordergrund gerendert - die Reihenfolge ist der Hierarchie nach von hinten nach vorne. Das passiert auch unabhängig von der Z-Position. Zustäzlich haben UI-Komponenten, wie etwa Image, auch keine Sorting-Optionen, die man von der Sprite-Komponente gewohnt ist. Entsprechend ist die einzige Möglichkeit UI im Canvas zu sortieren ihre Stelle in der Hierarchie, der sog. SiblingIndex.

Außerhalb des Canvas verhält es sich wie ein einziger Sprite - egal wie viele Objekte unter dem Canvas liegen und auf welchen Positionen sie sich befinden, für die Renderreihenfolge sind sie ein Objekt. Für die Reihenfolge von Canvas und Welt-Objekten ist dann noch der Render Mode des Canvasses ausschlaggebend.

Canvas Render Modes

Die drei Rendern Modes sind:

  • Screen Space - Overlay,
  • Screen Space - Camera,
  • World Space.

Grundsätzlich wird hier unterschieden, ob die UI in Screen oder World Space gerendered wird. Ersteres bedeutet, dass sich die Maße des Canvas basierend auf denen des Bildschirms verändern. Ein Screen Space Canvas probiert immer die gleichen Maße wie der Bildschirm anzunehmen. Das World Space Canvas hingegen hat eine feste Größe, die ihm manuell zugewiesen werden muss.

Screen Space - Overlay

In diesem Modus wird die UI nach dem Rendern der Szene über diese gelegt. Sie befindet sich also immer vor allen Objekten, die sich nicht innerhalb weiterer Overlay Canvasses befinden.

[Beispiel]

Für das Sortieren von mehreren Overlays steht jetzt ein Sorting-Wert zur Verfügung - dieser funktioniert analog zum Sprite-Renderer.

Diese Art von Canvas eignet sich meiner Meinung nach vor allem für 3D-Spiele, da man sich dort auf der Z-Achse bewegt - was für die beiden folgenden Render-Modes relevant ist.

Screen Space - Camera

Im Modus "Camera" wird die UI in einer festen Distanz vor der zugewiesenen Kamera platziert. Diese Kamera ist dann auch für das Rendern der UI zuständig - entsprechend beeinflussen Kamera-Einstellungen (z.B. perspektivisch vs. orthografisch) das Canvas, ähnlich wie es der Bildschirm tut. Der Abstand zur Kamera und damit letztendlich die Z-Position des Canvas wird über den Parameter "Plane Distance" auf dem Canvas gesteuert.

Dadurch entsteht der wichtigste Unterschied zum Overlay: Die Z-Position des Canvas ist nun für die Render-Reihenfolge der Szene relevant. Genauer gesagt können nun Objekte in der Welt vor der UI angezeigt werden, sollten sie sich näher an der Kamera befinden.

[Beispiel]

Wichtig: Die Plane Distance beeinflusst nicht die Größe der UI, sondern Bildschirm und Kamera.

Zum Sortieren dieses Canvas gibt es neben der Plane Distance noch die bekannten Optionen Sorting-Wert und Sorting-Layer.

Screen Space Camera ist denke ich der beste Render-Mode für Spiele, in denen die Kamera eine feste Distanz zum Geschehen beibehält oder orthografisch ist. Bei Spielen mit Bewegung in die Tiefe besteht immer die Gefahr, dass unsere UI verdeckt wird, weswegen dieser Modus für 3D eher ungeeignet ist.

Wenn man trotzdem seine UI gegen die Welt sortieren möchte, kann man den nächsten Modus nutzen.

World Space

Ein World Space Canvas verhält sich wie jedes andere GameObject auch - wenn es näher an der Kamera ist wird es vorne gezeigt und wenn nicht, dann eben hinter anderen Sachen. Die Nähe zur Kamera beeinflusst jetzt auch die Größe der UI-Elemente.

[Beispiel]

Gängige Sortieroptionen bleiben auch hier erhalten (Wert, Layer).

Im Vergleich zu vorangegangenen Modi müssen Breite und Höhe des Canvas nun manuell gewählt werden - sie sind nicht mehr abhängig von Bildschirm oder Kamera.

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

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:

  • 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 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!

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.

[Beispiel]

Innerhalb der Gruppe werden Renderer nach den für sie geltenden Regeln sortiert: Beispielsweise gelten für Sprites Sorting-Layer und Wert, während Z zugunsten der Hierarchie ignoriert wird (erneut, wie beim Canvas). Bei 3D-Renderern wird nach Z sortiert und bei gleichem Z-Wert der Hierarchie nach.

[Beispiele]

Insbesondere bei 3D-Renderern ist wichtig, dass Sorting nur auf transparente Geometry angewendet wird. Wird der Render-Mode "Opaque" verwendet sortieren 3D-Renderer mit Sorting-Groups trotzdem über die Z-Position gegen andere Sorting-Groups.

[Beispiel]

Sorting groups sind für zwei Anwendungen wichtig:

  1. Gruppieren von Objekten, die optisch eine Einheit sein sollen - z.B. Charakter aus mehreren Sprites,
  2. Anwendung von Sortieroptionen auf 3D-Renderer.

Layering - Beispiel

Wie man seine UI am besten layered ist sehr stark vom Spiel abhängig. Um eine Idee davon zu haben kann man sich beispielsweise folgende Fragen stellen:

  • Ist die UI immer vorne?

Die UI könnte ein klassisches HUD sein - in diesem Fall nutzt man ganz einfach den Modus "Screen Space - Overlay" und sortiert verschiedene Canvasse über den Sorting Wert.

  • Benutze ich Z als Achse für Sortierung von Transparenz?

In diesem Fall würde ich keinen "Screen Space - Camera" Modus verwenden. Diese Art von Canvas sortiert sich immer nach Abstand zur Kamera auf Z - ihr X- und Y-Wert ist immer 0. Entsprechend könnten Weltobjekte nun ungewollt vor der UI erscheinen und es wird unmöglich die Sortierung im Scene View nachzuvollziehen.

  • Soll UI dynamisch Spielobjekten folgen?

Canvas mit "World Space" ist dein Freund. Sortiert werden kann das Canvas dann über Abstand zur Kamera oder mittels Sortieroptionen.

  • Was ist wenn UI hinter Spielobjekten auftauchen soll?

Angenommen wir haben ein 2D-Spiel mit Seitenansicht. Es wäre denkbar, dass z.B. Texte zwischen Spielebene und Hintergrund auftauchen sollen.

[Beispiel]

Für diesen Effekt sollten wir "Screen Space - Camera" oder "World Space" verwenden. Mit beiden Optionen können wir entsprechend unserer Szene die UI entweder über Tiefe oder Sortieroptionen einsortieren. Wenn sich diese UI dem Bildschirm oder der Kamera anpassen soll ist "Screen Space - Camera" zu verwenden.

Ein solches Setup erlaubt es auch Partikelsysteme in der Welt vor bzw. hinter der UI zu rendern. Warum das sehr praktisch ist steht im Kapitel über Partikel.

Wenn UI gegen Welt sortiert wird stellt sich dann die Frage nach der Sortierung:

  • Benutze ich Sortieroptionen oder Distanz zur Kamera bzw. Transparenzachse Z?

Meiner Meinung nach schließen sich diese beiden Optionen ein wenig aus und bieten unterschiedliche Vor- bzw. Nachteile. Das liegt vor allem an der zuvor beschriebenen einfachen Hierarchie - Sortieroptionen werden immer vor Z angewendet. Letztendlich muss die Wahl hier vor allem anderen mit der Sortierung der Spielszenen funktionieren.

Ich halte folgende Kombinationen für sinnvoll:

  • Alles sortiert sich nach Tiefe,
  • Alles sortiert sich mit den Sortieroptionen,
  • Global werden Sortieroptionen verwendet, aber UI sortiert sich intern nach Z.

Eine Sortierung nach Tiefe bietet vor allem den "What you see is what you get" - Vorteil. Wenn wir im Scene View auf 3D schalten, können wir unser Layering ganz bequem überblicken.

[Beispiel]

Die verschiedenen Ebenen der Spielszene könnten mit einer Sorting Group versehen werden, damit man diese intern über Sortieroptionen sortieren kann.

[Beispiel]

Nachteil ist natürlich, dass sie nicht funktioniert, wenn Z nicht die Transparenzachse ist - also für Top-Down eher nicht zu gebrauchen.

Wenn die Transparenzachse oder die Szenenhierarchie eine Sortierung per Tiefe nicht zulässt, eignen sich die Sortieroptionen am besten, auch wenn sie die Sortierung unübersichtlicher machen.

Ein Mittelweg kann es sein allein die Sortierung der UI über Z zu regeln und für die Welt Sortieroptionen zu nutzen. Dafür würde man alle Canvasses auf ein Vordergrundlayer verschieben, das über dem Weltlayer liegt. So behält man sich den Überblick der Tiefensortierung bei. 

[Beispiel]

TODO:

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.

Partikel in der UI

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:

  • UI Elemente (Button, Toggle, Slider etc,)
  • Icons
  • Firmenlogos oder andere Markenzeichen
  • Platform Komponenten (für Android, iOS)

[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:

  • Panels,
  • Elemente,
  • Views.

Beginnen wir mit der View:

  • Alles was mindestens eine Grafik (auch Text) hat ist grundsätzlich erstmal eine View,
  • View ist die kleinste Einheit in der Hierarchie,
  • Beispiel: Ein einzelner Button oder ein Icon.

Eine Gruppe von Views ist ein Element:

  • Elemente haben idR. eine View und ggf. weitere Grafiken unter sich,
  • Beispiel: Ein Button mit zugehörigem Erklärungstext.

Ganz oben in der Hierarchie steht das Panel:

  • Ein Panel ist die größte Einheit und wird entsprechend nicht geschachtelt,
  • Es kann sowohl Elemente, als auch Views und einzelne Grafiken unter sich haben,
  • Beispiel: Ein Optionsmenü oder Inventar etc.

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?

Die Safe Zone

Spätestens mit der Ankündigung des iPhone X wurden sogenannte "edge-to-edge" oder randlose Displays zu einem weitverbreiteten Design auf der Mobile-Plattform. Damit die UI nicht von Notches oder Flächen für Gesten (z.B. iPhone Home-Geste) überlagert wird, gibt es die Safe Zone. Innerhalb der Safe Zone ist garantiert, dass Elemente bedienbar und vollständig sichtbar sind.

Hier mal ein paar Beispiele aus dem Device Simulator:

[Beispiele]

Veraltete Apps, die Notches bzw. Safe Zone nicht korrekt unterstützen sehen inzwischen so aus:

[Beipiel]

Beim Aufbau und Design der eigenen UI sollte die Safe Zone unbedingt miteinbezogen werden. Damit verhindert man das,

  • Elemente der UI durch Rundungen in den Ecken des Displays abgeschnitten werden,
  • Bedienelemente von der Notch verschluckt werden oder sonst wie nicht bedienbar sind,
  • beim Interagieren ungewollt Systemgesten angesteuert werden.

Für das Design bedeutet die Safe Zone vor allem erst einmal weniger Platz für so gut wie alles. Beim Bau der UI stellt sich die Frage, wie man sich an die Safe Zone anpasst.

Ich habe hier ein Asset aus dem Store, mit samt Artikel zur Erklärung parat. Zusammengefasst erhält man über die Screen-Klasse ein Rect, welches die Maße der Safe Zone darstellt. Dieses Rect kann man dann mit den Maßen der vollen Auflösung verrechnen und so Werte für anchorMin bzw. anchorMax des anzupassenden RectTransforms erhalten, um dieses auf die Dimensionen der Safe Zone zu setzen. Zur Erinnerung: Die Anker Werte sind zwischen 0 und 1 - sie beschreiben wie viel des Rects auf einer Achse genutzt wird. Man errechnet also, wie viel die Safe Zone anteilig vom gesamten Bildschirm ausmacht.

Final stellt sich damit noch die Frage, auf welcher Ebene dieses Safe Zone einhaltende Script liegen sollte:

  • Canvas
  • Panel
  • Element

Die Antwort darauf hängt ganz vom Design ab - vor allem, ob das/die Panel/s einen Full Screen Hintergrund haben.

Safe Zone auf dem gesamten Canvas

Stumpf ist Trumpf - wir passen einfach den gesamten Canvas auf die Safe Zone an. Daraus ergeben sich schnell zwei Probleme:

  1. Was ist mit Fullscreen-Hintergründen bzw. Panels?
  2. Was ist, wenn ich Elemente bündig zum Displayrand platzieren muss?

In beiden Szenarien muss man die Safe Zone Änderung quasi wieder zurückrechnen. Bei Hintergründen von Panels könnte man einfach ein großzügiges Maß an Padding hinzufügen, das ist jedoch ein wenig hacky und kann in Zukunft nicht mehr ausreichen, wenn wieder andere Display Dimensionen auf den Markt kommen. Am besten platziert man also eine Safe Zone auf Canvas Ebene, wenn man weiß, das Panels darunter sowieso keine Fullscreen-Grafiken oder Elemente am Displayrand haben werden.

[Beispiel]

Safe Zone pro Panel

Eine flexiblere Lösung kann es sein pro Panel zu entscheiden, ob dieses eine Safe Zone benötigt oder nicht. Auf diese Art und Weise können Safe Zone-konforme und nicht-konforme Panel auf dem gleichen Canvas existieren, was Hierarchie und Layering vereinfacht. Eine weitere Idee wäre es Panels unter dem Canvas in eine Safe Zone - konforme und eine nicht-konforme Gruppe aufzuteilen - allerdings kann das wieder zu Problemen beim Layering führen, wenn Panels aus zwei verschiedenen Gruppen gleichzeitig auftreten.

[Beispiele]

Safe Zone pro Element

In manchen Fällen kann es sogar notwendig sein einzelne Elemente innerhalb eines Panels an die Safe Zone anzupassen oder diese ignorieren zu lassen. Das dritte Beispiel aus dem Artikel zeigt das ganz gut:

[Beispiel]

Die horizontalen blauen Streifen sollen fullscreen sein, entsprechend erhalten sie die Option nur auf der vertikalen mit der Safe Zone konform zu sein.

Man kann auch innerhalb eines Panels in Safe Zone-konform und nicht-konform unterteilen - eine beispielhafte Hierarchie wäre Panel → Background → Content mit Safe Zone. Ansonsten würde ich diesen Anwendungsfall als Randfall betrachten und ihn möglichst vermeiden, da die Hierarchie immer kleinteiliger bzw. komplexer wird.

Flexibles Spacing

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

  • Spacing Option der Layoutgruppe
  • Force Expand Option der Layoutgruppe
  • Spacer Objekte mit Layout Element

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:

  • Dieser Wert ist fix - Auto Layout wird ihn immer einhalten. Er wird ebenfalls bei der Berechnung der Size mit einbezogen und somit auch an Fitter Komponenten kommuniziert.

Nachteil:

  • Dieser Wert ist fix. Wenn die Gruppe schrumpft, rücken Elemente nicht näher zusammen - es kann also vorkommen, das nun nicht mehr genug Platz für alle vorhanden ist und Elemente aus der Gruppe ragen.

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:

  • Die Menge an Leerraum ist gleichverteilt und flexibel. Sie ist abhängig vom Größenverhältnis der Childs zur Gruppe.

Nachteil:

  • Es gibt kein Minimum für die Abstände. Wenn die Gruppe schrumpft, schrumpfen auch die Abstände. Sollte die Gruppe ihre Childs beispielsweise exakt einfassen oder gar zu klein sein, gehen die Abstände gegen Null,
  • Die Option "Control Child Size" kann nicht verwendet werden, da die Childs so den gesamten Raum einnehmen.

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:

  • Die Menge an Leerraum ist flexibel - sowohl mit Maximum als auch Minimum,
  • sie kann zwischen jedem Objekt der Gruppe individuell angepasst werden,
  • Spacer können Prefabs sein und so Abstände über mehrere Panels synchronisieren,
  • man kann Elemente an den Spacern verankern.

Nachteil:

  • Neue Elemente können nicht mehr dynamisch in die Gruppe eingefügt werden, es sei denn, man fügt auch weitere Spacer hinzu,
  • erhöht die Anzahl an GameObjects, was Hierarchien komplizierter gestaltet und mehr Performance kostet,
  • kann Iterationsgeschwindigkeit verringern, wenn man nicht mit Prefabs arbeitet,
  • erhöht Komplexität von Hierarchie und ggf. Projekt.

[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:

  • Anker Positionen entsprechend wählen
  • Size der Layoutgruppe mit Layoutelement überschreiben
  • Size der Childs manipulieren
  • Spacer Objekte um die Layoutgruppe herum

Flexibles Padding über die Anker

Vorteil:

  • Funktioniert ohne Auto Layout

Nachteil:

  • Kann nicht auf Childs einer Layout Gruppe gesetzt werden,
  • nur inneres Padding möglich,
  • keine Obergrenze.

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:

  • Das Padding wird immer noch auf dem gleichen GameObject kontrolliert, auf dem sich die Layoutgruppe befindet.

Nachteil:

  • Wenn sich durch neues Design die Werte der Children ändern, muss man per Hand nachrechnen und eintragen - normalerweise wäre das automatisch passiert,
  • kein Force Expand mehr möglich, wenn Padding auf der Achse vorhanden sein soll.

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:

  • Die Layoutgruppe berechnet ihre Maße weiterhin automatisch,
  • Spacer Prefabs können Iterationszeit sparen.

Nachteil:

  • Padding wird in gewisser Hinsicht versteckt, da es implizit entsteht,
  • kein Force Expand mehr möglich, wenn Padding auf der Achse vorhanden sein soll,
  • erfordert Spacer Objekte,
  • Hacky.

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:

  • Die Layoutgruppe berechnet ihre Maße weiterhin automatisch,
  • Spacer Prefabs können Iterationszeit sparen,
  • Padding ist unabhängig vom Alignment der Childs,
  • erlaubt Force Expand.

Nachteil:

  • Komplexere Hierachie,
  • Padding ist nur auf der von der Obergruppe kontrollierten Achse möglich.

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.

Basic Layout

Für die einfachste Form von Layout platziert man UI Elemente relative zum Canvas oder einander. Jedes GameObject unter einem Canvas erhält ein Rect(angle) Transform an Stelle der normalen Transform Komponente.

[Beispiel]

Ganz oben ist wie gewohnt die Position des Objekts. Darunter befinden sich nun Einstellungen für die Breite und Höhe des Elements - denn jedes Element unter dem Canvas wird als Rechteck dargestellt. Ebenfalls wieder zu finden sind Rotation und Skalierung.

Skalierung versus Resizing

Im normalen Kontext ist man gewöhnt Skalierung zu benutzen, um Objekte größer oder kleiner zu machen. Innerhalb der UI macht es mehr Sinn stattdessen Breite und Höhe anzupassen (Resizing). Das hat vor allem den Grund, das die meisten UI Komponenten nur mit diesen Werten interagieren. Ein weiterer Anwendungsfall sind Sliced Images, wo das Resizing keinen Einfluss auf den Rand des Bildes hat - Skalierung aber schon.

Es bietet sich als Konvention an für statische Größen immer Breite und Höhe zu verwenden, während dynamische Größe über Skalierung geregelt wird (Beispiel: Animationen).

Der Pivot Point

Wenn man ein UI Objekt anwählt sieht man einen Kreis, meist in der Mitte des Rechtecks. Dieser Kreis ist der sog. Pivot Point, das Zentrum des Elements. Seine Position ist im Rect Transform mit relativen Werten zwischen 0 und 1 angegeben. Die Position (0,0) ist dabei links oben im Rechteck.

[Beispeil aus der rect doku]

Alle Transformationen - Verschieben, rotieren, skalieren, resizing - passieren ausgehend vom Pivot bzw. um ihn herum. Dazu ein paar Beispiele:

[Beispiel Position 0,0,0 mit zwei unterschiedlichen pivots]

[Beispiel Rotation 0,0,10 mit zwei unterschiedlichen pivots]

[Beispiel Skalierung 2,1,1 mit zwei unterschiedlichen pivots]

[Beispiel gleiche Sizes mit zwei unterschiedlichen pivots]

Die Anker

Anker sind ein gängiges Layout Konzept. Sie bestimmen zu welchem Punkt innerhalb des Paren-Rechtecks die eigene Position relativ ist – ähnlich wie beim normalen Transform, wo die Position relativ zum Ursprung des Parents ist.

Repräsentiert werden sie durch vier dreieckige Griffe im Scene View. Im Inspector gibt es die Sektion „Anchors“, unter der Min und Max als Ankerpositionen gelistet sind. Min beschreibt dabei die linke untere Ecke bzw. den linken unteren Anker und Max den rechten oberen.

Die Ankerpositionen werden als Werte zwischen 0 und 1 angegeben. Solange die beiden Anker am selben Punkt liegen, besitzt das Rechteck eine fixe Breite und Höhe, während es sich relativ zum Parent positioniert.

[Beispiel]

Zieht man sie auseinander, entsteht auf der entsprechenden Achse eine flexible Größe. In diesem Fall kann man die Werte von Min und Max als Prozente interpretieren, also 0 entspricht 0% und 1 entspricht 100%.

[Beispiele]

Diese Veränderung macht sich auch im Inspektor bemerkbar, wo jetzt ein Padding auf der jeweiligen Achse eingegeben wird, an Stelle einer Position.

[Beispiel]

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.

Pro Dimension (Width, Height) verfügt jedes Layoutelement über drei Sizes:

  1. Minimum Size,
  2. Preferred Size,
  3. Flexible Size.

Sehen kann man diese ganz unten im Inspector, unter "Layout Properties". Manchmal ist diese Ansicht in einem Dropdown versteckt - wenn beispielsweise ein Image auf dem GameObject liegt, wird ein Preview vom Sprite angezeigt.

[Beispiel]

Wenn ein Layoutcontroller beginnt die Size zuzuweisen, betrachtet er nacheinander von jedem Element diese drei Sizes. Zuerst erhält jedes Element seine Minimum Size. Wenn danach noch Raum übrig ist, wird damit begonnen Elemente in Richtung ihrer Preferred Size wachsen zu lassen. Die Elemente wachsen so lang weiter, bis sie ihre Preferred Size erreicht haben oder kein weiterer Raum mehr zur Verfügung steht. Sollte danach immer noch Raum zur Verfügung stehen, wachsen alle Elemente mit Flexible Size weiter, bis auch dieser Raum aufgebraucht ist.

Die Standardwerte für die Sizes sind 0. Während Minimum und Preferred Size tatsächlich Größenangaben sind, ist Flexible Size ein relativer Wert zwischen 0 und 1 - bedeutet also das Element nimmt gar keinen (Wert 0) oder den ganzen (Wert 1) verfügbaren Restraum ein. Sollten mehrere Elemente die gleiche Flexible Size haben, erhalten sie gleich viel Raum.

Anmerkung: Wenn mehrere Elemente eine Flexible Size haben und diese Sizes aufsummiert mehr als 1 ergeben wird die ganze Berechnung ein wenig unintuitiv. Nehmen wir zwei Layoutelemente A und B an. A hat eine Flexible Size von 1 und B eine von 4. Es steht ein Restraum von 10 Units zur Verfügung. Die zugeteilte Size ist jetzt 10 x Flexible Size / Summe aller Flexible Sizes - bedeutet also A erhält 10 x 1 / 5 = 2 units und B erhält 8 units. Ich würde daher vorschlagen darauf zu achten, dass Flexible Sizes entweder gleich groß sind oder sich zu 1 aufsummieren.

Die Komponente Layout Element

Es gibt bestimmte (Layout-) Komponenten, die die Standardwerte der Sizes überschreiben. Einfache Beispiele dafür sind "Layout Element", "Image" und "Text". Image und Text werden die Sizes immer so setzen, dass das Element den entsprechenden Inhalt fassen kann. Wenn ein Sprite also 50 units Breite hat, setzt die Image-Komponente seine Preferred Size entsprechend.

Laylout Element bietet die Möglichkeit alle Werte auf dem GameObject explizit zu überschreiben.

[Beispiel]

Wann immer man Elemente in seinem Layout hat, deren Werte sich nicht automatisch ergeben oder denen man eine andere Größe als die automatische zuweisen will, benutzt man diese Komponente. Ein Anwendungsfall von mir sind "Spacer"-Objekte, welche ich für flexiblen Leerraum bzw. flexible Abstände nutze. Solche Objekte sind nichts anderes als empty GameObjects mit einem Layout Element drauf, damit sie im Layout Platz einnehmen.

[Beispiel]

Neben den Sizes bietet das Layout Element noch zwei weitere Optionen:

  • Ignore Layout
  • Layout Priority

Ignore Layout ist ein Toggle und kommuniziert an Layoutcontroller auf dem Parent, dass dieses Objekt nicht teil des Layouts ist. Das ist nützlich, um Objekte innerhalb einer Layoutgruppe oder in Relation zu dieser zu platzieren, ohne das Layout zu beeinflussen.

[Beispiel]

Layout Priority legt die Priorität dieser Layout-Komponente gegenüber anderen fest. Das bedeutet, dass immer die Sizes von dem Element mit der höchsten Priorität an Layoutcontroller kommuniziert werden. Eine Layoutgruppe hat beispielsweise die Priorität 0 (steht als Property im Source Code). Wenn man jetzt auf dem gleichen GameObject ein Layout Element hinzufügt, dass standardmäßig mit Priorität 1 startet, überschreibt dieses die Sizes der Gruppe.

[Beispiel]

Setzt man die Priorität des Layout Elements auf -1, ist die Gruppe wieder priorisiert.

[Beispiel]

Sind mehrere Komponenten mit der gleichen Priorität aktiv, wird immer der höchste Wert von allen gewählt.

Layout Beispiele

Jetzt mal ein paar Beispiele.

Hierarchie:

  • Gruppe
    Size: 100
    • Element A
      Minimum Size: 10
      Preffered Size: 50
      Flexible Size: 0
    • Element B
      Minimum Size: 30
      Preffered Size: 0
      Flexible Size: 0

Zunächst erhalten Element A und B ihre Minimum Size von 10 bzw. 30. Danach ist noch Raum von 60 units übrig. Element B verfügt über keine weiteren Size Einstellungen und ist entsprechend fertig. Element A hat eine Preferred Size von 50 und möchte damit noch um weitere 40 Units wachsen. Da dieser Raum noch verfügbar ist, kann Element A seine Preferred Size erreichen und ist nun 50 units groß. Es bleiben 20 units als Leerraum im Layout zurück, da diese nicht beansprucht wurden.

Was ist aber nun, wenn beide Elemente eine Flexible Size haben?

Hierarchie:

  • Gruppe
    Size: 100
    • Element A
      Minimum Size: 10
      Preffered Size: 50
      Flexible Size: 1
    • Element B
      Minimum Size: 30
      Preffered Size: 0
      Flexible Size: 1

Das verhalten bleibt bis zum Zuweisen der Flexible Size zunächst einmal gleich. Danach werden die Flexible Size Werte betrachtet. Diese sind gleich groß, also erhalten beide Elemente die Hälfte der verbleidenden 20 units. Element A erreicht so eine Größe von 60 units. Element B wächst auf 40 Units heran.

Nehmen wir als nächstes an, dass wir die Flexible Size ungleich verteilen wollen:

Hierarchie:

  • Gruppe
    Size: 100
    • Element A
      Minimum Size: 10
      Preffered Size: 50
      Flexible Size: 0.25
    • Element B
      Minimum Size: 30
      Preffered Size: 0
      Flexible Size: 0.75

In diesem Fall erhält A genau ein Viertel, also 5, zusätzliche units und B die restlichen drei Viertel, entsprechend 15 units.

Im letzten Beispiel reicht die Size der Gruppe nicht aus, um beide Elemente auf ihre gesamte Preferred Size anwachsen zu lassen:

Hierarchie:

  • Gruppe
    Size: 100
    • Element A
      Minimum Size: 10
      Preffered Size: 50
      Flexible Size: 0
    • Element B
      Minimum Size: 30
      Preffered Size: 60
      Flexible Size: 0

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:

  • Width controls height

Die Breite des Parents wird für die Bestimmung der Höhe genutzt. [Beispiel]

  • Height controls width

Die Höhe des Parents wird für die Bestimmung der Breite genutzt. [Beispiel]

  • Fit in Parent

In diesem Modus wächst das Element gleichmäßig auf beiden Achsen unter Einhaltung der Aspect Ratio. Sobald eine Achse mit der des Parents gleich zieht wird gestoppt. Je nach dem, wie gut die gewählte Aspect Ratio zu der des Parents passt, kann es sein, dass Element diesen nicht komplett ausfüllt. [Beispiele]

  • Envelope Parent

Ähnliches Verhalten wie "Fit in Parent". Allerdings wird hier gewachsen, bis beide Achsen gleichziehen. Das führt dazu, dass das Element den Parent komplett ausfüllt und gegebenenfalls auf einer Achse über steht.

[Beispiele]

Ich habe den Aspect Ratio Fitter z.B. für einen Loadscreen genutzt. Dazu nutze ich den Modus Envelope Parent und trage die Aspect Ratio der Grafik in den Fitter ein.

[Beispiel]

Dieser Loadscreen wird immer den ganzen Bildschirm ausfüllen - allerdings ist die Grafik gegebenenfalls herangezoomed.

Mit den Control Modi für Width/Height kann man ein ähnliches Verhalten wie die Funktion "Preserve Aspect" auf der Image-Komponente erhalten. Der wichtige Unterschied dabei ist, dass das Bild auch tatsächlich so groß wird, wie es die Aspect-Ratio vorgibt und nicht nur so aussieht. Das erlaubt einem dann korrekte Verankerung am Image.

[Beispiel]

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:

  • Unconstrained

Der Content Size Fitter hat keinen Effekt auf diese Achse.

  • Min Size

Das Element wird vom Content Size Fitter immer auf seine minimale Größe gesetzt.

  • Preferred Size

Das Element wird immer auf die Preffered Size gesetzt.

Der Catch an dem Content Size Fitter ist für mich, dass er nicht flexibel funktioniert, sondern ausschließlich fix ist. Man könnte annehmen, dass die letzte Option ein flexibles Wachsen wie bei den anderen Elementen von Auto Layout bewirkt - das ist aber nicht der Fall. Es wird immer exakt auf die Preffered Size gesetzt.

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.

Jede Layoutgruppe hat folgende Optionen:

  • Padding
    Innerer Rand er Gruppe aus vier Richtungen,
  • Spacing
    Abstand zwischen den Childs,

Man kann auch negative Werte nutzen, was die Size der Gruppe verringert. Damit sich die Childs überlappen, fügt man negatives Spacing hinzu. Bei negativen Padding ragen sie dann aus der Gruppe heraus.

  • Child Alignment
    Bestimmt Ausrichtung der Childs.

Diese Einstellung beeinflusst vor allem, in bzw. aus welcher Richtung sich die Gruppe füllt. Mit Alignment "Center …" bleiben die kontrollierten Elemente beispielsweise zentriert.

Eine Layoutgruppe fungiert selbst als Layoutelement. Ihre Werte entsprechen der Summe der Werte ihrer Childs. Sind in einer Gruppe beispielsweise drei Elemente mit Minimum Width 50, so hat die Gruppe eine Minimum Width von 150.
Padding bzw. Spacing werden ebenfalls noch dazugerechnet. Hat die Gruppe zusätzlich noch ein Spacing von 30, ist ihre Minimum Width 210 (150 von den Childs plus zwei Spacings von 30 zwischen den drei Childs).

Die berechnete Größe wird dann auch an Layout Controller auf dem Parent der Gruppe oder an Fitter auf dem gleichen Objekt weitergegeben. Entsprechend kann man Layoutgruppen auch schachteln.

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.

Bei diesen Gruppen gibt es noch weitere Optionen:

  • Reverse Arrangement
    Kehrt die Reihenfolge der Childs um.

Ist vor allem nützlich, wenn die Sortierung von der Reihenfolge abweicht - da in der UI immer das unterste Objekt vorne angezeigt wird und sich die Gruppe immer von oben nach unten füllt. Möchte man das vorn angezeigte Objekt im Layout an erster Stelle haben, nutzt man "Reverse Arrangement".

Basierend auf der eigenen Größe probiert die Gruppe ihren Childs die entsprechende Menge an Minimum-/Preffered-/Flexible Size zuzuteilen. Wie genau diese Verteilung ausfällt wird von den drei Kontroll-Optionen beeinflusst:

  • Control Child Size
    Kontrolliert die Gruppe die Size?

Wenn ja - wird den Childs auf der entsprechenden Achse Minimum-/Preffered-/Flexible Size zugeteilt. Nehmen wir einmal folgendes Beispiel an: Unsere Layoutgruppe hat eine feste Größe von 300px, ohne Spacing und Padding. Unter ihr befinden sich drei Elemente mit Min Size 50 und Preferred Size 200. Zunächst wachsen alle Elemente auf ihre Minimum Size von 50, womit 150px der Gruppe belegt sind. Da alle Elementen eigentlich 200px groß sein wollen, kann jedes von ihnen noch wachsen. Die restlichen 150px werden also der entsprechenden Priorität nach auf die Elemente verteilt. Somit sind schlussendlich alle Elemente 100px groß.

Wenn nein - wird nur die Position der Childs durch die Layoutgruppe gesetzt.
So oder so gilt: Sollte die Gruppe nicht groß genug sein, um ihre Childs zu fassen, beginnen diese aus ihr herauszuragen.

  • Use Child Scale
    Soll die Gruppe bei Zuweisung von Size und Position die Skalierung der Elemente beachten?

Beispiel: Ein Element hat Min Size 100, aber einen Scale von 0.5 auf der entsprechenden Achse.

Wenn ja - Die Layoutgruppe behandelt das Element mit seiner echten Größe von 50.

Wenn nein - Das Element wird so behandelt, als hätte es trotz Scaling seine volle Größe von 100.

  • Child Force Expand
    Sollen die Childs die entsprechende Achse voll ausfüllen?

Bei dieser Option dreht es sich darum, was mit freiem Platz geschehen soll, der von keinem Element beansprucht wird.

Wenn nein - Der freie Platz bleibt leer.

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:

  • Cell Size
    Größe der einzelnen Grid-Zellen.

Diese Layoutgruppe ignoriert Size-Einstellungen der enthaltenen Elemente. Stattdessen wird jedem Child die fixe Zellengröße zugeteilt.

  • Start Corner
    Ecke in der sich das erste Element befindet.
  • Start Axis
    Die primäre Achse für das platzieren von Elementen.

Die Starteinstellungen sind maßgeblich dafür, wie sich das Grid füllt. Start Corner bestimmt den Ursprung. Start Axis kann horizontal oder vertikal sein und bestimmt die Tendenz zuerst Reihen bzw. Spalten zu füllen, bevor eine neue Spalte bzw. Reihe angefangen wird. Wann eine Zeile bzw. Spalte voll ist, wird durch die Breite bzw. Höhe des Gruppen Elements bestimmt.

  • Constraint
    Limitiert das Grid auf eine feste Anzahl an Zeilen bzw. Spalten.

Es gibt drei Einstellungen für Constraint: Flexible, Fixed Row- und Fixed Column Count. Flexibel kann man als eine Art ausgeglichene Einstellung verstehen. Reihen und Spalten werden so zugeordnet, dass die Childs in die Gruppe passen. Dabei wird versucht eine ungefähr gleiche Anzahl an Reihen und Spalten zu erhalten.

Allgemein kann es viele mögliche Kombinationen von der Anzahl an Reihen und Spalten geben, welche die Zellen in die Layoutgruppe einfassen. Mit Hilfe von Start Axis und den Row- bzw. Column Count Constrains wird beeinflusst, welche Kombination ausgewählt wird. Das ist vor allem wichtig, wenn man in Kombination mit anderen Auto Layout-Komponenten wie z.B. dem Content Size Fitter arbeitet.

Zur Erinnerung: Auto Layout berechnet Höhe und Breite unabhängig von einander, aber innerhalb eines Grids hängt die Anzahl an Zeilen mit der Anzahl an Spalten zusammen und umgekehrt. Um also korrekt flexible Grids zu erstellen bieten sich folgende Kombinationen an:

  • Grid mit flexibler Breite und fester Höhe
    Fixed Row Count aktiv, Content Size Fitter mit Horizontal Fit - Preffered Size

In dieser Konfiguration wird das Grid horizontal wachsen, wenn mehr Elemente hinzugefügt werden. Vertical fit auf dem Content Size Fitter sollte ebenfalls auf Preffered Size gesetzt sein, da man sich ansonsten selbst um die Zuweisung einer korrekten Höhe kümmern muss.

  • Grid mit flexibler Höhe und fester Breite
    Fixed Column Count aktiv, Content Size Fitter mit Vertical Fit - Preffered Size

Dieses Grid wird vertikal wachsen, wenn mehr Elemente hinzugefügt werden. Erneut: Horizontal fit auf dem Content Size Fitter sollte ebenfalls auf Preffered Size gesetzt sein, da man sich ansonsten selbst um die Zuweisung einer korrekten Breite kümmern müsste.

  • Voll flexibles Grid
    Flexible aktiv, Content Size Fitter mit Horizontal Fit und Vertical Fit auf Preffered Size

Man kann ebenfalls auf beiden Achsen flexibel wachsen, bestimmt durch Start Axis. Allerdings verliert man die Kontrolle über die Anzahl an Zeilen und Spalten, welche ungefähr gleich verteilt werden.


Der vielleicht größte Nachteil der Gridlayoutgruppe für die Mobile Plattform ist die fixe Größe der Zellen. Es kann keine unterschiedlich breiten oder hohen Zellen geben. Natürlich lässt sich das Ganze trotzdem auf unterschiedliche Wege erreichen:

  • Das Grid als eine Art Gestaltungsraster

Anstatt die Gridzellen direkt mit Inhalt zu füllen können sie auch als Parent für Elemente dienen, die dann frei in der Wahl ihrer Größe sind bzw. ihre Größe immer noch abhängig von der Zellengröße machen können. Beispielsweise kann innerhalb einer Gridzelle auch eine Layoutgruppe geschachtelt sein.

[Beispiel]

  • Schachteln von Layoutgruppen zur Erzeugung des Grids

Im weitesten Sinn ist die Gridlayoutgruppe nur eine praktische Hilfe für Grids mit einer dynamischen Anzahl an Elementen. Ist die Anzahl an Elementen jedoch statisch bzw. zur Designzeit bereits bekannt, so kann man ein Grid ebenfalls aus verschachtelten Layoutgruppen bauen. Beispielsweise eine horizontale Layoutgruppe als einzelne Zeile, unter der dann vertikale Gruppen als Spalten platziert werden.

[Beispiel]

Die fixe Größe der Zellen kann ebenfalls nicht von Auto Layout, also den Sizes, beeinflusst werden. Sie ist immer gleich, was dazu führen kann, dass Gridlayouts aus ihrem Parent oder dem Bildschirm ragen. Aufgrund dessen würde ich erneut das Schachteln der anderen Layoutgruppen bevorzugen, da man sich ansonsten mit eigenem Code zum Setzen der Zellengröße oder einer eigenen Gridlayoutgruppe behelfen muss.

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.


  • No labels