Indo fundo na personalização do IMGUI e do Editor

Momento estranho, você pode pensar. Por que se preocupar com o antigo sistema de interface do usuário agora que o novo está disponível? Bem, embora o novo sistema de interface de usuário tenha a intenção de cobrir todas as situações de interface de usuário do jogo que você queira aplicar, o IMGUI ainda é usado, particularmente em uma situação muito importante: o próprio Editor Unity . Se você estiver interessado em estender o Unity Editor com ferramentas e recursos personalizados, é bem provável que uma das coisas que você precise fazer seja enfrentar o IMGUI.
Primeira pergunta, então: Por que é chamado de 'IMGUI'? IMGUI é a abreviação de Immediate Mode GUI. OK, então, o que é isso? Bem, há duas abordagens principais para sistemas GUI : 'imediata' e 'retida'.
Uma GUI de modo retido é aquela em que o sistema GUI "retém" informações sobre sua GUI: você configura seus vários widgets de GUI - rótulos, botões, controles deslizantes, campos de texto, etc. - e então essas informações são mantidas e usadas pelo sistema para renderizar a tela, responder a eventos e assim por diante. Quando você quer alterar o texto em um rótulo ou mover um botão, você está manipulando alguma informação que está armazenada em algum lugar e, quando você faz a alteração, o sistema continua funcionando em seu novo estado. À medida que o usuário altera os valores e move os controles deslizantes, o sistema simplesmente armazena as alterações, e cabe a você consultar os valores ou responder aos retornos de chamada. O novo sistema de Unity UI é um exemplo de uma interface gráfica de usuário (GUI) de modo retido; você cria seus UI.Labels, UI.Buttons e assim por diante como componentes, configura-os e então os deixa lá, e o novo sistema de interface de usuário cuidará do resto.
Enquanto isso, uma GUI de modo imediato é aquela em que o sistema GUI geralmente não retém informações sobre sua GUI, mas, em vez disso, solicita repetidamente que você especifique novamente quais são seus controles, onde eles estão e assim por diante. Conforme você especifica cada parte da IU na forma de chamadas de função, ela é processada imediatamente (desenhada, clicada, etc.) e as consequências de qualquer interação do usuário são retornadas a você imediatamente, sem que você precise consultá-las. Isso é ineficiente para uma interface de usuário de jogo - e inconveniente para artistas trabalharem, pois tudo se torna muito dependente de código - mas acaba sendo muito útil para situações que não são em tempo real (como painéis do Editor) que são fortemente orientadas por código (como painéis do Editor) e querem alterar os controles exibidos facilmente em resposta ao estado atual (como painéis do Editor!), então é uma boa escolha para coisas como equipamentos pesados de construção. Não, espere. O que eu quis dizer é que é uma boa escolha para painéis do Editor.
Se você quiser saber mais, Casey Muratori tem um ótimo vídeo onde ele discute algumas das vantagens e princípios de uma GUI de Modo Imediato. Ou você pode simplesmente continuar lendo!
Sempre que o código IMGUI está em execução, há um 'Evento' atual sendo manipulado - pode ser algo como 'o usuário clicou no botão do mouse' ou algo como 'a GUI precisa ser repintada'. Você pode descobrir qual é o evento atual verificando Event.current.type.
Imagine como seria se você estivesse criando um conjunto de botões em uma janela em algum lugar e tivesse que escrever um código separado para responder a "o usuário clicou no botão do mouse" e "a GUI precisa ser repintada". Em nível de bloco, pode parecer algo assim:

Escrever essas funções para cada evento GUI separado é meio tedioso; mas você notará que há uma certa similaridade estrutural entre as funções. Em cada etapa do caminho, estamos fazendo algo relacionado ao mesmo controle (botão 1, botão 2 ou botão 3). O que exatamente faremos depende do evento, mas a estrutura é a mesma. O que isto significa é que podemos fazer isto:

