25 junio 2024

Cómo implementar Gizmos y Handles en Godot

Los Gizmos te permiten dibujar líneas y formas en la ventana del editor para representar visualmente valores de un objeto.

Por ejemplo, imagina que quieres crear un componente que implemente un cono de visión:


Un cono de visión se modela con dos parámetros:

  • Alcance: Es la distancia entre el observador y el objeto observado a partir de la cual este último deja de ser visible.
  • Apertura: Es el ángulo del cono. Generalmente se calcula como un ángulo desde el vector que marca la dirección de mirada del observador. Si el objeto se encuentra en un ángulo respecto al observador mayor que el ángulo de apertura, entonces el objeto no es visible.
Por tanto tu componente puede tener esos dos parámetros, en forma de campos, y los puedes editar desde el inspector de dicho componente. El problema es que resulta muy complicado fijar los valores idóneos de esos parámetros sin tener una referencia visual del resultado. Es más fácil comprender qué queda dentro del cono de visión si, al fijar la apertura en el inspector, en el editor de la escena aparece un triángulo representando el cono de visión con ese ángulo.

Para representar esas formas geométricas, que nos ayuden a representar los valores de los campos de nuestros componentes, es para lo que sirven los Gizmos. 

Por supuesto, los Gizmos sólo aparecerán en el editor. Son ayudas para el desarrollador y para el diseñador de niveles, pero serán invisibles al iniciar el juego final. 

En realidad, si has estado practicando con Godot, ya habrás usado Gizmos, por ejemplo al fijar la forma de un CollisionShape. Las cajas, círculos y demás formas geométricas que aparecen en el editor cuando quieres configurar un CollisionShape son precisamente Gizmos, dibujados tal y como vamos a ver aquí.

Precisamente, si te fijas en los CollisionShape, verás que, además del Gizmo, aparecen unos puntos sobre los que pulsar y arrastrar para cambiar la forma del componente. Esos puntos se denominan Handles y son las "agarraderas" para manipular lo que representemos en el editor. En este artículo también veremos cómo implementar nuestros propios Handles.

Visión lateral de un CollisionShape con forma de caja. Los bordes azules son el Gizmo, representando la forma, y los puntos rojos son los Handles, para cambiar la forma.


Los Gizmos se asocian a los nodos que complementan, de tal manera que Godot sepa qué Gizmos activar en caso de que se incluya un nodo determinado en la escena. Como hemos visto, muchos de los nodos de Godot ya tienen Gizmos asociados (como hemos visto con los nodos CollisionShape).

Creación de un nodo personalizado


Como no quiero enredar con los Gizmos por defecto de los nodos de Godot, vamos a empezar añadiendo un nodo personalizado a la lista de nodos de Godot. A ese nodo personalizado le asociaremos un Gizmo, con sus respectivos Handles, que nos sirva como ejemplo.

En nuestro ejemplo, para crear ese nodo personalizado he creado la carpeta nodes/CustomNode3D, dentro de la del proyecto. En esa carpeta podemos crear el script de nuestro nodo personalizado pulsando en la carpeta con el botón derecho del ratón y eligiendo Create New > Script.... Saldrá una ventana emergente como la siguiente, en la que he completado los valores para este ejemplo:



Una vez generado el script, sólo necesitaremos que implemente 2 propiedades públicas Vector3 exportadas. Yo las he llamado NodeMainPoint y NodeSecondaryPoint:

[Export] public Vector3 NodeMainPoint { get; set; }
[Export] public Vector3 NodeSecondaryPoint { get; set; }

No pongo captura, porque más adelante incluiremos código en la parte de set.

La idea es que al arrastrar los Handles en el editor se actualice el valor de las dos propiedades anteriores. A la inversa también debe funcionar: si cambiamos el valor de las propiedades en el inspector, los Handles deberán resituarse en las posiciones señaladas por las propiedades. Por otro lado, también dibujaremos un Gizmo en forma de línea, desde el origen del nodo hasta las posiciones de las propiedades.

Con eso debería bastar para ilustrar las mecánicas principales: representar con Gizmos propiedades de un nodo y cambiar dichas propiedades mediante Handles.

Lo siguiente será crear una carpeta addons dentro del proyecto de Godot. Los Gizmos y los Handles personalizados se consideran plugins por lo que el consenso es depositarlos en una carpeta addons dentro del proyecto.

Hecho eso, iremos a Project > Project Settings ... >  Plugins y pulsaremos el botón Create New Plugin. Nos saldrá una ventana como la de la siguiente figura, en la que yo ya he rellenado los valores para el ejemplo:



Hay que tener en cuenta que la carpeta que fijemos en el campo Subfolder, de la ventana anterior, se creará dentro de la carpeta addons que mencionábamos antes. Lo mismo pasará con el script del plugin, definido en el campo Script Name

Fíjate también en que he desactivado el check de Activate now?. Los plugins en GDScript se pueden activar inmediatamente, con el código de la plantilla que se genera, pero los plugin en C# requieren de cierta configuración previa, por lo que darán error si se intentan activar con el código de la plantilla por defecto. Tampoco es que el error resultante rompa nada, pero sale una ventana de error que hay que cerrar y queda feo. Así que lo mejor es desactivar ese check y dejar la activación para un paso posterior, como veremos a continuación.

Hecho lo anterior, se habrá generado una carpeta, dentro de addons, con un archivo plugin.cfg y el script de C# de la ventana anterior. El propósito de ese script será registrar en Godot el tipo representado por CustomNode3D para que podamos elegirlo en la lista de nodos del engine. Recuerda que CustomNode3D hereda de Node3D, así que tiene sentido incluirlo junto al resto de nodos.

Como en cualquier otro plugin, CustomNode3DRegister tendrá que heredar la Clase EditorPlugin e implementar los métodos _EnterTree() y _ExitTree(). En el primer método registraremos CustomNode3D como un nodo elegible en la lista de nodos, y en el segundo método lo daremos de baja para que deje de aparecer en dicha lista. La implementación es sencilla:

addons/custom_node3D_register/CustomNode3DRegister.cs


Como se puede ver, en el método _EnterTree() cargamos dos cosas: el script asociado al nodo personalizado y el icono con el que queremos representar el nodo en la lista de Godot. Para el icono he usado el que se incluye en todos los proyectos de Godot, copiándolo desde la raíz en la carpeta del nodo personalizado.

Luego, asociamos esos elementos a un nodo base, mediante el método AddCustomType() que es el que inscribe el nodo personalizado en la lista de nodos. Dado que el script del nodo personalizado hereda de Node3D es el que hemos usado como clase base en la llamada a AddCustomType(). Con esta llamada, cuando elijamos CustomNode3D en la lista de nodos, se creará un Node3D en la escena y se le asociará el script que hayamos definido.

La implementación de _ExitTree() es justo la contraria: usamos el método RemoveCustomType() para borrar al nodo personalizado de la lista de nodos.

Para hacer que se ejecute el registro, compilaremos el juego para que se hagan efectivos los cambios sobre CustomNode3DRegister.cs. Hecho eso, iremos a Project > Project Settings ... >  Plugins y nos aseguraremos de marcar el check de Enable del plugin CustomNode3DRegister. Eso hará que se ejecute su lógica y se inscriba a nuestro nodo personalizado en la lista de nodos. A partir de ahí, podremos localizar a nuestro nodo en la lista y añadirlo a la escena:



Añádelo a la escena antes de avanzar.

Creación de un Gizmo para el nodo personalizado


Ahora que ya tenemos nuestro nodo personalizado, vamos a crear un Gizmo que represente visualmente sus propiedades.

Los Gizmos se consideran addons, así que lo lógico será crear una carpeta addons/CustomNode3DGizmo para alojar sus ficheros. Esos ficheros serán dos: un script para definir el Gizmo, que será una clase que herede de EditorNode3DGizmoPlugin, y otro script para registrar el Gizmo, que heredará de EditorPlugin y se parecerá bastante al que hemos usado para registrar al nodo personalizado.

El script del Gizmo es el que tiene toda la enjundia. Yo lo he llamado CustomNode3DGizmo.cs. Como ya he dicho, debe heredar de EditorNode3DGizmoPlugin e implementar algunos de sus métodos.

