Dungeon Generation senza corridoi e dipendenze dalla stanza


15

Sto realizzando un gioco con un mondo generato proceduralmente creato all'inizio del gioco, costituito da diverse aree rappresentate da griglie (diciamo, 8x8, 9x6, le dimensioni sarebbero idealmente arbitrarie). Queste aree dovrebbero essere collegate tra loro attraverso un elenco di dipendenze.

Esiste una connessione quando almeno 3 spazi di quella griglia sono esposti tra queste due aree. Nella cella centrale di quella 3 area di connessione spaziale si trova la porta tra le aree:

Ho cercato di trovare un modo per collegarli, ma diventa sempre più complesso quanto più aree devi considerare allo stesso tempo.

Ho provato alcuni prototipi di carta e sebbene sia un processo molto semplice quando lo faccio visivamente, non ho scoperto una buona serie di espressioni matematiche che mi permettono di posizionare le stanze con la stessa efficienza con il codice.

Ecco un esempio "semplice" con cui sto lottando in questo momento:

  • L'area "a" deve essere collegata a "b" e "c"
  • L'area "b" deve essere collegata a "a" e "d"
  • L'area 'c' deve essere collegata a 'a' e 'd'
  • L'area 'd' deve essere collegata a 'b' e 'c'

Considera, per semplicità, stiamo posizionando le stanze in base al loro ordine di apparizione nell'elenco (ne ho provate altre). Quindi mi sto avvicinando a questo come al tuo algoritmo procedurale standard di generazione sotterranea.

Posizioniamo "a" in qualsiasi punto del tabellone, poiché è la prima area. Quindi, scegliamo un muro a caso e, dal momento che nulla è collegato a quel muro, possiamo posizionare 'b' lì:

Ora dobbiamo posizionare 'c', ma 'a' è già sul tabellone e ha un muro occupato, quindi decidiamo di metterlo su un altro muro. Ma non tutti i posizionamenti lo faranno, perché 'd' sta arrivando e deve essere collegato anche a 'b' e 'c':

Ho provato una possibile limitazione che 2 stanze che hanno lo stesso insieme di dipendenze non possono trovarsi su pareti opposte, ma anche ciò non garantisce il successo:

E in altri casi, dove le aree hanno dimensioni diverse, trovarsi su pareti opposte può funzionare:

Inoltre, non considerare un muro usato è un presupposto imperfetto poiché esclude soluzioni valide:

Ho provato a cercare ricerche su altri algoritmi di generazione procedurale o simili, come gli algoritmi Optimal Rectangle Packing e Graph Layout, ma di solito quegli algoritmi non tengono conto di tutti i vincoli di questo problema e sono difficili da mescolare insieme.

Ho pensato a una serie di approcci, incluso il posizionamento di un'area e il backtrack fino a quando non viene trovato un posizionamento adatto, ma sembrano molto dipendenti da tentativi ed errori e costosi in termini di calcolo. Ma, data la vasta ricerca sugli ultimi due problemi che ho citato, potrebbe essere l'unica / migliore soluzione?

Volevo solo vedere se qualcuno ha avuto problemi simili in passato o è disposto ad aiutarmi a capirlo e darmi alcuni suggerimenti su dove dovrei iniziare con l'algoritmo. Oppure, in mancanza, dovrò cercare di allentare i vincoli che ho fissato.


Le stanze devono essere completamente quadrate?
Wolfdawn,

Se vuoi dire se devono avere 4 muri e non di più, allora sì, ma l'ho fatto per semplificare lo spazio mondiale. Devo calcolare facilmente lo spazio occupato da ciascuna area, quindi so se sarò in grado di inserire tutto ciò che desidero.
Joana Almeida,

Risposte:


6

Questo è un bel problema. Credo che possa essere risolto utilizzando la pianificazione dell'azione nello spazio dei posizionamenti delle stanze.

Definisci lo stato del mondo come segue:

