Scrivere un motore di avventura testuale multigiocatore in Node.js (parte 1)

Pubblicato: 2022-03-10
Riassunto veloce ↬ Mai sentito parlare di un'avventura testuale? In questa serie di articoli, Fernando Doglio spiega il processo di creazione di un intero motore in grado di farti giocare a qualsiasi avventura testuale che tu e i tuoi amici apprezzino. Esatto, lo ravviveremo un po' aggiungendo il multiplayer al genere dell'avventura testuale!

Le avventure testuali sono state una delle prime forme di giochi di ruolo digitali là fuori, quando i giochi non avevano grafica e tutto ciò che avevi era la tua immaginazione e la descrizione che leggi sullo schermo nero del tuo monitor CRT.

Se vogliamo avere nostalgia, forse il nome Colossal Cave Adventure (o semplicemente Adventure, come era originariamente chiamato) suona un campanello. Quello è stato il primo gioco di avventura testuale mai realizzato.

Un'immagine di una vera avventura testuale di un tempo
Un'immagine di una vera avventura testuale di un tempo. (Grande anteprima)

L'immagine sopra è come vedresti effettivamente il gioco, ben lontano dai nostri attuali migliori giochi di avventura AAA. Detto questo, erano divertenti da suonare e ti rubavano centinaia di ore del tuo tempo, mentre ti sedevi davanti a quel testo, da solo, cercando di capire come batterlo.

Comprensibilmente, le avventure testuali sono state sostituite nel corso degli anni da giochi che presentano una grafica migliore (anche se si potrebbe sostenere che molti di loro hanno sacrificato la storia per la grafica) e, soprattutto negli ultimi anni, la crescente capacità di collaborare con altri amici e giocare insieme. Questa caratteristica particolare è quella che mancava alle avventure testuali originali e che voglio riportare in questo articolo.

Altre parti di questa serie

  • Parte 2: Progettazione del server del motore di gioco
  • Parte 3: Creazione del client terminale
  • Parte 4: aggiungere chat al nostro gioco

Altro dopo il salto! Continua a leggere sotto ↓

Il nostro obbiettivo

Lo scopo di questo sforzo, come probabilmente avrai già intuito dal titolo di questo articolo, è creare un motore di avventura testuale che ti permetta di condividere l'avventura con gli amici, consentendoti di collaborare con loro in modo simile a come faresti durante un gioco Dungeons & Dragons (in cui, proprio come nelle vecchie avventure testuali, non ci sono elementi grafici da guardare).

Nella creazione del motore, del server di chat e del client è un bel lavoro. In questo articolo, ti mostrerò la fase di progettazione, spiegando cose come l'architettura dietro il motore, come il client interagirà con i server e quali saranno le regole di questo gioco.

Solo per darti un aiuto visivo su come sarà, ecco il mio obiettivo:

Wireframe generale per l'interfaccia utente finale del client di gioco
Wireframe generale per l'interfaccia utente finale del client di gioco (anteprima grande)

Questo è il nostro obiettivo. Una volta arrivati, avrai screenshot invece di prototipi veloci e sporchi. Quindi, scendiamo con il processo. La prima cosa che tratteremo è il design dell'intera cosa. Quindi, tratteremo gli strumenti più rilevanti che userò per codificare questo. Infine, ti mostrerò alcuni dei bit di codice più rilevanti (con un collegamento al repository completo, ovviamente).

Se tutto va bene, alla fine ti ritroverai a creare nuove avventure testuali da provare con gli amici!

Fase di progettazione

Per la fase di progettazione, tratterò il nostro progetto generale. Farò del mio meglio per non annoiarti a morte, ma allo stesso tempo, penso che sia importante mostrare alcune delle cose dietro le quinte che devono accadere prima di stabilire la tua prima riga di codice.

