Cuocia i tuoi dungeon 3D con ricette procedurali

In questo tutorial, imparerai come costruire dungeon complessi da parti prefabbricate, griglie non vincolate a 2D o 3D. I tuoi giocatori non saranno mai a corto di dungeon da esplorare, i tuoi artisti apprezzeranno la libertà creativa e il tuo gioco avrà una migliore rigiocabilità.

Per trarre vantaggio da questo tutorial, è necessario comprendere le trasformazioni 3D di base e sentirsi a proprio agio con i grafici delle scene e i sistemi di componenti di entità.

Un po 'di storia

Uno dei primi giochi a usare la generazione del mondo procedurale era Rogue. Realizzato nel 1980, presentava dungeon basati su griglia, generati dinamicamente. Grazie a questo, non ci sono due giochi identici, e il gioco ha generato un nuovo genere di giochi, chiamato "roguelikes". Questo tipo di dungeon è ancora abbastanza comune oltre 30 anni dopo.

Nel 1996, Daggerfall è stato rilasciato. Contiene dungeon 3D e città procedurali, che hanno permesso agli sviluppatori di creare migliaia di luoghi unici, senza doverli costruire manualmente tutti. Anche se il suo approccio 3D offre molti vantaggi rispetto ai classici dungeon della griglia 2D, non è molto comune.


Questa immagine mostra una parte di un dungeon più grande, estratta per illustrare quali moduli sono stati usati per costruirlo. L'immagine è stata generata con "Daggerfall Modeling", scaricata da dfworkshop.net.

Ci concentreremo sulla generazione di dungeon simili a quelli di Daggerfall.

Come costruire un Dungeon?

Per costruire un dungeon, dobbiamo definire cos'è un dungeon. In questo tutorial, definiremo un dungeon come un insieme di moduli (modelli 3D) collegati tra loro in base a un insieme di regole. Noi useremo camere collegato da corridoi e giunzioni:

  • UN camera è una vasta area che ha una o più uscite
  • UN corridoio è un'area stretta e lunga che può essere inclinata e ha esattamente due uscite
  • UN giunzione è una piccola area che ha tre o più uscite

In questo tutorial, utilizzeremo modelli semplici per i moduli: le loro mesh conterranno solo floor. Useremo tre di ciascuno: stanze, corridoi e incroci. Visualizzeremo i marker di uscita come oggetti asse, con l'asse X -X / + rosso, + l'asse Y verde e + l'asse Z blu.

Moduli usati per costruire un dungeon

Si noti che l'orientamento delle uscite non è limitato a incrementi di 90 gradi.

Quando si tratta di collegare i moduli, definiremo le seguenti regole:

  • Le camere possono connettersi ai corridoi
  • I corridoi possono connettersi a stanze o incroci
  • Le giunzioni possono connettersi ai corridoi

Ogni modulo contiene un insieme di uscite-contrassegnare oggetti con una posizione e rotazione note. Ogni modulo è taggato per dire di che tipo si tratta, e ogni uscita ha una lista di tag a cui è possibile connettersi.

Al più alto livello, il processo di costruzione del dungeon è il seguente:

  1. Istanziare un modulo di partenza (preferibilmente uno con un numero maggiore di uscite).
  2. Istanziare e connettere moduli validi a ciascuna delle uscite non connesse del modulo.
  3. Ricostruisci un elenco di uscite non connesse in tutto il dungeon fino ad ora.
  4. Ripeti il ​​processo finché non viene creato un dungeon abbastanza grande.
Uno sguardo alle iterazioni dell'algoritmo al lavoro.

Il processo dettagliato di collegamento di due moduli insieme è:

  1. Scegli un'uscita non connessa dal vecchio modulo.
  2. Scegli un prefabbricato di un nuovo modulo con tag di corrispondenza dei tag consentiti dall'uscita del vecchio modulo.
  3. Istanzia il nuovo modulo.
  4. Scegli un'uscita dal nuovo modulo.
  5. Collega i moduli: abbina l'uscita del nuovo modulo a quella precedente.
  6. Segna entrambe le uscite come connesse o semplicemente cancellale dal grafico della scena.
  7. Ripeti per il resto delle uscite non connesse del vecchio modulo.

Per connettere insieme due moduli, dobbiamo allinearli (ruotarli e tradurli nello spazio 3D), in modo che un'uscita dal primo modulo corrisponda a un'uscita dal secondo modulo. Le uscite sono accoppiamento quando la loro posizione è la stessa e i loro assi + Z sono opposti, mentre i loro assi + Y sono corrispondenti.