//State:
//    A list of room states.
//    Room state:
//      - Does room exist?
//      - Where is room's location?
//      - What is the room's size?

Definisci un vincolo come:

 // Constraint(<1>, <2>):
 //  - If room <1> and <2> exist, Room <1> is adjacent to Room <2>

Dove "adiacente" è come descritto (condivisione di almeno 3 vicini)

Si dice che un vincolo è invalidato ogni volta che le due stanze non sono adiacenti ed entrambe le stanze esistono.

Definire uno stato per essere valido quando:

// foreach Constraint:
//        The Constraint is "not invalidated".
// foreach Room:
//       The room does not intersect another room.

Definisci un'azione come posizionamento di una stanza, dato uno stato attuale. L' azione è valida ogni volta che lo stato risultante dall'azione è valido. Pertanto, possiamo generare un elenco di azioni per ogni stato:

// Returns a list of valid actions from the current state
function List<Action> GetValidActions(State current, List<Constraint> constraints):
    List<Action> actions = new List<Action>();
    // For each non-existent room..
    foreach Room room in current.Rooms:
        if(!room.Exists)
            // Try to put it at every possible location
            foreach Position position in Dungeon:
                 State next = current.PlaceRoom(room, position)
                 // If the next state is valid, we add that action to the list.
                 if(next.IsValid(constraints))
                     actions.Add(new Action(room, position));

Ora, ciò che ti rimane è un grafico , in cui gli stati sono nodi e le azioni sono collegamenti. L'obiettivo è quello di trovare uno stato che sia valido e tutte le stanze siano state posizionate. Possiamo trovare un posizionamento valido effettuando una ricerca nel grafico in modo arbitrario, magari utilizzando una ricerca approfondita. La ricerca sarà simile a questa:

// Given a start state (with all rooms set to *not exist*), and a set of
// constraints, finds a valid end state where all the constraints are met,
// using a depth-first search.
// Notice that this gives you the power to pre-define the starting conditions
// of the search, to for instance define some key areas of your dungeon by hand.
function State GetFinalState(State start, List<Constraint> constraints)
    Stack<State> stateStack = new Stack<State>();
    State current = start;
    stateStack.push(start);
    while not stateStack.IsEmpty():
        current = stateStack.pop();
        // Consider a new state to expand from.
        if not current.checked:
            current.checked = true;
            // If the state meets all the constraints, we found a solution!
            if(current.IsValid(constraints) and current.AllRoomsExist()):
                return current;

            // Otherwise, get all the valid actions
            List<Action> actions = GetValidActions(current, constraints);

            // Add the resulting state to the stack.
            foreach Action action in actions:
                State next = current.PlaceRoom(action.room, action.position);
                stateStack.push(next);

    // If the stack is completely empty, there is no solution!
    return NO_SOLUTION;

Ora la qualità del dungeon generato dipenderà dall'ordine in cui vengono considerate le stanze e le azioni. Puoi ottenere risultati interessanti e diversi probabilmente semplicemente permutando casualmente le azioni che intraprendi in ogni fase, facendo così una passeggiata casuale attraverso il grafico stato-azione. L'efficienza della ricerca dipenderà in larga misura dalla velocità con cui è possibile rifiutare gli stati non validi. Può essere utile generare stati validi dai vincoli ogni volta che si desidera trovare azioni valide.


Divertente dovresti menzionare questa soluzione. Ho parlato con un amico in precedenza e mi ha detto che probabilmente dovrei esaminare gli algoritmi di ricerca basata sugli alberi, ma non ero sicuro di come usarli in questo contesto. Il tuo post ha aperto gli occhi! Sembra certamente una soluzione fattibile se gestisci la generazione dei rami e fai alcune ottimizzazioni per tagliare i rami cattivi il più presto possibile.
Joana Almeida,

7