I quattro componenti che voglio trattare qui con una discreta quantità di dettagli sono:

  • Il motore
    Questo sarà il server di gioco principale. Le regole del gioco verranno implementate qui e fornirà un'interfaccia tecnologicamente agnostica per qualsiasi tipo di client da utilizzare. Implementeremo un client terminale, ma potresti fare lo stesso con un client browser Web o qualsiasi altro tipo che desideri.
  • Il server di chat
    Poiché è abbastanza complesso da avere un proprio articolo, anche questo servizio avrà un proprio modulo. Il server di chat si occuperà di consentire ai giocatori di comunicare tra loro durante il gioco.
  • Il cliente
    Come affermato in precedenza, questo sarà un client terminale, uno che, idealmente, sarà simile al mockup di prima. Utilizzerà i servizi forniti sia dal motore che dal server di chat.
  • Giochi (file JSON)
    Infine, esaminerò la definizione dei giochi reali. Il punto centrale di questo è creare un motore in grado di eseguire qualsiasi gioco, purché il file di gioco soddisfi i requisiti del motore. Quindi, anche se questo non richiederà la codifica, spiegherò come strutturerò i file dell'avventura per scrivere le nostre avventure in futuro.

Il motore

Il motore di gioco, o server di gioco, sarà un'API REST e fornirà tutte le funzionalità richieste.

Ho scelto un'API REST semplicemente perché, per questo tipo di gioco, il ritardo aggiunto da HTTP e la sua natura asincrona non causeranno alcun problema. Tuttavia, dovremo seguire un percorso diverso per il server di chat. Ma prima di iniziare a definire gli endpoint per la nostra API, dobbiamo definire di cosa sarà capace il motore. Quindi, andiamo a questo.

Caratteristica Descrizione
Partecipa a una partita Un giocatore potrà partecipare a una partita specificando l'ID della partita.
Crea un nuovo gioco Un giocatore può anche creare una nuova istanza di gioco. Il motore dovrebbe restituire un ID, in modo che altri possano usarlo per partecipare.
Scena di ritorno Questa funzione dovrebbe restituire la scena corrente in cui si trova la festa. Fondamentalmente, restituirà la descrizione, con tutte le informazioni associate (possibili azioni, oggetti in essa contenuti, ecc.).
Interagisci con la scena Questo sarà uno dei più complessi, perché richiederà un comando dal client ed eseguirà quell'azione: cose come spostare, spingere, prendere, guardare, leggere, solo per citarne alcuni.
Controlla l'inventario Sebbene questo sia un modo per interagire con il gioco, non è direttamente correlato alla scena. Quindi, il controllo dell'inventario per ogni giocatore sarà considerato un'azione diversa.

Una parola sul movimento

Abbiamo bisogno di un modo per misurare le distanze nel gioco perché muoversi nell'avventura è una delle azioni principali che un giocatore può intraprendere. Useremo questo numero come misura del tempo, solo per semplificare il gameplay. Misurare il tempo con un orologio reale potrebbe non essere il massimo, considerando che questo tipo di giochi ha azioni a turni, come il combattimento. Invece, useremo la distanza per misurare il tempo (il che significa che una distanza di 8 richiederà più tempo per attraversare di una di 2, permettendoci così di fare cose come aggiungere effetti ai giocatori che durano per un determinato numero di "punti di distanza" ).

Un altro aspetto importante da considerare riguardo al movimento è che non stiamo giocando da soli. Per semplicità, il motore non consentirà ai giocatori di dividere la festa (anche se potrebbe essere un miglioramento interessante per il futuro). La versione iniziale di questo modulo consentirà a tutti di spostarsi ovunque decida la maggioranza del partito. Quindi, il trasferimento dovrà essere fatto per consenso, il che significa che ogni azione di movimento attenderà che la maggioranza del partito lo richieda prima di aver luogo.

Combattere

Il combattimento è un altro aspetto molto importante di questi tipi di giochi e dovremo considerare di aggiungere al nostro motore; altrimenti, finiremo per perdere parte del divertimento.

