Architekturlösungen für ein Handyspiel. Teil 3: Blick auf den Strahlschub



In früheren Artikeln haben wir beschrieben, wie ein Modell bequem und mit umfangreichen Funktionen angeordnet werden sollte, welche Art von Befehlssystem als Controller geeignet wäre. Es ist an der Zeit, über den dritten Buchstaben unserer alternativen MVC-Abkürzung zu sprechen.

Tatsächlich verfügt der Assetstore über eine vorgefertigte, sehr hoch entwickelte UniRX-Bibliothek, die Reaktivität implementiert und die Inversion für die Einheit steuert. Aber wir werden am Ende des Artikels darüber sprechen, da dieses leistungsstarke, riesige und RX-konforme Tool für unseren Fall ziemlich überflüssig ist. Alles zu tun, was wir brauchen, ist durchaus möglich, ohne den RX hochzuziehen, und wenn Sie ihn besitzen, können Sie das auch problemlos damit tun.

Architekturlösungen für ein Handyspiel. Teil 1: Modell
Architekturlösungen für ein Handyspiel. Teil 2: Befehl und ihre Warteschlangen

Wenn eine Person gerade erst anfängt, das erste Spiel zu schreiben, scheint es für sie logisch, eine Funktion zu existieren, die die gesamte Form oder einen Teil davon zeichnet und jedes Mal zieht, wenn sich etwas Wichtiges ändert. Mit der Zeit wächst die Oberfläche, die Form und Teile der Formen werden einhundert, dann zweihundert, und wenn die Brieftasche ihren Zustand ändert, muss ein Viertel von ihnen neu gezeichnet werden. Und dann kommt der Manager und sagt, dass Sie "wie in diesem Spiel" einen kleinen roten Punkt auf der Schaltfläche machen müssen, wenn sich innerhalb der Schaltfläche ein Abschnitt befindet, in dem sich ein Unterabschnitt befindet, in dem sich die Schaltfläche befindet, und Sie jetzt über genügend Ressourcen verfügen, um etwas zu tun, indem Sie darauf klicken das ist wichtig Und das ist alles, gesegelt ...

Die Abkehr vom Konzept des Zeichnens erfolgt in mehreren Schritten. Zunächst wird das Problem einzelner Felder gelöst. Sie haben beispielsweise ein Feld im Modell und ein Textfeld, in dem der gesamte Inhalt angezeigt werden soll. Ok, wir starten ein Objekt, das Aktualisierungen dieses Felds abonniert, und bei jeder Aktualisierung werden die Ergebnisse einem Textfeld hinzugefügt. Im Code so etwas:

var observable = new ChildControl(FCPlayerModel.ASSIGNED, Player); observable.onChange(i => Assigned.text = i.ToString()) 

Jetzt müssen wir nicht mehr dem Neuzeichnen folgen, sondern nur dieses Design erstellen, und dann fällt alles, was im Modell passiert, in die Benutzeroberfläche. Gut, aber umständlich, es enthält viele offensichtlich unnötige Gesten, die ein Programmierer 100.500 Mal mit den Händen schreiben muss und manchmal Fehler macht. Lassen Sie uns diese Anzeigen in eine Erweiterungsfunktion einbinden, die die zusätzlichen Buchstaben unter der Haube verbirgt.

 Player.Get(c, FCPlayerModel.ASSIGNED).Action(c, i => Assigned.text = i.ToString()); 

Viel besser, aber das ist noch nicht alles. Das Verschieben des Modellfelds in das Textfeld ist eine so häufige und typische Operation, dass wir eine separate Wrapper-Funktion dafür erstellen. Jetzt fällt es ganz kurz und gut aus, wie es mir scheint.

 Player.Get(c, FCPlayerModel.ASSIGNED).SetText(c, Assigned); 

Hier zeigte ich die Hauptidee, an der ich mich beim Erstellen von Schnittstellen für den Rest meines Lebens orientieren werde: „Wenn ein Programmierer mindestens zweimal etwas tun musste, wickeln Sie es in eine spezielle praktische und kurze Funktion ein.“

