Estendendo o Unity Editor com a Janela do Editor, Objeto Scriptável e Editor Personalizado

Olá pessoal! Meu nome é Grisha e sou o fundador da CGDevs. Hoje eu quero falar sobre extensões de editor e sobre um dos meus projetos, que decidi publicar no OpenSource.

A unidade é uma ótima ferramenta, mas tem um pequeno problema. Para um iniciante, para criar uma sala simples (uma caixa com janelas), é preciso dominar a modelagem 3D ou tentar montar algo a partir de quads. Recentemente, tornou-se ProBuilder totalmente gratuito, mas também é um pacote de modelagem 3D simplificado. Eu queria uma ferramenta simples que nos permitisse criar rapidamente ambientes como salas com janelas e UVs corretos. Há muito tempo, desenvolvi um plug-in para o Unity, que permite prototipar rapidamente ambientes como apartamentos e quartos usando um desenho 2D, e agora decidi colocá-lo no OpenSource. Usando seu exemplo, analisaremos como você pode expandir o editor e quais ferramentas existem para isso. Se você estiver interessado, bem-vindo ao gato. Um link para o projeto no final, como sempre, é anexado.



O Unity3d possui uma caixa de ferramentas ampla o suficiente para expandir os recursos do editor. Graças a classes como EditorWindow , bem como a funcionalidade do Custom Inspector , Property Drawer e TreeView (+ UIElements devem aparecer em breve), é fácil criar estruturas de graus variados de complexidade na parte superior da unidade.

Hoje falaremos sobre uma das abordagens que eu usei para desenvolver minha solução e sobre alguns problemas interessantes que tive que enfrentar.



A solução é baseada no uso de três classes, como EditorWindow (todas as janelas adicionais), ScriptableObject (armazenamento de dados) e CustomEditor (funcionalidade adicional do inspetor para Scriptable Object).

Ao desenvolver extensões de editor, é importante tentar aderir ao princípio de que os desenvolvedores do Unity usarão a extensão, para que as interfaces sejam claras, nativas e integradas ao fluxo de trabalho do Unity.

Vamos falar sobre tarefas interessantes.

Para podermos criar um protótipo de algo, primeiro precisamos aprender a desenhar desenhos a partir dos quais geraremos nosso ambiente. Para fazer isso, precisamos de uma janela especial do EditorWindow, na qual exibiremos todos os desenhos. Em princípio, seria possível desenhar o SceneView, mas a idéia inicial era que, ao finalizar a solução, você poderia abrir vários desenhos ao mesmo tempo. Em geral, criar uma janela separada em uma unidade é uma tarefa bastante simples. Isso pode ser encontrado nos manuais do Unity. Mas a grade de desenho é uma tarefa mais interessante. Existem vários problemas neste assunto.

O Unity possui vários estilos que afetam as cores da janela.

O fato é que a maioria das pessoas que usa a versão Pro do Unity usa um tema sombrio e apenas a versão light está disponível na versão gratuita. No entanto, as cores usadas no editor de desenhos não devem ser mescladas com o plano de fundo. Aqui você pode encontrar duas soluções. O difícil é criar sua própria versão dos estilos, verificar e alterar a paleta da versão da unidade. E o mais simples é preencher o fundo da janela com uma determinada cor. Ao desenvolver, foi decidido usar uma maneira simples. Um exemplo de como isso pode ser feito é chamar esse código no método OnGUI.

Uma certa cor
GUI.color = BgColor; GUI.DrawTexture(new Rect(Vector2.zero, maxSize), EditorGUIUtility.whiteTexture); GUI.color = Color.white; 



Em essência, acabamos de desenhar a textura da cor BgColor para toda a janela.



Desenhar e mover a grade

Aqui vários problemas foram revelados ao mesmo tempo. Primeiro, você tinha que entrar no seu sistema de coordenadas. O fato é que, para um trabalho correto e conveniente, precisamos recalcular as coordenadas da GUI da janela nas coordenadas da grade. Para isso, foram implementados dois métodos de conversão (em essência, são duas matrizes TRS pintadas)

Convertendo coordenadas da janela em coordenadas da tela
 public Vector2 GUIToGrid(Vector3 vec) { Vector2 newVec = ( new Vector2(vec.x, -vec.y) - new Vector2(_ParentWindow.position.width / 2, -_ParentWindow.position.height / 2)) * _Zoom + new Vector2(_Offset.x, -_Offset.y); return newVec.RoundCoordsToInt(); } public Vector2 GridToGUI(Vector3 vec) { return (new Vector2(vec.x - _Offset.x, -vec.y - _Offset.y) ) / _Zoom + new Vector2(_ParentWindow.position.width / 2, _ParentWindow.position.height / 2); } 



onde _ParentWindow é a janela na qual vamos desenhar a grade, _Offset é a posição atual da grade e _Zoom é o grau de aproximação.