Questo non è qualcosa che deve essere reinventato, ad essere onesti. Il combattimento di gruppo a turni esiste da decenni, quindi implementeremo solo una versione di quella meccanica. Lo mescoleremo con il concetto di "iniziativa" di Dungeons & Dragons, tirando un numero casuale per mantenere il combattimento un po' più dinamico.

In altre parole, l'ordine in cui tutte le persone coinvolte in un combattimento possono scegliere la propria azione sarà randomizzato, inclusi i nemici.

Infine (sebbene lo esaminerò più in dettaglio di seguito), avrai oggetti che puoi raccogliere con un numero di "danno" impostato. Questi sono gli oggetti che potrai usare durante il combattimento; tutto ciò che non ha quella proprietà causerà 0 danni ai tuoi nemici. Probabilmente aggiungeremo un messaggio quando proverai a usare quegli oggetti per combattere, in modo che tu sappia che quello che stai cercando di fare non ha senso.

Interazione client-server

Vediamo ora come un determinato client interagirebbe con il nostro server utilizzando la funzionalità precedentemente definita (non pensando ancora agli endpoint, ma ci arriveremo tra un secondo):

(Grande anteprima)

L'interazione iniziale tra il client e il server (dal punto di vista del server) è l'inizio di una nuova partita e i passaggi per essa sono i seguenti:

  1. Crea un nuovo gioco .
    Il client richiede la creazione di un nuovo gioco dal server.
  2. Crea chat room .
    Sebbene il nome non lo specifichi, il server non sta solo creando una chat room nel server di chat, ma sta anche configurando tutto ciò di cui ha bisogno per consentire a un gruppo di giocatori di giocare attraverso un'avventura.
  3. Restituisci i metadati del gioco .
    Una volta che il gioco è stato creato dal server e la chat room è pronta per i giocatori, il client avrà bisogno di tali informazioni per le richieste successive. Questo sarà principalmente un insieme di ID che i clienti possono utilizzare per identificare se stessi e il gioco corrente a cui vogliono partecipare (ne parleremo più in un secondo).
  4. Condividi manualmente l'ID di gioco .
    Questo passaggio dovrà essere fatto dai giocatori stessi. Potremmo trovare una sorta di meccanismo di condivisione, ma lo lascerò nella lista dei desideri per miglioramenti futuri.
  5. Unisciti al gioco .
    Questo è piuttosto semplice. Una volta che tutti hanno l'ID di gioco, si uniranno all'avventura utilizzando le loro applicazioni client.
  6. Entra nella loro chat room .
    Infine, le app client dei giocatori utilizzeranno i metadati del gioco per entrare nella chat room della loro avventura. Questo è l'ultimo passaggio richiesto prima del gioco. Una volta fatto tutto, i giocatori sono pronti per iniziare l'avventura!
Ordine di azione per un gioco esistente
Ordine di azione per un gioco esistente (anteprima grande)

Una volta soddisfatti tutti i prerequisiti, i giocatori possono iniziare a giocare all'avventura, condividere i propri pensieri attraverso la chat di gruppo e far avanzare la storia. Il diagramma sopra mostra i quattro passaggi necessari per questo.

I seguenti passaggi verranno eseguiti come parte del ciclo di gioco, il che significa che verranno ripetuti costantemente fino alla fine del gioco.

  1. Richiedi scena .
    L'app client richiederà i metadati per la scena corrente. Questo è il primo passo in ogni iterazione del ciclo.
  2. Restituisce i metadati .
    Il server, a sua volta, invierà i metadati per la scena corrente. Queste informazioni includeranno cose come una descrizione generale, gli oggetti che si trovano al suo interno e il modo in cui si relazionano tra loro.
  3. Invia comando .
    È qui che inizia il divertimento. Questo è l'input principale del giocatore. Conterrà l'azione che vogliono eseguire e, facoltativamente, l'obiettivo di quell'azione (ad esempio, soffiare una candela, afferrare il sasso e così via).
  4. Restituisce la reazione al comando inviato .
    Questo potrebbe essere semplicemente il passaggio due, ma per chiarezza, l'ho aggiunto come passaggio aggiuntivo. La differenza principale è che il secondo passaggio potrebbe essere considerato l'inizio di questo ciclo, mentre questo tiene conto del fatto che stai già giocando e, quindi, il server deve capire chi influenzerà questa azione (un giocatore singolo o tutti i giocatori).

