Scrierea unui motor de aventură text multiplayer în Node.js (Partea 1)
Publicat: 2022-03-10Aventurile text au fost una dintre primele forme de jocuri de rol digitale, pe vremea când jocurile nu aveau grafică și tot ce aveai era propria ta imaginație și descrierea pe care o citești pe ecranul negru al monitorului CRT.
Dacă vrem să devenim nostalgici, poate că numele Colossal Cave Adventure (sau doar Adventure, așa cum a fost numit inițial) sună un clopoțel. Acesta a fost primul joc de aventură text creat vreodată.

Imaginea de mai sus este modul în care ați vedea de fapt jocul, foarte departe de cele mai bune jocuri de aventură actuale AAA. Acestea fiind spuse, era distractiv de jucat și ți-ar fura sute de ore din timp, în timp ce te așezai în fața acelui text, singur, încercând să-ți dai seama cum să-l învingi.
De înțeles, aventurile text au fost înlocuite de-a lungul anilor cu jocuri care prezintă imagini mai bune (deși, s-ar putea argumenta că multe dintre ele au sacrificat povestea pentru grafică) și, mai ales în ultimii câțiva ani, capacitatea crescândă de a colabora cu alte persoane. prieteni și joacă împreună. Această caracteristică specială este una care le lipsea aventurilor text originale și una pe care vreau să o aduc înapoi în acest articol.
Alte părți ale acestei serii
- Partea 2: Proiectarea serverului motorului de joc
- Partea 3: Crearea clientului terminal
- Partea 4: Adăugarea de chat în jocul nostru
Scopul nostru
Scopul acestui demers, așa cum probabil ați ghicit până acum din titlul acestui articol, este de a crea un motor de aventură text care să vă permită să împărtășiți aventura cu prietenii, permițându-vă să colaborați cu ei în mod similar cum ați face-o în timpul un joc Dungeons & Dragons (în care, la fel ca în vechile aventuri cu text, nu există nicio grafică de văzut).
În crearea motorului, serverul de chat și clientul este destul de multă muncă. În acest articol, vă voi arăta faza de proiectare, explicând lucruri precum arhitectura din spatele motorului, cum va interacționa clientul cu serverele și care vor fi regulile acestui joc.
Doar pentru a vă oferi un ajutor vizual despre cum va arăta, iată scopul meu:

Acesta este scopul nostru. Odată ce ajungem acolo, veți avea capturi de ecran în loc de machete rapide și murdare. Așadar, să ne ocupăm de proces. Primul lucru pe care îl vom acoperi este designul întregului lucru. Apoi, vom acoperi cele mai relevante instrumente pe care le voi folosi pentru a codifica acest lucru. În cele din urmă, vă voi arăta câteva dintre cele mai relevante fragmente de cod (cu un link către depozitul complet, desigur).
Să sperăm că, până la sfârșit, te vei trezi că creezi noi aventuri text pe care să le încerci cu prietenii!
Fază de proiectare
Pentru faza de proiectare, voi acoperi planul nostru general. Voi încerca din răsputeri să nu te plictisesc de moarte, dar, în același timp, cred că este important să arăți unele dintre lucrurile din culise care trebuie să se întâmple înainte de a-ți pune prima linie de cod.
Cele patru componente pe care vreau să le acopăr aici cu o cantitate decentă de detalii sunt:
- Motorul
Acesta va fi serverul principal de joc. Regulile jocului vor fi implementate aici și vor oferi o interfață agnostică din punct de vedere tehnologic pentru orice tip de client de consumat. Vom implementa un client terminal, dar puteți face același lucru cu un client de browser web sau orice alt tip doriți. - Serverul de chat
Deoarece este suficient de complex pentru a avea propriul articol, acest serviciu va avea și propriul său modul. Serverul de chat se va ocupa de a permite jucătorilor să comunice între ei în timpul jocului. - Clientul
După cum am menționat mai devreme, acesta va fi un client terminal, unul care, în mod ideal, va arăta similar cu macheta de mai devreme. Acesta va folosi serviciile oferite atât de motor, cât și de serverul de chat. - Jocuri (fișiere JSON)
În cele din urmă, voi trece peste definiția jocurilor reale. Scopul acestui lucru este de a crea un motor care poate rula orice joc, atâta timp cât fișierul dvs. de joc respectă cerințele motorului. Deci, deși nu va necesita codificare, voi explica cum voi structura fișierele de aventură pentru a ne scrie propriile aventuri în viitor.
Motorul
Motorul de joc sau serverul de joc va fi un API REST și va oferi toate funcționalitățile necesare.
Am optat pentru un API REST pur și simplu pentru că — pentru acest tip de joc — întârzierea adăugată de HTTP și natura sa asincronă nu vor cauza probleme. Totuși, va trebui să mergem pe o altă rută pentru serverul de chat. Dar înainte de a începe să definim punctele finale pentru API-ul nostru, trebuie să definim de ce va fi capabil motorul. Deci, să trecem la asta.
Caracteristică | Descriere |
---|---|
Alăturați-vă unui joc | Un jucător se va putea alătura unui joc specificând ID-ul jocului. |
Creați un joc nou | Un jucător poate crea și o nouă instanță de joc. Motorul ar trebui să returneze un ID, astfel încât alții să îl poată folosi pentru a se alătura. |
Scena de întoarcere | Această funcție ar trebui să returneze scena curentă în care se află petrecerea. Practic, va returna descrierea, cu toate informațiile asociate (acțiuni posibile, obiecte din ea etc.). |
Interacționează cu scena | Acesta va fi unul dintre cele mai complexe, deoarece va lua o comandă de la client și va efectua acea acțiune - lucruri precum mutarea, împingerea, luarea, privirea, citirea, pentru a numi doar câteva. |
Verificați inventarul | Deși aceasta este o modalitate de a interacționa cu jocul, nu are legătură directă cu scena. Deci, verificarea inventarului pentru fiecare jucător va fi considerată o acțiune diferită. |
Un cuvânt despre mișcare
Avem nevoie de o modalitate de a măsura distanțele în joc, deoarece trecerea prin aventură este una dintre acțiunile de bază pe care le poate face un jucător. Vom folosi acest număr ca măsură de timp, doar pentru a simplifica jocul. Măsurarea timpului cu un ceas real ar putea să nu fie cea mai bună, având în vedere că acest tip de jocuri au acțiuni pe rând, cum ar fi lupta. În schimb, vom folosi distanța pentru a măsura timpul (înseamnă că o distanță de 8 va necesita mai mult timp pentru a parcurge decât una din 2, permițându-ne astfel să facem lucruri precum adăugarea de efecte jucătorilor care durează o anumită cantitate de „puncte de distanță” ).
Un alt aspect important de luat în considerare despre mișcare este că nu ne jucăm singuri. De dragul simplității, motorul nu va permite jucătorilor să împartă petrecerea (deși asta ar putea fi o îmbunătățire interesantă pentru viitor). Versiunea inițială a acestui modul va permite tuturor să se miște numai acolo unde decide majoritatea partidului. Deci, mutarea va trebui făcută prin consens, ceea ce înseamnă că fiecare acțiune de mutare va aștepta ca majoritatea partidului să o solicite înainte de a avea loc.
Luptă
Lupta este un alt aspect foarte important al acestor tipuri de jocuri și unul pe care va trebui să luăm în considerare adăugarea motorului nostru; în caz contrar, vom ajunge să pierdem o parte din distracție.
Acesta nu este ceva care trebuie reinventat, să fiu sincer. Lupta de partide pe rând există de zeci de ani, așa că vom implementa doar o versiune a acelei mecanici. Îl vom amesteca cu conceptul Dungeons & Dragons de „inițiativă”, rulând un număr aleatoriu pentru a menține lupta un pic mai dinamică.
Cu alte cuvinte, ordinea în care toți cei implicați într-o luptă își vor alege acțiunea va fi aleatorie, iar aceasta include și inamicii.
În cele din urmă (deși voi trece peste asta mai detaliat mai jos), veți avea articole pe care le puteți ridica cu un număr de „daune” stabilit. Acestea sunt elementele pe care le vei putea folosi în timpul luptei; orice nu are această proprietate va cauza 0 daune inamicilor tăi. Probabil că vom adăuga un mesaj când vei încerca să folosești acele obiecte pentru a lupta, astfel încât să știi că ceea ce încerci să faci nu are sens.
Interacțiunea client-server
Să vedem acum cum ar interacționa un anumit client cu serverul nostru folosind funcționalitatea definită anterior (nu ne gândim încă la punctele finale, dar vom ajunge acolo într-o secundă):

