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.