Come passaggio aggiuntivo, sebbene non faccia realmente parte del flusso, il server notificherà ai client gli aggiornamenti di stato che sono rilevanti per loro.

Il motivo di questo passaggio aggiuntivo ricorrente è dovuto agli aggiornamenti che un giocatore può ricevere dalle azioni di altri giocatori. Richiamare l'obbligo di spostarsi da un luogo all'altro; come ho detto prima, una volta che la maggior parte dei giocatori ha scelto una direzione, tutti i giocatori si muoveranno (non è richiesto alcun input da parte di tutti i giocatori).

La parte interessante qui è che HTTP (abbiamo già menzionato che il server sarà un'API REST) ​​non consente questo tipo di comportamento. Quindi, le nostre opzioni sono:

  1. eseguire il polling ogni X secondi dal client,
  2. utilizzare una sorta di sistema di notifica che funziona in parallelo con la connessione client-server.

Nella mia esperienza, tendo a preferire l'opzione 2. In effetti, userei (e lo farò per questo articolo) Redis per questo tipo di comportamento.

Il diagramma seguente illustra le dipendenze tra i servizi.

Interazioni tra un'app client e il motore di gioco
Interazioni tra un'app client e il motore di gioco (anteprima grande)

Il Chat Server

Lascerò i dettagli della progettazione di questo modulo per la fase di sviluppo (che non fa parte di questo articolo). Detto questo, ci sono cose che possiamo decidere.

Una cosa che possiamo definire è l'insieme delle restrizioni per il server, che semplificherà il nostro lavoro su tutta la linea. E se giochiamo bene le nostre carte, potremmo ritrovarci con un servizio che fornisce un'interfaccia robusta, consentendoci così di estendere o addirittura modificare l'implementazione per fornire meno restrizioni senza influire affatto sul gioco.

  • Ci sarà solo una stanza per festa.
    Non permetteremo la creazione di sottogruppi. Questo va di pari passo con il non lasciare che il partito si divida. Forse una volta implementato quel miglioramento, consentire la creazione di sottogruppi e chat room personalizzate sarebbe una buona idea.
  • Non ci saranno messaggi privati.
    Questo è puramente a scopo di semplificazione, ma avere una chat di gruppo è già abbastanza buono; non abbiamo bisogno di messaggi privati ​​in questo momento. Ricorda che ogni volta che stai lavorando sul tuo prodotto minimo praticabile, cerca di evitare di andare nella tana del coniglio di funzionalità non necessarie; è un percorso pericoloso e da cui è difficile uscire.
  • Non persisteremo i messaggi.
    In altre parole, se lasci la festa, perderai i messaggi. Ciò semplificherà enormemente il nostro compito, perché non dovremo occuparci di alcun tipo di archiviazione dati, né dovremo perdere tempo a decidere la migliore struttura dati per archiviare e recuperare vecchi messaggi. Vivrà tutto nella memoria e rimarrà lì finché la chat room sarà attiva. Una volta chiuso, li saluteremo semplicemente!
  • La comunicazione avverrà tramite le prese .
    Purtroppo, il nostro client dovrà gestire un doppio canale di comunicazione: uno RESTful per il motore di gioco e un socket per il server di chat. Ciò potrebbe aumentare un po' la complessità del client, ma allo stesso tempo utilizzerà i migliori metodi di comunicazione per ogni modulo. (Non ha senso forzare REST sul nostro server di chat o forzare i socket sul nostro server di gioco. Questo approccio aumenterebbe la complessità del codice lato server, che è anche quello che gestisce la logica aziendale, quindi concentriamoci su quel lato per adesso.)

Questo è tutto per il server di chat. Dopotutto, non sarà complesso, almeno non inizialmente. C'è altro da fare quando è il momento di iniziare a codificarlo, ma per questo articolo sono informazioni più che sufficienti.

Il cliente

Questo è l'ultimo modulo che richiede la codifica e sarà il nostro più stupido del lotto. Come regola generale, preferisco che i miei clienti siano stupidi e che i miei server siano intelligenti. In questo modo, la creazione di nuovi client per il server diventa molto più semplice.

Solo così siamo sulla stessa pagina, ecco l'architettura di alto livello con cui dovremmo finire.

Architettura finale di alto livello dell'intero sviluppo
Architettura finale di alto livello dell'intero sviluppo (Anteprima Large)

Il nostro semplice client CLI non implementerà nulla di molto complesso. In effetti, la parte più complicata che dovremo affrontare è l'interfaccia utente reale, perché è un'interfaccia basata su testo.

Detto questo, la funzionalità che l'applicazione client dovrà implementare è la seguente:

  1. Crea un nuovo gioco .
    Poiché voglio mantenere le cose il più semplici possibile, questo verrà fatto solo tramite l'interfaccia CLI. L'interfaccia utente effettiva verrà utilizzata solo dopo aver partecipato a una partita, il che ci porta al punto successivo.
  2. Partecipa a un gioco esistente .
    Dato il codice del gioco restituito dal punto precedente, i giocatori possono usarlo per partecipare. Ancora una volta, questo è qualcosa che dovresti essere in grado di fare senza un'interfaccia utente, quindi questa funzionalità farà parte del processo necessario per iniziare a utilizzare l'interfaccia utente di testo.
  3. Analizza i file di definizione del gioco .
    Ne discuteremo tra poco, ma il cliente dovrebbe essere in grado di comprendere questi file per sapere cosa mostrare e sapere come usare quei dati.
  4. Interagisci con l'avventura.
    Fondamentalmente, questo dà al giocatore la possibilità di interagire con l'ambiente descritto in qualsiasi momento.
  5. Mantieni un inventario per ogni giocatore .
    Ogni istanza del client conterrà un elenco di elementi in memoria. Verrà eseguito il backup di questo elenco.
  6. Chat di supporto .
    L'app client deve anche connettersi al server di chat e accedere all'utente nella chat room della parte.

Maggiori informazioni sulla struttura interna del cliente e sul design in seguito. Nel frattempo, concludiamo la fase di progettazione con l'ultimo pezzo di preparazione: i file di gioco.

Il gioco: file JSON

È qui che diventa interessante perché fino ad ora ho trattato le definizioni di microservizi di base. Alcuni di loro potrebbero parlare REST e altri potrebbero funzionare con i socket, ma in sostanza sono tutti uguali: li definisci, li codifichi e forniscono un servizio.

Per questo particolare componente, non ho intenzione di codificare nulla, ma dobbiamo progettarlo. Fondamentalmente, stiamo implementando una sorta di protocollo per definire il nostro gioco, le scene al suo interno e tutto ciò che contiene.

Se ci pensi, un'avventura testuale è, fondamentalmente, un insieme di stanze collegate tra loro, e al loro interno ci sono "cose" con cui puoi interagire, tutte legate insieme da una storia, si spera, decente. Ora, il nostro motore non si occuperà di quest'ultima parte; quella parte dipenderà da te. Ma per il resto c'è speranza.

Ora, tornando all'insieme delle stanze interconnesse, per me suona come un grafico, e se aggiungiamo anche il concetto di distanza o velocità di movimento che ho menzionato prima, abbiamo un grafico ponderato. E questo è solo un insieme di nodi che hanno un peso (o solo un numero - non preoccuparti di come si chiama) che rappresenta quel percorso tra di loro. Ecco una visuale (mi piace imparare vedendo, quindi guarda l'immagine, ok?):

