Los mundos procedimentales aparecen en númerosos videojuegos de bastante éxito, pero probablemente el ejemplo más emblemático sea Minecraft. En español, es habitual llamarlos procedurales como consecuencia de un error de traducción, pues viene del inglés "procedural", derivado de "procedure". Esta palabra, traducida al español, significa "procedimiento". ¿Por qué estoy llenando texto con comentarios sobre rigor lingüistico cuando lo único que quiere el lector es saber cómo crear mundos de forma automática? Pues porque la palabra "procedimiento" ya nos va aclarando de qué va este tema de la generación mágica y aleatoria. La generación procedimental se basa en un algoritmo (o procedimiento) que automatiza las reglas que un computador debe seguir a la hora de crear un mundo virtual. Esta técnica no se limita a mundos en videojuegos, se puede automatizar la generación computacional de prácticamente cualquier cosa: modelos 3D, personajes, animaciones, texto, y un largo etcétera.
Cuando creamos un mundo en Minecraft por primera vez, se nos genera de forma automática un inmenso terreno con valles, montañas, mares, lagos y toda clase de elementos cúbicos que bien podrían representar un paisaje natural del mundo real (poniendo un poco de imaginación por parte del jugador). Lo cierto es que está realmente bien logrado. Los paisajes son coherentes, se separan en distintos biomas y podemos encontrar toda clase de detalles. Y además (y aquí está parte de la gracia de la generación procedimental), cada mundo nuevo que creamos es completamente distinto del anterior. ¿Cómo es posible programar un computador para que haga algo así?
La respuesta al desafío de la generación automática se encuentra en el algoritmo conocido como Ruido Perlin. Lo ideó el profesor de Ciencias de la Computación Ken Perlin, como parte de su trabajo en los efectos visuales de la película Tron, la de 1982. Este mecanismo fue creado por la necesidad de añadir pseudoaleatoriedad a los gráficos generados por computador. Necesitaban simular cierta variabilidad para lograr resultados orgánicos y naturales, es decir, querían una generación aleatoria pero que respetase cierta continuidad y orden. Eso fue lo que consiguió el Ruido Perlin y, actualmente, se ha convertido en un estándar en generación de gráficos computacionales. Como gran ejemplo de uso, se puede encontrar en los algoritmos que construyen los mundos de Minecraft.
En definitiva y, sin enrollarnos mucho más, sirviéndonos del Ruido Perlin, vamos a escribir un programa en Python que logre un resultado generado automáticamente como este:
El programa se divide en dos ficheros: grid.py y main.py. El primero contiene la clase que implementa la cuadrícula y, el segundo, el programa principal que instancia la cuadrícula y dibuja el mundo resultante.
Fichero grid.py
from perlin_noise import PerlinNoise import matplotlib.pyplot as plt class Grid: """ Definimos una clase Grid que contiene la estructura que compone el mundo. Cada posición de la cuadrícula almacena uno de los siguientes caracteres según su tipo: g = grass w = water s = sand r = rock """ # Definimos los parámetros de la cuadrícula (alto y ancho) e inicializamos algunas otras variables que nos servirán # para otras cosas height = 0 width = 0 octaves = 0 noise = None # La variable grid contendrá en sí misma la cuadrícula con cada uno de los caracteres que mencionamos antes. grid = [] # Y la variable noise_grid almacena una cuadrícula equivalente pero en lugar de los caracteres, almacena los # valores numéricos propocionados por el Ruido Perlin (el Ruido Perlin funciona con valores numéricos que nosotros # tendremos que traducir). noise_grid = [] # Estas variables contendrán los valores numéricos que delimitan el tipo de cada bloque de la cuadrícula. # Se verá más adelante. water_level = 0 grass_level = 0 sand_level = 0 def __init__(self, height, width, water_level=-0.05, sand_level=0, grass_level=0.3, octaves=6.5): # Este método inicializa la clase Grid. En primer lugar se establecen todos los valores de los atributos de la # clase Grid. self.height = height self.width = width # Inicializamos el Ruido Perlin. El parámetro octaves, a grandes rasgos, afecta a cómo de brusco varía el # mundo que generemos. Para este y los parámetros que delimitan los niveles numéricos de agua, arena y hierba # he definido algunos parámetros por defecto que dan buen resultado visual. Cada uno es libre de cambiarlos. self.noise = PerlinNoise(octaves=octaves) self.water_level = water_level self.grass_level = grass_level self.sand_level = sand_level # Ahora lo importante, generar la cuadrícula del mundo a partir del Ruido Perlin que obtuvimos antes. for i in range(height): row = [] noise_row = [] for j in range(width): # Extraemos el valor de ruido para cada casilla. La librería ya nos lo da como un objeto con coordenadas. # Sólo tenemos que extraer el valor de ruido correspondiente con las coordenadas de nuestra cuadrícula. noise_value = self.noise([i / height, j / width]) # Aquí es donde utilizamos los niveles que definimos antes, para decidir de qué tipo es cada casilla # según el valor de ruido que le corresponde. if noise_value <= self.water_level: position = 'w' elif self.water_level < noise_value <= self.sand_level: position = 's' elif self.sand_level < noise_value <= self.grass_level: position = 'g' else: position = 'r' row.append(position) noise_row.append(noise_value) self.grid.append(row) self.noise_grid.append(noise_row) def get_value(self, x, y): """ Nos creamos un método que nos permitirá obtener el tipo de cada casilla desde fuera (desde el programa principal) :param x: Coordenada x :param y: Coordenada y :return: Valor correspondiente a las coordenadas """ if x < self.width and y < self.width: return self.grid[x][y] else: return 'error'
Fichero main.py
from grid import Grid import pyglet # He definido algunos valores RGB para los colores que necesitamos. Cada uno es libre de probar los colores # que prefiera, sólo hay que modificar los valores RGB. color_grass = (142, 255, 111) color_water = (0, 162, 237) color_sand = (249, 231, 159) color_rock = (153, 163, 164) # Definimos las dimensiones (en píxeles) de la ventana que se mostrará. window_height = 800 window_width = 1000 # Y definimos el tamaño de cada casilla del mundo. tile_size = 10 # Instanciamos la clase Grid que nos hemos creado. En este caso voy a utilizar los valores por defecto que definí # así que esos parámetros no es necesario especificarlos. grid = Grid(height=int(window_height/tile_size), width=int(window_width/tile_size)) # Con la librería Pyglet inicializamos la ventana que mostrará el resultado. La variable batch es un mecanismo de la # librería para agrupar elementos visuales y optimizar el renderizado. window = pyglet.window.Window(window_width, window_height) batch = pyglet.graphics.Batch() # Pasamos a generar la imagen del mundo a partir de la cuadrícula que hemos creado. tiles=[] for i in range(int(window_height/tile_size)): for j in range(int(window_width/tile_size)): if grid.get_value(i, j) == 'g': square = pyglet.shapes.Rectangle(height=tile_size, width=tile_size, x=tile_size * j, y=tile_size * i, color=color_grass, batch=batch) elif grid.get_value(i, j) == 'w': square = pyglet.shapes.Rectangle(height=tile_size, width=tile_size, x=tile_size * j, y=tile_size * i, color=color_water, batch=batch) elif grid.get_value(i, j) == 's': square = pyglet.shapes.Rectangle(height=tile_size, width=tile_size, x=tile_size * j, y=tile_size * i, color=color_sand, batch=batch) elif grid.get_value(i, j) == 'r': square = pyglet.shapes.Rectangle(height=tile_size, width=tile_size, x=tile_size * j, y=tile_size * i, color=color_rock, batch=batch) else: square = pyglet.shapes.Rectangle(height=tile_size, width=tile_size, x=tile_size * j, y=tile_size * i, color=(0, 0, 0), batch=batch) tiles.append(square) @window.event def on_draw(): batch.draw() pyglet.app.run()
Con esto el programa ya funcionaría. Ahora podemos jugar con los parámetros de generación de la cuadrícula para alterar los resultados. Lo único que hay que hacer es modificar los parámetros de construcción del objeto Grid (water_level, sand_level, grass_level y octaves).
Por ejemplo, podemos generar un terreno más abrupto aumentando el parámetro octaves. Sólo es necesario modificar esta línea del fichero main.py:
grid = Grid(height=int(window_height/tile_size), width=int(window_width/tile_size), octaves=9)
tile_size = 1
O modificar los niveles de cada uno de los tipos de terreno, de nuevo, en la instanciación de la cuadrícula en main.py:
grid = Grid(height=int(window_height/tile_size), width=int(window_width/tile_size), water_level=-0.1, grass_level=0.1, octaves=9)
Comentarios
Publicar un comentario