Schreiben einer Multiplayer-Text-Adventure-Engine in Node.js: Game-Engine-Serverdesign (Teil 2)
Veröffentlicht: 2022-03-10Nach sorgfältiger Überlegung und tatsächlicher Implementierung des Moduls mussten einige der Definitionen, die ich während der Designphase getroffen hatte, geändert werden. Dies sollte jedem bekannt sein, der jemals mit einem eifrigen Kunden gearbeitet hat, der von einem idealen Produkt träumt, aber vom Entwicklungsteam zurückgehalten werden muss.
Sobald Funktionen implementiert und getestet wurden, wird Ihr Team feststellen, dass einige Merkmale möglicherweise vom ursprünglichen Plan abweichen, und das ist in Ordnung. Einfach benachrichtigen, anpassen und weitermachen. Erlauben Sie mir also, ohne weitere Umschweife, zunächst zu erklären, was sich gegenüber dem ursprünglichen Plan geändert hat.
Weitere Teile dieser Serie
- Teil 1: Die Einführung
- Teil 3: Den Terminal-Client erstellen
- Teil 4: Hinzufügen von Chat zu unserem Spiel
Kampfmechanik
Dies ist wahrscheinlich die größte Änderung gegenüber dem ursprünglichen Plan. Ich weiß, dass ich gesagt habe, dass ich eine D&D-ähnliche Implementierung wählen würde, bei der jeder beteiligte PC und NPC einen Initiativewert erhalten würde und danach würden wir einen rundenbasierten Kampf führen. Es war eine nette Idee, aber die Implementierung auf einem REST-basierten Dienst ist etwas kompliziert, da Sie weder die Kommunikation von der Serverseite aus initiieren noch den Status zwischen Aufrufen beibehalten können.
Stattdessen werde ich die vereinfachte Mechanik von REST nutzen und damit unsere Kampfmechanik vereinfachen. Die implementierte Version wird spielerbasiert statt gruppenbasiert sein und es Spielern ermöglichen, NPCs (Nicht-Spieler-Charaktere) anzugreifen. Wenn ihr Angriff erfolgreich ist, werden die NPCs getötet oder sie greifen zurück, indem sie den Spieler entweder beschädigen oder töten.
Ob ein Angriff erfolgreich ist oder fehlschlägt, hängt von der Art der verwendeten Waffe und den Schwächen eines NPCs ab. Wenn also das Monster, das Sie zu töten versuchen, schwach gegen Ihre Waffe ist, stirbt es. Andernfalls bleibt es unbeeinflusst und – höchstwahrscheinlich – sehr wütend.
Löst aus
Wenn Sie die JSON-Spieldefinition aus meinem vorherigen Artikel genau beachtet haben, ist Ihnen vielleicht die Definition des Triggers aufgefallen, die in Szenenelementen zu finden ist. Eine davon betraf die Aktualisierung des Spielstatus ( statusUpdate
). Während der Implementierung wurde mir klar, dass es nur eine begrenzte Freiheit bietet, wenn es als Umschalter funktioniert. Sie sehen, in der Art und Weise, wie es implementiert wurde (aus idiomatischer Sicht), konnten Sie einen Status setzen, aber das Aufheben war keine Option. Stattdessen habe ich diesen Trigger-Effekt durch zwei neue ersetzt: addStatus
und removeStatus
. Dadurch können Sie genau definieren, wann diese Effekte stattfinden können – wenn überhaupt. Ich denke, das ist viel einfacher zu verstehen und zu begründen.
Das bedeutet, dass die Trigger jetzt so aussehen:
"triggers": [ { "action": "pickup", "effect":{ "addStatus": "has light", "target": "game" } }, { "action": "drop", "effect": { "removeStatus": "has light", "target": "game" } } ]
Beim Abholen des Artikels richten wir einen Status ein und beim Ablegen entfernen wir ihn. Auf diese Weise ist es möglich, mehrere Statusanzeigen auf Spielebene zu haben und einfach zu verwalten.
Die Umsetzung
Nachdem diese Aktualisierungen erledigt sind, können wir mit der eigentlichen Implementierung beginnen. Aus architektonischer Sicht hat sich nichts geändert; Wir bauen immer noch eine REST-API, die die Logik der Hauptspiel-Engine enthalten wird.
Der Tech-Stack
Für dieses spezielle Projekt werde ich die folgenden Module verwenden:
Modul | Beschreibung |
---|---|
Express.js | Natürlich werde ich Express als Basis für die gesamte Engine verwenden. |
Winston | Alles in Bezug auf die Protokollierung wird von Winston gehandhabt. |
Konfig | Jede Konstante und umgebungsabhängige Variable wird vom config.js-Modul verarbeitet, was den Zugriff auf sie erheblich vereinfacht. |
Mungo | Dies wird unser ORM sein. Ich werde alle Ressourcen mit Mongoose-Modellen modellieren und diese verwenden, um direkt mit der Datenbank zu interagieren. |
uuid | Wir müssen einige eindeutige IDs generieren – dieses Modul hilft uns bei dieser Aufgabe. |
Was andere Technologien neben Node.js betrifft, haben wir MongoDB und Redis . Ich verwende gerne Mongo, da kein Schema erforderlich ist. Diese einfache Tatsache ermöglicht es mir, über meinen Code und die Datenformate nachzudenken, ohne mich um die Aktualisierung der Struktur meiner Tabellen, Schemamigrationen oder widersprüchliche Datentypen kümmern zu müssen.
In Bezug auf Redis neige ich dazu, es in meinen Projekten so oft wie möglich als Unterstützungssystem zu verwenden, und dieser Fall ist nicht anders. Ich werde Redis für alles verwenden, was als flüchtige Informationen angesehen werden kann, wie Gruppenmitgliedsnummern, Befehlsanfragen und andere Arten von Daten, die klein genug und flüchtig genug sind, um keine dauerhafte Speicherung zu verdienen.
Ich werde auch die Schlüsselablauffunktion von Redis verwenden, um einige Aspekte des Flusses automatisch zu verwalten (mehr dazu in Kürze).
API-Definition
Bevor ich mich mit Client-Server-Interaktionen und Datenflussdefinitionen befasse, möchte ich die für diese API definierten Endpunkte durchgehen. Sie sind nicht so viele, meistens müssen wir die in Teil 1 beschriebenen Hauptmerkmale einhalten:
Feature | Beschreibung |
---|---|
Nehmen Sie an einem Spiel teil | Ein Spieler kann einem Spiel beitreten, indem er die ID des Spiels angibt. |
Erstellen Sie ein neues Spiel | Ein Spieler kann auch eine neue Spielinstanz erstellen. Die Engine sollte eine ID zurückgeben, damit andere sie zum Beitritt verwenden können. |
Rückszene | Diese Funktion sollte die aktuelle Szene zurückgeben, in der sich die Party befindet. Grundsätzlich wird die Beschreibung mit allen zugehörigen Informationen (mögliche Aktionen, darin enthaltene Objekte usw.) zurückgegeben. |
Interagiere mit der Szene | Dies wird eine der komplexesten sein, da es einen Befehl vom Client entgegennimmt und diese Aktion ausführt – Dinge wie Bewegen, Drücken, Nehmen, Schauen, Lesen, um nur einige zu nennen. |
Inventar prüfen | Obwohl dies eine Möglichkeit ist, mit dem Spiel zu interagieren, bezieht es sich nicht direkt auf die Szene. Das Überprüfen des Inventars für jeden Spieler wird daher als eine andere Aktion betrachtet. |
Registrieren Sie die Client-Anwendung | Die oben genannten Aktionen erfordern einen gültigen Client, um sie auszuführen. Dieser Endpunkt überprüft die Clientanwendung und gibt eine Client-ID zurück, die für Authentifizierungszwecke bei nachfolgenden Anforderungen verwendet wird. |
Die obige Liste wird in die folgende Liste von Endpunkten übersetzt:
Verb | Endpunkt | Beschreibung |
---|---|---|
POST | /clients | Clientanwendungen müssen mithilfe dieses Endpunkts einen Client-ID-Schlüssel abrufen. |
POST | /games | Neue Spielinstanzen werden mithilfe dieses Endpunkts von den Clientanwendungen erstellt. |
POST | /games/:id | Sobald das Spiel erstellt ist, ermöglicht dieser Endpunkt den Gruppenmitgliedern, ihm beizutreten und mit dem Spielen zu beginnen. |
BEKOMMEN | /games/:id/:playername | Dieser Endpunkt gibt den aktuellen Spielstatus für einen bestimmten Spieler zurück. |
POST | /games/:id/:playername/commands | Schließlich kann die Clientanwendung mit diesem Endpunkt Befehle senden (mit anderen Worten, dieser Endpunkt wird zum Spielen verwendet). |
Lassen Sie mich etwas detaillierter auf einige der Konzepte eingehen, die ich in der vorherigen Liste beschrieben habe.
Client-Apps
Die Client-Anwendungen müssen sich im System registrieren, um es verwenden zu können. Alle Endpunkte (mit Ausnahme des ersten auf der Liste) sind gesichert und benötigen einen gültigen Anwendungsschlüssel, der mit der Anfrage gesendet werden muss. Um diesen Schlüssel zu erhalten, müssen Client-Apps einfach einen anfordern. Einmal bereitgestellt, halten sie so lange, wie sie verwendet werden, oder verfallen nach einem Monat der Nichtbenutzung. Dieses Verhalten wird gesteuert, indem der Schlüssel in Redis gespeichert und ihm eine einmonatige TTL zugewiesen wird.
Spielinstanz
Das Erstellen eines neuen Spiels bedeutet im Grunde, eine neue Instanz eines bestimmten Spiels zu erstellen. Diese neue Instanz enthält eine Kopie aller Szenen und ihrer Inhalte. Alle am Spiel vorgenommenen Änderungen wirken sich nur auf die Party aus. Auf diese Weise können viele Gruppen das gleiche Spiel auf ihre individuelle Weise spielen.
Spielstatus des Spielers
Dies ist ähnlich wie das vorherige, aber für jeden Spieler einzigartig. Während die Spielinstanz den Spielstatus für die gesamte Gruppe enthält, enthält der Spielstatus des Spielers den aktuellen Status für einen bestimmten Spieler. Hauptsächlich enthält dies Inventar, Position, aktuelle Szene und HP (Gesundheitspunkte).
Spielerbefehle
Sobald alles eingerichtet ist und sich die Client-Anwendung registriert hat und einem Spiel beigetreten ist, kann sie mit dem Senden von Befehlen beginnen. Zu den implementierten Befehlen in dieser Version der Engine gehören: move
, look
, pickup
und attack
.
- Mit dem
move
Befehl können Sie die Karte durchqueren. Sie können die Richtung angeben, in die Sie sich bewegen möchten, und die Engine teilt Ihnen das Ergebnis mit. Wenn Sie einen kurzen Blick auf Teil 1 werfen, können Sie sehen, wie ich mit Karten umgegangen bin. (Kurz gesagt, die Karte wird als Diagramm dargestellt, wobei jeder Knoten einen Raum oder eine Szene darstellt und nur mit anderen Knoten verbunden ist, die benachbarte Räume darstellen.)
Der Abstand zwischen Knoten ist auch in der Darstellung vorhanden und mit der Standardgeschwindigkeit eines Spielers gekoppelt; Von Raum zu Raum zu gehen ist vielleicht nicht so einfach wie das Erteilen Ihres Befehls, aber Sie müssen auch die Entfernung zurücklegen. In der Praxis bedeutet dies, dass der Wechsel von einem Raum in den anderen möglicherweise mehrere Bewegungsbefehle erfordert). Der andere interessante Aspekt dieses Befehls ergibt sich aus der Tatsache, dass diese Engine Multiplayer-Partys unterstützen soll und die Party nicht geteilt werden kann (zumindest nicht zu diesem Zeitpunkt).
Daher ähnelt die Lösung hierfür einem Abstimmungssystem: Jedes Parteimitglied sendet eine Zugbefehlsanfrage, wann immer es möchte. Sobald mehr als die Hälfte von ihnen dies getan haben, wird die am häufigsten nachgefragte Richtung verwendet. -
look
ist etwas ganz anderes als Bewegung. Es ermöglicht dem Spieler, eine Richtung, einen Gegenstand oder einen NPC anzugeben, den er inspizieren möchte. Die Schlüssellogik hinter diesem Befehl kommt in Betracht, wenn Sie an statusabhängige Beschreibungen denken.
Nehmen wir zum Beispiel an, Sie betreten einen neuen Raum, aber es ist völlig dunkel (Sie sehen nichts), und Sie gehen vorwärts, während Sie ihn ignorieren. Ein paar Räume später heben Sie eine brennende Fackel von einer Wand auf. Jetzt können Sie also zurückgehen und diesen dunklen Raum erneut inspizieren. Da Sie die Fackel aufgehoben haben, können Sie jetzt hineinsehen und mit allen Gegenständen und NPCs interagieren, die Sie darin finden.
Dies wird erreicht, indem ein spielweiter und spielerspezifischer Satz von Statusattributen verwaltet wird und dem Spieleentwickler ermöglicht wird, mehrere Beschreibungen für unsere statusabhängigen Elemente in der JSON-Datei anzugeben. Jede Beschreibung wird dann je nach aktuellem Status mit einem Standardtext und einem Satz bedingter Texte ausgestattet. Letztere sind optional; Der einzige, der obligatorisch ist, ist der Standardwert.
Zusätzlich gibt es für diesen Befehl eine Kurzversion fürlook at room: look around
; Das liegt daran, dass die Spieler sehr oft versuchen werden, einen Raum zu inspizieren, daher ist es sehr sinnvoll, einen Kurzbefehl (oder Alias) bereitzustellen, der einfacher zu tippen ist. - Der
pickup
Befehl spielt eine sehr wichtige Rolle für das Gameplay. Dieser Befehl kümmert sich um das Hinzufügen von Gegenständen in das Inventar des Spielers oder seine Hände (wenn sie frei sind). Um zu verstehen, wo jeder Gegenstand aufbewahrt werden soll, hat seine Definition eine „Ziel“-Eigenschaft, die angibt, ob er für das Inventar oder die Hände des Spielers bestimmt ist. Alles, was erfolgreich aus der Szene aufgenommen wurde, wird dann daraus entfernt, wodurch die Version des Spiels der Spielinstanz aktualisiert wird. - Mit dem
use
-Befehl können Sie die Umgebung mit Gegenständen in Ihrem Inventar beeinflussen. Wenn Sie beispielsweise einen Schlüssel in einem Raum aufheben, können Sie damit eine verschlossene Tür in einem anderen Raum öffnen. - Es gibt einen speziellen Befehl, der sich nicht auf das Gameplay bezieht, sondern ein Hilfsbefehl, mit dem bestimmte Informationen abgerufen werden können, z. B. die aktuelle Spiel-ID oder der Name des Spielers. Dieser Befehl heißt get , und die Spieler können damit die Spiel-Engine abfragen. Zum Beispiel: get gameid .
- Schließlich ist der letzte Befehl, der für diese Version der Engine implementiert wurde, der
attack
. Ich habe dieses bereits abgedeckt; Im Grunde müssen Sie Ihr Ziel und die Waffe angeben, mit der Sie es angreifen. Auf diese Weise kann das System die Schwächen des Ziels überprüfen und die Leistung Ihres Angriffs bestimmen.
Client-Engine-Interaktion
Um zu verstehen, wie die oben aufgeführten Endpunkte verwendet werden, möchte ich Ihnen zeigen, wie jeder potenzielle Kunde mit unserer neuen API interagieren kann.
Schritt | Beschreibung |
---|---|
Kunde registrieren | Das Wichtigste zuerst: Die Clientanwendung muss einen API-Schlüssel anfordern, um auf alle anderen Endpunkte zugreifen zu können. Um diesen Schlüssel zu erhalten, muss er sich auf unserer Plattform registrieren. Der einzige Parameter, der bereitgestellt werden muss, ist der Name der App, das ist alles. |
Erstellen Sie ein Spiel | Nachdem Sie den API-Schlüssel erhalten haben, müssen Sie als Erstes (vorausgesetzt, es handelt sich um eine brandneue Interaktion) eine brandneue Spielinstanz erstellen. Stellen Sie sich das so vor: Die JSON-Datei, die ich in meinem letzten Beitrag erstellt habe, enthält die Definition des Spiels, aber wir müssen eine Instanz davon nur für Sie und Ihre Gruppe erstellen (denken Sie an Klassen und Objekte, gleiche Sache). Sie können mit dieser Instanz machen, was Sie wollen, und es wird andere Parteien nicht beeinträchtigen. |
Nimm am Spiel teil | Nachdem Sie das Spiel erstellt haben, erhalten Sie von der Engine eine Spiel-ID zurück. Sie können dann diese Spiel-ID verwenden, um der Instanz mit Ihrem eindeutigen Benutzernamen beizutreten. Wenn Sie dem Spiel nicht beitreten, können Sie nicht spielen, da das Beitreten zum Spiel auch eine Spielstatusinstanz für Sie allein erstellt. Hier werden dein Inventar, deine Position und deine Grundstatistiken in Bezug auf das Spiel, das du spielst, gespeichert. Sie könnten möglicherweise mehrere Spiele gleichzeitig spielen und in jedem unabhängige Zustände haben. |
Befehle senden | Mit anderen Worten: Spielen Sie das Spiel. Der letzte Schritt besteht darin, mit dem Senden von Befehlen zu beginnen. Die Menge der verfügbaren Befehle wurde bereits abgedeckt und kann leicht erweitert werden (mehr dazu später). Jedes Mal, wenn Sie einen Befehl senden, gibt das Spiel den neuen Spielstatus zurück, damit Ihr Client Ihre Ansicht entsprechend aktualisieren kann. |
Machen wir uns die Hände schmutzig
Ich habe so viel Design wie möglich durchgesehen, in der Hoffnung, dass diese Informationen Ihnen helfen, den folgenden Teil zu verstehen, also lassen Sie uns in die Grundlagen der Spiel-Engine einsteigen.
Hinweis : Ich werde Ihnen in diesem Artikel nicht den vollständigen Code zeigen, da er ziemlich umfangreich und nicht alles davon interessant ist. Stattdessen zeige ich die relevanteren Teile und verlinke auf das vollständige Repository, falls Sie weitere Details wünschen.
Die Hauptdatei
Das Wichtigste zuerst: Dies ist ein Express-Projekt und der basierte Boilerplate-Code wurde mit dem Express-eigenen Generator generiert, daher sollte Ihnen die Datei app.js vertraut sein. Ich möchte nur zwei Optimierungen durchgehen, die ich gerne an diesem Code vornehme, um meine Arbeit zu vereinfachen.
Zuerst füge ich das folgende Snippet hinzu, um die Aufnahme neuer Routendateien zu automatisieren:
const requireDir = require("require-dir") const routes = requireDir("./routes") //... Object.keys(routes).forEach( (file) => { let cnt = routes[file] app.use('/' + file, cnt) })
Es ist wirklich ganz einfach, aber es beseitigt die Notwendigkeit, jede Routendatei, die Sie in Zukunft erstellen, manuell anzufordern. Übrigens ist require-dir
ein einfaches Modul, das sich darum kümmert, jede Datei in einem Ordner automatisch anzufordern. Das ist es.
Die andere Änderung, die ich gerne mache, ist, meinen Fehlerhandler nur ein wenig zu optimieren. Ich sollte wirklich anfangen, etwas Robusteres zu verwenden, aber für die vorliegenden Anforderungen habe ich das Gefühl, dass dies die Arbeit erledigt:
// error handler app.use(function(err, req, res, next) { // render the error page if(typeof err === "string") { err = { status: 500, message: err } } res.status(err.status || 500); let errorObj = { error: true, msg: err.message, errCode: err.status || 500 } if(err.trace) { errorObj.trace = err.trace } res.json(errorObj); });
Der obige Code kümmert sich um die verschiedenen Arten von Fehlermeldungen, mit denen wir möglicherweise umgehen müssen – entweder vollständige Objekte, tatsächliche Fehlerobjekte, die von Javascript geworfen werden, oder einfache Fehlermeldungen ohne anderen Kontext. Dieser Code nimmt alles und formatiert es in ein Standardformat.
Umgang mit Befehlen
Dies ist ein weiterer Aspekt des Motors, der einfach zu erweitern sein musste. In einem Projekt wie diesem ist es absolut sinnvoll anzunehmen, dass in Zukunft neue Befehle auftauchen werden. Wenn Sie etwas vermeiden möchten, dann ist das wahrscheinlich, Änderungen am Basiscode zu vermeiden, wenn Sie versuchen, in drei oder vier Monaten etwas Neues hinzuzufügen.
Keine Menge Codekommentare wird die Aufgabe, Code zu modifizieren, den Sie seit mehreren Monaten nicht mehr berührt (oder auch nur daran gedacht) haben, einfach machen, daher ist die Priorität, so viele Änderungen wie möglich zu vermeiden. Glücklicherweise gibt es ein paar Muster, die wir implementieren können, um dieses Problem zu lösen. Insbesondere habe ich eine Mischung aus dem Command- und dem Factory-Pattern verwendet.
Ich habe im Grunde das Verhalten jedes Befehls in einer einzelnen Klasse gekapselt, die von einer BaseCommand
-Klasse erbt, die den generischen Code für alle Befehle enthält. Gleichzeitig habe ich ein CommandParser
-Modul hinzugefügt, das die vom Client gesendete Zeichenfolge erfasst und den eigentlichen auszuführenden Befehl zurückgibt.
Der Parser ist sehr einfach, da alle implementierten Befehle jetzt den eigentlichen Befehl in Bezug auf ihr erstes Wort haben (z. B. „nach Norden bewegen“, „Messer aufheben“ usw.). Es ist einfach, die Zeichenfolge aufzuteilen und den ersten Teil zu erhalten:
const requireDir = require("require-dir") const validCommands = requireDir('./commands') class CommandParser { constructor(command) { this.command = command } normalizeAction(strAct) { strAct = strAct.toLowerCase().split(" ")[0] return strAct } verifyCommand() { if(!this.command) return false if(!this.command.action) return false if(!this.command.context) return false let action = this.normalizeAction(this.command.action) if(validCommands[action]) { return validCommands[action] } return false } parse() { let validCommand = this.verifyCommand() if(validCommand) { let cmdObj = new validCommand(this.command) return cmdObj } else { return false } } }
Hinweis : Ich verwende erneut das Modul require-dir
, um das Einbinden vorhandener und neuer Befehlsklassen zu vereinfachen. Ich füge es einfach dem Ordner hinzu und das gesamte System kann es aufnehmen und verwenden.
Abgesehen davon gibt es viele Möglichkeiten, dies zu verbessern. Zum Beispiel wäre es eine großartige Funktion, Synonymunterstützung für unsere Befehle hinzuzufügen (also würde es dasselbe bedeuten, „nach Norden zu gehen“, „nach Norden zu gehen“ oder sogar „nach Norden zu gehen“). Das könnten wir in dieser Klasse zentralisieren und alle Befehle gleichzeitig beeinflussen.
Ich werde auf keinen der Befehle im Detail eingehen, da das wiederum zu viel Code ist, um ihn hier zu zeigen, aber Sie können im folgenden Routencode sehen, wie ich es geschafft habe, diese Behandlung der vorhandenen (und zukünftigen) Befehle zu verallgemeinern:
/** Interaction with a particular scene */ router.post('/:id/:playername/:scene', function(req, res, next) { let command = req.body command.context = { gameId: req.params.id, playername: req.params.playername, } let parser = new CommandParser(command) let commandObj = parser.parse() //return the command instance if(!commandObj) return next({ //error handling status: 400, errorCode: config.get("errorCodes.invalidCommand"), message: "Unknown command" }) commandObj.run((err, result) => { //execute the command if(err) return next(err) res.json(result) }) })
Alle Befehle erfordern nur die run
-Methode – alles andere ist extra und für den internen Gebrauch bestimmt.
Ich ermutige Sie, den gesamten Quellcode zu überprüfen (laden Sie ihn sogar herunter und spielen Sie damit, wenn Sie möchten!). Im nächsten Teil dieser Serie zeige ich Ihnen die tatsächliche Client-Implementierung und Interaktion dieser API.
Abschließende Gedanken
Ich habe hier vielleicht nicht viel von meinem Code behandelt, aber ich hoffe dennoch, dass der Artikel hilfreich war, um Ihnen zu zeigen, wie ich Projekte angehe – auch nach der anfänglichen Designphase. Ich habe das Gefühl, dass viele Leute versuchen, als erste Reaktion auf eine neue Idee mit dem Programmieren zu beginnen, und das kann manchmal für einen Entwickler entmutigend sein, da es keinen wirklichen Plan gibt und keine Ziele zu erreichen sind – außer das Endprodukt fertig zu haben ( und das ist ein zu großer Meilenstein, um ihn von Tag 1 an zu bewältigen). Also noch einmal, meine Hoffnung ist es, mit diesen Artikeln einen anderen Weg aufzuzeigen, wie man alleine (oder als Teil einer kleinen Gruppe) an großen Projekten arbeiten kann.
Ich hoffe, Sie haben das Lesen genossen! Bitte zögern Sie nicht, unten einen Kommentar mit Vorschlägen oder Empfehlungen jeglicher Art zu hinterlassen. Ich würde gerne lesen, was Sie denken und ob Sie die API mit Ihrem eigenen clientseitigen Code testen möchten.
Wir sehen uns beim nächsten!
Weitere Teile dieser Serie
- Teil 1: Die Einführung
- Teil 3: Den Terminal-Client erstellen
- Teil 4: Hinzufügen von Chat zu unserem Spiel