Un esempio di grafico ponderato
Un esempio di grafico ponderato (anteprima grande)

Questo è un grafico ponderato: tutto qui. E sono sicuro che l'hai già capito, ma per completezza, lascia che ti mostri come faresti una volta che il nostro motore sarà pronto.

Una volta che inizi a configurare l'avventura, creerai la tua mappa (come vedi a sinistra dell'immagine qui sotto). E poi lo tradurrai in un grafico ponderato, come puoi vedere a destra dell'immagine. Il nostro motore sarà in grado di prelevarlo e di fartelo scorrere nell'ordine giusto.

Esempio di grafico per un determinato dungeon
Esempio di grafico per un determinato dungeon (Anteprima grande)

Con il grafico ponderato sopra, possiamo assicurarci che i giocatori non possano andare dall'ingresso fino all'ala sinistra. Dovrebbero passare attraverso i nodi tra questi due, e così facendo consumerà tempo, che possiamo misurare usando il peso delle connessioni.

Ora, sulla parte "divertente". Vediamo come sarebbe il grafico in formato JSON. Abbi pazienza qui; questo JSON conterrà molte informazioni, ma le esaminerò il più possibile:

 { "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 } } ] } } }

So che sembra molto, ma se lo riduci a una semplice descrizione del gioco, hai un dungeon composto da sei stanze, ognuna interconnessa con altre, come mostrato nel diagramma sopra.

Il tuo compito è spostarti ed esplorarlo. Scoprirai che ci sono due posti diversi dove puoi trovare un'arma (o in cucina o nella stanza buia, rompendo la sedia). Ti troverai anche di fronte a una porta chiusa a chiave; quindi, una volta trovata la chiave (situata all'interno della stanza simile a un ufficio), sarai in grado di aprirla e combattere il boss con qualsiasi arma tu abbia raccolto.

