Generare livelli di cavità casuali usando gli automi cellulari

I generatori di contenuti procedurali sono pezzi di codice scritti nel tuo gioco che possono creare nuovi contenuti di gioco in qualsiasi momento - anche quando il gioco è in esecuzione! Gli sviluppatori di giochi hanno cercato di generare proceduralmente tutto, dai mondi 3D alle colonne sonore musicali. Aggiungere una generazione al tuo gioco è un ottimo modo per aggiungere valore aggiunto: i giocatori lo adorano perché ottengono contenuti nuovi, imprevedibili ed eccitanti ogni volta che giocano.

In questo tutorial, vedremo un ottimo metodo per generare livelli casuali e tenteremo di estendere i limiti di ciò che potresti pensare possa essere generato.

Post correlati

Se sei interessato a leggere di più sugli argomenti di generazione di contenuti procedurali, progettazione di livelli, AI o automi cellulari, assicurati di controllare questi altri post:

  • Creating Life: Conway's Game of Life
  • Portal 2 Level Design: creazione di puzzle per sfidare i tuoi giocatori
  • StarCraft II Level Design: suggerimenti di design ed editor estetico
  • Codifica di un generatore di sequenze personalizzato per il rendering di Starscape
  • Gamedev Glossary: ​​Sequence Generators e Pseudorandom Number Generators

Benvenuti nelle grotte!

In questo tutorial, costruiremo un generatore di grotte. Le grotte sono perfette per tutti i generi di giochi e ambientazioni, ma in particolare mi ricordano i vecchi dungeon nei giochi di ruolo.

Dai un'occhiata alla demo qui sotto per vedere i tipi di output che sarai in grado di ottenere. Clicca 'Nuovo Mondo' per produrre una nuova grotta da guardare. Parleremo di ciò che le diverse impostazioni fanno a tempo debito.


Questo generatore in realtà ci restituisce una grande matrice bidimensionale di blocchi, ognuno dei quali è solido o vuoto. Quindi, in effetti, puoi utilizzare questo generatore per tutti i tipi di giochi oltre ai dungeon-crawler: livelli casuali per giochi di strategia, tilemaps per giochi con piattaforma, forse anche come arene per uno sparatutto multiplayer! Se guardi attentamente, lanciando i blocchi pieni e vuoti diventa anche un generatore di isole. Utilizza lo stesso codice e lo stesso output, il che rende questo strumento davvero flessibile.

Iniziamo ponendo una semplice domanda: cosa diavolo è un automa cellulare, comunque?


Iniziare con le cellule

Negli anni '70, un matematico di nome John Conway pubblicò una descrizione di The Game Of Life, a volte chiamata semplicemente Life. La vita non era davvero un gioco; era più simile a una simulazione che prendeva una griglia di celle (che poteva essere viva o morta) e applicava loro delle semplici regole.

Quattro regole sono state applicate a ciascuna cella in ogni fase della simulazione:

  1. Se una cellula vivente ha meno di due vicini viventi, muore.
  2. Se una cellula vivente ha due o tre vicini viventi, rimane in vita.
  3. Se una cellula vivente ha più di tre vicini viventi, muore.
  4. Se una cellula morta ha esattamente tre vicini viventi, diventa viva.

Bello e semplice! Tuttavia, se provi diverse combinazioni di griglie di partenza, puoi ottenere risultati molto strani. Loop infiniti, macchine che sputano forme e altro ancora. The Game of Life è un esempio di a automa cellulare - una griglia di celle che sono governate da determinate regole.

Stiamo implementando un sistema molto simile a Life, ma invece di produrre modelli e forme divertenti, creerà incredibili sistemi di grotte per i nostri giochi.


Implementazione di un automa cellulare

Rappresenteremo la nostra griglia cellulare come una matrice bidimensionale di Boolean (vero o falso) valori. Questo ci sta bene perché ci interessa solo se una tessera è solida o no.

Ecco noi inizializzando la nostra griglia di celle:

booleano [] [] cellmap = new booleano [larghezza] [altezza];

