Sectorizing a three dimensional map

Implementing pathfinding in a three dimensional map has clarified for me why so many games refuse to exploit all three dimensions. Even in the relatively small space of 64x64x8 tiles (32,768 in total) I’ve needed to stop some of the pathfinding requests after 300 tile checks, because they blocked the main thread.

To solve that issue, first I intended to use multiprocessing: I would send the pathfinding requests to the other processor cores, and I would let them return the results whenever they calculated them. Unfortunately, every pathfinding request needs to use the block of memory that contains pathfinding information about the 32,768 tiles (which include for each the neighboring reachable coordinates). The size comes up to about 8 MB, too much to scatter to the other cores without freezing the main thread. If just scattering some data needed for the pathfinding requests takes more time than executing them in the main core, I can’t choose that route.

One of the issues with pathfinding is that there seems to be no way to tell, at this level of abstraction, if the agent can reach a walkable destination tile; for example, what if that tile is beyond a ravine that blocks off that part of the map? To figure out if a route exists to that blocked off tile, the A* algorithm, on which pathfinding is based, will check literally thousands of tiles of the potential 32,768.

The most interesting solution I discovered was hierarchical pathfinding. It consists in dividing the map in sectors of a fixed size (they recommended 16 tiles in all dimensions), and inside that sector identifying the accessible regions. In my case the case gets more complicated, because I not only include the third dimension, but different movement capabilities as well. My agents walk, swim in shallow waters, in deep waters, or fly. The system doesn’t forbid an agent from featuring a combination of those capabilities: for example, an amphibious creature, or some sort of bird that also dives in the water.

Once the map is divided in sectors and regions, I needed to calculate the center of mass of each region, and those region centers are the ones used in the high level pathfinding process to figure out a route. Now, figuring out if a walkable tile is inaccessible takes looking up about ten to twenty region centers, instead of literally thousands of tiles. Once a route has been determined through the region centers, the low level route can get calculated from one of those region centers to the next, which also limits those pathfinding requests to about 20 tiles.

To make sure the programming sectorized any map properly (maps that get generated each time through the Perlin noise algorithm), I made a visualizer. The following video shows my first attempt to implement the sectorization, but I failed at identifying the regions properly, as I will explain later.

The video shows the regions colored according to the number they have been assigned. In every sector the regions start from 1, and the counter goes up for as many new regions as the process identifies. The crosses indicate the region centers. That apparently contiguous regions showed different colors should have alerted me to the fact that something was wrong. Although the created regions seemed reasonable for flying and swimming agents, the changes in elevation screwed with the process that identifies the walkable regions. Every time the elevation changed, a new region was created, something that makes no sense because walking agents can use the ramps to move up and down.

Fortunately I had already programmed the very complicated reachability checks to find a route from tile to tile. Each tile of the original pathfinding map includes information about its reachable neighbors. For every one of those lists, I converted the coordinates to the coordinate system used inside each sector (from 0 to 15 in the x and y dimensions, and from 0 to 7 in the z), and added each coordinate as an internal neighbor as long as that neighbor existed in the same sector.

The following video shows the result.

It shows clearly, particularly in the upper left corner of the map, how the sectorizing process has considered that the changes of elevation belonged to the same region. In the upper right there is an interesting case: a sector includes both the top of a mountain and the base of a cliff, but they are identified correctly as different regions because you can’t move from one to the other, at least through the inside of the sector.

Sectorizing a map to implement pathfinding at this level also implies discovering the edges of every sector, and whether they are open for traffic in every direction (which includes diagonals, and up and down). Although I programmed it in, painstakingly, the visualizer doesn’t show it.

Now I need to adapt the A* pathfinding algorithm so it will only consider the region centers when the system requests a high level route, and it will do something similar to the following:

HierarchicalPath.png

Sectorización de un mapa en tres dimensiones

Implementar la búsqueda de ruta en un mapa en tres dimensiones ha evidenciado por qué muchos juegos se resisten a explotar las tres dimensiones. Incluso en un espacio relativamente pequeño de 64x64x8 (32.768 casillas), he necesitado paralizar algunas de las búsquedas después de unas 300 peticiones de casillas, porque bloqueaba demasiado el sistema.

Para solucionarlo, en un primer momento pretendí utilizar el multiproceso: enviaría las búsquedas de ruta al resto de núcleos del procesador y que me devolvieran el resultado cuando lo lograran. Por desgracia, cada búsqueda de ruta necesita usar el bloque de memoria del mapa con toda la información sobre las 32.768 casillas (que además incluyen para cada una las coordenadas vecinas y accesibles). El tamaño alcanza unos ocho megas, una cantidad de datos demasiado grande para dispersar por el resto de núcleos sin que paralizara el núcleo principal. Si sólo diseminar las peticiones de búsqueda de ruta a otros núcleos del procesador tarda más que ejecutarlos en el mismo procesador, eso invalida la opción.

Uno de los problemas principales es que la búsqueda de ruta no entiende que por mucho que el destino sea una casilla atravesable por un agente no implica que pueda llegar hasta ella. Por ejemplo, una casilla en la cima de una montaña pero rodeada de acantilados es accesible si ya empiezas en ella, pero no si falta una ruta hasta la cima, y averiguarlo puede implicar que el algoritmo A*, que busca la ruta, pruebe literalmente miles de casillas de las potenciales 32.768.

La solución más interesante que descubrí es la búsqueda de ruta jerárquica: consiste en dividir un mapa en sectores de cierto tamaño (recomendaban 16 casillas en todas las dimensiones), y dentro de cada sector identificar las regiones de casillas accesibles. En mi caso el asunto se complica porque no sólo incluyo tres dimensiones, sino diferentes tipos de movimiento. Mis agentes andan, nadan por aguas poco profundas, por aguas profundas, o por el aire. No hay además impedimentos a nivel programático para que pudieran hacer una combinación de esas cosas: por ejemplo, alguna criatura anfibia, o pájaros que se sumergieran en el agua. Una vez el mapa original está dividido en sectores y regiones, se calcula el centro de masa de cada región para determinar su centro, y serán esos centros de región los que se usarán para buscar una ruta en el mapa. Eso conseguirá que descubrir que no se puede acceder a una casilla aislada se reduzca a mirar quizá unos diez centros de región, en vez de literalmente miles de casillas. Además, una vez se ha determinado una ruta a través de los centros de región, la ruta en el mapa entero puede realizarse de centro de región en centro de región, lo que también limita esas búsquedas a una media de unas 16-20 casillas.