Le priorità della tua generazione sono in conflitto. Quando si genera livelli, il primo obiettivo dovrebbe essere un nastro di planare (non sovrapposte), collegate punti , indipendentemente dalle dimensioni. Quindi procedi a creare stanze dai punti all'interno di quella rete. Creare prima le forme di una stanza è un errore, in generale. Crea prima la connettività, quindi vedi quali moduli di room possono essere ospitati al suo interno.

Algoritmo generale

  1. Crea una griglia del pavimento quantizzata di dimensioni sufficienti per supportare il tuo livello, usando una matrice o un'immagine 2D.

  2. Spargere i punti in modo casuale attraverso questo spazio vuoto. È possibile utilizzare un semplice controllo casuale su ogni riquadro per vedere se ottiene un punto o utilizzare la distribuzione standard / gaussiana per disperdere i punti. Assegna un valore colore / numerico univoco a ciascun punto. Questi sono ID. (PS Se dopo questo passaggio senti di aver bisogno di ridimensionare il tuo spazio, in ogni caso, fallo.)

  3. Per ciascuno di questi punti generati, in sequenza, aumentare progressivamente un cerchio di limiti o rettangolo di limiti di un singolo passaggio (in genere una velocità di 0,5-1,0 celle / pixel per passaggio) in xe y. Puoi far crescere tutti i limiti in parallelo, iniziando tutti dalla dimensione zero nello stesso passaggio, oppure puoi iniziare a farli crescere in momenti diversi e a velocità diverse, dando la tendenza alla dimensione di quelli che iniziano prima (immagina che crescano piantine, dove alcuni iniziare tardi). Per "crescere" intendo riempire i limiti appena incrementati con il colore / ID univoco al punto iniziale per tali limiti. Una metafora per questo sarebbe tenere pennarelli sul retro di un pezzo di carta e guardare le macchie d'inchiostro di diversi colori crescere, fino a quando non si incontrano.

  4. Ad un certo punto i limiti di un punto e un altro punto si scontreranno, durante la fase di crescita. Questo è il punto in cui dovresti smettere di aumentare i limiti per quei due punti - almeno nel senso uniforme descritto nel passaggio 3.

  5. Una volta che hai ampliato il più possibile i limiti dei punti e interrotto tutti i processi di crescita, avrai una mappa che dovrebbe essere in gran parte, ma non interamente riempita. Ora potresti voler riempire quegli spazi vuoti, che suppongo siano bianchi, come se colorassero un foglio di carta.

Post-processo di riempimento dello spazio

È possibile utilizzare una varietà di tecniche per riempire gli spazi vuoti / bianchi che rimangono, per il passaggio 5:

  • Chiedi a una singola area vicina e già colorata di rivendicare lo spazio, riempiendola di quel colore in modo che tutto si unisca.
  • Riempi con nuovi colori / numeri / ID non ancora utilizzati, in modo da formare aree completamente nuove.
  • Approccio round robin in modo tale che ogni area vicina già piena "cresca" un po 'nello spazio vuoto. Pensa agli animali che bevono intorno a un abbeveratoio: tutti ottengono un po 'd'acqua.
  • Non riempire completamente lo spazio vuoto, basta attraversarlo per collegare le aree esistenti usando passaggi diritti.

Perturbazione

Come ultimo passo per rendere le cose più organiche, potresti fare perturbazioni ai bordi in vari gradi, sulle celle dei bordi delle aree. Assicurati solo di non bloccare percorsi di movimento cruciali.

Teoria, per interesse

Questo è simile all'approccio adottato nei diagrammi Voronoi / Triangolazione di Delaunay , tranne per il fatto che in precedenza non si creano esplicitamente bordi - invece, quando le aree delimite si scontrano, la crescita cessa. Noterai che i diagrammi Voronoi riempiono lo spazio; questo perché non cessano la crescita semplicemente toccando, ma piuttosto con un certo grado di sovrapposizione nominale. Potresti provare simile.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.