Generación procedimental de mapas 2D

Análisis e investigación del problema.

Autor: Alejandro Seguí Díaz
Tutores: Gonzalo A. Aranda Corral y Daniel Márquez Quintanilla

Contexto de la investigación

Videojuegos

Software de entretenimiento

Mercado lider indiscutible a nivel internacional

La industria cinematográfica deja paso a la industria del videojuego

Problema

Videojuegos requieren mapas

Consumo de recursos (horas/persona)

Equipo AAA

Desarrollador indie

Solución

Elaborar un generador de mapas

$\implies$ ahorro de recursos

Especificaciones

Coordinación

Cada juego tiene sus reglas y mecánicas

El sistema generador debe estar adaptado

Especificaciones de este proyecto han sido impuestas por

Definición del tipo de juego

  • Mapas de tiles en 2D
  • El jugador comienza en una habitación
  • Objetivo: llegar a una habitación considerada como final
  • Vista cenital
  • Captura de Action Hollywood, publicado por TCH en 1995 para recreativas

Especificaciones para la generación (I)

  • Partir de una lista de habitaciones previamente creadas
  • Maximizar el camino desde la habitación de inicio hasta la final
  • Fomentar caminos alternativos
  • Fomentar la variabilidad en los niveles generados

Especificaciones para la generación (II)

  • Mapa generado al inicio de la partida
  • No importa habitaciones inicial y final
  • Debe funcionar en sistemas móviles modernos
  • No hay restricción fuerte del tamaño del mapa

Estado del arte

Generación procedimental de contenido

Inicios en la demoscene

Campo emergente en la aplicación a videojuegos:

  • Generación de mapas
  • Storytelling procedimental
  • Generación de puzles

Representación

Topología de los elementos

Representación física de mapas y habitaciones: matriz de dos dimensiones

$\begin{matrix} 0 & 1 & 1 & 1 & 0 \\ 0 & 1 & 0 & 1 & 1 \\ 1 & 1 & 0 & 0 & 1 \\ 1 & 0 & 0 & 0 & 1 \\ 1 & 1 & 1 & 1 & 1 \end{matrix}$ $\leftrightarrow$

Habitaciones

Lista de puertas que servirán para conectar habitaciones

Representación dividida en:

  • Prefab: clase de una habitación
  • Instancia: instancia concreta de una habitación

Prefab de habitación

También nos referiremos como clase o modelo.

Contiene la matriz 2D asociada al modelo de habitación.

Puertas potenciales precomputadas

Puertas potenciales mostradas como tiles azules

Puertas potenciales

¿Cuándo un tile $t(x,y)$ es una puerta potencial?

$\begin{cases} o(x+1,y), i(x-1,y), s(x, y\pm1) & \quad \text{vertical} \\ o(x-1,y), i(x+1,y), s(x, y\pm1) & \quad \text{vertical} \\ o(x,y+1), i(x,y-1), s(x\pm1, y) & \quad \text{horizontal} \\ o(x,y-1), i(x,y+1), s(x\pm1, y) & \quad \text{horizontal} \\ \end{cases}$

$o(x,y) = outer(x,y)$: tile vacío exterior
$i(x,y) = inner(x,y)$: tile vacío interior
$s(x,y) = solid(x,y)$: tile sólido (pared)

Instancia de habitación

Contiene un subconjunto de todas las puertas potenciales del modelo asociado

Referencia al modelo

Además,

  • Lista de puertas activas
  • Lista de puertas no activas
  • Posición de la instancia en el mapa (si procede)

Mapa

Matriz 2D con habitaciones colocadas

Suficientemente grande como para colocar todas las habitaciones

Conexiones entre habitaciones, empleando una matriz superior

Estrategia constructiva

Definición

Construiremos el mapa a partir de movimientos $M_i(R_i,P_i)$ compuestos por:

  • $R_i$: modelo de habitación a colocar
  • $P_i$: posición del mapa donde se coloca

Cómputo de posibles movimientos

Dado un estado concreto del proceso

  • Lista de habitaciones colocadas
  • Lista de habitaciones por colocar

La lista de posibles movimientos se computará a partir de las posibles conexiones entre las puertas potenciales del mapa y de las habitaciones restantes

$$\bigcup_{r \in RR}posiblesConexiones(map, r)$$

$$RR = habitacionesRestantes$$

Conexiones entre el mapa y una habitación

$possibleConnections(map, r) =$
$\bigcup_{p \in rooms(map)} \left\{ cx(map,r,p,u,v,w) : \neg col(map, r, w) \right\}$