Müllabfuhr


Ein Nebeneffekt des reaktiven Interface-Engineerings ist die Erstellung einer Reihe von Objekten, die etwas abonniert haben und daher den Speicher nicht ohne einen besonderen Kick verlassen. In der Antike habe ich mir einen Weg ausgedacht, der nicht so schön, aber einfach und erschwinglich ist. Beim Erstellen eines Formulars wird eine Liste aller Controller erstellt, die in Verbindung mit diesem Formular erstellt wurden. Der Kürze halber wird es einfach als "c" bezeichnet. Alle speziellen Wrapper-Funktionen akzeptieren diese Liste als ersten erforderlichen Parameter. Wenn DisconnectModel das Formular erstellt, übergibt es die Liste aller Steuerelemente und verunsichert sie gnadenlos mit dem Code des gemeinsamen Vorfahren. Keine Schönheit und Anmut, aber billig, zuverlässig und relativ praktisch. Sie können etwas mehr Sicherheit haben, wenn Sie anstelle des Kontrollblatts IView benötigen, um dies einzugeben und an alle diese Stellen weiterzugeben. Im Wesentlichen das Gleiche, das Vergessen, genau das gleiche auszufüllen, wird nicht funktionieren, aber es ist schwieriger zu hacken. Ich habe Angst zu vergessen, aber ich habe keine große Angst, dass jemand das System absichtlich kaputt macht, weil solche klugen Leute mit einem Gürtel und anderen Nicht-Software-Methoden bekämpft werden müssen, also beschränke ich mich einfach auf c.

Ein alternativer Ansatz kann von UniRX abgeleitet werden. Jeder Wrapper erstellt ein neues Objekt, das einen Link zu dem vorherigen Objekt hat, das er abhört. Am Ende wird die AddTo-Methode (Komponentenmethode) aufgerufen, die die gesamte Kette von Steuerelementen einem zerstörbaren Objekt zuordnet. In unserem Beispiel würde ein solcher Code folgendermaßen aussehen:

 Player.Get(FCPlayerModel.ASSIGNED).SetText(Assigned).AddTo(this); 

Wenn dieser letzte Besitzer der Kette beschließt, zerstört zu werden, sendet er an alle ihm zugewiesenen Kontrollen den Befehl "Töte dich wegen Entsorgung, wenn niemand außer mir auf dich hört". Und die ganze Kette wird gehorsam aufgeräumt. Natürlich ist es viel prägnanter, aber aus meiner Sicht gibt es einen wichtigen Fehler. AddTo kann versehentlich vergessen werden und niemand wird es jemals erfahren, bis es zu spät ist.

Tatsächlich können Sie den schmutzigen Unity-Hack verwenden und auf zusätzlichen Code in View verzichten:

 public static T AddTo<T>(this T disposable, Component component) where T : IDisposable { var composite = new CompositeDisposable(disposable); Observable .EveryUpdate() .Where(_ => component == null) .Subscribe(_ => composite.Dispose()) .AddTo(composite); return disposable; } 

Wie Sie wissen, ist ein Link zu einer Unicomponent-Komponente oder einem GameObject in Unity null. Sie müssen jedoch verstehen, dass dieser Hakokostyl einen Update-Listener für jede zerstörte Kontrollkette erstellt, und dies ist bereits ein wenig höflich.

Modellunabhängige Schnittstelle


Unser Ideal, das wir jedoch leicht erreichen können, ist die Situation, in der wir jederzeit den vollständigen GameState laden können, sowohl das vom Server verifizierte Modell als auch das Datenmodell für die Benutzeroberfläche, und die Anwendung sich bis zum Status aller Schaltflächen in genau demselben Zustand befindet. Dafür gibt es zwei Gründe. Das erste ist, dass einige Programmierer gerne im Formular-Controller oder sogar in der Ansicht selbst speichern und dabei darauf hinweisen, dass ihr Lebenszyklus genau dem des Formulars selbst entspricht. Das zweite ist, dass selbst wenn sich alle Daten für das Formular in seinem Modell befinden, der Befehl zum Erstellen und Ausfüllen des Formulars selbst die Form eines expliziten Funktionsaufrufs hat, mit einigen zusätzlichen Parametern, zum Beispiel, auf welches Feld aus der Liste fokussiert werden soll.

