26 octubre 2025

Implementación de una regla para medir distancias en escenarios de Unity

Por alguna razón que desconozco, Unity carece de una herramienta que permita medir distancias en los escenarios y prefabs. Godot sí que la tiene, pero Unity no, a pesar de que es muy útil tanto para construir tus escenarios como para evaluar tus pruebas de funcionalidad. Por ese motivo, el Unity Asset Store está repleta de assets que ofrecen esta funcionalidad... previo pago. Hace poco me vi en la necesidad de medir distancias en un proyecto de Unity en el que ando metido y me planteé comprar uno de estos assets, pero luego me lo pensé mejor. "No puede ser tan difícil de implementar por uno mismo", lo intenté y resultó ser sencillísimo. En este artículo te voy a explicar cómo. Primero lo que queremos conseguir y luego pasaremos a los detalles de implementación.

Una regla para medir distancias es conceptualmente sencilla. Se basa en dos variables independientes, una posición A y una posición B, cuya diferencia da lugar a la variable dependiente que queremos calcular, la distancia. Eso es lo mínimo. A partir de ahí, podemos añadir mejoras visuales o facilidades de manipulación. Para el ejemplo de este artículo quería una herramienta visual que pudiese mover por el escenario, con dos puntos de agarre para ajustar con el ratón las dos posiciones a medir.


También quería poder disponer de múltiples reglas simultáneamente, para poder desplegarlas por diferentes ubicaciones del escenario, siendo aquellas visibles en todo momento sin necesidad de que estuviesen seleccionadas. La apariencia visual de las reglas debía ser configurable para que pudieran resaltar adecuadamente en diferentes escenarios. Así que debía poder configurar el color de las líneas, su grosor, y las dimensiones de los extremos de la regla. Además la regla debía dar las posiciones globales de sus extremos para permitir un emplazamiento preciso. Con todas esas condiciones, lo que buscaba era que la regla tuviese un inspector como el de la figura siguiente.

Inspector de la herramienta
Inspector de la herramienta

Teniendo en cuenta todo lo anterior, podemos pasar a los detalles de implementación. Por cierto, todo el código que vamos a ver está recogido en el repositorio de GitHub de la herramienta. Allí se describe cómo instalar la herramienta cómodamente, usando el Package Manager de Unity. En otro articulo explicaré cómo he empaquetado la herramienta para que se pueda instalar desde el repositorio con el Package Manager.

Usando el repositorio como referencia, el primer fichero a explicar es el de Assets/Runtime/Scripts/MeasuringTape.cs. Este fichero es el componente MonoBehaviour que se montará sobre un Transform para ser instanciado en el escenario, y es el que contiene el modelo de datos de la herramienta. Este no puede ser más sencillo:

Assets/Runtime/Scripts/MeasuringTape.cs
Assets/Runtime/Scripts/MeasuringTape.cs


Los campos de las líneas 8 y 9 son las dos variables independientes de las que hablábamos antes, las posiciones de los puntos A y B. Es importante señalar que son posiciones relativas a la del GameObject sobre el que se monte este componente. Quería hacerlo así para tener la posibilidad de mover la regla por el escenario de una sola vez, sin necesidad de tener que desplazar primero un extremo y luego el otro. También es importante darle un valor por defecto, diferente de cero, a ambas posiciones. De otra manera, sus agarradera visuales coincidirán con la del Transform del GameObject y no podremos manipularlas.

El resto de campos se refieren a la configuración visual de la herramienta:

  • color: El color de las líneas de la regla.
  • thickness: El grosor de dichas líneas.
  • endWidth: La longitud de las líneas transversales de los extremos de la regla.
  • endAlignment: Un valor entre -1 y +1 para bascular las líneas transversales de los extremos a un lado o a otro de la regla. Es una opción interesante si queremos poner múltiples reglas en el escenario a modo de cotas.
  • textSize: Tamaño de la fuente utilizada para mostrar la distancia medida.
  • textDistance: Distancia a la regla del texto con la distancia medida. Puede recibir valores negativos, en cuyo caso el texto pasa a mostrarse por el otro lado de la regla.
Aparte de lo anterior, MeasuringTape.cs ofrece dos propiedades con la posición global de los extremos de la regla:

Assets/Runtime/Scripts/MeasuringTape.cs
Assets/Runtime/Scripts/MeasuringTape.cs

Para tener dos agarraderas visuales con las que fijar los valores de localPositionA y localPositionB, arrastrando el ratón, recurriremos a los Handles de Unity. Los Handles son controles visuales que responden a la interacción del usuario, como clicks, arrastres y rotaciones. Cada vez que mueves un objeto por la escena, lo haces manipulando el Handle que representa la posición del objeto.

Para mostrar Handles personalizados tienes que crear un script en la carpeta Editor con una clase que herede de UnityEditor.Editor. Esa clase debe estar decorada con la etiqueta [CustomEditor] para señalar a qué MonoBehavior asociar los Handles. En el caso de nuestro ejemplo, este script está en Assets/Editor/DrawMeasuringTape.cs.

Es muy importante resaltar que cualquier script como este, que utilice clases del namespace UnityEditor, debe ir forzosamente en una carpeta llamada Editor. Si metes el script directamente en la carpeta Scripts, junto a los MonoBehaviours, te será imposible compilar el juego. Hay gente que mete la carpeta Editor dentro de la de Scripts. Yo prefiero tenerlas bien separadas.

Assets/Editor/DrawMeasuringTape.cs
Assets/Editor/DrawMeasuringTape.cs

Fíjate en la línea 6. A la etiqueta [CustomEditor] hay que pasarle el tipo del MonoBehaviour al que se asocia. Es una manera de decirle al editor: "cada vez que te encuentres con un MonoBehaviour de este tipo represéntalo, en el inspector y en la escena, tal y como se define aquí".

Es importante destacar que he dejado este script dentro del namespace por defecto. No sé muy bien la razón, pero sin intentaba ponerle un namespace personalizado me fallaba el dibujado de Gizmos del método DrawTapeGizmos(), que veremos más adelante.

La configuración de Handles se hace en el método OnSceneGUI(), propio de la clase UnityEditor.Editor

Assets/Editor/DrawMeasuringTape.cs
Assets/Editor/DrawMeasuringTape.cs

La gestión de Handles dentro de OnSceneGUI() siempre tiene la misma estructura. De hecho, no sería descabellado guardarte una plantilla, porque siempre acabas haciendo lo mismo.

Empiezas recuperando una referencia al MonoBehaviour al que se asocian los Handles. En OnSceneGUI(), lo habitual es usar el campo target de UnitEditor.Editor. Ese campo tiene la referencia que buscamos, aunque hay que hacerle un cast (línea 160) para recuperar el tipo exacto. Esa referencia nos será muy útil para leer y fijar las propiedades y campos del MonoBehaviour original.

Luego, viene el segmento en el que muestras tus Handles y recuperas sus valores. Dicho segmento se inicia con una llamada a EditorGUI.BeginChangeCheck() (línea 162). Esa llamada sirve para detectar si el usuario ha modificado algún control de la interfaz del editor entre esta llamada y su correspondiente EditorGUI.EndChangeCheck() (línea 172). En caso de que esta última llamada detecte algún cambio en los controles del editor, se recuperan los valores de estos y se guardan en el MonoBehaviour original (líneas 176 y 177). Esos cambios se registran en el sistema de Undo/Redo con la llamada a Undo.RecordObject() (línea 175). Dicho método registra todos los cambios que se realicen sobre el objeto pasado como parámetro, a partir de la llamada. El texto del segundo parámetro sirve para identificar el cambio en el historial de estos.

Entre las líneas 165 y 170 es donde se produce la creación de los Handles. Los hay de muchos tipos, cada uno con su propia apariencia y forma de ser manipulado. Los que he usado aquí son los más sencillos. Los PositionHandle se limitan a presentar un eje de coordenadas que podemos desplazar por el escenario. En caso de moverse, el Handle devuelve la nueva posición que, en nuestro caso, se almacena en las variables positionAHandle (línea 165) y positionBHandle (línea 168). Esas variables son las que se utilizan para actualizar los campos del MonoBehaviour de origen (líneas 176 y 177).