El primero de esos métodos es _GetGizmoName(). Este método se limita a devolver una cadena con el nombre del Gizmo:

addons/custom_node3D_gizmo/CustomNode3DGizmo.cs

Algo más intrigante es el método _HasGizmo(), al que se le pasarán todos los nodos de la escena hasta que el método devuelva true para alguno de ellos, señalando que el Gizmo debe aplicarse a ese nodo. Por tanto, en nuestro caso, el método deberá devolver true cuando se le pase un nodo de tipo CustomNode3D:

addons/custom_node3D_gizmo/CustomNode3DGizmo.cs

Aquí hay que tener en cuenta cierto problema que se da en C# y no en GDScript. Aunque la comparación con "is" es sintácticamente correcta, en la práctica no funciona en Godot C# a no ser que la clase con la que comparemos esté marcada con el atributo [Tool]. Así que este es un buen momento para añadirle ese atributo a la cabecera de la clase CustomNode3D:

nodes/CustomNode3D/CustomNode3D.cs

En realidad, esto es una anomalía. No deberíamos necesitar el atributo [Tool] para hacer funcionar esa comparación. De hecho, el código equivalente de GDScript (el que aparece en la documentación oficial) no lo requiere. Se trata de un bug reportado varias veces en los foros de Godot y pendiente de solución. Hasta que lo resuelvan, el workaround en C# es usar el atributo [Tool].

El siguiente método a implementar es el constructor de la clase de nuestro Gizmo. En GDScript usaríamos el método _init(), pero en C# usaremos el constructor de la clase:


addons/custom_node3D_gizmo/CustomNode3DGizmo.cs


En ese constructor crearemos los materiales a aplicar a nuestro Gizmo y a sus Handles. No se trata de un material completo, como el que haríamos con un shader, sino más bien un conjunto de estilos a aplicar a las líneas que dibujemos para nuestro Gizmo. Se crean usando los métodos CreateMaterial(), para el Gizmo, y CreateHandleMaterial(), para el Handle. Ambos aceptan, como primer parámetro, una cadena con el nombre que queramos darle al material. Ese nombre se utiliza con el método GetMaterial() para obtener una referencia a dicho material. Esa referencia puede ser útil, por ejemplo, para asignársela a una variable de tipo StandardMaterial3D con la que personalizar a fondo el material, fijando los valores de sus respectivas propiedades. Sin embargo, lo normal es que no tengas que recurrir a ese grado de personalización y te baste con fijar el color de las líneas usando el segundo parámetro del método CreateMaterial(). Sin embargo, el método CreateHandleMaterial() no acepta ese segundo parámetro, por lo que no queda más remedio que recurrir, como decía antes, al método GetMaterial() (líneas 19 y 20 de la captura anterior) para obtener las referencias al material, con las que fijar el valor de su propiedad AlbedoColor (líneas 21 y 22).

En el constructor del ejemplo, he configurado que las líneas que se tracen entre el origen de coordenadas y la posición marcada por la propiedad NodeMainPoint utilicen el color rojo. Las líneas que vayan hasta la posición de la propiedad NodeSecondaryPoint usarán el color verde. He configurado los materiales de los respectivos Handles para que usen el mismo color.

Por último, queda el método _Redraw(). Es el que se encarga de dibujar los Gizmos cada vez que se llame al método UpdateGizmo() con el que cuentan todos los Node3D.

addons/custom_node3D_gizmo/CustomNode3DGizmo.cs


El método _Redraw() es como nuestra pizarra, y lo normal con una pizarra es borrarla al principio para pintar sobre ella. Esa es la razón por la que se suele llamar al método Clear() al principio del método (línea 29 de la captura anterior).

Luego recopilamos en un array de Vector3 las posiciones de las líneas que queremos dibujar. En este caso queremos trazar una línea entre el origen de coordenadas y la posición marcada por la propiedad NodeMainPoint, así que guardamos ambos puntos en el array (líneas 33 a 37 de la captura anterior). 

Con los Handles hacemos lo mismo, guardando en otro array los puntos en los que queramos que aparezca un Handle. En este caso, como queremos que aparezca un Handle en el extremo de la línea, el marcado por la posición de NodeMainPoint, sólo metemos esa posición en el array de Handles (líneas 38 a 41 de la captura anterior).