Interacțiunea inițială dintre client și server (din punct de vedere al serverului) este începutul unui nou joc, iar pașii pentru acesta sunt următorii:
- Creați un joc nou .
Clientul solicită crearea unui nou joc de pe server. - Creați o cameră de chat .
Deși numele nu o specifică, serverul nu creează doar o cameră de chat în serverul de chat, ci și configurează tot ce are nevoie pentru a permite unui set de jucători să joace printr-o aventură. - Returnează metadatele jocului .
Odată ce jocul a fost creat de server și camera de chat este amenajată pentru jucători, clientul va avea nevoie de acele informații pentru solicitările ulterioare. Acesta va fi în mare parte un set de ID-uri pe care clienții le pot folosi pentru a se identifica și a jocului curent la care doresc să se alăture (mai multe despre asta într-o secundă). - Partajați manual ID-ul jocului .
Acest pas va trebui să fie făcut chiar de jucătorii. Am putea veni cu un fel de mecanism de partajare, dar îl voi lăsa pe lista de dorințe pentru îmbunătățiri viitoare. - Alăturați-vă jocului .
Acesta este destul de simplu. Odată ce toată lumea are ID-ul jocului, se vor alătura aventurii folosind aplicațiile client. - Alăturați-vă camerei lor de chat .
În cele din urmă, aplicațiile client ale jucătorilor vor folosi metadatele jocului pentru a se alătura camerei de chat a aventurii lor. Acesta este ultimul pas necesar înainte de joc. Odată ce toate acestea sunt făcute, atunci jucătorii sunt gata să înceapă aventura!

Odată ce toate condițiile au fost îndeplinite, jucătorii pot începe să joace aventura, să-și împărtășească gândurile prin chat-ul de petrecere și să avanseze povestea. Diagrama de mai sus prezintă cei patru pași necesari pentru aceasta.
Următorii pași vor rula ca parte a buclei de joc, ceea ce înseamnă că se vor repeta în mod constant până la sfârșitul jocului.
- Solicitați scena .
Aplicația client va solicita metadatele pentru scena curentă. Acesta este primul pas în fiecare iterație a buclei. - Returnează metadatele .
Serverul, la rândul său, va trimite înapoi metadatele pentru scena curentă. Aceste informații vor include lucruri precum o descriere generală, obiectele găsite în interiorul ei și modul în care se leagă între ele. - Trimite comanda .
Aici începe distracția. Aceasta este intrarea principală a jucătorului. Acesta va conține acțiunea pe care doresc să o efectueze și, opțional, ținta acelei acțiuni (de exemplu, suflați în lumânare, apucați piatra și așa mai departe). - Returnați reacția la comanda trimisă .
Acesta ar putea fi pur și simplu pasul doi, dar pentru claritate, l-am adăugat ca pas suplimentar. Principala diferență este că pasul doi ar putea fi considerat începutul acestei bucle, în timp ce acesta ia în considerare faptul că jucați deja și, prin urmare, serverul trebuie să înțeleagă pe cine va afecta această acțiune (fie un singur jucător). sau toți jucătorii).
Ca un pas suplimentar, deși nu face parte cu adevărat din flux, serverul va notifica clienții despre actualizările de stare care sunt relevante pentru ei.
Motivul pentru acest pas suplimentar recurent este din cauza actualizărilor pe care un jucător le poate primi din acțiunile altor jucători. Amintiți-vă cerința de a vă muta dintr-un loc în altul; după cum am spus mai înainte, odată ce majoritatea jucătorilor au ales o direcție, atunci toți jucătorii se vor mișca (nu este necesară nicio intervenție din partea tuturor jucătorilor).
Partea interesantă aici este că HTTP (am menționat deja că serverul va fi un API REST) nu permite acest tip de comportament. Deci, opțiunile noastre sunt:
- efectuează sondaje la fiecare X secunde de la client,
- utilizați un fel de sistem de notificare care funcționează în paralel cu conexiunea client-server.
Din experiența mea, tind să prefer opțiunea 2. De fapt, aș folosi (și voi pentru acest articol) Redis pentru acest tip de comportament.
Următoarea diagramă demonstrează dependențele dintre servicii.

