Schreiben einer Multiplayer-Text-Adventure-Engine in Node.js (Teil 1)
Veröffentlicht: 2022-03-10Textadventures waren eine der ersten Formen digitaler Rollenspiele da draußen, als Spiele noch keine Grafiken hatten und alles, was Sie hatten, Ihre eigene Vorstellungskraft und die Beschreibung war, die Sie auf dem schwarzen Bildschirm Ihres CRT-Monitors lesen konnten.
Wenn wir nostalgisch werden wollen, klingelt uns vielleicht der Name Colossal Cave Adventure (oder einfach Adventure, wie es ursprünglich hieß). Das war das allererste Text-Adventure-Spiel, das jemals gemacht wurde.
Das obige Bild zeigt, wie Sie das Spiel tatsächlich sehen würden, weit entfernt von unseren aktuellen Top-AAA-Abenteuerspielen. Abgesehen davon hat es Spaß gemacht, sie zu spielen, und Sie würden Hunderte von Stunden Ihrer Zeit stehlen, während Sie alleine vor diesem Text saßen und versuchten, herauszufinden, wie Sie ihn schlagen könnten.
Verständlicherweise wurden Textabenteuer im Laufe der Jahre durch Spiele ersetzt, die eine bessere Grafik bieten (obwohl man argumentieren könnte, dass viele von ihnen die Geschichte der Grafik geopfert haben) und insbesondere in den letzten Jahren die zunehmende Fähigkeit, mit anderen zusammenzuarbeiten Freunde und spielen zusammen. Dieses besondere Feature fehlte den ursprünglichen Text-Abenteuern und ich möchte es in diesem Artikel zurückbringen.
Weitere Teile dieser Serie
- Teil 2: Design des Game-Engine-Servers
- Teil 3: Den Terminal-Client erstellen
- Teil 4: Hinzufügen von Chat zu unserem Spiel
Unser Ziel
Der springende Punkt bei diesem Unterfangen ist, wie Sie wahrscheinlich bereits aus dem Titel dieses Artikels erraten haben, eine Text-Adventure-Engine zu erstellen, die es Ihnen ermöglicht, das Abenteuer mit Freunden zu teilen und mit ihnen ähnlich wie währenddessen zusammenzuarbeiten ein Dungeons & Dragons-Spiel (in dem es, genau wie bei den guten alten Textabenteuern, keine Grafiken zum Anschauen gibt).
In der Erstellung der Engine, des Chatservers und des Clients steckt ziemlich viel Arbeit. In diesem Artikel zeige ich Ihnen die Designphase und erkläre Dinge wie die Architektur hinter der Engine, wie der Client mit den Servern interagiert und welche Spielregeln gelten.
Nur um Ihnen eine visuelle Hilfestellung zu geben, wie das aussehen wird, hier ist mein Ziel:
Das ist unser Ziel. Sobald wir dort angekommen sind, haben Sie Screenshots anstelle von schnellen und schmutzigen Modellen. Lassen Sie uns also mit dem Prozess fortfahren. Das erste, was wir behandeln werden, ist das Design des Ganzen. Dann behandeln wir die relevantesten Tools, die ich verwenden werde, um dies zu codieren. Abschließend zeige ich Ihnen einige der relevantesten Codeteile (natürlich mit einem Link zum vollständigen Repository).
Hoffentlich erfindest du dich am Ende dabei, neue Textabenteuer zu erstellen, um sie mit Freunden auszuprobieren!
Design-Phase
Für die Designphase werde ich unseren Gesamtplan behandeln. Ich werde mein Bestes geben, Sie nicht zu Tode zu langweilen, aber gleichzeitig denke ich, dass es wichtig ist, einige der Dinge hinter den Kulissen zu zeigen, die passieren müssen, bevor Sie Ihre erste Codezeile niederlegen.
Die vier Komponenten, die ich hier mit einer anständigen Menge an Details behandeln möchte, sind:
- Der Motor
Dies wird der Hauptspielserver sein. Die Spielregeln werden hier implementiert und es wird eine technologisch agnostische Schnittstelle für jede Art von Client zur Verfügung gestellt. Wir werden einen Terminal-Client implementieren, aber Sie könnten dasselbe mit einem Webbrowser-Client oder einem anderen Typ tun, den Sie möchten. - Der Chatserver
Da es komplex genug ist, einen eigenen Artikel zu haben, wird dieser Dienst auch ein eigenes Modul haben. Der Chat-Server kümmert sich darum, dass die Spieler während des Spiels miteinander kommunizieren können. - Der Kunde
Wie bereits erwähnt, handelt es sich dabei um einen Terminal-Client, der im Idealfall dem Mockup von früher ähnlich sieht. Es nutzt die Dienste, die sowohl von der Engine als auch vom Chat-Server bereitgestellt werden. - Spiele (JSON-Dateien)
Abschließend gehe ich auf die Definition der eigentlichen Spiele ein. Der springende Punkt dabei ist, eine Engine zu erstellen, die jedes Spiel ausführen kann, solange Ihre Spieldatei die Anforderungen der Engine erfüllt. Obwohl dies keine Codierung erfordert, werde ich erklären, wie ich die Abenteuerdateien strukturiere, um in Zukunft unsere eigenen Abenteuer zu schreiben.
Der Motor
Die Spiel-Engine oder der Spielserver wird eine REST-API sein und alle erforderlichen Funktionen bereitstellen.
Ich habe mich für eine REST-API entschieden, einfach weil – für diese Art von Spielen – die durch HTTP hinzugefügte Verzögerung und ihre asynchrone Natur keine Probleme verursachen werden. Für den Chatserver müssen wir allerdings einen anderen Weg gehen. Aber bevor wir mit der Definition von Endpunkten für unsere API beginnen, müssen wir definieren, wozu die Engine in der Lage sein wird. Kommen wir also zur Sache.
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. |
Ein Wort zur Bewegung
Wir brauchen eine Möglichkeit, Entfernungen im Spiel zu messen, denn das Bewegen durch das Abenteuer ist eine der Kernaktionen, die ein Spieler ausführen kann. Wir werden diese Zahl als Zeitmaß verwenden, nur um das Gameplay zu vereinfachen. Die Zeitmessung mit einer tatsächlichen Uhr ist möglicherweise nicht die beste, wenn man bedenkt, dass diese Art von Spielen rundenbasierte Aktionen wie Kämpfe hat. Stattdessen verwenden wir die Entfernung, um die Zeit zu messen (was bedeutet, dass eine Entfernung von 8 mehr Zeit zum Durchlaufen benötigt als eine von 2, wodurch wir Dinge wie das Hinzufügen von Effekten zu Spielern tun können, die für eine festgelegte Anzahl von „Entfernungspunkten“ anhalten. ).
Ein weiterer wichtiger Aspekt bei der Bewegung ist, dass wir nicht alleine spielen. Der Einfachheit halber erlaubt die Engine nicht, dass Spieler die Gruppe teilen (obwohl dies eine interessante Verbesserung für die Zukunft sein könnte). In der Anfangsversion dieses Moduls wird jeder nur dorthin ziehen können, wo die Mehrheit der Partei entscheidet. Der Umzug muss also im Konsens erfolgen, was bedeutet, dass jede Umzugsaktion darauf wartet, dass die Mehrheit der Partei sie anfordert, bevor sie stattfindet.
Kampf
Der Kampf ist ein weiterer sehr wichtiger Aspekt dieser Art von Spielen und einer, den wir in Betracht ziehen müssen, um ihn unserer Engine hinzuzufügen. Andernfalls verpassen wir am Ende etwas von dem Spaß.
Das muss nicht neu erfunden werden, um ehrlich zu sein. Rundenbasierte Gruppenkämpfe gibt es schon seit Jahrzehnten, also implementieren wir einfach eine Version dieser Mechanik. Wir werden es mit dem Dungeons & Dragons-Konzept der „Initiative“ verwechseln und eine Zufallszahl würfeln, um den Kampf etwas dynamischer zu halten.
Mit anderen Worten, die Reihenfolge, in der alle an einem Kampf Beteiligten ihre Aktion auswählen können, wird zufällig festgelegt, und dazu gehören auch die Feinde.
Schließlich (obwohl ich weiter unten ausführlicher darauf eingehen werde) haben Sie Gegenstände, die Sie mit einer festgelegten "Schadens" -Nummer aufheben können. Dies sind die Gegenstände, die Sie während des Kampfes verwenden können; Alles, was diese Eigenschaft nicht hat, fügt deinen Feinden 0 Schaden zu. Wir werden wahrscheinlich eine Nachricht hinzufügen, wenn Sie versuchen, diese Objekte zum Kämpfen zu verwenden, damit Sie wissen, dass das, was Sie zu tun versuchen, keinen Sinn ergibt.
Client-Server-Interaktion
Lassen Sie uns nun sehen, wie ein bestimmter Client mit unserem Server unter Verwendung der zuvor definierten Funktionalität interagieren würde (wir denken noch nicht an Endpunkte, aber wir kommen gleich dazu):
Die anfängliche Interaktion zwischen dem Client und dem Server (aus Sicht des Servers) ist der Beginn eines neuen Spiels, und die Schritte dafür sind wie folgt:
- Erstellen Sie ein neues Spiel .
Der Client fordert die Erstellung eines neuen Spiels vom Server an. - Chatroom erstellen .
Obwohl der Name es nicht angibt, erstellt der Server nicht nur einen Chatroom auf dem Chatserver, sondern richtet auch alles ein, was erforderlich ist, damit eine Gruppe von Spielern ein Abenteuer durchspielen kann. - Gibt die Metadaten des Spiels zurück .
Sobald das Spiel vom Server erstellt wurde und der Chatroom für die Spieler eingerichtet ist, benötigt der Client diese Informationen für nachfolgende Anfragen. Dies wird hauptsächlich eine Reihe von IDs sein, die die Clients verwenden können, um sich selbst und das aktuelle Spiel, dem sie beitreten möchten, zu identifizieren (mehr dazu gleich). - Spiel-ID manuell teilen .
Dieser Schritt muss von den Spielern selbst durchgeführt werden. Wir könnten uns eine Art Freigabemechanismus einfallen lassen, aber ich werde das auf der Wunschliste für zukünftige Verbesserungen belassen. - Nehmen Sie am Spiel teil .
Dieser ist ziemlich einfach. Sobald jeder die Spiel-ID hat, nimmt er mit seinen Client-Anwendungen am Abenteuer teil. - Treten Sie ihrem Chatraum bei .
Schließlich verwenden die Client-Apps der Spieler die Metadaten des Spiels, um dem Chatroom ihres Abenteuers beizutreten. Dies ist der letzte Schritt, der vor dem Spiel erforderlich ist. Sobald dies alles erledigt ist, können die Spieler mit dem Abenteuer beginnen!
Sobald alle Voraussetzungen erfüllt sind, können die Spieler mit dem Spielen des Abenteuers beginnen, ihre Gedanken im Party-Chat austauschen und die Geschichte vorantreiben. Das obige Diagramm zeigt die dafür erforderlichen vier Schritte.
Die folgenden Schritte werden als Teil der Spielschleife ausgeführt, was bedeutet, dass sie ständig wiederholt werden, bis das Spiel endet.
- Szene anfordern .
Die Client-App fordert die Metadaten für die aktuelle Szene an. Dies ist der erste Schritt in jeder Iteration der Schleife. - Geben Sie die Metadaten zurück .
Der Server sendet wiederum die Metadaten für die aktuelle Szene zurück. Diese Informationen umfassen Dinge wie eine allgemeine Beschreibung, die darin enthaltenen Objekte und ihre Beziehung zueinander. - Befehl senden .
Hier beginnt der Spaß. Dies ist die Haupteingabe des Players. Es enthält die Aktion, die sie ausführen möchten, und optional das Ziel dieser Aktion (z. B. Kerze ausblasen, Stein greifen usw.). - Geben Sie die Reaktion auf den gesendeten Befehl zurück .
Dies könnte einfach Schritt zwei sein, aber aus Gründen der Übersichtlichkeit habe ich ihn als zusätzlichen Schritt hinzugefügt. Der Hauptunterschied besteht darin, dass Schritt zwei als Beginn dieser Schleife betrachtet werden könnte, während dieser berücksichtigt, dass Sie bereits spielen, und daher muss der Server verstehen, wen diese Aktion betreffen wird (entweder einen einzelnen Spieler oder alle Spieler).
Als zusätzlichen Schritt, obwohl dies nicht wirklich Teil des Ablaufs ist, benachrichtigt der Server Clients über Statusaktualisierungen, die für sie relevant sind.
Der Grund für diesen zusätzlichen wiederkehrenden Schritt liegt in den Aktualisierungen, die ein Spieler durch die Aktionen anderer Spieler erhalten kann. Erinnern Sie sich an die Notwendigkeit, sich von einem Ort zum anderen zu bewegen; Wie ich bereits sagte, sobald die Mehrheit der Spieler eine Richtung gewählt hat, werden sich alle Spieler bewegen (es ist keine Eingabe von allen Spielern erforderlich).
Das Interessante hier ist, dass HTTP (wir haben bereits erwähnt, dass der Server eine REST-API sein wird) diese Art von Verhalten nicht zulässt. Unsere Optionen sind also:
- Abfragen alle X Sekunden vom Client durchführen,
- Verwenden Sie eine Art Benachrichtigungssystem, das parallel zur Client-Server-Verbindung arbeitet.
Meiner Erfahrung nach bevorzuge ich eher Option 2. Tatsächlich würde (und werde ich für diesen Artikel) Redis für diese Art von Verhalten verwenden.
Das folgende Diagramm veranschaulicht die Abhängigkeiten zwischen Diensten.
Der Chat-Server
Ich überlasse die Details des Designs dieses Moduls der Entwicklungsphase (die nicht Teil dieses Artikels ist). Davon abgesehen gibt es Dinge, die wir entscheiden können.
Eine Sache, die wir definieren können, ist die Menge der Einschränkungen für den Server, die unsere Arbeit später vereinfachen wird. Und wenn wir unsere Karten richtig spielen, könnten wir am Ende einen Dienst haben, der eine robuste Schnittstelle bietet, die es uns schließlich ermöglicht, die Implementierung zu erweitern oder sogar zu ändern, um weniger Einschränkungen bereitzustellen, ohne das Spiel überhaupt zu beeinträchtigen.
- Pro Partei wird es nur einen Raum geben.
Wir lassen keine Untergruppen entstehen. Das geht Hand in Hand damit, die Partei nicht spalten zu lassen. Vielleicht wäre es eine gute Idee, sobald wir diese Erweiterung implementiert haben, die Erstellung von Untergruppen und benutzerdefinierten Chatrooms zu ermöglichen. - Es werden keine privaten Nachrichten gesendet.
Dies dient lediglich der Vereinfachung, aber ein Gruppenchat ist bereits gut genug; Wir brauchen jetzt keine privaten Nachrichten. Denken Sie daran, dass Sie bei der Arbeit an Ihrem minimal lebensfähigen Produkt versuchen sollten, das Kaninchenloch unnötiger Funktionen zu vermeiden. Es ist ein gefährlicher Weg und einer, dem man nur schwer entkommen kann. - Wir werden keine Nachrichten beibehalten.
Mit anderen Worten, wenn Sie die Party verlassen, gehen die Nachrichten verloren. Dies wird unsere Aufgabe enorm vereinfachen, da wir uns weder mit irgendeiner Art von Datenspeicherung befassen müssen, noch Zeit damit verschwenden müssen, die beste Datenstruktur zum Speichern und Wiederherstellen alter Nachrichten zu finden. Es wird alles in Erinnerung bleiben, und es wird dort bleiben, solange der Chatroom aktiv ist. Sobald es geschlossen ist, verabschieden wir uns einfach von ihnen! - Die Kommunikation erfolgt über Sockets .
Leider muss unser Client einen doppelten Kommunikationskanal handhaben: einen RESTful-Kanal für die Spiel-Engine und einen Socket für den Chat-Server. Dies erhöht möglicherweise die Komplexität des Clients etwas, verwendet aber gleichzeitig die besten Kommunikationsmethoden für jedes Modul. (Es hat keinen wirklichen Sinn, REST auf unserem Chatserver oder Sockets auf unserem Spielserver zu erzwingen. Dieser Ansatz würde die Komplexität des serverseitigen Codes erhöhen, der auch die Geschäftslogik handhabt, also konzentrieren wir uns auf diese Seite zur Zeit.)
Das war's für den Chat-Server. Immerhin wird es nicht kompliziert, zumindest nicht am Anfang. Es gibt noch mehr zu tun, wenn es an der Zeit ist, mit dem Programmieren zu beginnen, aber für diesen Artikel sind es mehr als genug Informationen.
Der Kunde
Dies ist das letzte Modul, das Codierung erfordert, und es wird unser dümmstes von allen sein. Als Faustregel ziehe ich es vor, meine Kunden dumm und meine Server schlau zu haben. Auf diese Weise wird das Erstellen neuer Clients für den Server viel einfacher.
Nur damit wir auf der gleichen Seite sind, hier ist die High-Level-Architektur, die wir am Ende haben sollten.
Unser einfacher CLI-Client wird nichts sehr Komplexes implementieren. Tatsächlich ist das Komplizierteste, was wir angehen müssen, die eigentliche Benutzeroberfläche, da es sich um eine textbasierte Schnittstelle handelt.
Davon abgesehen ist die Funktionalität, die die Client-Anwendung implementieren muss, wie folgt:
- Erstellen Sie ein neues Spiel .
Da ich die Dinge so einfach wie möglich halten möchte, wird dies nur über die CLI-Schnittstelle erfolgen. Die eigentliche Benutzeroberfläche wird erst nach dem Beitritt zu einem Spiel verwendet, was uns zum nächsten Punkt bringt. - Tritt einem bestehenden Spiel bei .
Angesichts des vom vorherigen Punkt zurückgegebenen Spielcodes können Spieler ihn zum Mitmachen verwenden. Auch dies ist etwas, das Sie ohne eine Benutzeroberfläche tun können sollten, daher ist diese Funktionalität Teil des Prozesses, der erforderlich ist, um mit der Verwendung der Text-Benutzeroberfläche zu beginnen. - Spieldefinitionsdateien parsen .
Wir werden diese gleich besprechen, aber der Client sollte in der Lage sein, diese Dateien zu verstehen, um zu wissen, was angezeigt werden soll, und um zu wissen, wie er diese Daten verwenden kann. - Interagiere mit dem Abenteuer.
Grundsätzlich gibt dies dem Spieler die Möglichkeit, jederzeit mit der beschriebenen Umgebung zu interagieren. - Pflegen Sie ein Inventar für jeden Spieler .
Jede Instanz des Clients enthält eine In-Memory-Liste von Elementen. Diese Liste wird gesichert. - Support-Chat .
Die Client-App muss sich auch mit dem Chat-Server verbinden und den Benutzer im Chatroom der Partei anmelden.
Mehr über die interne Struktur und das Design des Kunden später. Lassen Sie uns in der Zwischenzeit die Designphase mit dem letzten bisschen Vorbereitung beenden: den Spieldateien.
Das Spiel: JSON-Dateien
Hier wird es interessant, denn bisher habe ich grundlegende Microservices-Definitionen behandelt. Einige von ihnen sprechen möglicherweise REST, andere arbeiten mit Sockets, aber im Wesentlichen sind sie alle gleich: Sie definieren sie, Sie codieren sie und sie stellen einen Dienst bereit.
Für diese spezielle Komponente habe ich nicht vor, irgendetwas zu codieren, aber wir müssen sie entwerfen. Im Grunde implementieren wir eine Art Protokoll, um unser Spiel, die Szenen darin und alles darin zu definieren.
Wenn Sie darüber nachdenken, ist ein Textadventure im Grunde genommen eine Reihe von Räumen, die miteinander verbunden sind und in denen sich „Dinge“ befinden, mit denen Sie interagieren können, die alle mit einer hoffentlich anständigen Geschichte verbunden sind. Jetzt wird sich unsere Engine nicht um diesen letzten Teil kümmern; Dieser Teil liegt bei Ihnen. Aber für den Rest gibt es Hoffnung.
Nun, zurück zu den miteinander verbundenen Räumen, das klingt für mich wie ein Diagramm, und wenn wir auch das Konzept der Entfernung oder Bewegungsgeschwindigkeit hinzufügen, das ich zuvor erwähnt habe, haben wir ein gewichtetes Diagramm. Und das ist nur eine Reihe von Knoten, die ein Gewicht (oder nur eine Zahl – machen Sie sich keine Gedanken darüber, wie es heißt) haben, die diesen Pfad zwischen ihnen darstellen. Hier ist ein Bild (ich liebe es, durch Sehen zu lernen, also schau dir einfach das Bild an, OK?):
Das ist ein gewichteter Graph – das ist es. Und ich bin sicher, Sie haben es bereits herausgefunden, aber der Vollständigkeit halber möchte ich Ihnen zeigen, wie Sie vorgehen würden, wenn unser Motor fertig ist.
Sobald Sie mit der Einrichtung des Abenteuers beginnen, erstellen Sie Ihre Karte (wie Sie links im Bild unten sehen). Und dann übersetzen Sie das in ein gewichtetes Diagramm, wie Sie rechts im Bild sehen können. Unsere Engine wird in der Lage sein, es aufzunehmen und Sie in der richtigen Reihenfolge durchgehen zu lassen.
Mit dem gewichteten Diagramm oben können wir sicherstellen, dass Spieler nicht vom Eingang bis zum linken Flügel gehen können. Sie müssten die Knoten zwischen diesen beiden durchlaufen, und dies würde Zeit verbrauchen, die wir anhand der Gewichtung der Verbindungen messen können.
Nun zum „lustigen“ Teil. Mal sehen, wie das Diagramm im JSON-Format aussehen würde. Geduld mit mir hier; Dieser JSON wird viele Informationen enthalten, aber ich werde so viel wie möglich davon durchgehen:
{ "graph": [ { "id": "entrance", "name": "Entrance", "north": { "node": "1stroom", "distance": 1 } }, { "id": "1st room", "name": "1st Room", "south": {"node": "entrance", "distance": 1} , "north": { "node": "bigroom", "distance": 1} } , { "id": "bigroom", "name": "Big room", "south": { "node": "1stroom", "distance": 1}, "north": { "node": "bossroom", "distance": 2}, "east": { "node": "rightwing", "distance": 3} , "west": { "node": "leftwing", "distance": 3} }, { "id": "bossroom", "name": "Boss room", "south": {"node": "bigroom", "distance": 2} } { "id": "leftwing", "name": "Left Wing", "east": {"node": "bigroom", "distance": 3} } { "id": "rightwing", "name": "Right Wing", "west": { "node": "bigroom", "distance": 3 } } ], "game": { "win-condition": { "source": "finalboss", "condition": { "type": "comparison", "left": "hp", "right": "0", "symbol": "<=" } }, "lose-condition": { "source": "player", "condition": { "type": "comparison", "left": "hp", "right": "0", "symbol": "<=" } } }, "rooms": { "entrance": { "description": { "default": "You're at the entrance of the dungeon. There are two lit torches on each wall (one on your right and one on your left). You see only one path: ahead." }, "items": [ { "id": "littorch1", "name": "Lit torch on the right", "triggers": [ { "action": "grab", //grab Lit torch on the right "effect":{ "statusUpdate": "has light", "target": "game", } } ] , "destination": "hand" }, { "id": "littorch2", "name": "Lit torch on the left", "triggers": [ { "action": "grab", //grab Lit torch on the left "effect":{ "statusUpdate": "has light", "target": "game", } } ] , "destination": "hand" } ] }, "1stroom": { "description": { "default": "You're in a very dark room. There are no windows and no source of light, other than the one at the entrance. You get the feeling you're not alone here.", "conditionals": { "has light": "The room you find yourself in appears to be empty, aside from a single chair in the right corner. There appears to be only one way out: deeper into the dungeon." } }, "items": [ { "id": "chair", "name": "Wooden chair", "details": "It's a wooden chair, nothing fancy about it. It appears to have been sitting here, untouched, for a while now.", "subitems": [ { "id": "woodenleg", "name": "Wooden leg", "triggeractions": [ { "action": "break", "target": "chair"}, //break { "action": "throw", "target": "chair"} //throw ], "destination": "inventory", "damage": 2 } ] } ] }, "bigroom": { "description": { "default": "You've reached the big room. On every wall are torches lighting every corner. The walls are painted white, and the ceiling is tall and filled with painted white stars on a black background. There is a gateway on either side and a big, wooden double door in front of you." }, "exits": { "north": { "id": "bossdoor", "name": "Big double door", "status": "locked", "details": "A aig, wooden double door. It seems like something big usually comes through here."} }, "items": [] }, "leftwing": { "description": { "default": "Another dark room. It doesn't look like it's that big, but you can't really tell what's inside. You do, however, smell rotten meat somewhere inside.", "conditionals": { "has light": "You appear to have found the kitchen. There are tables full of meat everywhere, and a big knife sticking out of what appears to be the head of a cow." } }, "items": [ { "id": "bigknife", "name": "Big knife", "destination": "inventory", "damage": 10} ] }, "rightwing": { "description": { "default": "This appear to be some sort of office. There is a wooden desk in the middle, torches lighting every wall, and a single key resting on top of the desk." }, "items": [ { "id": "key", "name": "Golden key", "details": "A small golden key. What use could you have for it?", "destination": "inventory", "triggers": [{ "action": "use", //use on north exit (contextual) "target": { "room": "bigroom", "exit": "north" }, "effect": { "statusUpdate": "unlocked", "target": { "room": "bigroom", "exit": "north" } } } ] } ] }, "bossroom": { "description": { "default": "You appear to have reached the end of the dungeon. There are no exits other than the one you just came in through. The only other thing that bothers you is the hulking giant looking like it's going to kill you, standing about 10 feet from you." }, "npcs": [ { "id": "finalboss", "name": "Hulking Ogre", "details": "A huge, green, muscular giant with a single eye in the middle of his forehead. It doesn't just look bad, it also smells like hell.", "stats": { "hp": 10, "damage": 3 } } ] } } }
{ "graph": [ { "id": "entrance", "name": "Entrance", "north": { "node": "1stroom", "distance": 1 } }, { "id": "1st room", "name": "1st Room", "south": {"node": "entrance", "distance": 1} , "north": { "node": "bigroom", "distance": 1} } , { "id": "bigroom", "name": "Big room", "south": { "node": "1stroom", "distance": 1}, "north": { "node": "bossroom", "distance": 2}, "east": { "node": "rightwing", "distance": 3} , "west": { "node": "leftwing", "distance": 3} }, { "id": "bossroom", "name": "Boss room", "south": {"node": "bigroom", "distance": 2} } { "id": "leftwing", "name": "Left Wing", "east": {"node": "bigroom", "distance": 3} } { "id": "rightwing", "name": "Right Wing", "west": { "node": "bigroom", "distance": 3 } } ], "game": { "win-condition": { "source": "finalboss", "condition": { "type": "comparison", "left": "hp", "right": "0", "symbol": "<=" } }, "lose-condition": { "source": "player", "condition": { "type": "comparison", "left": "hp", "right": "0", "symbol": "<=" } } }, "rooms": { "entrance": { "description": { "default": "You're at the entrance of the dungeon. There are two lit torches on each wall (one on your right and one on your left). You see only one path: ahead." }, "items": [ { "id": "littorch1", "name": "Lit torch on the right", "triggers": [ { "action": "grab", //grab Lit torch on the right "effect":{ "statusUpdate": "has light", "target": "game", } } ] , "destination": "hand" }, { "id": "littorch2", "name": "Lit torch on the left", "triggers": [ { "action": "grab", //grab Lit torch on the left "effect":{ "statusUpdate": "has light", "target": "game", } } ] , "destination": "hand" } ] }, "1stroom": { "description": { "default": "You're in a very dark room. There are no windows and no source of light, other than the one at the entrance. You get the feeling you're not alone here.", "conditionals": { "has light": "The room you find yourself in appears to be empty, aside from a single chair in the right corner. There appears to be only one way out: deeper into the dungeon." } }, "items": [ { "id": "chair", "name": "Wooden chair", "details": "It's a wooden chair, nothing fancy about it. It appears to have been sitting here, untouched, for a while now.", "subitems": [ { "id": "woodenleg", "name": "Wooden leg", "triggeractions": [ { "action": "break", "target": "chair"}, //break { "action": "throw", "target": "chair"} //throw ], "destination": "inventory", "damage": 2 } ] } ] }, "bigroom": { "description": { "default": "You've reached the big room. On every wall are torches lighting every corner. The walls are painted white, and the ceiling is tall and filled with painted white stars on a black background. There is a gateway on either side and a big, wooden double door in front of you." }, "exits": { "north": { "id": "bossdoor", "name": "Big double door", "status": "locked", "details": "A aig, wooden double door. It seems like something big usually comes through here."} }, "items": [] }, "leftwing": { "description": { "default": "Another dark room. It doesn't look like it's that big, but you can't really tell what's inside. You do, however, smell rotten meat somewhere inside.", "conditionals": { "has light": "You appear to have found the kitchen. There are tables full of meat everywhere, and a big knife sticking out of what appears to be the head of a cow." } }, "items": [ { "id": "bigknife", "name": "Big knife", "destination": "inventory", "damage": 10} ] }, "rightwing": { "description": { "default": "This appear to be some sort of office. There is a wooden desk in the middle, torches lighting every wall, and a single key resting on top of the desk." }, "items": [ { "id": "key", "name": "Golden key", "details": "A small golden key. What use could you have for it?", "destination": "inventory", "triggers": [{ "action": "use", //use on north exit (contextual) "target": { "room": "bigroom", "exit": "north" }, "effect": { "statusUpdate": "unlocked", "target": { "room": "bigroom", "exit": "north" } } } ] } ] }, "bossroom": { "description": { "default": "You appear to have reached the end of the dungeon. There are no exits other than the one you just came in through. The only other thing that bothers you is the hulking giant looking like it's going to kill you, standing about 10 feet from you." }, "npcs": [ { "id": "finalboss", "name": "Hulking Ogre", "details": "A huge, green, muscular giant with a single eye in the middle of his forehead. It doesn't just look bad, it also smells like hell.", "stats": { "hp": 10, "damage": 3 } } ] } } }
{ "graph": [ { "id": "entrance", "name": "Entrance", "north": { "node": "1stroom", "distance": 1 } }, { "id": "1st room", "name": "1st Room", "south": {"node": "entrance", "distance": 1} , "north": { "node": "bigroom", "distance": 1} } , { "id": "bigroom", "name": "Big room", "south": { "node": "1stroom", "distance": 1}, "north": { "node": "bossroom", "distance": 2}, "east": { "node": "rightwing", "distance": 3} , "west": { "node": "leftwing", "distance": 3} }, { "id": "bossroom", "name": "Boss room", "south": {"node": "bigroom", "distance": 2} } { "id": "leftwing", "name": "Left Wing", "east": {"node": "bigroom", "distance": 3} } { "id": "rightwing", "name": "Right Wing", "west": { "node": "bigroom", "distance": 3 } } ], "game": { "win-condition": { "source": "finalboss", "condition": { "type": "comparison", "left": "hp", "right": "0", "symbol": "<=" } }, "lose-condition": { "source": "player", "condition": { "type": "comparison", "left": "hp", "right": "0", "symbol": "<=" } } }, "rooms": { "entrance": { "description": { "default": "You're at the entrance of the dungeon. There are two lit torches on each wall (one on your right and one on your left). You see only one path: ahead." }, "items": [ { "id": "littorch1", "name": "Lit torch on the right", "triggers": [ { "action": "grab", //grab Lit torch on the right "effect":{ "statusUpdate": "has light", "target": "game", } } ] , "destination": "hand" }, { "id": "littorch2", "name": "Lit torch on the left", "triggers": [ { "action": "grab", //grab Lit torch on the left "effect":{ "statusUpdate": "has light", "target": "game", } } ] , "destination": "hand" } ] }, "1stroom": { "description": { "default": "You're in a very dark room. There are no windows and no source of light, other than the one at the entrance. You get the feeling you're not alone here.", "conditionals": { "has light": "The room you find yourself in appears to be empty, aside from a single chair in the right corner. There appears to be only one way out: deeper into the dungeon." } }, "items": [ { "id": "chair", "name": "Wooden chair", "details": "It's a wooden chair, nothing fancy about it. It appears to have been sitting here, untouched, for a while now.", "subitems": [ { "id": "woodenleg", "name": "Wooden leg", "triggeractions": [ { "action": "break", "target": "chair"}, //break { "action": "throw", "target": "chair"} //throw ], "destination": "inventory", "damage": 2 } ] } ] }, "bigroom": { "description": { "default": "You've reached the big room. On every wall are torches lighting every corner. The walls are painted white, and the ceiling is tall and filled with painted white stars on a black background. There is a gateway on either side and a big, wooden double door in front of you." }, "exits": { "north": { "id": "bossdoor", "name": "Big double door", "status": "locked", "details": "A aig, wooden double door. It seems like something big usually comes through here."} }, "items": [] }, "leftwing": { "description": { "default": "Another dark room. It doesn't look like it's that big, but you can't really tell what's inside. You do, however, smell rotten meat somewhere inside.", "conditionals": { "has light": "You appear to have found the kitchen. There are tables full of meat everywhere, and a big knife sticking out of what appears to be the head of a cow." } }, "items": [ { "id": "bigknife", "name": "Big knife", "destination": "inventory", "damage": 10} ] }, "rightwing": { "description": { "default": "This appear to be some sort of office. There is a wooden desk in the middle, torches lighting every wall, and a single key resting on top of the desk." }, "items": [ { "id": "key", "name": "Golden key", "details": "A small golden key. What use could you have for it?", "destination": "inventory", "triggers": [{ "action": "use", //use on north exit (contextual) "target": { "room": "bigroom", "exit": "north" }, "effect": { "statusUpdate": "unlocked", "target": { "room": "bigroom", "exit": "north" } } } ] } ] }, "bossroom": { "description": { "default": "You appear to have reached the end of the dungeon. There are no exits other than the one you just came in through. The only other thing that bothers you is the hulking giant looking like it's going to kill you, standing about 10 feet from you." }, "npcs": [ { "id": "finalboss", "name": "Hulking Ogre", "details": "A huge, green, muscular giant with a single eye in the middle of his forehead. It doesn't just look bad, it also smells like hell.", "stats": { "hp": 10, "damage": 3 } } ] } } }
{ "graph": [ { "id": "entrance", "name": "Entrance", "north": { "node": "1stroom", "distance": 1 } }, { "id": "1st room", "name": "1st Room", "south": {"node": "entrance", "distance": 1} , "north": { "node": "bigroom", "distance": 1} } , { "id": "bigroom", "name": "Big room", "south": { "node": "1stroom", "distance": 1}, "north": { "node": "bossroom", "distance": 2}, "east": { "node": "rightwing", "distance": 3} , "west": { "node": "leftwing", "distance": 3} }, { "id": "bossroom", "name": "Boss room", "south": {"node": "bigroom", "distance": 2} } { "id": "leftwing", "name": "Left Wing", "east": {"node": "bigroom", "distance": 3} } { "id": "rightwing", "name": "Right Wing", "west": { "node": "bigroom", "distance": 3 } } ], "game": { "win-condition": { "source": "finalboss", "condition": { "type": "comparison", "left": "hp", "right": "0", "symbol": "<=" } }, "lose-condition": { "source": "player", "condition": { "type": "comparison", "left": "hp", "right": "0", "symbol": "<=" } } }, "rooms": { "entrance": { "description": { "default": "You're at the entrance of the dungeon. There are two lit torches on each wall (one on your right and one on your left). You see only one path: ahead." }, "items": [ { "id": "littorch1", "name": "Lit torch on the right", "triggers": [ { "action": "grab", //grab Lit torch on the right "effect":{ "statusUpdate": "has light", "target": "game", } } ] , "destination": "hand" }, { "id": "littorch2", "name": "Lit torch on the left", "triggers": [ { "action": "grab", //grab Lit torch on the left "effect":{ "statusUpdate": "has light", "target": "game", } } ] , "destination": "hand" } ] }, "1stroom": { "description": { "default": "You're in a very dark room. There are no windows and no source of light, other than the one at the entrance. You get the feeling you're not alone here.", "conditionals": { "has light": "The room you find yourself in appears to be empty, aside from a single chair in the right corner. There appears to be only one way out: deeper into the dungeon." } }, "items": [ { "id": "chair", "name": "Wooden chair", "details": "It's a wooden chair, nothing fancy about it. It appears to have been sitting here, untouched, for a while now.", "subitems": [ { "id": "woodenleg", "name": "Wooden leg", "triggeractions": [ { "action": "break", "target": "chair"}, //break { "action": "throw", "target": "chair"} //throw ], "destination": "inventory", "damage": 2 } ] } ] }, "bigroom": { "description": { "default": "You've reached the big room. On every wall are torches lighting every corner. The walls are painted white, and the ceiling is tall and filled with painted white stars on a black background. There is a gateway on either side and a big, wooden double door in front of you." }, "exits": { "north": { "id": "bossdoor", "name": "Big double door", "status": "locked", "details": "A aig, wooden double door. It seems like something big usually comes through here."} }, "items": [] }, "leftwing": { "description": { "default": "Another dark room. It doesn't look like it's that big, but you can't really tell what's inside. You do, however, smell rotten meat somewhere inside.", "conditionals": { "has light": "You appear to have found the kitchen. There are tables full of meat everywhere, and a big knife sticking out of what appears to be the head of a cow." } }, "items": [ { "id": "bigknife", "name": "Big knife", "destination": "inventory", "damage": 10} ] }, "rightwing": { "description": { "default": "This appear to be some sort of office. There is a wooden desk in the middle, torches lighting every wall, and a single key resting on top of the desk." }, "items": [ { "id": "key", "name": "Golden key", "details": "A small golden key. What use could you have for it?", "destination": "inventory", "triggers": [{ "action": "use", //use on north exit (contextual) "target": { "room": "bigroom", "exit": "north" }, "effect": { "statusUpdate": "unlocked", "target": { "room": "bigroom", "exit": "north" } } } ] } ] }, "bossroom": { "description": { "default": "You appear to have reached the end of the dungeon. There are no exits other than the one you just came in through. The only other thing that bothers you is the hulking giant looking like it's going to kill you, standing about 10 feet from you." }, "npcs": [ { "id": "finalboss", "name": "Hulking Ogre", "details": "A huge, green, muscular giant with a single eye in the middle of his forehead. It doesn't just look bad, it also smells like hell.", "stats": { "hp": 10, "damage": 3 } } ] } } }
Ich weiß, es sieht nach viel aus, aber wenn Sie es auf eine einfache Beschreibung des Spiels reduzieren, haben Sie einen Dungeon, der aus sechs Räumen besteht, von denen jeder mit anderen verbunden ist, wie in der Abbildung oben gezeigt.
Ihre Aufgabe ist es, sich darin zu bewegen und sie zu erkunden. Sie werden feststellen, dass es zwei verschiedene Orte gibt, an denen Sie eine Waffe finden können (entweder in der Küche oder im dunklen Raum, indem Sie den Stuhl zerbrechen). Sie werden auch mit einer verschlossenen Tür konfrontiert; Sobald Sie also den Schlüssel gefunden haben (der sich in dem büroähnlichen Raum befindet), können Sie ihn öffnen und den Boss mit einer beliebigen Waffe bekämpfen, die Sie gesammelt haben.
Sie werden entweder gewinnen, indem Sie es töten, oder verlieren, indem Sie von ihm getötet werden.
Lassen Sie uns nun einen detaillierteren Überblick über die gesamte JSON-Struktur und ihre drei Abschnitte erhalten.
Graph
Dieser enthält die Beziehung zwischen den Knoten. Im Grunde wird dieser Abschnitt direkt in die Grafik übersetzt, die wir uns zuvor angesehen haben.
Die Struktur für diesen Abschnitt ist ziemlich einfach. Es ist eine Liste von Knoten, wobei jeder Knoten die folgenden Attribute umfasst:
- eine ID, die den Knoten unter allen anderen im Spiel eindeutig identifiziert;
- ein Name, der im Grunde eine für Menschen lesbare Version der ID ist;
- eine Reihe von Links zu den anderen Knoten. Dies wird durch die Existenz von vier möglichen Schlüsseln belegt: Norden“, Süden, Osten und Westen. Wir könnten schließlich weitere Richtungen hinzufügen, indem wir Kombinationen dieser vier hinzufügen. Jeder Link enthält die ID des zugehörigen Knotens und die Entfernung (oder Gewichtung) dieser Beziehung.
Spiel
Dieser Abschnitt enthält die allgemeinen Einstellungen und Bedingungen. Im obigen Beispiel enthält dieser Abschnitt insbesondere die Gewinn- und Verlustbedingungen. Mit anderen Worten, mit diesen beiden Bedingungen teilen wir der Engine mit, wann das Spiel enden kann.
Um die Dinge einfach zu halten, habe ich nur zwei Bedingungen hinzugefügt:
- Sie gewinnen entweder, indem Sie den Boss töten,
- oder verlieren, indem Sie getötet werden.
Räume
Hier kommen die meisten der 163 Zeilen her, und es ist der komplexeste der Abschnitte. Hier beschreiben wir alle Räume unseres Abenteuers und alles darin.
Für jedes Zimmer gibt es einen Schlüssel mit der zuvor definierten ID. Und jeder Raum wird eine Beschreibung, eine Liste von Gegenständen, eine Liste von Ausgängen (oder Türen) und eine Liste von nicht spielbaren Charakteren (NPCs) haben. Von diesen Eigenschaften sollte nur die Beschreibung obligatorisch sein, da diese erforderlich ist, damit die Engine Ihnen mitteilt, was Sie sehen. Die anderen werden nur dort sein, wenn es etwas zu zeigen gibt.
Schauen wir uns an, was diese Eigenschaften für unser Spiel tun können.
Die Beschreibung
Dieser Punkt ist nicht so einfach, wie man vielleicht denken mag, da sich Ihre Ansicht eines Raums je nach Umständen ändern kann. Wenn Sie sich beispielsweise die Beschreibung des ersten Raums ansehen, werden Sie feststellen, dass Sie standardmäßig nichts sehen können, es sei denn, Sie haben natürlich eine brennende Taschenlampe dabei.
Das Aufheben und Verwenden von Gegenständen kann also globale Bedingungen auslösen, die sich auf andere Teile des Spiels auswirken.
Die Gegenstände
Diese stellen all die Dinge dar, die Sie in einem Raum finden können. Jedes Element hat die gleiche ID und den gleichen Namen wie die Knoten im Diagrammabschnitt.
Sie haben auch eine „Ziel“-Eigenschaft, die angibt, wo dieser Artikel nach der Abholung aufbewahrt werden soll. Dies ist relevant, da Sie nur einen Gegenstand in Ihren Händen halten können, während Sie in Ihrem Inventar so viele haben können, wie Sie möchten.
Schließlich können einige dieser Gegenstände andere Aktionen oder Statusaktualisierungen auslösen, je nachdem, was der Spieler mit ihnen macht. Ein Beispiel dafür sind die brennenden Fackeln am Eingang. Wenn Sie einen von ihnen greifen, lösen Sie im Spiel eine Statusaktualisierung aus, wodurch das Spiel Ihnen wiederum eine andere Beschreibung des nächsten Raums anzeigt.
Gegenstände können auch „Untergegenstände“ haben, die ins Spiel kommen, sobald der ursprüngliche Gegenstand zerstört wird (z. B. durch die Aktion „Zerbrechen“). Ein Item kann in mehrere Items zerlegt werden, was im Element „subitems“ definiert wird.
Im Wesentlichen ist dieses Element nur eine Reihe neuer Elemente, die auch die Reihe von Aktionen enthält, die ihre Erstellung auslösen können. Dies eröffnet grundsätzlich die Möglichkeit, verschiedene Unterelemente basierend auf den Aktionen zu erstellen, die Sie am ursprünglichen Element ausführen.
Schließlich haben einige Gegenstände eine „Schadens“-Eigenschaft. Wenn Sie also einen Gegenstand verwenden, um einen NPC zu treffen, wird dieser Wert verwendet, um Leben von ihm abzuziehen.
Die Ausgänge
Dies ist einfach eine Reihe von Eigenschaften, die die Richtung des Ausgangs und seine Eigenschaften angeben (eine Beschreibung, falls Sie ihn untersuchen möchten, sein Name und in einigen Fällen sein Status).
Ausgänge sind eine von Elementen getrennte Entität, da die Engine verstehen muss, ob Sie sie basierend auf ihrem Status tatsächlich durchqueren können. Gesperrte Ausgänge lassen Sie nicht passieren, es sei denn, Sie finden heraus, wie Sie ihren Status in entsperrt ändern können.
Die NPCs
Schließlich werden NPCs Teil einer anderen Liste sein. Sie sind im Grunde Elemente mit Statistiken, die die Engine verwendet, um zu verstehen, wie sich jeder einzelne verhalten sollte. Die, die wir in unserem Beispiel definiert haben, sind „hp“, was für Gesundheitspunkte steht, und „damage“, was genau wie die Waffen die Zahl ist, die jeder Treffer von der Gesundheit des Spielers abzieht.
Das war es für den Dungeon, den ich erstellt habe. Es ist viel, ja, und in Zukunft könnte ich darüber nachdenken, eine Art Level-Editor zu erstellen, um die Erstellung der JSON-Dateien zu vereinfachen. Aber vorerst wird das nicht nötig sein.
Falls Sie es noch nicht bemerkt haben, besteht der Hauptvorteil der Definition unseres Spiels in einer solchen Datei darin, dass wir JSON-Dateien wechseln können, so wie Sie es in der Super Nintendo-Ära mit Cartridges getan haben. Laden Sie einfach eine neue Datei hoch und starten Sie ein neues Abenteuer. Leicht!
Abschließende Gedanken
Danke fürs Lesen bisher. Ich hoffe, Ihnen hat der Designprozess gefallen, den ich durchlaufe, um eine Idee zum Leben zu erwecken. Denken Sie jedoch daran, dass ich mir das gerade ausdenke, damit wir später vielleicht erkennen, dass etwas, das wir heute definiert haben, nicht funktionieren wird, in diesem Fall müssen wir zurückgehen und es beheben.
Ich bin mir sicher, dass es eine Menge Möglichkeiten gibt, die hier vorgestellten Ideen zu verbessern und einen verdammt guten Motor zu bauen. Aber das würde viel mehr Worte erfordern, als ich in einen Artikel packen kann, ohne dass es für alle langweilig wird, also belassen wir es erstmal dabei.
Weitere Teile dieser Serie
- Teil 2: Design des Game-Engine-Servers
- Teil 3: Den Terminal-Client erstellen
- Teil 4: Hinzufügen von Chat zu unserem Spiel