Architektonische Lösungen für mobile Spiele. Teil 3: Blick auf den Jetantrieb



    In vorherigen Artikeln haben wir beschrieben, wie ein bequemes und gut entworfenes Modell angeordnet werden sollte, welches Befehlssystem, das die Funktionen der Controller erfüllt, für sie geeignet wäre. Es war an der Zeit, über den dritten Buchstaben unserer alternativen MVC-Abkürzung zu sprechen.

    Tatsächlich gibt es eine vorgefertigte, sehr ausgereifte Bibliothek UniRX, die Reaktivität und Umkehrung der Kontrolle für die Einheit implementiert. Aber wir werden am Ende des Artikels darüber sprechen, denn dieses leistungsstarke, riesige und RX-kompatible Tool für unseren Fall ist ziemlich überflüssig. Es ist durchaus möglich, alles zu tun, was wir brauchen, ohne den RX hochzuziehen, und wenn Sie es besitzen, wird es Ihnen nicht schwer fallen, das Gleiche zu tun.

    Architektonische Lösungen für mobile Spiele. Teil 1: Modell
    Architektonische Lösungen für mobile Spiele. Teil 2: Befehl und ihre Warteschlangen

    Wenn eine Person gerade damit beginnt, das erste Spiel zu schreiben, erscheint es logisch, dass es eine Funktion gibt, die die gesamte Form oder einen Teil davon zieht und jedes Mal, wenn sich etwas Wichtiges geändert hat, zieht. Die Zeit vergeht, die Schnittstelle wächst, Fomochek und Teile der Formen werden zu hundert, dann zu zweihundert. Wenn sich der Zustand der Brieftasche ändert, muss ein Viertel von ihnen neu gezeichnet werden. Und dann kommt der Manager und sagt, dass es "wie in diesem Spiel" notwendig ist, einen kleinen roten Punkt auf der Schaltfläche zu setzen, wenn sich innerhalb der Schaltfläche ein Abschnitt befindet, in dem sich ein Unterabschnitt befindet, in dem sich eine Schaltfläche befindet, und jetzt genügend Ressourcen vorhanden sind das ist wichtig Und alles segelte ...

    Die Abweichung vom Zeichnungskonzept erfolgt in mehreren Schritten. Zuerst wurde das Problem einzelner Felder gelöst. Sie haben beispielsweise ein Feld in einem Modell und ein Textfeld, in dem der gesamte Inhalt angezeigt werden soll. Ok, wir erhalten ein Objekt, das Aktualisierungen dieses Felds abonniert und bei jedem Update die Ergebnisse in ein Textfeld einfügt. Im Code etwa so:

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

    Jetzt müssen wir dem Neuzeichnen nicht mehr folgen, es reicht aus, diese Konstruktion zu erstellen, und alles, was im Modell passiert, wird in die Benutzeroberfläche gelangen. Nun, aber umständlich, enthält es eine Menge offensichtlich unnötiger Gesten, die ein Programmierer mit seinen Händen 100.500-mal schreiben muss und manchmal Fehler macht. Packen Sie diese Anzeigen in die Erweiterungsfunktion ein, wodurch die zusätzlichen Kleinbuchstaben unter der Haube ausgeblendet werden.

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

    Viel besser, aber das ist noch nicht alles. Wenn das Modellfeld für so viele häufige und typische Operationen in ein Textfeld verschoben wird, erstellen wir dafür eine eigene Wrapper-Funktion. Jetzt fällt es ziemlich kurz und gut aus, denke ich.

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

    Hier habe ich die Hauptidee gezeigt, die ich verwenden werde, um die Erstellung des Interfaces für den Rest meines Lebens zu leiten: "Wenn Sie mindestens zwei Mal etwas für den Programmierer tun mussten, packen Sie es in eine besonders praktische und kurze Funktion."

    Müllsammlung


    Ein Nebeneffekt der reaktiven Schnittstellenkonstruktion ist die Erzeugung von Haufen von Objekten, die für etwas signiert sind und daher ohne besonderen Tritt kein Gedächtnis hinterlassen. Für mich selbst hatte ich eine Methode, die 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 werden. Der Einfachheit halber wird es einfach "c" genannt. Alle speziellen Wrapper-Funktionen akzeptieren diese Liste als den ersten erforderlichen Parameter. Wenn DisconnectModel verwendet wird, wird Code im gemeinsamen Vorfahren verwendet, um die Liste aller Steuerelemente durchzugehen und alle diese Elemente gnadenlos zu verschieben. Keine Schönheit und Anmut, aber billig, zuverlässig und relativ praktisch. Es ist möglich, ein bisschen mehr Sicherheit zu haben, wenn Sie anstelle eines Kontrollblatts einen IView anfordern und ihn allen diesen Stellen zuweisen müssen. Im Wesentlichen dasselbe vergessen Sie nicht, auf die gleiche Weise auszufüllen wird nicht funktionieren, aber schwieriger zu hacken. Ich habe Angst zu vergessen, aber ich habe keine Angst, dass jemand bewusst das System durchbricht, denn bei so klugen Leuten muss man mit einem Gürtel und anderen nicht-softwaremäßigen Methoden kämpfen, also beschränke ich mich auf c.

    Ein alternativer Ansatz kann von UniRX gelernt werden. Jeder Wrapper erstellt ein neues Objekt, das eine Verknüpfung zum vorherigen Objekt hat, auf das er sich hört. Am Ende wird die AddTo (Komponenten) -Methode aufgerufen, die die gesamte Kette von Steuerelementen einem gelöschten Objekt zuordnet. In unserem Beispiel sieht dieser Code folgendermaßen aus:

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

    Wenn dieser letzte Besitzer der Kette die Vernichtung beschließt, gibt er den Befehl: "Töte dich selbst, wenn alle außer dir nicht mehr auf mich hören". Und die ganze Kette hat gehorsam aufgeräumt. Es ist natürlich viel prägnanter, aber aus meiner Sicht gibt es einen wichtigen Mangel. AddTo kann versehentlich vergessen werden und niemand wird es je wissen, bis es zu spät ist.

    Tatsächlich können Sie den schmutzigen Hack von Unity verwenden und auf zusätzlichen Code in der Ansicht 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 der Link zur Komponente oder zum GameObject in Unity null. Aber Sie müssen verstehen, dass hakokostyl hier einen Update-Listener für jede zu zerstörende Kette von Steuerelementen 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 überprüfte Modell als auch das Datenmodell für die Benutzeroberfläche, und die Anwendung befindet sich in genau demselben Zustand, bis alle Schaltflächen aktiviert sind. Zwei Gründe stören dies. Die erste ist, dass einige variable Programmierer gerne im Controller des Formulars oder sogar in der Ansicht selbst speichern und argumentieren, dass ihr Lebenszyklus genau derselbe ist wie der des Formulars selbst. Die zweite ist, dass selbst wenn alle Daten für das Formular in seinem Modell sind, das Team das Formular selbst erstellt und ausfüllt und als expliziten Funktionsaufruf durchgeht, auch mit einigen zusätzlichen Parametern, zum Beispiel, auf welches Feld aus der Liste Sie sich konzentrieren müssen.

    Mit diesem können Sie nicht kämpfen, wenn Sie nicht wirklich die Bequemlichkeit des Debuggens wollen. Dies ist jedoch nicht der Fall. Wir möchten die Benutzeroberfläche genauso komfortabel debuggen wie die Hauptoperationen mit dem Modell. Dazu den nächsten Fokus. Im UI-Teil des Modells wird eine Variable festgelegt, z. B. .main, und innerhalb des Befehls wird das Modell des Formulars platziert, das Sie sehen möchten. Der Zustand dieser Variablen wird von einem speziellen Controller überwacht. Wenn ein Modell in Abhängigkeit von seinem Typ in dieser Variablen angezeigt wird, wird das gewünschte Formular instanziiert, dort platziert, wo es benötigt wird, und ConnectModel (Modell) wird aufgerufen. Wenn die Variable vom Modell befreit wird, entfernt der Controller das Formular aus der Zeichenfläche und gibt es ab. Daher wird keine Aktion ausgeführt, um das Modell zu umgehen, und alles, was Sie mit der Schnittstelle gemacht haben, ist im ExportChanges-Modell perfekt sichtbar. Und dann orientieren wir uns am Prinzip „Alles, was doppelt gewickelt wird“ und verwenden auf allen Ebenen der Schnittstelle exakt denselben Controller. Wenn im Formular ein Platz für ein anderes Formular vorhanden ist, wird ein UI-Modell erstellt, und im Modell des übergeordneten Formulars wird eine Variable erstellt. Genau das gleiche mit Listen.

    Ein Nebeneffekt dieses Ansatzes ist, dass zwei Dateien zu jedem Formular hinzugefügt werden, eine mit dem Datenmodell für dieses Formular und die andere, normalerweise ein einzelnes Element, das Verweise auf die Elemente der Benutzeroberfläche enthält. Nachdem das Modell in seiner ConnectModel-Funktion empfangen wurde, werden alle reaktiven Controller für alle erstellt Modellfelder und alle Elemente der Benutzeroberfläche. Nun, es ist noch kompakter zu handhaben, so dass es auch praktisch ist, damit zu arbeiten, wahrscheinlich ist es unmöglich. Wenn du kannst - schreibe in die Kommentare.

    Steuerelemente auflisten


    Eine typische Situation ist, wenn das Modell eine Liste einiger Elemente enthält. Da ich möchte, dass alles sehr praktisch und vorzugsweise in einer einzigen Zeile ausgeführt wird, wollte ich für die Listen etwas tun, das für die Verarbeitung geeignet wäre. Es ist nur eine Zeile möglich, aber es ist unangenehm lang. Es hat sich empirisch herausgestellt, dass fast alle Fälle von nur zwei Arten von Kontrollen abgedeckt werden. Die erste überwacht den Status einer Auflistung und ruft drei Lambda-Funktionen auf, die erste wird aufgerufen, wenn der Auflistung ein Element hinzugefügt wird, die zweite, wenn das Element die Auflistung verlässt, und die dritte wird aufgerufen, wenn die Elemente der Auflistung ihre Reihenfolge ändern. Der zweithäufigste Kontrolltyp verfolgt die Liste und ist die Quelle einer Unterliste davon - Seiten mit einer bestimmten Nummer. Das ist zum Beispiel Es verfolgt eine Liste von 102 Artikeln und enthält eine Liste mit 10 Elementen vom 20. bis 29. September. Und die Ereignisse werden genauso generiert, als wäre er selbst eine Liste.

    Nach dem Prinzip „Erstellen eines Wrappers für alles, was zweimal gemacht wurde“, erschien natürlich eine große Anzahl praktischer Wrapper, beispielsweise eine, die Factory nur für die Eingabe akzeptiert, eine Korrespondenz zwischen den Typen von Modellen und ihrer Ansicht herstellt und einen Link zu der Leinwand enthält Es ist notwendig, Elemente hinzuzufügen. Und viele andere ähnliche, nur etwa ein Dutzend Umhüllungen für typische Fälle.

    Komplexere Steuerungen


    Manchmal gibt es Situationen, die durch das Modell übertrieben dargestellt werden können, da sie offensichtlich sind. Hier können Steuerelemente zur Rettung gebracht werden, indem der Wert sowie einige Steuerelemente, die andere Steuerelemente überwachen, ausgeführt werden. Zum Beispiel eine typische Situation: Eine Aktion hat einen Preis und eine Schaltfläche ist nur aktiv, wenn auf dem Konto mehr Geld vorhanden ist als sein Preis.

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

    In der Tat ist die Situation so typisch, dass es nach meinem Prinzip einen fertigen Wrapper dafür gibt, aber hier habe ich den Inhalt gezeigt.

    Sie kauften ein Objekt, erstellten ein Objekt, das eines seiner Felder abonniert hatte und dessen Wert den Wert long hat. Es wurde ein weiteres Steuerelement hinzugefügt, dessen Typ zu lang war. Die Methode gab ein Steuerelement mit mehreren Werten zurück. Das Änderungsereignis Changed wird geändert, wenn sich einer von ihnen ändert. Anschließend erstellt Func ein Objekt für jede Änderung der Eingabe-Berechnungsfunktion und das Generierungsereignis Changed, wenn der Gesamtwert gezählt wird Funktionen wurden geändert.

    Der Compiler selbst erstellt erfolgreich den erforderlichen Kontrolltyp basierend auf den Eingabedatentypen und dem Typ 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 anzugeben. Der letzte Anruf hört schließlich auf das Bulenovsky-Steuerelement, je nachdem, mit welchem ​​Schalter die Taste ein- oder ausgeschaltet wird.

    Tatsächlich akzeptiert der eigentliche Wrapper des Projekts zwei Schaltflächen an der Eingabe, eine für den Fall, wenn Geld vorhanden ist, und eine, wenn nicht genügend Geld vorhanden ist. Außerdem wird mit der zweiten Schaltfläche der Befehl zum Öffnen des modalen Fensters "Zusätzliche Währungen kaufen" angezeigt. Und das alles in einer einfachen Zeile.

    Es ist leicht zu erkennen, dass Sie mit Join und Func beliebig komplexe Strukturen erstellen können. Ich hatte eine Funktion im Code, die eine komplexe Steuerung generiert und berechnet, wie viel ein Spieler Truppen kaufen kann, angesichts der Anzahl der Spieler auf seiner Seite. Die Regel besagt, dass jeder das Budget um 10% überschreiten kann, wenn alle das Gesamtbudget nicht überschreiten. Und dies ist ein Beispiel dafür, wie dies nicht getan werden kann, denn wie einfach und einfach es ist, so lange Debuggen, was in Modellen geschieht, als es schwierig ist, den Fehler in den reaktiven Steuerungen zu erkennen. Sie werden sich sogar viel Zeit nehmen, um zu verstehen, was dazu geführt hat.

    Das allgemeine Prinzip der Verwendung komplexer Steuerelemente lautet daher wie folgt: Beim Prototyping eines Formulars können Sie Konstruktionen für reaktive Steuerelemente verwenden, insbesondere wenn Sie nicht sicher sind, ob sie in Zukunft komplizierter werden, aber sobald Sie den Verdacht haben, dass es bricht, verstehen Sie nicht, was passiert ist. Sie sollten diese Manipulationen sofort in das Modell übertragen, und die Berechnungen, die zuvor in den Steuerelementen ausgeführt wurden, sollten in Erweiterungsmethoden in statischen Regelklassen abgelegt werden.

    Dies unterscheidet sich erheblich von dem von Perfektionisten so beliebten Prinzip, dass alles in Ordnung ist, weil wir in der Welt der Spieleentwickler leben und wenn man anfängt, eine Form zu verbrennen, kann man sich absolut nicht sicher sein, was in drei Tagen zu tun ist. Einer meiner Kollegen sagte: "Wenn ich jedes Mal fünf Kopeken bekomme, wenn Spieledesigner ihre Meinung ändern, wäre ich eine sehr reiche Person." In der Tat ist das nicht schlecht, aber auch das Gegenteil ist gut. Das Spiel sollte durch Versuch und Irrtum entwickelt werden, denn wenn Sie keinen dummen Klon machen, können Sie sich schließlich nicht vorstellen, was die Spieler wirklich brauchen.

    Eine Datenquelle für mehrere Ansichten


    In so vielen archetypischen Fällen müssen Sie gesondert darüber sprechen. Es kommt vor, dass dasselbe Elementmodell als Teil eines Schnittstellenmodells in verschiedenen Ansichten gezeichnet wird, abhängig davon, wo und in welchem ​​Kontext dies geschieht. Und wir verwenden das Prinzip - "ein Typ, eine Ansicht". Beispielsweise haben Sie eine Waffenkaufkarte, die die gleichen unkomplizierten Informationen enthält. In den verschiedenen Modi des Ladens sollte diese durch verschiedene Prefabs dargestellt werden. Die Lösung besteht aus zwei Teilen für zwei verschiedene Situationen.

    Die erste ist, wenn diese Ansicht in zwei verschiedene Ansichten platziert wird, beispielsweise ein Geschäft in Form einer Kurzliste und ein Geschäft mit großen Bildern. In diesem Fall helfen zwei separate, unterschiedlich konfigurierte Fabriken, die die Übereinstimmung von Typ-Fertigteilen herstellen. In der ConnectModel-Methode einer Ansicht verwenden Sie eine und in der anderen. Es ist ein völlig anderer Fall, wenn Sie Karten mit absolut identischen Informationen an einer Stelle etwas anders zeigen müssen. Manchmal erscheint in diesem Fall ein zusätzliches Feld für das Elementmodell, das auf das festliche Scheinwerferlicht dieses bestimmten Elements verweist, und manchmal erscheint nur ein Element im Elementmodell, das keine Felder enthält, und muss nur von einem anderen Prefab gezeichnet werden. Grundsätzlich widerspricht nichts.

    Es scheint eine naheliegende Lösung zu sein, aber ich habe genug Code von jemand anderem für merkwürdige Tänze mit einem Tamburin um diese Situation gesehen und fand es notwendig, darüber zu schreiben.

    Sonderfall: Kontrollen mit einer Menge Abhängigkeiten


    Es gibt einen ganz besonderen Fall, über den ich getrennt sprechen möchte. Dies sind Steuerelemente, die eine sehr große Anzahl von Elementen überwachen. Beispielsweise ein Steuerelement, das die Liste der Modelle überwacht und den Inhalt eines Felds zusammenfasst, das in jedem der Elemente liegt. Wenn die Liste zum Beispiel stark umgestürzt wird, z. B. durch Füllen der Daten, besteht die Gefahr, dass eine solche Steuerung so viele Änderungsereignisse wie in der Liste der Elemente plus eins erfasst. Natürlich zählt das Aggregieren so oft als schlechte Idee. Speziell für solche Fälle übernehmen wir die Kontrolle, die wir für das Ereignis onTransactionFinished abonnieren, das von GameState abhebt, und wie wir uns erinnern, gibt es in jedem Modell einen Link zu GameState. Bei jeder Änderung der Eingabedaten wird dieses Steuerelement einfach mit einem Etikett versehen, dass sich die Quelldaten geändert haben. und nur dann neu berechnet, wenn eine Nachricht über das Ende der Transaktion empfangen wird oder wenn festgestellt wird, dass die Transaktion zu dem Zeitpunkt bereits abgeschlossen ist, als sie die Nachricht vom Eingangsereignisstrom empfangen hat. Es ist klar, dass eine solche Steuerung nicht vor unnötigen Meldungen geschützt werden kann, wenn zwei solcher Steuerelemente in der Threadverarbeitungskette vorhanden sind. Der erste akkumuliert eine Änderungswolke, wartet bis zum Ende der Transaktion, startet den Änderungsfluss weiter, und es gibt einen weiteren, der bereits viele Änderungen mitbekommen hat, ein Ereignis über das Ende der Transaktion erhalten hat (es war ein Unglück, zuvor in der Liste der Funktionen, die das Ereignis abonniert haben), dann alles zu zählen Knall und ein anderes Änderungsereignis, und erzählen Sie alles ein zweites Mal. Also vielleicht, aber selten und vor allem

    UniRX-fähige Bibliothek


    Und es wäre möglich, alles auf das, was oben gesagt wurde, zu beschränken und in aller Ruhe Ihr Meisterstück zu schreiben, umso mehr im Vergleich zu den Modell- und Kontrollteams, es ist sehr einfach und sie haben alles in weniger als einer Woche geschrieben, wenn die Idee, dass Sie ein Fahrrad erfinden, nicht ausgetrickst wurde alles ist bereits durchdacht und geschrieben, bevor ich es kostenlos an alle verteilt habe.

    Nachdem uniRX aufgedeckt wurde, finden wir ein schönes und normenkonformes Design, das Streams von allem generell erstellen kann, sie schnell merzhit, vom Hauptthread zum Nicht-Hauptthread filtern kann, oder die Steuerung an den Hauptthread zurücksenden kann weiter. Wir haben dort nicht genau zwei Dinge: Simplicity und Convenience-Debugging. Haben Sie schon einmal versucht, einige mehrstöckige Konstruktionen von Linq anhand von Schritten im Debager zu debuggen? Also hier ist noch viel schlimmer. Gleichzeitig fehlt uns völlig etwas, wofür all diese hochentwickelten Maschinen geschaffen wurden. Der Einfachheit beim Debuggen und Reproduzieren von Zuständen fehlt es völlig an einer Vielzahl von Signalquellen, alles geschieht im Hauptstrom.

    Wenn Sie bereits wissen, wie man UniRX ausführt, mache ich es in der Regel für IObservable-Modelle, und Sie können die Trump-Funktionen Ihrer Lieblingsbibliothek verwenden. Für den Rest empfehle ich jedoch, nicht versuchen, Panzer aus Hochgeschwindigkeitsautos und Autos nur auf dieser Basis zu bauen dass sowohl der eine als auch der andere Räder hat.

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

    Nur registrierte Benutzer können an der Umfrage teilnehmen. Bitte melden Sie sich an.

    Können Sie die im Artikel beschriebenen reaktiven Steuerelemente schreiben?

    Verwenden Sie einen vollständig modellbasierten Ansatz zum Erstellen von Schnittstellen?

    Was ist der beste Weg, um Müll zu sammeln:

    Würden Sie komplexe Verbundsteuerungen verwenden?

    Kennen Sie die UniRX-Bibliothek?

    Würden Sie eine Engine wie meine verwenden, wenn Sie sie mit einem Assembler herunterladen könnten?

    Versuchen Sie in Ihrer Arbeit, einige der hier beschriebenen Ideen in den nächsten sechs Monaten anzuwenden:


    Jetzt auch beliebt: