08 febrero 2025

Navegación 2D en Godot

Los NPC o Non-Playable-Characters son todos los personajes del juego que no están controlados por el jugador, pero que interactúan con él. Pueden abarcar desde los aliados del jugador hasta los enemigos que intentan acabar con él. Uno de los grandes retos del desarrollador de juegos es conseguir dotar a sus NPC de una serie de comportamientos que transmitan la apariencia de vida y de inteligencia.

Uno de los síntomas más claros de vida es el movimiento. Si algo se mueve por iniciativa propia, una de las causas que contempla instintivamente nuestra mente es que pueda estar vivo. Por otro lado, uno de los síntomas de un mínimo de inteligencia es que ese movimiento se produzca evitando los obstáculos que se presente. Si una criatura se mueve cuando nos acercamos, pero inmediatamente se da de bruces con la pared que tiene enfrente, podremos pensar que esa criatura está viva, pero no que sea muy inteligente.

Por eso, la mayor parte de los engines de videojuegos, a poco que tengan un paquete de inteligencia artificial, lo primero que meten en él es alguna herramienta de búsqueda de caminos (pathfinding), para permitir que los NPC puedan orientarse por el escenario.

Godot no es una excepción y por eso incorpora la funcionalidad pathfinding mediante mallas, tanto en 2D como en 3D. Por sencillez, vamos a centrarnos en el primer ámbito.

Creación de un mapa

Para orientarnos, lo que mejor nos viene a la personas es un mapa. A un engine le pasa lo mismo. Ese mapa se basa en nuestro escenario, pero modelarlo hace falta un nodo denominado NavigationRegion2D. Debemos añadirlo a la escena de cualquier nivel que queramos mapear. Por ejemplo, suponiendo que nuestro nivel de basase en varios TileMapLayer (uno con los tiles del suelo, otro con los muros perimetrales y otro con los tiles de los obstáculos del interior del perímetro), la estructura de nodos con el del NavigationRegion2D podría ser la siguiente:

Jerarquía de nodos con un NavigationRegion2D
Jerarquía de nodos con un NavigationRegion2D

Observa que el nodo del NavigationRegion2D es el padre de los TileMapLayer ya que por defecto sólo construye el mapa con las conclusiones que saque de analizar sus nodos hijos.

Al configurar el NavigationRegion2D a través del inspector, veremos que requiere la creación de un recursos NavigationPolygon, en ese recurso será donde se guarde la información del mapa de nuestro nivel.

Configuración de un NavigationRegion2D
Configuración de un NavigationRegion2D

Una vez creado el recurso, al pinchar sobre él, veremos que tiene bastantes propiedades a configurar.

Configuración de un NavigationPoligon
Configuración de un NavigationPoligon

En un caso sencillo como el de nuestro ejemplo, sólo hay que configurar dos parámetros:

  • Radius: Aquí pondremos el radio de nuestro NPC, con un pequeño margen adicional. De esta manera, el algoritmo de pathfinding añadirá esta distancia al contorno de los obstáculos para evitar que los NPC rocen con ellos.
  • Parsed Collision Mask: Todos los objetos de los nodos hijos que se encuentren en las capas de colisión que marquemos aquí se considerarán un obstáculo.
Hecho lo anterior, nos bastará con marcar los límites de nuestro mapa. Para hacerlo fíjate en que, cuando pulses el recurso del NavigationPolygon en el inspector, aparecerá el siguiente toolbar en la pestaña de la escena:

Toolbar para definir la forma de un NavigationPolygon
Toolbar para definir la forma de un NavigationPolygon

Gracias a dicho toolbar podremos definir las lindes de nuestro mapa. Luego, el NavigationRegion2D se encargará de identificar los obstáculos y hacer "agujeros" en nuestro mapa para señalar las zonas por las que nuestro NPC no podrá pasar.

El primer botón (verde) del toolbar sirve para añadir nuevos vértices a la forma de nuestro mapa, el segundo botón (azul) para editar un vértice ya situado, mientras que el tercero (rojo) sirve para borrar vértices.

En un escenario sencillo, como un tilemap, puede que nos baste con dibujar los cuatro vértices que limiten la extensión máxima del tilemap. En la siguiente captura se puede ver que he situado un vértice en cada esquina del recinto que quiero mapear.

Vértices de nuestro NavigationPolygon
Vértices de nuestro NavigationPolygon

Una vez definidos los límites de nuestro mapa, tendremos que lanzar su generación. Para eso pulsaremos el botón Bake NavigationPolygon, del toolbar. 

El resultado será que NavigationRegion2D marcará de azul las zonas por las que se puede puede deambular, una vez analizados los límites del mapa y detectados los obstáculos dentro de estos.

Tendremos que acordarnos de pulsar el botón Bake NavigationPolygon, siempre que añadamos nuevos obstáculos al nivel o movamos los ya existentes, o si no no se actualizará el mapa de las zonas navegables.

NavigationPolygon, una vez construido
NavigationPolygon, una vez construido

Para que un objeto sea identificado como obstáculo, tiene que ser configurado para pertenecer a alguna de las capas de colisión que hayamos configurado en el campo Parsed Collision Mask del NavigationPolygon. En el caso de un TileMapLayer esto se configura de la siguiente manera:

  1. En aquellos TileMapLayer que contengan obstáculos tendremos que marcar el parámetro Physics -> Collision Enabled.
  2. En el recurso Tile Set que estemos usando en el TileMapLayer, tendremos que asegurarnos de añadir una capa física y situarla en una de las capas de las contempladas en el Parsed Collision Mask del NavigationPolygon.
  3. También debes añadir una capa de navegación y fijarla al mismo valor que la capa de navegación del nodo NavigationRegion2D.
Por ejemplo, el TileMapLayer que contiene los obstáculos interiores en mi ejemplo, tiene la siguiente configuración para el Tile Set:

Configuración del TileMapLayer
Configuración del TileMapLayer


Luego, dentro del TileSet, no todos los tiles tienen que ser obstáculos, solo aquellos que tengan un collider configurado. Recuerda que los collider de los tiles se configuran desde la pestaña TileSet. No entro en más detalles al respecto porque eso tiene más que ver con la configuración de los TileMaps que con la navegación en sí.

Configuración del collider de un tile
Configuración del collider de un tile


Uso del mapa

Una vez creado el mapa, nuestros NPC deben ser capaces de leerlo. Para eso necesitan contar con una nodo MeshNavigationAgent2D en su jerarquía.

El nodo MeshNavigationAgent2D dentro de la jerarquía de un NPC
El nodo MeshNavigationAgent2D dentro de la jerarquía de un NPC

En un caso sencillo, puede ser que ni siquiera tengas que cambiar nada de su configuración por defecto. Basta con que te asegures de que su campo Navigation Layers este en el mismo valor que el del NavigationRegion2D.

Desde tu script, una vez que tengas una referencia al nodo MeshNavigationAgent2D, te bastará con ir colocando la posición a la que quiera ir en la propiedad TargetPosition del MeshNavigationAgent2D. Por ejemplo, si tuviéramos un NPC que quisiera esconderse en un punto del mapa, podría incluir una propiedad en la que se le pidiera al nodo MeshNavigationAgent2D que buscase la ruta para alcanzar ese punto, tal y como se puede ver en la captura.

