paint-brush
StatefulUI: Eine Unity-UI-Bibliothek basierend auf Zuständen und Markupvon@dmitrii
2,914 Lesungen
2,914 Lesungen

StatefulUI: Eine Unity-UI-Bibliothek basierend auf Zuständen und Markup

von Dmitrii Ivashchenko12m2023/05/17
Read on Terminal Reader
Read this story w/o Javascript

Zu lang; Lesen

Dmitrii Ivashchenko ist Softwareentwickler bei MY.GAMES. In diesem Artikel sprechen wir über die Entwicklung einer Benutzeroberfläche in Unity basierend auf Zuständen und Markup von Elementen. Der beschriebene Ansatz ist nicht auf [UI Toolkit] oder andere UI-Erstellungssysteme anwendbar.

People Mentioned

Mention Thumbnail
featured image - StatefulUI: Eine Unity-UI-Bibliothek basierend auf Zuständen und Markup
Dmitrii Ivashchenko HackerNoon profile picture
0-item
1-item

Hallo zusammen, mein Name ist Dmitrii Ivashchenko und ich bin Softwareentwickler bei MY.GAMES. In diesem Artikel sprechen wir über die Entwicklung einer Benutzeroberfläche in Unity basierend auf Zuständen und Markup von Elementen.

Einführung

Zunächst ist anzumerken, dass wir im Zusammenhang mit der Unity UI (uGUI)-Technologie sprechen, die laut Dokumentation weiterhin für Runtime empfohlen wird. Der beschriebene Ansatz ist nicht auf UI Toolkit , IMGUI oder andere UI-Erstellungssysteme anwendbar.



Am häufigsten werden Sie in Unity- Projekten auf eine UI-Implementierung stoßen, die auf von MonoBehaviour geerbten View-Klassen basiert und mit einer großen Anzahl von SerializeField Feldern gespickt ist. Dieser Ansatz ermöglicht die vollständige Kontrolle über das Verhalten der Benutzeroberfläche, erfordert jedoch auch das Schreiben einer großen Menge Code auf der View- und Presenter-Ebene (abhängig von der verwendeten Architektur).


Im Laufe der Projektentwicklung wachsen diese Klassen oft auf unglaubliche Größen an und die Komponenten auf GameObject selbst sind mit einer großen Anzahl von Links zu internen Objekten abgedeckt:



Das Modifizieren solcher Komponenten macht auch keinen Spaß: Um einen Verweis auf ein neues Element in einer Klasse zu erhalten, müssen Sie SerializeField hinzufügen, den Code neu kompilieren, das neue Feld in der vorgefertigten Komponente suchen und das erforderliche Objekt hineinziehen. Wenn das Projekt wächst, nehmen auch die Kompilierzeit, die Anzahl der Felder und die Komplexität der Organisation von Fertighäusern zu.


Infolgedessen erhalten wir umfangreiche und überlastete Unterklassen von MonoBehaviour (oder eine große Anzahl kleiner Unterklassen, je nach Wunsch).


Es ist auch zu bedenken, dass alle Änderungen am Verhalten einer solchen Benutzeroberfläche eine Aufgabe des Programmierers sind und diese Aufgabe mit allen damit verbundenen Kosten verbunden ist: Codeüberprüfung, Lösung von Zusammenführungskonflikten, Codeabdeckung mit Tests usw.


Ich möchte die Implementierung von Fenstern mit mehreren Zuständen hervorheben. Ich habe viele Variationen gesehen, die sich in zwei Ansätze unterteilen lassen:


  1. Zunächst erfolgt jede Änderung des Fensterstatus mithilfe von Code . Um die Farbe von Text zu ändern, ein Bild zu ändern, eine Animation abzuspielen, ein Objekt auf dem Bildschirm zu verschieben – alle beteiligten Objekte und Parameter erfordern ein entsprechendes SerializeField , und dann wird eine große Menge Code geschrieben, damit es entsprechend den Anforderungen funktioniert . Natürlich kann damit nur ein Programmierer umgehen, und die Implementierung erweist sich als langwierig, teuer und äußerst effizient (oft viel effizienter, als irgendjemand bemerken kann).
  2. Ein anderer Ansatz kann als „ allmächtiger Animator “ bezeichnet werden. Zusätzlich zur View-Klasse wird ein Animator-Controller erstellt und über Parameter gesteuert. Im neuen Fenster erscheint ein neuer Animator usw., bis die FPS beim Anzeigen von Fenstern zu sinken beginnen.



Nachdem wir nun einige der Schwierigkeiten bei der Arbeit mit uGUI hervorgehoben haben, möchte ich über einen anderen Ansatz zur Lösung dieses Problems sprechen.