En casos sencillos, es bastante habitual ver implementaciones que introducen la representación visual de los Gizmos del objeto en el mismo método OnSceneGUI(), generalmente después del bloque del método EditorGUI.EndChangeCheck(). Yo lo he hecho así a menudo, pero tiene un par de inconvenientes. El primero es que mezclas en el mismo método la gestión de los controles visuales del objeto y la de los Gizmos que lo representan, lo que alarga y complica la implementación del método. El segundo problema es que toda la representación visual que incluyas en ese método sólo se mostrará cuando el objeto de origen esté seleccionado. Esto último era lo que me resultó bloqueante, dado que quería que la representación de la regla permaneciese aunque tuviésemos seleccionado otro objeto.

La alternativa que he descubierto es la de decorar un método estático con el atributo [DrawGizmo]. Este atributo señala a un método como el responsable de dibujar los Gizmos de un objeto. Por un lado te permite concentrar en él toda la lógica de la representación visual, dejando OnSceneGUI() para la de la gestión de Handles; y por otro lado, en función de los parámetros que le pasemos al atributo podremos definir cuándo queremos que se muestren los Gizmos.

Assets/Editor/DrawMeasuringTape.cs

Assets/Editor/DrawMeasuringTape.cs
Assets/Editor/DrawMeasuringTape.cs

En el caso del ejemplo, como se puede ver en la línea 108, le he pasado al atributo los flags necesarios para que los Gizmos se muestren tanto cuando el Gizmo esté seleccionado, como cuando no lo esté.

Para dibujar los Gizmos, he utilizado las funciones de dibujado de la librería Handles. Aquí hay cierto solapamiento con lo que ofrece la librería Gizmos, con la que también podemos dibujar. La ventaja de Handles es que te permite seleccionar el grosor de la línea dibujada, mientras que Gizmos sólo te permite dibujar con un grosor de 1 pixel. Partiendo de lo anterior, el resto de las líneas del método son muy similares a cuando dibujamos con Gizmos. 

En la línea 112, fijos el color de las líneas a dibujar, mientras que en la 113 dibujo la línea entre los dos extremos de la herramienta de medida. Fíjate que el tercer parámetro que se le pasa al método DrawLine() es precisamente el grosor de la línea a dibujar.  

Para resaltar los extremos y sus agarraderas visuales, en la línea 116 y en la 121 dibujo una circunferencia alrededor de los extremos. Para rematar estos, he dibujado líneas perpendiculares a la principal. Si quisiese que mi regla se limitase a medir en escenarios de juegos 2D me habría conformado con calcular la perpendicular a la línea principal, pero como quiero poder usarla en entornos 3D hay que buscar que la líneas que crucen los extremos sean también perpendiculares a la dirección de enfoque de la cámara. De otra manera, podría haber momentos en los que la cámara se moviese y las líneas de los extremos desapareciesen por quedar longitudinales a la dirección de enfoque. Para calcular la perpendicular a dos vectores, se usa el producto cruzado, el cual calculo en la línea 133. Hecho eso, ese vector perpendicular me sirve para calcular la semilongitud de los extremos (línea 146) y para dibujar estos, incorporando la basculación de endAlignment (líneas 147 y 150).

El vector perpendicular también es muy útil para separar el texto de la línea principal, desde el centro de esta. Dicho centro se calcula en la línea 138 y desde él se calcula la separación en la línea 142. Una vez que tenemos la ubicación, dibujo el texto en la línea 143. El "F2" de la llamada a ToString() es para que la distancia se muestre con dos decimales.