L'algoritmo per farlo è semplice:

  1. Ruota il nuovo modulo sull'asse + Y con l'origine della rotazione nella nuova posizione dell'uscita, in modo che l'asse + Z della vecchia uscita sia opposto all'asse + Z della nuova uscita, e i loro assi + Y siano gli stessi.
  2. Traduci il nuovo modulo in modo che la nuova posizione dell'uscita sia uguale alla posizione della vecchia uscita.

Collegamento di due moduli.

Implementazione

Lo pseudo-codice è Python-ish, ma dovrebbe essere leggibile da chiunque. Il codice sorgente di esempio è un progetto Unity.

Supponiamo che stiamo lavorando con un sistema di componenti di entità che contiene entità in un grafico di scena, definendo la loro relazione genitore-figlio. Un buon esempio di un motore di gioco con un tale sistema è Unity, con i suoi oggetti e componenti di gioco. I moduli e le uscite sono entità; le uscite sono figli di moduli. I moduli hanno un componente che definisce il loro tag e le uscite hanno un componente che definisce i tag a cui è consentito connettersi.

Ci occuperemo innanzitutto dell'algoritmo di generazione di dungeon. Il vincolo finale che useremo è un numero di iterazioni di passaggi di generazione di dungeon.

 def generate_dungeon (starting_module_prefab, module_prefabs, iterations): starting_module = instantiate (starting_module_prefab) pending_exits = elenco (starting_module.get_exits ()) while iterations> 0: new_exits = [] per pending_exit in pending_exits: tag = random.choice (pending_exit.tags) new_module_prefab = get_random_with_tag (module_prefabs, tag) new_module_instance = instantiate (new_module_prefab) exit_to_match = random.choice (new_module_instance.exits) match_exits (pending_exit, exit_to_match) per new_exit in new_module_instance.get_exits (): se new_exit! = exit_to_match: new_exits.append (new_exit ) pending_exits = new_exits iterations - = 1

Il istanziare () function crea un'istanza di un modulo prefabbricato: crea una copia del modulo, insieme alle sue uscite, e le inserisce nella scena. Il get_random_with_tag () funzione itera su tutti i moduli prefabbricati e ne seleziona uno a caso, taggato con il tag fornito. Il random.choice () la funzione ottiene un elemento casuale da una lista o un array passato come parametro.

Il match_exits la funzione è dove avviene tutta la magia, ed è mostrata in dettaglio qui sotto:

 def match_exits (old_exit, new_exit): new_module = new_exit.parent forward_vector_to_match = old_exit.backward_vector corrective_rotation = azimuth (forward_vector_to_match) - azimuth (new_exit.forward_vector) rotate_around_y (new_module, new_exit.position, corrective_rotation) corrective_translation = old_exit.position - new_exit.position translate_global (new_module, corrective_translation) def azimut (vector): # Restituisce l'angolo firmato questo vettore viene ruotato rispetto all'asse globale + Z avanti = [0, 0, 1] return vector_angle (forward, vector) * math.copysign (vector. X)

Il backward_vector la proprietà di un'uscita è il suo vettore -Z. Il rotate_around_y () funzione ruota l'oggetto attorno a un asse + Y con il suo perno in un punto previsto, con un angolo specificato. Il translate_global () la funzione traduce l'oggetto con i suoi figli nello spazio globale (scena), indipendentemente da qualsiasi relazione figlio di cui possa far parte. Il vector_angle () la funzione restituisce un angolo tra due vettori arbitrari e, infine, il math.copysign () la funzione copia il segno di un numero fornito: -1 per un numero negativo, 0 per zero, e +1 per un numero positivo.

Estensione del generatore

L'algoritmo può essere applicato ad altri tipi di generazione mondiale, non solo a dungeon. Possiamo estendere la definizione di un modulo per coprire non solo parti del dungeon come stanze, corridoi e incroci, ma anche mobili, scrigni del tesoro, decorazioni della stanza, ecc. Posizionando i marcatori di uscita nel centro di una stanza, o in una stanza muro e taggandolo come a bottino, decorazione, o anche mostro, possiamo dare vita al dungeon, con oggetti che puoi rubare, ammirare o uccidere.

C'è solo una modifica che deve essere fatta, in modo che l'algoritmo funzioni correttamente: uno dei marcatori presenti in un oggetto posizionabile deve essere contrassegnato come predefinito, in modo che venga sempre selezionato come quello che sarà allineato alla scena esistente.