Zustandsbehaftete Benutzeroberfläche

Während meiner Arbeit an einem meiner Lieblingsprojekte habe ich eine Bibliothek für die strukturierte UI-Entwicklung in Unity entwickelt. Später testeten mein Team und ich es in der Produktion und waren mit den Ergebnissen zufrieden.


Der Quellcode der Bibliothek steht auf GitHub zum Download bereit .

Zustandsbehaftete Komponente

Das Schlüsselelement der Bibliothek ist die StatefulComponent Komponente. Diese Komponente wird im Stamm-GameObject jedes Bildschirms platziert und enthält alle notwendigen Verweise auf interne Elemente, verteilt auf Registerkarten:



Jeder Link wird nach seiner Rolle benannt. Aus Code-Sicht ist der Rollensatz eine reguläre enum . Für jeden Typ von UI-Element (Schaltflächen, Bilder, Texte usw.) werden separate Rollensätze vorbereitet:


 public enum ButtonRole { ... } public enum ImageRole { ... } public enum TextRole { ... } ...


Rollen werden direkt aus der Komponente generiert und es ist nicht erforderlich, die enum manuell zu bearbeiten. Auch das Warten auf eine Neukompilierung beim Anlegen einer Rolle ist nicht notwendig, da diese enum Elemente direkt nach der Erstellung verwendet werden können.