Finalmente, usamos el método AddLines() para dibujar las líneas a lo largo de las posiciones recogidas en el array (línea 42); y, por otro lado, el método AddHandles() para posicionar Handles en las posiciones recogidas en su array (línea 43). Fíjate que, en ambos casos, pasamos el material que define el estilo con el que queremos que se dibujen los elementos.

No lo he incluido en la captura anterior, pero la mecánica para dibujar la línea y el Handle de un segundo punto (en este caso NodeSecondaryPoint) sería la misma: confirmaríamos sus arrays de posiciones y los pasaríamos a los métodos AddLines() y AddHandles().

Manipulación de un Gizmo usando Handles


En este punto, nuestro Gizmo dibujará líneas y Handles dependiendo de los valores recogidos en las propiedades del nodo al que se asocia (en este ejemplo, CustomNode3D). Sin embargo, si pulsamos en los Handles, no pasará nada. Estos permanecerán inmóviles.

Para interactuar con los Handles, hay que implementar unos cuantos métodos más de EditorNode3DGizmoPlugin en nuestra clase CustomNode3DGizmo. Sin embargo, la documentación oficial de Godot no cubre dichas implementaciones. Si sigues el tutorial de la documentación oficial, te quedarás en la sección anterior de este artículo. Es extrañísimo, pero no hay nada en la documentación oficial que explique cómo manipular los Handles. Todo lo que sigue a partir de aquí lo he deducido probando e interpretando los comentarios de cada función a implementar. Quizás, a partir de este artículo, haga una aportación a la documentación de Godot para resolver esta carencia.

Veamos qué métodos hay que implementar en CustomNode3DGizmo para poder manipular los Handles que ubicamos en el método _Redraw().

El primero de todos es _GetHandleName(). Este método debe devolver una cadena con el nombre identificativo del Handle. Lo normal es devolver el nombre de la propiedad que se ve modificada por el Handle:

addons/custom_node3D_gizmo/CustomNode3DGizmo.cs


En la captura anterior hay que destacar dos cosas. 

La primera es que podríamos haber devuelto el nombre de la propiedad como una cadena puesta a mano, pero usando el método nameof() nos aseguraremos de que si refactorizamos el nombre de la propiedad, usando nuestro IDE, esta parte del código se actualizará también. 

La segunda cosa a destacar es que cada Handle está identificado con un entero, de tal manera que podemos saber para qué Handle se está pidiendo el nombre en función del parámetro handleId que se le pase al método _GetHandleName(). El entero de cada Handle depende del orden en el que metiésemos los Handles al llamar al método AddHandles() en el método _Redraw(). Por defecto, si dejas vacío el parámetro ids de AddHandles(), el primer Handle que le pases se le asignará el id o, al segundo el 1 y así. Sin embargo, si te fijas en la captura que puse más atrás de _Redraw(), yo no dejé vacío el parámetro ids. En vez de eso, le pasé un array con un único elemento. Ese elemento era un entero, definido como una constante, para forzar que a ese Handle se le asignase ese entero como id, y poder usar esa constante como identificador a lo largo del código.

addons/custom_node3D_gizmo/CustomNode3DGizmo.cs

Una vez que se implementa cómo identificar cada Handle, el siguiente paso es definir qué valor devuelve el Handle cuando pinchamos en él. Eso se hace implementando el método _GetHandleValue().

addons/custom_node3D_gizmo/CustomNode3DGizmo.cs


Al igual que con _GetHandleName(), a _GetHandleValue() también se le pasa el identificador del Handle para el que se está pidiendo el valor. Con el parámetro gizmo podemos obtener el nodo asociado al Gizmo, usando el método GetNode3D() (línea 130 de la captura anterior). Y una vez que tenemos una referencia al nodo podemos devolver el valor de la propiedad asociada a cada Handle (líneas 133 a 136).

Cuando pulses en un Handle, fíjate en la esquina inferior izquierda de la vista de escena del editor, aparecerá una cadena conformada por lo que devuelva _GetHandleName() y lo que dé _GetHandleValue() para ese Handle.

