
Im ersten Teil des Artikels haben wir untersucht, wie das Modell so angeordnet werden sollte, dass es einfach zu verwenden ist, aber das Debuggen und Verschrauben von Schnittstellen ist einfach. In diesem Teil werden wir die Rückgabe von Befehlen für Änderungen im Modell in all seiner Schönheit und Vielfalt betrachten. Nach wie vor liegt die Priorität für uns in der Bequemlichkeit des Debuggens, der Minimierung der Gesten, die ein Programmierer zum Erstellen einer neuen Funktion ausführen muss, sowie der Lesbarkeit des Codes für eine Person.
Architekturlösungen für ein Handyspiel. Teil 1: ModellArchitekturlösungen für ein Handyspiel. Teil 3: Blick auf den StrahlschubWarum befehlen
Das Befehlsmuster klingt laut, aber tatsächlich ist es nur ein Objekt, in dem alles, was für die angeforderte Operation erforderlich ist, hinzugefügt und dort gespeichert wird. Wir wählen diesen Ansatz, zumindest weil unsere Teams über das Netzwerk gesendet werden und sogar wir einige Kopien des Spielstatus zur offiziellen Verwendung erhalten. Wenn der Benutzer auf die Schaltfläche klickt, wird eine Instanz der Befehlsklasse erstellt und an den Empfänger gesendet. Die Bedeutung des Buchstabens C in der Abkürzung MVC ist etwas anders.
Vorhersage des Ergebnisses und Überprüfung der Befehle über das Netzwerk
In diesem Fall ist der spezifische Code weniger wichtig als die Idee. Und hier ist die Idee:
Ein Spiel mit Selbstachtung kann nicht auf eine Antwort vom Server warten, bevor es auf die Schaltfläche reagiert. Natürlich wird das Internet immer besser und Sie können eine Reihe von Servern auf der ganzen Welt haben, und ich kenne sogar ein paar erfolgreiche Spiele, die auf eine Antwort vom Server warten. Eines davon ist sogar Summoning Wars, aber das müssen Sie trotzdem nicht tun. Da für das mobile Internet Verzögerungen von 5 bis 15 Sekunden eher die Norm als eine Ausnahme sind, sollte zumindest in Moskau das Spiel wirklich großartig sein, damit die Spieler nicht darauf achten.
Dementsprechend haben wir einen Spielstatus, der alle für die Schnittstelle erforderlichen Informationen darstellt, und die Befehle werden sofort darauf angewendet und erst danach an den Server gesendet. Normalerweise sitzen fleißige Java-Programmierer auf dem Server und duplizieren alle neuen Funktionen einzeln in einer anderen Sprache. Bei unserem „Hirsch“ -Projekt erreichte ihre Anzahl 3 Personen, und Fehler beim Portieren waren eine ständige Quelle schwer fassbarer Freude. Stattdessen können wir es anders machen. Wir führen auf dem .NET-Server und auf der Serverseite denselben Befehlscode wie auf dem Client aus.
Das im letzten Artikel beschriebene Modell bietet uns eine neue interessante Möglichkeit zum Selbsttest. Nachdem Sie den Befehl auf dem Client ausgeführt haben, berechnen wir den Hash der Änderung, die im GameState-Baum aufgetreten ist, und wenden ihn auf das Team an. Wenn der Server denselben Befehlscode ausführt und der Hash der Änderungen nicht übereinstimmt, ist ein Fehler aufgetreten.
Erste Vorteile:
- Diese Lösung beschleunigt die Entwicklung erheblich und minimiert die Anzahl der Serverprogrammierer.
- Wenn der Programmierer beispielsweise Fehler gemacht hat, die zu nicht deterministischem Verhalten geführt haben, hat er den ersten Wert aus dem Wörterbuch abgerufen oder DateTime.now verwendet und im Allgemeinen einige Werte verwendet, die nicht explizit in die Befehlsfelder geschrieben wurden. Wenn sie auf dem Server ausgeführt werden, stimmt der Hash nicht überein wir werden es herausfinden.
- Die Client-Entwicklung kann vorerst überhaupt ohne Server durchgeführt werden. Sie können sogar in ein benutzerfreundliches Alpha wechseln, ohne einen Server zu haben. Dies ist nicht nur für Indie-Entwickler nützlich, die nachts ihr Traumspiel verpassen. Als ich in Piksonik war, gab es einen Fall, in dem der Serverprogrammierer alle Polymere verlor und unser Spiel einer Moderation unterzogen werden musste, da anstelle des Servers hin und wieder ein Dummy dumm den gesamten Spielstatus verteidigte.
Ein Nachteil, der aus irgendeinem Grund systematisch unterschätzt wird:
- Wenn der Client-Programmierer etwas falsch gemacht hat und es beim Testen unsichtbar ist, zum Beispiel die Wahrscheinlichkeit von Waren in den mysteriösen Kisten, gibt es niemanden, der dasselbe ein zweites Mal schreibt und einen Fehler findet. Autoportable Code erfordert eine viel verantwortungsvollere Haltung gegenüber Tests.
Detaillierte Debugging-Informationen
Eine unserer erklärten Prioritäten ist das bequeme Debuggen. Wenn wir während der Ausführung des Teams die Ausführung abgefangen haben - alles ist klar, wir setzen den Spielstatus zurück, senden den vollständigen Status an die Protokolle und serialisieren den Befehl, der ihn abgelegt hat, alles ist bequem und schön. Die Situation ist komplizierter, wenn wir eine Desynchronisierung mit dem Server haben. Weil der Client seitdem bereits mehrere andere Befehle ausgeführt hat und nicht nur herausgefunden werden muss, in welchem Zustand sich das Modell befand, bevor der Befehl ausgeführt wurde, der zur Katastrophe geführt hat, sondern ich möchte es wirklich. Das Klonen eines Gamestate vor jedem Team ist zu kompliziert und zu teuer. Um das Problem zu lösen, erschweren wir das unter der Motorhaube des Motors eingenähte Schema.
Im Client haben wir nicht einen Spielstatus, sondern zwei. Die erste dient als Hauptschnittstelle für das Rendern, die Befehle werden sofort darauf angewendet. Danach werden die angewendeten Befehle zum Senden an den Server in die Warteschlange gestellt. Der Server führt dieselbe Aktion auf seiner Seite aus und bestätigt, dass alles in Ordnung und korrekt ist. Nachdem der Client eine Bestätigung erhalten hat, nimmt er denselben Befehl entgegen und wendet ihn auf den zweiten Spielstatus an, wodurch er in den Zustand versetzt wird, der vom Server bereits als korrekt bestätigt wurde. Gleichzeitig haben wir auch die Möglichkeit, den Hash der aus Sicherheitsgründen vorgenommenen Änderungen zu vergleichen, und wir können auch den vollständigen Hash des gesamten Baums auf dem Client vergleichen, den wir berechnen können, nachdem der Befehl ausgeführt wurde. Er wiegt ein wenig und wird als schnell genug angesehen. Wenn der Server nicht sagt, dass alles in Ordnung ist, fragt er den Client nach Einzelheiten zu dem, was passiert ist, und der Client kann ihm einen serialisierten zweiten Gamestate genau so senden, wie er aussah, bevor der Befehl erfolgreich auf dem Client ausgeführt wurde.
Die Lösung sieht sehr attraktiv aus, schafft jedoch zwei Probleme, die auf Codeebene gelöst werden müssen:
- Unter den Befehlsparametern können nicht nur einfache Typen, sondern auch Links zu Modellen vorhanden sein. In einem anderen Spielzustand befinden sich genau an derselben Stelle andere Objekte des Modells. Wir lösen dieses Problem folgendermaßen: Bevor der Befehl auf dem Client ausgeführt wird, serialisieren wir alle seine Daten. Darunter befinden sich möglicherweise Links zu Modellen, die wir in Form eines Pfads zum Modell aus der Wurzel des Spielstatus schreiben werden. Wir tun dies vor dem Team, da sich die Pfade nach der Ausführung ändern können. Dann senden wir diesen Pfad an den Server, und der Server-Gamestate kann unterwegs einen Link zu seinem Modell erhalten. In ähnlicher Weise kann das Modell aus dem zweiten Spielzustand erhalten werden, wenn ein Team auf den zweiten Spielzustand angewendet wird.
- Zusätzlich zu elementaren Typen und Modellen kann ein Team Links zu Sammlungen haben. Wörterbuch <Schlüssel, Modell>, Wörterbuch <Modell, Schlüssel>, Liste <Modell>, Liste <Wert>. Für alle müssen sie Serialisierer schreiben. Zwar kann man sich nicht darauf stürzen, in einem realen Projekt entstehen solche Felder überraschend selten.
- Das Senden von Befehlen nacheinander an den Server ist keine gute Idee, da der Benutzer sie schneller erstellen kann als das Internet sie hin und her ziehen kann. In einem schlechten Internet wächst der Pool von Befehlen, die vom Server nicht ausgearbeitet wurden. Anstatt Befehle einzeln zu senden, senden wir sie in Stapeln von mehreren Teilen. In diesem Fall müssen Sie, nachdem Sie vom Server eine Antwort erhalten haben, dass ein Fehler aufgetreten ist, zunächst alle vorherigen Befehle aus demselben Paket, die vom Server bestätigt wurden, auf den zweiten Status anwenden und erst dann den zweiten Status des Steuerelements löschen und an den Server senden.
Komfort und einfache Schreibbefehle
Der Befehlsausführungscode ist der zweitgrößte und der am meisten verantwortliche Code im Spiel. Je einfacher und klarer es sein wird und je weniger der Programmierer das Extra mit seinen Händen tun muss, um es zu schreiben, desto schneller wird der Code geschrieben, desto weniger Fehler werden gemacht und, ganz unerwartet, desto glücklicher wird der Programmierer sein. Ich platziere den Ausführungscode direkt im Befehl selbst, zusätzlich zu den allgemeinen Teilen und Funktionen, die sich in separaten statischen Regelklassen befinden, meist in Form von Erweiterungen der Modellklassen, mit denen sie arbeiten. Ich zeige Ihnen einige Beispiele für Befehle aus meinem Lieblingsprojekt, eines sehr einfach und das andere etwas komplizierter:
namespace HexKingdoms { public class FCSetSideCostCommand : HexKingdomsCommand {
Und hier ist das Protokoll, das dieser Befehl hinter sich lässt, wenn dieses Protokoll dafür nicht deaktiviert ist.
[FCSetSideCostCommand id=1 match=FCMatchModel[0] newCost=260] Execute:00:00:00.0027546 Apply:00:00:00.0008689 { "LOCAL_PERSISTENTS":{ "@changed":{ "0":{"SIDE_COST":260}, "1":{"POSSIBLE_COST":260}, "2":{"POSSIBLE_COST":260}}}}
Das erste im Protokoll angegebene Mal ist die Zeit, in der alle erforderlichen Änderungen am Modell vorgenommen wurden, und das zweite Mal ist die Zeit, in der alle Änderungen von den Schnittstellencontrollern ausgearbeitet wurden. Dies sollte im Protokoll angezeigt werden, um nicht versehentlich etwas schrecklich Langsames zu tun oder um rechtzeitig zu bemerken, wenn die Vorgänge aufgrund der Größe des Modells selbst zu lange dauern.
Abgesehen von Aufrufen von persistenten Objekten auf Id-shniks, die die Lesbarkeit des Protokolls erheblich beeinträchtigen, was hier übrigens hätte vermieden werden können, sind der Befehlscode selbst und das Protokoll, das er mit dem Spielstatus erstellt hat, erstaunlich klar. Bitte beachten Sie, dass der Programmierer im Befehlstext keine einzige zusätzliche Bewegung ausführt. Alles, was Sie brauchen, erledigt der Motor unter der Motorhaube.
Schauen wir uns nun ein Beispiel eines größeren Teams an
namespace HexKingdoms { public class FCSetUnitForPlayerCommand : HexKingdomsCommand {
Und hier ist das Protokoll, das das Team hinterlassen hat:
[FCSetUnitForPlayerCommand id=3 screen=/UI_SCREENS[main] unit=militia count=1] Execute:00:00:00.0065625 Apply:00:00:00.0004573 { "LOCAL_PERSISTENTS":{ "@changed":{ "2":{ "UNITS":{ "@set":{"militia":1}}, "ASSIGNED":7}}}, "UI_SCREENS":{ "@changed":{ "main":{ "SELECTED_UNITS":{ "@set":{ "militia":{"@new":null, "TYPE":"militia", "REMARK":null, "COUNT":1, "SELECTED":false, "DISABLED":false, "HIGHLIGHT_GREEN":false, "HIGHLIGHT_RED":false, "BUTTON_ENABLED":false}}}}}}}
Wie sie sagen, ist es viel klarer. Nehmen Sie sich Zeit, um das Team mit einem praktischen, kompakten und informativen Protokoll auszustatten. Dies ist der Schlüssel zu Ihrem Glück. Das Modell muss sehr schnell funktionieren, daher haben wir dort verschiedene Tricks mit Methoden zur Speicherung und zum Zugriff auf die Felder angewendet. Befehle werden im schlimmsten Fall einmal pro Frame ausgeführt, und zwar mehrmals seltener, sodass wir die Serialisierung und Deserialisierung der Befehlsfelder ohne Phantasie durchführen, nur durch Reflexion. Wir sortieren die Felder nur nach Namen, damit die Reihenfolge festgelegt ist. Nun, wir werden die Liste der Felder einmal während der Laufzeit des Befehls zusammenstellen und mit den nativen Methoden C # lesen und schreiben.
Informationsmodell für die Schnittstelle.
Machen wir den nächsten Schritt, um unsere Engine zu komplizieren. Dieser Schritt sieht beängstigend aus, vereinfacht jedoch das Schreiben und Debuggen von Schnittstellen erheblich. Insbesondere im zugehörigen MVP-Muster enthält das Modell häufig nur eine servergesteuerte Geschäftslogik, und Informationen zum Status der Schnittstelle werden im Präsentator gespeichert. Zum Beispiel möchten Sie fünf Tickets bestellen. Sie haben ihre Nummer bereits ausgewählt, aber noch nicht auf die Schaltfläche "Bestellen" geklickt. Informationen darüber, wie viele Tickets Sie genau im Formular ausgewählt haben, können irgendwo in den geheimen Ecken der Klasse gespeichert werden, die als Dichtung zwischen dem Modell und seiner Anzeige dienen. Zum Beispiel wechselt ein Spieler von einem Bildschirm zum anderen, aber nichts ändert sich am Modell, und wo er sich befand, als die Tragödie passierte, weiß der am Debuggen beteiligte Programmierer nur aus den Worten eines äußerst disziplinierten Testers. Der Ansatz ist einfach, verständlich, fast immer verwendet und meiner Meinung nach ein wenig bösartig. Denn wenn etwas schief gelaufen ist, ist der Zustand dieses Präsentators, der zu einem Fehler geführt hat, absolut unmöglich herauszufinden. Vor allem, wenn der Fehler auf dem Battle Server während des Vorgangs für 1000 US-Dollar aufgetreten ist und nicht beim Tester in einer kontrollierten und reproduzierbaren Umgebung.
Anstelle dieses üblichen Ansatzes verbieten wir jedem außer dem Modell, Informationen über den Status der Schnittstelle zu enthalten. Dies hat wie üblich Vor- und Nachteile, die bekämpft werden müssen.
- (+1) Der wichtigste Vorteil, der monatelange Programmierarbeit spart - wenn etwas schief geht, lädt der Programmierer einfach den Spielstatus vor dem Unfall und erhält nicht nur den gleichen Status des Geschäftsmodells, sondern der gesamten Benutzeroberfläche bis zur letzten Schaltfläche auf dem Bildschirm.
- (+2) Wenn ein Team etwas an der Benutzeroberfläche geändert hat, kann der Programmierer einfach in das Protokoll gehen und sehen, was sich genau in einer praktischen json-Form geändert hat, wie im vorherigen Abschnitt.
- (-1) Im Modell werden viele redundante Informationen angezeigt, die zum Verständnis der Geschäftslogik des Spiels nicht benötigt werden und vom Server nicht zweimal benötigt werden.
Um dieses Problem zu lösen, markieren wir einige Felder als notServerVerified. Es sieht beispielsweise so aus:
public EDictionary<string, UIStateModel> uiScreens { get { return UI_SCREENS.Get(this); } } public static PDictionaryModel<string, UIStateModel> UI_SCREENS = new PDictionaryModel<string, UIStateModel>() { notServerVerified = true };
Dieser Teil des Modells und alles darunter bezieht sich ausschließlich auf den Kunden.
Wenn Sie sich noch erinnern, die Flaggen dessen, was Sie exportieren müssen und was nicht so aussieht:
[Flags] public enum ExportMode { all = 0x0, changes = 0x1, serverVerified = 0x2 }
Dementsprechend können Sie beim Exportieren oder Berechnen eines Hashs angeben, ob der gesamte Baum oder nur der Teil davon exportiert werden soll, der vom Server überprüft wird.
Die erste offensichtliche Komplikation, die sich hier ergibt, ist die Notwendigkeit, separate Befehle zu erstellen, die vom Server überprüft werden müssen, und solche, die nicht benötigt werden, aber es gibt auch solche, die nicht vollständig überprüft werden müssen. Um den Programmierer nicht mit unnötigen Operationen zum Einrichten des Befehls zu beladen, werden wir erneut versuchen, alles Notwendige mit der Motorhaube zu tun.
public partial class Command { public virtual void Apply(ModelRoot root) {} public virtual void ApplyClientSide(ModelRoot root) {} }
Der Programmierer, der den Befehl erstellt, kann eine oder beide dieser Funktionen überschreiben. Das alles ist natürlich wunderbar, aber wie kann ich sicherstellen, dass der Programmierer nichts durcheinander gebracht hat, und wenn er etwas durcheinander gebracht hat - wie kann er ihm helfen, es schnell und einfach zu beheben? Es gibt zwei Möglichkeiten. Ich habe das erste angewendet, aber das zweite gefällt Ihnen vielleicht besser.
Erster Weg
Wir nutzen die coolen Funktionen unseres Modells:
- Die Engine ruft die erste Funktion auf, nach der sie einen Hash von Änderungen im vom Server überprüften Teil des Spielstatus erhält. Wenn sich nichts ändert, haben wir es ausschließlich mit dem Kundenteam zu tun.
- Wir erhalten den Modell-Hash der Änderungen im gesamten Modell, nicht nur im vom Server verifizierten. Wenn es sich vom vorherigen Hash unterscheidet, hat der Programmierer etwas in dem Teil des Modells durcheinander gebracht und geändert, der nicht vom Server überprüft wurde. Wir gehen um den Statusbaum herum und geben dem Programmierer als Ausführung eine vollständige Liste der Felder notServerVerified = true und der Felder unter dem Baum aus, die er geändert hat.
- Wir nennen die zweite Funktion. Wir erhalten vom Modell einen Hash der Änderungen, die im geprüften Teil aufgetreten sind. Wenn es nach dem ersten Aufruf nicht mit dem Hash übereinstimmt, hat der Programmierer in der zweiten Funktion etwas getan. Wenn wir in diesem Fall ein sehr informatives Protokoll erhalten möchten, setzen wir das gesamte Modell auf den ursprünglichen Zustand zurück, serialisieren es in eine Datei, dann ist der Programmierer zum Debuggen nützlich, klonen es dann vollständig (zwei Zeilen - Serialisierung-Deserialisierung) und wenden jetzt zuerst das erste an Funktion, dann übernehmen wir die Änderungen, so dass das Modell unverändert aussieht, wonach wir die zweite Funktion anwenden. Und dann exportieren wir alle Änderungen im servergeprüften Teil in Form von JSON und beziehen sie in die missbräuchliche Ausführung ein, damit der beschämte Programmierer sofort sehen kann, was und wo er geändert hat, was nicht geändert werden sollte.
Es sieht natürlich beängstigend aus, aber tatsächlich sind es 7 Zeilen, denn die Funktionen, die dies tun, sind alle (außer das Durchqueren des Baums aus dem zweiten Absatz), wir sind bereit. Und da dies Rezeption ist, können wir uns erlauben, nicht optimal zu handeln.
Zweiter Weg
Etwas brutaler, jetzt haben wir in ModelRoot ein Sperrfeld, aber wir können es in zwei Teile teilen. Eines sperrt nur die markierten Felder für den Server, das andere nur die markierten Felder. In diesem Fall erhält der Programmierer, der etwas falsch gemacht hat, sofort eine Erklärung mit einer Bindung an den Ort, an dem er es getan hat. Der einzige Nachteil dieses Ansatzes besteht darin, dass, wenn in unserem Baum eine Modelleigenschaft als nicht überprüfbar markiert ist, alles im Baum darunter in Bezug auf die Berechnung von Hashes und die Änderungskontrolle nicht überprüft wird, selbst wenn nicht jedes Feld markiert wurde. Eine Sperre untersucht natürlich nicht die Hierarchie, was bedeutet, dass alle Felder des nicht aktivierten Teils des Baums markiert werden müssen, und es funktioniert an einigen Stellen nicht, dieselben Klassen in der Benutzeroberfläche und im üblichen Teil des Baums zu verwenden. Optional ist eine solche Konstruktion möglich (ich werde sie vereinfacht schreiben):
public class GameState : Model { public RootModelData data; public RootModelLocal local; } public class RootModel { public bool locked { get; } }
Dann stellt sich heraus, dass jeder Teilbaum eine eigene Sperre hat. GameState erbt Modelle, da es einfacher ist, eine separate Implementierung mit derselben Funktionalität zu erstellen.
Notwendige Verbesserungen
Natürlich muss der Manager, der für die Verarbeitung der Teams verantwortlich ist, neue Funktionen hinzufügen. Das Wesentliche an den Änderungen ist, dass nicht alle Befehle an den Server gesendet werden, sondern nur diejenigen, die die überprüften Änderungen erstellen. Der Server auf seiner Seite erhöht nicht den gesamten Spielstatusbaum, sondern nur den Teil, der geprüft wird, und dementsprechend stimmt der Hash nur für den Teil überein, der geprüft wird. Wenn ein Befehl auf dem Server ausgeführt wird, wird nur die erste der beiden Funktionen des Befehls gestartet. Wenn beim Auflösen von Verweisen auf Modelle im Gamestate der Pfad zu einem nicht überprüfbaren Teil des Baums führt, wird anstelle des Modells null in die Befehlsvariable eingefügt. Alle nicht sendenden Teams werden ehrlich mit den üblichen übereinstimmen, gelten jedoch als bereits bestätigt. Sobald sie die Linie erreichen und keine unbestätigten vor ihnen liegen, werden sie sofort auf den zweiten Zustand angewendet.
Die Implementierung ist nicht grundlegend kompliziert. Es ist nur so, dass die Eigenschaft jedes Felds des Modells eine weitere Bedingung hat, eine Baumdurchquerung.
Eine weitere notwendige Verfeinerung: Sie benötigen eine separate Factory für ParsistentModel in den geprüften und nicht geprüften Teilen des Baums, und NextFreeId für diese Teile ist unterschiedlich.
Vom Server initiierte Befehle
Es gibt ein Problem, wenn der Server seinen Befehl an den Client senden möchte, da der Client-Status relativ zum Server bereits einige Schritte vorwärts springen könnte. Die Hauptidee ist, dass der Server, wenn er seinen Befehl senden musste, die Serverbenachrichtigung mit der nächsten Antwort an den Client sendet und diese in das Feld für Benachrichtigungen schreibt, die an diesen Client gesendet werden. Der Client erhält eine Benachrichtigung, bildet auf seiner Basis einen Befehl und stellt ihn an das Ende seiner Warteschlange, nachdem diejenigen, die auf dem Client abgeschlossen wurden, aber den Server noch nicht erreicht haben. Nach einiger Zeit wird der Befehl im Rahmen der normalen Arbeit mit dem Modell an den Server gesendet. Nachdem der Server diesen Befehl zur Verarbeitung erhalten hat, wirft er die Benachrichtigung aus der ausgehenden Warteschlange. Wenn der Client nicht innerhalb der festgelegten Zeit mit dem nächsten Paket auf die Benachrichtigung geantwortet hat, wird ein Neustartbefehl an ihn gesendet. Wenn der Client, der die Benachrichtigung erhalten hat, abgefallen ist, später eine Verbindung herstellt oder aus irgendeinem Grund das Spiel lädt, wandelt der Server alle Benachrichtigungen in Befehle um, bevor er den Status erhält, führt sie auf seiner Seite aus und gibt dem beitretenden Client erst danach seinen neuen Status. Bitte beachten Sie, dass ein Spieler möglicherweise einen Konflikt mit negativen Ressourcen hat, wenn es dem Spieler gelungen ist, das Geld genau zu dem Zeitpunkt auszugeben, als der Server sie ihm weggenommen hat. Ein Zufall ist unwahrscheinlich, aber bei einer großen DAU fast unvermeidlich. Daher sollten die Benutzeroberfläche und die Spielregeln in einer solchen Situation nicht zu Tode fallen.
Auszuführende Befehle, die Sie benötigen, um die Serverantwort zu kennen
Ein typischer Fehler ist zu glauben, dass eine Zufallszahl nur vom Server abgerufen werden kann. Nichts hindert Sie daran, dass derselbe Pseudozufallszahlengenerator gleichzeitig vom Client und vom Server ausgehend von einer gemeinsamen Seite ausgeführt wird. Darüber hinaus kann der aktuelle Startwert direkt im Gamestate gespeichert werden. Einige finden es möglicherweise schwierig, die Reaktion dieses Generators zu synchronisieren. In der Tat reicht es aus, eine weitere Nummer im selben Artikel zu haben - genau wie viele Nummern vom Generator bis zu diesem Moment empfangen wurden. Wenn Ihr Generator aus irgendeinem Grund nicht konvergiert, liegt irgendwo ein Fehler vor und der Code funktioniert nicht deterministisch. Und diese Tatsache sollte nicht unter dem Teppich versteckt werden, sondern aussortiert und nach einem Fehler gesucht werden. Für die überwiegende Mehrheit der Fälle, einschließlich der mysteriösen Kisten, reicht dieser Ansatz aus.
Es gibt jedoch Zeiten, in denen diese Option nicht geeignet ist. Zum Beispiel spielen Sie einen sehr teuren Preis und möchten nicht, dass der listige Kamerad das Spiel dekompiliert. Schreiben Sie einen Bot, der Ihnen im Voraus sagt, was aus der Diamantbox fällt, wenn Sie sie gerade öffnen, und was, wenn Sie die Trommel vorher an einer anderen Stelle drehen. Sie können Samen für jede Zufallsvariable separat speichern. Dies schützt vor frontalem Hacking, hilft jedoch in keiner Weise vor einem Bot, der Ihnen sagt, wie viele Kisten das benötigte Produkt derzeit enthält. Nun, der offensichtlichste Fall ist, dass Sie in der Client-Konfiguration möglicherweise nicht mit Informationen über die Wahrscheinlichkeit eines seltenen Ereignisses glänzen möchten. Kurz gesagt, manchmal muss auf eine Serverantwort gewartet werden.
Solche Situationen sollten nicht durch die zusätzlichen Funktionen der Engine gelöst werden, sondern indem das Team in zwei Teile geteilt wird - der erste bereitet die Situation vor und versetzt die Schnittstelle in einen Wartezustand für Benachrichtigungen, der zweite tatsächlich für Benachrichtigungen mit der Antwort, die Sie benötigen. Selbst wenn Sie die Schnittstelle zwischen ihnen auf dem Client fest blockieren, kann ein anderer Befehl durchgehen - beispielsweise wird eine Energieeinheit rechtzeitig wiederhergestellt.
Es ist wichtig zu verstehen, dass solche Situationen nicht die Regel, sondern die Ausnahme sind. Tatsächlich muss in den meisten Spielen nur ein Team auf eine Antwort warten - GetInitialGameState. Ein weiteres Paket solcher Befehle ist die Interaktion zwischen Spielern in einem Metaspiel, beispielsweise GetLeaderboard. Alle anderen zweihundert Stücke sind deterministisch.
Serverdatenspeicherung und das schlammige Thema der Serveroptimierung
Ich gebe sofort zu, dass ich ein Kunde bin, und manchmal habe ich solche Ideen und Algorithmen von bekannten Server-Servanten gehört, dass sie sich nicht einmal in meinen Kopf eingeschlichen haben. Durch die Kommunikation mit meinen Kollegen entwickelte ich irgendwie ein Bild davon, wie meine Architektur im Idealfall auf der Serverseite funktionieren sollte. Allerdings: Es gibt Kontraindikationen, es ist notwendig, einen spezialisierten Server zu konsultieren.
Zunächst zur Datenspeicherung. Auf Ihrer Serverseite gelten möglicherweise zusätzliche Einschränkungen. Beispielsweise kann Ihnen die Verwendung statischer Felder untersagt werden. Außerdem ist der Code von Befehlen und Modellen automatisch portierbar, aber der Eigenschaftscode auf dem Client und auf dem Server muss überhaupt nicht übereinstimmen. Dort kann alles versteckt werden, bis hin zur verzögerten Initialisierung von Feldwerten aus dem Memcache. Eigenschaftsfelder können auch zusätzliche Parameter empfangen, die vom Server verwendet werden, die Arbeit des Clients jedoch nicht beeinträchtigen.
Der erste Hauptunterschied des Servers: Hier werden die Felder serialisiert und deserialisiert. Eine vernünftige Lösung besteht darin, dass der größte Teil des Statusbaums in ein großes Binär- oder JSON-Feld serialisiert wird. Gleichzeitig werden einige Felder aus Tabellen entnommen. Dies ist erforderlich, da die Werte einiger Felder ständig erforderlich sind, damit die Interaktionsdienste zwischen den Spielern funktionieren. Zum Beispiel zucken das Symbol und die Ebene ständig von einer Vielzahl von Personen. Sie werden am besten in einer regulären Datenbank gespeichert. Ein vollständiger oder teilweiser, aber detaillierter Zustand einer Person wird von einer anderen Person als ihr sehr selten benötigt, wenn sich jemand entscheidet, in sein Gebiet zu schauen.
Darüber hinaus ist es unpraktisch, Felder einzeln von der Basis zu ziehen, und es kann sich als langer Widerstand herausstellen. Eine sehr ungewöhnliche Lösung, die nur für unsere Architektur verfügbar ist, kann darin bestehen, dass der Client beim Ausführen eines Befehls Informationen zu allen Feldern sammelt, die separat in Tabellen gespeichert sind, deren Getter es geschafft haben, diese zu berühren, und diese Informationen dem Befehl hinzufügt, damit der Server diese Gruppe von Feldern auslösen kann eine Anfrage an die Datenbank. Natürlich mit vernünftigen Einschränkungen, um nicht um DDOS zu betteln, das von Programmierern mit gebogenen Händen verursacht wurde, die alles unaufmerksam berührten.
Bei einem solchen separaten Speicher sollten die Transaktionsmechanismen berücksichtigt werden, wenn ein Spieler in die Daten eines anderen kriecht und ihm beispielsweise Geld stiehlt. Im Allgemeinen tun wir dies jedoch durch Benachrichtigung. Das heißt, der Dieb erhält sein Geld sofort und die ausgeraubte Person erhält eine Benachrichtigung mit Anweisungen, um Geld abzuschreiben, wenn es darum geht.
Wie Teams zwischen Servern aufgeteilt werden
Nun der zweite wichtige Moment für den Server. Es gibt zwei Ansätze. Zum Verarbeiten einer Anforderung (oder eines Pakets von Anforderungen) wird zunächst der gesamte Status von der Datenbank oder dem Cache in den Speicher verschoben, verarbeitet und dann an die Datenbank zurückgegeben. Operationen werden atomar auf einer Reihe von verschiedenen ausführenden Servern ausgeführt, und sie haben nur eine gemeinsame Basis, und selbst dann nicht immer. Als Kunde ist es schockierend, den gesamten Status für jedes Team zu erhöhen, aber ich habe gesehen, wie es funktioniert, und es funktioniert sehr zuverlässig und skalierbar. Die zweite Möglichkeit besteht darin, dass der Status einmal im Speicher ansteigt und dort lebt, bis der Client nur gelegentlich abfällt und seinen aktuellen Status zur Datenbank hinzufügt. . - . , . , , . 10 . , , — . , . — .
, : , . . . , . , . — — .
, , - . , . , VR CS, - . , , , 30%.
, — , . , . , , , , , .
, , - , : . , . , 35 . , , . , , , — .
: — 30 . ? №1: . №2: , 3000 .
, — . Irgendwie so:
public interface Command { void Apply(ModelRoot root, long time); }
, , Unity — . UnixTime , , PTime, PValue<long> , JSON : - . . .
: , , , , . , . , . PTimeOut, . , :
public class MyModel : Model { public static PTimeOut RESTORE_ENERGY = new PTimeOut() {command = (model, property) => new RestoreEnergyCommand() { model = model}} public long restoreEnergy { get { return RESTORE_ENERGY.Get(this); } set { RESTORE_ENERGY.Set(this, value); }} }
, . , , . , , , , . , , .
- , . , , , , , . , currentTime, :
public partial class Model { public void SetCurrentTime(long time); } vs public partial class RootModel { public event Action<long> setCurrentTime; }
, , , , . , , , - GC.
1, ,
. , - . , , . , , , , callback, . , . , , , , « » , , . , , .
, . , inventory, . , , -, , . , « » , , , . « ». , . , , . :
public class OpenMisterBox : Command { public BoxItemModel item; public int slot;
Was haben wir am Ende? View, , , . . GameState , , . , , , .
Insgesamt
- , , , , , , . . , , . , , .
. , , — . .