Fijado de una posición de destino en un MeshNavigationAgent2D

Una vez que le hayamos dicho al nodo a dónde queremos ir, este calculará una ruta y nos irá dando las diferentes escalas de esa ruta, conforme las vayamos alcanzando.

Para que el nodo nos diga dónde se encuentra la siguiente escala de la ruta, tendremos que llamar al método GetNextPathPosition(). Es importante tener en cuenta que este método se encarga de actualizar bastantes cosas internas del algoritmo de pathfinding, por lo que un requisito es que lo llamemos una vez en cada llamada a _PhysicsProcess() de nuestro NPC.

En la captura tienes el _PhysicProcess() del agente que estoy utilizando de ejemplo. La mayor parte del código de la captura se refiere a temas que no son objeto de este artículo, pero lo incluyo por ofrecer algo de contexto. En realidad, para lo que nos ocupa, sólo tienes que fijarte en las líneas 221 a 225.

Obtención de la siguiente escala en la ruta, dentro del _PhysicsProcess.
Obtención de la siguiente escala en la ruta, dentro del _PhysicsProcess.

En la línea 225 se puede ver cómo llamamos a GetNextPathPosition() para que obtener cuál es el siguiente punto al que debemos dirigirnos para seguir la ruta trazada hasta el objetivo fijado en TargetPosition. Cómo hagamos para llegar hasta ese punto será cosa nuestra. El MeshNavigationAgent2D se limita a garantizar dos cosas: 

  • Que no hay ningún obstáculo entre el NPC y el siguiente punto de la ruta que te marque.
  • Que si vas siguiendo los puntos de ruta que te vaya dando acabarás llegando al objetivo... si existe alguna ruta que lleve a él.
Hago especial hincapié porque, a diferencia del NavigationAgent de Unity, el de Godot no mueve al NPC en el que vaya incrustado. Se limita a ir dando indicaciones sobre a dónde ir.

Aparte del caso general, en el código de la captura hay ciertas salvedades que hay que tener claras.

Para empezar, la documentación de Godot comenta que no hay que seguir llamando a GetNextPathPosition() una vez que hayamos acabado de recorrer la ruta o, de otro modo, el NPC puede mostrar "temblores" por seguir forzando actualizaciones del algoritmo de pathfinding habiendo alcanzado ya el objetivo. Ese es el motivo por el que, en la línea 224, compruebo que no hemos alcanzado aún el final de la ruta, antes de llamar a GetNextPathPosition(). Así que no te olvides de comprobar que IsNavigationFinished() devuelve falso, antes de consultar a GetNextPathPosition().

Por otro lado, el algoritmo de pathfinding tarda un cierto tiempo en converger, sobre todo al principio. Si le preguntamos demasiado pronto provocará una excepción. Lo normal es que tarde uno o dos fotogramas físicos (es decir, una o dos llamadas a _PhysicsProcess). Ese ese el motivo de que en la línea 222 compruebe si la propiedad IsReady es verdadero antes de continuar. El problema es que MeshNavigationAgent2D no tiene una propiedad IsReady (aunque debería tenerla), lo que pasa es que es una propiedad que me he creado yo para hacer una consulta nada intuitiva:

Manera de comprobar que el algoritmo de pathfinding está listo para responder consultas
Manera de comprobar que el algoritmo de pathfinding está listo para responder consultas

Básicamente, lo que hace la propiedad es asegurarse de que el algoritmo de pathfinding ha llegado a generar al menos una versión de la ruta. 

Conclusión

Y ya está, fijando el TargetPosition del MeshNavigationAgent2D, y dirigiéndote a los sucesivos puntos que te vayan marcando las llamadas a GetNextPathPosition(), podrás llegar a cualquier punto alcanzable del NavigationRegion2D que hayas definido. 

Con eso ya tienes lo básico, pero si en algún momento necesitases analizar la ruta completa que haya calculado el algoritmo, puedes pedírsela llamando al método GetCurrentNavigationPath(). Ese método te devolverá un array con las posiciones de las diferentes escalas de la ruta.

01 febrero 2025

Estructura de carpetas de un proyecto de desarrollo de un videojuego

Foto de Niklas Ohlrogge
Un juego utiliza una cantidad masiva de assets. Imágenes, sonidos, ficheros de código, etc, necesitas una manera de organizarlos o, pasado poco tiempo, te costará encontrar las cosas. Por eso, una pregunta típica en los foros es ¿Qué estructura de carpetas le doy a mi proyecto?

Bueno, lo cierto es que esta pregunta no tiene una única respuesta. Muchas veces depende del proyecto, de tus propias particularidades como desarrollador y de los requisitos que a veces imponen los diferentes engines. De todos modos, sí que hay cierto consenso en algunas buenas prácticas que enumeraré en este artículo para que elijas la que más se ajuste a ti.

Los requisitos de los engines

Para empezar, vamos a enumerar las carpetas que siempre tendrás como requisito del engine que estés usando.

En el caso de Unity, este te exige tener 3 carpetas:

  • Assets: Aquí depositarás todos los recursos que crees para el juego.
  • Packages: Aquí se guardan los paquetes que te descargues del package manager.
  • Project Settings: Es donde el editor almacena las configuraciones de tu proyecto.

Las tres carpetas anteriores carpetas anteriores son lo mínimo que deberías guardar en tu repositorio de versiones, en caso de usar uno (lo más recomendable).

Lo normal es que toques dentro de la carpeta Assets, ya que del contenido de las otras carpetas ya se ocupa el editor por ti. La estructura de carpetas que tengas dentro de la de Assets es cosa tuya, pero el editor te exige tener al menos dos:

  • Scripts: Debes contar con esta carpeta para guardar en ella los scripts con los que vaya a funcionar tu juego. Unity compila el juego con los scripts que encuentre en esta carpeta.
  • Editor: Si desarrollas extensiones para el editor o inspectores personalizados, deberás crear esta carpeta y guardar el código de las extensiones allí. No los guardes en la carpeta de Scripts, porque la compilación fallará cuando quieras construir el paquete final, para ejecutar el juego al margen del editor.

En el caso de Godot, los requisitos son más laxos. La única carpeta fija es la de addons, ya que es ahí donde se recogen tanto los plugins que hayas desarrollado tú como los que te hayas bajado del AssetLib.

Partiendo del cumplimiento de los requisitos anteriores, el cielo es el límite a la hora de organizar nuestros assets. Veamos ahora los esquemas habituales que más usa la gente.

Distribución por tipos

Esta es la distribución más clásica. Yo me basé en ella para organizar los recursos de Prince of Unity.

Creas una carpeta para albergar cada tipo de asset: una para imágenes, otra para sonidos, otra para prefabs/escenas, otra para niveles, otra para shaders, etc.

Esta es la estructura que suele recomendar la documentación de Unity y por eso es probable que sea la primera que utilices. 

https://unity.com/es/how-to/organizing-your-project
Ejemplo de organización por tipos, según la documentación de Unity

Distribución funcional