$w = v - u + \begin{cases} (-1,0) \\ (+1,0) \\ (0,-1) \\ (0,+1) \\ \end{cases}$

$potentialDoor(u, r), potentialDoor(v, p)$

Ejemplo de conexión entre mapa y habitación

Ilustración de posible conexión entre habitación restante y mapa

Flujo del algoritmo

Interfaz de selección de movimiento

Delegamos la selección de un movimiento entre todos los posibles a una interfaz que implementaremos



interface IMovementSelector {
    Movimiento ElegirMovimiento( Mapa mapa, List<Movimiento> movimientos );
}

Mapa GenerarMapa( List<Habitacion> habitaciones, IMovementSelector mapSolver ) {
  Mapa mapa = MapaVacio();
  while( !habitaciones.isEmpty() ) {
    List<Movimiento> movimientos = GenerarMovimientos( mapa, habitaciones );
    Movimiento elegido = mapSolver.ElegirMovimiento( mapa, movimientos );
    mapa.InsertarHabitacion( elegido.habitacion, elegido.posicion );
    habitaciones.remove( elegido.habitacion );
    GuardarMovimiento( elegido );
  }
  EstablecerRecorridoPrincipal( mapa );
  return mapa;
}
						

Guardado de movimientos

La selección de un movimiento será el cuello de botella

Guardamos la lista de movimientos necesaria para reconstruir el mapa

Sirve para mejora añadiendo backtracking, permitiéndonos reconstruir el mapa rápidamente

Implementación de interfaz de selección aleatoria

Propiedades

No tiene interés computacional

Elige un movimiento de forma aleatoria

Resultados buenos pero no proporciona ningún tipo de control

Eficiencia computacional

Ejemplo

Ejemplo de mapa generado por RandomSelector

Implementación de interfaz de selección basada en búsqueda

Método de búsqueda

Encontrar una solución que satisfaga el enunciado del problema

Problema debe ser discreto, accesible, estático, observable y determinista

Definición de acciones posibles en los estados (movimientos)

Búsqueda no informada

Estado inicial: mapa vacío

Función objetivo: todas las habitaciones colocadas

Función de coste.

Estrategia básica

Asignar una puntuación a cada movimiento (fitness)

Elegiremos movimiento con mejor puntuación

Delegamos el cómputo del fitness a una interfaz


interface IFitnessSolver {
    float ComputarFitness( Mapa mapa, Movimiento m );
}

class SearchMovementSelector implements IMovementSelector {
    IFitnessSolver fitnessSolver;
    Movimiento ElegirMovimiento( Mapa mapa, List<Movimiento> movimientos ) {
        List<Float> fitness;
        for( Movimiento m : movimientos ) {
            fitness[m.index] = fitnessSolver.ComputarFitness( mapa, m );
        }
        Movimiento elegido = ObtenerMejor( movimientos, fitness );
        return elegido;
    }
}
						

Árbol de búsqueda

Generado implícitamente

Múltiples objetivos

  • Tamaño del camino principal
  • Caminos no-principales
  • Cantidad de bifurcaciones (extra)

Fitness basado en múltiples objetivos

Implementamos la interfaz IFitnessSolver

Adición de interfaz para combinar todos los objetivos


interface IFitnessCombinator {
    float Combinar( float[] fitnesses );
}

class MultiObjectiveFitnessSolver implements IFitnessSolver {
    IFitnessCombinator fitnessCombinator;
    float ComputarFitness( Mapa mapa, Movimiento m ) {
        float fitness = new float[3];
        fitness[0] = ComputarObjetivo0( mapa, m );
        fitness[1] = ComputarObjetivo1( mapa, m );
        fitness[2] = ComputarObjetivo2( mapa, m );
        fitnessCombinator.Combinar( fitness );
    }
}
						

Tipos de combinadores de fitness

Combinador parametrizado

$ \sum_{f \in F} f \cdot k_f$
$F = \text{conjunto de objetivos}$
$k_f = \text{ponderación de objetivo f}$

Combinador parametrizado adaptativo

Notificamos con último movimiento elegido
$O_b$ = objetivo con mejor puntuación
$O_r = F - {O_b}$
$k_b = k_b \cdot decay, 0 \lt decay \lt 1$
$\forall O_i \in O_r, k_i = k_i \cdot attack, 1 \lt attack \lt \infty$

Fitness Caché

Cómputo de fitness es el cuello de botella del sistema

Se ha ideado una caché que evita computar todos los fitness en cada paso

Sacrifica calidad del algoritmo pero es posible controlar el impacto

Ejemplo de fallo de caché con objetivo de bifurcaciones