Sie müssen sich nicht damit befassen, wenn Sie nicht wirklich die Bequemlichkeit des Debuggens wünschen. Aber wir sind nicht so, wir wollen die Schnittstelle so bequem wie die grundlegenden Operationen mit dem Modell debuggen. Dazu liegt der folgende Fokus. Im UI-Teil des Modells wird eine Variable eingerichtet, z. B. .main, und darin wird als Teil des Befehls das Modell des Formulars eingefügt, das angezeigt werden soll. Der Status dieser Variablen wird von einem speziellen Controller überwacht. Wenn ein Modell in dieser Variablen angezeigt wird, instanziiert es je nach Typ das gewünschte Formular, platziert es bei Bedarf und sendet einen Aufruf an ConnectModel (Modell). Wenn die Variable aus dem Modell freigegeben wird, entfernt der Controller das Formular aus der Zeichenfläche und verwendet es. Daher werden keine Aktionen zum Umgehen des Modells ausgeführt, und alles, was Sie mit der Schnittstelle ausgeführt haben, ist im ExportChanges-Modell deutlich sichtbar. Und dann orientieren wir uns am Prinzip "Alles, was zweimal verpackt wurde" und verwenden auf allen Ebenen der Schnittstelle genau denselben Controller. Wenn die Form einen Platz für eine andere Form hat, wird ein UI-Modell für sie erstellt und eine Variable im Modell der übergeordneten Form erstellt. Genau das gleiche mit Listen.

Ein Nebeneffekt dieses Ansatzes besteht darin, dass jedem Formular zwei Dateien hinzugefügt werden, eine mit einem Datenmodell für dieses Formular und die andere, normalerweise eine Monobah mit Links zu UI-Elementen, die, nachdem sie das Modell in ihrer ConnectModel-Funktion erhalten haben, alle reaktiven Controller für alle erstellen Modellfelder und alle UI-Elemente. Nun, es ist noch kompakter, so dass es auch bequem ist, damit zu arbeiten, wahrscheinlich ist es unmöglich. Wenn möglich, schreiben Sie in die Kommentare.

Listensteuerelemente


Eine typische Situation ist, wenn das Modell eine Liste einiger Elemente enthält. Da ich möchte, dass alles sehr bequem und vorzugsweise in einer einzigen Zeile erledigt wird, wollte ich auch etwas für Listen tun, die bequem zu handhaben sind. Eine Zeile ist möglich, aber es stellt sich heraus, dass sie unangenehm lang ist. Empirisch stellte sich heraus, dass fast die gesamte Fallvielfalt nur von zwei Arten von Kontrollen abgedeckt wird. Der erste überwacht den Status einer Sammlung und ruft drei Lambda-Funktionen auf. Der erste wird aufgerufen, wenn ein Element zur Sammlung hinzugefügt wird, der zweite, wenn das Element die Sammlung verlässt, und der dritte wird aufgerufen, wenn die Elemente der Sammlung die Reihenfolge ändern. Die zweithäufigste Art der Steuerung überwacht die Liste und ist die Quelle eines Abonnements - Seiten mit einer bestimmten Nummer. Das heißt, es folgt beispielsweise einer Liste mit einer Länge von 102 Elementen und gibt selbst eine Liste von 10 Elementen vom 20. bis zum 29. zurück. Und die generierten Ereignisse sind genau die gleichen, als wäre es eine Liste selbst.

Nach dem Prinzip „Erstellen eines Wrappers für alles, was zweimal ausgeführt wurde“, wurde natürlich eine große Anzahl praktischer Wrapper angezeigt, die beispielsweise Factory nur als Eingabe akzeptieren, eine Korrespondenz zwischen Modelltypen und ihren Ansichten erstellen und einen Link zu Canvas erstellen Sie müssen die Elemente hinzufügen. Und viele andere ähnliche, nur etwa ein Dutzend Wrapper für typische Fälle.