Serverul de chat
Voi lăsa detaliile designului acestui modul pentru faza de dezvoltare (care nu face parte din acest articol). Acestea fiind spuse, sunt lucruri pe care le putem decide.
Un lucru pe care îl putem defini este setul de restricții pentru server, care ne va simplifica munca pe linie. Și dacă ne jucăm cărțile corect, s-ar putea să ajungem la un serviciu care oferă o interfață robustă, permițându-ne astfel, în cele din urmă, să extindem sau chiar să schimbăm implementarea pentru a oferi mai puține restricții fără a afecta deloc jocul.
- Va fi o singură cameră per petrecere.
Nu vom lăsa să fie create subgrupuri. Acest lucru merge mână în mână cu a nu lăsa partidul să se despartă. Poate că, odată ce implementăm această îmbunătățire, să permitem crearea de subgrupuri și săli de chat personalizate ar fi o idee bună. - Nu vor exista mesaje private.
Acest lucru este doar în scopuri de simplificare, dar a avea un chat de grup este deja suficient de bun; nu avem nevoie de mesaje private acum. Amintiți-vă că ori de câte ori lucrați la produsul dvs. minim viabil, încercați să evitați să treceți prin târgul iepurelui de caracteristici inutile; este o cale periculoasă și din care e greu de ieșit. - Nu vom persista mesajele.
Cu alte cuvinte, dacă părăsești petrecerea, vei pierde mesajele. Acest lucru ne va simplifica enorm sarcina, pentru că nu va trebui să ne ocupăm de niciun tip de stocare a datelor și nici nu va trebui să pierdem timpul decidend asupra celei mai bune structuri de date pentru stocarea și recuperarea mesajelor vechi. Totul va rămâne în memorie și va rămâne acolo atâta timp cât camera de chat este activă. Odată ce este închis, pur și simplu ne vom lua rămas bun de la ei! - Comunicarea se va face prin prize .
Din păcate, clientul nostru va trebui să gestioneze un canal de comunicare dublu: unul RESTful pentru motorul de joc și un socket pentru serverul de chat. Acest lucru ar putea crește puțin complexitatea clientului, dar, în același timp, va folosi cele mai bune metode de comunicare pentru fiecare modul. (Nu are rost să forțați REST pe serverul nostru de chat sau să forțați socket-uri pe serverul nostru de joc. Această abordare ar crește complexitatea codului de pe server, care este cel care se ocupă și de logica de afaceri, așa că să ne concentrăm pe acea parte. pentru acum.)
Asta e pentru serverul de chat. La urma urmei, nu va fi complex, cel puțin nu inițial. Există mai multe de făcut atunci când este timpul să începeți să îl codați, dar pentru acest articol, este o informație mai mult decât suficientă.
Clientul
Acesta este modulul final care necesită codare și va fi cel mai prost din mulți. Ca regulă generală, prefer să-mi am clienții proști și serverele inteligente. În acest fel, crearea de noi clienți pentru server devine mult mai ușoară.
Doar ca să fim pe aceeași pagină, iată arhitectura de nivel înalt cu care ar trebui să ajungem.