Flujo con caché

Fitness Caché (II)


interface IFitnessCache {
    Fitness Get( Movimiento m );
    void Cachear( Movimiento m, float fitness );
}
class SearchMovementSelector implements IMovementSelector {
    IFitnessSolver fitnessSolver;
    IFitnessCache fitnessCache;
    Movimiento ElegirMovimiento( Mapa mapa, List<Movimiento> movimientos ) {
        List<Float> fitness;
        for( Movimiento m : movimientos ) {
            Fitness f = fitnessCache.Get(m);
            if( f != null ) {
                fitness[m.index] = fitnessSolver.ComputarFitness( mapa, m );
                fitnessCache.Cachear( m, fitness[m.index] );
            } else {
                fitness[m.index] = f;
            }
        }
        Movimiento elegido = ObtenerMejor( movimientos, fitness );
        return elegido;
    }
}
						

Tipos de caché

Se han creado 3 tipos de caché:

  • NoCaché. Por defecto. No cachea nunca.
  • AlwaysCaché. Se cachea siempre.
  • Refresher. Se vacía la caché cada N habitaciones colocadas

Además, gracias a que se implementan con una interfaz, es posible idear otros tipos distintos.

Resumen de diseño

Otros componentes

Gestor de instancias por prefab

1 prefab - N instancias

En cómputo de posibles movimientos, comprobamos 1 instancia por modelo

Instancias a comprobar = número de prefabs

Conseguimos agilizar el sistema

Selector de prefabs

Tener control sobre tipo de habitacion elegida en cada paso

Nombre

Descripción

Dummy No interviene en la selección de prefab
Probabilidad Probabilidad de que se elija cada prefab
Ciclo Alterna entre prefabs

Divisor de movimientos

Elige un subconjunto entre todos los movimientos posibles

Basado en porcentaje

Experimentación

Parametrización

Una vez construido el sistema, es necesario configurar el generador con distintos parámetros

De esta forma, el desarrollador podrá adaptar los mapas generados a sus preferencias según el juego que vaya a construir

Combinador parametrizado (I)

Camino principal
100
Caminos alternativos
0
Bifurcaciones
0

Combinador parametrizado (II)

Camino principal
0
Caminos alternativos
100
Bifurcaciones
0

Combinador parametrizado (III)

Camino principal
0
Caminos alternativos
0
Bifurcaciones
100

Combinador parametrizado (IV)

Camino principal
5.8
Caminos alternativos
1
Bifurcaciones
0

Combinador parametrizado (V)

Camino principal
5.8
Caminos alternativos
1
Bifurcaciones
1

Combinador adaptativo (I)

Attack
1
Decay
0.5
Camino principal
10
Caminos alternativos
1
Bifurcaciones
1

Combinador adaptativo (II)

Attack
1
Decay
0.5
Camino principal
10
Caminos alternativos
10
Bifurcaciones
10

Combinador adaptativo (III)

Attack
1.1
Decay
0.9
Camino principal
10
Caminos alternativos
10
Bifurcaciones
10

Impacto de divisor de movimientos (parámetros)

Propiedad

Valor

DoorGenType ALL
SolverType BestSearch
CacheType NO CACHE

Impacto de divisor de movimientos (datos)

Tam. habs.

Num. modelos

Instancias / modelo

Total habs.

Divisor 0.75

Divisor 0.85

Divisor 0.95

10 2 20 40 0.1762 0.1967 0.3667
10 2 30 60 0.4944 0.6254 1.4391
10 4 20 80 1.2335 1.6429 4.0735
10 4 30 120 4.8804 6.6693 18.185

Impacto de divisor de movimientos (comparativa)

Impacto de subconjunto de puertas (parámetros)

Propiedad

Valor

DoorGenType RANDOM
SolverType BestSearch
Divisor de movimientos 1.0
CacheType NO CACHE

Impacto de subconjunto de puertas (datos)

Tam. habs.

Num. modelos

Instancias / modelo

Total habs.

40% puertas

60% puertas

80% puertas

10 4 8 32 1.7974 3.3016 4.6551
10 4 10 40 4.1856 8.2365 9.8173
10 6 8 48 15.4529 20.9266 29.2930
10 6 10 60 34.6856 57.3447 69.2746

Impacto de subconjunto de puertas (comparativa)

Impacto de caché refresher

Propiedad

Valor

DoorGenType ALL
SolverType BestSearch
Divisor de movimientos 1.0
CacheType REFRESHER

Impacto de caché refresher (datos)

Tam. habs.

Num. modelos

Instancias / modelo

Total habs.

Refresher 2

