Generación Procedural en Godot Engine – Perlin Noise

¡Hola! En este tutorial les explicaré el funcionamiento de mi Asset más reciente, un script que genera mapas aleatorios con el algoritmo de Perlin Noise que nos ofrece Godot. Pueden encontrar una versión en inglés de la que me he basado, aquí.

Empecemos entonces con el famoso Procedurall Generation. Primero que todo usaremos un nodo TileMap, necesitarán un recurso TileSet con todas las imágenes del mapa que queremos generar. En este caso yo estoy usando imágenes hexagonales, así que en la configuración del TileMap tengo que, además de indicar el tamaño de las celdas, marcar un half_offset como me sea conveniente. Este último paso para el TileMap, es probable que no lo necesiten si sus imágenes son cuadradas o rectangulares.

Ya he cubierto en varios vídeos la creación de recursos TileSet desde el mismo “Sistema de Archivos” de Godot, simplemente hacemos clic derecho “Crear Nuevo Recurso”, buscamos el TileSet y listo. Añadan los Tiles como mejor les parezca; los míos están todos reunidos en una plantilla, entonces sólo tengo que ir añadiendo tiles únicos con el botón amarillo. En caso de que tenga imágenes separadas tampoco hay problema.

Coloquen el recurso TileSet en el TileMap, y guarden la escena de este último en un lugar que recuerden, necesitarán la ruta de este mapa para poder instanciarlo en la escena donde estaremos generando el mapa.


Lo que sigue es la escena que podrá generar los mapas. Su nodo principal puede ser un Node, para no usar características innecesarias de cualquier otro nodo. Gráficamente esto será todo por aquí, desde ahora añadan un script con el nombre de “Generador de Mundos con Ruido” o algo así, y continuemos.

En fin, primero tenemos las variables: el ancho y alto del mapa son unos números enteros que representan la cantidad de tiles horizontales y verticales que se añadirán. Seguro ya saben que la palabra reservada “export” nos permite editar el valor de una variable desde el inspector. Si además la acompañamos de un paréntesis, podemos indicar el rango de nuestro valor, es decir, desde el inspector no podemos superar el valor de 400 o 300, y tampoco tener menos de 40 o 20.

Lo que sigue es más diccionario que variable. La clave de cada valor será el nombre de la imagen que estamos dibujando en el mapa, si tienen imágenes de agua, tierra, o árboles, dentro de su recurso TileSet deben buscar el ID de cada una. Lo podemos hacer de la siguiente forma: entran en el TileSet y en la derecha verán en secciones, cada imagen añadida. Las secciones tienen un número en el centro, ese es el ID, sólo tienen que abrirlas y comprobar cuál es la imagen que guarda. También pueden llevar la cuenta viendo las imágenes añadidas desde el editor de TileSet.

Si tienen por ejemplo dos imágenes para el agua o tres imágenes para sus árboles, pongan sólo el ID de una que quieran por sobre todos los demás, y las otras se ajustarán más abajo en el script.

Por último para las variables, añadan una que guarde nodos TileMap y otra que nos permita trabajar con el recurso OpenSimplexNoise. Cuando usamos “:” después del nombre de una variable, podemos decirle a Godot cuál es el tipo de valor que va a recibir. Es como si estuviéramos creando una constante, con la diferencia de que podemos cambiar el valor, siempre y cuando sea el mismo tipo especificado. Ese valor puede variar entre los tipos de datos conocidos, como enteros y boleanos, o también como es nuestro caso, para especificar nodos (tilemap) o recursos (opensimplexnoise).

Ya que terminamos de definir las variables, es momento de pasar rápidamente por la función de inicio. En ella activaremos la aleatoriedad en el script, con “randomize()” y, después de cancelar la función de procesos para que no ocupe memoria, llamamos a las funciones personalizadas que ajustarán el ruido, los tilemaps y terminarán generando el anhelado mapa. No tienen que crear todas las funciones de golpe, primero explicaré la que configura al Perlin Noise.


Cada vez que se llame a esa función, vamos a asignarle a nuestra variable de perlin_noise un nuevo recurso. Para crear ese recurso OpenSimplexNoise escribimos su nombre y usamos la función .new(). Los motores de vídeojuegos generalmente incluyen sus algoritmos para recrear este “ruido” que nosotros interpretaremos como un mapa. En Unity tienen directamente la función “Perlin Noise”, en Godot nos podemos referir a ella con el recurso OpenSimplexNoise.

