Extender el editor de Unity con la ventana del editor, el objeto con secuencia de comandos y el editor personalizado

Hola a todos! Mi nombre es Grisha y soy el fundador de CGDevs. Hoy quiero hablar sobre las extensiones del editor y sobre uno de mis proyectos, que decidí publicar en OpenSource.

La unidad es una gran herramienta, pero tiene un pequeño problema. Para un principiante, para hacer una habitación simple (una caja con ventanas), uno debe dominar el modelado 3D o tratar de ensamblar algo a partir de quads. Recientemente se ha convertido en ProBuilder completamente gratuito, pero también es un paquete de modelado 3D simplificado. Quería una herramienta simple que nos permitiera crear rápidamente entornos como habitaciones con ventanas y corregir los rayos UV. Hace mucho tiempo desarrollé un complemento para Unity, que le permite crear rápidamente prototipos de entornos como apartamentos y habitaciones utilizando un dibujo 2D, y ahora decidí ponerlo en OpenSource. Usando su ejemplo, analizaremos cómo puede expandir el editor y qué herramientas existen para esto. Si estás interesado, bienvenido a cat. Se adjunta un enlace al proyecto al final, como siempre.



Unity3d tiene una caja de herramientas lo suficientemente amplia como para expandir las capacidades del editor. Gracias a clases como EditorWindow , así como a la funcionalidad de Custom Inspector , Property Drawer y TreeView (+ UIElements debería aparecer pronto), es fácil construir sus marcos de diversos grados de complejidad en la parte superior de la unidad.

Hoy hablaremos sobre uno de los enfoques que utilicé para desarrollar mi solución y sobre un par de problemas interesantes que tuve que enfrentar.



La solución se basa en el uso de tres clases, como EditorWindow (todas las ventanas adicionales), ScriptableObject (almacenamiento de datos) y CustomEditor (funcionalidad de inspector adicional para Scriptable Object).

Al desarrollar extensiones de editor, es importante tratar de adherirse al principio de que los desarrolladores de Unity usarán la extensión, por lo que las interfaces deben ser claras, nativas e integradas en el flujo de trabajo de Unity.

Hablemos de tareas interesantes.

Para poder crear un prototipo de algo, antes que nada necesitamos aprender a dibujar dibujos a partir de los cuales generaremos nuestro entorno. Para hacer esto, necesitamos una ventana especial de EditorWindow, en la que mostraremos todos los dibujos. En principio, sería posible dibujar en SceneView, pero la idea inicial era que al finalizar la solución, es posible que desee abrir varios dibujos al mismo tiempo. En general, crear una ventana separada en una unidad es una tarea bastante simple. Esto se puede encontrar en los manuales de Unity. Pero la cuadrícula de dibujo es una tarea más interesante. Hay varios problemas sobre este tema.

Unity tiene varios estilos que afectan los colores de las ventanas.

El hecho es que la mayoría de las personas que usan la versión Pro de Unity usan un tema oscuro, y solo la versión ligera está disponible en la versión gratuita. Sin embargo, los colores utilizados en el editor de dibujo no deben fusionarse con el fondo. Aquí puede encontrar dos soluciones. Lo difícil es hacer su propia versión de los estilos, verificarla y cambiar la paleta para la versión de la unidad. Y lo simple es llenar el fondo de la ventana con un color determinado. En el desarrollo, se decidió utilizar una forma simple. Un ejemplo de cómo se puede hacer esto es llamar a dicho código en el método OnGUI.

Un cierto color
GUI.color = BgColor; GUI.DrawTexture(new Rect(Vector2.zero, maxSize), EditorGUIUtility.whiteTexture); GUI.color = Color.white; 



En esencia, acabamos de dibujar la textura de color BgColor en toda la ventana.



Dibuja y mueve la cuadrícula

Aquí se revelaron varios problemas a la vez. Primero, tenía que ingresar su sistema de coordenadas. El hecho es que para un trabajo correcto y conveniente necesitamos recalcular las coordenadas GUI de la ventana en las coordenadas de la cuadrícula. Para esto, se implementaron dos métodos de conversión (en esencia, estas son dos matrices TRS pintadas)

Convertir coordenadas de ventana a coordenadas de pantalla
 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); } 



donde _ParentWindow es la ventana en la que vamos a dibujar la cuadrícula, _Offset es la posición actual de la cuadrícula y _Zoom es el grado de aproximación.

