08 mayo 2024

"Programming Game AI by Example" por Mat Buckland

No es fácil encontrar buenos libros sobre inteligencia artificial (IA) aplicada a juegos. La mayoría no pasan del nivel introductorio y cuando llegan a explicar el algoritmo A*, lo consideran ya material avanzado y lo dejan ahí.

En mi opinión, la gran joya en esta materia es el "AI for Games" de Ian Millington, que combina unos contenidos vastísimos con unas explicaciones claras, detalladas y realmente pertinentes para el desarrollo de juegos. Desde que lo leí no me había encontrado con otra obra que se le pudiera comparar hasta que me topé con este al que le dedicamos el artículo. No en vano, el libro de Buckland fue una de las referencias de Millington.

Es cierto que "Programming AI by Example" no alcanza en amplitud de contenidos a "AI for Games", pero sí es cierto que los que trata lo hace con gran profundidad y claridad. También hay que reconocerle que trata varios temas ausentes de la obra de Millington, como la integración de sistemas de scripting en nuestros juegos (para simplificar el diseño de los algoritmos de IA) y el capítulo sobre agentes guiados por objetivos.

El libro está lleno de buenas figuras ilustrativas que permiten seguir las explicaciones. 

A diferencia de Millington, las implementaciones no las ilustra en pseudocódigo, sino en C++; lo que para mi gusto es un punto negativo porque no se trata precisamente del lenguaje más intuitivo del mundo. También es cierto que el libro acumula ya algunos años y, cuando se publicó por primera vez, la única alternativa para crear juegos era C++. En todo caso, las explicaciones son lo suficientemente extensas y claras como para que el entendimiento pleno del código no sea imprescindible.

Me ha llamado también la atención la insistencia en mostrar diagramas de UML de las jerarquías de clases de las diferentes implementaciones. Entiendo por qué lo hace y tampoco es que sobren, pero es algo que me ha hecho sonreír por lo pasado de moda que está. Ya no se suelen ver diagramas de UML en los libros.

A pesar de todas esas señales del tiempo transcurrido desde la publicación del libro, el campo de la IA para juegos no ha avanzado tanto como para dejar desfasado su contenido. Estos siguen estando vigentes. Si acaso, se echan en falta algunas herramientas que han surgido posteriormente como los árboles de comportamiento o el aprendizaje neuronal.

Conclusión: el libro me ha parecido excelente. Recomiendo que lo leas lectura  antes del de Millington. De esa manera, se puede potenciar su utilidad como pieza introductoria para facilitar la comprensión posterior del Millington y ampliar con este lo que deja pendiente el de Buckland. Con esta aproximación creo que cubrirás lo mejor que se ha escrito hasta el momento en el campo de la IA para juegos.

07 mayo 2024

Cómo implementar un MiniMap en Godot


Hace unos días publiqué un artículo sobre un curso de Godot que acababa de terminar en Udemy. Se trataba de un juego de acción en 2.5D, así que aunque el curso en sí no hablaba de ello, me pareció la base perfecta para aprender a hacer MiniMaps en Godot.

Como ya sabrás, un MiniMap es una pequeña ventana que incluyen muchos juegos para mostrar dónde se encuentra el personaje dentro del nivel. Tienes un ejemplo en la imagen que abre este artículo y que se corresponde con el MiniMap de War of Warcraft.

Resulta que implementar un MiniMap básico en Godot es muy fácil y, en su versión más básica, ni siquiera requiere código. Vamos empezar explicando cómo se puede implementar un MiniMap estacionario y a partir de ahí lo vamos a sofisticar iterativamente añadiéndole sucesivamente la siguientes características:

  1. Haremos que el MiniMap se mueva siguiendo al personaje principal.
  2. Representaremos al personaje y a su entorno con iconos, dentro del MiniMap.
  3. Veremos cómo hacer para que el MiniMap sea redondo.
  4. Adornaremos el MiniMap con un borde.

MiniMap básico estacionario

Este MiniMap no se mueve con el personaje sino que muestra una sección del escenario. Sólo tiene sentido si nuestro nivel es muy pequeño y puede mostrarse entero dentro del MiniMap. Aun así, a pesar de su limitada utilidad, es la base de la que parten todo los MiniMaps.

Para implementarlo tendremos que añadir los siguientes nodos a la jerarquía de nuestra escena:


El nodo SubViewportContainer nos permitirá anclar el MiniMap a la zona de la pantalla en la que queramos que se sitúe, simplemente eligiendo ese nodo y fijando su anclaje en la pestaña de la vista principal:


Una vez colocado el MiniMap probablemente querrás definir su tamaño. Para eso, tienes que fijar primero el tamaño del nodo Subviewport, encargado de mostrar la vista de la cámara. Para ello tienes que seleccionar ese nodo y configurar la propiedad Size:



Es posible que tras redimensionar el nodo Subviewport tengas que volver a fijar el anclaje del nodo SubViewportContainer.

Hecho eso, tendremos una ventana, superpuesta a la principal del juego, que mostrará lo que enfoque el tercer nodo: el de la cámara. Lo que tendremos que hacer es colocar dicha cámara sobre el escenario, enfocándolo en vertical. Yo suelo configurar esa cámara en proyección ortogonal porque así el MiniMap queda más claro. Con su parámetro Size podemos abarcar más o menos escenario dentro del enfoque. Por último, es importante desmarcar el parámetro Current para no usurpar la función de la cámara principal del juego. Al final, esos parámetros de la cámara pueden quedar quedar así:



En este punto ya tendremos la versión más básica del MiniMap.



Sin embargo, este MiniMap tiene muchas carencias que resolveremos en las secciones siguientes.

MiniMap móvil


El primer problema que tenemos en este punto es que al no moverse la cámara hay que ponerle un Size gigantesco para que todo el escenario quepa en la recuadro del MiniMap. Eso le resta valor porque cuanto mayor sea el escenario, más se reducirán sus detalles en el MiniMap. Lo que se hace normalmente en esos casos es reducir el Size de la cámara, haciendo que enfoque sólo una parte del escenario en torno al personaje del jugador y hacer que le siga. De esta manera, aunque la cámara no abarque todo el escenario sí que dará suficiente detalle del entorno del jugador, que es lo que realmente le interesa a este.

Para hacer que la cámara del MiniMap siga al jugador, tienes que añadirle a aquella el siguiente script:


Como verás, se limita a ubicar la cámara en la misma posición que el usuario, conservando la altura inicial a la que la hubiéramos colocado en el editor. Dado que hemos implementado el método _Process(), la posición de la cámara se actualiza en cada fotograma. El efecto será que la cámara se moverá con el jugador, enfocándole cenitalmente.

El parámetro _player es una referencia que habrá que pasar por el inspector, arrastrando el objeto del escenario del jugador sobre el campo _player del script. 

Representación con iconos de los personajes y los elementos del mapa


El siguiente problema será que, por mucho que hayamos acercado la cámara, es muy posible que muchos de los elementos importantes sean demasiado pequeños o no se vean con claridad. Para resolver eso, la mayor parte de los MiniMaps representan esos elementos con iconos, tal y como se puede ver en el MiniMap de ejemplo que inicia este artículo.

En mi caso particular, dado que mi juego era 2.5D, los personajes eran sprites por lo que al ser planos resultaban invisibles al enfocarlos cenitalmente con la cámara del MiniMap.

La solución consiste en añadir un sprite justo encima de los elementos que queramos que se muestren en el MiniMap. Esos sprites mostrarán el icono que queramos que represente a esos elementos.

Por ejemplo, en mi caso he añadido el sprite de un casco encima de mis enemigos.


Lógicamente, ese icono debe estar en un plano perpendicular al eje de enfoque de la cámara del MiniMap para que esta lo pueda captar bien. Si la cámara del MiniMap enfoca desde el eje Y, el icono tendrá que reposar en el plano XZ.

En este punto, la problemática será que esos iconos sean invisibles para la cámara principal del juego y sólo se muestren en la del MiniMap. Para resolverlo podemos usar las capas de renderizado.

En mi juego he creado las siguientes capas de renderizado, dentro de Project > Project Settings... > General > Layer Names > 3D Render:


Las piezas del escenario las he colocado en la capa 1, al personaje del jugador en la capa 1 y a los enemigos en la capa 3. Esto se configura en el apartado de VisualInstance3D del sprite principal de cada uno de los elementos. Por ejemplo, en el caso del sprite con el que represento al jugador, su configuración es:


Para los elementos 3D, la capa de renderizado se configura en el apartado del MeshInstance.

En cuanto a los iconos de los marcadores, tanto del jugador, como de los enemigos, los he dejado en la capa 4.




Para que la cámara principal del juego no muestre los iconos del MiniMap se configura su Cull Mask para que no muestre la capa donde hayamos situado dichos iconos.

En el caso de nuestro ejemplo, la cámara principal tiene el siguiente Cull Mask:


Teniendo en cuenta las capas que yo había creado, lo anterior significa que la cámara principal del juego sólo muestra el escenario (capa 1), al jugador (capa 2), a los enemigos (capa3), pero no los marcadores del MiniMap (capa 4).

Por contra, el Cull Mask de la cámara del MiniMap deberá incluir la capa donde estén los iconos.

En el caso del ejemplo, la cámara del MiniMap tiene la siguiente configuración:


Fíjate por tanto, que en el MiniMap se mostrará el escenario (capa 1) i los iconos de los marcadores (capa 4). No muestro ni al jugador ni a los enemigos porque estos ya está representados por sus respectivos marcadores.