Para asegurar que la programación sectorizaba los mapas correctamente (mapas que además se generan cada vez mediante el algoritmo de ruido Perlin), me planteé programar un visualizador. El siguiente vídeo muestra el primer intento de implementar la sectorización, pero fallé en identificar las regiones correctamente, como explicaré después.

El vídeo muestra las regiones coloreadas en función del número que se les ha asignado; en cada sector las regiones empiezan desde 1, y el contador sube tanto como necesita. Las cruces indican los centros de región. Que regiones aparentemente contiguas aparecieran coloreadas de maneras diferentes habría debido avisarme de que algo funcionaba mal. Aunque las regiones creadas parecían razonables para los agentes voladores y nadadores, los cambios de elevación destrozaban la identificación de regiones para los agentes que andan. Cada vez que la elevación cambiaba se creaba una región nueva, algo que no tiene sentido, ya que los agentes podrían usar las rampas para desplazarse.

Por fortuna yo ya había programado esas comprobaciones complicadísimas de accesibilidad para buscar la ruta de casilla en casilla. Cada casilla del mapa original incluye la información sobre sus vecinos accesibles. De cada una de esas listas convertí las coordenadas al sistema de coordenadas internas de cada sector (de 0 a 15 en las dimensiones x e y, y de 0 a 7 en la z), y las añadí como vecinos internos siempre que el vecino existiera en el mismo sector.

El resultado se ve en el siguiente vídeo.

Muestra con claridad, particularmente en el extremo superior izquierdo del mapa, cómo la sectorización ha considerado que los cambios de elevación formaban parte de la misma región. Hay un caso interesante en el extremo superior derecho del mapa, donde en un sector que coincide con la cima de la montaña se han identificado dos regiones distintas, una en la cima y la otra en la tierra que rodea la base, porque un acantilado impide la ruta.

La sectorización de un mapa para programar una búsqueda de ruta a este nivel implica también reconocer las fronteras de un sector, y si están abiertas al tránsito en esa dirección. Aunque está programado, el visualizador del vídeo no lo muestra.

Ahora queda adaptar el algoritmo A* de búsqueda de ruta, ya programado, para que sólo considere los centros de región, y hará algo similar a lo siguiente:

HierarchicalPath.png

Generation of images with a neuroevolutionary algorithm

Thanks to my recently adquired knowledge about how to display stuff on screen with OpenGL, I have implemented in Python an experiment that years ago I wrote in Java: an algorithm that generates images through the NEAT neuroevolutionary method invented in the mid 2000s. When I implemented it in Java, I had to write the NEAT method from zero by reading the scientific papers, because the Java libraries that existed back then didn’t inspire me much confidence. Fortunately, these days and in Python there are a couple solid modules that free me from that responsibility.

I’ll start showing a six minute video that shows some images generated through a few independent evolutionary processes:

A curious phenomenon, although logical, of the results of the programmed neuroevolution mimics what happens in natural evolution: when some pattern appears close to the beginning of the evolutionary run, and for some reason it benefits the genome, the pattern tends to persist for the rest of the evolution in some form or another (for example, the spinal cord).

The experiment works the following way: it generates a population of around 100 genomes that contains the nodes and connections of a neural network. When it gets activated, it will receive two inputs: the x coordinate divided by the width of the resolution the future image will have, and the y coordinate divided by the height of the future image. After the internal calculation, the neural network will produce the four components of a RGBA color: the value for red, for green, for blue, and for the transparency.

When I implemented this experiment for the first time in Java years ago, I programmed it so that each generation’s genome would produce a 32×32 pixels image, that it saved in the hard disk. I had to choose by hand which interested me and add them to a special folder, from which the program would sample to produce the next evolutionary run. However, even then I realized that I was sacrificing vital information so that seeded evolution would work entirely as expected: the genomes hold information about when its nodes and connections appeared for the first time, along with to what species the genomes belong. That information is vital when the genomes reproduce. This time I intended to solve the issue as soon as possible for the Python version, but I ended concluding that I shouldn’t only save the wanted genomes, but all the species involved in that run, along with the generation number. So it made sense to allow the user to save the entire evolutionary state at the chosen stage, all the genomes and the species they belong to. The equivalent to saving the game and reloading it.

The code draws on the screen a 32×32 version of the image each genome produces. In opposition to my previous implementation in Java, in which the genomes got promoted according to how novel they were, now the user has the responsibility to select the genomes he wants to promote. When he decides to move to the next generation, the algorithm scores the genomes in descending order of selection.

I’ve recorded 100 generations of the process in the following video:

The genomes start either completely disconnected or partially connected to the end nodes; I’ve configured it so a possibility of 10% exists so each connection is present at the beginning. That causes some genomes to output no color, or to come out completely black. However, even in the first generation different patterns are already present: vertical bars, diagonals with distorsions, and gradients. It takes just five generations for three genomes to connect with the green output node. However, curiously, it’s only in the 26th generation when a genome connects with the blue output node. A mix of both dominates the evolutionary run, and there’s a complete, or almost complete absence of connections to the red output.

The neural network that the genome converts to must be executed 32 * 32 times (1024 times) only to generate the texture that I’ll display on screen, so it wasn’t feasible during the recording of the video to generate the full images that I include separately in other videos, because the resolution of 1080×1080 implies executing the neural network for each of the selected genomes 1,166,400 times, which takes a while, and for now I haven’t managed to parallelize it (the overhead kills it).

Although the images generated during the recorded evolutionary run didn’t interest me that much, I’ve gathered some of them in the following video:

UPDATE: I’ve managed to parallelize both generating the pixels for all 100 pictures that get displayed on screen for each generation, as well as saving in the background the selected genomes in full. During the last few months I’ve searched for the right parallelization library written in Python, and after considering Pathos for a while, I’ve settled on the brilliant Dask (https://dask.org/), which makes it very transparent.

This is all it takes to map the generation of the pixels for a population and gather the results:

parallelism1.PNG

For saving the full pictures, I love the “fire and forget” feature, that allows you to keep advancing the generations while the other cores handle saving the full pictures for the previously selected genomes:

parallelism2.PNG

From here I’ll move on to parallelize certain tasks of my main “experiment”, that game thing: running the behavior trees as well as launching the pathfinding queries.

Generación de imágenes mediante un algoritmo neuroevolutivo

Gracias a mi recientemente adquirido conocimiento sobre cómo mostrar cosas en la pantalla mediante OpenGL, he vuelto a implementar en Python un experimento que hace años implementé en Java: un algoritmo que genera imágenes mediante el método de neuroevolución NEAT inventado a mediados de los 2000s. Cuando lo implementé en Java, tuve que programar el método NEAT desde cero tirando de los artículos científicos, porque las librerías de Java existentes en ese momento no me inspiraban mucha confianza. Por fortuna hoy en día y en Python existen un par de paquetes muy sólidos que me libran de esa responsabilidad.

Empiezo enseñando un vídeo de seis minutos que muestra imágenes generadas a lo largo de varios procesos evolutivos independientes:

Un fenómeno curioso, aunque lógico, de los resultados de la neuroevolución programada se asemeja a lo que pasa en la natural: cuando algún patrón surge cerca del comienzo de la evolución, y por un motivo u otro beneficia al genoma, tiende a perpetuarse durante el resto de la evolución en diferentes formas (por ejemplo, la columna vertebral en los seres vivos).

El experimento funciona de la siguiente manera: se genera una población de unos 100 genomas que contienen los nodos y las conexiones de una red neural. Cuando se necesite activarla, se le pasarán dos valores: la coordenada x dividida entre la anchura de la resolución que la imagen tendrá, y la coordenada y dividida entre la altura de la resolución que la imagen tendrá. Tras el cálculo interno, la red neural produce los cuatro componentes de un color RGBA: el valor para el rojo, para el verde, para el azul, y para la transparencia.

Cuando implementé este experimento por primera vez en Java hace años, yo programé que cada genoma de cada generación produjera una imagen de 32 por 32 píxeles que guardaba en el disco duro. Yo tendría que elegir a mano cuáles me interesaran y añadirlos a otra carpeta, de los que el programa leería al iniciarse la siguiente vez, y sólo consideraría esos genomas elegidos para comenzar otra evolución. Sin embargo, aun entonces yo sabía que estaba sacrificando información vital para que la segunda evolución con genomas anteriores funcionara de la manera adecuada: los genomas guardan información sobre cuándo surgieron por primera vez sus nodos y conexiones, además de a qué especie pertenecen. Ambas informaciones son vitales cuando los genomas se reproducen. Por aquel entonces a mí no se me ocurría cómo implementarlo adecuadamente. En esta ocasión me planteé solucionarlo lo antes posible para la nueva versión en Python, pero acabé llegando a la conclusión de que no sólo habría que guardar los genomas queridos, sino también todas las especies y la generación a la que pertenecen, así que he optado por permitir que el usuario salve una generación entera, todos los genomas y las especies a los que pertenecen. Es el equivalente de salvar la partida y volver a cargarla.

Además, el código dibuja en la pantalla la imagen en 32×32 píxeles que cada genoma produce. Al revés que en mi pasada implementación en Java, en la que los genomas se promovían dependiendo de lo novedosos que fueran, ahora se le da la responsabilidad al usuario de seleccionar los genomas que quiere promover en la evolución. Cuando decida pasar a la siguiente generación, el algoritmo puntua de manera descendiente dependiendo del orden en el que el usuario ha seleccionado los genomas.

He grabado 100 generaciones de este proceso en el siguiente vídeo:

Los genomas empiezan o completamente desconectados o parcialmente conectados a los nodos de salida; lo he configurado para que exista una posibilidad del 10% de que cada conexión esté presente nada más empezar. Eso hace que algunos genomas no produzcan ningún color, o que salgan completamente negros. Sin embargo, en la primera generación ya se presentan patrones diferentes: barras verticales, diagonales con distorsiones, y gradientes. Sólo transcurren cinco generaciones hasta que tres genomas conectan con el nodo de salida verde. Sin embargo, de manera curiosa, es sólo en la generación 26 cuando un genoma conecta con el nodo de salida azul. Una mezcla de ambos acaba dominando la evolución, y faltan por entero, o casi, genomas que contemplen la salida roja.

Dado que la red neural que un genoma forma debe ejecutarse 32 * 32 veces (1024 veces) sólo para generar la textura que luego plasmo en la pantalla, no era factible generar durante la grabación las imágenes grandes que incluyo en los otros vídeos, ya que la resolución de 1080 por 1080 píxeles implica ejecutar la red neural de cada genoma 1.166.400 veces, lo que tarda un buen rato, y de momento no he conseguido paralelizarlo.

Aunque las imágenes generadas durante esa evolución no me han interesado demasiado, he recogido unas cuántas de ellas en el siguiente vídeo:

How to render text using OpenGL 4 and Python 3

The first time I implemented the following simulation, and the architecture of the entire application, following Python’s Pygame library. However, that library already struggled showing the entire map and the actors. Searching how to improve the performance of that library I understood that I needed to restructure the architecture to base it on native OpenGL, so all the data about what needed to be rendered ran in the graphics card. I took my time understanding how to store data in the GPU and how to organize the calls, because it works as a state machine. But I didn’t expect to get blocked rendering text, something that in 2018 should be as easy as making a single call saying what you want to write and on what position of the screen.

Before I talk about the issues rendering text, the next video shows the pathfinding experiment rendered entirely with OpenGL.

Each cell of terrain, as well as the images that represent the actors, are drawings of 32×32 pixels. That uniformity makes it easy to store them in textures even by hand, and rendering them. However, each character of a text could have a different size, and its position could vary depending on the previous character (something called kerning, apparently). I assumed some library would manage that much data for all the characters in a font, which led me to the library freetype-py. Given a font with a ttf extension, the library provides the necessary information, but doesn’t produce an image. I searched for around three to four weeks without success (to be fair, I was working full time). All the examples, about how to translate the information that the library provided into textures that could be rendered, were either based on obsolete OpenGL 2 techniques, or used Python 2 code that featured complicated maths that didn’t work in 3, and I couldn’t figure out how to make them.

The alternative to using that library and creating the image with the characters in the graphics card would be a bitmap font. It involves stuffing all the wanted characters of a font in a jpg, png, etc. image, and then rendering each character according to the UV coordinates associated to that texture. However, how would I organize all the characters in a texture by hand, given that they have different sizes? And how would I handle those widths and heights and divergent positions of the characters?

During my search I tried a few programs that turned a font into bitmaps. I ended up settling for Font Builder because it exported the information about each of those characters to a XML file.

fontBuilder.PNG

fontXML.png

Writing the code to turn that XML file into Python variables was easy. Afterwards I structured the code so that, from the upper layer of the application, rendering a text on the screen needed a single call and a few arguments.

drawZLevel.PNG

That call only needs the chosen font, the text that will be rendered, the screen coordinates, the height and width of the screen, the color the text will be rendered with, the relative scale to its original size, and the notion of whether that line of text will stick around. Although it seems like plenty of arguments, they are few in comparison to how much is handled in the lower layers.

In the second layer, a renderer object that holds the data structure that OpenGL understands (VAOs, VBOs, IBOs and other structures specific to working with graphics in the GPU), and that has already been initialized with the data of the combined text glyphs into an image, renders it in a similar way to the other elements on the screen.

mainMessageDraw.PNG

In the third layer, the program creates the renderer object. You need to take certain decisions with each element you are going to render on the screen. If you intend to draw an image that is not going to change, you better establish its data once and lock the internal structures as static, because otherwise you are sacrificing performance. A text gets created once and doesn’t change, so the code produces the image once when the renderer object gets initialized.

createTextRenderer.png

Here, the passed arguments to produce an image from the text get complicated. You need to define the max amount of quads that you believe the image of the text will occupy, the structure of the data that will get sent to the graphics card to render each vertex, and how many floats represent a vertex. In this case it’s nine floats: three for the position (x, y, z), four for the color (r, g, b, a), and two for the UV coordinates that represent in what part of the texture it will find the character that the vertex relates to.

The color of the text doesn’t change, so it would have been easier to send the color once to the internal program that runs in the GPU (called shader, a sort of “legacy name” that these days doesn’t necessarily have anything to do with rendering shadows), but maybe in the future I might want to render some characters with a different color, so it seemed like a good idea to send the data this way.

Optimization is vital when you need to update the screen 60 times a second. Every renderer object gets created once and stored, and when it isn’t needed, it gets discarded.

In the fourth layer of that initial call to draw the text you can find the most complicated part: how to translate the information of each character to form the wanted text.

calculateBufferData.PNG

For each character in the text, the code does the following:

  • Retrieves the character object formed from the data contained in the XML file, to know its height, width and other concrete information.
  • The UV coordinates get calculated, to know in which part of the texture of that font you can locate a particular character. Floating-point arithmetic that requires precision to avoid mixing the characters shown on the texture.
  • Adjusts the height and width of the character according to the passed scale, as well as to the natural offset of that character (for example, a p must get drawn lower than the other characters).
  • Checks the previous character and adjusts the cursor of the text depending on whether the current character must move closer to the previous one or further apart.
  • Creates 9 * 4 floats corresponding to the position, color and UV coordinates that form the four vertices of a quad.
  • Increases the cursor of the text with the width of the character we just handled.

I hope this guide helps someone, because I wish I had found something like this instead of cobbling the knowledge together by trial and error.

Afterwards I will restructure the code that draws a map so it can handle an experiment on neuroevolution: instead of drawing the cells from a texture, dozens of neural networks will generate 32×32 images, and they will keep evolving depending on which ones the user prefers.

Cómo dibujar texto mediante OpenGL 4 y Python 3

En un primer momento implementé la simulación siguiente, y la arquitectura de la aplicación entera, mediante la librería Pygame de Python. Sin embargo, a Pygame ya le costaba demasiado sólo mostrar el mapa y los actores. Mientras yo investigaba cómo mejorar el rendimiento de esa librería se hizo evidente que yo necesitaría reescribir la arquitectura para basarme en OpenGL nativo, y que toda la información sobre lo que debiera dibujarse en la pantalla la gestionara la tarjeta gráfica. Tardé bastante ya en entender cómo almacenar los datos en la tarjeta y cómo organizar las llamadas a OpenGL para que dibujara lo necesario. Donde no había esperado bloquearme era en dibujar texto, algo que en 2018 yo pienso que debería ser tan sencillo como hacer una única llamada diciendo qué quieres escribir y en qué coordenadas de la pantalla.

Antes de hablar sobre los problemas con el texto, este siguiente vídeo muestra el experimento de búsqueda de ruta dibujado por entero con OpenGL.

Cada casilla de terreno, y las imágenes de los actores, son dibujos de 32 por 32 píxeles. La uniformidad hace muy fácil almacenarlos en texturas y dibujarlos. Sin embargo, cada carácter de un texto puede tener un tamaño diferente, y además su posición puede variar dependiendo del carácter previo (algo llamado kerning, aparentemente). Asumí que alguna librería gestionaría esos múltiples datos individuales de los caracteres, lo que me llevó a la librería freetype-py. Dada una fuente con extensión ttf, la librería te da la información necesaria, pero no incorpora su imagen. Buscando en internet cómo implementarlo encallé durante unas semanas, porque todos los ejemplos sobre cómo convertir esa información de freetype-py en texturas y dibujarlas o bien se basaban en técnicas obsoletas de OpenGL 2, o en código de Python 2 que contenía partes matemáticas que no compilaban bien con Python 3 por motivos que no acabé de solucionar.

La alternativa a usar la librería y crear la imagen con los caracteres en la tarjeta gráfica sería una fuente bitmap, que consiste en meter los caracteres queridos de una fuente concreta en una imagen jpg, png, etc., y luego dibujar cada carácter mediante las coordenadas UV asociadas para esa textura. Sin embargo, ¿cómo metería yo todos los caracteres en una sola textura a mano, cuando tienen tamaños diferentes? ¿Y cómo gestionaría yo esas alturas y anchuras y posicionamientos divergentes de los caracteres?

Durante la búsqueda probé varias utilidades que convertían una fuente en bitmaps. Me acabé quedando con Font Builder porque exportaba la información sobre cada uno de esos caracteres a un archivo XML.

fontBuilder.PNG

fontXML.png

Escribir el código para convertir ese archivo XML resultante en variables de Python fue sencillo. Después estructuré el código de manera que desde la capa superior de la aplicación, dibujar un texto en la pantalla se redujera a una simple llamada y unos pocos argumentos:

drawZLevel.PNG

Esa llamada sólo necesita la fuente con la que quieres escribir el texto, el texto que mostrará, sus coordenadas en la pantalla, la altura y anchura de la pantalla, el color con el que se dibujará el texto, la escala relativa a su tamaño original, y la noción de si el texto es de usar y tirar o si reaparecerá. Aunque parecen muchos argumentos, son muy pocos en comparación con lo que se gestiona en las capas inferiores.

En la segunda capa, un objecto dibujante, que contiene la estructura de datos que OpenGL entiende (VAOs, VBOs, IBOs y demás estructuras específicas a trabajar con gráficos en la tarjeta), y que se ha construido ya con los datos que componen el texto completo en una imagen, la dibuja de una manera similar al resto de elementos de la pantalla.

mainMessageDraw.PNG

En la tercera capa, el programa crea el objeto dibujante. Hay que tomar ciertas decisiones con cada elemento que se va a dibujar en la pantalla. Si pretendes dibujar una imagen que no vas a alterar, más vale que establezcas los datos una vez y que definas las estructuras internas como estáticas, porque de lo contrario estás sacrificando rendimiento. Un texto se crea una vez y no se modifica, así que toda la imagen se compone una única vez cuando se inicializa el objeto dibujante.

createTextRenderer.png

Aquí, los argumentos que se pasan para definir la imagen del texto se complican. Necesitas declarar la cantidad máxima de recuadros que crees que el texto ocupará, la estructura de los datos que se enviarán a la tarjeta gráfica para dibujar cada vértice, y cuántos números reales representan un vértice. En este caso son nueve números reales: tres para la posición (x, y, z), cuatro para el color (r, g, b, a), y dos para las coordenadas UV que representan en qué punto de la textura se encuentra el carácter de texto al que corresponde ese vértice.

Dado que el color del texto no cambia, habría sido más sencillo enviar el color una vez al programa interno que corre en la tarjeta gráfica (llamado shader), pero en vistas a que en el futuro algunas letras pudieran tener otros colores, me pareció una buena idea enviar los datos así.

La optimización es vital cuando necesitas mostrar imágenes 60 veces por segundo. Cada objeto dibujante se crea una vez y se almacena, y cuando sobra se elimina.

En la cuarta capa de esta llamada se encuentra lo más complicado: cómo traducir la información de cada carácter al texto que se pretende mostrar:

calculateBufferData.PNG

Por cada carácter en el texto, el código hace lo siguiente:

  • Pide el objeto carácter que se ha extraído del archivo XML, para conocer su altura, anchura y demás datos concretos.
  • Se calculan las coordenadas UV, para saber en qué posición de la textura de la fuente se encuentra ese carácter. Calcula divisiones con números reales, y necesitan tener una precisión perfecta para que los caracteres no se mezclen.
  • Ajusta la altura y anchura del carácter en función de la escala que se le haya pasado, además de del desplazamiento natural de ese carácter en concreto (por ejemplo, una p debe dibujarse más abajo que las demás letras).
  • Comprueba el carácter previo y cambia el cursor del texto dependiendo de si el carácter actual debe acercarse al previo o distanciarse de él.
  • Crea 9 * 4 números reales correspondientes a la posición, color y coordenadas UV correspondientes a los cuatro vértices de un cuadrado.
  • Añade al cursor del texto la anchura del carácter cuyos datos acabamos de calcular.

Espero que esta guía ayude a alguien, ya que me hubiera gustado encontrarme una similar en vez de averiguar todo esto desde cero.

Después de esto reestructuraré el código que dibuja un mapa para hacer un experimento de neuroevolución: en vez de casillas leídas de una textura, unas docenas de redes neurales generarán imágenes de 32 por 32 píxeles, que seguirán evolucionando dependiendo de si el usuario las prefiere o no.

Experimento sobre búsqueda de ruta en 3D y para personajes con diferentes reglas de movimiento

Pretendo programar un juego de construcción de colonias similar a Dwarf Fortress. Desde el principio supe que debía arreglármelas primero para que el sistema calculara las rutas de los agentes de manera fiable, aprovechando los diferentes hilos del procesador, y que generara rutas adecuadas para agentes que pudieran andar, volar, nadar en aguas poco profundas, en aguas profundas, o una combinación de cualquiera de esas posibilidades.

Hace un par de semanas programé una versión sólida de un generador de mapas locales basado en el ruido Perlin. El vídeo recoge el resultado. Ninguna inteligencia artificial involucrada: el personaje lo muevo yo con el teclado numérico.

Sin embargo, tras varios experimentos en los que yo no tenía claro desde un principio cómo proceder, la arquitectura se había vuelto engorrosa. Dediqué varios días a refactorizar la mayoría del código e implementar nuevas clases que representaban abstracciones que antes ni siquiera existían. El bucle actual consiste en lo siguiente:

  • Una clase llamada Inputs gestiona si el usuario ha presionado alguna tecla, ha movido el ratón o pulsado alguno de sus botones. El sistema ejecuta las funciones dependiendo de las relaciones escritas en un archivo json.inputsMapping.png
  • Una clase llamada Display encapsula todo lo relacionado con dibujar los elementos en la pantalla. Aparte de que aislar esas responsabilidades mejora la arquitectura, ya empezaba a ver que pygame se quedaba corto incluso para dibujar el mapa a una velocidad consistente. Pronto deberé averiguar si puedo sustituir todas las referencias a pygame con llamadas a OpenGL.displayMapping.png
  • Si el usuario pulsa la tecla punto, se simula un turno. Sabía desde un principio que para programar una simulación compleja tendría que aislarla y limitar la cantidad de veces que se ejecutaría en un segundo, aunque la simulación acabara aprovechando los diferentes núcleos del procesador.
  • Durante la simulación de un turno, una clase Messenger, encargada de publicar mensajes a su subscriptores, envía el mensaje de update para que todas las clases que deban hacerlo ejecuten su lógica de actualización. Para encapsular este comportamiento tuve que reestructurar por completo cómo representaba una entidad en el programa. Aunque ya conocía la tendencia moderna de representar entidades mediante componentes en vez de como jerarquías de herencias, consideré que para los experimentos simples del principio bastaba con heredar de clases simples como Actor y Personaje. Había programado las casillas del mapa como entidades aparte, pero la necesidad de todos esos objetos de actualizarse y mostrarse en la pantalla evidenciaba que necesitaba reducir la idea de cada entidad a un identificador (en este caso, un UUID), y que una multitud de componentes independientes se encargaran de su funcionalidad. Programé el sistema para que bastara con definir los componentes de cada entidad en un archivo de texto json. Una serie de constructores y factorías compondrían una entidad cuando fuera necesario.

entities.png

  • Una clase llamada ComponentManager se encarga de archivar la relación de identificador y componente en un diccionario. Permite recuperar cualquier componente existente de un identificador concreto sin involucrar al resto de componentes.

componentManager.png

  • Una clase de componentes que reciben el mensaje para actualizarse son los Behavior trees, o árboles de comportamiento. La inteligencia artificial para los agentes modernos se ha reducido a estos árboles o a planning systems (sistemas de planificado), sobre los que no he leído todavía. Pero los árboles de comportamiento encajan de maravilla con la neuroevolución. Algunos de los nodos de un árbol de comportamiento se limitan a decidir qué comportamiento se ejecutará, así que bastaría con crear nodos específicos que cumplieran esa función mediante redes neurales evolucionadas. Los árboles de comportamiento son enrevesados, y a pesar de su aparente simplicidad, decidir su estructura para que reflejen el comportamiento exacto que quieres puede complicarse bastante. Para empezar, decidí que bastara con definir un comportamiento aislado en un archivo json. Una serie de constructores y factorías crearía la red final de objetos. La siguiente es la definición de la inteligencia artificial completa que permite a cada uno de los agentes del siguiente vídeo moverse:

moveToBT.png

  • Refleja el siguiente esquema:

Move_to_destination Behavior.jpg

  • La mayoría de los libros y artículos sobre árboles de comportamiento recomiendan reducir su composición a los selectores básicos: sequencias y fallbacks (o selectores clásicos), aparte de decoradores. La sequencia funciona de la siguiente manera: si el primer hijo falla, la sequencia entera falla y envía el fallo al nivel superior. Si todos sus hijos devuelven que han satisfecho su tarea, la sequencia se considera satisfecha. Los fallback funcionan al revés: si un hijo falla, se prueba el siguiente. Si todos fallan, el fallback falla, pero si alguno de los hijos ha satisfecho su tarea, el fallback se considera satisfecho. Aunque me devané los sesos para componer el árbol de manera que no necesitara repetir ningún nodo, no lo conseguí. Como el diagrama refleja, la lógica considera dos veces si el agente ha terminado de calcular su ruta, y otras dos veces si puede moverse al siguiente punto de la ruta. Pero funciona como está.
  • Este comportamiento tiene poco de inteligente, claro. Sólo refleja una secuencia lógica de acciones para pedir un cálculo de ruta, asegurarse de que haya terminado de calcularse, consumirla y determinar si el agente ha alcanzado su destino. Pero la estructura está dispuesta para implementar comportamientos mucho más complicados.
  • En un principio pretendí ejecutar los árboles de comportamiento en diferentes hilos o procesos, para aliviar el núcleo principal del procesador. Sin embargo, Python es muy peculiar a la hora de tratar los hilos, y para ejecutar cada árbol de comportamiento en un núcleo diferente, el sistema copiaría todo el árbol y las clases involucradas. Si la estructura se limitara a la lógica, no habría mucho problema, pero muchos de esos nodos deben preguntar cosas a diferentes componentes de la entidad, lo que implica tener que copiar el diccionario entero de comportamientos. Podría arreglármelas para aislarlo, pero en el futuro es de esperar que alguno de los nodos debiera mirar dentro del mapa, lo que implicaría copiar el mapa entero a otro proceso. Demasiado pronto para decidirme.
  • Lo que sí implementé fue separar la idea de razonar de la de ejecutar el razonamiento. Los árboles de comportamiento sólo devuelven tareas a ejecutar, y no se encargan de modificar el mundo ni los actores. Cada turno, otra clase dedicada a almacenar tareas y procesarlas las ejecuta como considere oportuno, ya sea en el mismo núcleo o en los hilos disponibles. En el futuro también debería ejecutar tareas en otros procesadores.

tasksScheduler.png

  • Mencionaré que aunque había implementado el algoritmo A* de búsqueda de ruta hace unas semanas, no encontré la manera de incorporar las reglas de movimiento en él, así que tuve que programarlo otra vez desde cero siguiendo otro ejemplo. Pero la versión actual es mucho mejor, y refleja las reglas de movimiento perfectamente.
  • De momento sólo ejecuta fuera del hilo principal los cálculos de ruta, y únicamente en los hilos disponibles. Mis primeros intentos por implementar mediante el multiproceso la búsqueda de ruta fueron descorazonadores: tardaba al menos tres o cuatro segundos en devolver incluso las rutas vacías. No acababa de verle el sentido, pero se debía a que no entendía la diferencia entre hilos y procesos. Los hilos existen en un proceso y usan el mismo espacio de memoria. Pueden acceder a los datos. Si uno de ellos escribe un dato mientras otro hilo intenta leerlo, se producirán inconsistencias, pero para evitar ese problema basta con dividir la ejecución interna de los efectos externos. Sin embargo, yo mandaba calcular la ruta mediante un pool de procesos, para lo que el ordenador debía iniciar el Python en otro núcleo del procesador y copiar todos los datos involucrados. Ahora resulta evidente que sólo las tareas que puedan permitirse tardar varios segundos en devolver un resultado se benefician del multiproceso, pero de todas formas son muchas: por ejemplo, si se quiere procesar la temperatura, la presión del aire, etc., de cada casilla del mapa, o calcular un mapa de amenazas o de visibilidad, o procesar qué pasa en el mundo en general mientras el juego transcurre en el mapa local. En el futuro imagino que podría aprovechar el octree del mapa, que distribuye a los agentes según su cercanía, para ejecutar la inteligencia de los agentes cercanos en hilos, pero procesar la de los demás en otros procesadores.
  • Algunas tareas producen comandos: acciones que modifican a algunos agentes y/o el mapa. En este caso producen la acción de moverse al siguiente punto de su ruta. Como se verá en el vídeo, también gestiona si el siguiente punto de la ruta está bloqueado por otro agente, y en ese caso permitirá volver a buscar una ruta otro par de veces antes de renunciar a ese destino. Una clase se encarga de procesar la cola de comandos ejecutándolos uno tras otro.

El experimento tenía una complejidad añadida: yo quería que convivieran agentes que andaran por el suelo con otros que volaran, nadaran en aguas poco profundas, en profundas, etc. En vistas al futuro, el sistema debería permitir añadir comportamientos como por ejemplo los de un agente que se moviera abriendo túneles en los bloques sólidos de roca, o que nadara en lava. Además, esas capacidades de movimiento deberían poder combinarse: algunos agentes deberían poder volar, nadar y además abrir túneles en roca, por ejemplo. Para ello tuve que abstraer las reglas de movimiento a archivos json.

movementRules.png

La búsqueda de ruta necesitaba conocer las posibles casillas vecinas de cada casilla que consideraba. La accesibilidad de cada casilla dependía del agente que pedía la ruta, así que en un primer momento no se me ocurría otra cosa que calcular los vecinos cada vez que se pedía calcular una ruta. Eso ralentizaba mucho la búsqueda, obviamente. Al final opté por generar cada posible vecino en un diccionario interno del mapa, poco después de generarse al comienzo del experimento. Eso tarda unos ocho segundos, pero no necesitará volver a hacerlo durante el transcurso del experimento o del juego salvo que alguna casilla cambie, y aun así sólo deberán recalcularse los vecinos inmediatos de la casilla que haya cambiado. Esa tarea se puede delegar a un hilo aparte.

Sin embargo, la lógica que genera los vecinos de cada casilla es de lo más complicado que he programado recientemente:

neighbors.png

El resultado se ve en el siguiente vídeo. Hay cuatro tipos diferentes de agentes. Unos andan, y sólo pueden moverse por la tierra (aunque podría incluir sin problemas que nadaran en aguas poco profundas). Otros vuelan, lo que implica que pueden moverse por el aire y por la tierra. Otro agente sólo nada por aguas poco profundas. El último agente nada por aguas poco profundas y por las profundas.

Cuando todo funcionaba ya, he cambiado un par de detalles de la implementación. La búsqueda de ruta se ejecuta en hilos; aunque los hilos funcionan de manera semiindependiente, si a una búsqueda le costaba encontrar el camino, bloqueaba el sistema durante un segundo o algo más. Las búsquedas normales consideran unas veinte, treinta o cincuenta casillas. Algunas raras superan las cien. Pero esas búsquedas que bloqueaban el sistema consideraban hasta mil quinientas o más. Acabé limitando artificialmente la consideración de casillas a unas trescientas. Como resultado, algunos agentes no se moverán a ese destino, pero en el transcurso de un juego podría considerarse razonable: el destino es demasiado complicado como para alcanzarlo desde su punto de origen. La inteligencia artificial lo tendría en cuenta y lo derivaría a otras acciones.

Después de esto me toca refactorizar un poco más el sistema para integrar a la arquitectura los bloques más sólidos del experimento, y luego investigaré cómo trabaja Python con OpenGL y si es factible reemplazar pygame por completo.

Implementation of pathfinding with Z levels

The following video shows an actor moving through the different levels of depth of a simple map, and then going for a while to random coordinates on the first level:

Although the actor reaches the objective, for now I haven’t managed to prevent that in two particular moments the actor decides to go through the roof as part of his route, although I had modified the code so it wouldn’t allow him to do so, at least in theory. But it’s a minor problem for what I intended implementing the pathfinding: that when I programmed new experiments in which the user moved his character, around him other actors would find their way and act according to their AI.

Movement in three dimensions tends to divide these kinds of games. Dwarf Fortress is famous in part for how it handles the different layers, allowing the user to strike the earth for a couple dozen levels to build his fortress, or raise it several levels above sea level if he wants to. On the other hand, Rimworld, programmed with Unity, sacrifices that verticality to offer better graphics and effects and a complexity limited in comparison. I was convinced that simple sprites were enough in exchange of a complex simulation and an artificial intelligence that would, hopefully, surprise often. And behave reasonably to begin with.

I programmed this experiment a few days ago, but I recall spending a couple hours trying to solve a problem. When the actor had to find his route to the inside of the house, which should force him to pass through the doorway, the actor went straight to the western wall of the house, his image passed through and stopped in the final coordinates. I was making sure that the pathfinding algorithm identified the wall as impassable, but the actor was still going through it. In the end, after a lot of testing, I revised my assumptions. Were the actor sprites being displayed according to the level they were in? Turns out I had let that for later, and the pathfinding was doing its job: instead of going through the wall, the actor jumped it and “crashed” through the roof to reach his objective. The function that had to draw the actor showed him indistinctly going through a wall that in reality existed a level below.

After that, to make sure the pathfinding algorithm worked better in three dimensions, I had to add more booleans to the tiles. Apart from whether or not they blocked the straight path, they should mark whether they blocked going upwards or downwards (days later I wrote a couple of booleans more: whether the tile blocked the path straight up and down). Otherwise the actor would go through several layers of underground tiles to reach a basement. The changes worked for the most part, shown in the video where the actor goes down the stairs to reach the basement.

To draw the different layers of depth I paid attention to how Dwarf Fortress did it. When the user went up a z level, if any tile was defined as “empty”, the program should draw the closest “real” tile below, but tinted blue to show the user that he was seeing tiles belonging to another z level. On my first try, the code copied each of those sprites and modified its RGB component according to how close they were to the current z level. That’s shown in the video. However, copying all those sprites every frame hurt the framerate too much. A couple of days later I opted for something simpler and that even works better: I draw the first “real” tile normally, but over it I draw translucent tiles for each z level in between. The translucent tiles stack up, darkening the tile below.

This experiment worked for what I intended, so I moved on to new ones.

Implementación de búsqueda de ruta con niveles de profundidad

El siguiente vídeo muestra a un actor moviéndose por los diferentes niveles de profundidad de un mapa simple, y luego dirigiéndose por un rato a puntos aleatorios del primer nivel:

Aunque el actor alcanza su objetivo, no conseguí de momento evitar que en dos puntos concretos decidiera atravesar el techo como parte de su ruta, a pesar de que yo había modificado el código para que en teoría lo evitara. Pero se trata de un problema menor para lo que pretendía implementando la búsqueda de ruta: que en otros experimentos en el que el usuario moviera a su personaje, a su alrededor hubiera actores moviéndose y actuando en base a su inteligencia artificial.

El movimiento en tres dimensiones suele dividir este tipo de juegos. Dwarf Fortress es famoso en parte por cómo gestiona las diferentes capas, permitiendo profundizar en la tierra durante un par de decenas de niveles para construir tu fortaleza, o elevándola varios niveles sobre el nivel del mar si se quiere. En contra, Rimworld, programado con Unity, sacrifica esa verticalidad para ofrecer mejores gráficos y una complejidad muy limitada en comparación con Dwarf Fortress. Yo tenía claro que los sprites en dos dimensiones bastaban a cambio de ofrecer un juego con una simulación compleja y una inteligencia artificial que, con suerte, sorprenda a menudo. Y que actúe de una manera razonable en un primer lugar.

Programé este experimento hace unos pocos días, pero recuerdo que dediqué un par de horas intentando solucionar un problema. Cuando el actor debía encontrar su ruta hasta el interior de la casa, lo que debería obligarlo a pasar por la puerta, dado que se trata de una única entrada despejada, el actor se dirigía al muro occidental de la casa, su imagen pasaba a través del muro y acababa la ruta en su objetivo. Yo me aseguraba de que el algoritmo de búsqueda de ruta identificaba el muro como impasable, pero a pesar de ello lo pasaba. Al final, después de mucho probar, revisé lo que yo había asumido sobre la escena: ¿de verdad los gráficos dibujaban al actor en el nivel de altitud en el que está de verdad? Resultó que había dejado eso para otro momento, y la búsqueda de ruta hacía su trabajo: en vez de atravesar el muro, lo subía y luego atravesaba el techo para llegar a su objetivo. La función que dibuja la escena reflejaba al actor de manera indistinta atravesando el muro que en realidad se encontraba en un nivel por debajo.

A raíz de eso, y de manera previsible, para que el algoritmo de búsqueda de ruta funcionara mejor en tres dimensiones tuve que añadir otros booleanos a cada terreno. Aparte de si bloqueaban el paso a través, debían marcar si bloqueaban subir a un nivel superior, para que a un actor no le diera por elevarse por el cielo, y también debían marcar si bloqueaban lo contrario, bajar a un nivel inferior, para que no atravesaran varias capas de tierra subterránea para llegar a un sótano. La modificación acabó funcionando en su mayor parte, lo que se ve en el vídeo cuando el actor baja por la escalera para alcanzar el sótano.

Para dibujar las diferentes capas de altitud me fijé en cómo lo hacía Dwarf Fortress. Cuando el usuario sube la vista a un nivel superior, si alguna casilla está vacía, el programa debería dibujar lo que está por debajo, pero tintado de otra manera para que no le parezca al usuario que está viendo elementos en la misma dimensión. En un primer lugar el código copiaba cada una de esas casillas inferiores y las tintaba con más o menos azul en función de lo lejos del nivel actual que se encontraban. Eso se ve en el vídeo. Sin embargo, reducía los fotogramas por segundo de una manera bestial. Un par de días después opté por algo más simple y que además funciona mejor: dibujo la casilla original de manera normal, pero luego voy dibujando casillas traslúcidas por cada nivel de altitud que lo separe de la vista. Las transparencias se acumulan y oscurecen la casilla inferior.

El algoritmo me sirve de momento como ha quedado, así que paso a otros experimentos.

(Iteration #2) Simple experiment about neuroevolution

After changing some elements of the experiment, I got the actors behaving in a way closer to what I wanted:

I reestructured the inputs, the sensorial information, that each of the neural networks received. I thought that including so many values that only held the information about where some fruit was located, even if they included the notion of the cardinal direction where it was, destroyed the balance with the rest of the inputs. So I reduced them to the following:

  • A normalized value, from 0.0 to 1.0, that represents each turtle’s health
  • A normalized value, from 0.0 to 1.0, that represents how close is the closest fruit
  • A value of 1.0 if a turtle has another one right next, and 0.0 otherwise

Although I couldn’t think of an obvious way the final input would affect the behavior, it was information present in the simulation, and part of a neural network’s job consists in not using the information that doesn’t help it achieve its objective.

I also added an output: if it received the maximum value, the actor would walk a tile in a random direction. As the video shows, in a few generations those turtles that received the highest values for that single output ended up reproducing more, because moving through the map got them closer to the fruit. They dominated so much that I reduced the amount of fruit present at any given moment, to make sure they weren’t just walking over it randomly. Many of the members of many generations gravitate towards the fruit; after all, the inputs include a measure of how close the closest piece is. I don’t know if the information of whether each turtle had another one right next to them affected anything.

The experiment went well enough for me, and I moved on to more interesting ones.