Ahora viene lo que puede ser la parte más difícil de este tutorial: usar el Handle para asignarle un valor a la propiedad asociada del nodo. Eso se hace implementando el método _SetHandle():

addons/custom_node3D_gizmo/CustomNode3DGizmo.cs

A ese método se le pasa un parámetro gizmo, con el que podemos tener acceso al nodo asociado, con el método GetNode3D(), tal y como hacíamos con _GetHandleValue(). También se le pasa el identificador del Handle para el que se está fijando el valor. Pero lo más importante es que se le pasa la cámara que está visualizando la escena y la posición en pantalla del Handle.

En este método tendremos que interpretar la posición en pantalla del Handle, para fijar, en función de esa posición, el valor de la propiedad del nodo asociada al Handle. En este caso parece fácil: la posición del Handle debe ser el valor que se guarde en la propiedad asociada, dado que tanto NodeMainPoint como NodeSecondaryPoint son posiciones. El problema es que os Handles se arrastran sobre la superficie bidimensional de la pantalla, esa es la razón por la que el parámetro screenPos es un Vector2, por lo que no es inmediato saber a qué coordenada tridimensional del escenario corresponde ese punto en la pantalla.

Cuando añadimos un nodo Camera3D a una escena, dicho nodo se representa con el siguiente Guizmo:


A mí me resulta muy esclarecedor pensar que nuestra cabeza esta en la punta de la pirámide del Gizmo y que miramos a una pantalla que se encuentra en la base. Sobre dicha pantalla se retroproyecta la escena que se encuentra delante de la cámara.

Supongamos que tenemos un objeto A en el escenario. La posición de la pantalla en la que se pinta A (vamos a llamar a esa posición Ap) es el resultado de trazar una línea recta entre el objeto y el foco de la cámara y ver en qué punto corta con el plano de retroproyección de la cámara:


  
Hasta ahí, todo es muy fácil. De hecho, la clase Camera3D tiene el método UnprojectPosition() al que se le pasa la posición tridimensional (Vector3) de un objeto de la escena y devuelve la posición bidimensional (Vector2) de ese objeto en la pantalla. En nuestro caso, si le pasásemos a UnprojectPosition() la posición de A, el método nos devolvería Ap (entendida esta como una posición bidimensional de la carpeta).

Ahora supongamos que en la posición Ap de la pantalla tuviéramos un Handle que representase la posición de A, y que arrastrásemos el Handle hasta la posición Bp de la pantalla, ¿Cómo calcularíamos la nueva posición del objeto en el espacio tridimensional? (vamos a llamar a esa nueva posición B). Lo lógico será aplicar el proceso inverso al que hicimos para retroproyectar el objeto sobre el plano de la cámara. Para ello trazaríamos una línea entre el foco de la cámara y Bp. Siguiendo ese razonamiento, la nueva posición del objeto estará a lo largo de esa línea, ¿pero dónde?, ¿en qué punto de la línea?

La clave está en darse cuenta de que el objeto se desplazará en un plano (Pm) paralelo al de la cámara. La intersección entre ese plano y la línea que parte del foco de la cámara, atravesando Bp, será la nueva posición del objeto (B):



El nodo Camera3D cuenta con un método ProjectPosition(). Sirve precisamente para convertir coordenadas bidimensionales de pantalla en coordenadas tridimensionales de escena. El método acepta dos parámetros. El primero es una posición bidimensional de la pantalla (en nuestro ejemplo Bp). Con ese parámetro, el método trazará la línea que parte del foco de la cámara y atraviesa la coodenada dimensional de la cámara (Bp). El segundo parámetro del método se denomina zDepth y es un float que señala la distancia desde el foco de la cámara a la que se situará el plano Pm con el que debe cortar la línea.  




Esa distancia será la longitud de la línea que parte del foco de la cámara e incida perpendicularmente sobre el plano Pm. En el diagrama anterior será la distancia entre el foco (F) y el punto D. 

¿Pero cómo podemos calcular esa distancia? Usando trigonometría. Si recordamos nuestra enseñanza secundaria, el coseno del ángulo entre el segmento FA y FD equivale a la relación de FD dividido entre FA. Así que FA multiplicado por el coseno nos dará FD.

