Es ist naiv. Super: Code und Architektur eines einfachen Spiels

Wir leben in einer komplexen Welt und haben anscheinend begonnen, einfache Dinge zu vergessen. Zum Beispiel über Occams Rasiermesser, dessen Prinzip lautet: "Was auf der Grundlage einer kleineren Anzahl getan werden kann, sollte nicht auf der Grundlage einer größeren Zahl getan werden." In diesem Artikel werde ich über einfache und nicht die zuverlässigsten Lösungen sprechen, die bei der Entwicklung einfacher Spiele verwendet werden können.



Bis zum Herbst DotNext in Moskau haben wir beschlossen, ein Spiel zu entwickeln. Es war eine IT-Variante der beliebten Alchemie. Die Spieler mussten 128 Konzepte in Bezug auf IT, Pizza und Dodo aus den verfügbaren 4 Elementen sammeln. Und wir mussten dies in etwas mehr als einem Monat von einer Idee zu einem funktionierenden Spiel realisieren.

In einem früheren Artikel habe ich über die Designkomponente der Arbeit geschrieben: Planung, Fakapy und Emotionen. Und dieser Artikel handelt vom technischen Teil. Es wird sogar Code geben!

Haftungsausschluss: Die Ansätze, der Code und die Architektur, über die ich unten schreibe, sind nicht kompliziert, originell oder zuverlässig. Im Gegenteil, sie sind sehr einfach, manchmal naiv und nicht für schwere Lasten ausgelegt. Wenn Sie jedoch noch nie ein Spiel oder eine Anwendung erstellt haben, die Logik auf dem Server verwendet, kann dieser Artikel als Startimpuls dienen.

Kundencode


Kurz gesagt zur Architektur des Projekts: Wir hatten mobile Clients für Android und iOS unter Unity und einen Back-End-Server unter ASP.NET mit CosmosDB als Speicher.

Ein Client in Unity repräsentiert nur die Interaktion mit der Benutzeroberfläche. Der Spieler klickt auf die Elemente, sie bewegen sich fest auf dem Bildschirm. Wenn ein neues Element erstellt wird, wird ein Fenster mit seiner Beschreibung angezeigt.


Der Spielprozess

Dieser Prozess kann durch eine ziemlich einfache Zustandsmaschine beschrieben werden. Die Hauptsache in dieser Zustandsmaschine ist, auf die Animation des Übergangs zum nächsten Zustand zu warten und die Benutzeroberfläche für den Player zu blockieren.



Ich habe die sehr coole UnitRx- Bibliothek verwendet , um Unity-Code in einem vollständig asynchronen Stil zu schreiben. Zuerst habe ich versucht, meine native Aufgabe zu verwenden, aber sie verhielten sich bei Builds für iOS instabil. Aber UniRx.Async funktionierte wie eine Uhr.

Jede Aktion, für die eine Animation erforderlich ist, wird über die AnimationRunner-Klasse aufgerufen:

public static class AnimationRunner
   {
       private const int MinimumIntervalMs = 20;
     public static async UniTask Run(Action<float> action, float durationInSeconds)
       {
           float t = 0;
           var delta = MinimumIntervalMs / durationInSeconds / 1000;
           while (t <= 1)
           {
               action(t);
               t += delta;
               await UniTask.Delay(TimeSpan.FromMilliseconds(MinimumIntervalMs));
           }
       }
   }

Dies ersetzt tatsächlich die klassische Coroutine durch UnitTask. Darüber hinaus wird jeder Aufruf, der die Benutzeroberfläche blockieren soll, über eine HandleUiOperationglobale Klassenmethode aufgerufen GameManager:

public async UniTask HandleUiOperation(UniTask uiOperation)
       {
           _inputLocked = true;
           await uiOperation;
           _inputLocked = false;
       }

Dementsprechend wird in allen Steuerelementen zuerst der Wert von InputLocked überprüft, und nur wenn er falsch ist, reagiert das Steuerelement.

Dies machte es einfach genug, die oben dargestellte Zustandsmaschine, einschließlich Netzwerkanrufen und E / A, unter Verwendung des Async / Wait-Ansatzes mit verschachtelten Aufrufen zu implementieren, wie bei einer russischen Puppe.

Das zweite wichtige Merkmal des Kunden war, dass alle Anbieter, die Daten zu den Elementen erhielten, in Form von Schnittstellen erstellt wurden. Nach der Konferenz, als wir unser Backend ausschalteten, war es buchstäblich an einem Abend möglich, den Client-Code neu zu schreiben, so dass das Spiel vollständig offline wurde. Diese Version kann jetzt von Google Play heruntergeladen werden .

Client-Back-Interaktion


Lassen Sie uns nun darüber sprechen, welche Entscheidungen wir bei der Entwicklung der Client-Server-Architektur getroffen haben.



Bilder wurden auf dem Client gespeichert, und der Server war für die gesamte Logik verantwortlich. Beim Start las der Server die CSV-Datei mit der ID und den Beschreibungen aller Elemente und speicherte sie in seinem Speicher. Danach war er bereit zu gehen.

API-Methoden waren ein notwendiges Minimum - nur fünf. Sie haben die gesamte Logik des Spiels implementiert. Alles ist ganz einfach, aber ich erzähle Ihnen einige interessante Punkte.

Authentifizierungs- und Starterelemente


Wir haben jedes komplizierte Authentifizierungssystem und im Allgemeinen alle Passwörter aufgegeben. Wenn ein Spieler auf dem Startbildschirm einen Namen eingibt und die Schaltfläche "Start" drückt, wird im Client ein eindeutiger zufälliger Token (ID) erstellt. Es ist jedoch nicht an das Gerät angeschlossen. Der Name des Spielers wird zusammen mit dem Token an den Server gesendet. Alle anderen Anforderungen vom Client an die Rückseite enthalten dieses Token.