Sin embargo, hay muchos que reniegan de la estructura anterior. Alegan que para distinguir unos assets de otros por el tipo resulta inútil clasificarlos a nivel de carpeta, porque los editores (tanto de Unity como de Godot) te permiten buscar los assets, filtrando por su tipo.

Por eso, la documentación de Godot aboga por situar los assets lo más cerca posible de las escenas que los utilizan. Hay guías de estilo para Unreal Engine que recomiendan lo mismo.

En la práctica, eso supone crear una carpeta por escena y agrupar allí los assets que utiliza, independientemente de su tipo. En Unity, eso significa crear una carpeta por prefab.

Generalmente, la excepción a lo anterior es el código que se suele preferir que vaya en una carpeta aparte donde se concentren todos los ficheros de código fuente (en el caso de Unity ya hemos dicho que contar con la carpeta Scripts es un requisito).

Pero lo normal es que nuestras escenas/prefabs no sean islas autónomas, sino que compartan recursos entre si. ¿Cómo se hace en esos casos? ¿se copia el recurso en la carpeta de cada escena/prefab? No, en esos casos se suele crear una carpeta para los recursos compartidos, puede ser una carpeta  del mismo nivel que la de las escenas/prefab o una carpeta situada en un nivel superior a ellas.

Por ejemplo, la guía de estilo para Unreal Engine que mencionaba antes ofrece esta estructura de ejemplo:

Ejemplo de organización funcional
Ejemplo de organización funcional

Los defensores de esta práctica alegan que es preferible tener una estructura profunda de carpetas, cada una de ellas con unos pocos archivos (aunque sean de distintos tipos), que tener pocos directorios pero abarrotados de archivos. Resulta más fácil saber qué recursos moviliza una escena/prefab si lo ves todo dentro de la misma carpeta.

Conclusión

Organizar tu proyecto es una decisión muy personal. No hay grandes reglas grabadas en piedra. Al final todo se reduce a ordenar tus assets de la manera que te resulte más cómoda y te haga ser más productivo. Quizás, probar alguno de los sistemas de organización de los que te he hablado aquí te ayude a encontrar el tuyo propio.

 

30 enero 2025

"Game Development Patterns with Godot 4" por Henrique Campos

Hace tanto que se desarrollan aplicaciones y videojuegos que será difícil que, cuando lo hagas tú, te encuentres con una situación que no haya resuelto otro desarrollador antes. Con el tiempo, eso ha dado lugar a un conjunto de soluciones generales consideradas como las mejores prácticas a la hora de solucionar problemas comunes. Es lo que se conoce como patrones de diseño; un conjunto de "recetas" o "plantillas" que los desarrolladores pueden seguir para estructurar su código a la hora de implementar las soluciones a determinadas problemáticas.

Es una materia que se estudia en cualquier carrera de relacionada con la programación, siendo el libro "Design Patterns, Elements of Reusable Object-Oriented Software", de Gamma, Helm, Johnson y Vlissides (el célebre Grupo de los Cuatro) el libro, ya clásico, que inició esta rama de estudio. 

Sin embargo, contados de manera académica, los patrones de diseño pueden ser arduos de entender y no siempre queda clara cual es su aplicación. Es precisamente aquí donde brilla la obra "Game Development Patterns with Godot 4" de Henrique Campos.

Campos elige con bastante acierto 9 de los 23 patrones de diseño enunciados por el Grupo de los Cuatro y los explica de una manera clara, sencilla y rica en ejemplos aplicados al desarrollo de juegos. Los patrones elegidos por Campos me parecen los más útiles y los que tienen aplicación más inmediata en cualquier juego. No he echado de menos ninguno de los otros patrones de diseño restantes porque siempre me parecieron demasiado abstractos y esotéricos.

De manera muy original, Campos plantea sus ejemplos a partir de diálogos o comunicaciones entre el diseñador de un juego ficticio y su programador. En esas comunicaciones, el diseñador demanda funcionalidades enfocadas a enriquecer aspectos concretos de un juego, mientras que el programador recibe esas demandas y las tiene que encajar con su objetivo de crear un código escalable y fácil de mantener. En cada uno de esos ejemplos, se explica un patrón de diseño concreto como la mejor solución para encajar la demanda del diseñador con el objetivo de programador.

Siguiendo ese esquema, el autor explica el patrón singleton, el del observador, la factoría, las máquinas de estados, el patrón comando, el de estrategia, los decoradores, los localizadores de servicios y las colas de eventos. Todo ello en un orden creciente de complejidad y apoyándose en lo anterior. Por el camino, explica también de manera sencilla pero efectiva principios básicos del desarrollo como los de la programación orientada a objetos o los principios SOLID.