Clientul nostru simplu ClI nu va implementa nimic foarte complex. De fapt, cel mai complicat lucru pe care va trebui să-l abordăm este interfața de utilizare reală, deoarece este o interfață bazată pe text.
Acestea fiind spuse, funcționalitatea pe care va trebui să o implementeze aplicația client este următoarea:
- Creați un joc nou .
Pentru că vreau să păstrez lucrurile cât mai simple posibil, acest lucru se va face doar prin interfața CLI. Interfața de utilizare reală va fi folosită numai după alăturarea unui joc, ceea ce ne duce la următorul punct. - Alăturați-vă unui joc existent .
Având în vedere codul jocului returnat de la punctul anterior, jucătorii îl pot folosi pentru a se alătura. Din nou, acesta este ceva pe care ar trebui să îl puteți face fără o interfață de utilizare, așa că această funcționalitate va face parte din procesul necesar pentru a începe să utilizați interfața de utilizare text. - Analizați fișierele de definiție a jocului .
Vom discuta despre acestea puțin, dar clientul ar trebui să poată înțelege aceste fișiere pentru a ști ce să arate și să știe cum să folosească acele date. - Interacționează cu aventura.
Practic, acest lucru oferă jucătorului capacitatea de a interacționa cu mediul descris la un moment dat. - Menține un inventar pentru fiecare jucător .
Fiecare instanță a clientului va conține o listă de articole în memorie. Această listă va fi copiată de rezervă. - Chat de asistență .
Aplicația client trebuie să se conecteze și la serverul de chat și să conecteze utilizatorul în camera de chat a părții.
Mai multe despre structura internă și designul clientului mai târziu. Între timp, să terminăm etapa de proiectare cu ultimul pic de pregătire: fișierele jocului.
Jocul: fișiere JSON
Aici devine interesant, deoarece până acum am acoperit definițiile de bază ale microserviciilor. Unii dintre ei ar putea vorbi REST, iar alții ar putea funcționa cu socket-uri, dar în esență, toate sunt la fel: le definiți, le codificați și ei oferă un serviciu.
Pentru această componentă anume, nu plănuiesc să codific nimic, dar trebuie să o proiectăm. Practic, implementăm un fel de protocol pentru definirea jocului nostru, a scenelor din el și a tot ceea ce este în interiorul lor.
Dacă te gândești bine, o aventură text este, în esență, un set de camere conectate între ele, iar în interiorul lor se află „lucruri” cu care poți interacționa, toate legate între ele cu o poveste, sperăm, decentă. Acum, motorul nostru nu se va ocupa de ultima parte; acea parte va depinde de tine. Dar pentru restul, există speranță.
Acum, revenind la setul de camere interconectate, asta mi se pare un grafic, iar dacă adăugăm și conceptul de distanță sau viteză de mișcare pe care l-am menționat mai devreme, avem un grafic ponderat. Și acesta este doar un set de noduri care au o greutate (sau doar un număr - nu vă faceți griji despre cum se numește) care reprezintă calea dintre ele. Iată o imagine vizuală (îmi place să învăț văzând, așa că uită-te doar la imagine, OK?):

Acesta este un grafic ponderat - asta este. Și sunt sigur că v-ați dat deja seama, dar pentru a fi complet, permiteți-mi să vă arăt cum ați proceda după ce motorul nostru este gata.
Odată ce începeți să configurați aventura, vă veți crea harta (cum vedeți în stânga imaginii de mai jos). Și apoi îl vei traduce într-un grafic ponderat, așa cum poți vedea în partea dreaptă a imaginii. Motorul nostru îl va putea ridica și vă va permite să treceți prin el în ordinea corectă.