En este punto, ya tengo una regla de medir cuyos extremos puedo manipular y que se dibuja de la forma que se muestra en la imagen que se muestra al comienzo del artículo. Queda por aclarar cómo he hecho para que el inspector muestre la posición global en campos de sólo lectura. Aunque el MonoBehavior MeasuringTape tenga dos propiedades que ofrecen la posición global de los extremos, Unity no puede mostrar en el inspector propiedades (es una pena porque Godot sí que puede hacerlo). Para mostrar esas propiedades, y que sean sólo de lectura, hay que recurrir a un inspector personalizado. Para hacerlo hay que crear una clase que herede de UnityEditor.Editor, precisamente como la que ya hemos usado para los Handles y la representación visual, pero para representar inspectores personalizados hay que usar el método CreateInspectorGUI().

Dicho método se basa en la lectura que se hace de los valores serializados de los campos del objeto original a representar en el inspector. Esos valores se le pasan a la clase a través de un campo llamado serializedObject. Lo que yo suelo hacer es usar el método OnEnable() para conseguir referencias a cada uno de los campos del objeto original.

Assets/Editor/DrawMeasuringTape.cs

Para conseguir cada una de las referencias hay que llamar al método serializedObject.FindProperty() y pasarle una cadena de texto con el nombre del campo del objeto original que nos interesa. Lo de usar una cadena de texto no me convence, porque es fácil equivocarse, pero es lo que hay.

Una vez obtenidas las referencias a los campos del objeto original, ya podemos usarlas en CreateInspectorGUI().

Assets/Editor/DrawMeasuringTape.cs
Assets/Editor/DrawMeasuringTape.cs
Assets/Editor/DrawMeasuringTape.cs

En la línea 38 se crea el panel donde se representarán todos los campos del inspector. No quería hacer grandes alardes con el orden de los campos, así que me he limitado a organizarlos en el layout clásico vertical.

¿Te acuerdas cuando en OnSceneGUI() conseguía una referencia al MonoBehavior original, usando el campo target? Bueno, puede que estés tentado a hacer lo mismo en este método, pero la documentación de Unity advierte contra ello y exige que en este método en concreto se use serializedObject.targetObject, tal y como se puede ver en la línea 47. No entiendo bien a qué se debe esta peculiaridad, pero he preferido obedecer y hacerlo como recomendaba la documentación, en vez de ir a contracorriente y encontrarme con problemas raros luego.

Como me vale la visualización por defecto que se hace de los dos primeros campos, los de las posiciones locales, me he limitado a crear dos campos por defecto y a poblarlos con los valores del objeto original (líneas 50 y 52). Luego, añadimos los campos al panel (líneas 51 y 53)

En mi caso particular, suelo usar inspectores personalizados cuando quiero mostrar u ocultar campos en función del valor de otro anterior (por ejemplo, un booleano). En esas situaciones, me gusta situar los campos variables (los que se pueden mostrar o no) sobre un panel aparte del principal. Ese panel aparte es el que creo en la línea 56 y añado al principal en la 57. Luego, en la línea 60, le pasamos ese panel al método UpdateGlobalPositionFields() para que se creen sobre él los campos de sólo lectura con las posiciones globales. En un momento, veremos cómo funciona por dentro UpdateGlobalPositionFields(), pero para no perder el hilo de CreateInspectorGUI(), antes vamos a acabar de ver cómo funciona. 

Una vez que añadidos los campos donde se mostrarán las posiciones globales, quiero que estos se mantengan actualizados. Para ello, he enlazado UpdatePositionFields() con los campos de las posiciones locales, para que se ejecute cada vez que cambien los valores de las posiciones locales (líneas 64 a 67). Esto tendrá como efecto la actualización de las posiciones globales. 

Como me vale con que el resto de campos muestren su visualización por defecto, me limito a añadir sus respectivos PropertyField al panel principal (líneas 70 a 75).

Fíjate en que CreateInspectorGUI() tiene que devolver el panel principal para que el editor pueda mostrarlo en el inspector. Es lo que hago en la línea 77.

Lo prometido es deuda, así que ahora vamos a ver qué ocurre dentro de UpdateGlobalPositionFields().

Assets/Editor/DrawMeasuringTape.cs
Assets/Editor/DrawMeasuringTape.cs

