Per molti, la generazione procedurale è un concetto magico che è appena fuori portata. Solo gli sviluppatori di giochi veterani sanno come costruire un gioco in grado di creare i propri livelli ... giusto? Esso può sembrare come la magia, ma PCG (generazione di contenuti procedurali) può essere appresa dagli sviluppatori di giochi per principianti. In questo tutorial, ti mostrerò come generare proceduralmente un sistema di grotte del dungeon.
Ecco una demo SWF che mostra il tipo di layout di livello che questa tecnica può generare:
Imparare le basi di solito significa un sacco di ricerca e sperimentazione di Google. Il problema è che ce ne sono pochissimi semplice guide su come iniziare. Per riferimento, ecco alcune eccellenti fonti di informazioni sull'argomento, che ho studiato:
Prima di entrare nei dettagli, è una buona idea considerare come risolveremo il problema. Ecco alcuni blocchi facili da digerire che useremo per mantenere questa cosa semplice:
Una volta che abbiamo analizzato i seguenti esempi, dovresti avere le competenze necessarie per sperimentare con PCG nei tuoi giochi. Emozionante, eh?
La prima cosa che faremo è posizionare casualmente le stanze di un livello di dungeon generato proceduralmente.
Per seguire, è una buona idea avere una conoscenza di base di come funzionano le mappe delle tessere. Se hai bisogno di una rapida panoramica o di un aggiornamento, consulta questo tutorial sulla mappa delle tessere. (È orientato verso Flash ma, anche se non hai familiarità con Flash, è comunque buono per ottenere l'essenza delle mappe delle tessere.)
Prima di iniziare dobbiamo riempire la nostra mappa con le tessere del muro. Tutto quello che devi fare è scorrere ogni punto della tua mappa (una matrice 2D, idealmente) e posizionare la tessera.
Abbiamo anche bisogno di convertire le coordinate dei pixel di ciascun rettangolo nelle nostre coordinate della griglia. Se si desidera passare dalla posizione dei pixel alla griglia, dividere la coordinata dei pixel per la larghezza del riquadro. Per passare dalla griglia ai pixel, moltiplicare la coordinata della griglia per la larghezza del riquadro.
Ad esempio, se vogliamo posizionare l'angolo in alto a sinistra della nostra stanza (5, 8)
sulla nostra griglia e abbiamo una larghezza di piastrella di 8
pixel, avremmo bisogno di mettere quell'angolo a (5 * 8, 8 * 8)
o (40, 64)
in coordinate pixel.
Creiamo a Camera
classe; potrebbe assomigliare a questo nel codice Haxe:
class Room estende Sprite // questi valori mantengono le coordinate della griglia per ogni angolo della stanza public var x1: Int; public var x2: Int; public var y1: Int; public var y2: Int; // larghezza e altezza della stanza in termini di griglia pubblica var w: Int; public var h: Int; // punto centrale della stanza public var center: Point; // costruttore per la creazione di nuove stanze public function new (x: Int, y: Int, w: Int, h: Int) super (); x1 = x; x2 = x + w; y1 = y; y2 = y + h; this.x = x * Main.TILE_WIDTH; this.y = y * Main.TILE_HEIGHT; questo.w = w; this.h = h; center = new Point (Math.floor ((x1 + x2) / 2), Math.floor ((y1 + y2) / 2)); // restituisce true se questa stanza interseca la stanza che la funzione pubblica pubblica interseca (room: Room): Bool return (x1 <= room.x2 && x2 >= room.x1 && y1 <= room.y2 && room.y2 >= room.y1);
Abbiamo valori per la larghezza, l'altezza, la posizione del punto centrale e le posizioni di quattro angoli di ogni stanza, e una funzione che ci dice se questa stanza si interseca con un'altra. Si noti inoltre che tutto tranne i valori xey sono nel nostro sistema di coordinate della griglia. Questo perché rende la vita molto più facile usare piccoli numeri ogni volta che accediamo ai valori della stanza.
Ok, abbiamo il quadro per una stanza sul posto. Ora come generiamo e posizioniamo proceduralmente una stanza? Bene, grazie ai generatori di numeri casuali incorporati, questa parte non è troppo difficile.
Tutto quello che dobbiamo fare è fornire valori casuali xey per la nostra stanza entro i limiti della mappa e fornire valori di larghezza e altezza casuali all'interno di un intervallo predeterminato.
Poiché utilizziamo posizioni e dimensioni casuali per le nostre sale, siamo costretti a sovrapporsi a stanze create in precedenza mentre riempiamo il dungeon. Bene, abbiamo già codificato un semplice interseca ()
metodo per aiutarci a prenderci cura del problema.
Ogni volta che proviamo a posizionare una nuova stanza, chiamiamo semplicemente interseca ()
su ogni coppia di stanze all'interno dell'intero elenco. Questa funzione restituisce un valore booleano: vero
se le stanze sono sovrapposte, e falso
altrimenti. Possiamo usare quel valore per decidere cosa fare con la stanza che abbiamo appena tentato di posizionare.
interseca ()
funzione. Puoi vedere come i valori xey si sovrappongono e ritornano vero
. placeRooms di funzione privata () // crea una matrice per l'archiviazione della stanza per stanze di facile accesso = nuova matrice (); // randomize values for each room for (r in 0 ... maxRooms) var w = minRoomSize + Std.random (maxRoomSize - minRoomSize + 1); var h = minRoomSize + Std.random (maxRoomSize - minRoomSize + 1); var x = Std.random (MAP_WIDTH - w - 1) + 1; var y = Std.random (MAP_HEIGHT - h - 1) + 1; // crea room con valori randomizzati var newRoom = new Room (x, y, w, h); var failed = false; for (otherRoom in rooms) if (newRoom.intersects (otherRoom)) failed = true; rompere; if (! failed) // funzione locale per ritagliare la nuova room createRoom (newRoom); // spinge la nuova stanza nelle stanze array rooms.push (newRoom)
La chiave qui è la mancato
booleano; è impostato sul valore di ritorno di interseca ()
, e così è vero
se (e solo se) le tue stanze si sovrappongono. Una volta che usciremo dal ciclo, controlleremo questo mancato
variabile e, se è falso, possiamo ritagliare la nuova stanza. Altrimenti, scarichiamo la stanza e riproviamo finché non raggiungiamo il numero massimo di stanze.
La maggior parte dei giochi che utilizzano contenuti generati proceduralmente si sforzano di rendere tutti i contenuti raggiungibili dal giocatore, ma ci sono alcune persone là fuori che credono che questa non sia necessariamente la migliore decisione di progettazione. E se tu avessi alcune stanze nel tuo dungeon che il giocatore potrebbe solo raramente raggiungere ma potrebbe sempre vedere? Questo potrebbe aggiungere una dinamica interessante al tuo dungeon.
Ovviamente, non importa su quale parte dell'argomento tu stia, probabilmente è comunque una buona idea assicurarsi che il giocatore possa sempre progredire nel gioco. Sarebbe piuttosto frustrante se tu arrivassi a un livello del dungeon del gioco e l'uscita fosse completamente bloccata.
Considerando che la maggior parte dei giochi sparano per contenuti raggiungibili al 100%, ci atterremo a questo.
A questo punto, dovresti avere una mappa delle tessere attiva e in esecuzione e dovrebbe esserci un codice per creare un numero variabile di stanze di varie dimensioni. Guarda quello; hai già delle dungeon rooms generate a livello procedurale!
Ora l'obiettivo è quello di collegare ogni stanza in modo che possiamo camminare attraverso il nostro dungeon e alla fine raggiungere un'uscita che porta al livello successivo. Possiamo realizzare questo scavando corridoi tra le stanze.
Dovremo aggiungere un punto
variabile al codice per tenere traccia del centro di ogni stanza creata. Ogni volta che creiamo e posizioniamo una stanza, determiniamo il suo centro e lo colleghiamo al centro della stanza precedente.
Innanzitutto, implementeremo i corridoi:
funzione privata hCorridor (x1: Int, x2: Int, y) for (x in Std.int (Math.min (x1, x2)) ... Std.int (Math.max (x1, x2)) + 1) // destina le tessere per "ritagliare" la mappa del corridoio [x] [y] .parent.removeChild (map [x] [y]); // posiziona una nuova mappa di tile non bloccata [x] [y] = new Tile (Tile.DARK_GROUND, false, false); // aggiungi una tessera come nuovo oggetto di gioco addChild (map [x] [y]); // imposta la posizione della piastrella in modo appropriato map [x] [y] .setLoc (x, y); // crea un corridoio verticale per connettere le stanze private function vCorridor (y1: Int, y2: Int, x) for (y in Std.int (Math.min (y1, y2)) ... Std.int (Math.max (y1, y2)) + 1) // distruggi le tessere per "ritagliare" la mappa del corridoio [x] [y] .parent.removeChild (map [x] [y]); // posiziona una nuova mappa di tile non bloccata [x] [y] = new Tile (Tile.DARK_GROUND, false, false); // aggiungi una tessera come nuovo oggetto di gioco addChild (map [x] [y]); // imposta la posizione della piastrella in modo appropriato map [x] [y] .setLoc (x, y);
Queste funzioni agiscono quasi allo stesso modo, ma una si ritaglia orizzontalmente e l'altra verticalmente.
Il collegamento della prima stanza alla seconda stanza richiede avCorridor
e un hCorridor
. Abbiamo bisogno di tre valori per farlo. Per i corridoi orizzontali abbiamo bisogno del valore x iniziale, del valore x finale e del valore y corrente. Per i corridoi verticali abbiamo bisogno dei valori iniziali e finali y insieme al valore x attuale.
Dato che ci stiamo spostando da sinistra a destra, abbiamo bisogno dei due valori x corrispondenti, ma solo un valore y poiché non ci sposteremo in alto o in basso. Quando ci muoviamo verticalmente avremo bisogno dei valori y. Nel per
loop all'inizio di ogni funzione, iteriamo dal valore iniziale (x o y) al valore finale fino a che non abbiamo scavato l'intero corridoio.
Ora che abbiamo il codice del corridoio, possiamo cambiare il nostro placeRooms ()
funzione e chiama le nostre nuove funzioni di corridoio:
placeRooms di funzione privata () // memorizza le stanze in un array per stanze di facile accesso = nuova matrice (); // variabile per il centro di rilevamento di ogni stanza var newCenter = null; // randomize values for each room for (r in 0 ... maxRooms) var w = minRoomSize + Std.random (maxRoomSize - minRoomSize + 1); var h = minRoomSize + Std.random (maxRoomSize - minRoomSize + 1); var x = Std.random (MAP_WIDTH - w - 1) + 1; var y = Std.random (MAP_HEIGHT - h - 1) + 1; // crea room con valori randomizzati var newRoom = new Room (x, y, w, h); var failed = false; for (otherRoom in rooms) if (newRoom.intersects (otherRoom)) failed = true; rompere; if (! failed) // funzione locale per ritagliare la nuova room createRoom (newRoom); // centro di vendita per la nuova stanza newCenter = newRoom.center; if (rooms.length! = 0) // centro di archiviazione della room precedente var prevCenter = rooms [rooms.length - 1] .center; // ritaglia i corridoi tra le stanze in base ai centri // Avvia a caso con corridoi orizzontali o verticali se (Std.random (2) == 1) hCorridor (Std.int (prevCenter.x), Std.int (newCenter.x ), Std.int (prevCenter.y)); vCorridor (Std.int (prevCenter.y), Std.int (newCenter.y), Std.int (newCenter.x)); else vCorridor (Std.int (prevCenter.y), Std.int (newCenter.y), Std.int (prevCenter.x)); hCorridor (Std.int (prevCenter.x), Std.int (newCenter.x), Std.int (newCenter.y)); if (! failed) rooms.push (newRoom);
Nell'immagine sopra, puoi seguire la creazione del corridoio dalla prima alla quarta stanza: rosso, verde, poi blu. È possibile ottenere risultati interessanti in base al posizionamento delle stanze, ad esempio due corridoi uno accanto all'altro formano un corridoio a doppia larghezza.
Abbiamo aggiunto alcune variabili per tracciare il centro di ogni stanza e abbiamo collegato le stanze con corridoi tra i loro centri. Ora ci sono più sale e corridoi non sovrapposti che mantengono connesso l'intero livello dei sotterranei. Non male.
Hai fatto molta strada costruendo il tuo primo livello di dungeon generato proceduralmente, e spero che tu abbia capito che PCG non è una bestia magica che non avrai mai la possibilità di uccidere.
Abbiamo esaminato in modo casuale come posizionare i contenuti attorno al tuo livello di Dungeon con semplici generatori di numeri casuali e alcuni intervalli prestabiliti per mantenere il tuo contenuto della giusta dimensione e approssimativamente nel posto giusto. Successivamente, abbiamo scoperto un modo molto semplice per determinare se il posizionamento casuale aveva senso controllando la sovrapposizione di stanze. Infine, abbiamo parlato un po 'del merito di mantenere i tuoi contenuti raggiungibili e abbiamo trovato un modo per garantire che il tuo giocatore possa raggiungere tutte le stanze del tuo dungeon.
I primi tre passaggi del nostro processo in quattro fasi sono finiti, il che significa che hai gli elementi costitutivi di un grande dungeon per il tuo prossimo gioco. L'ultimo passo spetta a te: devi scorrere su ciò che hai imparato a creare più contenuti generati proceduralmente per una rigiocabilità infinita.
Il metodo per ritagliare livelli di Dungeon semplici in questo tutorial graffia solo la superficie di PCG e ci sono altri semplici algoritmi che puoi facilmente raccogliere.
La mia sfida per te è iniziare a sperimentare gli inizi del tuo gioco che hai creato qui e fare qualche ricerca su più metodi per cambiare i tuoi dungeon.
Un ottimo metodo per creare livelli di grotte è l'uso di automi cellulari, che ha infinite possibilità per personalizzare i livelli dei dungeon. Un altro ottimo metodo per imparare è Binary Space Partitioning (BSP), che crea alcuni livelli di celle sotterranee dall'aspetto malvagio.
Spero che questo ti abbia dato un buon inizio nella generazione di contenuti procedurali. Assicurati di commentare qui sotto per qualsiasi domanda tu abbia, e mi piacerebbe vedere alcuni esempi di ciò che stai creando con PCG.
Post correlati