Ahora me detendré para hablar sobre las propiedades con las que configuramos nuestro “ruido”: la propiedad “seed” recibirá un valor aleatorio, a partir de cada semilla podemos producir un ruido diferente, si un mundo les gusta más que otros, o simplemente quieren reproducir una copia exacta, pueden guardar la semilla, les mostraré más adelante cómo. Seguimos con la propiedad “octaves”, esta maneja un número entre el uno y el seis. Es la cantidad de “capas” que generan ruido al mismo tiempo. Cuando estemos generando el mapa, les explicaré mejor cómo funciona. La propiedad “period” se encarga de decir cuál es la frecuencia del ruido. Mientras más pequeño sea el valor, más grande será la frecuencia. “Lacunarity” o “Lacunaridad” se encarga de la diferencia entre los periodos de cada octavo, es decir, de la diferencia con respecto a cada capa. El valor de Lacunarity puede variar del 1 al 4, mientras sea mayor, encontraremos más diferencia entre los tiles. Y la última propiedad “persistence” es casi como la anterior, con la diferencia de que se encarga de contribuir a la diferencia del ruido en cada octavo, o sea en cada capa del Open Simplex Noise… y su valor varía entre el 0 y el 1.


La función que configura al TileMap es más sencilla: lo único que hace es guardar en la variable del mapa, una nueva instancia de nuestra escena TileMap con el recurso TileSet, para después añadirla como hija del nodo. Para cargar una nueva instancia escribimos la palabra reservada “load” y entre su paréntesis arrastramos desde el Sistema de Archivos la escena del TileMap. Les recomiendo que al principio de esta función comprueben que la variable del mapa sigue siendo nula, así se evitarán el proceso si ya habían generado antes un mapa.


Por fin llegamos entonces a la función que genera el mapa. Lo primero que haremos será limpiar todos los tiles que se encuentren en el TileMap de la variable de hace un momento. Después generaremos un perímetro, que básicamente es un borde con los tiles que queramos, para el resultado del mapa. Esta función del perímetro la explicaré después.

Cerrando con el generador de terreno, tenemos dos bucles for. Se ejecutarán según el tamaño horizontal y vertical de nuestro mapa, y en cada vuelta del segundo bucle se añadirá una nueva celda en la posición horizontal y vertical que tiene en ese momento el parámetro de los dos bucles, respectivamente. Las posiciones en un TileMap se manejan por vectores que van desde el 0,0 al infinito, por lo tanto empezaremos a poner celdas desde la misma posición 0 hasta el tamaño que especifiquemos en las variables que definimos al principio del tutorial. El tile que colocaremos en cada posición se escogerá según la función de ruido 2D que tiene el recurso OpenSimplexNoise. Esa función retornará un valor entre dos posiciones, como por ejemplo nuestro eje horizontal o vertical.

Con la función de ruido tendremos un valor entre 0, 1, y -1, así que todavía no nos serviría para lo que necesitamos. Es por eso que usamos otra función llamada get_tile() que recibirá el pequeño valor para convertirlo en el ID necesario de cada Tile. Veamos como funciona:

La función get_tile() tiene un parámetro llamado “ruido” que guarda los valores que nos manda el OpenSimplexNoise. Lo que sigue es comprobar que según las décimas que tenga ese valor, podamos dibujar un Tile diferente. Podemos empezar así: si el ruido tiene un valor menor que 0.0, entonces sal de la función devolviendo el 0, que es el ID de los tiles “grass”. Dentro de la misma comprobación de un número menor que 0.0, también podemos añadir otra, que sea para mandar otro Tile en caso de que sea menor que -0.1. Si esta última se cumple entonces retornas un valor diferente.

Supongo que están captando la idea. Hasta ahora sólo estamos trabajando con los valores que son menores de 0.0 para asignar la posición de dos tiles. Todavía hay más décimas que se pueden aprovechar, así que ahora usaremos números entre 0.1 y 0.0. Dentro de esa misma comprobación diremos que cuando se trate de un número menor que 0.03 la clave “montain” que guarda los tiles para las montañas, cambie al ID “5” para generar otro tipo de montaña. Algo así haremos cuando el valor sea menor que 0.05, y así sucesivamente.


Sólo falta explicar un par de funciones simples: el generador de perímetro ejecuta dos bucles que toman el valor de “-1” en el eje horizontal y vertical para poner ahí una celda que sea el límite. Recuerden que el algoritmo en sí sólo trabaja con los valores que están del 0 en adelante, por lo que en las celdas de “-1” no se coloca ningún tile. Por otro lado, la última función es la de _input() y se encarga de detectar cuando presionemos la tecla de “Enter” para configurar otra vez el OpenSimplexNoise y generar el terreno.

Hasta aquí el artículo de hoy. Dudé mucho para publicar este tutorial, como los temas de generación procedural no están muy explorados en Godot, pude aprovechar para vender un curso o algo más lucrativo. Sin embargo, me verán publicar este y otros vídeos muy interesantes de la serie de Kids Can Code. Sólo pido que si se animan y pueden permitírselo, hagan una donación me viene bien para pagar la Internet.