Generazione procedurale per semplici puzzle

Cosa starai creando

I puzzle sono una parte integrante del gameplay per molti generi. Sia semplice o complesso, lo sviluppo manuale di enigmi può diventare rapidamente ingombrante. Questo tutorial mira ad alleggerire questo carico e aprire la strada ad altri aspetti più divertenti del design.

Insieme creeremo un generatore per la composizione di semplici enigmi procedurali "annidati". Il tipo di enigma su cui ci concentreremo è il tradizionale "lucchetto e chiave" più spesso ripetuto come: ottenere x item per sbloccare l'area. Questi tipi di puzzle possono diventare noiosi per i team che lavorano su determinati tipi di giochi, in particolare dungeon crawler, sandbox e giochi di ruolo in cui i puzzle sono più spesso utilizzati per il contenuto e l'esplorazione.

Usando la generazione procedurale, il nostro obiettivo è creare una funzione che richieda alcuni parametri e restituisca un asset più complesso per il nostro gioco. L'applicazione di questo metodo fornirà un ritorno esponenziale al tempo degli sviluppatori senza sacrificare la qualità del gameplay. La costernazione degli sviluppatori potrebbe anche diminuire come un felice effetto collaterale.

Cosa devo sapere?

Per seguire, è necessario avere familiarità con un linguaggio di programmazione di tua scelta. Poiché la maggior parte di ciò di cui stiamo discutendo è solo dati e generalizzata in pseudocodice, qualsiasi linguaggio di programmazione orientato agli oggetti sarà sufficiente. 

In effetti, anche alcuni editor drag-and-drop funzioneranno. Se desideri creare una demo giocabile del generatore menzionato qui, avrai anche bisogno di familiarità con la tua libreria di gioco preferita.

Creazione del generatore

Iniziamo con uno sguardo su alcuni pseudocodici. I componenti fondamentali del nostro sistema saranno le chiavi e le stanze. In questo sistema, un giocatore non può entrare nella porta di una stanza se non possiede la sua chiave. Ecco come appariranno questi due oggetti come classi:

class key Var playerHas; Posizione Var; Funzione init (setLocation) Location = setLocation; PlayerHas = false;  Funzione pickUp () this.playerHas = true;  class Room Var isLocked; Var assocKey; Funzione init () isLocked = true; assocKey = new Key (this);  Funzione unlock () this.isLocked = false;  Funzione canUnlock If (this.key.PlayerHas) Restituisce true;  Altro Restituisce falso; 

La nostra classe chiave contiene solo due informazioni al momento: la posizione della chiave e se il giocatore ha quella chiave nel suo inventario. Le sue due funzioni sono l'inizializzazione e il ritiro. L'inizializzazione determina le basi di una nuova chiave, mentre il ritiro è per quando un giocatore interagisce con la chiave.

A sua volta, la nostra classe di camera contiene anche due variabili: è bloccato, che mantiene lo stato attuale della serratura della stanza, e assocKey, che contiene l'oggetto Key che sblocca questa stanza specifica. Contiene anche una funzione per l'inizializzazione da chiamare per sbloccare la porta e un'altra per verificare se la porta può essere aperta.

Una singola porta e una chiave sono divertenti, ma possiamo sempre ravvivarlo con il nesting. L'implementazione di questa funzione ci consentirà di creare porte all'interno delle porte mentre serviamo da generatore principale. Per mantenere il nesting, dovremo aggiungere alcune variabili addizionali alla nostra porta:

class Room Var isLocked; Var assocKey; Var parentRoom; Profondità var; Funzione init (setParentRoom, setDepth) If (setParentRoom) parentRoom = setParentRoom;  Else parentRoom = none;  Depth = setDepth; isLocked = true; assocKey = new Key (this);  Funzione unlock () this.isLocked = false;  Funzione canUnlock If (this.key.playerHas) Restituisce true;  Altro Restituisce falso;  FunctionroomGenerator (depthMax) Array roomsToCheck; Array finishedRooms; Room initialRoom.init (none, 0); roomsToCheck.add (initialRoom); Mentre (roomsToCheck! = Vuoto) If (currentRoom.depth == depthMax) finishedRooms.add (currentRoom); roomsToCheck.remove (currentRoom);  Else Room newRoom.init (currentRoom, currentRoom.depth + 1); roomsToCheck.add (newRoom); finishedRooms.add (currentRoom); roomsToCheck.remove (currentRoom); 