Em segundo lugar, para desenhar as linhas, precisamos do método Handles.DrawLine . A classe Handles possui muitos métodos úteis para renderizar gráficos simples nas janelas do editor, no inspetor ou no SceneView. No momento do desenvolvimento do plug-in (Unity 5.5), Handles.DrawLine - alocava memória e geralmente trabalhava devagar. Por esse motivo, o número de linhas possíveis para renderização foi limitado pela constante CELLS_IN_LINE_COUNT e também foi feito o "nível LOD" no zoom para alcançar fps aceitáveis ​​no editor.

Desenho de grade
 void DrawLODLines(int level) { var gridColor = SkinManager.Instance.CurrentSkin.GridColor; var step0 = (int) Mathf.Pow(10, level); int halfCount = step0 * CELLS_IN_LINE_COUNT / 2 * 10; var length = halfCount * DEFAULT_CELL_SIZE; int offsetX = ((int) (_Offset.x / DEFAULT_CELL_SIZE)) / (step0 * step0) * step0; int offsetY = ((int) (_Offset.y / DEFAULT_CELL_SIZE)) / (step0 * step0) * step0; for (int i = -halfCount; i <= halfCount; i += step0) { Handles.color = new Color(gridColor.r, gridColor.g, gridColor.b, 0.3f); Handles.DrawLine( GridToGUI(new Vector2(-length + offsetX * DEFAULT_CELL_SIZE, (i + offsetY) * DEFAULT_CELL_SIZE)), GridToGUI(new Vector2(length + offsetX * DEFAULT_CELL_SIZE, (i + offsetY) * DEFAULT_CELL_SIZE)) ); Handles.DrawLine( GridToGUI(new Vector2((i + offsetX) * DEFAULT_CELL_SIZE, -length + offsetY * DEFAULT_CELL_SIZE)), GridToGUI(new Vector2((i + offsetX) * DEFAULT_CELL_SIZE, length + offsetY * DEFAULT_CELL_SIZE)) ); } offsetX = (offsetX / (10 * step0)) * 10 * step0; offsetY = (offsetY / (10 * step0)) * 10 * step0; ; for (int i = -halfCount; i <= halfCount; i += step0 * 10) { Handles.color = new Color(gridColor.r, gridColor.g, gridColor.b, 1); Handles.DrawLine( GridToGUI(new Vector2(-length + offsetX * DEFAULT_CELL_SIZE, (i + offsetY) * DEFAULT_CELL_SIZE)), GridToGUI(new Vector2(length + offsetX * DEFAULT_CELL_SIZE, (i + offsetY) * DEFAULT_CELL_SIZE)) ); Handles.DrawLine( GridToGUI(new Vector2((i + offsetX) * DEFAULT_CELL_SIZE, -length + offsetY * DEFAULT_CELL_SIZE)), GridToGUI(new Vector2((i + offsetX) * DEFAULT_CELL_SIZE, length + offsetY * DEFAULT_CELL_SIZE)) ); } } 



Quase tudo está pronto para a grade. Seu movimento é descrito de maneira muito simples. _Offset é essencialmente a posição atual da “câmera”.

Movimento da grade
  public void Move(Vector3 dv) { var x = _Offset.x + dv.x * _Zoom; var y = _Offset.y + dv.y * _Zoom; _Offset.x = x; _Offset.y = y; } 



No próprio projeto, você pode se familiarizar com o código da janela em geral e ver como os botões podem ser adicionados à janela.

Nós estamos indo além. Além de uma janela separada para desenhar desenhos, precisamos armazenar os desenhos de alguma forma. O mecanismo interno de serialização do Unity, o Scriptable Object, é ótimo para isso. De fato, permite armazenar as classes descritas como ativos no projeto, o que é muito conveniente e nativo para muitos desenvolvedores de unidades. Por exemplo, a parte da classe Apartment responsável por armazenar informações de layout em geral