Recuerda que acabo de mencionar que a este método le paso como primer parámetro el subpanel en el que se dibujan los campos que queremos actualizar manualmente, bien para insertarles valores o bien para mostrarlos u ocultarlos. 

Lo primero que hago con ese subpanel, en la línea 91, es borrar su contenido para empezar con un lienzo en blanco.

En la línea 93 defino que quiero colar en columna descendente aquellos elementos que vaya añadiendo al subpanel.

Por último, añado los campos con las posiciones globales (líneas 96 y 101). En ambos casos hago lo mismo, creo un campo especializado en mostrar valores posicionales (Vector3Field). Luego, le doy valor a esos campos posicionales usando las propiedades de MeasuringTape que devolvían las respectivas posiciones globales (líneas 97 y 102). Como quiero que los campos sean de sólo lectura, y por tanto no se le puedan insertar valores manualmente, los deshabilito con SetEnabled(false) (líneas 98 y 103). Por último, añado los campos recién creados y configurados al subpanel (líneas 99 y 104).

Con esto ya tendríamos un regla para medir distancias completamente funcional... aunque bastante incómoda. Para usarla, tendríamos que crear un GameObject vacío dentro de la escena y luego añadirle el componente MeasuringTape. Ciertamente trabajoso. Podríamos simplificar las cosas creando un prefab que sólo incorporase el componente MeasuringTape. Así sólo habría que arrastrar el prefab a la jerarquía, cada vez que quisiéramos una regla en la escena. Sin embargo, es nos obligaría a tener que buscar el prefab en nuestras carpetas cada vez que necesitásemos medir algo. Por eso, lo que he hecho es crear una entrada en el menú principal del editor con la que crear una instancia del prefab cada vez que sea necesario. Veamos cómo lo he hecho.

Para añadir entradas al menú principal de Unity te basta con decorar un método estático con el atributo [MenuItem]. A este atributo se le pasa la ruta, dentro del menú principal, de nuestra entrada. Lo que ocurrirá a partir de entonces es que, cada vez que se pulse a esa entrada en el menú, se llamará al método estático.

En mi caso, he implementado el método estático en el fichero Assets/Editor/InstanceMeasuringTape.cs. Fíjate en que el atributo [MenuItem] está en el namespace de UnityEditor, así que un script que lo utilice tiene que situarse en la carpeta Editor.

Assets/Editor/InstanceMeasuringTape.cs
Assets/Editor/InstanceMeasuringTape.cs

Lo primero que hace el método, en la línea 15, es llamar a LoadAssetAtPath() para cargar el prefab con MeasuringTape del que hablábamos antes. La ruta donde buscar dependerá de cómo ejecutes la herramienta. Si copia-pegas todo el código directamente dentro de tu proyecto, tendrás que poner la ruta donde dejes el prefab (siendo Assets la raíz de la ruta). En mi caso, lo he configurado todo para que se ejecute como un paquete descargado desde GitHub con el Package Manager de Unity (en otro artículo explicaré cómo se hace). Así que la ruta tiene que ser la de la carpeta donde se instale el paquete. Los paquetes se instalan en la carpeta Packages, así que mi ruta parte de esa raíz, tal y como se puede ver en la línea 9.

Una vez cargado el prefab, en forma de GameObject, se puede instanciar dentro de la escena usando el método PrefabUtility.InstantiatePrefab() (línea 23). El resultado de esa llamada es que la instancia del prefab aparece en la escena, colgando de la raíz de la jerarquía.

Por último, como lo lógico es que quieras operar sobre la regla recién creada, en la línea 32 se selecciona dicha regla dentro de la jerarquía para que no tengas que buscarla para pulsar sobre ella. 

Y eso es todo. Espero que te haya resultado interesante y que te hay dado alguna idea para crear tus propias herramientas dentro de Únity. Como ya he mencionado en este artículo, en un futuro próximo espero poder sacar un rato para explicar cómo se puede hacer para colgar tus herramientas en GitHub de manera que se se puedan cargar, y mantener actualizadas, usando el PackageManager de Unity.