Autor: Alejandro Seguí Díaz
Tutores: Gonzalo A. Aranda Corral y Daniel Márquez Quintanilla
Software de entretenimiento
Mercado lider indiscutible a nivel internacional
La industria cinematográfica deja paso a la industria del videojuego
Videojuegos requieren mapas
Consumo de recursos (horas/persona)
|
|
Elaborar un generador de mapas
$\implies$ ahorro de recursos
Cada juego tiene sus reglas y mecánicas
El sistema generador debe estar adaptado
Especificaciones de este proyecto han sido impuestas por
Captura de Action Hollywood, publicado por TCH en 1995 para recreativas
Inicios en la demoscene
Campo emergente en la aplicación a videojuegos:
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$ |
Lista de puertas que servirán para conectar habitaciones
Representación dividida en:
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
¿Cuándo un tile $t(x,y)$ es una puerta potencial?
$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)
Contiene un subconjunto de todas las puertas potenciales del modelo asociado
Referencia al modelo
Además,
Matriz 2D con habitaciones colocadas
Suficientemente grande como para colocar todas las habitaciones
Conexiones entre habitaciones, empleando una matriz superior
Construiremos el mapa a partir de movimientos $M_i(R_i,P_i)$ compuestos por:
Dado un estado concreto del proceso
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$$
$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)$
Ilustración de posible conexión entre habitación restante y mapa
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;
}
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
No tiene interés computacional
Elige un movimiento de forma aleatoria
Resultados buenos pero no proporciona ningún tipo de control
Eficiencia computacional
Ejemplo de mapa generado por RandomSelector
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.
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;
}
}
Generado implícitamente
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 );
}
}
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
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;
}
}
Se han creado 3 tipos de caché:
Además, gracias a que se implementan con una interfaz, es posible idear otros tipos distintos.
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
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 |
Elige un subconjunto entre todos los movimientos posibles
Basado en porcentaje
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
|
|
|
|
|
|
|
|
Propiedad |
Valor |
DoorGenType | ALL |
SolverType | BestSearch |
CacheType | NO CACHE |
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 |
Propiedad |
Valor |
DoorGenType | RANDOM |
SolverType | BestSearch |
Divisor de movimientos | 1.0 |
CacheType | NO CACHE |
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 |
Propiedad |
Valor |
DoorGenType | ALL |
SolverType | BestSearch |
Divisor de movimientos | 1.0 |
CacheType | REFRESHER |
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 |
Propiedad |
Valor |
DoorGenType | ALL |
SolverType | BestSearch |
Divisor de movimientos | 1.0 |
CacheType | ALWAYS |
Propiedad |
Valor |
DoorGenType | ALL |
SolverType | BestSearch |
Divisor de movimientos | 1.0 |
CacheType | NO CACHE |
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 |
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 |
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 |
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 |
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 |
Cachear cada N movimientos
Cachear con probabilidad
Permitir al desarrollador elegir un subconjunto de puertas entre todas las posibles
Attack y decay personalizados por cada objetivo
Verticalidad y horizontalidad
Condensación
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