Komplexere Steuerungen


Manchmal treten Situationen auf, die überflüssig sind, um durch das Modell ausgedrückt zu werden, soweit dies offensichtlich ist. Hier können Steuerelemente, die eine Operation an einem Wert ausführen, zur Rettung kommen, sowie Steuerelemente, die andere Steuerelemente überwachen. Zum Beispiel eine typische Situation: Eine Aktion hat einen Preis, und die Schaltfläche ist nur aktiv, wenn sich mehr Geld auf dem Konto befindet als der Preis.

 item.Get(c, FCUnitItem.COST).Join(c, Player.Get(c, MONEY)).Func(c, (cost, money) => cost <= money).SetActive(c, BuyButton); 

Tatsächlich ist die Situation so typisch, dass es nach meinem Prinzip einen fertigen Wrapper dafür gibt, aber dann habe ich seinen Inhalt gezeigt.

Wir haben den zu kaufenden Artikel genommen, ein Objekt erstellt, das eines seiner Felder abonniert hat und einen Wert vom Typ long hat. Sie fügten ein weiteres Steuerelement hinzu, das ebenfalls vom Typ long ist. Die Methode gab ein Steuerelement mit zwei Werten zurück. Das geänderte Ereignis wird ausgelöst, wenn sich eines von ihnen ändert. Anschließend erstellt Func ein Objekt für jede Änderung der Eingabe, die die Funktion berechnet, und das geänderte Ereignis, wenn der endgültige Wert berechnet wird Funktion hat sich geändert.

Der Compiler erstellt erfolgreich den erforderlichen Steuerelementtyp auf der Grundlage der Eingabedatentypen und des Typs des resultierenden Ausdrucks. In seltenen Fällen, in denen der von der Lambda-Funktion zurückgegebene Typ nicht offensichtlich ist, werden Sie vom Compiler aufgefordert, ihn explizit zu präzisieren. Schließlich hört der letzte Aufruf das Boolesche Steuerelement ab, je nachdem, welches es die Taste ein- oder ausschaltet.

Tatsächlich akzeptiert der echte Wrapper im Projekt zwei Schaltflächen als Eingabe, eine für den Fall, dass Geld vorhanden ist, und die andere für den Fall, dass nicht genügend Geld vorhanden ist. Der Befehl zum Öffnen des modalen Fensters „Währungen kaufen“ hängt auch an der zweiten Schaltfläche. Und das alles in einer einfachen Zeile.

Es ist leicht zu erkennen, dass Sie mit Join und Func beliebig komplexe Strukturen erstellen können. In meinem Code gab es eine Funktion, die komplexe Steuerelemente erzeugte, die berechnete, wie viel ein Spieler kaufen konnte, indem er die Anzahl der Spieler auf seiner Seite berücksichtigte, und die Regel, dass jeder das Budget um 10% überschreiten konnte, wenn alle zusammen das Gesamtbudget nicht überstiegen. Und dies ist ein Beispiel dafür, wie es nicht notwendig ist, dies zu tun, denn wie einfach und leicht es zu debuggen ist, was in den Modellen passiert, ist ebenso schwierig, einen Fehler in reaktiven Steuerungen zu erkennen. Sie werden sogar die Hinrichtung abfangen und viel Zeit damit verbringen, zu verstehen, was dazu geführt hat.

Daher lautet das allgemeine Prinzip der Verwendung komplexer Steuerelemente wie folgt: Beim Prototyping eines Formulars können Sie Entwürfe für reaktive Steuerelemente verwenden, insbesondere wenn Sie nicht sicher sind, ob sie in Zukunft komplizierter werden. Sobald Sie jedoch den Verdacht haben, dass sie nicht funktionieren, werden Sie nicht verstehen, was passiert ist. Sie sollten diese Manipulationen sofort auf das Modell übertragen und die zuvor in den Steuerelementen durchgeführten Berechnungen in Erweiterungsmethoden in statischen Regelklassen einfügen.

