Ir al contenido principal

Generación procedimental desde 0 con Python

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:


Para ello, vamos a imaginar el mundo como una cuadrícula en 2D, igual que una hoja de cuaderno. Tenemos una rejilla de cuadrados ordenada y cada cuadrado se pintará de un color según lo que represente (hierba, agua, arena o roca). Traduciéndolo al mundo de la programación, lo primero que necesitamos es una matriz de dos dimensiones que almacene los datos sobre cada uno de los cuadrados que componen la cuadrícula. De qué tipo es cada cuadrado (hierba, agua, arena o roca), nos lo dirá el Ruido Perlin.

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)


También podemos aumentar la resolución reduciendo el tamaño de las casillas (en main.py):

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)


Desde aquí, todo es experimentar, probar parámetros y buscar el mejor resultado. Esta es la base para crear mundos procedimentales para videojuegos pero se pueden añadir más tipos de terreno, dispersar árboles aleatoriamente, pasarlo a 3D con un motor gráfico o añadirle cualquier funcionalidad extra. Todo queda a la imaginación del desarrollador.

Espero que este artículo haya resultado útil e interesante. Hasta el próximo.

Comentarios

Entradas populares de este blog

AMD Ryzen 4000 a principios de 2020.

Hace menos de una semana que la directora ejecutiva de AMD, Lisa Su, ha comunicado que la cuarta generación de los procesadores Ryzen llegará a principios de 2020. Concretamente, los primeros en llegar serán para portátiles y posteriormente para equipos de sobremesa. De este modo, AMD estrenaría la arquitectura Zen3 de 7nm este próximo año. Eso sí, como hemos comentado antes, los primeros en llegar, en el mes de enero, serían para ordenadores portátiles con arquitectura Zen2 de 7nm, que según lo que se sabe se presentarán en el CES de Las Vegas. Los Ryzen 4000 para ordenadores de sobremensa, con la nueva arquitectura Zen3 de 7nm, llegarán posteriormente, según se especula, para mediados de año. Cabe destacar también que se espera que la nueva arquitectura Zen3 sea compatible con los sockets actuales de AMD por lo que debería ser soportada por la mayoría de las placas base actuales para Ryzen (socket AM4). Como es lógico, con la arquitectura Zen3 se esperan aumentos e...

Funciones hash: el invento que protege tus contraseñas de los hackers y te permite minar criptomonedas.

La criptografía es casi tan antigua como la necesidad de ocultar la información escrita. Son muy diversas las técnicas que se han empleado desde siglos atrás para transformar un texto en claro en uno cifrado, ilegible para quien no lo pueda descifrar. Uno de los ejemplos clásicos y más sencillos es el cifrado César, utilizado por Julio César para comunicarse con sus tropas durante la guerra contra los galos. Este mecanismo de cifrado se basa en sustituir cada letra del mensaje por aquella que se encuentra desplazada n posiciones en el alfabeto. Por ejemplo, si n = 2 , la sustitución alfabética sería la siguiente: Alfabeto en claro:  ABCDEFG HIJKLMNÑOPQRSTUVWXYZ   Alfabeto cifrado:  CDEFGHIJKLMNÑOPQRSTUVWXYZAB Y si cifrasemos la palabra HOLA  con n = 2  obtendríamos el siguiente resultado:  H -> J, O -> Q, L ->N, A -> C:  JQNC Lo que llamaríamos la clave de cifrado es, en este caso  n = 2 . Como es lógico, cualquiera que conozca la cla...