Cu graficul ponderat de mai sus, ne putem asigura că jucătorii nu pot merge de la intrare până în aripa stângă. Ar trebui să treacă prin nodurile dintre cele două, iar acest lucru va consuma timp, pe care îl putem măsura folosind greutatea conexiunilor.
Acum, la partea „distracției”. Să vedem cum ar arăta graficul în format JSON. Ai grijă de mine aici; acest JSON va conține multe informații, dar voi parcurge cât de mult pot:
{ "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 } } ] } } }
Știu că arată mult, dar dacă o rezumați la o simplă descriere a jocului, aveți o temniță cu șase camere, fiecare interconectată cu altele, așa cum se arată în diagrama de mai sus.

Sarcina ta este să treci prin ea și să o explorezi. Veți descoperi că există două locuri diferite unde puteți găsi o armă (fie în bucătărie, fie în camera întunecată, rupând scaunul). De asemenea, te vei confrunta cu o ușă încuiată; așa că, odată ce vei găsi cheia (situată în interiorul unei camere asemănătoare biroului), vei putea să o deschizi și să te lupți cu șeful cu orice armă pe care ai adunat-o.
Fie vei câștiga ucigându-l, fie vei pierde dacă vei fi ucis de el.
Să intrăm acum într-o prezentare mai detaliată a întregii structuri JSON și a celor trei secțiuni ale acesteia.
Grafic
Acesta va conține relația dintre noduri. Practic, această secțiune se traduce direct în graficul la care ne-am uitat înainte.
Structura acestei secțiuni este destul de simplă. Este o listă de noduri, în care fiecare nod cuprinde următoarele atribute:
- un ID care identifică în mod unic nodul printre toate celelalte din joc;
- un nume, care este practic o versiune a ID-ului care poate fi citită de om;
- un set de legături către celelalte noduri. Acest lucru este dovedit de existența a patru chei posibile: nord”, sud, est și vest. În cele din urmă, am putea adăuga direcții suplimentare adăugând combinații ale acestor patru. Fiecare link conține ID-ul nodului aferent și distanța (sau greutatea) relației respective.
Joc
Această secțiune va conține setările și condițiile generale. În special, în exemplul de mai sus, această secțiune conține condițiile de câștig și pierdere. Cu alte cuvinte, cu aceste două condiții, vom anunța motorul când se poate termina jocul.
Pentru a simplifica lucrurile, am adăugat doar două condiții:
- fie câștigi ucigând șeful,
- sau pierzi fiind ucis.
Camerele
De aici provin majoritatea celor 163 de linii și este cea mai complexă dintre secțiuni. Aici vom descrie toate camerele din aventura noastră și tot ce este în interiorul lor.
Va exista o cheie pentru fiecare cameră, folosind ID-ul pe care l-am definit anterior. Și fiecare cameră va avea o descriere, o listă de articole, o listă de ieșiri (sau uși) și o listă de personaje nejucabile (NPC-uri). Dintre acele proprietăți, singura care ar trebui să fie obligatorie este descrierea, pentru că aceasta este necesară pentru ca motorul să vă spună ce vedeți. Restul vor fi acolo doar dacă există ceva de arătat.
Să vedem ce pot face aceste proprietăți pentru jocul nostru.
Descrierea
Acest articol nu este atât de simplu pe cât s-ar putea crede, deoarece vederea dvs. asupra unei camere se poate schimba în funcție de diferite circumstanțe. Dacă, de exemplu, te uiți la descrierea primei camere, vei observa că, implicit, nu poți vedea nimic, decât dacă, desigur, ai o torță aprinsă cu tine.
Deci, ridicarea obiectelor și utilizarea acestora ar putea declanșa condiții globale care vor afecta alte părți ale jocului.
Obiectele
Acestea reprezintă toate lucrurile” pe care le puteți găsi în interiorul unei camere. Fiecare articol are același ID și același nume pe care le-au avut nodurile din secțiunea de grafic.
Ei vor avea, de asemenea, o proprietate „destinație”, care indică locul în care ar trebui să fie depozitat acel articol, odată ridicat. Acest lucru este relevant deoarece veți putea avea un singur articol în mâini, în timp ce veți putea avea în inventar atâtea câte doriți.
În cele din urmă, unele dintre aceste elemente ar putea declanșa alte acțiuni sau actualizări de stare, în funcție de ceea ce jucătorul decide să facă cu ele. Un exemplu în acest sens sunt torțele aprinse de la intrare. Dacă prindeți unul dintre ele, veți declanșa o actualizare de stare în joc, care, la rândul său, va face ca jocul să vă arate o descriere diferită a camerei următoare.
Articolele pot avea, de asemenea, „subarticole”, care intră în joc odată ce elementul original este distrus (prin acțiunea „pauză”, de exemplu). Un articol poate fi împărțit în mai multe, iar acesta este definit în elementul „subitemi”.
În esență, acest element este doar o serie de elemente noi, unul care conține și setul de acțiuni care pot declanșa crearea lor. Practic, acest lucru deschide posibilitatea de a crea diferite subarticole pe baza acțiunilor pe care le efectuați asupra articolului original.
În cele din urmă, unele articole vor avea o proprietate de „daune”. Deci, dacă folosiți un element pentru a lovi un NPC, acea valoare va fi folosită pentru a scădea viața din el.
Ieșirile
Acesta este pur și simplu un set de proprietăți care indică direcția de ieșire și proprietățile acesteia (o descriere, în cazul în care doriți să o inspectați, numele și, în unele cazuri, starea acesteia).
Ieșirile sunt o entitate separată de articole, deoarece motorul va trebui să înțeleagă dacă le puteți traversa în funcție de starea lor. Ieșirile care sunt blocate nu vă vor permite să treceți prin ele decât dacă aflați cum să le schimbați starea în deblocat.
NPC-urile
În cele din urmă, NPC-urile vor face parte dintr-o altă listă. Ele sunt practic articole cu statistici pe care motorul le va folosi pentru a înțelege cum ar trebui să se comporte fiecare. Cele pe care le-am definit în exemplul nostru sunt „hp”, care înseamnă puncte de sănătate și „daune”, care, la fel ca și armele, este numărul pe care fiecare lovitură îl va scădea din sănătatea jucătorului.
Asta este pentru temnița pe care am creat-o. Este mult, da, și în viitor aș putea lua în considerare crearea unui fel de editor de nivel, pentru a simplifica crearea fișierelor JSON. Dar deocamdată nu va fi necesar.
În cazul în care nu ți-ai dat seama încă, principalul avantaj de a avea jocul nostru definit într-un fișier ca acesta este că vom putea schimba fișierele JSON așa cum ai făcut cu cartușele în era Super Nintendo. Încărcați un fișier nou și începeți o nouă aventură. Uşor!
Gânduri de închidere
Mulțumesc că ai citit până acum. Sper că v-a plăcut procesul de design prin care trec pentru a da viață unei idei. Amintiți-vă, totuși, că inventez asta pe măsură ce merg, așa că s-ar putea să realizăm mai târziu că ceva definit astăzi nu va funcționa, caz în care va trebui să dam înapoi și să-l reparăm.
Sunt sigur că există o mulțime de moduri de a îmbunătăți ideile prezentate aici și de a face un motor al naibii. Dar asta ar necesita mult mai multe cuvinte decât pot pune într-un articol, fără a-l face plictisitor pentru toată lumea, așa că o vom lăsa așa deocamdată.
Alte părți ale acestei serii
- Partea 2: Proiectarea serverului motorului de joc
- Partea 3: Crearea clientului terminal
- Partea 4: Adăugarea de chat în jocul nostru