La implementación de los ejemplos se hace en GDScript, con Godot 4. He de reconocer que al principio me acerqué al libro con desconfianza porque no creía que GDScript fuera un lenguaje lo suficientemente rico como para ilustrar estos patrones (reconozco que desarrollo en Godot C#). Sin embargo, el código de campos es muy expresivo y el GDScript es tan conciso que al final los ejemplos se leen como si fueran pseudocódigo. Al final no he echado de menos al C#, porque el código de GDScript conseguía transmitir en unas pocas líneas la idea de los ejemplos lo que los hacía muy fáciles de leer y comprender.

Por tanto, creo que es un libro muy recomendable que hace ameno y divertido una materia que suele repeler por el tratamiento excesivamente académico que ha recibido en otras obras anteriores. Si le das una oportunidad, creo que lo disfrutarás y que te ayudará considerablemente a mejorar la calidad de tu código. 

26 enero 2025

Creación de Gizmos interactivos en Unity

En un artículo anterior expliqué cómo usar los callback OnDrawGizmos() y OnDrawGizmosSelected(), presentes en todos los MonoBehaviour, para dibujar Gizmos que representasen visualmente las magnitudes de los campos de un GameObject. Sin embargo, ya expliqué entonces que los Gizmos implementados de esta manera eran pasivos en el sentido de que se limitaban a representar los valores que metiésemos en el inspector. Pero el editor de Unity está lleno de Gizmos con los que se puede interactuar para cambiar los valores del GameObject, simplemente pinchando sobre el Gizmo y arrastrando. Un ejemplo es el Gizmo que sirve para definir las dimensiones de los Collider. 

¿Cómo se implementan esos Gizmos interactivos? La clave está en los Handles. Estos son "agarraderas" visuales que podemos situar sobre nuestro GameObject y que permiten ser pinchados y arrastrados con el ratón, devolviendo los valores de su cambio de posición, para que podamos calcular con ellos los cambios resultantes sobre las magnitudes representadas. Se entenderá mejor cuando veamos los ejemplos.

Lo primero que hay que tener en cuenta es que los Handles pertenecen al espacio de nombres del editor y por tanto sólo se pueden usar dentro de él. Eso quiere decir que no podemos usar los Handles en las compilaciones finales del juego que ejecutemos de manera independiente al editor. Por esa razón, todo el código que use Handles tiene que situarse o bien en la carpeta Editor o bien con guardas #if UNITY_EDITOR ... #endif. En este artículo explicaré la primera aproximación, por ser más limpia.

Teniendo en cuenta lo anterior, el código de los Gizmos que utilicen Handles deberán situarse en la carpeta Editor y no en la de Scripts. Unity sólo tiene en cuenta la carpeta Scripts a la hora de compilar el código C# para el ejecutable final. Es importante ser riguroso con esto porque si mezclas el código de los Handles con el de los MonoBehaviour parecerá que todo funciona mientras arranques el juego desde el editor, pero cuando intentes compilarlo con File --> Build Profiles... --> Windows --> Build te saldrán errores que te impedirán la compilación. Llegados a ese punto tendrás dos opciones: o reconduces la distribución del código que use Handles a la estructura que voy a explicar aquí o llenas tu código de guardas #if UNITY_EDITOR ... #endif en torno a todas las llamadas a objetos de la librería Handles. De todos modos, creo que la estructura que voy a explicarte aquí es lo suficientemente genéricas como para que no necesites mezclar el código de tus Handles con la de los MonoBehaviour.

Para empezar con los ejemplos, vamos a suponer que tenemos un MonoBehaviour (en nuestra carpeta Scripts) llamado EvadeSteeringBehavior que se encarga de iniciar la huida de su GameObject en caso de detectar que una amenaza se acerca por debajo de un determinado umbral. Ese umbral es un radio, una distancia en torno al GameObject que huye.  Si la distancia a la amenaza es inferior a ese radio, el EvadeSteeringBehavior empezará a ejecutar la lógica de huida. Supongamos que la propiedad que guarda ese radio es EvadeSteeringBehavior.PanicDistance.

Para representar PanicDistance como una circunferencia alrededor del GameObject, podríamos usar llamadas a Gizmos.DrawWireSphere() desde el método OnDrawGizmos() de MonoBehaviour, pero con esa aproximación sólo podríamos cambiar el valor de PanicDistance modificándolo en el inspector. Queremos ir más allá y poder alterar el valor de PanicDistance pinchando sobre la circunferencia y arrastrando en la ventana Scene. Para conseguirlo, vamos a usar Handles a través de un editor personalizado.

Para eso, podemos crear una clase en la carpeta Editor. El nombre no importa demasiado. En mi caso he llamado al fichero DrawEvadePanicDistance.cs. Su contenido es el siguiente:

Código del editor personalizado DrawEvadePanicDistance
Código del editor personalizado DrawEvadePanicDistance

Fíjate en la línea 1, deberían encenderse todas tus alarmas si te encontrases importando esta librería en alguna clase destinada a formar parte del compilado final de tu juego, ya que en ese caso te saldrán los errores que mencionaba antes. Sin embargo, al situar este fichero en la carpeta Editor ya no tendremos que preocuparnos por este problema.

La línea 6 es clave ya que permite asociar este editor personalizado con un MonoBehaviour concreto. Con esta línea le estamos diciendo al editor que queremos ejecutar la lógica recogida en OnSceneGUI() siempre que vaya a representar una instancia de EvadeSteeringBehavior en la pestaña Scene.

Como editor personalizado, nuestra clase debe heredar de UnityEditor.Editor, tal y como puede verse en la línea 7.

Hasta ahora, siempre que me he visto en la necesidad de usar Handles, he acabado estructurando el contenido de OnSceneGUI() de la misma manera que ves entre las líneas 10 y 24. Así que al final se trata casi de una plantilla.

Si queremos acceder a los datos del MonoBehaviour cuyos valores queremos modificar, lo tendremos en el campo target al que tienen acceso todos los editores personalizados. Si bien es cierto que tendrás que hacer casting al tipos que sepas que tiene el MonoBehaviour que estamos representando, tal y como se puede ver en la línea 11.

Debemos situar el código de nuestros Handles entre una llamada a EditorGUI.BeginChangeCheck() (línea 13) y una llamada a EditorGUI.EndChangeCheck() (línea 19) de esta manera, el editor monitorizará si se produce alguna interacción con los Handles. La llamada EditorGUI.EndChangeCheck() devolverá true en caso de que el usuario hubiese interactuado con alguno de los Handles creados desde la llamada a EditorGUI.BeginChangeCheck().

Para definir el color con el que se dibujarán los Handles se hace de una manera muy parecida a como hacíamos con los Gizmos, en este caso cargando un valor en Handles.color (línea 14).

Tenemos múltiples Handles para elegir, los principales son:

  • PositionHandle: Dibuja un origen de coordenadas en la pestaña Scene, idéntico al de los Transforms. Devuelve la posición en la que se encuentre Handle.
  • RotationHandle: Dibuja unas circunferencias de rotación iguales a las que aparecen cuando quieres rotar un objeto en el editor. Si el usuario interactúa con el Handle, este devolverá un Quaternion con el nuevo valor de rotación, mientras que si no lo toca, lo que devolverá será el mismo valor de rotación inicial con el que se creó el Handle.
  • ScaleHandler: De funcionamiento similar al RotationHandle, pero en este caso saca un los ejes y cubos de escalado habituales cuando quieres modificar la escala de un objeto en Unity. Lo que devuelve es un Vector3 con la nueva escala, en caso de que el usuario haya tocado el Handle o la inicial, en caso contrario.
  • RadiusHandle: Dibuja una esfera (o una circunferencia si estamos en 2D) con puntos de agarre para modificar si radio. En este caso, lo que devuelve es un float con dicho radio.

En el ejemplo que nos ocupa, lo natural era elegir el RadiusHandle (línea 15), dado que lo que buscamos es definir el radio que hemos denominado PanicDistance. Cada Handle tiene sus parámetros de creación, para configurar como se muestran en pantalla. En este caso, RadiusHandle requiere de una rotación inicial (línea 16), la posición de su centro (línea 17) y el radio inicial (línea 18).

En caso de que el usuario interactuase con el Handle, su nuevo valor se devolvería como retorno del método de creación del Handle. En nuestro ejemplo lo hemos guardado en la variable newPanicDistance (línea 15). En esas ocasiones, el método EditorGUI.EndChangeCheck() devolvería verdadero, por lo que podríamos guardar el nuevo valor en la propiedad del MonoBehaviour cuyo valor estamos definiendo (línea 22).

Para asegurar que podamos deshacer con Ctrl+Z los cambios en el MonoBehaviour, es conveniente precederlos con una llamada a Undo.RecordObject() (línea 21), señalando el objeto que vamos a cambiar y facilitando un mensaje explicativo del cambio que se va a realizar.

El resultado de todo el código anterior será que, cada vez que se pulse un GameObject que tenga el script EvadeSteeringBehavior, se dibujará una una circunferencia en torno a él, con puntos que se pueden pinchar y arrastrar. Si se interactúa con esos puntos, cambiara el tamaño de la circunferencia, pero también el valor mostrado en el inspector para la propiedad PanicDistance de EvadeSteeringBehavior.

El RadiusHandle mostrado gracias al código anterior
El RadiusHandle mostrado gracias al código anterior

Lo que podemos conseguir con los Handles no acaba aquí. Si nos pusiéramos en lugar de Unity, lo lógico habría sido ofrecer los Handles a los usuarios, para la modificación interactiva de las propiedades de los scripts, y dejar los Gizmos para la representación visual de esos valores desde las llamadas a OnDrawGizmos(). Sin embargo, Unity no dejó una separación de funciones tan clara y, en vez de eso, dotó a la librería Handles de primitivas de dibujo muy similares a las que ofrecen los Gizmos. Eso quiere decir que hay cierto solapamiento entre las funcionalidades de Handles y las de Gizmos, sobre todo en lo que se refiere a dibujado.

Es importante conocer las primitivas de dibujado que puede ofrecer Handles. En muchas ocasiones es más rápido y directo dibujar con las primitivas de Gizmos, desde el OnDrawGizmos(), pero hay cosas que no se pueden conseguir con Gizmos que sí que se pueden dibujar con los métodos de Handles. Por ejemplo, con Gizmos no puedes definir el grosor de las líneas (siempre serán de un pixel de ancho), mientras que las primitivas de Handles sí que tienen un parámetro para definir el grosor. Handles también permite pintar líneas discontinuas, así como arcos o rectángulos con rellenos traslúcidos.

Mucha gente aprovecha las ventajas de los Handles y los usa no sólo para la introducción de nuevos valores sino también para el dibujado, sustituyendo por entero a los Gizmos. Lo que pasa es que eso obliga a extraer toda la lógica de dibujado a un editor personalizado como el que hemos visto antes, lo que implica crear un fichero más y guardarlo en la carpeta Editor.

En todo caso, no se trata de cumplir dogma alguno, sino de conocer lo que ofrecen los Handles y los Gizmos para elegir lo que más nos convenga en cada ocasión.

15 enero 2025

Alertas de configuración de nodos en Godot

Godot se caracteriza por poner el acento en la composición. Cada funcionalidad atómica se concentra en un nodo específico y los objetos complejos (lo que Godot llama escenas) se forman agrupando nodos, y configurándolos, hasta conseguir la funcionalidad deseada. Esto da lugar a jerarquías de nodos en las que unos complementan a otros.

Por eso, hay nodos que, de hecho, no pueden aportar funcionalidad completa si no se ven complementados por otros nodos que se cuelguen de ellos. Un ejemplo clásico es el nodo RigidBody3D, el cual no puede funcionar si no se ve complementado por un CollisionShape3D que defina su forma física.

Godot ofrece un sistema de alertas para avisarte de que un nodo depende de otro para poder funcionar. Lo habrás visto muchísimas veces. Se trata de un triángulo de aviso amarillo que muestra una explicación si pasas el ratón por encima.

Mensaje de aviso de que falta un nodo hijo
Mensaje de aviso de que falta un nodo hijo

Cuando desarrollas escenas en Godot, estas se convierten en nodos dentro de otras. Si te tomas en serio, los principios del buen diseño, y separas responsabilidades, antes o después te encontrarás diseñando nodos que dependan de otros nodos para personalizarse. 

Al llegar a ese punto es cuando sueles preguntarte si tú también puedes emitir alertas si a alguno de tus nodos le falta un nodo complementario. La respuesta es que sí, se puede hacer y te voy a enseñar cómo.

Por ejemplo, supongamos que tenemos un nodo denominado MovingAgent. Su implementación da igual, pero para ilustrar el ejemplo vamos a suponer que ese nodo define las características de movimiento (velocidad, aceleración, frenada, etc) de un agente. Para definir cómo queremos que el agente se mueva queremos implementar nodos con los diferentes algoritmos de movimiento (por ejemplo, en línea recta, zigzag, en inversa). Esos últimos nodos tienen implementaciones dispares, pero se comprometen a cumplir el interfaz ISteeringBehavior, ofreciendo una serie de métodos comunes que pueden ser llamados desde MovingAgent. Por tanto, el agente se moverá de una manera o de otra dependiendo del nodo (cumplidor de ISteeringBehavior) que colguemos de MovingAgent.

En este caso, nos interesará alertar de esa dependencia al usuario del nodo MovingAgent en caso de que lo use sin un nodo ISteeringBehavior que cuelgue de él.

Para alertar de esa dependencia, todos los nodos bases de Godot ofrecen el método _GetConfigurationWarnings(). Si queremos que el nuestro lance avisos sólo tenemos que implementar dicho método. En el caso de MovingAgent, este podría hacer la siguiente implementación;

Implementación de _GetConfigurationWarnings()
Implementación de _GetConfigurationWarnings()

El método espera devolver un array con todos los mensajes describiendo los errores que se hayan detectado (línea 148). En caso de que Godot vea que el método devuelve un array vacío, entonces interpretará que todo está correcto y no sacará el icono de alerta.

Como verás, lo primero que hace el método es mirar si el nodo MovingAgent tiene un nodo hijo que implemente ISteeringBehavior (línea 149). En caso de que no encontrar hijo alguno con esas características (línea 154), se genera un mensaje de error (línea 156).

No hay razón para que nos limitemos a comprobar un único tipo de nodo. Podemos buscar múltiples nodos, comprobar sus configuraciones y generar múltiples mensajes de error, mientras que guardemos los mensajes de error generados en un array que luego podamos devolver como resultado del método.

En este ejemplo, guardo los mensajes de error en una lista (línea 152) que convierto en un array antes de finalizar el método (línea 159).

_GetConfigurationWarnings() se ejecuta en las siguientes situaciones:

  • Cuando se adjunte un nuevo script al nodo.
  • Cuando se abra una escena que contenga al nodo.
  • Cuando se cambie alguna propiedad en el inspector del nodo.
  • Cuando se actualice el script que cuelga del nodo.
  • Cuando se cuelga un nuevo nodo del que tiene el script.
Por tanto, puedes esperar que el script refresque el aviso, mostrando o quitando la alerta, en cualquiera de las situaciones anteriores. 

Y ya está no tiene más misterio... o quizás sí. Los lectores atentos pueden haberse dado cuenta de que en la línea 150 he buscado un nodo hijo sólo por su tipo. Los que desarrollen en Unity estarán acostumbrados a buscar componentes por tipo porque este engine ofrece un método nativo para hacerlo (GetComponent<T>()). Sin embargo, Godot no ofrece ningún método para buscar por tipo. Las implementaciones nativas de FindChild buscan nodos por nombre y no por tipo. Eso era una molestia para mí porque quería poder colgar de MovingAgent nodos con diferentes nombres, identificativos de funcionalidad, mientras cumpliesen el interfaz ISteeringBehavior. Así que, a falta de un método nativo, he implementado uno que lo hiciese a través de un extension method:

Extension method para ofrecer búsqueda de nodos hijos por tipo
Extension method para ofrecer búsqueda de nodos hijos por tipo

El extension method recorre todos los nodos hijos (línea 20) y comprueba si son del tipo que se ha pasado como parámetro (línea 22).

En caso de localizar un nodo del tipo buscado, lo devuelve y finaliza el método (línea 24). En caso contrario puede seguir haciendo búsquedas recursivas en los nodos hijos de nuestros hijos (línea 28), si así se lo hemos pedido vía parámetros (línea 26).

Gracias a este extension method, cualquier instancia de una clase de Godot que herede de Node (¿hay alguna que no lo haga?) nos ofrecerá la posibilidad de hacer búsquedas por tipos, como en la línea 150 de _GetConfigurationWarnings().

En tu caso, puede que te valga con buscar por nombre a los nodos hijos. Si no es así, puede que la solución del extension method, para buscar por tipo, te encaje mejor. Es la única complejidad que le veo a este sistema de alertas que, por lo demás, es extremadamente sencillo de usar.

Alerta resultante de nuestra implementación
Alerta resultante de nuestra implementación

11 enero 2025

Creación de Gizmos personalizados en Unity

Llevo varios artículos explicando cómo implementar Gizmos en Godot, pero me he dado cuenta de que no he explicado cómo hacer lo equivalente en Unity. Godot tiene varias ventajas sobre Unity, principalmente su ligereza y la rapidez que permite a la hora de iterar un desarrollo, pero hay que reconocer que Unity está muy maduro, y en la parte de los Gizmos se nota.

Como recordarás, los Gizmos son ayudas visuales que se implementan para representar magnitudes relacionadas con los campos y propiedades de un objeto. Se usan sobre todo en el editor, para ayudar durante la fase de desarrollo. Por ejemplo, siempre es más fácil hacerse una idea del alcance de tiro de un tanque de nuestro juego si dicho alcance se representa como un círculo en torno a él. Otro ejemplo: cuando editas la forma de un Collider, lo que se representa en pantalla es su Gizmo.

En Unity, cualquier MonoBehaviour puede implementar dos llamadas para dibujar Gizmos: OnDrawGizmos() y OnDrawGizmosSelected(). El primero dibuja los Gizmos en todo momento y el segundo sólo cuando se selecciona el GameObject que contiene el script. Es importante señalar una importante salvedad respecto a OnDrawGizmos() y es que no se llama si su script está minimizado en el inspector.

El script SeekSteeringBehavior está minimizado
El script SeekSteeringBehavior está minimizado

En teoría, se puede interactuar con los Gizmos dibujados en OnDrawGizmos(), pero no con los que se dibujan en OnDrawGizmosSelected(). En la práctica, el conocimiento sobre cómo interactuar con los Gizmos se perdió hace mucho. En el pasado se podía hacer click sobre ellos, pero parece que dejó de poder hacerse allá por la versión 2.5 de Unity. La mención que aparece en la documentación de OnDrawGizmos(), acerca de que se puede hacer click sobre los Gizmos, parece más bien un síntoma de que la documentación no está todo lo actualizada que debiera. 

En todo caso, el editor de Unity está lleno de Gizmos con los que se puede interactuar, pero eso es porque incluyen un elemento adicional que son los Handles. En este artículo nos centraremos en los Gizmos, como medio de representación visual pasiva y dejaré para un artículo posterior la explicación sobre cómo hacer Gizmos interactivos mediante Handles. Por simplificar aun más, me referiré sólo a OnDrawGizmos(); el otro método es exactamente igual, pero sólo se llama cuando su GameObject se selecciona en la jerarquía.

El método OnDrawGizmos() sólo se llama desde el editor cuando se produce una actualización o cuando el foco está puesto en la Scene View. No nos conviene sobrecargar el método con cálculos complicados porque podemos tumbar el rendimiento del editor. Aunque sólo se llame desde el editor, lo podríamos implementar tal cual, con la tranquilidad de que los Gizmos no se mostrarían en la versión compilada final del juego; sin embargo, yo prefiero meter la implementación del método entre guardas #if UNITY_EDITOR ... #endif. Manías de la edad. Cuando sólo usas Gizmos es redundante, pero sí es verdad que las necesitarás si incluyes Handles en el método, como veremos en un artículo posterior.

Supongamos, como ejemplo, que estamos diseñando un agente que se interponga entre otros dos (Agente-A y Agente-B). El algoritmo de movimiento del agente de interposición no viene al caso, pero tendrá el efecto de que medirá un vector entre los agentes A y B y situará al de interposición en el punto medio. En un caso así nos interesará dibujar en pantalla dónde está ese punto medio, para comprobar que el agente de interposición se dirige realmente a él. Es un caso ideal para los Gizmos.

El MonoBehaviour que realiza los cálculos para calcular ese punto medio, también implementa el método OnDrawGizmos() con el contenido de la captura:

Ejemplo de implementación de OnDrawGizmos()
Ejemplo de implementación de OnDrawGizmos()

Vamos a analizarla, línea a línea, para entender cómo podemos dibujar cualquier figura que queramos dentro de ese método.

Las líneas 127 y 128 evitan que el método se ejecute si hemos decidido hacer invisibles los Gizmos poniendo a falso la variable predictedPositionMarkerVisible o si _predictedPositionMarker es null. Esta última variable se refiere a la implementación del agente de interposición. Por razones que no vienen al caso, cuando arranca el MonoBehaviour, creo un GameObject que enlazo a _predictedPositionMarker. Conforme el script va calculando los puntos medios entre los agentes A y B, va situando ese GameObject en esos puntos medios. A los efectos que nos interesan en este artículo _predictedPositionMarker es un GameObject que hace de marcador del punto en el que se tiene que situar el agente que ejecuta el script. Por tanto, si fuese null no habría nada que dibujar.

En la línea 130 definimos el color con el que se dibujarán todos los Gizmos hasta que se meta un valor diferente en Gizmos.color.

En la línea 131 a 133 usamos la llamada a Gizmos.DrawLine() para dibujar una línea entre la posición del agente A y la del marcador.

En la línea 134 cambiamos el color de dibujado a magenta (morado para los amigos), para dibujar posteriormente un círculo en la posición del marcador, llamando a Gizmos.DrawSphere(). Este método dibuja un círculo relleno de color. Si sólo quisiésemos una circunferencia, podríamos usar Gizmos.DrawWireSphere().

Por último, en las líneas 137 a 139, llamamos a Gizmos.DrawLine() para dibujar otra línea (con su propio color) entre la posición del agente B y el marcador.

El resultado puede verse al ejecutar el juego desde el editor.

Gizmos dibujados al ejecutar el juego desde el editor
Gizmos dibujados al ejecutar el juego desde el editor

Los agentes A y B son los coloreados de azul y rojo, mientras que el agente de interposición es el de verde. Los gizmos son las líneas azules, roja y el círculo morado.

Y ya está. Usando estas primitivas, y el resto de las que ofrece el módulo Gizmos, podemos dibujar cualquier forma que queramos. Estas formas se irán actualizando en el editor cuando cambiemos en el inspector los campos de los que dependan o, si las ligamos a variables, conforme estas cambien al ejecutar el juego desde el editor.

Otra cosa más, yo suelo usar booleanos como predictedPositionMarkerVisible para poder decidir qué scripts concretos pueden pintar sus Gizmos; pero el editor de Unity permite desactivar el dibujado de todos los Gizmos. Para ello basta pulsar el botón que hay más a la derecha en el toolbar superior de la pestaña Scene.

Botón para activar o desactivar los Gizmos
Botón para activar o desactivar los Gizmos

Te aconsejo que te asegures de que el botón esté pulsado. Internet está lleno de posts de gentes preguntando la razón por la que no se le dibujan los Gizmos... y que luego resultó que habían desactivado sin querer este botón.

01 enero 2025

Implementación de Gizmos en Godot 2D

Recuerda que los Gizmos son ayudas visuales que se activan en el editor, asociadas a un nodo concreto, para representar visualmente las magnitudes de algunos de los campos de un nodo. Por ejemplo, si un nodo tuviese un campo Direccion, podríamos implementar un Gizmo que dibujase una flecha que naciese del nodo y apuntase en la dirección configurada en el campo. De esa manera, se facilitaría la configuración del campo porque veríamos el resultado que nos ofrecerían los distintos valores que metiésemos. Por su parte, los Handles son puntos de agarre que se dibujan junto al Gizmo que nos permiten manipularlo de manera visual y, con ello, al campo que hay por debajo. En el ejemplo que estoy planteando, un Handle sería un punto que se dibujaría en el extremo de la flecha del Gizmo. Seleccionando ese punto y arrastrando con el ratón, la flecha cambiaría de dirección y, al hacerlo, cambiaría el valor del campo Dirección del nodo al que va ligado el Gizmo.

En un artículo anterior ya hablé sobre cómo implementar Gizmos e incluso Handles en proyectos de Godot. El problema de esa implementación es que sólo valía para proyectos 3D. Godot tiene tan separadas su API 3D y 2D que lo que vale en una no sirve para la otra. Que todo girase en torno al nodo EditorNode3DGizmo debería haberme dado una pista. 

Por tanto, en un proyecto 2D de Godot no vamos a poder utilizar el método de EditorNode3DGizmo para dibujar Gizmos y Handles en pantalla. En este artículo veremos un método alternativo para incluir Gizmos en nuestro proyectos 2D. No hablaré de Handles por motivos que explicaré al final del artículo.

Lo primero es señalar que, a diferencia de Unity, Godot no ofrece nodos especializados para dibujar Gizmos en 2D. Las funcionalidades nativas de dibujado son tan potentes que basta con emplearlas cuando el nodo se ejecuta dentro del editor. La clave está en cómo hacer que el nodo se ejecute dentro del editor y que además sepa distinguir cuándo está dentro del editor y cuándo se está ejecutando como juego. 

A modo de ejemplo, vamos a implementar un Gizmo de distancia circular. Tendrá forma de nodo que podremos asociar a otros que tengan campos relativos a distancias circulares respecto a su posición. Para ver una posible implementación, lo vamos a asociar a un agente de huida, el cual define una distancia de pánico. Si otro agente se acerca por debajo de la distancia de pánico, el de huida se alejará. El propósito del Gizmo será representar visualmente dicho campo de distancia de pánico.

Uso de un Gizmo para representar el campo Panic Distance

Como se puede ver en la figura, el Gizmo dibuja una circunferencia alrededor del agente, con un radio igual al valor del campo PanicDistance. Tenemos que lograr que esa circunferencia se actualice cada vez cambiemos el valor de PanicDistance.

A nivel de la escena del nivel no se puede ver el nodo del Gizmo, porque está incluido dentro de la escena FleeSteeringBehavior (seleccionada en la figura). Si abrimos la implementación de esa última escena ya veremos el nodo de nuestro Gizmo.

El nodo de nuestro Gizmo
El nodo de nuestro Gizmo

En la figura anterior, se puede ver que nuestro Gizmo tiene la forma de un nodo personalizado denominado CircularRange. Este nodo ofrece una API muy sencilla. A nivel de inspector exporta una variable de color (RangeColor) para que podamos decidir el color de la circunferencia dibujada. A nivel de código, el nodo ofrece una propiedad pública con el radio de la circunferencia. El nodo padre que use CircularRange no tiene más que actualizar la propiedad del radio, para que CircularRange redibuje la circunferencia con el radio actualizado.

Lo que pasa es que no encontrarás CircularRange entre los nodos disponibles en Godot. Tendremos que implementarlo y registrarlo en Godot para que este lo muestre en su lista. Todo eso se puede hacer a través de un plugin.

Creación de un plugin

Esta parte es muy similar a la del artículo sobre los Gizmos y los Handles 3D. Hay que utilizar el wizard de Godot para crear la estructura de nuestro plugin. El wizard está en Project --> Project Settings... --> Plugins --> Create New Plugin. Te saldrá una ventana como la siguiente:


Para que te hagas una idea, algunos de los datos que configuré para el mío fueron los siguientes:

Configuración del plugin
Configuración del plugin


En el apartado de "Subfolder" puse res://addons/InteractiveRanges para que me crease esa carpeta y construyese el esqueleto del plugin allí. Dejé la casilla de "Activate now?" en blanco, porque en C# tienes que implementar el código de tu nodo y compilarlo, antes de activar el plugin que lo usa.

Una vez que creas el plugin se generará la carpeta que dijiste y se poblará con un archivo plugin.cfg con los datos que has metido a través del wizard.

 

Contenido de plugin.cfg
Contenido de plugin.cfg

En mi caso, planeo incluir varios tipos de Gizmos en el plugin, así que he creado una carpeta específica para el Gizmo del ejemplo: res//addons/InteractiveRanges/CircularRanges. En esa carpeta he metido todos los recursos necesarios para el nodos a implementar.

Contenido final de la carpeta del plugin
Contenido final de la carpeta del plugin


Implementación del nodo del Gizmo

Para implementar el nodo, sólo necesitamos un script con su código y un icono que lo represente al buscar en la lista de nodos de godot.

Para el icono, yo he buscado uno en formato SVG, por aquello de facilitar su escalado. Cuanto más sencillo y conceptual mejor. En mi caso es un mero círculo, con una línea que va desde su centro a su perímetro. Por coherencia con el resto de iconos de Godot, le he dado el color morado típico de los nodos 2D de Godot.

Para el código del Gizmo, en mi caso me ha bastado con heredar de Node2D.

Primera parte de la implementación del nodo en CircularRange.cs
Primera parte de la implementación del nodo en CircularRange.cs

Observa que he decorado a la clase con el atributo [Tool]. Esto es imprescindible para que su implementación se ejecute dentro del editor. De otra manera, el nodo estaría inerte hasta la ejecución del juego, pero este nodo no tiene utilidad durante el juego sino para ayudarnos a nosotros en el editor.

Tal y como describí más arriba, el nodo exporta una propiedad TangeColor para que podamos configurar el color del Gizmo desde el inspector.

También hay otra propiedad, Radius, que no se exporta al inspector pero que es pública para dejar que los nodos padres que utilicen el Gizmo puedan actualizar el radio representado. Fíjate en la línea 27, cada vez que se actualice el valor de Radius se forzará un redibujado del Gizmo mediante QueueRedraw().

QueueRedraw() encola una petición para que se llame lo antes posible al método de dibujado del método. Este método es _Draw() y debemos implementarlo con el aspecto que le queramos dar al Gizmo.

Implementación del dibujado del Gizmo en CircularRange.cs

Como puedes ver, no hay que complicarse mucho para dibujar una circunferencia. Una simple llamada, en la línea 35, a DrawCircle con la posición del centro del círculo (relativa al nodo padre), el radio y el color. El parámetro "filled" sirve para definir si queremos un círculo relleno de color (true) o una mera circunferencia (false), vacía por dentro y coloreada en su perímetro.

Fíjate en la línea 33. Es importante. La llamada a Engine.IsEditorHint() devuelve verdadero si el nodo se está ejecutando dentro del editor y falso si se está ejecutando con el juego. Dado que sólo queremos que el Gizmo se dibuje en el editor, abandonaremos el método _Draw() si Engine.IsEditorHint() devuelve falso.

Es muy importante utilizar Engine.IsEditorHint() en todos los métodos de un nodo [Tool]. Habrá nodos de este tipo que sólo queremos que se ejecuten en el editor, por lo que todos los métodos tendrían que empezar con una guarda como la anterior. También habrá casos en los que haya nodos mixtos, que queramos que tengan una funcionalidad en el editor y otra durante el juego, por lo que cada método deberá incorporar una guarda (o varias) que limiten la ejecución del código, en función de lo que devuelva Engine.IsEditorHint(). Este ejemplo es sencillo, pero no siempre será así. Incorporar las guardas de Engine.IsEditorHint() puede complicar tu código y puedes sufrir errores porque se ejecute durante el juego código que estaba pensado para el editor. Por eso, la recomendación es que decores con el atributo [Tool] sólo los nodos imprescindibles. De hecho, si tienes nodos mixtos (con funcionalidad para el editor y el juego) mi recomendación es que intentes separar su funcionalidad en dos nodos: uno con la funcionalidad para el juego y otro con la del editor.

Registro del nodo del Gizmo dentro de Godot

Volvemos a una parte común al artículo de los Gizmos 3D: el registro del nodo de nuestro Gizmo, para que Godot lo muestre en la lista de nodos.

El script que se encarga del registro es el que configuraste en el wizard de creación del plugin. En mi caso InteractiveRanges.cs. En mi caso, lo he situado en la carpeta raíz del plugin.

Registro del nodo del Gizmo en InteractiveRanges.cs
Registro del nodo del Gizmo en InteractiveRanges.cs

Como recordarás, el registro del nodo es muy sencillo. Basta una llamada al método AddCustomType() (línea 30), al que le pasamos el nombre del nodo, el nodo base del que hereda, la ubicación de su script y el icono que lo representará.

En mi caso, como planeo crear más Gizmos, he concentrado todo el proceso en el método RegisterCustomNode() (línea 22) al que llamaré cada vez que quiera registrar un nuevo Gizmo. Esa llamada se produce desde el método _EnterTree() (línea 8) que es el que se ejecuta cada vez que se activa el plugin.

Una vez finalizado este último script, ya tenemos todo lo que necesita nuestro plugin para funcionar. Es el momento de compilar y acudir a Project --> Project Settings... --> Plugins para activar el plugin.

Cuando lo hayamos hecho podremos elegir nuestro nodo de la lista de Godot.

Uso de nuestro Gizmo personalizado

Cuando hayamos incluido el nodo del Gizmo dentro de la jerarquía de la escena, tendremos que integrarlo con el resto del código.

Estructura de la escena del ejemplo
Estructura de la escena del ejemplo

En nuestro ejemplo, el nodo que usará al Gizmo pasándole sus datos es el nodo FleeSteeringBehavior.

FleeSteeringBehavior localiza a sus nodos hijos, y obtiene referencias a ellos, en su método _Ready().

El nodo FleeSteeringBehavior consigue referencias a sus hijos en el método _Ready()
El nodo FleeSteeringBehavior consigue referencias a sus hijos en el método _Ready()

Que no te despiste la llamada a FindChild(). Se trata de un extension method que me he implementado para poder buscar a los hijos por tipo. Lo he tenido que implementar yo porque el FindChild() que viene por defecto con Godot sólo busca nodos por nombre. No es que hacer las búsquedas por nombre hubiera sido un drama, pero en este caso he preferido hacerlo por tipo.

Implementación del extension method para buscar nodos hijo por tipo
Implementación del extension method para buscar nodos hijo por tipo

Una vez obtenida la referencia al Gizmo, FleeSteeringBehavior debe hacer uso de ella cada vez que se cambie el valor del campo a representar con el Gizmo.

Actualización del Gizmo desde FleeSteeringBehavior
Actualización del Gizmo desde FleeSteeringBehavior

Este tipo de cosas es la razón de ser de las propiedades. En este caso, cada vez que se actualice el valor de la propiedad PanicDistance de FleeSteeringBehavior, este actualizará la propiedad Radius del Gizmo (línea 50). Recuerda que Radius es a su vez una propiedad que fuerza al redibujado del Gizmo cada vez que alguien cambia su valor. De esta manera, el Gizmo se actualizará cada vez que se produzca un cambio en PanincDistance.

Es importante destacar que FleeSteeringBehavior debe marcarse también como [Tool] para que la lógica ligada a la propiedad PanicDistance también se ejecute en el editor.

Conclusión

Los Gizmos son extremadamente útiles a la hora de dar forma a tu juego. Te permiten contar con orientaciones del efecto de los valores de los campos de los nodos, sin tener que ejecutar el juego, lo que acelera el desarrollo.

Unity cuenta con una implementación muy madura para los Gizmos y los Handles. De hecho, las APIs de unos y de otros son redundantes en algunos apartados. Espero poder hacer un artículo al respecto pronto.

El caso de Godot no está tan maduro, parece haber cierto esfuerzo en llegar al punto en el que está Unity, pero aún se notan las carencias. Los Gizmos y Handles como tal sólo parecen existir en el mundo 3D, tienen limitaciones (sólo se pueden dibujar rectas) y aun así están muy pobremente documentados, como ya mencioné en mi artículo al respecto. El caso de los Gizmos 2D es más llamativo aún ya que no parece haber una API especializada, sino que se usan las primitivas de dibujo del engine. En sí mismo, eso no es malo, pero llama la atención que no se haya hecho un esfuerzo para generar clases auxiliares que hiciera similar la gestión de Gizmos en 3D y 2D. 

Pero lo peor viene en el caso de los Handles en Godot 2D. Tampoco existen como tales, pero es que su implementación manual es compleja o incluso imposible. En mi caso, conseguí una implementación parecida a un Handle haciendo que InteractiveRanges.cs interceptase las pulsaciones de ratón, implementando los métodos _ForwardCanvasGuiInput() y Handles() de la clase madre EditorPlugin, y calculando que las posiciones se produjesen sobre un círculo dibujado sobre el Gizmo, a modo de Handle. Sin embargo, esa implementación sólo funcionaba cuando se seleccionaba el nodo del Gizmo en la jerarquía, lo que la hacía inútil ya que lo normal es que el Gizmo estuviese oculto, como nodo auxiliar dentro de otras escenas, por lo que al seleccionar estas lo que se pulsaría serían los nodos raíces de esas escenas y no la del Gizmo, por lo que este no llegaría a dibujarse. Le di muchas vueltas, busqué mil veces en internet, hasta que finalmente me di por vencido. Mi impresión es que no deben usarse muchos handles en el desarrollo con Godot.. Quizás me equivoco, pero poca gente pregunta por el tema en Internet y, desde luego, nadie plantea ni una solución ni una alternativa a la problemática que me he encontrado.

A ver si encuentro un hueco para explicar cómo se conseguiría una funcionalidad muy similar en Unity, para que así podáis contrastar el soporte que ofrecen ambos engines.