Generación de mazmorras sin pasillos y dependencias de habitaciones

15

Estoy haciendo un juego con un mundo generado por procedimientos creado al comienzo del juego, que consta de varias áreas representadas por cuadrículas (por ejemplo, 8x8, 9x6, los tamaños serían idealmente arbitrarios). Se supone que estas áreas están conectadas entre sí a través de una lista de dependencias.

Existe una conexión cuando al menos 3 espacios de esa cuadrícula están expuestos entre esas dos áreas. En la celda central de esa área de conexión de 3 espacios está la puerta entre las áreas:

He estado tratando de encontrar una manera de conectarlos, pero se vuelve cada vez más complejo a medida que se necesitan más áreas al mismo tiempo.

Probé algunos prototipos de papel y, aunque es un proceso muy simple al hacerlo visualmente, no he encontrado un buen conjunto de expresiones matemáticas que me permita colocar habitaciones con la misma eficiencia por código.

Aquí hay un ejemplo "simple" con el que estoy luchando en este momento:

  • El área 'a' necesita conectarse a 'b' y 'c'
  • El área 'b' debe estar conectada a 'a' y 'd'
  • El área 'c' debe conectarse a 'a' y 'd'
  • El área 'd' necesita conectarse a 'b' y 'c'

Considere, por simplicidad, estamos colocando las habitaciones por su orden de aparición en la lista (he probado otras). Así que me estoy acercando a esto como su algoritmo de procedimiento estándar de Dungeon Generation.

Colocamos 'a' en cualquier lugar del tablero, ya que es la primera área. A continuación, elegimos un muro al azar y, dado que nada está conectado a ese muro, podemos colocar 'b' allí:

Ahora necesitamos colocar 'c', pero 'a' ya está en el tablero y tiene una pared ocupada, por lo que decidimos colocarlo en otra pared. Pero no todas las ubicaciones funcionarán, porque 'd' se acerca y también necesita conectarse a 'b' y 'c':

Intenté una posible limitación de que 2 habitaciones que tienen el mismo conjunto de dependencias no pueden estar en paredes opuestas, pero incluso eso no garantiza el éxito:

Y en otros casos, donde las áreas tienen diferentes tamaños, estar en paredes opuestas puede funcionar:

Además, no considerar una pared usada es una suposición defectuosa ya que descarta soluciones válidas:

He intentado buscar investigaciones sobre otros algoritmos de Generación de Procedimientos o similares, como los algoritmos Optimal Rectangle Packing y Graph Layout, pero generalmente esos algoritmos no tienen en cuenta todas las restricciones de este problema y son difíciles de mezclar.

Pensé en un montón de enfoques, incluida la colocación de un área y retroceso hasta encontrar una ubicación adecuada, pero parecen muy dependientes de la prueba y el error y son costosos en términos de cómputo. Pero, dada la extensa investigación sobre los dos últimos problemas que mencioné, ¿podría ser la única / mejor solución?

Solo quería ver si alguien ha tenido problemas similares en el pasado o si está dispuesto a ayudarme a resolver esto y darme algunos consejos sobre dónde debo comenzar con el algoritmo. O, en su defecto, tendré que buscar aflojar las restricciones que he establecido.

Joana Almeida
fuente
¿Las habitaciones tienen que ser completamente cuadradas?
wolfdawn
Si te refieres a si tienen que tener 4 paredes y no más, sí, pero lo hice para simplificar el espacio mundial. Necesito calcular fácilmente el espacio que ocupa cada área para saber si podré poner todo lo que quiera en ella.
Joana Almeida

Respuestas:

6

Este es un problema genial. Creo que se puede resolver mediante la planificación de acciones en el espacio de las ubicaciones de las habitaciones.

Defina el estado del mundo de la siguiente manera:

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

Defina una restricción como:

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

Donde "adyacente" es como lo describió (compartiendo al menos 3 vecinos)

Se dice que una restricción se invalida cuando las dos habitaciones no son adyacentes y ambas habitaciones existen.

Defina un estado para que sea válido cuando:

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

Definir una acción como una ubicación de una habitación, dado un estado actual. La acción es válida siempre que el estado resultante de la acción sea válido. Por lo tanto, podemos generar una lista de acciones para cada estado:

// 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));

Ahora, lo que queda es un gráfico , donde los Estados son nodos y las Acciones son enlaces. El objetivo es encontrar un Estado que es tanto válida, y todas las habitaciones han sido colocados. Podemos encontrar una ubicación válida buscando en el gráfico de manera arbitraria, quizás usando una búsqueda de profundidad primero. La búsqueda se verá más o menos así:

// 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;