El resultado será que el jugador y los enemigos se verán representados de la siguiente manera en el MiniMap:



MiniMap redondo


En este punto tendremos un MiniMap funcionalmente completo, aunque estéticamente modesto.

Puede ser que en nuestro juego nos encaje tener un MiniMap cuadrado, ¿pero cómo lo haríamos si quisiéramos un MiniMap redondo?

Hay varias maneras de hacerlo. Unos prefieren usar un shader, pero yo creo que es mucho más sencillo usar una máscara. Esta es un sprite que se superpone a una imagen para definir qué partes de la imagen se pueden ver y cuales no. El sprite que hace de máscara definirá las partes que se pueden ver con un alpha 1 (independientemente del color) y las que no con un alpha 0 (o sea transparente).

Un editor gráfico como Gimp te puede bastar para hace una máscara como la siguiente:


Importa la imagen de la máscara en Godot y asóciala a un nodo Sprite2D que debes situar como padre del MiniMask:


Para que el sprite haga de máscara debes fijar el parámetro Clip Children de su sección CanvasItem a Clip Only. Eso hará que enmascare a todos los nodos que tenga como hijos.



Es posible que cuando recoloques a los nodos del MiniMap debajo del sprite de máscara, te veas obligado a asegurar que los tamaños (Transform-Sizes) de los nodos SubviewPortContainer y Subviewport coincidan. De no coincidir es posible que sólo veas una porción del MiniMap. 

También asegúrate de que el el parámetro Anchor Preset del SubViewportContainer esté en FullRect para que se amolde al tamaño del sprite de máscara.


El problema de este esquema es que el MiniMap perderá el anclaje automático respecto a la pantalla, ya que el padre del MiniMap (el sprite de máscara) ya no se recolocará automáticamente cuando se redimensione la ventana del juego. En estos casos, suele ser buena idea colocar al sprite de máscara (ya a todos sus hijos) por debajo de una nodo PanelContainer, de tal manera que podamos anclar este a la posición de la pantalla que queramos.


En este punto, MiniMap será redondo, pero el problema será que por alguna razón el panel sobre el que se muestra no será transparente del todo (amplía la foto y fíjate en los bordes del MiniMap):



La causa es que el estilo por defecto de los paneles de Godot incluye que su fondo no sea transparente. Así que hay que cambiar eso. Ve a las propiedades del PanelContainer y, en la sección ThemeOverrides > Styles, crea un recurso StyleBoxEmpty. Eso reseteará los valores por defecto del estilo del Panel Container y este pasará a ser completamente transparente.



Borde del MiniMap


Lo normal es dotar al MiniMap de un borde que lo resalte respecto al fondo y que lo ponga a tono con la estética general del juego. Veamos cómo hacerlo.

Si no somos artistas podemos buscar en Internet bordes para nuestros MiniMaps. Eso sí, te aconsejo que revises los derechos de lo que encuentres, para asegurarte de que lo puedas usar en tu juego. Una búsqueda por los términos "Minimap circular texture download" puede ofrecerte muchos ejemplos. Uno de ellos es el que he usado en este tutorial:


Fíjate en que la imagen debe contener un canal Alpha, dejando con valor 0, todas aquellas zonas, como el interior del anillo, a través de las cuales queramos que se vea nuestro MiniMap.

La idea es colocar esa imagen sobre el MiniMap, tapando los bordes. Para hacerlo, tenemos que añadir un nodo TextureRect como hijo del PanelContainer del MiniMap. Sobre ese nuevo nodo deberemos arrastrar la imagen del borde, lo que la mostrará en el editor.

Es probable que la imagen tenga un tamaño que no nos encaje en el MiniMap, así que deberemos redimensionarla. Para eso, deberemos fijar el parámetro Expand Mode, del TextureRect, a Ignore Size. A partir de ahí, podremos cambiar el tamaño de la imagen del borde modificando su parámetro Custom Minimum Size. Probablemente también tengamos que cambiar los parámetros Position y Scale del nodo MiniMapMask para redimensionar y centrar la imagen del MiniMap dentro de la del borde.

El esquema de nodos resultante será el siguiente:



Y el resultado:


Fíjate que, en Godot, el orden de pintado de los nodos es de arriba a abajo de la jerarquía de la escena. Eso quiere decir que primero se pintan los nodos que están en la parte más cercana a la raíz y luego sus hijos. Por eso es importante que el TextureRect esté por debajo del MiniMapMask, de manera que primero se pinte el MiniMapMask y luego el TextureRect. La consecuencia de lo anterior será que el TextureRect se pintará encima del nodo MiniMapMask, tapando sus bordes. 

Prueba a invertir el orden y verás el efecto.