Dies unterscheidet sich erheblich von dem Prinzip „Mach es sofort gut“, das unter Perfektionisten so beliebt ist, weil wir in einer Welt der Spieleentwickler leben und wenn du anfängst, ein Formular zu überspringen, kannst du absolut nicht sicher sein, was es in drei Tagen tun wird. Einer meiner Kollegen sagte: "Wenn ich jedes Mal fünf Cent bekommen würde, wenn Spieledesigner ihre Meinung ändern, wäre ich bereits eine sehr reiche Person." In der Tat ist das nicht schlecht, aber auch umgekehrt ist gut. Das Spiel sollte sich durch Ausprobieren entwickeln, denn wenn Sie keinen dummen Klon machen, können Sie sich nicht vorstellen, was die Spieler wirklich brauchen.

Eine Datenquelle für mehrere Ansichten


Für so viele archetypische Fälle, dass Sie separat darüber sprechen müssen. Es kommt vor, dass dasselbe Modell eines Elements als Teil eines Schnittstellenmodells in einer anderen Ansicht gerendert wird, je nachdem, wo und in welchem ​​Kontext dies geschieht. Und wir verwenden das Prinzip "ein Typ, eine Ansicht". Sie haben beispielsweise eine Waffeneinkaufskarte, die dieselben unkomplizierten Informationen enthält, in verschiedenen Geschäftsmodi jedoch durch unterschiedliche Fertighäuser dargestellt werden sollte. Die Lösung besteht aus zwei Teilen für zwei verschiedene Situationen.

Die erste ist, wenn diese Ansicht in zwei verschiedenen Ansichten platziert wird, z. B. einem Geschäft in Form einer kurzen Liste und einem Geschäft mit großen Bildern. In diesem Fall werden zwei separate Fabriken eingerichtet, um eine Typ-Fertighaus-Übereinstimmung zu erstellen. In der ConnectModel-Methode einer Ansicht verwenden Sie die eine und die andere in der anderen. Ganz anders ist es, wenn Sie Karten mit absolut identischen Informationen an einem Ort etwas anders anzeigen müssen. In diesem Fall verfügt das Elementmodell manchmal über ein zusätzliches Feld, das den festlichen Hintergrund eines bestimmten Elements angibt, und manchmal hat das Elementmodell nur einen Erben, der keine Felder hat und nur mit einem anderen Fertighaus gezeichnet werden muss. Im Prinzip widerspricht nichts.

Es scheint eine offensichtliche Lösung zu sein, aber ich sah genug in einem seltsamen Code über seltsame Tänze mit einem Tamburin um diese Situation herum und hielt es für notwendig, darüber zu schreiben.

Sonderfall: Steuerelemente mit verdammt vielen Abhängigkeiten