O vincerai uccidendolo o perderai venendo ucciso da esso.

Entriamo ora in una panoramica più dettagliata dell'intera struttura JSON e delle sue tre sezioni.

Grafico

Questo conterrà la relazione tra i nodi. Fondamentalmente, questa sezione si traduce direttamente nel grafico che abbiamo visto prima.

La struttura di questa sezione è piuttosto semplice. È un elenco di nodi, in cui ogni nodo comprende i seguenti attributi:

  • un ID che identifica in modo univoco il nodo tra tutti gli altri nel gioco;
  • un nome, che è fondamentalmente una versione leggibile dall'uomo dell'ID;
  • un insieme di collegamenti agli altri nodi. Ciò è dimostrato dall'esistenza di quattro possibili chiavi: nord, sud, est e ovest. Alla fine potremmo aggiungere ulteriori direzioni aggiungendo combinazioni di queste quattro. Ogni collegamento contiene l'ID del relativo nodo e la distanza (o peso) di quella relazione.

Gioco

Questa sezione conterrà le impostazioni e le condizioni generali. In particolare, nell'esempio sopra, questa sezione contiene le condizioni di vittoria e sconfitta. In altre parole, con queste due condizioni, faremo sapere al motore quando il gioco può finire.

Per semplificare le cose, ho aggiunto solo due condizioni:

  • o vinci uccidendo il capo,
  • o perdere venendo ucciso.

Camere

Ecco da dove proviene la maggior parte delle 163 linee, ed è la più complessa delle sezioni. È qui che descriveremo tutte le stanze della nostra avventura e tutto ciò che contiene.

Ci sarà una chiave per ogni stanza, usando l'ID che abbiamo definito prima. E ogni stanza avrà una descrizione, un elenco di oggetti, un elenco di uscite (o porte) e un elenco di personaggi non giocabili (NPC). Di queste proprietà, l'unica che dovrebbe essere obbligatoria è la descrizione, perché quella è richiesta dal motore per farti sapere cosa stai vedendo. Il resto di loro sarà lì solo se c'è qualcosa da mostrare.

Diamo un'occhiata a cosa possono fare queste proprietà per il nostro gioco.

La descrizione

Questo articolo non è così semplice come si potrebbe pensare, perché la tua visione di una stanza può cambiare a seconda delle circostanze. Se, ad esempio, guardi la descrizione della prima stanza, noterai che, per impostazione predefinita, non puoi vedere nulla, a meno che, ovviamente, non hai una torcia accesa con te.