Resulta que el cálculo anterior es tan habitual que la biblioteca de operaciones vectoriales de los engines de desarrollo lo incluyen con el nombre de Dot Product. Con ese operador podemos transformar la fórmula anterior en la siguiente:

La fórmula anterior quiere decir que si hacemos el Dot Product del vector FA sobre el vector normalizado de FD nos dará el vector completo FD. 

Es habitual visualizar el Dot Product como una proyección de un vector sobre otro. Si colocases una luz muy potente por detrás del vector FA, enfocándola de manera perpendicular al vector normalizado FDn, la sombra que proyectaría FA sobre FDn sería precisamente FD.

Por lo tanto, para conseguir la distancia FD, que nos sirva como parámetro zDepth sólo necesitamos hacer el Dot Product de FA sobre el valor normalizado de FD, que es precisamente el vector Forward del nodo Camera3D (por defecto, el valor inverso de su eje local Z). 

Todo este razonamiento se reduce a unas pocas líneas, en el método GetZDepth():

addons/custom_node3D_gizmo/CustomNode3DGizmo.cs


En dicho método, la variable vectorToPosition corresponde a FA y cameraForwardVector a FDn. El resultado devuelto por el método zDepth es FD y es lo que se utiliza en las llamadas a ProjectPosition(), en _SetHandle() para fijar las nuevas posiciones de NodeMainPoint y NodeSecondaryPoint.

Resuelto _SetHandle(), el único método que quedaría por implementar es _CommitHandle(). Este método es el responsable de ir construyendo el historial de modificaciones que vamos realizando sobre nuestros Handles, de manera que podamos movernos por dicho historial cuando hagamos undo/redo (Ctrl+Z o Ctrl+Shift+Z).


addons/custom_node3D_gizmo/CustomNode3DGizmo.cs


El historial se construye sobre un objeto de tipo EditorUndoRedoManager (en este caso _undoRedo), que se obtienes desde el objeto EditorPlugin que registra el Gizmo (en este ejemplo CustomNode3DGizmoRegister, del que hablaremos ahora) y le pasa por su constructor una instancia del EditorUndoRedoManager.

Con el EditorUndoRedoManager, cada entrada en el historial se crea con el método CreateAction() (línea 78 de la captura anterior). En cada entrada debe haber acciones que se ejecuten en caso de que el usuario haga Undo y de Do (llamadas cuando se haga Redo). Esas acciones pueden implicar fijar una propiedad, con los métodos AddDoProperty() y AddUndoProperty(), o ejecutar un método, con AddDoMethod() y AddUndoMethod(). Si la acción directa sólo ha implicado cambiar el valor de una propiedad, lo normal es que para deshacer esa acción sólo tengas que poner la propiedad a su valor anterior. Pero si la acción directa disparó algún tipo de método, además del cambio de método de la propiedad, entonces lo más probable es que necesite lanzar otro método para deshacer lo hecho por el primero. 

En el caso de este ejemplo, me limito a cambiar el valor de las propiedades de customNode3D, así que para el historial de acciones basta con usar los métodos Add...Property(). Estos métodos piden como parámetro la instancia dueña de las propiedades a modificar, la cadena con el nombre de la propiedad a manipular y el valor al que fijar dicha propiedad. Cada acción captura el valor que le pasemos al método Add...Property(). En el caso de AddDoProperty() le pasamos el valor que tiene en ese momento la propiedad (líneas 84 y 91); mientras que en el caso de AddUndoProperty() le pasamos el valor del parámetro restore, que contiene el valor rescatado del historial cuando hacemos Undo.

Cuando _CommitHandle() se llama con el parámetro cancel a verdadero, es equivalente a un Undo sobre el Handle, así que restauramos el valor de restore sobre la propiedad (líneas 101 a 106).

Por último, pero no menos importante, una vez que hayamos dado forma a los cambios de propiedad y llamadas de método que conforman la entrada en el historial, registraremos dicha entrada con CommitAction() (línea 109).

Actualización del Gizmo tras los cambios


La representación visual de un Gizmo puede tener que actualizarse por dos razones:
  1. Porque hayamos cambiado los campos del nodo representado desde el inspector.
  2. Porque hayamos manipulado los Handle del Gizmo.
