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.
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.
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).
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.
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 .
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.
UnityEngine.UI.Text
und TextMeshProUGUI
UnityEngine.UI.InputField
und TMP_InputField
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); } }
Die Registerkarte mit Texten enthält neben der Rolle und dem Link zum Objekt folgende Spalten:
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.
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");
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.
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:
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.
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.
Ü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)); }
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.
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.
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:
Klicken Sie auf die Schaltfläche ButtonRole.Settings
Überprüfen Sie, ob der Text in TextRole.SomeText
mit „irgendeinem Wert“ übereinstimmt.
Ü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!