Parte da classe Apartamento
  public class Apartment : ScriptableObject { #region fields public float Height; public bool IsGenerateOutside; public Material OutsideMaterial; public Texture PlanImage; [SerializeField] private List<Room> _Rooms; [SerializeField] private Rect _Dimensions; private Vector2[] _DimensionsPoints = new Vector2[4]; #endregion 



No editor, ele se parece com isso na versão atual:



Aqui, é claro, o CustomEditor já foi aplicado, mas, no entanto, você pode observar que parâmetros como _Dimensions, Height, IsGenerateOutside, OutsideMaterial e PlanImage são exibidos no editor.

Todos os campos públicos e os campos marcados com [SerializeField] são serializados (ou seja, salvos em um arquivo nesse caso). Isso ajuda muito se você precisar salvar desenhos, mas ao trabalhar com ScriptableObject e todos os recursos do editor, lembre-se de que é melhor chamar o método AssetDatabase.SaveAssets () para salvar o estado dos arquivos. Caso contrário, as alterações não serão salvas. Se você simplesmente não salvar o projeto com as mãos.

Agora, analisaremos parcialmente a classe ApartmentCustomInspector e como ela funciona.

Class ApartmentCustomInspector
  [CustomEditor(typeof(Apartment))] public class ApartmentCustomInspector : Editor { private Apartment _ThisApartment; private Rect _Dimensions; private void OnEnable() { _ThisApartment = (Apartment) target; _Dimensions = _ThisApartment.Dimensions; } public override void OnInspectorGUI() { TopButtons(); _ThisApartment.Height = EditorGUILayout.FloatField("Height (cm)", _ThisApartment.Height); var dimensions = EditorGUILayout.Vector2Field("Dimensions (cm)", _Dimensions.size).RoundCoordsToInt(); _ThisApartment.PlanImage = (Texture) EditorGUILayout.ObjectField(_ThisApartment.PlanImage, typeof(Texture), false); _ThisApartment.IsGenerateOutside = EditorGUILayout.Toggle("Generate outside (Directional Light)", _ThisApartment.IsGenerateOutside); if (_ThisApartment.IsGenerateOutside) _ThisApartment.OutsideMaterial = (Material) EditorGUILayout.ObjectField( "Outside Material", _ThisApartment.OutsideMaterial, typeof(Material), false); GenerateButton(); var dimensionsRect = new Rect(-dimensions.x / 2, -dimensions.y / 2, dimensions.x, dimensions.y); _Dimensions = dimensionsRect; _ThisApartment.Dimensions = _Dimensions; } private void TopButtons() { GUILayout.BeginHorizontal(); CreateNewBlueprint(); OpenBlueprint(); GUILayout.EndHorizontal(); } private void CreateNewBlueprint() { if (GUILayout.Button( "Create new" )) { var manager = ApartmentsManager.Instance; manager.SelectApartment(manager.CreateOrGetApartment("New Apartment" + GUID.Generate())); } } private void OpenBlueprint() { if (GUILayout.Button( "Open in Builder" )) { ApartmentsManager.Instance.SelectApartment(_ThisApartment); ApartmentBuilderWindow.Create(); } } private void GenerateButton() { if (GUILayout.Button( "Generate Mesh" )) { MeshBuilder.GenerateApartmentMesh(_ThisApartment); } } } 


O CustomEditor é uma ferramenta muito poderosa que permite resolver de maneira elegante muitas tarefas típicas para a extensão do editor. Emparelhado com ScriptableObject, permite criar extensões de editor simples, convenientes e intuitivas. Essa classe é um pouco mais complicada do que simplesmente adicionar botões, como você pode ver na classe original que o campo List _Rooms privado [SerializeField] está sendo serializado. Exibi-lo no inspetor, em primeiro lugar, para nada e em segundo lugar - isso pode levar a erros imprevistos e estados de desenho. O método OnInspectorGUI é responsável por renderizar o inspetor e, se você precisar adicionar botões, poderá chamar o método DrawDefaultInspector () nele e todos os campos serão desenhados.

Os campos e botões necessários são desenhados manualmente. A própria classe EditorGUILayout possui muitas implementações para uma ampla variedade de tipos de campos suportados pela unidade. Mas a renderização de botão no Unity é implementada na classe GUILayout. Como o processamento do botão pressionado funciona neste caso. OnInspectorGUI - executa em cada evento do mouse de entrada do usuário (movimento do mouse, cliques do mouse dentro da janela do editor, etc.) Se o usuário clicar no botão da caixa, o método retornará true e processará os métodos que estão dentro do if 'descrito por você' a. Por exemplo:

Botão de geração de malha
 private void GenerateButton() { if (GUILayout.Button( "Generate Mesh" )) { MeshBuilder.GenerateApartmentMesh(_ThisApartment); } } 


Quando você clica no botão Gerar malha, o método estático é chamado, responsável por gerar uma malha de um layout específico.

Além desses mecanismos básicos usados ​​ao expandir o editor do Unity, gostaria de mencionar separadamente uma ferramenta muito simples e muito conveniente, sobre a qual, por algum motivo, muitos esquecem - Seleção. A seleção é uma classe estática que permite selecionar os objetos necessários no inspetor e no ProjectView.

Para selecionar um objeto, você só precisa escrever Selection.activeObject = MyAwesomeUnityObject. E a melhor parte é que ele funciona com ScriptableObject. Neste projeto, ele é responsável por selecionar um desenho e salas em uma janela com desenhos.

Obrigado pela atenção! Espero que o artigo e o projeto sejam úteis para você e que você aprenda algo novo para si mesmo em uma das abordagens para expandir o editor do Unity. E como sempre - um link para o projeto GitHub , onde você pode ver todo o projeto. Ainda está um pouco úmido, mas, no entanto, já permite que você faça planos em 2D de maneira simples e rápida.

Source: https://habr.com/ru/post/pt431856/


All Articles