Die offensichtlichen Nachteile dieser Lösung sind:

  1. Wenn der Benutzer die Anwendung zerstört und neu installiert, wird er als neuer Spieler betrachtet und sein gesamter Fortschritt geht verloren.
  2. Sie können das Spiel nicht auf einem anderen Gerät fortsetzen.

Dies waren absichtliche Annahmen, da wir verstanden haben, dass die Leute nur mit nur einem Gerät auf der Konferenz spielen würden. Und sie haben keine Zeit, zu einem anderen Gerät zu wechseln.

Daher hat der Client im geplanten Szenario die Servermethode AddNewUsernur einmal aufgerufen .

Beim Laden des Spielbildschirms GetBaseElementswurde die Methode auch einmal aufgerufen , wobei id, der Name des Sprites und die Beschreibung für die vier Grundelemente zurückgegeben wurden. Der Client fand die erforderlichen Sprites in seinen Ressourcen, erstellte die Objekte der Elemente, schrieb sie lokal in sich selbst und malte sie auf den Bildschirm.

Bei wiederholten Starts registrierte sich der Client nicht mehr auf dem Server und forderte keine Startelemente an, sondern nahm sie aus dem lokalen Speicher. Infolgedessen wurde der Spielbildschirm sofort geöffnet.

Elemente zusammenführen


Wenn ein Spieler versucht, zwei Elemente zu verbinden, wird eine Methode aufgerufen MergeElements, die entweder Informationen über das neue Element zurückgibt oder meldet, dass diese beiden Elemente nicht gesammelt werden. Wenn der Spieler ein neues Element gesammelt hat, werden Informationen dazu in der Datenbank aufgezeichnet.



Wir haben die offensichtliche Lösung angewendet: Um die Belastung des Servers zu verringern, wird das Ergebnis, nachdem der Player versucht hat, zwei Elemente hinzuzufügen, auf dem Client zwischengespeichert (im Speicher und in CSV). Wenn ein Spieler versucht, Gegenstände neu zu stapeln, wird zuerst der Cache überprüft. Und nur wenn das Ergebnis nicht vorhanden ist, wird die Anfrage an den Server gesendet.

Somit wissen wir, dass jeder Spieler eine begrenzte Anzahl von Interaktionen mit dem Rücken durchführen kann und die Anzahl der Einträge in der Datenbank die Anzahl der verfügbaren Elemente (von denen wir 128 hatten) nicht überschreitet. Wir konnten die Probleme vieler anderer Konferenzanwendungen vermeiden, die nach einem großen und gleichzeitigen Zustrom von Teilnehmern häufig gesichert wurden.

Highscore-Tabelle


Die Teilnehmer spielten unsere "Alchemie" nicht nur so, sondern um der Preise willen. Deshalb brauchten wir eine Tabelle mit Aufzeichnungen, die wir auf dem Bildschirm an unserem Stand und auch in einem separaten Fenster in unserem Spiel anzeigten.



Um eine Tabelle mit Aufzeichnungen zu erstellen, werden die letzten beiden Methoden verwendet, GetCurrentLadderund GetUser.es gibt auch eine merkwürdige Nuance. Wenn ein Spieler unter den Top 20 Ergebnissen ist, wird sein Name in der Tabelle hervorgehoben. Das Problem war, dass die Informationen dieser beiden Methoden nicht direkt miteinander verbunden waren.

Die Methode GetCurrentLaddergreift auf die Sammlung zu Stats, erhält 20 Ergebnisse und erledigt dies schnell. Die Methode GetUsergreift auf die Sammlung zu.Usersvon UserId und macht es zu schnell. Die Zusammenführung der Ergebnisse erfolgt bereits auf der Client-Seite. Das ist nur so, dass wir UserId nicht in den Ergebnissen glänzen wollten, also sind sie nicht da. Der Vergleich erfolgte nach dem Namen des Spielers und der Anzahl der erzielten Punkte. Bei Tausenden von Spielern wären Kollisionen unvermeidlich. Wir haben jedoch damit gerechnet, dass es unter allen Spielern wahrscheinlich keine Spieler mit denselben Namen und Punkten gibt. In unserem Fall ist dieser Ansatz völlig gerechtfertigt.

Spiel ist aus


Unsere ursprüngliche Aufgabe war es, ein Spiel für eine zweitägige Konferenz in einem Monat zusammenzustellen. Alle diese Entscheidungen, die wir in Architektur und Code verkörpert haben, haben sich voll und ganz gerechtfertigt. Der Server hat sich nicht hingelegt, alle Anfragen wurden korrekt verarbeitet und die Spieler haben sich nicht über Fehler in der Anwendung beschwert.

Bis zum nächsten Moskauer DotNext werden wir höchstwahrscheinlich ein weiteres Spiel aufrühren, da es mittlerweile zu unserer guten Tradition geworden ist ( CMAN-2018 , IT-Alchemy-2019 ). Schreiben Sie in die Kommentare, für welchen Zeitkiller Sie bereit sind, Hardcore-Berichte von Entwicklungsstars auszutauschen. :) :)
Aus dem gleichen naiven und interessierten Grund haben wir den Client-Code der IT-Alchemie öffentlich zugänglich gemacht .

Und schauen Sie in den Telegrammkanal , in dem ich alles über Entwicklung, Leben, Mathematik und Philosophie schreibe.

All Articles