La actualización del Gizmo se hace llamando al método UpdateGizmos() que tienen todos los Node3D. 

La cuestión es desde dónde llamar a ese método para asegurar que se actualizan los dos tipos de cambios anteriores. En las capturas de código anteriores verás que hay varias llamadas a UpdateGizmos() comentadas. Son pruebas de posibles sitios desde donde ejecutar ese método. Todas las llamadas comentadas tenían algún problema: o no se llamaban en alguno de los dos casos anteriores o se actualizaban a trompicones, como si hubiera algún problema de rendimiento. 

Al final, mis pruebas me han hecho llegar a la conclusión de que, en mi caso, el mejor sitio desde donde llamar a UpdateGizmos() es desde las mismas propiedades de CustomNode3D que estamos modificando. Por ejemplo, en el caso de NodeMainPoint:

nodes/CustomNode3D/CustomNode3D.cs

Al llamar a UpdateGizmos() desde el set de la propiedad, estando esta exportada, nos aseguramos que el método se llame tanto cuando se modifique la propiedad desde el inspector como desde el _SetHandle() del Gizmo.

Registro del Gizmo

Igual que pasó con el nodo personalizado, nuestro Gizmo también debe registrarse en el editor, para que este sepa que tiene que contar con él. Para ello volveremos a usar un plugin que haga la labor de registro.

addons/custom_node3D_gizmo/CustomNode3DGizmoRegister.cs

Crearemos el plugin con el mismo método con el que creamos el plugin CustomNode3DRegister, sólo que esta vez, el plugin se llamará CustomNode3DRegister y se basará en un script C# del mismo nombre.

En este caso cargamos el script de C# en el que hemos configurado el Gizmo y lo instanciamos, pasándole a su constructor una instancia de EditorUndoRedoManager, llamando al método GetUndoRedo() (líneas 13 y 14).

Hecho eso, registramos la instancia del plugin, entregándoselo al método AddNode3DGizmoPlugin() (línea 15).

También de manera similar a como hicimos en el registro del nodo personalizado, en este caso también aprovechamos el método _ExitTree() para dar de baja al Gizmo mediante el método RemoveNode3DGizmoPlugin() (línea 21). 

Finalizado el script, podremos activar el plugin desde Project > Project Settings... > Plugins, y la próxima vez que añadamos un CustomNode3D a la escena podremos usar los Handles del Gizmo. 

Puede que al principio no se distingan bien los Handles, porque ambos coincidirán en el origen de coordenadas:



Sin embargo, están ahí. Son los puntos que se adivinan en el origen del eje de coordenadas. Si pulsamos en dichos puntitos y arrastramos veremos como estos se mueven alterando el valor de las propiedades de CustomNode3D.



Conclusiones

Este artículo ha sido extremadamente largo, pero he querido solventar con él una carencia desoladora, por parte de la documentación oficial, sobre este tema.

Mi experiencia anterior había sido en Unity, que también tiene su propia API de Gizmos y de Handles. En comparación con Unity, el enfoque de Godot me ha parecido más compacto y sencillo, al concentrar tanto la configuración de los Gizmos, como de los Handles en una misma clase heredera de EditorNode3dGizmoPlugin. En contraste, para hacer lo mismo en Unity tienes que recoger el código de los Gizmos y el de los Handles en clases dispares.

Todo hay que decirlo, la documentación de Unity para este tema me ha parecido mucho más completa.

También hay que tener en cuenta que la API de Gizmos y Handles de Unity cubre tanto los juegos 2D como los 3D, mientras que en Godot, todo lo que hemos visto en este artículo, en realidad sólo aplica a los juegos 3D. No existe una clase EditorNode2DGizmoPlugin, o yo al menos no tengo claro cual sería el equivalente de todo este código para un juego 2D de Godot. Lo investigaré y cuando lo descubra probablemente haga otro artículo, pero en un primer vistazo a la documentación oficial no me queda nada claro cómo hacer todo esto en 2D.


Código de este tutorial

El código del proyecto utilizado como ejemplo está disponible para descarga en mi repositorio GodotCustomGizmoAndHandleExample en GitHub. No dudes en descargártelo para examinar detenidamente el código y probarlo tú mismo.