Quindi, raccogliere oggetti e usarli potrebbe innescare condizioni globali che influenzeranno altre parti del gioco.

Gli oggetti

Questi rappresentano tutte le cose” che puoi trovare all'interno di una stanza. Ogni elemento condivide lo stesso ID e nome che avevano i nodi nella sezione del grafico.

Avranno anche una proprietà "destinazione", che indica dove deve essere conservato quell'oggetto, una volta ritirato. Questo è importante perché potrai avere un solo oggetto nelle tue mani, mentre potrai averne quanti ne desideri nel tuo inventario.

Infine, alcuni di questi oggetti potrebbero attivare altre azioni o aggiornamenti di stato, a seconda di cosa il giocatore decide di farne. Un esempio di questo sono le torce accese dall'ingresso. Se ne prendi uno, attiverai un aggiornamento dello stato nel gioco, che a sua volta farà sì che il gioco ti mostri una descrizione diversa della stanza successiva.

Gli oggetti possono anche avere dei "sottooggetti", che entrano in gioco una volta che l'oggetto originale viene distrutto (attraverso l'azione "rompi", ad esempio). Un elemento può essere scomposto in più elementi, e ciò è definito nell'elemento "sottovoci".

In sostanza, questo elemento è solo un array di nuovi elementi, uno che contiene anche l'insieme di azioni che possono innescare la loro creazione. Questo sostanzialmente apre la possibilità di creare diversi elementi secondari in base alle azioni che esegui sull'elemento originale.

Infine, alcuni oggetti avranno una proprietà "danno". Quindi, se usi un oggetto per colpire un NPC, quel valore verrà utilizzato per sottrarre vita da esso.

Le uscite

Questo è semplicemente un insieme di proprietà che indicano la direzione dell'uscita e le sue proprietà (una descrizione, nel caso si voglia ispezionarla, il suo nome e, in alcuni casi, il suo stato).

Le uscite sono un'entità separata dagli elementi perché il motore dovrà capire se puoi effettivamente attraversarli in base al loro stato. Le uscite bloccate non ti permetteranno di attraversarle a meno che tu non decida come cambiarne lo stato in sbloccato.

Gli NPC

Infine, gli NPC faranno parte di un altro elenco. Sono fondamentalmente elementi con statistiche che il motore utilizzerà per capire come ognuno dovrebbe comportarsi. Quelli che abbiamo definito nel nostro esempio sono "hp", che sta per punti salute, e "danno", che, proprio come le armi, è il numero che ogni colpo sottrae dalla salute del giocatore.

Questo è tutto per il dungeon che ho creato. È molto, sì, e in futuro potrei prendere in considerazione la creazione di una sorta di editor di livelli, per semplificare la creazione dei file JSON. Ma per ora non sarà necessario.

Nel caso non te ne fossi ancora reso conto, il vantaggio principale di avere il nostro gioco definito in un file come questo è che saremo in grado di cambiare file JSON come facevi con le cartucce nell'era del Super Nintendo. Basta caricare un nuovo file e iniziare una nuova avventura. Facile!

Pensieri di chiusura

Grazie per aver letto finora. Spero che ti sia piaciuto il processo di progettazione che ho seguito per dare vita a un'idea. Ricorda, però, che me lo sto inventando mentre procedo, quindi potremmo renderci conto in seguito che qualcosa che abbiamo definito oggi non funzionerà, nel qual caso dovremo tornare indietro e aggiustarlo.

Sono sicuro che ci sono un sacco di modi per migliorare le idee presentate qui e per creare un motore infernale. Ma ciò richiederebbe molte più parole di quelle che posso inserire in un articolo senza renderlo noioso per tutti, quindi per ora lo lasceremo così.

Altre parti di questa serie

  • Parte 2: Progettazione del server del motore di gioco
  • Parte 3: Creazione del client terminale
  • Parte 4: Aggiunta di chat al nostro gioco