Questo codice generatore sta facendo quanto segue:

  1. Prendendo in considerazione il parametro per il nostro puzzle generato (in particolare quanti strati deve essere scavato in una stanza nidificata).

  2. Creazione di due array: uno per le stanze che vengono verificate per il potenziale nidificazione e un altro per la registrazione di stanze già nidificate.

  3. Creare una stanza iniziale per contenere l'intera scena e quindi aggiungerla all'array per consentirci di controllare in seguito.

  4. Prendendo la stanza nella parte anteriore della serie per mettere il ciclo.

  5. Controllo della profondità della stanza corrente rispetto alla profondità massima fornita (questo decide se creiamo un'ulteriore stanza del bambino o se completiamo il processo).

  6. Stabilire una nuova stanza e popolarla con le informazioni necessarie dalla stanza genitore.

  7. Aggiungere la nuova stanza al roomsToCheck array e spostando la stanza precedente sull'array finito.

  8. Ripetere questo processo fino al completamento di ciascuna stanza dell'array.

Ora possiamo avere un numero di stanze che la nostra macchina può gestire, ma abbiamo ancora bisogno di chiavi. Il posizionamento chiave ha una grande sfida: risolvibilità. Ovunque posizioniamo la chiave, dobbiamo assicurarci che un giocatore possa accedervi! Non importa quanto sia eccellente il nascondiglio della chiave nascosta, se il giocatore non riesce a raggiungerlo, lui o lei è effettivamente intrappolato. Affinché il giocatore possa continuare il puzzle, le chiavi devono essere ottenibili.

Il metodo più semplice per garantire la risolubilità nel nostro puzzle consiste nell'utilizzare il sistema gerarchico delle relazioni tra oggetti padre e figlio. Poiché ogni stanza risiede in un'altra, ci aspettiamo che un giocatore debba avere accesso al genitore di ogni stanza per raggiungerlo. Quindi, finché la chiave è al di sopra della stanza nella catena gerarchica, garantiamo che il nostro giocatore sia in grado di ottenere l'accesso.

Per aggiungere la generazione di chiavi alla nostra generazione procedurale, inseriremo il seguente codice nella nostra funzione principale:

 Function roomGenerator (depthMax) Array roomsToCheck; Array finishedRooms; Room initialRoom.init (none, 0); roomsToCheck.add (initialRoom); Mentre (roomsToCheck! = Vuoto) If (currentRoom.depth == depthMax) finishedRooms.add (currentRoom); roomsToCheck.remove (currentRoom);  Else Room newRoom.init (currentRoom, currentRoom.depth + 1); roomsToCheck.add (newRoom); finishedRooms.add (currentRoom); roomsToCheck.remove (currentRoom); Array allParentRooms; roomCheck = newRoom; While (roomCheck.parent) allParentRooms.add (roomCheck.parent); roomCheck = roomCheck.parent;  Key newKey.init (Random (allParentRooms)); newRoom.Key = newKey; Restituire camere terminate;  Altrimenti finishedRooms.add (currentRoom); roomsToCheck.remove (currentRoom);  

Questo codice aggiuntivo produrrà ora un elenco di tutte le stanze che si trovano al di sopra della stanza corrente nella gerarchia delle mappe. Quindi scegliamo uno di questi casualmente e impostiamo la posizione della chiave in quella stanza. Dopodiché, assegniamo la chiave alla stanza che sblocca.

Una volta chiamata, la nostra funzione di generatore ora creerà e restituirà un determinato numero di stanze con le chiavi, risparmiando potenzialmente ore di tempo di sviluppo!

Questo avvolge la parte pseudocodice del nostro semplice generatore di puzzle, quindi ora mettiamola in azione.

Demo di generazione di puzzle procedurali

Abbiamo creato la nostra demo utilizzando JavaScript e la libreria Crafty.js per mantenerla il più leggera possibile, consentendoci di mantenere il nostro programma sotto 150 righe di codice. Ci sono tre componenti principali della nostra demo come spiegato di seguito:

  1. Il giocatore può spostarsi all'interno di ogni livello, prendere le chiavi e sbloccare le porte.

  2. Il generatore che utilizzeremo per creare automaticamente una nuova mappa ogni volta che viene eseguita la demo.

  3. Un'estensione per il nostro generatore da integrare con Crafty.js, che ci consente di memorizzare informazioni su oggetti, collisioni e entità.

Lo pseudocodice di cui sopra funge da strumento di spiegazione, pertanto l'implementazione del sistema nel proprio linguaggio di programmazione richiederà alcune modifiche.

Per la nostra demo, una parte delle classi è semplificata per un uso più efficiente in JavaScript. Ciò include l'eliminazione di alcune funzioni correlate alle classi, poiché JavaScript consente un accesso più facile alle variabili all'interno delle classi.

Per creare la parte del gioco della nostra demo, inizializziamo Crafty.js e quindi un'entità giocatore. Successivamente forniamo all'entità giocatore i controlli di base a quattro direzioni e alcuni rilevamenti di collisione minori per impedire l'ingresso in stanze chiuse.

Alle stanze viene ora assegnata un'entità astuta, che memorizza informazioni sulla loro dimensione, posizione e colore per la rappresentazione visiva. Aggiungeremo anche una funzione di disegno per permetterci di creare una stanza e disegnarla sullo schermo.

Forniremo le chiavi con aggiunte simili, inclusa la memorizzazione della sua entità Crafty, le dimensioni, la posizione e il colore. Le chiavi saranno anche codificate per colore per abbinare le stanze che sbloccano. Finalmente, possiamo ora posizionare i tasti e creare le loro entità usando una nuova funzione di disegno.

Ultimo ma non meno importante, svilupperemo una piccola funzione di supporto che crea e restituisce un valore di colore esadecimale casuale per rimuovere l'onere della scelta dei colori. A meno che non ti piacciano i colori, naturalmente.

Cosa fare dopo?

Ora che hai il tuo generatore semplice, ecco alcune idee per estendere i nostri esempi:

  1. Porta il generatore per abilitare l'uso nel tuo linguaggio di programmazione di scelta.

  2. Estendi il generatore per includere la creazione di stanze di ramificazione per un'ulteriore personalizzazione.

  3. Aggiungi la possibilità di gestire più accessi al nostro generatore per consentire puzzle più complessi.

  4. Estendi il generatore per consentire il posizionamento delle chiavi in ​​posizioni più complicate per migliorare la risoluzione dei problemi dei giocatori. Questo è particolarmente interessante se abbinato a più percorsi per i giocatori.

Avvolgendo

Ora che abbiamo creato questo generatore di puzzle insieme, usa i concetti mostrati per semplificare il tuo ciclo di sviluppo. Quali compiti ripetitivi ti ritrovi a fare? Ciò che ti preoccupa di più della creazione del tuo gioco? 

Le possibilità sono, con un po 'di pianificazione e generazione procedurale, di rendere il processo notevolmente più semplice. Speriamo che il nostro generatore ti permetta di concentrarti sulle parti più accattivanti del gioco mentre tagli il banale.

Buona fortuna e ci vediamo nei commenti!