En segundo lugar, para dibujar las líneas necesitamos el método Handles.DrawLine . La clase Handles tiene muchos métodos útiles para representar gráficos simples en las ventanas del editor, el inspector o SceneView. En el momento del desarrollo del complemento (Unity 5.5), Handles.DrawLine asignaba memoria y generalmente funcionaba bastante lentamente. Por esta razón, el número de líneas posibles para el renderizado estaba limitado por la constante CELLS_IN_LINE_COUNT , y también se hizo "nivel LOD" en el zoom para lograr fps aceptables en el editor.

Dibujo de cuadrícula
 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)) ); } } 



Casi todo está listo para la red. Su movimiento se describe de manera muy simple. _Offset es esencialmente la posición actual de la "cámara".

Movimiento de rejilla
  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; } 



En el proyecto en sí, puede familiarizarse con el código de la ventana en general y ver cómo se pueden agregar botones a la ventana.

Vamos más lejos Además de una ventana separada para dibujar dibujos, necesitamos almacenar los dibujos de alguna manera. El motor interno de serialización de Unity, el Objeto Scriptable, es ideal para esto. De hecho, le permite almacenar las clases descritas como activos en el proyecto, lo cual es muy conveniente y nativo para muchos desarrolladores de unidades. Por ejemplo, la parte de la clase de apartamento que se encarga de almacenar información de diseño en general

Parte de la clase de apartamentos
  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 



En el editor, se ve así en la versión actual:



Aquí, por supuesto, CustomEditor ya se ha aplicado, pero puede observar que parámetros como _Dimensiones, Altura, IsGenerateOutside, OutsideMaterial y PlanImage se muestran en el editor.

Todos los campos públicos y los campos marcados con [SerializeField] se serializan (es decir, se guardan en un archivo en este caso). Esto ayuda mucho si necesita guardar dibujos, pero cuando trabaja con ScriptableObject y todos los recursos del editor, debe recordar que es mejor llamar al método AssetDatabase.SaveAssets () para guardar el estado de los archivos. De lo contrario, los cambios no se guardarán. Si simplemente no guarda el proyecto a mano.

Ahora analizaremos parcialmente la clase ApartmentCustomInspector y cómo funciona.

Clase ApartamentoCustomInspector
  [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); } } } 


CustomEditor es una herramienta muy poderosa que le permite resolver elegantemente muchas tareas típicas para la extensión del editor. Junto con ScriptableObject, le permite hacer extensiones de editor simples, convenientes e intuitivas. Esta clase es un poco más complicada que solo agregar botones, como puede ver en la clase original que el campo [SerializeField] Private List _Rooms se está serializando. Mostrarlo en el inspector, en primer lugar, en nada, y en segundo lugar, esto puede conducir a errores imprevistos y estados de dibujo. El método OnInspectorGUI es responsable de representar el inspector, y si solo necesita agregar botones, puede llamar al método DrawDefaultInspector () y se dibujarán todos los campos.

Los campos y botones necesarios se dibujan manualmente. La propia clase EditorGUILayout tiene muchas implementaciones para una amplia variedad de tipos de campo compatibles con la unidad. Pero la representación de botones en Unity se implementa en la clase GUILayout. Cómo funciona el proceso de presionar botones en este caso. OnInspectorGUI: ejecuta la entrada del mouse para cada evento (movimiento del mouse, clics del mouse dentro de la ventana del editor, etc.) Si el usuario hace clic en el botón en el cuadro, el método devuelve los métodos verdaderos y procesados ​​que están dentro del if 'descrito por usted. a. Por un ejemplo:

Botón de generación de malla
 private void GenerateButton() { if (GUILayout.Button( "Generate Mesh" )) { MeshBuilder.GenerateApartmentMesh(_ThisApartment); } } 


Cuando hace clic en el botón Generar malla, se llama al método estático, que es responsable de generar una malla de un diseño específico.

Además de estos mecanismos básicos utilizados al expandir el editor de Unity, me gustaría mencionar por separado una herramienta muy simple y muy conveniente, sobre la cual, por alguna razón, muchos olvidan: la selección. La selección es una clase estática que le permite seleccionar los objetos necesarios en el inspector y ProjectView.

Para seleccionar un objeto, solo necesita escribir Selection.activeObject = MyAwesomeUnityObject. Y la mejor parte es que funciona con ScriptableObject. En este proyecto, es responsable de seleccionar un dibujo y habitaciones en una ventana con dibujos.

Gracias por su atencion! Espero que el artículo y el proyecto le sean útiles, y que aprenda algo nuevo para usted en uno de los enfoques para expandir el editor de Unity. Y, como siempre, un enlace al proyecto GitHub , donde puede ver todo el proyecto. Todavía está un poco húmedo, pero sin embargo ya le permite hacer planes en 2d de manera simple y rápida.

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


All Articles