Nell'immagine sopra, una stanza, due casse, tre pilastri, un altare, due luci e due oggetti sono stati creati e taggati. Una stanza contiene una serie di marcatori che fanno riferimento ai tag di altri modelli, come ad esempio il petto, pilastro, altare, o luce a muro. Un altare ha tre articolo marcatori su di esso. Applicando la tecnica di generazione di dungeon ad una singola stanza, possiamo creare numerose varianti di essa.

Lo stesso algoritmo può essere utilizzato per creare elementi procedurali. Se vuoi creare una spada, puoi definire la sua presa come un modulo di partenza. L'impugnatura si collegava al pomello e alla guardia incrociata. Il cross-guard si collegherebbe alla lama. Avendo solo tre versioni di ciascuna delle parti della spada, è possibile generare 81 spade uniche.

Avvertenze

Probabilmente hai notato alcuni problemi con il funzionamento di questo algoritmo.

Il primo problema è che la versione più semplice di esso costruisce i dungeon come un albero di moduli, con la radice come modulo di partenza. Se segui qualsiasi ramo della struttura del dungeon, sei sicuro di colpire un vicolo cieco. I rami degli alberi non sono interconnessi e il dungeon mancherà di anelli di stanze o corridoi. Un modo per ovviare a questo sarebbe mettere da parte alcune delle uscite del modulo per l'elaborazione successiva e non collegare nuovi moduli a queste uscite. Una volta che il generatore ha attraversato abbastanza iterazioni, avrebbe scelto un paio di uscite a caso e avrebbe cercato di collegarle con una serie di corridoi. C'è un po 'di lavoro algoritmico che dovrebbe essere fatto, al fine di trovare una serie di moduli e un modo per interconnetterli in un modo che creerebbe un percorso passabile tra queste uscite. Questo problema di per sé è abbastanza complesso da meritare un articolo separato.

Un altro problema è che l'algoritmo non è a conoscenza delle caratteristiche spaziali dei moduli che colloca; conosce solo le uscite contrassegnate, i loro orientamenti e posizioni. Ciò causa la sovrapposizione dei moduli. Un'aggiunta di un semplice controllo di collisione tra un nuovo modulo da collocare attorno ai moduli esistenti consentirebbe all'algoritmo di costruire dungeon che non soffrono di questo problema. Quando i moduli si scontrano, potrebbe scartare il modulo che ha provato a posizionare e invece ne proverebbe uno diverso.


La più semplice implementazione dell'algoritmo senza controlli di collisione provoca la sovrapposizione dei moduli.

Gestire le uscite e i loro tag è un altro problema. L'algoritmo suggerisce di definire i tag su ogni istanza di uscita e di taggare tutte le stanze, ma questo è un bel po 'di lavoro di manutenzione, se c'è un modo diverso di collegare i moduli che vorresti provare. Ad esempio, se si desidera consentire alle stanze di connettersi ai corridoi e agli incroci invece dei soli corridoi, è necessario passare attraverso tutte le uscite di tutti i moduli della stanza e aggiornare i tag. Un modo per aggirare questo è definire le regole di connettività su tre livelli separati: dungeon, module e exit. Il livello di Dungeon definirà le regole per l'intero dungeon: definirà quali tag possono interconnettere. Alcune camere sarebbero in grado di ignorare le regole di connettività, quando vengono elaborate. Potresti avere una stanza "capo" che garantisca che ci sia sempre una stanza "tesoro" dietro di essa. Alcune uscite annullerebbero i due livelli precedenti. La definizione dei tag per uscita consente la massima flessibilità, ma a volte troppa flessibilità non è così buona.

La matematica a virgola mobile non è perfetta e questo algoritmo si basa su di esso. Tutte le trasformazioni di rotazione, gli orientamenti di uscita arbitraria e le posizioni si sommano e possono causare artefatti come giunture o sovrapposizioni in corrispondenza delle uscite, in particolare dal centro del mondo. Se questo fosse troppo evidente, è possibile estendere l'algoritmo per posizionare un puntello aggiuntivo dove i moduli si incontrano, come un telaio della porta o una soglia. Il tuo artista amichevole troverà sicuramente un modo per nascondere le imperfezioni. Per i dungeon di dimensioni ragionevoli (inferiori a 10.000 unità), questo problema non è nemmeno evidente, assumendo che sia stata prestata sufficiente attenzione quando si posizionano e ruotano i marker di uscita dei moduli.

Conclusione

L'algoritmo, nonostante alcune delle sue mancanze, offre un modo diverso di guardare alla generazione dei dungeon. Non sarai più costretto a virate di 90 gradi e stanze rettangolari. I tuoi artisti apprezzeranno la libertà creativa che questo approccio offrirà e i tuoi giocatori apprezzeranno la sensazione più naturale dei dungeon.