Profundizando en la personalización de IMGUI y del editor

Podrías pensar que es un momento extraño. ¿Por qué preocuparse por el antiguo sistema de interfaz de usuario ahora que el nuevo está disponible? Bueno, si bien el nuevo sistema de interfaz de usuario está pensado para cubrir todas las situaciones de interfaz de usuario que puedas imaginar en el juego, IMGUI todavía se utiliza, particularmente en una situación muy importante: el propio Editor de Unity . Si está interesado en ampliar el Editor de Unity con herramientas y características personalizadas, es muy probable que una de las cosas que necesitará hacer sea enfrentarse directamente a IMGUI.
Primera pregunta entonces: ¿Por qué se llama IMGUI? IMGUI es la abreviatura de GUI de modo inmediato. Bueno, entonces ¿qué es eso? Bueno, hay dos enfoques principales para los sistemas GUI : "inmediato" y "retenido".
Una GUI en modo retenido es aquella en la que el sistema GUI "retiene" información sobre su GUI: usted configura sus diversos widgets GUI (etiquetas, botones, controles deslizantes, campos de texto, etc.) y luego esa información se conserva y el sistema la usa para renderizar la pantalla, responder a eventos, etc. Cuando desea cambiar el texto de una etiqueta o mover un botón, está manipulando información almacenada en algún lugar y, cuando realiza el cambio, el sistema continúa trabajando en su nuevo estado. A medida que el usuario cambia valores y mueve controles deslizantes, el sistema simplemente almacena sus cambios y depende de usted consultar los valores o responder a las devoluciones de llamadas. El nuevo sistema de Unity UI es un ejemplo de una GUI en modo retenido; usted crea sus etiquetas de interfaz de usuario, botones de interfaz de usuario, etc. como componentes, los configura y luego los deja allí, y el nuevo sistema de interfaz de usuario se encargará del resto.
Mientras tanto, una GUI de modo inmediato es aquella en la que el sistema GUI generalmente no retiene información sobre su GUI, sino que, en cambio, le pide repetidamente que vuelva a especificar cuáles son sus controles, dónde están, etc. A medida que especifica cada parte de la interfaz de usuario en forma de llamadas de función, se procesa inmediatamente (se dibuja, se hace clic, etc.) y las consecuencias de cualquier interacción del usuario se le devuelven de inmediato, en lugar de tener que consultarlas. Esto es ineficiente para la interfaz de usuario de un juego y poco práctico para los artistas, ya que todo se vuelve muy dependiente del código, pero resulta muy útil para situaciones que no son en tiempo real (como los paneles del editor) que están fuertemente controlados por el código (como los paneles del editor) y quieren cambiar los controles mostrados fácilmente en respuesta al estado actual (¡como los paneles del editor!), por lo que es una buena opción para cosas como equipos de construcción pesados. No, espera. Quise decir que es una buena opción para los paneles del editor.
Si desea saber más, Casey Muratori tiene un excelente video donde analiza algunas de las ventajas y principios de una GUI de modo inmediato. ¡O simplemente puedes seguir leyendo!
Cada vez que se ejecuta el código IMGUI, hay un 'Evento' actual que se está manejando: podría ser algo como 'el usuario ha hecho clic en el botón del mouse' o algo como 'la GUI necesita ser repintada'. Puedes averiguar cuál es el evento actual consultando Event.current.type.
Imagínese cómo se vería si estuviera haciendo un conjunto de botones en una ventana en algún lugar y tuviera que escribir un código separado para responder a "el usuario hizo clic en el botón del mouse" y "la GUI necesita ser repintada". A nivel de bloque podría verse así:

Escribir estas funciones para cada evento GUI separado es un poco tedioso; pero notarás que hay una cierta similitud estructural entre las funciones. En cada paso del proceso, hacemos algo relacionado con el mismo control (botón 1, botón 2 o botón 3). Exactamente lo que hacemos depende del evento, pero la estructura es la misma. Lo que esto significa es que podemos hacer esto en su lugar:

Tenemos una única función OnGUI que llama a funciones de biblioteca como GUI.Button, y esas funciones de biblioteca hacen cosas diferentes dependiendo del evento que estemos manejando. ¡Simple!
Hay 5 tipos de eventos que se utilizan la mayor parte del tiempo:
EventType.MouseDownSet cuando el usuario acaba de presionar un botón del mouse. EventType.MouseUpSet cuando el usuario acaba de soltar un botón del mouse. EventType.KeyDownSet cuando el usuario acaba de presionar una tecla. EventType.KeyUpSet cuando el usuario acaba de soltar una tecla. EventType.RepaintSet cuando IMGUI necesita redibujar la pantalla.
Esta no es una lista exhaustiva: consulte la documentación de EventType para obtener más información.
¿Cómo podría un control estándar, como GUI.Button, responder a algunos de estos eventos?
EventType.RepaintDibuja el botón en el rectángulo proporcionado.EventType.MouseDownComprueba si el mouse está dentro del rectángulo del botón. Si es así, marque el botón como presionado y active un repintado para que se vuelva a dibujar como se presionó. EventType.MouseUp Quite la marca del botón como presionado y active un repintado, luego verifique si el mouse aún está dentro del rectángulo del botón: si es así, devuelva verdadero, para que el llamador pueda responder al botón que se está presionando.
La realidad es más complicada que esto: un botón también responde a eventos del teclado y hay un código para garantizar que solo el botón en el que hizo clic inicialmente pueda responder a MouseUp, pero esto le da una idea general. Siempre que llame a GUI.Button en el mismo punto de su código para cada uno de estos eventos, con la misma posición y contenido, los diferentes comportamientos trabajarán juntos para proporcionar toda la funcionalidad de un botón.
Para ayudar a vincular estos diferentes comportamientos bajo diferentes eventos, IMGUI tiene el concepto de "ID de control". La idea de un ID de control es brindar una forma consistente de hacer referencia a un control determinado en cada tipo de evento. Cada parte distinta de la interfaz de usuario que tenga un comportamiento interactivo no trivial solicitará un ID de control; se utiliza para realizar un seguimiento de cosas como qué control tiene actualmente el foco del teclado o para almacenar una pequeña cantidad de información asociada con un control. Los ID de control simplemente se otorgan a los controles en el orden en que los solicitan, por lo que, nuevamente, siempre que esté llamando a las mismas funciones de GUI en el mismo orden bajo diferentes eventos, terminarán recibiendo los mismos ID de control y los diferentes eventos se sincronizarán.
Si desea crear sus propias clases de Editor personalizadas, sus propias clases de EditorWindow o sus propias clases de PropertyDrawer, la clase GUI (así como la clase EditorGUI ) proporciona una biblioteca de controles estándar útiles que verá utilizados en Unity.
Es un error común que los programadores novatos del Editor pasen por alto la clase GUI , pero los controles en esa clase se pueden usar al extender el Editor con tanta libertad como los controles en EditorGUI. No hay nada particularmente especial entre GUI y EditorGUI: son solo dos bibliotecas de controles para que uses, pero la diferencia es que los controles en EditorGUI no se pueden usar en compilaciones de juegos, porque el código para ellos es parte del Editor, mientras que GUI es parte del motor en sí.
¿Pero qué pasa si quieres hacer algo que va más allá de lo que está disponible en la biblioteca estándar?
Exploremos cómo podríamos crear un control de interfaz de usuario personalizado. Intente hacer clic y arrastrar los cuadros de colores en esta pequeña demostración:
(NOTA: La aplicación WebGL original incorporada aquí ya no funciona en los navegadores)
(Necesitará un navegador con soporte WebGL para ver la demostración, como las versiones actuales de Firefox).
Cada uno de estos controles deslizantes personalizados controla un valor "flotante" independiente entre 0 y 1. Es posible que desees usar algo así en el Inspector como otra forma de mostrar, por ejemplo, la integridad del casco de diferentes partes de un objeto de nave espacial, donde 1 representa "sin daño" y 0 representa "totalmente destruido"; tener las barras representando los valores como colores puede hacer que sea más fácil saber, de un vistazo, en qué estado se encuentra la nave. El código para construir esto como un control IMGUI personalizado que puedes usar como cualquier otro control es bastante fácil, así que veámoslo.
El primer paso es decidir la firma de nuestra función. Para cubrir todos los diferentes tipos de eventos, nuestro control necesitará tres cosas:
- un Rect que define dónde debe dibujarse y dónde debe responder a los clics del mouse.
- el valor flotante actual que representa la barra.
- un GUIStyle, que contiene toda la información necesaria sobre espaciado, fuentes, texturas, etc. que necesitará el control. En nuestro caso eso incluye la textura que usaremos para dibujar la barra. Hablaremos más sobre este parámetro más adelante.
También será necesario devolver el valor que el usuario ha establecido al arrastrar la barra. Esto solo tiene sentido en ciertos eventos, como eventos del mouse, y no en cosas como eventos de repintado; por lo tanto, de manera predeterminada, devolveremos el valor que pasó el código de llamada. La idea es que el código de llamada pueda simplemente hacer “value = MyCustomSlider(... value ...)” sin preocuparse por el evento que está sucediendo, por lo que si no estamos devolviendo un nuevo valor establecido por el usuario, necesitamos preservar el valor que se encuentra actualmente.
La firma resultante se ve así:
public static float MyCustomSlider(Rect controlRect, float value, GUIStyle style)
Ahora comenzamos a implementar la función. El primer paso es recuperar un ID de control. Usaremos esto para ciertas cosas al responder a los eventos del mouse. Sin embargo, incluso si el evento que se está manejando no es uno que realmente nos importa, debemos solicitar un ID de todos modos, para asegurarnos de que no se asigne a algún otro control para este evento en particular. Recuerde que IMGUI simplemente proporciona las ID en el orden en que se solicitan, por lo que si no solicita una ID, se terminará entregando al siguiente control, lo que provocará que ese control termine con diferentes ID para diferentes eventos, lo que probablemente lo rompa. Entonces, al solicitar identificaciones, es todo o nada: o solicita una identificación para cada tipo de evento o nunca la solicita para ninguno de ellos (lo que puede estar bien si está creando un control que es extremadamente simple o no interactivo).
{
int controlID = GUIUtility.GetControlID (FocusType.Passive);
El FocusType.Passive que se pasa como parámetro allí le dice a IMGUI qué papel juega este control en la navegación del teclado: si es posible que el control sea el actual que reacciona a las pulsaciones de teclas. Mi control deslizante personalizado no responde en absoluto a las pulsaciones de teclas, por lo que especifica Pasivo, pero los controles que responden a las pulsaciones de teclas podrían especificar Nativo o Teclado. Consulte la documentación de FocusType para obtener más información sobre ellos.
A continuación, hacemos lo que la mayoría de los controles personalizados harán en algún momento de su implementación: nos ramificamos dependiendo del tipo de evento, usando una declaración switch. En lugar de usar directamente Event.current.type, usaremos Event.current.GetTypeForControl()y le pasaremos nuestro ID de control; esto filtra el tipo de evento para garantizar que, por ejemplo, los eventos del teclado no se envíen al control equivocado en determinadas situaciones. Sin embargo, no filtra todo, por lo que igualmente tendremos que realizar algunas comprobaciones propias.
switch (Event.current.GetTypeForControl(controlID))
{
Ahora podemos comenzar a implementar los comportamientos específicos para los diferentes tipos de eventos. Comencemos dibujando el control:
case EventType.Repaint:
{
// Work out the width of the bar in pixels by lerping
int pixelWidth = (int)Mathf.Lerp (1f, controlRect.width, value);
// Build up the rectangle that the bar will cover
// by copying the whole control rect, and then setting the width
Rect targetRect = new Rect (controlRect){ width = pixelWidth };
// Tint whatever we draw to be red/green depending on value
GUI.color = Color.Lerp (Color.red, Color.green, value);
// Draw the texture from the GUIStyle, applying the tint
GUI.DrawTexture (targetRect, style.normal.background);
// Reset the tint back to white, i.e. untinted
GUI.color = Color.white;
break;
}
En este punto podrías finalizar la función y tendrás un control de 'solo lectura' funcional para visualizar valores flotantes entre 0 y 1. Pero continuemos y hagamos el control interactivo.
Para implementar un comportamiento agradable del mouse para el control, tenemos un requisito: una vez que haya hecho clic en el control y haya comenzado a arrastrarlo, no debería ser necesario mantener el mouse sobre el control. Es mucho más agradable para el usuario poder centrarse únicamente en la posición horizontal del cursor y no preocuparse por el movimiento vertical. Esto significa que pueden mover el mouse sobre otros controles mientras arrastran, y necesitamos que esos controles ignoren el mouse hasta que el usuario suelte el botón nuevamente.
La solución a esto es utilizar GUIUtility.hotControl. Es solo una variable simple que tiene como objetivo contener el ID del control que ha capturado el mouse. IMGUI usa este valor en GetTypeForControl(); cuando no es 0, los eventos del mouse se filtran a menos que el ID de control que se pasa sea hotControl.
Entonces, configurar y restablecer hotControl es bastante simple:
case EventType.MouseDown:
{
// If the click is actually on us...
if (controlRect.Contains (Event.current.mousePosition)
// ...and the click is with the left mouse button (button 0)...
&& Event.current.button == 0)
// ...then capture the mouse by setting the hotControl.
GUIUtility.hotControl = controlID;
break;
}
case EventType.MouseUp:
{
// If we were the hotControl, we aren't any more.
if (GUIUtility.hotControl == controlID)
GUIUtility.hotControl = 0;
break;
}
Tenga en cuenta que cuando algún otro control es el control activo (es decir, GUIUtility.hotControl es algo distinto de 0 y nuestro propio ID de control), estos casos simplemente no se ejecutarán, porque GetTypeForControl() devolverá 'ignore' en lugar de eventos mouseUp/mouseDown.
Está bien configurar hotControl, pero todavía no hemos hecho nada para cambiar el valor mientras el mouse está presionado. La forma más sencilla de hacerlo es cerrar el interruptor y luego decir que cualquier evento del mouse (hacer clic, arrastrar o soltar) que ocurra mientras somos el hotControl (y, por lo tanto, estamos en medio de hacer clic y arrastrar, aunque no soltando, porque pusimos en cero el hotControl en ese caso anterior) debería provocar un cambio en el valor:
if (Event.current.isMouse && GUIUtility.hotControl == controlID) {
// Get mouse X position relative to left edge of the control
float relativeX = Event.current.mousePosition.x - controlRect.x;
// Divide by control width to get a value between 0 and 1
value = Mathf.Clamp01 (relativeX / controlRect.width);
// Report that the data in the GUI has changed
GUI.changed = true;
// Mark event as 'used' so other controls don't respond to it, and to
// trigger an automatic repaint.
Event.current.Use ();
}
Esos dos últimos pasos (configurar GUI.changed y llamar a Event.current.Use() ) son particularmente importantes, no solo para lograr que este control se comporte correctamente, sino también para que funcione bien con otros controles y características de IMGUI. En particular, establecer GUI.changed en verdadero permitirá que el código de llamada use las funciones EditorGUI.BeginChangeCheck() y EditorGUI.EndChangeCheck() para detectar si el usuario realmente cambió el valor de su control o no; pero también debe evitar establecer GUI.changed en falso, porque eso podría terminar ocultando el hecho de que se cambió el valor de un control anterior.
Por último, necesitamos devolver un valor de la función. Recordarás que dijimos que devolveríamos el valor flotante modificado, o el valor original, si nada ha cambiado, lo que será así la mayoría de las veces:
return value;
}
Y ya está. MyCustomSlider es ahora un control IMGUI de funcionamiento sencillo, listo para usarse en editores personalizados, PropertyDrawers, ventanas de editor, etc. Aún hay más que podemos hacer para mejorarlo (como admitir la edición múltiple), pero lo analizaremos a continuación.
Hay otra cosa particularmente importante y no obvia sobre IMGUI, y es su relación con la Vista de Escena. Todos estarán familiarizados con los elementos de la interfaz de usuario auxiliar que se dibujan en la vista de escena cuando intenta trasladar, rotar y escalar objetos: las flechas ortogonales, los anillos y las líneas con recuadros en los que puede hacer clic y arrastrar para manipular objetos. Estos elementos de la interfaz de usuario se denominan "Identificadores".
¡Lo que no es obvio es que los Handles también funcionan con IMGUI!
Después de todo, no hay nada inherente en lo que hemos dicho sobre IMGUI hasta ahora que sea específico de 2D o Editors/EditorWindows. Los controles estándar que encontrará en las clases GUI y EditorGUI son todos 2D, ciertamente, pero los conceptos básicos como EventType y los ID de control no dependen del 2D en absoluto. Entonces, mientras que GUI y EditorGUI proporcionan controles 2D destinados a EditorWindows y Editors para componentes en el Inspector, la clase Handles proporciona controles 3D pensados para usarse en la Vista de Escena. Así como EditorGUI.IntField dibujará un control que permite al usuario editar un solo entero, tenemos funciones como:
Vector3 PositionHandle(Posición Vector3, Rotación de cuaternión);
que permitirá al usuario editar un valor Vector3, visualmente, proporcionando un conjunto de flechas interactivas en la Vista de Escena. Y al igual que antes, también puede definir sus propias funciones Handle para dibujar elementos de interfaz de usuario personalizados; lidiar con la interacción con el mouse es un poco más complejo, ya que ya no es suficiente simplemente verificar si el mouse está dentro de un rectángulo o no (la clase HandleUtility puede ser de ayuda allí), pero la estructura y los conceptos básicos son todos iguales.
Si proporciona una función OnSceneGUI en su clase de editor personalizado, puede usar las funciones Handle allí para dibujar en la vista de la escena, y se posicionarán correctamente en el espacio mundial como lo esperaría. Aunque tenga en cuenta que es posible usar Handles en contextos 2D como editores personalizados, o usar funciones GUI en la vista de escena, es posible que solo necesite hacer cosas como configurar matrices GL o llamar a Handles.BeginGUI() y Handles.EndGUI() para configurar el contexto antes de usarlos.
En el caso de MyCustomSlider, realmente solo había dos piezas de información que necesitábamos seguir: el valor actual del control deslizante (que fue pasado por el usuario y le fue devuelto) y si el usuario estaba en proceso de cambiar el valor (para lo cual usamos hotControl para seguirlo). ¿Pero qué pasa si un control necesita conservar más información que esa?
IMGUI proporciona un sistema de almacenamiento simple para 'objetos de estado' que están asociados con un control. Usted define su propia clase para almacenar valores y luego le pide a IMGUI que administre una instancia de ella, asociada con el ID de su control. Solo se le permite un objeto de estado por ID de control, y no lo instancia usted mismo: IMGUI lo hace por usted, utilizando el constructor predeterminado del objeto de estado. Los objetos de estado tampoco se serializan cuando se recarga el código del editor (algo que sucede cada vez que se vuelve a compilar el código), por lo que solo debería usarlos para cosas de corta duración. (Tenga en cuenta que esto es cierto incluso si marca sus objetos de estado como [Serializable]: el serializador simplemente no visita este rincón particular del montón).
He aquí un ejemplo. Supongamos que queremos un botón que devuelva verdadero siempre que se presione, pero que también parpadee en rojo si lo mantiene presionado durante más de dos segundos. Necesitaremos realizar un seguimiento del momento en el que se presionó originalmente el botón; haremos esto almacenándolo en un objeto de estado. Entonces, aquí está nuestra clase de objeto de estado:
public class FlashingButtonInfo
{
private double mouseDownAt;
public void MouseDownNow()
{
mouseDownAt = EditorApplication.timeSinceStartup;
}
public bool IsFlashing(int controlID)
{
if (GUIUtility.hotControl != controlID)
return false;
double elapsedTime = EditorApplication.timeSinceStartup - mouseDownAt;
if (elapsedTime < 2f)
return false;
return (int)((elapsedTime - 2f) / 0.1f) % 2 == 0;
}
}
Almacenaremos el momento en el que se presionó el mouse en 'mouseDownAt' cuando se llame a MouseDownNow(), y luego usaremos la función IsFlashing para indicarnos '¿el botón debería estar de color rojo en este momento?' - como puede ver, definitivamente no será rojo si no es el hotControl o si han pasado menos de 2 segundos desde que se hizo clic, pero después de eso hacemos que cambie de color cada 0,1 segundos.
Aquí está el código para el control del botón en sí:
public static bool FlashingButton(Rect rc, GUIContent content, GUIStyle style)
{
int controlID = GUIUtility.GetControlID (FocusType.Native);
// Get (or create) the state object
var state = (FlashingButtonInfo)GUIUtility.GetStateObject(
typeof(FlashingButtonInfo),
controlID);
switch (Event.current.GetTypeForControl(controlID)) {
case EventType.Repaint:
{
GUI.color = state.IsFlashing (controlID)
? Color.red
: Color.white;
style.Draw (rc, content, controlID);
break;
}
case EventType.MouseDown:
{
if (rc.Contains (Event.current.mousePosition)
&& Event.current.button == 0
&& GUIUtility.hotControl == 0)
{
GUIUtility.hotControl = controlID;
state.MouseDownNow();
}
break;
}
case EventType.MouseUp:
{
if (GUIUtility.hotControl == controlID)
GUIUtility.hotControl = 0;
break;
}
}
return GUIUtility.hotControl == controlID;
}
Bastante sencillo: deberías reconocer el código en los casos mouseDown/mouseUp como muy similar a lo que hicimos para capturar el mouse en el control deslizante personalizado, arriba. Las únicas diferencias son la llamada a state.MouseDownNow() al presionar el mouse y cambiar el color de la GUI en el evento de repintado.
Los más observadores probablemente hayan notado que hay otra diferencia clave en el evento de repintado: la llamada a style.Draw(). ¿Qué pasa con eso?
Cuando estábamos construyendo el control deslizante personalizado, usamos GUI.DrawTexture para dibujar la barra en sí. Eso funcionó bien, pero nuestro FlashingButton necesita tener un título, además de la imagen de "rectángulo redondeado" que es el botón en sí. Podríamos intentar organizar algo con GUI.DrawTexture para dibujar la imagen del botón y luego GUI.Label encima para dibujar el título… pero podemos hacerlo mejor. Podemos utilizar la misma técnica que GUI.Label utiliza para dibujarse a sí mismo y eliminar al intermediario.
Un GUIStyle contiene información sobre las propiedades visuales de un elemento GUI : tanto cosas básicas como la fuente o el color del texto que debe usar, como propiedades de diseño más sutiles como el espaciado que se le debe dar. Toda esta información se almacena en un GUIStyle junto con funciones para calcular el ancho y la altura de algún contenido usando el estilo y las funciones para dibujar el contenido en la pantalla.
De hecho, GUIStyle no solo se ocupa de un estilo para un control: puede encargarse de representarlo en un montón de situaciones en las que un elemento GUI podría encontrarse: dibujándolo de manera diferente cuando se pasa el cursor sobre él, cuando tiene el foco del teclado, cuando está deshabilitado y cuando está "activo" (por ejemplo, cuando un botón está en medio de ser presionado). Puede proporcionar la información de color e imagen de fondo para todas estas situaciones, y GUIStyle elegirá la adecuada en el momento del dibujo en función del ID de control.
Hay cuatro formas principales de obtener GUIStyles que puedes usar para dibujar tus controles:
- Construya uno en código (new GUIStyle()) y configure los valores en él.
- Utilice uno de los estilos incorporados de la claseEditorStyles. Si desea que sus controles personalizados se parezcan a los integrados (dibujando sus propias barras de herramientas, controles estilo Inspector, etc.), este es el lugar para buscar.
- Si solo desea crear una pequeña variación de un estilo existente (por ejemplo, un botón normal pero con texto alineado a la derecha), puede clonar los estilos en la clase EditorStyles (new GUIStyle(existingStyle)) y luego simplemente cambiar las propiedades que desea cambiar.
- Recuperarlos de unGUISkin.
Un GUISkin es esencialmente un gran paquete de objetos GUIStyle; lo que es más importante, se puede crear como un activo en su proyecto y editar libremente a través del Inspector. Si crea uno y le echa un vistazo, verá espacios para todos los tipos de controles estándar (cuadros, botones, etiquetas, conmutadores, etc.), pero como autor de un control personalizado, dirija su atención a la sección "estilos personalizados" cerca de la parte inferior. Aquí puede configurar cualquier cantidad de entradas GUIStyle personalizadas, dándole a cada una un nombre único, y luego puede recuperarlas usando GUISkin.GetStyle(“nameOfCustomStyle”). La única pieza que falta en el rompecabezas es descubrir cómo obtener el objeto GUISkin desde el código en primer lugar; si mantiene su skin en la carpeta 'Recursos predeterminados del editor', puede usar EditorGUIUtility.LoadRequired(); alternativamente, puede usar un método como AssetDatabase.LoadAssetAtPath() para cargar desde otra parte del proyecto. (¡Simplemente no coloques tus recursos exclusivos del editor en algún lugar donde se empaqueten en paquetes de recursos o en la carpeta Recursos por error!)
Armado con un GUIStyle, puede dibujar un GUIContent (una mezcla de texto, ícono e información sobre herramientas) usando GUIStyle.Draw(), pasándole el rectángulo en el que está dibujando, el GUIContent que desea dibujar y el ID de control que debe usarse para determinar si el contenido tiene cosas como el foco del teclado.
Habrás notado que todos los controles GUI que hemos discutido y escrito hasta ahora incluyen un parámetro Rect que determina la posición del control en la pantalla. Y ahora que hemos hablado de GUIStyle, es posible que te hayas detenido cuando dije que un GUIStyle incluye "propiedades de diseño como cuánto espaciado necesita". Podrías estar pensando: “uh oh. ¿Significa esto que tenemos que hacer un montón de trabajo para calcular nuestros valores Rect de modo que se respeten los valores de espaciado?
Sin duda, es un enfoque que está a nuestro alcance, pero hay una manera más fácil. IMGUI incluye un mecanismo de "diseño" que puede calcular automáticamente valores Rect apropiados para nuestros controles, teniendo en cuenta aspectos como el espaciado. Entonces, ¿cómo funciona?
El truco es un valor EventType adicional al que los controles deben responder: Tipo de evento.Diseño. IMGUI envía el evento a su código GUI y los controles que usted invoca responden llamando a las funciones de diseño de IMGUI: GUILayoutUtility.GetRect(), GUILayout.BeginHorizontal / Verticaly GUILayout.EndHorizontal / Vertical, entre otras, que IMGUI registra, construyendo efectivamente un árbol de los controles en su diseño y el espacio que requieren. Una vez terminado y el árbol completamente construido, IMGUI realiza un paso recursivo sobre el árbol, calculando los anchos y alturas reales de los elementos y dónde se encuentran en relación entre sí, posicionando controles sucesivos uno al lado del otro, y así sucesivamente.
Luego, cuando llega el momento de realizar un evento EventType.Repaint (o cualquier otro tipo de evento), los controles llaman a las mismas funciones de diseño de IMGUI. Solo que esta vez, en lugar de grabar las llamadas, IMGUI 'reproduce' las llamadas que grabó previamente en el evento Layout, devolviendo los rectángulos que calculó; después de haber llamado a GUILayoutUtility.GetRect() durante el evento Layout para registrar que necesita un rectángulo, lo llama nuevamente durante el evento repaint y en realidad devuelve el rectángulo que debe usar.
Al igual que con los identificadores de control, esto significa que debe ser coherente con las llamadas de diseño que realiza entre los eventos de diseño y otros eventos; de lo contrario, terminará recuperando rectángulos calculados para los controles incorrectos. También significa que los valores devueltos por GUILayoutUtility.GetRect() durante un evento Layout son inútiles, porque IMGUI en realidad no sabrá el rectángulo que se supone que debe proporcionarle hasta que el evento se haya completado y se haya procesado el árbol de diseño.
¿Cómo se ve esto para nuestro control deslizante personalizado? En realidad, podemos escribir una versión de nuestro control habilitada para diseño con mucha facilidad, ya que una vez que tengamos un rectángulo de IMGUI, podemos simplemente llamar al código que ya escribimos:
public static float MyCustomSlider(float value, GUIStyle style)
{
Rect position = GUILayoutUtility.GetRect(GUIContent.none, style);
return MyCustomSlider(position, value, style);
}
La llamada a GUILayoutUtility.GetRect hará dos cosas: durante un evento Layout, registrará que queremos usar el estilo dado para dibujar algún contenido vacío (vacío porque no hay texto o imagen específicos para los que necesitemos hacer lugar) y durante otros eventos, recuperará un rectángulo real para que lo usemos. Esto significa que durante un evento de diseño estamos llamando a MyCustomSlider con un rectángulo falso, pero no importa: todavía debemos hacerlo para asegurarnos de que se realicen las llamadas habituales a GetControlID() y que el rectángulo no se use realmente para nada durante un evento de diseño.
Quizás te preguntes cómo IMGUI puede calcular el tamaño del control deslizante, dado un contenido "vacío" y solo un estilo. No es mucha información para continuar: dependemos de que el estilo tenga toda la información necesaria especificada, que IMGUI puede usar para determinar el rectángulo a asignar. Pero ¿qué pasa si queremos dejar que el usuario controle eso (o, digamos, usar una altura fija del estilo pero dejar que el usuario controle el ancho)? ¿Cómo haríamos eso?
La respuesta está en la clase GUILayoutOption . Las instancias de esta clase representan directivas para el sistema de diseño que indican que un rectángulo en particular debe calcularse de una manera particular; por ejemplo, “debe tener una altura de 30” o “debe expandirse horizontalmente para llenar el espacio” o “debe tener al menos 20 píxeles de ancho”. Los creamos llamando a funciones de fábrica en la clase GUILayout ( GUILayout.ExpandWidth(), GUILayout.MinHeight(), etc.) y los pasamos a GUILayoutUtility.GetRect() como una matriz. Se almacenan en el árbol de diseño y se tienen en cuenta cuando se procesa el árbol al final del evento de diseño.
Para facilitar que el usuario proporcione tantas o tan pocas instancias de GUILayoutOption como desee sin tener que crear y administrar sus propias matrices, aprovechamos la palabra clave 'params' de C#, que le permite llamar a un método pasando cualquier cantidad de parámetros y hacer que esos parámetros lleguen dentro del método empaquetado en una matriz automáticamente. Aquí está nuestro control deslizante modificado ahora:
public static float MyCustomSlider(float value, GUIStyle style, params GUILayoutOption[] opts)
{
Rect position = GUILayoutUtility.GetRect(GUIContent.none, style, opts);
return MyCustomSlider(position, value, style);
}
Como puedes ver, simplemente tomamos lo que el usuario nos proporciona y lo pasamos a GetRect.
El enfoque que hemos utilizado aquí (envolver una función de control IMGUI posicionada manualmente en una versión con diseño automático) funciona para prácticamente cualquier control IMGUI, incluidos los integrados en la clase GUI . De hecho, la clase GUILayout utiliza exactamente este enfoque para proporcionar versiones con diseño automático de los controles en la clase GUI (y ofrecemos una clase EditorGUILayout correspondiente para envolver los controles en la clase EditorGUI). Es posible que desees seguir esta convención de clases gemelas al crear tus propios controles IMGUI.
También es completamente viable mezclar controles posicionados manualmente y con diseño automático. Puede llamar a GetRect para reservar una porción de espacio y luego hacer sus propios cálculos para dividir ese rectángulo en subrectángulos que luego usará para dibujar múltiples controles; el sistema de diseño no usa identificaciones de control de ninguna manera, por lo que no hay problema en tener múltiples controles por rectángulo de diseño (o incluso múltiples rectángulos de diseño por control). A veces esto puede ser mucho más rápido que utilizar el sistema de diseño por completo.
Además, tenga en cuenta que si está escribiendo PropertyDrawers, no debe utilizar el sistema de diseño; en su lugar, debe utilizar únicamente el rectángulo pasado a su anulación PropertyDrawer.OnGUI(). La razón de esto es que, en realidad, la clase Editor no utiliza el sistema de diseño por razones de rendimiento; simplemente calcula un rectángulo simple y lo mueve hacia abajo para cada propiedad sucesiva. Entonces, si utilizara el sistema de diseño en su PropertyDrawer, este no tendría conocimiento de ninguna de las propiedades que se habían dibujado antes que la suya y terminaría posicionándolo encima de ellas. ¡Eso no es lo que quieres!
Hasta ahora, todo lo que hemos discutido te equipará para construir tu propio control IMGUI que funcionará sin problemas. Solo quedan un par de cosas más para discutir cuando realmente quieras pulir lo que has creado al mismo nivel que los controles integrados de Unity .
El primero es el uso de SerializedProperty. No quiero entrar en demasiados detalles sobre el sistema SerializedProperty en esta publicación (lo dejaremos para otro momento), pero solo para resumir rápidamente: Una SerializedProperty 'envuelve' una única variable manejada por el sistema de serialización (carga y guardado) de Unity. Se puede acceder a cada variable en cada script que escriba y que aparezca en el Inspector (así como a cada variable en cada objeto del motor que vea en el Inspector) a través de la API SerializedProperty, al menos en el Editor.
SerializedProperty es útil porque no solo le da acceso al valor de la variable, sino también a información como si el valor de la variable es diferente al valor en un prefab del que proviene, o si una variable con campos secundarios (por ejemplo, una estructura) está expandida o contraída en el Inspector. También integra cualquier cambio que realice en el valor en los sistemas de Deshacer y de limpieza de escena. También le permite hacer esto sin tener que crear realmente la versión administrada de su objeto, lo que puede mejorar enormemente el rendimiento. Entonces, si queremos que nuestros controles IMGUI funcionen bien y de manera sencilla con una gran cantidad de funciones del editor (deshacer, ensuciar la escena, anulaciones de prefabricados, etc.), debemos asegurarnos de admitir SerializedProperty.
Si observa los métodos EditorGUI que toman una SerializedProperty como argumento, verá que la firma es ligeramente diferente. En lugar del enfoque "tomar un flotante, devolver un flotante" de nuestro control deslizante personalizado anterior, los controles IMGUI habilitados para SerializedProperty solo toman una instancia de SerializedProperty como argumento y no devuelven nada. Esto se debe a que cualquier cambio que necesiten realizar en el valor se aplica directamente a la propiedad serializada. Así que nuestro control deslizante personalizado anterior ahora puede verse así:
public static void MyCustomSlider(Rect controlRect, SerializedProperty prop, GUIStyle style)
El parámetro 'valor' que solíamos tener desapareció, junto con el valor de retorno, y en su lugar, el parámetro 'prop' está allí para pasar en SerializedProperty. Para recuperar el valor actual de la propiedad para dibujar la barra deslizante, simplemente accedemos a prop.floatValue, y cuando el usuario cambia la posición del control deslizante simplemente lo asignamos a prop.floatValue.
Sin embargo, tener toda la SerializedProperty presente en el código de control IMGUI tiene otros beneficios. Por ejemplo, considere la forma en que las propiedades modificadas en instancias prefabricadas se muestran en negrita. Simplemente verifique la propiedad prefabOverride en SerializedProperty y, si es verdadera, haga lo que sea necesario para mostrar el control de manera diferente. Afortunadamente, si poner el texto en negrita es realmente lo único que desea hacer, entonces IMGUI se encargará de eso automáticamente por usted siempre y cuando no especifique una fuente en su GUIStyle cuando dibuje. (Si especifica una fuente en su GUIStyle, entonces tendrá que encargarse de esto usted mismo: tener versiones regulares y en negrita de su fuente y seleccionar entre ellas según prefabOverride cuando desee dibujar).
La otra característica principal que necesitas es soporte para la edición de múltiples objetos, es decir, manejar las cosas con elegancia cuando tu control necesita mostrar múltiples valores simultáneamente. Pruebe esto verificando el valor de EditorGUI.showMixedValue; si es verdadero, su control se está utilizando para representar múltiples valores diferentes simultáneamente, así que haga lo que sea necesario para indicarlo.
Tanto el mecanismo bold-on-prefabOverride como el mecanismo showMixedValue requieren que el contexto de la propiedad se haya configurado mediante EditorGUI.BeginProperty() y EditorGUI.EndProperty(). El patrón recomendado es decir que si su método de control toma una SerializedProperty como argumento, entonces realizará las llamadas a BeginProperty y EndProperty por sí mismo, mientras que si trata con valores "sin procesar" (similar a, digamos, EditorGUI.IntField, que toma y devuelve ints directamente y no funciona con propiedades), entonces el código de llamada es responsable de llamar a BeginProperty y EndProperty. (Tiene sentido, realmente, porque si su control trata con valores "sin procesar", entonces no tiene un valor SerializedProperty que pueda pasar a BeginProperty de todos modos).
public class MySliderDrawer : PropertyDrawer
{
public override float GetPropertyHeight (SerializedProperty property, GUIContent label)
{
return EditorGUIUtility.singleLineHeight;
}
private GUISkin _sliderSkin;
public override void OnGUI (Rect position, SerializedProperty property, GUIContent label)
{
if (_sliderSkin == null)
_sliderSkin = (GUISkin)EditorGUIUtility.LoadRequired ("MyCustomSlider Skin");
MyCustomSlider (position, property, _sliderSkin.GetStyle ("MyCustomSlider"), label);
}
}
// Then, the updated definition of MyCustomSlider:
public static void MyCustomSlider(Rect controlRect, SerializedProperty prop, GUIStyle style, GUIContent label)
{
label = EditorGUI.BeginProperty (controlRect, label, prop);
controlRect = EditorGUI.PrefixLabel (controlRect, label);
// Use our previous definition of MyCustomSlider, which we’ve updated to do something
// sensible if EditorGUI.showMixedValue is true
EditorGUI.BeginChangeCheck();
float newValue = MyCustomSlider(controlRect, prop.floatValue, style);
if(EditorGUI.EndChangeCheck())
prop.floatValue = newValue;
EditorGUI.EndProperty ();
}
Espero que esta publicación haya arrojado algo de luz sobre algunas de las partes centrales de IMGUI que necesitarás comprender si realmente quieres llevar la personalización de tu editor al siguiente nivel. Hay más para cubrir antes de que puedas ser un gurú del editor: el sistema SerializedObject / SerializedProperty, el uso de CustomEditor versus EditorWindow versus PropertyDrawer, el manejo de Undo, etc., pero IMGUI juega un papel importante en desbloquear el inmenso potencial de Unity para crear herramientas personalizadas, tanto con vistas a vender en Asset Store como con vistas a empoderar a los desarrolladores en sus propios equipos.
¡Envíame tus preguntas y opiniones en los comentarios!