Refresher 5

Refresher 10

6 4 10 40 1.3768 1.0284 0.7652
6 4 15 60 6.7212 5.5230 3.4247
6 6 10 60 8.7299 6.7348 5.5665
6 6 15 90 43.7861 35.1948 26.3090

Impacto de caché refresher (comparativa)

Configuración AlwaysCaché

Propiedad

Valor

DoorGenType ALL
SolverType BestSearch
Divisor de movimientos 1.0
CacheType ALWAYS

Configuración NoCaché

Propiedad

Valor

DoorGenType ALL
SolverType BestSearch
Divisor de movimientos 1.0
CacheType NO CACHE

Configuración personalizada

Propiedad

Valor

DoorGenType RANDOM
% puertas generadas 0.5
CacheType REFRESHER
Divisor de refresco 10
SolverType BestSearch
Divisor de movimientos 0.9

Tam. habs.

Num. modelos

Instancias / modelo

Total habs.

No Caché

Always Caché

Personaliz.

8 4 2 8 0.0333 0.0173 0.0030
8 4 4 16 0.2818 0.0648 0.0122
8 4 6 24 1.2533 0.1948 0.0353
8 4 8 32 3.3726 0.4230 0.0779
8 6 2 12 0.1696 0.0666 0.0107
8 6 4 24 1.7436 0.2855 0.0480

Tam. habs.

Num. modelos

Instancias / modelo

Total habs.

No Caché

Always Caché

Personaliz.

8 6 6 36 8.6427 1.0330 0.1302
8 6 8 48 19.8587 2.7444 0.3154
8 8 2 16 0.5825 0.2523 0.0210
8 8 4 32 7.3916 1.1891 0.1030
8 8 6 48 33.7261 3.6201 0.3438
8 8 8 64 107.0143 9.6228 0.8998

Ejemplo real usando la configuración personalizada

Tamaños y parámetros parecidos a los necesitados según las especificaciones

Tam. habs.

Num. modelos

Instancias / modelo

Total habs.

Tiempo

6 5 1 5 0.0007
6 5 2 10 0.0029
6 5 3 15 0.0072
6 5 4 20 0.0167
6 10 1 10 0.0050
6 10 2 20 0.0265
6 10 3 30 0.0678
6 10 4 40 0.1601

Tam. habs.

Num. modelos

Instancias / modelo

Total habs.

Tiempo

6 15 1 15 0.0156
6 15 2 30 0.0905
6 15 3 45 0.3026
6 15 4 60 0.6858
6 20 1 20 0.0398
6 20 2 40 0.2525
6 20 3 60 0.7766
6 20 4 80 2.0803

Ejemplo grande usando la configuración personalizada

Tam. habs.

Num. modelos

Instancias / modelo

Total habs.

Tiempo

10 5 1 5 0.00348
10 5 2 10 0.01271
10 5 3 15 0.0245
10 10 1 10 0.0257
10 10 2 20 0.1215
10 10 3 30 0.1667
10 15 1 15 0.1143
10 15 2 30 0.3775
10 15 3 45 0.8163

Ejemplo con mucha variabilidad usando la configuración personalizada

Pasaremos a añadir muchos modelos distintos

Tam. habs.

Num. modelos

Instancias/modelo

Total habs.

Tiempo ejec.

6 20 1 20 0.0395
6 20 2 40 0.2504
6 30 1 30 0.1479
6 30 2 60 0.9845
6 40 1 40 0.4307
6 40 2 80 2.9425
6 50 1 50 0.9979
6 50 2 100 7.6388

Arreglo de ejemplo con mucha variabilidad

Propiedad

Valor

DoorGenType RANDOM
SolverType BestSearch
Divisor de movimientos 0.5
CacheType ALWAYS
% puertas generadas 0.3

Tam. habs.

Num. modelos

Instancias/modelo

Total habs.

Tiempo

6 50 2 100 1.6345024

Trabajo futuro.

Más tipos de caché

Cachear cada N movimientos

Cachear con probabilidad

Selección de puertas

Permitir al desarrollador elegir un subconjunto de puertas entre todas las posibles

Combinadores

Attack y decay personalizados por cada objetivo

Adición de objetivos

Verticalidad y horizontalidad

Condensación

Backtracking (I)

Según nuestro modelo actual, no se permite backtracking

Guardamos la lista de movimientos, el cual representa un estado concreto del sistema

Esto permite reanudar la búsqueda cuando sea conveniente

Para fomentar que el camino sea distinto, se puede forzar a elegir una posición con una distancia mínima a la elegida anteriormente

Backtracking (II)

EOF