Temos uma única função OnGUI que chama funções de biblioteca como GUI.Button, e essas funções de biblioteca fazem coisas diferentes dependendo de qual evento estamos manipulando. Simples!
Existem 5 tipos de eventos que são usados na maioria das vezes:
EventType.MouseDownSet quando o usuário acaba de pressionar um botão do mouse. EventType.MouseUpSet quando o usuário acaba de soltar um botão do mouse. EventType.KeyDownSet quando o usuário acaba de pressionar uma tecla. EventType.KeyUpSet quando o usuário acaba de soltar uma tecla. EventType.RepaintSet quando o IMGUI precisa redesenhar a tela.
Esta não é uma lista exaustiva - consulte a documentação do EventType para mais informações.
Como um controle padrão, como GUI.Button, pode responder a alguns desses eventos?
EventType.RepaintDesenhe o botão no retângulo fornecido.EventType.MouseDownVerifique se o mouse está dentro do retângulo do botão. Se for o caso, sinalize o botão como inativo e acione uma repintura para que ele seja redesenhado quando pressionado. EventType.MouseUp Desmarque o botão como inativo e acione uma repintura, depois verifique se o mouse ainda está dentro do retângulo do botão: se for o caso, retorne true para que o chamador possa responder ao clique no botão.
A realidade é mais complicada do que isso: um botão também responde a eventos do teclado, e há um código para garantir que somente o botão em que você clicou inicialmente possa responder ao MouseUp, mas isso lhe dá uma ideia geral. Contanto que você chame GUI.Button no mesmo ponto do seu código para cada um desses eventos, com a mesma posição e conteúdo, os diferentes comportamentos trabalharão juntos para fornecer toda a funcionalidade de um botão.
Para ajudar a vincular esses diferentes comportamentos em diferentes eventos, o IMGUI tem o conceito de "ID de controle". A ideia de um ID de controle é fornecer uma maneira consistente de se referir a um determinado controle em todos os tipos de evento. Cada parte distinta da interface do usuário que tem comportamento interativo não trivial solicitará um ID de controle; ele é usado para monitorar coisas como qual controle tem o foco do teclado no momento ou para armazenar uma pequena quantidade de informações associadas a um controle. Os IDs de controle são simplesmente atribuídos aos controles na ordem em que eles os solicitam, então, novamente, enquanto você estiver chamando as mesmas funções da GUI na mesma ordem em eventos diferentes, elas acabarão recebendo os mesmos IDs de controle e os diferentes eventos serão sincronizados.
Se você quiser criar suas próprias classes Editor personalizadas, suas próprias classes EditorWindow ou suas próprias classes PropertyDrawer, a classe GUI - assim como a classe EditorGUI - fornece uma biblioteca de controles padrão úteis que você verá usados em todo o Unity.
(É um erro comum para programadores iniciantes do Editor ignorar a classe GUI - mas os controles nessa classe podem ser usados ao estender o Editor tão livremente quanto os controles no EditorGUI. Não há nada particularmente especial sobre GUI vs EditorGUI - elas são apenas duas bibliotecas de controles para você usar - mas a diferença é que os controles no EditorGUI não podem ser usados em compilações de jogos, porque o código para eles é parte do Editor, enquanto a GUI é parte do próprio mecanismo.
Mas e se você quiser fazer algo que vá além do que está disponível na biblioteca padrão?
Vamos explorar como podemos criar um controle de interface de usuário personalizado. Tente clicar e arrastar as caixas coloridas nesta pequena demonstração:
(OBSERVAÇÃO: O aplicativo WebGL original incorporado aqui não funciona mais em navegadores)
(Você precisará de um navegador com suporte a WebGL para ver a demonstração, como as versões atuais do Firefox).
Cada um desses controles deslizantes personalizados aciona um valor "flutuante" separado entre 0 e 1. Talvez você queira usar algo assim no Inspetor como outra maneira de exibir, digamos, a integridade do casco de diferentes partes de um objeto de nave espacial, onde 1 representa "nenhum dano" e 0 representa "totalmente destruído" - ter as barras representando os valores como cores pode tornar mais fácil dizer, rapidamente, em que estado a nave está. O código para criar isso como um controle IMGUI personalizado que você pode usar como qualquer outro controle é bem fácil, então vamos examiná-lo.
O primeiro passo é decidir sobre nossa assinatura de função. Para cobrir todos os diferentes tipos de eventos, nosso controle precisará de três coisas:
- um Rect que define onde ele deve se desenhar e onde deve responder aos cliques do mouse.
- o valor flutuante atual que a barra está representando.
- um GUIStyle, que contém todas as informações necessárias sobre espaçamento, fontes, texturas e assim por diante que o controle precisará. No nosso caso, isso inclui a textura que usaremos para desenhar a barra. Mais sobre esse parâmetro mais tarde.
Ele também precisará retornar o valor que o usuário definiu ao arrastar a barra. Isso só faz sentido em certos eventos, como eventos de mouse, e não em coisas como eventos de repintura; então, por padrão, retornaremos o valor que o código de chamada passou. A ideia é que o código de chamada possa simplesmente fazer “value = MyCustomSlider(... value ...)” sem se importar com o evento que está acontecendo, então se não estamos retornando algum novo valor definido pelo usuário, precisamos preservar o valor atual.
Então a assinatura resultante fica assim:
public static float MyCustomSlider(Rect controlRect, float value, GUIStyle style)
Agora começamos a implementar a função. O primeiro passo é recuperar um ID de controle. Usaremos isso para certas coisas ao responder aos eventos do mouse. Entretanto, mesmo que o evento que está sendo manipulado não seja algo com o qual realmente nos importamos, ainda precisamos solicitar um ID de qualquer maneira, para garantir que ele não seja alocado a algum outro controle para esse evento específico. Lembre-se de que o IMGUI apenas distribui IDs na ordem em que são solicitados, então se você não solicitar um ID, ele acabará sendo fornecido ao próximo controle, fazendo com que esse controle acabe com IDs diferentes para eventos diferentes, o que provavelmente o quebrará. Então, ao solicitar IDs, é tudo ou nada: ou você solicita um ID para cada tipo de evento, ou nunca o solicita para nenhum deles (o que pode ser aceitável se você estiver criando um controle extremamente simples ou não interativo).
{
int controlID = GUIUtility.GetControlID (FocusType.Passive);
O FocusType.Passive passado como parâmetro informa ao IMGUI qual o papel que esse controle desempenha na navegação pelo teclado - se é possível que o controle seja o atual reagindo aos pressionamentos de teclas. Meu controle deslizante personalizado não responde aos pressionamentos de tecla, então ele especifica Passivo, mas os controles que respondem aos pressionamentos de tecla podem especificar Nativo ou Teclado. Confira a documentação do FocusType para mais informações sobre eles.
Em seguida, fazemos o que a maioria dos controles personalizados fará em algum momento de sua implementação: ramificamos dependendo do tipo de evento, usando uma instrução switch. Em vez de usar apenas Event.current.type diretamente, usaremos Event.current.GetTypeForControl(), passando nosso ID de controle; isso filtra o tipo de evento para garantir que, por exemplo, eventos de teclado não sejam enviados para o controle errado em determinadas situações. Mas ele não filtra tudo, então ainda precisaremos realizar algumas verificações por conta própria.
switch (Event.current.GetTypeForControl(controlID))
{
Agora podemos começar a implementar os comportamentos específicos para os diferentes tipos de eventos. Vamos começar desenhando o controle:
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;
}
Neste ponto, você pode finalizar a função e terá um controle 'somente leitura' funcional para visualizar valores flutuantes entre 0 e 1. Mas vamos continuar e tornar o controle interativo.
Para implementar um comportamento agradável do mouse para o controle, temos um requisito: depois de clicar no controle e começar a arrastá-lo, você não precisa manter o mouse sobre ele. É muito melhor para o usuário poder se concentrar apenas onde o cursor está horizontalmente, e não se preocupar com o movimento vertical. Isso significa que eles podem mover o mouse sobre outros controles enquanto arrastam, e precisamos que esses controles ignorem o mouse até que o usuário solte o botão novamente.
A solução para isso é usar GUIUtility.hotControl. É apenas uma variável simples que tem como objetivo armazenar o ID de controle do controle que capturou o mouse. O IMGUI usa esse valor em GetTypeForControl(); quando não é 0, os eventos do mouse são filtrados, a menos que o ID de controle passado seja o hotControl.
Então, configurar e redefinir o hotControl é bem simples:
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;
}
Observe que quando algum outro controle é o controle ativo - ou seja, GUIUtility.hotControl é algo diferente de 0 e nosso próprio ID de controle - esses casos simplesmente não serão executados, porque GetTypeForControl() retornará 'ignore' em vez de eventos mouseUp/mouseDown.
Definir o hotControl é bom, mas ainda não fizemos nada para alterar o valor enquanto o mouse está pressionado. A maneira mais simples de fazer isso é fechar o interruptor e então dizer que qualquer evento do mouse (clicar, arrastar ou soltar) que aconteça enquanto somos o hotControl (e, portanto, estamos no meio do clique+arrastar - mas não soltando, porque zeramos o hotControl naquele caso acima) deve resultar na alteração do 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 ();
}
Essas duas últimas etapas — definir GUI.changed e chamar Event.current.Use() — são particularmente importantes, não apenas para fazer com que esse controle se comporte corretamente, mas também para que ele funcione bem com outros controles e recursos do IMGUI. Em particular, definir GUI.changed como true permitirá que o código de chamada use as funções EditorGUI.BeginChangeCheck() e EditorGUI.EndChangeCheck() para detectar se o usuário realmente alterou o valor do seu controle ou não; mas você também deve evitar definir GUI.changed como false, porque isso pode acabar ocultando o fato de que um controle anterior teve seu valor alterado.
Por fim, precisamos retornar um valor da função. Você deve se lembrar que dissemos que retornaríamos o valor float modificado - ou o valor original, se nada tiver mudado, o que na maioria das vezes será o caso:
return value;
}
E terminamos. MyCustomSlider agora é um controle IMGUI simples e funcional, pronto para ser usado em editores personalizados, PropertyDrawers, janelas de editor e assim por diante. Ainda há mais que podemos fazer para fortalecê-lo - como oferecer suporte à multiedição - mas discutiremos isso abaixo.
Há outra coisa particularmente importante e não óbvia sobre o IMGUI: sua relação com a Visualização de Cena. Todos vocês estarão familiarizados com os elementos auxiliares da interface do usuário que são desenhados na visualização de cena quando você vai transladar, girar e dimensionar objetos: as setas ortogonais, anéis e linhas delimitadas por caixas que você pode clicar e arrastar para manipular objetos. Esses elementos da interface do usuário são chamados de "Alças".
O que não é óbvio é que os Handles também são alimentados pela IMGUI!
Afinal, não há nada inerente no que dissemos sobre IMGUI até agora que seja específico para 2D ou Editors/EditorWindows. Os controles padrão que você encontra nas classes GUI e EditorGUI são todos 2D, certamente, mas os conceitos básicos como EventType e IDs de controle não dependem de 2D. Então, enquanto GUI e EditorGUI fornecem controles 2D destinados a EditorWindows e Editors para componentes no Inspector, a classe Handles fornece controles 3D destinados ao uso na Scene View. Assim como EditorGUI.IntField desenhará um controle que permite ao usuário editar um único inteiro, temos funções como:
Vector3 PositionHandle(posição do vetor3, rotação do quaternion);
que permitirá ao usuário editar um valor Vector3, visualmente, fornecendo um conjunto de setas interativas na Visualização de Cena. E assim como antes, você pode definir suas próprias funções Handle para desenhar elementos personalizados da interface do usuário também; lidar com a interação do mouse é um pouco mais complexo, pois não é mais suficiente apenas verificar se o mouse está dentro de um retângulo ou não - a classe HandleUtility pode ajudar você nisso - mas a estrutura e os conceitos básicos são todos os mesmos.
Se você fornecer uma função OnSceneGUI na sua classe de editor personalizado, poderá usar funções Handle para desenhar na visualização da cena, e elas serão posicionadas corretamente no espaço do mundo, como você esperaria. Porém, tenha em mente que é possível usar Handles em contextos 2D, como editores personalizados, ou usar funções GUI na visualização de cena. Talvez você só precise fazer coisas como configurar matrizes GL ou chamar Handles.BeginGUI() e Handles.EndGUI() para configurar o contexto antes de usá-los.
No caso do MyCustomSlider, havia apenas duas informações que precisávamos monitorar: o valor atual do controle deslizante (que era passado pelo usuário e retornado a ele) e se o usuário estava alterando o valor (o que efetivamente usávamos o hotControl para monitorar). Mas e se um controle precisar manter mais informações do que isso?
O IMGUI fornece um sistema de armazenamento simples para 'objetos de estado' associados a um controle. Você define sua própria classe para armazenar valores e, em seguida, pede ao IMGUI para gerenciar uma instância dela, associada ao ID do seu controle. Você só tem permissão para um objeto de estado por ID de controle, e você não o instancia sozinho. O IMGUI faz isso para você, usando o construtor padrão do objeto de estado. Objetos de estado também não são serializados ao recarregar o código do editor — algo que acontece toda vez que seu código é recompilado — então você só deve usá-los para coisas de curta duração. (Observe que isso é verdadeiro mesmo se você marcar seus objetos de estado como [Serializáveis] - o serializador simplesmente não visita esse canto específico do heap).
Aqui está um exemplo. Suponha que queremos um botão que retorne verdadeiro sempre que for pressionado, mas que também pisque em vermelho se você o mantiver pressionado por mais de dois segundos. Precisaremos controlar o momento em que o botão foi pressionado originalmente; faremos isso armazenando-o em um objeto de estado. Então, aqui está nossa classe 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;
}
}
Armazenaremos o momento em que o mouse foi pressionado em 'mouseDownAt' quando MouseDownNow() for chamado e, então, usaremos a função IsFlashing para nos dizer 'o botão deve estar vermelho agora' - como você pode ver, ele definitivamente não estará vermelho se não for o hotControl ou se menos de 2 segundos tiverem se passado desde que foi clicado, mas depois disso faremos com que ele mude de cor a cada 0,1 segundos.
Aqui está o código para o controle do botão em si:
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;
}
Bem direto - você deve reconhecer o código nos casos mouseDown/mouseUp como sendo muito semelhante ao que fizemos para capturar o mouse no controle deslizante personalizado, acima. As únicas diferenças são a chamada para state.MouseDownNow() ao pressionar o mouse e a alteração da GUI.color no evento repaint.
Os mais atentos entre vocês devem ter notado que há outra diferença fundamental sobre o evento repaint: a chamada para style.Draw(). O que há com isso?
Quando estávamos criando o controle deslizante personalizado, usamos GUI.DrawTexture para desenhar a barra em si. Isso funcionou bem, mas nosso FlashingButton precisa ter uma legenda, além da imagem de "retângulo arredondado" que é o próprio botão. Poderíamos tentar organizar algo com GUI.DrawTexture para desenhar a imagem do botão e então GUI.Label em cima disso para desenhar a legenda... mas podemos fazer melhor. Podemos usar a mesma técnica que o GUI.Label usa para se desenhar e eliminar o intermediário.
Um GUIStyle contém informações sobre as propriedades visuais de um elemento GUI - tanto coisas básicas, como a fonte ou a cor do texto que ele deve usar, quanto propriedades de layout mais sutis, como o espaçamento a ser dado. Todas essas informações são armazenadas em um GUIStyle junto com funções para calcular a largura e a altura de algum conteúdo usando o estilo, e as funções para realmente desenhar o conteúdo na tela.
Na verdade, o GUIStyle não cuida apenas de um estilo para um controle: ele pode cuidar da renderização dele em diversas situações em que um elemento da GUI pode se encontrar — desenhando-o de forma diferente quando o cursor do mouse passa sobre ele, quando o foco do teclado está nele, quando ele está desabilitado e quando ele está "ativo" (por exemplo, quando um botão está sendo pressionado). Você pode fornecer informações de cor e imagem de fundo para todas essas situações, e o GUIStyle escolherá a mais apropriada no momento do desenho com base no ID de controle.
Há quatro maneiras principais de obter GUIStyles que você pode usar para desenhar seus controles:
- Construa um em código (new GUIStyle()) e configure os valores nele.
- Use um dos estilos integrados da classeEditorStyles. Se você quer que seus controles personalizados se pareçam com os integrados — desenhando suas próprias barras de ferramentas, controles no estilo Inspetor, etc. — então este é o lugar para procurar.
- Se você quiser apenas criar uma pequena variação de um estilo existente - digamos, um botão normal, mas com texto alinhado à direita - você pode clonar os estilos na classe EditorStyles (new GUIStyle(existingStyle)) e então alterar apenas as propriedades que deseja alterar.
- Recupere-os de umGUISkin.
Um GUISkin é essencialmente um grande pacote de objetos GUIStyle; o mais importante é que ele pode ser criado como um recurso no seu projeto e editado livremente por meio do Inspetor. Se você criar um e der uma olhada, verá slots para todos os tipos de controle padrão — caixas, botões, rótulos, botões de alternância e assim por diante — mas, como autor de um controle personalizado, direcione sua atenção para a seção "estilos personalizados" perto do final. Aqui você pode configurar qualquer número de entradas GUIStyle personalizadas, dando a cada uma um nome exclusivo e, posteriormente, recuperá-las usando GUISkin.GetStyle(“nameOfCustomStyle”). A única peça que falta no quebra-cabeça é descobrir como obter seu objeto GUISkin a partir do código em primeiro lugar; se você mantiver seu skin na pasta 'Editor Default Resources', poderá usar EditorGUIUtility.LoadRequired(); alternativamente, você pode usar um método como AssetDatabase.LoadAssetAtPath() para carregar de outro lugar no projeto. (Só não coloque seus recursos exclusivos para o editor em algum lugar que os agrupe em pacotes de recursos ou na pasta Recursos por engano!)
Munido de um GUIStyle, você pode desenhar um GUIContent - uma mistura de texto, ícone e dica de ferramenta - usando GUIStyle.Draw(), passando a ele o retângulo no qual você está desenhando, o GUIContent que deseja desenhar e o ID de controle que deve ser usado para descobrir se o conteúdo tem coisas como foco do teclado.
Você deve ter notado que todos os controles GUI que discutimos e escrevemos até agora incluem um parâmetro Rect que determina a posição do controle na tela. E, agora que discutimos o GUIStyle, você pode ter parado quando eu disse que um GUIStyle inclui “propriedades de layout, como a quantidade de espaçamento necessária”. Você pode estar pensando: “ah, não. Isso significa que temos que trabalhar muito para calcular nossos valores de Rect para que os valores de espaçamento sejam respeitados?”
Essa é certamente uma abordagem que está disponível para nós; mas há uma maneira mais fácil. O IMGUI inclui um mecanismo de "layout" que pode calcular automaticamente valores Rect apropriados para nossos controles, levando em consideração aspectos como espaçamento. Então como isso funciona?
O truque é um valor EventType extra para os controles responderem: EventType.Layout. O IMGUI envia o evento para o seu código GUI , e os controles que você invoca respondem chamando funções de layout do IMGUI - GUILayoutUtility.GetRect(), GUILayout.BeginHorizonal / Verticale GUILayout.EndHorizontal / Vertical, entre outras - que o IMGUI registra, construindo efetivamente uma árvore dos controles no seu layout e o espaço que eles requerem. Uma vez concluído e a árvore completamente construída, o IMGUI faz uma passagem recursiva sobre a árvore, calculando as larguras e alturas reais dos elementos e onde eles estão em relação uns aos outros, posicionando controles sucessivos um ao lado do outro e assim por diante.
Então, quando for a hora de executar um evento EventType.Repaint - ou qualquer outro tipo de evento - os controles chamarão as mesmas funções de layout IMGUI. Só que dessa vez, em vez de gravar as chamadas, o IMGUI "reproduz" as chamadas que ele gravou anteriormente no evento Layout, retornando os retângulos que ele calculou; tendo chamado GUILayoutUtility.GetRect() durante o evento layout para registrar que você precisa de um retângulo, você o chama novamente durante o evento repaint e ele realmente retorna o retângulo que você deve usar.
Assim como acontece com IDs de controle, isso significa que você precisa ser consistente sobre as chamadas de layout feitas entre eventos de Layout e outros eventos - caso contrário, você acabará recuperando retângulos computados para os controles errados. Isso também significa que os valores retornados por GUILayoutUtility.GetRect() durante um evento Layout são inúteis, porque o IMGUI não saberá realmente o retângulo que deve fornecer até que o evento seja concluído e a árvore de layout seja processada.
Como isso se parece com nosso controle deslizante personalizado? Na verdade, podemos escrever uma versão habilitada para Layout do nosso controle com muita facilidade, pois assim que obtivermos um retângulo do IMGUI, podemos simplesmente chamar o código que já escrevemos:
public static float MyCustomSlider(float value, GUIStyle style)
{
Rect position = GUILayoutUtility.GetRect(GUIContent.none, style);
return MyCustomSlider(position, value, style);
}
A chamada para GUILayoutUtility.GetRect fará duas coisas: durante um evento Layout, ele registrará que queremos usar o estilo fornecido para desenhar algum conteúdo vazio - vazio porque não há texto ou imagem específica para a qual precisamos abrir espaço - e durante outros eventos, ele recuperará um retângulo real para usarmos. Isso significa que, durante um evento de layout, estamos chamando MyCustomSlider com um retângulo falso, mas isso não importa - ainda precisamos fazer isso para garantir que as chamadas usuais sejam feitas para GetControlID() e que o retângulo não seja realmente usado para nada durante um evento de Layout.
Você deve estar se perguntando como o IMGUI consegue realmente calcular o tamanho do controle deslizante, dado o conteúdo "vazio" e apenas um estilo. Não há muitas informações para prosseguir - estamos contando com o estilo tendo todas as informações necessárias especificadas, que o IMGUI pode usar para descobrir o retângulo a ser atribuído. Mas e se quiséssemos deixar o usuário controlar isso - ou, digamos, usar uma altura fixa do estilo, mas deixar o usuário controlar a largura. Como faríamos isso?
A resposta está na classe GUILayoutOption . Instâncias dessa classe representam diretivas para o sistema de layout de que um retângulo específico deve ser calculado de uma maneira específica; por exemplo, “deve ter altura 30” ou “deve expandir horizontalmente para preencher o espaço” ou “deve ter pelo menos 20 pixels de largura”. Nós os criamos chamando funções de fábrica na classe GUILayout - GUILayout.ExpandWidth(), GUILayout.MinHeight()e assim por diante - e os passamos para GUILayoutUtility.GetRect() como um array. Eles são armazenados na árvore de layout e levados em consideração quando a árvore é processada no final do evento de layout.
Para tornar mais fácil para o usuário fornecer quantas instâncias de GUILayoutOption quiser sem ter que criar e gerenciar seus próprios arrays, aproveitamos a palavra-chave 'params' do C#, que permite chamar um método passando qualquer número de parâmetros e fazer com que esses parâmetros cheguem dentro do método compactados em um array automaticamente. Aqui está nosso controle deslizante modificado:
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 você pode ver, nós apenas pegamos o que o usuário nos deu e passamos para o GetRect.
A abordagem que usamos aqui — de envolver uma função de controle IMGUI posicionada manualmente em uma versão de layout automático — funciona para praticamente qualquer controle IMGUI, incluindo os integrados na classe GUI . Na verdade, a classe GUILayout usa exatamente essa abordagem para fornecer versões com layout automático dos controles na classe GUI (e oferecemos uma classe EditorGUILayout correspondente para encapsular controles na classe EditorGUI). Talvez você queira seguir essa convenção de classes duplas ao criar seus próprios controles IMGUI.
Também é completamente viável misturar controles de layout automático e posicionados manualmente. Você pode chamar GetRect para reservar um pedaço de espaço e, então, fazer seus próprios cálculos para dividir esse retângulo em sub-retângulos que você usará para desenhar vários controles; o sistema de layout não usa IDs de controle de forma alguma, então não há problema em ter vários controles por retângulo de layout (ou até mesmo vários retângulos de layout por controle). Às vezes, isso pode ser muito mais rápido do que usar o sistema de layout por completo.
Observe também que se você estiver escrevendo PropertyDrawers, não deverá usar o sistema de layout; em vez disso, deverá usar apenas o retângulo passado para sua substituição PropertyDrawer.OnGUI(). O motivo para isso é que, por questões de desempenho, a classe Editor em si não usa o sistema de layout; ela apenas calcula um retângulo simples, movendo-o para baixo para cada propriedade sucessiva. Então, se você usasse o sistema de layout no seu PropertyDrawer, ele não teria conhecimento de nenhuma das propriedades que foram desenhadas antes da sua e acabaria posicionando você em cima delas. O que não é o que você quer!
Até agora, tudo o que discutimos permitirá que você crie seu próprio controle IMGUI que funcionará perfeitamente. Há apenas mais algumas coisas a serem discutidas quando você realmente quiser polir o que construiu para o mesmo nível dos controles integrados do Unity .
O primeiro é o uso de SerializedProperty. Não quero entrar em muitos detalhes sobre o sistema SerializedProperty neste post - deixaremos isso para outra ocasião - mas apenas para resumir rapidamente: Uma SerializedProperty 'envolve' uma única variável manipulada pelo sistema de serialização (carregar e salvar) do Unity. Cada variável em cada script que você escreve e que aparece no Inspetor - assim como cada variável em cada objeto de mecanismo que você vê no Inspetor - pode ser acessada por meio da API SerializedProperty, pelo menos no Editor.
SerializedProperty é útil porque não apenas fornece acesso ao valor da variável, mas também informações como se o valor da variável é diferente do valor em um prefab de onde ela veio, ou se uma variável com campos filho (por exemplo, uma struct) é expandida ou recolhida no Inspetor. Ele também integra quaisquer alterações feitas no valor nos sistemas Desfazer e Sujar Cena. Ele permite que você faça isso sem nunca criar a versão gerenciada do seu objeto, o que pode ajudar muito no desempenho. Então, se quisermos que nossos controles IMGUI funcionem bem e facilmente com uma série de funcionalidades do editor (desfazer, sujar cenas, substituições de prefabs, etc.), devemos garantir que oferecemos suporte a SerializedProperty.
Se você observar os métodos do EditorGUI que recebem uma SerializedProperty como argumento, verá que a assinatura é um pouco diferente. Em vez da abordagem 'pegue um float, retorne um float' do nosso controle deslizante personalizado anterior, os controles IMGUI habilitados para SerializedProperty apenas pegam uma instância de SerializedProperty como argumento e não retornam nada. Isso ocorre porque quaisquer alterações que eles precisem fazer no valor serão aplicadas diretamente à SerializedProperty. Então nosso controle deslizante personalizado de antes agora pode ficar assim:
public static void MyCustomSlider(Rect controlRect, SerializedProperty prop, GUIStyle style)
O parâmetro 'value' que costumávamos ter desapareceu, junto com o valor de retorno, e em vez disso, o parâmetro 'prop' está lá para passar na SerializedProperty. Para recuperar o valor atual da propriedade e desenhar a barra deslizante, basta acessar prop.floatValuee, quando o usuário altera a posição do controle deslizante, basta atribuir a prop.floatValue.
Ter toda a SerializedProperty presente no código de controle IMGUI tem outros benefícios. Por exemplo, considere a maneira como as propriedades modificadas em instâncias prefab são mostradas em negrito. Basta verificar a propriedade prefabOverride na SerializedProperty e, se for verdadeira, faça o que for necessário para exibir o controle de forma diferente. Felizmente, se deixar o texto em negrito é realmente tudo o que você quer fazer, o IMGUI cuidará disso automaticamente para você, desde que você não especifique uma fonte no seu GUIStyle ao desenhar. (Se você especificar uma fonte no seu GUIStyle, precisará cuidar disso sozinho - ter versões regular e em negrito da sua fonte e selecionar entre elas com base no prefabOverride quando quiser desenhar).
O outro recurso importante que você precisa é suporte para edição de múltiplos objetos — ou seja, lidar com as coisas com elegância quando seu controle precisa exibir vários valores simultaneamente. Teste isso verificando o valor de EditorGUI.showMixedValue; se for verdadeiro, seu controle está sendo usado para representar vários valores diferentes simultaneamente, então faça o que for necessário para indicar isso.
Os mecanismos bold-on-prefabOverride e showMixedValue exigem que o contexto da propriedade tenha sido configurado usando EditorGUI.BeginProperty() e EditorGUI.EndProperty(). O padrão recomendado é dizer que se o seu método de controle receber uma SerializedProperty como argumento, ele fará as chamadas para BeginProperty e EndProperty, enquanto se ele lidar com valores "brutos" - semelhante a, digamos, EditorGUI.IntField, que recebe e retorna ints diretamente e não funciona com propriedades - então o código de chamada será responsável por chamar BeginProperty e EndProperty. (Faz sentido, realmente, porque se seu controle estiver lidando com valores 'brutos', então ele não tem um valor SerializedProperty que possa passar para BeginProperty de qualquer maneira).
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 postagem tenha esclarecido algumas das principais partes do IMGUI que você precisa entender se realmente quiser levar a personalização do seu editor para o próximo nível. Há muito mais a ser abordado antes que você possa se tornar um guru do Editor - o sistema SerializedObject / SerializedProperty, o uso do CustomEditor versus EditorWindow versus PropertyDrawer, o manuseio do Undo, etc. - mas o IMGUI desempenha um papel importante em desbloquear o imenso potencial do Unity para criar ferramentas personalizadas - tanto com vistas à venda na Asset Store quanto com vistas a capacitar desenvolvedores em suas próprias equipes.
Deixe suas perguntas e comentários nos comentários!