Unity UI Toolkit Spiel-UI
Struktur in UXML, Aussehen in USS, Verhalten in kleinen C#-Controllern, die auf Spielereignisse reagieren. Destilliert aus einem echten Projekt (Horror-Shooter-HUD); die Troubleshooting-Liste ist der Teil, der am ehesten Zeit spart.
1. Vor dem Schreiben von Code
Frage nur nach dem Fehlenden, kurz und gebündelt:
- UI-API.
[RequireComponent(typeof(PanelRenderer))]+RegisterUIReloadCallback= PanelRenderer-Workflow (keinUIDocument, keinerootVisualElement).UIDocument+rootVisualElement= klassisch. Erschließe es aus eingefügtem Code; falls unklar, frage nach einem funktionierenden Controller und dem zugehörigen UXML. Mische niemals beide APIs (führt zu NullReferenceExceptions). Eine funktionierende Datei vom Benutzer ist besser als deine Erinnerung an die API. - Genre/Thema (inkl. Screenshots oder Palette), Plattform und Eingabe, Referenzauflösung/Seitenverhältnis.
- Die Game-Skripte, auf die die UI hört. Frage danach, lies die öffentlichen Ereignisse/Eigenschaften, binde daran. Falls ein Ereignis fehlt, schlage die kleinste Erweiterung des Game-Skripts vor.
- Antworte in der Sprache des Benutzers; schreibe Codekommentare in der Sprache, die die bestehenden Skripte verwenden.
2. Zuerst den Styleguide
Konsistenz fällt Benutzern zuerst auf. Bevor du viele Teile baust: schlage einen kurzen Leitfaden vor, baue EINEN kleinen Teil, lass den Benutzer reagieren und verwandle sein Feedback in Regeln („kein Hintergrundkasten“, „kompakter“). Speichere ihn im Projekt (z.B. Assets/UI/UI_STYLE_GUIDE.md) und sage dem Benutzer, er solle ihn bei zukünftigen UI-Anfragen einfügen. Aktualisiere ihn, wenn sich Teile oder Regeln ändern. Inhalt: Designsprache (Ton, Container, Dichte, Bewegung), Palettentabelle (Text, dezent, Warnung, kritisch, Schatten), Typografie (Größen nach Rolle, Schattenregel), Layout (Zonen, Ränder, Panel-Einstellungen), USS-Namens- und Statusklassen, Code-Architekturregeln, UXML-Fallen, Liste vorhandener Teile, Checkliste für ein neues Teil.
3. Architektur
- Ein Teil = ein Controller + ein UXML + ein USS, mit einer Datenquelle. Kein Mega-Controller.
- Teile, die zusammen sein müssen, teilen sich ein UXML/flex-Container (zwei Controller können ein GameObject/PanelRenderer teilen; jeder findet seine Elemente über
Q<T>("name")). - Controller finden nur Elemente, setzen Text/Werte und schalten USS-Klassen um. Farben, Größen und Animationen leben in USS.
- PanelRenderer: Root kommt über
RegisterUIReloadCallback(OnUIReload)(registrieren in OnEnable, abmelden in OnDisable). Es feuert, wenn die UI bereit ist, und bei jedem Neuladen, daher gehören ALLEQ<T>("name")-Zugriffe, Caching, Ereignisbindung und Statuszurücksetzung dorthin. SchützeUpdate()mit Null-Prüfung, bis Elemente existieren. - Game-Ereignisse können ausgelöst werden, bevor die UI existiert: speichere die neuesten Werte in Feldern und wende sie am Ende von
OnUIReloaderneut an. - Player-/Game-Objekte können spät spawnen: serialisierter Verweis, ansonsten
FindFirstObjectByTypegedrosselt (~0,5 s), dann binden; abmelden in OnDisable. - Pushen, nicht pollern. Abonniere Ereignisse und schreibe nur bei Änderungen. Polle nur Werte ohne Ereignis (kontinuierliche Ausdauer, ein Reload-Bool) und schalte Klassen bei Änderung um. Keine String-Konstruktion pro Frame.
- Wiederholte Elemente (Punkte, Segmente) einmal im Code erstellen; nur neu erstellen, wenn sich ihre Quelle ändert; gecachte Listen beim Neuladen leeren.
- Timer/Pulse:
element.schedule.Execute(...).Every(ms)pausiert erstellt, nur bei Bedarf fortgesetzt. Einmalige Blitze:.StartingIn(ms). - HUD-Elemente:
picking-mode="Ignore"überall, keine fokussierbaren Steuerelemente. Menüs sind die Ausnahme. - Dateiname = MonoBehaviour-Klassenname.
4. Layout (Quelle der meisten visuellen Fehler)
- Platziere niemals verwandte Teile mit separat geschätzten Pixel-Offsets: zwei absolute Panels überlappten sich, weil
bottomblind geschätzt wurde. Setze sie in einen flex-Container und positioniere nur diesen Container. - Ein absolut positionierter Container pro Bildschirmzone; Kind-Elemente fließen mit Flex und Rändern.
- Verstecke mit
opacity+ Transition, wenn das Layout nicht springen soll;display:nonenur, wenn der Platz verschwinden soll. - Feste
widthbei numerischen Labels, damit 9 zu 10 zu 100 keine Nachbarn verschiebt. - Boxen sind border-box: Segment-Reihenbreite =
Anzahl x (Breite + Rand) - Rand. Richte verwandte Widgets an identischer linker Kante und berechneter Breite aus. - Panel Settings: Scale With Screen Size (z.B. 1920x1080, Match 0,5); bei HUDs Clear Color und Clear Depth Stencil AUS. Verwende den Sort Order des PanelRenderer, um Teile zu schichten (Vollbild-Vignette unter Text).
5. USS und Zustand
- Zustand = Klasse, die von C# umgeschaltet wird (
EnableInClassList); Animation = USS-Transition. Setze niemals Farben oder animiere in Code. - Benenne Zustände nach Bedeutung:
--low,--critical,--hidden,--hit,--pulse,--unavailable. Bevorzuge BEM für neue Teile. - Überschreibende Regeln (verbrauchte Segmente, Blitze) kommen NACH den Regeln, die sie überschreiben; gleiche Spezifität, letzte gewinnt.
- Eine Transition verwendet die Dauer/Verzögerung des Zielzustands:
.x--hidden { opazität:0; übergangsdauer:.5s; übergangsverzögerung:.9s; }blendet spät aus und sofort ein;.x--flash { übergangsdauer:.05s; }schaltet sofort ein, blendet mit Basisgeschwindigkeit aus. - Text, der ohne Hintergrund schwebt, benötigt
text-shadow. Keintext-transform(Großschreibung in C#); keine radialen Verläufe (siehe Vignette). - Für umgestaltbare UI platziere Farben/Größen in USS-Variablen auf einer Root-Klasse (
--c-text, verwendet alsvar(--c-text)); überprüfe, ob die Unity-Version des Benutzers dies unterstützt.
6. Controller-Grundgerüst (PanelRenderer)
[RequireComponent(typeof(PanelRenderer))]
public class PartController : MonoBehaviour
{
[SerializeField] private SourceComponent source; // leer = zur Laufzeit finden
private PanelRenderer _pr; private VisualElement _block; private Label _value;
private bool _bound; private float _v = -1f; private int _shown = -1; private float _nextFind;
private void Awake() => _pr = GetComponent<PanelRenderer>();
private void OnEnable() { _pr.RegisterUIReloadCallback(OnUIReload); TryBind(); }
private void OnDisable() { _pr.UnregisterUIReloadCallback(OnUIReload); Unbind(); _block = null; _value = null; }
private void Update()
{
if (_bound || Time.unscaledTime < _nextFind) return;
_nextFind = Time.unscaledTime + 0.5f; TryBind();
}
private void TryBind()
{
if (_bound) return;
if (source == null) source = FindFirstObjectByType<SourceComponent>();
if (source == null) return;
source.OnChanged += OnChanged; _bound = true;
_v = source.Current; Apply(); // aktuellen Zustand einmal abrufen
}
private void Unbind() { if (_bound && source != null) source.OnChanged -= OnChanged; _bound = false; }
private void OnUIReload(PanelRenderer r, VisualElement root)
{
_block = root.Q<VisualElement>("part"); _value = root.Q<Label>("part-value");
if (_block == null || _value == null) { Debug.LogError("Part: Elemente nicht in UXML gefunden (Quell-Asset prüfen).", this); _block = null; return; }
_shown = -1; Apply(); // neuer Baum: Neuschreiben aus Cache erzwingen
}
private void OnChanged(float v) { _v = v; Apply(); }
private void Apply()
{
if (_block == null || _v < 0f) return;
int n = Mathf.CeilToInt(_v);
if (n != _shown) { _shown = n; _value.text = n.ToString(); } // nur bei Änderung schreiben
_block.EnableInClassList("part--low", _v <= 0.3f);
}
}
7. Wiederverwendbare Schnipsel
- Segmente/Punkte: in
OnUIReload,row.Clear(); list.Clear();dann NVisualElement{pickingMode=Ignore}mit Klasseseghinzufügen; bei Änderunglist[i].EnableInClassList("seg--spent", i >= filled). Zeige mindestens 1 gefüllt, während Wert > 0:filled = v<=0 ? 0 : Clamp(CeilToInt(v*N-0.001f),1,N). - Puls:
_pulse = _block.schedule.Execute(() => _block.ToggleInClassList("part--pulse")).Every(400); _pulse.Pause();Fortsetzen beim Betreten des Zustands, Pause + Klasse entfernen beim Verlassen; merke dir ein_pulsing-Bool und setze es beim Neuladen zurück. - Blitz bei Ereignis:
var b=_block; b.AddToClassList("part--hit"); b.schedule.Execute(()=>b.RemoveFromClassList("part--hit")).StartingIn(150); - Vignette: erstelle einmalig in Awake eine 128x128
Texture2D(RGBA32, Clamp, Bilinear,HideFlags.HideAndDontSave) mit Alpha =SmoothStep(0,1,InverseLerp(inner,outer,dist/1.4142))vom Mittelpunkt, weißes RGB; setze_el.style.backgroundImage = new StyleBackground(tex)in OnUIReload; zerstöre in OnDestroy. USS: Vollbild-Absolute-Element,opacity:0,-unity-background-scale-mode: stretch-to-fill,-unity-background-image-tint-colorfür die Farbe, Klassen--low/--critical/--pulse/--flashmit Opacity (Flash zuletzt, schnelle Dauer). Niedrigerer Sort Order als Textteile.
8. Genres und Plattformen
Die Architektur ist genresübergreifend identisch; der Styleguide und das Eingabemodell ändern sich. Arcade: große klobige Zahlen, Count-up, Skalierungssprünge, Combo-Glow, knackige 0,08-0,25 s Transitionen, Container willkommen. Retro/Pixel: Pixel-Font, Ganzzahl-Skalierung, Point-gefilterte Texturen, 9-Slice-Sprite-Rahmen. Mobile: Touch-Ziele etwa 44-48 dp, Geräte-Safe-Area als Root-Padding (aktuelle API für die Unity-Version des Benutzers prüfen), keine Hover-Zustände, größere Mindestschrift, auf echtem Gerät testen. Konsole/Gamepad: fokussierbare Elemente mit starkem :focus-Stil, expliziter Initialfokus, TV-sichere Ränder (3-5 Prozent), größerer Text, Glyphen, die mit dem aktiven Gerät wechseln. VR/World-Space: hängt von der Unity-Version ab, zuerst nachschlagen. Diese Plattformpunkte sind solide allgemeine Praxis, aber in einem echten Projekt ungetestet: sag das und bitte den Benutzer, auf dem Gerät zu testen. Halte Eingabelabels (Tastennamen oder Glyphen) in Daten, die der Controller liest, nicht fest in UXML codiert.
9. Ausliefern eines Teils
In Reihenfolge: vollständiges UXML, vollständiges USS, vollständiger Controller, Einrichtungsschritte (GameObject, PanelRenderer + gemeinsam genutzte Panel Settings + Source Asset, Sort Order, Referenzen zum Füllen, was im Play-Modus zu sehen ist), Einstellmöglichkeiten (welcher USS-Wert Größe/Position/Farbe/Schwellwerte ändert) und ein kurzer Styleguide-Eintrag. Wenn bestehende Dateien geändert werden, gib ganze Dateien, wenn der Großteil der Datei geändert wird (partielle Einschübe haben zu echten Fehlern geführt).
10. Fehlerbehebung
| Symptom | Ursache/Behebung |
|---|---|
NullReference zu rootVisualElement / GetComponent<UIDocument>() | Code verwendet UIDocument, aber Objekt hat PanelRenderer. Verwende RegisterUIReloadCallback, binde in OnUIReload |
| Controller protokolliert „Element nicht gefunden“ | Falsches Source Asset, Namenskonflikt mit Q<>(), oder ungültiges UXML |
| UI Builder „Fehler beim Öffnen des Assets“ | Ungültiges UXML (doppeltes </ui:UXML>, falscher <Style>, Platzhalter-GUID, versteckte Zeichen). Korrigieren, dann Rechtsklick auf Datei > Reimport. Lese die Konsolenmeldung über dem generischen Fehler |
| Nicht gestylte Elemente | <Style> fehlt oder falscher Pfad. Im UI Builder: StyleSheets > + > Vorhandenes USS hinzufügen (schreibt vollständigen project://database/...guid=...-Pfad) |
| Alles nach einer Bearbeitung verschwunden | UXML defekt, oder Source Asset-Referenz nach Umbenennung veraltet. Korrigieren, Reimport, neu zuweisen. Testen mit background-color: rgba(255,0,0,.3) auf dem Root |
| Teil falsch positioniert oder überlappend | Altes GameObject zeichnet noch altes UXML, oder separate absolute Offsets. Altes Objekt löschen, Nachbarn in einen Container zusammenführen |
| Platzhaltertext in der Game-Ansicht | Der Edit-Modus zeichnet das UXML; drücke Play |
| Skript kann nicht hinzugefügt werden | Dateiname != Klassenname. Im Unity-Projektfenster umbenennen, damit .meta mitgeht |
Keine gültigen Korrekturen: der Stylesheet-Tag ist <Style src="..."/> OHNE das ui:-Präfix (<ui:Style> wird still ignoriert); erfinde oder lasse niemals eine Platzhalter-GUID; verwende den Standard-Header <ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" editor-extension-mode="False">.
11. Arbeiten ohne den Editor zu sehen
Bitte bei Layout-Beschwerden um einen Screenshot und lies ihn genau; er hat Fehler behoben, die das Raten nicht konnte. Gib eigene Fehler kurz zu und behebe sie mit vollständigen Dateien. Bei Kompilierungsfehlern frage nach dem genauen Text und der Unity-Version, anstatt erneut zu raten. Versprich kein Geräteverhalten oder Leistungszahlen; baue die Struktur und bitte den Benutzer zu testen.