Mancia: Si noti che il primo indice è la coordinata x per l'array e il secondo indice è la coordinata y. Ciò rende l'accesso alla matrice più naturale nel codice.

Nella maggior parte dei linguaggi di programmazione, questo array verrà inizializzato con tutti i suoi valori impostati su falso. Va bene per noi! Se un indice di matrice (X, y) è falso, diremo che la cella è vuota; se è vero, quella tessera sarà solida roccia.

Ciascuna di queste posizioni dell'array rappresenta una delle "celle" nella nostra griglia cellulare. Ora abbiamo bisogno di creare la nostra griglia in modo che possiamo iniziare a costruire le nostre caverne.

Iniziamo impostando casualmente ogni cella su "vivo o morto". Ogni cella avrà la stessa possibilità casuale di essere resa viva, e dovresti assicurarti che questo valore casuale sia impostato in una variabile da qualche parte, perché vorremmo sicuramente modificarlo più tardi e averlo in un posto facilmente accessibile ci aiuterà quello. Userò 45% per iniziare.

float chanceToStartAlive = 0.45f; public boolean [] [] initialiseMap (booleano [] [] map) per (int x = 0; x 
La nostra caverna casuale prima di ogni passo di simulazione di automa cellulare.

Se eseguiamo questo codice, finiamo con una grande griglia di celle come quella sopra che sono casualmente vivi o morti. È disordinato, e sicuramente non assomiglia ad alcun sistema di grotte che abbia mai visto. Allora, qual è il prossimo?


Coltivando le nostre grotte

Ricorda le regole che governano le cellule in The Game Of Life? Ogni volta che la simulazione andava avanti di un passo, ogni cellula controllava le regole della vita e vedeva se sarebbe cambiata in essere viva o morta. Useremo esattamente la stessa idea per costruire le nostre caverne: scriveremo una funzione ora che scorre su ogni cella della griglia e applica alcune regole di base per decidere se vive o muore.

Come vedremo più avanti, useremo questo bit di codice più di una volta, quindi metterlo in una sua funzione significa che possiamo chiamarlo tante o quante volte vogliamo. Gli daremo un bel nome informativo come doSimulationStep (), pure.

Che cosa deve fare la funzione? Per prima cosa, creeremo una nuova griglia in cui inserire i valori aggiornati delle celle. Per capire perché dobbiamo fare questo, ricorda che per calcolare il nuovo valore di una cella nella griglia, dobbiamo guardare i suoi otto vicini:

Ma se abbiamo già calcolato il nuovo valore di alcune celle e le abbiamo rimesse nella griglia, allora il nostro calcolo sarà un mix di dati vecchi e nuovi, come questo:

Oops! Non è quello che vogliamo. Quindi ogni volta che calcoliamo un nuovo valore di cella, invece di rimetterlo nella vecchia mappa, lo scriveremo in un altro.

Iniziamo a scrivere quello doSimulationStep () funzione, quindi:

public doSimulationStep (boolean [] [] oldMap) boolean [] [] newMap = new boolean [width] [height]; // ... 

Vogliamo considerare ogni cella della griglia a turno e contare quanti dei suoi vicini sono vivi e morti. Contare i tuoi vicini in un array è uno di quei noiosi bit di codice che dovrai scrivere milioni di volte. Ecco una rapida implementazione di esso in una funzione che ho chiamato countAliveNeighbours ():

// Restituisce il numero di celle di un anello attorno (x, y) che sono vivi. countAliveNeighbours pubblico (booleano [] [] map, int x, int y) int count = 0; per (int i = -1; i<2; i++) for(int j=-1; j<2; j++) int neighbour_x = x+i; int neighbour_y = y+j; //If we're looking at the middle point if(i == 0 && j == 0) //Do nothing, we don't want to add ourselves in!  //In case the index we're looking at it off the edge of the map else if(neighbour_x < 0 || neighbour_y < 0 || neighbour_x >= map.length || neighbour_y> = map [0] .length) count = count + 1;  // Altrimenti, un controllo normale del neighbor else if (map [neighour_x] [neighbour_y]) count = count + 1; 

Un paio di cose su questa funzione:

Prima il per i loop sono un po 'strani se non hai mai fatto qualcosa di simile prima. L'idea è che vogliamo guardare tutte le celle che sono intorno al punto (X, y). Se guardi l'illustrazione qui sotto, puoi vedere come gli indici che vogliamo siano uno in meno, uguale a, e uno in più rispetto all'indice originale. I nostri due per i loop ci danno proprio questo, a partire da -1, e loop through a +1. Quindi lo aggiungiamo all'indice originale all'interno di per loop per trovare ogni vicino.

In secondo luogo, notiamo come se stiamo controllando un riferimento alla griglia che non è reale (ad esempio, è fuori dal bordo della mappa), lo consideriamo come un vicino. Preferisco questo per la generazione di grotte perché tende a riempire i bordi della mappa, ma puoi provare a non farlo se ti piace.

Quindi ora, torniamo al nostro doSimulationStep () funzione e aggiungere un altro codice:

public boolean [] [] doSimulationStep (boolean [] [] oldMap) boolean [] [] newMap = new booleano [width] [height]; // Loop su ogni riga e colonna della mappa per (int x = 0; x birthLimit) newMap [x] [y] = true;  else newMap [x] [y] = false;  return newMap; 

Questo scorre su tutta la mappa, applicando le nostre regole a ciascuna cella della griglia per calcolare il nuovo valore e posizionarlo newMap. Le regole sono più semplici del Game of Life - abbiamo due variabili speciali, una per le cellule morte nascenti (birthLimit) e uno per uccidere le cellule vive (deathLimit). Se le cellule viventi sono circondate da meno di deathLimit le cellule muoiono e se le cellule morte sono vicine almeno birthLimit le cellule diventano vivi. Bello e semplice!

Tutto ciò che rimane alla fine è un tocco finale per restituire la mappa aggiornata. Questa funzione rappresenta un singolo passo delle regole del nostro automa cellulare - il passo successivo è capire cosa succede mentre lo applichiamo una volta, due o più volte alla nostra mappa iniziale iniziale.


Tweaking e Tuning

Diamo un'occhiata a come appare il codice di generazione principale, usando il codice che abbiamo scritto finora.

public boolean [] [] generateMap () // Crea una nuova mappa boolean [] [] cellmap = new boolean [width] [height]; // Imposta la mappa con valori casuali cellmap = initialiseMap (cellmap); // E ora esegui la simulazione per un determinato numero di passaggi per (int i = 0; i 

L'unico vero bit di codice è a per loop che esegue il nostro metodo di simulazione un certo numero di volte. Ancora una volta, inseriscilo in una variabile in modo che possiamo cambiarlo, perché ora inizieremo a giocare con questi valori!

Finora abbiamo impostato queste variabili:

  • chanceToStartAlive imposta la densità della griglia iniziale con le cellule viventi.
  • starvationLimit è il limite inferiore vicino a cui le cellule iniziano a morire.
  • overpopLimit è il limite superiore vicino a cui le cellule iniziano a morire.
  • birthNumber è il numero di vicini che fanno diventare viva una cellula morta.
  • numberOfSteps è il numero di volte in cui viene eseguita la fase di simulazione.

La nostra caverna casuale dopo due passaggi di simulazione di automa cellulare.

Puoi giocare con queste variabili nella demo nella parte superiore della pagina. Ogni valore cambierà drammaticamente la demo, quindi divertiti a vedere cosa si adatta.

Uno dei cambiamenti più interessanti che puoi fare è il numberOfSteps variabile. Mentre esegui la simulazione per più passaggi, la ruvidità della mappa scompare e le isole scompaiono nel nulla. Ho aggiunto un pulsante in modo che tu possa chiamare manualmente la funzione tu stesso e vedere gli effetti. Sperimenta un po 'e troverai una combinazione di impostazioni che si adattano al tuo stile e al tipo di livelli di cui il tuo gioco ha bisogno.


La nostra caverna casuale dopo sei simulazioni di automa cellulare.

Con quello, hai finito. Congratulazioni: hai appena creato un generatore di livello procedurale, ben fatto! Siediti, corri e riesegui il tuo codice e sorridi ai misteriosi e meravigliosi sistemi di grotte che escono. Benvenuti nel mondo della generazione procedurale.


Prenderlo ulteriormente

Se stai fissando il tuo bel generatore di grotte e ti stai chiedendo cos'altro puoi fare, ecco un paio di idee per l'assegnazione di crediti extra:

Usare Flood Fill per fare il controllo di qualità

Flood Fill è un algoritmo molto semplice che puoi usare per trovare tutti gli spazi in una matrice che si connettono a un particolare punto. Proprio come suggerisce il nome, l'algoritmo funziona un po 'come versare un secchio d'acqua nel tuo livello - si estende dal punto di partenza e riempie tutti gli angoli.

Il riempimento dell'inondazione è ottimo per gli automi cellulari perché puoi usarlo per vedere quanto è grande una grotta particolare. Se esegui la demo un paio di volte noterai che alcune mappe sono costituite da una grande grotta, mentre altre hanno alcune grotte più piccole che sono separate l'una dall'altra. Il riempimento dell'inondazione può aiutarti a rilevare quanto è grande una caverna, quindi rigenerare il livello se è troppo piccolo o decidere dove vuoi che il giocatore inizi se pensi che sia abbastanza grande. C'è un grande schema di riempimento di piena su Wikipedia.

Posizionamento del tesoro rapido e semplice

Posizionare il tesoro in aree interessanti a volte richiede molto codice, ma in realtà possiamo scrivere un bel po 'di codice per mettere il tesoro di mezzo nei nostri sistemi di grotte. Abbiamo già il nostro codice che conta quanti vicini ha un quadrato, quindi collegando il nostro sistema di grotte finito, possiamo vedere come circondato da mura un particolare quadrato è.

Se una cella della griglia vuota è circondata da un sacco di muri solidi, è probabilmente alla fine di un corridoio o nascosta nelle pareti del sistema di grotte. Questo è un ottimo posto per nascondere il tesoro - così facendo un semplice controllo dei nostri vicini possiamo far scivolare il tesoro negli angoli e nei vicoli.

public void placeTreasure (booleano [] [] world) // Quanto nascosto deve essere un punto per il tesoro? // Trovo che il 5 o 6 sia buono. 6 per un tesoro molto raro. int treasureHiddenLimit = 5; per (int x = 0; x < worldWidth; x++) for (int y=0; y < worldHeight; y++) if(!world[x][y]) int nbs = countAliveNeighbours(world, x, y); if(nbs >= treasureHiddenLimit) placeTreasure (x, y); 

Questo non è perfetto. A volte mette il tesoro in buchi inaccessibili nel sistema di grotte, ea volte anche le macchie saranno abbastanza evidenti. Ma, in un pizzico, è un ottimo modo per sparpagliare oggetti da collezione attorno al tuo livello. Provalo nella demo colpendo il placeTreasure () pulsante!


Conclusioni e ulteriore lettura

Questo tutorial ti ha mostrato come costruire un generatore procedurale di base ma completo. Con pochi semplici passaggi abbiamo scritto codice che può creare nuovi livelli in un batter d'occhio. Speriamo che questo ti abbia dato un assaggio del potenziale di creare generatori di contenuti procedurali per i tuoi giochi!

Se vuoi saperne di più, Roguebasin è una grande fonte di informazioni sui sistemi procedurali di generazione. Si concentra principalmente su giochi roguelike, ma molte delle sue tecniche possono essere utilizzate in altri tipi di giochi, e ci sono molte ispirazioni per generare proceduralmente anche altre parti di un gioco!

Se vuoi saperne di più su Procedural Content Generation o Cellular Automata, ecco una versione online di The Game Of Life (anche se consiglio vivamente di digitare "Conway's Game Of Life" in Google). Ti potrebbe piacere anche Wolfram Tones, un affascinante esperimento nell'uso di automi cellulari per generare musica!