Um Zusammenführungskonflikte zu vereinfachen, werden Aufzählungswerte basierend auf den Namen der Elemente berechnet:


 [StatefulUI.Runtime.RoleAttributes.ButtonRoleAttribute] public enum ButtonRole { Unknown = 0, Start = -1436209294, // -1436209294 == "Start".GetHashCode() Settings = 681682073, Close = -1261564850, Quests = 1375430261, }


Auf diese Weise können Sie vermeiden, dass serialisierte Werte in Fertighäusern beschädigt werden, wenn Sie und Ihre Kollegen zufällig gleichzeitig neue Rollen für Schaltflächen in verschiedenen Zweigen erstellen.


Jede Art von UI-Element (Schaltflächen, Texte, Bilder) befindet sich auf einer eigenen Registerkarte:



Durch die Verwendung von Rollen wird die vollständige Auszeichnung aller Elemente innerhalb des Fertighauses erreicht. Für den Zugriff auf Bilder und Texte sind keine SerializeField Sätze mehr erforderlich. Es reicht aus, einen Verweis auf StatefulComponent zu haben und die Rolle des gewünschten Bildes zu kennen, um beispielsweise dessen Sprite zu ersetzen.


Die derzeit zugänglichen Elementtypen sind:


  • Schaltflächen, Bilder, Umschalter, Schieberegler, Dropdowns, VideoPlayer, Animatoren
  • Texte, einschließlich UnityEngine.UI.Text und TextMeshProUGUI
  • TextInputs, einschließlich UnityEngine.UI.InputField und TMP_InputField
  • Objekte – für Verweise auf beliebige Objekte.


Für die Arbeit mit annotierten Objekten gibt es entsprechende Methoden. Im Code können Sie einen Verweis auf StatefulComponent verwenden oder die Klasse von StatefulView erben:


 public class ExamplePresenter { private StatefulComponent _view; public void OnOpen() { _view.GetButton(ButtonRole.Settings).onClick.AddListener(OnSettingsClicked); _view.GetButton(ButtonRole.Close).onClick.AddListener(OnCloseClicked); _view.GetSlider(SliderRole.Volume).onValueChanged.AddListener(OnVolumeChanged); } } public class ExampleScreen : StatefulView { private void Start() { SetText(TextRole.Title, "Hello World"); SetTextValues(TextRole.Timer, hours, minutes, seconds); SetImage(ImageRole.UserAvatar, avatarSprite); } }

Texte und Lokalisierung

Die Registerkarte mit Texten enthält neben der Rolle und dem Link zum Objekt folgende Spalten:


  • Code: ein Textschlüssel zur Lokalisierung
  • Kontrollkästchen Lokalisieren: ein Indikator dafür, dass das Textfeld einer Lokalisierung unterliegt
  • Wert: der aktuelle Textinhalt des Objekts
  • Lokalisiert: der aktuelle Text, der mit dem Schlüssel aus dem Feld „Code“ gefunden wurde



Die Bibliothek enthält kein integriertes Subsystem für die Arbeit mit Übersetzungen. Um Ihr Lokalisierungssystem zu verbinden, müssen Sie eine Implementierung der ILocalizationProvider Schnittstelle erstellen. Dies kann beispielsweise basierend auf Ihrem Back-End, ScriptableObjects oder Google Sheets erstellt werden.


 public class HardcodeLocalizationProvider : ILocalizationProvider { private Dictionary<string, string> _dictionary = new Dictionary<string, string> { { "timer" , "{0}h {1}m {2}s" }, { "title" , "Título do Jogo" }, { "description" , "Descrição longa do jogo" }, }; public string GetPhrase(string key, string defaultValue) { return _dictionary.TryGetValue(key, out var value) ? value : defaultValue; } }


Durch Klicken auf die Schaltfläche „Lokalisierung kopieren“ werden die Inhalte der Spalten „Code“ und „Wert“ in einem Format, das zum Einfügen in Google Sheets geeignet ist, in die Zwischenablage kopiert.

Interne Komponenten

Um die Wiederverwendung zu erleichtern, werden häufig einzelne Teile der Benutzeroberfläche in separate Fertighäuser extrahiert. StatefulComponent können wir außerdem eine Komponentenhierarchie erstellen, bei der jede Komponente nur mit ihren eigenen untergeordneten Schnittstellenelementen funktioniert.


Auf der Registerkarte Inner Comps können Sie internen Komponenten Rollen zuweisen:



Konfigurierte Rollen können im Code ähnlich wie andere Elementtypen verwendet werden:


 var header = GetInnerComponent(InnerComponentRole.Header); header.GetButton(ButtonRole.Close).onClick.AddListener(OnCloseClicked); header.SetText(TextRole.Title, "Header Title"); var footer = GetInnerComponent(InnerComponentRole.Footer); footer.GetButton(ButtonRole.Continue).onClick.AddListener(OnContinueClicked); footer.SetText(TextRole.Message, "Footer Message");

Behälter

Um eine Liste ähnlicher Elemente zu erstellen, können Sie die ContainerView Komponente verwenden. Sie müssen das Prefab für die Instanziierung und das Root-Objekt (optional) angeben. Im Bearbeitungsmodus können Sie Elemente mit StatefulComponent hinzufügen und entfernen:



Es ist praktisch, StatefulComponent zum Markieren des Inhalts instanziierter Fertighäuser zu verwenden. Zur Laufzeit können Sie die Methoden AddInstance<T> , AddStatefulComponent oder FillWithItems verwenden, um den Container zu füllen:


 var container = GetContainer(ContainerRole.Players); container.Clear(); container.FillWithItems(_player, (StatefulComponent view, PlayerData data) => { view.SetText(TextRole.Name, data.Name); view.SetText(TextRole.Level, data.Level); view.SetImage(ImageRole.Avatar, data.Avatar); });


Wenn Ihnen der Standard Object.Instantiate() zum Erstellen von Objekten nicht zusagt, können Sie dieses Verhalten beispielsweise für die Instanziierung mit Zenject überschreiben:


 StatefulUiManager.Instance.CustomInstantiateMethod = prefab => { return _diContainer.InstantiatePrefab(prefab); };


Interne Komponenten und Container bieten jeweils eine statische und dynamische Verschachtelung für StatefulComponent .


Wir haben über das Markup von Fertighäusern, die Lokalisierung und die Instanziierung nachgedacht. Jetzt ist es an der Zeit, zum interessantesten Teil überzugehen – der Entwicklung von Benutzeroberflächen basierend auf Zuständen.

Zustände

Wir betrachten das Konzept des Zustands als eine benannte Reihe von Änderungen an einem Fertighaus. Der Name ist in diesem Fall eine Rolle aus der StateRole Enumeration, und Beispiele für Änderungen am Fertighaus können sein:


  • Aktivieren und Deaktivieren eines GameObjects
  • Ersetzen von Sprites oder Materialien für Bildobjekte
  • Objekte auf dem Bildschirm bewegen
  • Texte und deren Aussehen verändern
  • Animationen abspielen
  • Und so weiter – Sie können Ihre eigenen Arten von Objektmanipulationen hinzufügen


Auf der Registerkarte „Zustände“ kann eine Reihe von Änderungen (Zustandsbeschreibung) konfiguriert werden. Ein konfigurierter Status kann direkt aus dem Inspektor übernommen werden:



Ein konfigurierter Status kann mithilfe der SetState -Methode aus dem Code angewendet werden:


 switch (colorScheme) { case ColorScheme.Orange: SetState(StateRole.Orange); break; case ColorScheme.Red: SetState(StateRole.Red); break; case ColorScheme.Purple: SetState(StateRole.Purple); break; }


Wenn auf der Registerkarte „Extras“ der Parameter „Anfangszustand bei Aktivierung anwenden“ aktiviert ist, können Sie den Zustand konfigurieren, der sofort bei der Objektinstanziierung angewendet wird.


Die Verwendung von Zuständen ermöglicht eine erhebliche Reduzierung der Codemenge, die auf der Ebene der View-Klasse erforderlich ist. Beschreiben Sie einfach jeden Status Ihres Bildschirms als eine Reihe von Änderungen in der StatefulComponent und wenden Sie je nach Spielsituation den erforderlichen Status aus dem Code an.

Staatsbaum

Tatsächlich ist die Entwicklung einer Benutzeroberfläche basierend auf Zuständen unglaublich praktisch. So sehr, dass es im Laufe der Zeit zu einem weiteren Problem führt: Mit der Weiterentwicklung des Projekts kann die Liste der Zustände für ein einzelnes Fenster eine unkontrollierbare Länge erreichen und daher schwierig zu navigieren sein. Darüber hinaus gibt es Staaten, die nur im Kontext einiger anderer Staaten sinnvoll sind. Um dieses Problem zu lösen, verfügt Statful UI über ein weiteres Tool: State Tree. Sie können darauf zugreifen, indem Sie auf der Registerkarte „Staaten“ auf die Schaltfläche „Statusbaum-Editor“ klicken.


Nehmen wir an, wir müssen ein Belohnungsfenster für eine Truhe erstellen. Das Fenster hat 3 Phasen:


  • Animierte Einführung der Truhe (State Intro )
  • Wiederholtes Erscheinen von drei verschiedenen Arten von Belohnungen aus der Truhe (je nach Belohnungsstatus gibt es „Geld “, „Emoji “ und „ Karten “, was eine Animation der aus der Truhe erscheinenden Belohnung auslöst)
  • Anzeige aller vergebenen Prämien in einer einzigen Liste (Status Ergebnisse )


Übergeordnete Zustände (in diesem Beispiel Reward ) werden jedes Mal angewendet, wenn untergeordnete Zustände aufgerufen werden:



Die Verwaltung einer konfigurierten StatefulComponent erfordert eine minimale Menge an einfachem und verständlichem Code, der die Komponenten mit den erforderlichen Daten füllt und den Status im richtigen Moment wechselt:


 public void ShowIntro() { SetState(StateRole.Intro); } public void ShowReward(IReward reward) { // Update the inner view with the reward reward.UpdateView(GetInnerComponent(InnerComponentRole.Reward)); // Switch on the type of reward switch (reward) { case ICardsReward cardsReward: SetState(StateRole.Cards); break; case IMoneyReward moneyReward: SetState(StateRole.Money); break; case IEmojiReward emojiReward: SetState(StateRole.Emoji); break; } } public void ShowResults(IEnumerable<IReward> rewards) { SetState(StateRole.Results); // Fill the container with the rewards GetContainer(ContainerRole.TotalReward) .FillWithItems(rewards, (view, reward) => reward.UpdateView(view)); }

Zustandsbehaftete API und Dokumentation

Rollen sollen eine bequeme und eindeutige Möglichkeit bieten, Links und Zustände für die spätere Verwendung im Code zu benennen. Es gibt jedoch Situationen, in denen die Beschreibung eines Zustands einen zu langen Namen erfordern würde und es praktischer wäre, einen kleinen Kommentar dazu zu hinterlassen, worauf dieser Link verweist oder welches Verhalten der Staat widerspiegelt. In solchen Fällen können Sie für jeden Link und Zustand in einer StatefulComponent eine Beschreibung hinzufügen:



Möglicherweise sind Ihnen bereits die Schaltflächen „API kopieren“ und „Dokumente kopieren“ auf jeder Registerkarte aufgefallen – diese kopieren Informationen für den ausgewählten Abschnitt. Darüber hinaus gibt es auf der Registerkarte „Extras“ ähnliche Schaltflächen – diese kopieren Informationen für alle Abschnitte gleichzeitig. Wenn Sie auf die Schaltfläche „API kopieren“ klicken, wird der generierte Code zur Verwaltung dieses StatfulComponent Objekts in die Zwischenablage kopiert. Hier ist ein Beispiel für unser Belohnungsfenster:


 // Insert the name of the chest here SetText(TextRole.Title, "Lootbox"); // Button to proceed to the reward issuance phase GetButton(ButtonRole.Overlay); // Button to display information about the card GetButton(ButtonRole.Info); // Container for displaying the complete list of awarded rewards GetContainer(ContainerRole.TotalReward); // Insert the card image here SetImage(ImageRole.Avatar, null); // Animated appearance of a chest SetState(StateRole.Intro);


Wenn Sie auf die Schaltfläche „Dokumente kopieren“ klicken, wird die Dokumentation für dieses Fertighaus im Markdown-Format in die Zwischenablage kopiert:


 ### RewardScreen Buttons: - Overlay - Button to proceed to the reward issuance phase - Info - Button to display information about the card Texts: - Title - Insert the name of the chest here Containers: - TotalReward - Container for displaying the complete list of awarded rewards Images: - Avatar - Insert the card image here States: - Intro - Animated appearance of a chest - Cards - Displaying rewards in the form of a card - Money - Displaying rewards in the form of game currency - Emoji - Displaying rewards in the form of an emoji - Results - Displaying a complete list of issued rewards


Es ist offensichtlich, dass es ziemlich schwierig ist, bei der Implementierung dieses Bildschirms mit solch detaillierten Anweisungen einen Fehler zu machen. In der Wissensdatenbank des Projekts können Sie problemlos aktuelle Informationen zu Ihrer UI-Organisation pflegen.


Gleichzeitig ermöglicht Stateful UI die Delegation der Erstellung von UI-Prefabs. Tatsächlich ermöglicht das zustandsbasierte Markup, das Verhalten des Fertighauses vollständig zu testen, bevor es an Programmierer weitergegeben wird. Dies bedeutet, dass Spieledesigner , technische Designer oder sogar separate UI-Entwickler Fertigteile vorbereiten können. Da außerdem eine API zwischen Code und Prefab erstellt wird, können Programmierung und Konfiguration von Prefabs parallel erfolgen! Es ist lediglich erforderlich, die API im Voraus zu formulieren. Aber auch wenn die Aufgabe der Konfiguration von Fertighäusern weiterhin bei den Programmierern liegt, beschleunigt der Einsatz von Stateful UI diese Arbeit erheblich.

Abschluss

Wie wir gesehen haben, vereinfacht Stateful UI die Arbeit mit UI-Elementzuständen erheblich. Es sind keine langen Zyklen mehr erforderlich, um SerializeFields zu erstellen, Code neu zu kompilieren und nach Referenzen in einer großen Anzahl von View-Klassenfeldern zu suchen. In den View-Klassen selbst ist es nicht mehr notwendig, eine große Menge Code für sich wiederholende Vorgänge wie das Ein- und Ausschalten von Objekten oder das Ändern der Textfarbe zu schreiben.


Die Bibliothek ermöglicht einen konsistenten Ansatz zum Organisieren von Layouts in einem Projekt, zum Markieren von Objekten in Fertighäusern, zum Erstellen von Zuständen, zum Verknüpfen mit UI-Elementen und zum Bereitstellen einer API und Dokumentation für die UI-Verwaltung. Es ermöglicht außerdem die Delegation der Erstellung von UI-Prefabs und beschleunigt die Arbeit damit.


Für die Zukunft umfasst die Projekt-Roadmap die folgenden Elemente:


  • Erweiterung der Fähigkeiten von Zuständen, Unterstützung neuer Arten von UI-Änderungen in der Beschreibung, wie z. B. neue Arten von Animationen, Abspielen von Sounds in Zuständen usw


  • Unterstützung für Farbpaletten zum Einfärben von Text und Bildern hinzugefügt


  • Unterstützung für Listen von Elementen mit GameObjects-Wiederverwendung hinzugefügt


  • Unterstützung einer größeren Anzahl von Unity-UI-Elementen


  • Automatisieren des Entladens hinzugefügter Texte zur Lokalisierung


  • Implementierung eines Test-Frameworks. Da wir über ein umfassendes Markup unserer Fertighäuser verfügen, können wir einfach einzurichtende ScriptableObject-basierte Szenarien im folgenden Format erstellen:


    1. Klicken Sie auf die Schaltfläche ButtonRole.Settings

    2. Überprüfen Sie, ob der Text in TextRole.SomeText mit „irgendeinem Wert“ übereinstimmt.

    3. Überprüfen Sie das Bild in ImageRole.SomeImage , um sicherzustellen, dass es einem bestimmten Sprite entspricht


  • Ein Tutorialsystem. Ähnlich wie beim Testen ermöglicht das markierte Layout das Erstellen von ScriptableObject-basierten Tutorialszenarien in Form von Anweisungen wie „Zeiger auf der Schaltfläche ButtonRole.UpgradeHero anzeigen“.


Der Quellcode des Projekts ist auf GitHub verfügbar . Sie sind herzlich eingeladen, Ausgaben zu erstellen oder zur Bibliothek beizutragen!