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.