Es gibt einen ganz besonderen Fall, über den ich separat sprechen möchte. Dies sind Steuerelemente, die eine sehr große Anzahl von Elementen überwachen. Zum Beispiel ein Steuerelement, das eine Liste von Modellen überwacht und den Inhalt eines Feldes zusammenfasst, das in jedem der Elemente liegt. Bei einem großen Überrohr in der Liste, das beispielsweise mit Daten gefüllt wird, besteht bei einem solchen Steuerelement die Gefahr, dass so viele Ereignisse über die Änderung erfasst werden, wie es plus eines in der Liste der Elemente gibt. Die Aggregatfunktion so oft neu zu berechnen, ist natürlich eine schlechte Idee. Insbesondere in solchen Fällen erstellen wir ein Steuerelement, das das Ereignis onTransactionFinished abonniert, das sich vom GameState abhebt. Wie wir uns erinnern, ist in jedem Modell ein Link zum GameState verfügbar. Und bei jeder Änderung der Eingabe markiert dieses Steuerelement einfach selbst, dass sich die Quelldaten geändert haben, und wird nur dann nachgezählt, wenn es eine Nachricht über das Ende der Transaktion empfängt oder wenn es feststellt, dass die Transaktion zu dem Zeitpunkt bereits abgeschlossen ist, als es eine Nachricht vom Eingabeereignisstrom empfangen hat . Es ist klar, dass ein solches Steuerelement möglicherweise nicht vor unnötigen Nachrichten geschützt ist, wenn es zwei solche Steuerelemente in der Flussverarbeitungskette gibt. Der erste wird eine Wolke von Änderungen ansammeln, auf das Ende der Transaktion warten, den Änderungsstrom weiter starten, und es gibt einen anderen, der bereits eine Reihe von Änderungen erfasst hat, das Ereignis über das Ende der Transaktion erhalten hat (er hatte das Pech, in der Liste der Funktionen zu sein, die das Ereignis zuvor abonniert haben), alles gezählt hat und dann bam und ein anderes Änderungsereignis und erzähle alles ein zweites Mal. Es mag sein, aber selten und noch wichtiger, wenn Ihre Steuerelemente solche monströsen Berechnungen mehr als einmal in einem Berechnungsfluss durchführen, dann machen Sie etwas falsch und müssen all diese höllischen Manipulationen auf das Modell und die Regeln übertragen, wo sie sich befinden in der Tat der Ort.

UniRX Ready Library


Und es wäre möglich, uns auf all das zu beschränken und ruhig mit dem Schreiben Ihres Meisterwerks zu beginnen, zumal es im Vergleich zum Modell und den Kontrollteams sehr einfach ist und in weniger als einer Woche geschrieben wird, wenn die Idee, dass Sie ein Fahrrad erfinden, nicht unklar ist Alles, was vor mir gedacht und geschrieben wurde, wird kostenlos an alle verteilt.

Wenn wir UniRX aufdecken, finden wir ein schönes und standardkonformes Design, mit dem Threads aus allem im Allgemeinen erstellt, geschickt zusammengeführt, vom Hauptthread zum Nicht-Hauptthread gefiltert oder die Steuerung an den Hauptthread zurückgegeben werden kann, der eine Reihe vorgefertigter Tools zum Senden an verschiedene Orte usw. enthält. weiter. Wir haben dort nicht genau zwei Dinge: Einfachheit und Bequemlichkeit des Debuggens. Haben Sie jemals versucht, ein mehrstöckiges Gebäude auf Linq schrittweise im Debugger zu debuggen? Hier ist es also noch viel schlimmer. Gleichzeitig fehlt uns völlig, wofür all diese hoch entwickelten Maschinen geschaffen wurden. Der Einfachheit halber fehlen Debugging- und Reproduktionszustände, es fehlt uns eine Vielzahl von Signalquellen, alles geschieht im Hauptstrom, da das Spielen mit Multithreading im Metaspiel völlig redundant ist, die gesamte Asynchronität der Befehlsverarbeitung in der Befehlssende-Engine verborgen ist und die Asynchronität selbst sehr viel in Anspruch nimmt Nicht viel Platz, viel mehr Aufmerksamkeit wird allen Arten von Überprüfungen, Selbstüberprüfungen und den Möglichkeiten der Protokollierung und Wiedergabe gewidmet.

Wenn Sie bereits wissen, wie man UniRX verwendet, mache ich es im Allgemeinen speziell für IObservable-Modelle. Sie können die Trumpffunktionen Ihrer Lieblingsbibliothek dort verwenden, wo Sie sie benötigen. Im Übrigen empfehle ich jedoch, keine Panzer aus Hochgeschwindigkeitsautos und Autos aus Panzern nur am Boden zu bauen dass beide Räder haben.

Am Ende des Artikels habe ich Ihnen, liebe Leser, traditionelle Fragen, die für mich sehr wichtig sind, meine Vorstellungen vom Schönen und die Aussichten für die Entwicklung meiner wissenschaftlichen und technischen Arbeit.

Source: https://habr.com/ru/post/de436060/


All Articles