Heute werde ich darüber sprechen, wie einige Projekte bei Pixonic zu dem gekommen sind, was seit langem die Norm für das gesamte globale Front-End ist - reaktive Verknüpfung.
Die überwiegende Mehrheit unserer Projekte ist in Unity 3D geschrieben. Und wenn andere Client-Technologien mit Reaktivität gut funktionieren (MVVM, Qt, Millionen von JS-Frameworks) und dies als selbstverständlich angesehen wird, gibt es in Unity keine integrierten oder allgemein akzeptierten Bindungen.
Zu diesem Zeitpunkt hatte wahrscheinlich jemand eine Frage: „Warum? Wir nutzen das nicht und leben gut. "
Es gab Gründe. Genauer gesagt gab es Probleme, eine der Lösungen, für die ein solcher Ansatz verwendet werden könnte. Infolgedessen wurde es eins. Und die Details sind unter dem Schnitt.
Zunächst zum Projekt, dessen Probleme eine solche Lösung erforderten. Natürlich sprechen wir über War Robots - ein gigantisches Projekt mit vielen verschiedenen Teams aus Entwicklung, Support, Marketing usw. Wir interessieren uns jetzt nur für zwei davon: das Team der Client-Programmierer und das Team der Benutzeroberfläche. Im Folgenden werden wir der Einfachheit halber "Code" und "Layout" nennen. Es kam vor, dass einige Leute mit dem Design und Layout der Benutzeroberfläche beschäftigt sind, während andere die "Revitalisierung" all dessen durchführen. Dies ist logisch und meiner Erfahrung nach habe ich viele ähnliche Beispiele für Teamorganisation gesehen.
Wir haben festgestellt, dass mit dem wachsenden Fluss von Funktionen im Projekt die Interaktion zwischen Code und Layout zu einem Ort von Deadlocks und Engpässen wird. Programmierer warten auf vorgefertigte Widgets für die Arbeit, Layout-Designer - auf einige Änderungen am Code. Ja, während dieser Interaktion sind viele Dinge passiert. Kurz gesagt, manchmal wurde daraus Chaos und Aufschub.
Lassen Sie mich jetzt erklären. Schauen Sie sich das klassische einfache Widget-Beispiel an - insbesondere die RefreshData-Methode. Der Rest der Kesselplatte habe ich gerade aus Gründen der Glaubwürdigkeit hinzugefügt, und es ist keine besondere Aufmerksamkeit wert.
public class PlayerProfileWidget : WidgetBehaviour
{
[SerializeField] private Text nickname;
[SerializeField] private Image avatar;
[SerializeField] private Text level;
[SerializeField] private GameObject hasUpgradeMark;
[SerializeField] private Button upgradeButton;
public void Initialize(ProfileService profileService)
{
RefreshData(profileService.Player);
upgradeButton.onClick
.Subscribe(profileService.UpgradePlayer)
.DisposeWith(Lifetime);
profileService.PlayerUpgraded
.Subscribe(RefreshData)
.DisposeWith(Lifetime);
}
private void RefreshData(in PlayerModel player)
{
nickname.text = player.Id;
avatar.overrideSprite = Resources.Load<Sprite>($"Avatars/{player.Avatar}_Small");
level.text = player.Level.ToString();
hasUpgradeMark.SetActive(player.HasUpgrade);
}
}
Dies ist ein Beispiel für eine statische Top-Down-Verknüpfung. In der Komponente des oberen (in der Hierarchie) GameObject verknüpfen Sie Komponenten der entsprechenden Arten von unteren Objekten. Hier ist alles sehr einfach, aber nicht sehr flexibel.
Die Funktionalität von Widgets wird mit dem Aufkommen neuer Funktionen ständig erweitert. Stellen wir uns vor. Es sollte jetzt einen Rand um den Avatar geben, dessen Aussehen vom Level des Spielers abhängt. Okay, fügen wir einen Link zum Bild des Rahmens hinzu und tauchen das Sprite ein, das der Ebene dort entspricht. Fügen Sie dann die Einstellung für die Anpassung der Ebene und des Rahmens hinzu und geben Sie alles dem Layout. Erledigt.
Ein Monat ist vergangen. Jetzt erscheint im Widget des Spielers ein Clansymbol, wenn er Mitglied ist. Und Sie müssen auch den Titel registrieren, den er dort hat. Und der Spitzname muss grün gestrichen werden, wenn es ein Upgrade gibt. Außerdem verwenden wir jetzt TextMeshPro. Und auch ...
Nun, Sie haben die Idee. Der Code wird immer komplizierter, mit verschiedenen Bedingungen überwachsen.
Es gibt verschiedene Möglichkeiten, hier zu arbeiten. Beispielsweise ändert der Programmierer den Widget-Code und gibt die Änderungen am Layout an. Sie fügen Komponenten hinzu und verknüpfen sie mit neuen Feldern. Oder umgekehrt: Das Layout kann rechtzeitig im Voraus eintreffen, der Programmierer selbst wird alles verknüpfen, was benötigt wird. Normalerweise gibt es mehrere weitere Iterationen von Fixes. In jedem Fall ist dieser Prozess nicht parallel. Beide Mitwirkenden arbeiten an derselben Ressource. Das Zusammenführen von Fertighäusern oder Szenen ist immer noch ein Vergnügen.
Für Ingenieure ist alles einfach: Wenn Sie ein Problem sehen, versuchen Sie es zu lösen. Also haben wir es versucht. Infolgedessen kamen wir auf die Idee, dass es notwendig ist, die Kontaktfront zwischen den beiden Teams einzuschränken. Und reaktive Muster verengen diese Front auf einen Punkt - das, was allgemein als Ansichtsmodell bezeichnet wird. Für uns ist es ein Vertrag zwischen Code und Layout. Wenn ich zu den Details komme, wird die Bedeutung des Vertrags klar und warum er den Parallelbetrieb zweier Teams nicht blockiert.
Zu der Zeit, als wir nur darüber nachdachten, gab es mehrere Lösungen von Drittanbietern. Wir haben nach Unity Weld, Peppermint Data Binding und DisplayFab gesucht. Sie alle hatten ihre Vor- und Nachteile. Aber einer der fatalen Fehler für uns war häufig - schlechte Leistung für unsere Zwecke. Sie mögen auf einfachen Schnittstellen gut funktionieren, aber zu diesem Zeitpunkt konnten wir die Komplexität der Schnittstellen nicht vermeiden.
Da die Aufgabe nicht unerschwinglich schwierig schien und selbst einschlägige Erfahrungen vorlagen, wurde beschlossen, ein reaktives Bindungssystem im Studio zu implementieren.
Die Aufgaben waren wie folgt:
- Performance. Der Mechanismus zur Weitergabe von Änderungen selbst muss schnell sein. Es ist auch wünschenswert, die Belastung des GC zu reduzieren, damit Sie all dies auch im Gameplay nutzen können, wo das Einfrieren überhaupt nicht zufrieden ist.
- Bequemes Authoring. Dies ist notwendig, damit die Mitarbeiter des UI-Teams mit dem System arbeiten können.
- Praktische API.
- Erweiterbarkeit.
Von oben nach unten oder allgemeine Beschreibung
Die Aufgabe ist klar, die Ziele sind klar. Beginnen wir mit dem "Vertrag" - dem ViewModel. Jede Person sollte in der Lage sein, es zu bilden, was bedeutet, dass die Implementierung des ViewModel so einfach wie möglich sein sollte. Es ist im Grunde nur eine Reihe von Eigenschaften, die den aktuellen Anzeigestatus bestimmen.
Der Einfachheit halber haben wir die Menge der Eigenschaftstypen mit Werten so weit wie möglich auf bool, int, float und string beschränkt. Dies wurde durch mehrere Überlegungen gleichzeitig diktiert:
- Das Serialisieren dieser Typen in Unity ist mühelos.
- , -, . , Sprite -, PlayerModel , ;
- , .
Alle Eigenschaften sind aktiv und informieren Abonnenten über Änderungen ihrer Werte. Diese Werte sind nicht immer vorhanden - es gibt nur Ereignisse in der Geschäftslogik, die irgendwie visualisiert werden müssen. In diesem Fall gibt es einen Eigenschaftstyp ohne Wert - Ereignis.
Natürlich können Sie auch nicht auf Sammlungen in Schnittstellen verzichten. Daher gibt es auch einen Sammlungseigenschaftstyp. Die Sammlung benachrichtigt die Abonnenten über jede Änderung ihrer Zusammensetzung. Sammlungselemente sind auch ViewModels einer bestimmten Struktur oder eines bestimmten Schemas. Dieses Schema wird auch im Vertrag bei der Bearbeitung beschrieben.
Im ViewModel-Editor sieht dies folgendermaßen aus:
Es ist zu beachten, dass Eigenschaften direkt im Inspektor und im laufenden Betrieb bearbeitet werden können. Auf diese Weise können Sie sehen, wie sich das Widget (oder Fenster oder Szene oder was auch immer) zur Laufzeit auch ohne Code verhält, was in der Praxis sehr praktisch ist.
Wenn das ViewModel die Oberseite unseres Bindungssystems ist, dann sind die sogenannten Applikatoren die Unterseite. Dies sind die endgültigen Abonnenten der ViewModel-Eigenschaften, die die gesamte Arbeit erledigen:
- Aktivieren / Deaktivieren von GameObject oder einzelnen Komponenten durch Ändern des Werts der booleschen Eigenschaft.
- Ändern Sie den Text im Feld abhängig vom Wert der Zeichenfolgeeigenschaft.
- Der Animator wird gestartet, seine Parameter werden geändert.
- Ersetzen Sie das gewünschte Sprite aus der Sammlung durch einen Index- oder Zeichenfolgenschlüssel.
Ich werde damit aufhören, da die Anzahl der Anwendungen nur durch die Vorstellungskraft und den Umfang der Aufgaben, die Sie lösen, begrenzt ist.
So sehen einige Applikatoren im Editor aus:
Für mehr Flexibilität können Adapter zwischen Eigenschaften und Applikatoren verwendet werden. Dies sind Entitäten zum Transformieren von Eigenschaften, bevor sie angewendet werden. Es gibt auch viele verschiedene:
- Boolean - zum Beispiel, wenn Sie eine boolesche Eigenschaft invertieren oder true oder false zurückgeben müssen, abhängig von einem Wert eines anderen Typs (ich möchte einen goldenen Rand, wenn der Level über 15 liegt).
- Arithmetik . Kein Kommentar hier.
- Vorgänge für Sammlungen : Invertieren, nur einen Teil einer Sammlung übernehmen, nach Schlüssel sortieren und vieles mehr.
Auch hier kann es eine Vielzahl verschiedener Adapteroptionen geben, daher werde ich nicht fortfahren.
Obwohl die Gesamtzahl der verschiedenen Applikatoren und Adapter groß ist, ist der überall verwendete Basissatz sehr begrenzt. Eine Person, die mit Inhalten arbeitet, muss zuerst dieses Set studieren, was die Trainingszeit geringfügig verlängert. Sie müssen jedoch einmal Zeit darauf verwenden, damit es hier keine großen Probleme gibt. Darüber hinaus haben wir ein Kochbuch und eine Dokumentation zu diesem Thema.
Wenn dem Layout etwas fehlt, fügen Programmierer die erforderlichen Komponenten hinzu. Gleichzeitig sind die meisten Applikatoren und Adapter universell und werden aktiv wiederverwendet. Unabhängig davon sollte beachtet werden, dass wir immer noch Applikatoren haben, die über UnityEvent an der Reflexion arbeiten. Sie sind in Fällen anwendbar, in denen der erforderliche Applikator noch nicht implementiert wurde oder seine Implementierung unpraktisch ist.
Dies trägt sicherlich zur Arbeit des Layout-Teams bei. In unserem Fall sind sie jedoch sogar mit dem Grad an Freiheit und Unabhängigkeit von Programmierern zufrieden, den sie erhalten. Und wenn die Arbeit von der Seite des Layouts zugenommen hat, dann ist von der Seite des Codes alles jetzt viel einfacher.
Kehren wir zum PlayerProfileWidget-Beispiel zurück. So sieht es jetzt in unserem hypothetischen Projekt als Präsentator aus, da wir kein Widget mehr als Komponente benötigen und alles aus dem ViewModel abrufen können, anstatt alles direkt zu verknüpfen:
public class PlayerProfilePresenter : Presenter
{
private readonly IMutableProperty<string> _playerId;
private readonly IMutableProperty<string> _playerAvatar;
private readonly IMutableProperty<int> _playerLevel;
private readonly IMutableProperty<bool> _playerHasUpgrade;
public PlayerProfilePresenter(ProfileService profileService, IViewModel viewModel)
{
_playerId = viewModel.GetString("player/id");
_playerAvatar = viewModel.GetString("player/avatar");
_playerLevel = viewModel.GetInteger("player/level");
_playerHasUpgrade = viewModel.GetBoolean("player/has-upgrade");
RefreshData(profileService.Player);
viewModel.GetEvent("player/upgrade")
.Subscribe(profileService.UpgradePlayer)
.DisposeWith(Lifetime);
profileService.PlayerUpgraded
.Subscribe(RefreshData)
.DisposeWith(Lifetime);
}
private void RefreshData(in PlayerModel player)
{
_playerId.Value = player.Id;
_playerAvatar.Value = player.Avatar;
_playerLevel.Value = player.Level;
_playerHasUpgrade.Value = player.HasUpgrade;
}
}
Im Konstruktor können Sie sehen, wie der Code Eigenschaften aus dem ViewModel abruft. Ja, in diesem Code werden Überprüfungen der Einfachheit halber weggelassen, aber es gibt Methoden, die eine Ausnahme auslösen, wenn sie die gewünschte Eigenschaft nicht finden. Darüber hinaus verfügen wir über mehrere Tools, die eine recht starke Garantie dafür bieten, dass die erforderlichen Felder vorhanden sind. Sie basieren auf der Asset-Validierung, über die Sie hier lesen können .
Ich werde nicht auf Implementierungsdetails eingehen, da dies viel Text und Ihre Zeit in Anspruch nehmen wird. Wenn es eine öffentliche Untersuchung gibt, ist es besser, sie in einem separaten Artikel zu veröffentlichen. Ich werde nur sagen, dass sich die Implementierung nicht sehr vom gleichen Rx unterscheidet, nur dass alles etwas einfacher ist.
Die Tabelle zeigt die Ergebnisse eines Benchmarks, der 500 Formulare mit InputField, Text und Button erstellt, die einem Eigenschaftsmodell und einer Aktionsfunktion zugeordnet sind.
Abschließend kann ich berichten, dass die oben genannten Ziele erreicht wurden. Vergleichende Benchmarks zeigen sowohl Speicher- als auch Zeitgewinne im Vergleich zu den genannten Optionen. Je vertrauter das Layout-Team und die Mitarbeiter anderer Abteilungen, die sich mit Inhalten befassen, werden, desto geringer werden Reibung und Blockierung. Die Effizienz und Qualität des Codes hat zugenommen, und jetzt erfordern viele Dinge kein Eingreifen des Programmierers.