Ahora la calidad de la mazmorra generada dependerá del orden en que se consideren las habitaciones y las acciones. Puede obtener resultados interesantes y diferentes probablemente simplemente permutando aleatoriamente las acciones que realiza en cada etapa, haciendo así un recorrido aleatorio a través del gráfico de estado-acción. La eficacia de la búsqueda dependerá en gran medida de la rapidez con la que pueda rechazar estados no válidos. Puede ser útil generar estados válidos a partir de las restricciones siempre que desee encontrar acciones válidas.

mklingen
fuente
Es curioso que debas mencionar esta solución. Hablé con un amigo antes y él mencionó que probablemente debería analizar los algoritmos de búsqueda basada en árboles, pero no estaba seguro de cómo usarlos en este contexto. ¡Tu publicación fue reveladora! Ciertamente parece una solución factible si gestionas la generación de sucursales y haces algunas optimizaciones para cortar sucursales defectuosas lo antes posible.
Joana Almeida
7

Sus prioridades de generación están en conflicto. Al generar niveles, su primer objetivo debe ser una red de puntos planos (no superpuestos) conectados , independientemente de la escala. Luego proceda a crear habitaciones a partir de los puntos dentro de esa red. Crear formas de habitación primero es un error, generalmente hablando. Primero, cree la conectividad y luego vea qué formas de habitación se pueden acomodar dentro de eso.

Algoritmo General

  1. Cree una cuadrícula de piso cuantizada de tamaño suficiente para soportar su nivel, usando una matriz o imagen 2D.

  2. Esparce puntos al azar en este espacio vacío. Puede usar una verificación aleatoria simple en cada mosaico para ver si obtiene un punto, o usar la distribución estándar / gaussiana para dispersar puntos. Asigne un color / valor numérico único a cada punto. Estas son las identificaciones. (PD: si después de este paso sientes que necesitas ampliar tu espacio, hazlo).

  3. Para cada punto generado de este tipo, en secuencia, haga crecer gradualmente un círculo de límites o un rectángulo de límites en un solo paso (generalmente una tasa de 0.5-1.0 celdas / píxeles por paso) en xy y. Puede crecer todos los límites en paralelo, todos comenzando desde el tamaño cero en el mismo paso, o puede comenzar a cultivarlos en diferentes momentos y a diferentes velocidades, dando un sesgo al tamaño de los que comienzan antes (imagine que las plántulas crecen, donde algunas empezar tarde) Por "crecer" me refiero a rellenar los límites recién incrementados con el color / ID único para el punto de partida de esos límites. Una metáfora para esto sería sostener rotuladores contra el reverso de un trozo de papel y ver crecer las manchas de tinta de diferentes colores, hasta que se encuentren.

  4. En algún momento, los límites de algún punto y otro punto van a chocar, durante el paso de crecimiento. Este es el punto en el que debe dejar de aumentar los límites para esos dos puntos, al menos en el sentido uniforme descrito en el paso 3.

  5. Una vez que haya aumentado los límites de todos los puntos lo más posible y haya detenido todos los procesos de crecimiento, tendrá un mapa que debe estar en gran medida, pero no completamente. Es posible que ahora desee empacar esos espacios vacíos, que supongo que son blancos, como si estuvieran coloreando una hoja de papel.

Post-proceso de relleno de espacio

Se puede usar una variedad de técnicas para completar los espacios vacíos / blancos que quedan, según el paso 5:

  • Haga que un área vecina, ya coloreada, reclame el espacio, inundando llenándolo de ese color para que todo se una.
  • Inunda con nuevos colores / números / ID aún no utilizados, de modo que formen áreas completamente nuevas.
  • Enfoque de round robin de modo que cada área vecina ya llena "crezca" un poco en el espacio vacío. Piense en los animales que beben alrededor de un abrevadero: todos obtienen algo del agua.
  • No llene totalmente el espacio vacío, solo crúcelo para vincular áreas existentes utilizando pasajes rectos.

Perturbación

Como paso final para hacer que las cosas se vean más orgánicas, puede hacer perturbaciones de bordes en diversos grados, en las celdas de los bordes de las áreas. Solo asegúrese de no bloquear rutas de movimiento cruciales.

Teoría, por el interés

Esto es similar al enfoque adoptado en los Diagramas de Voronoi / Triangulación de Delaunay , excepto que en lo anterior no está creando bordes explícitamente; en cambio, cuando las áreas delimitadas chocan, el crecimiento cesa. Notarás que los diagramas de Voronoi llenan el espacio; Esto se debe a que no dejan de crecer simplemente al tocarlos, sino más bien por algún grado nominal de superposición. Podrías intentar algo similar.

Ingeniero
fuente