Hallo allerseits! Mein Name ist Grisha und ich bin der Gründer von CGDevs. Heute möchte ich über Editorerweiterungen und über eines meiner Projekte sprechen, die ich in OpenSource veröffentlicht habe.
Einheit ist ein großartiges Werkzeug, aber es hat ein kleines Problem. Für einen Anfänger muss man, um einen einfachen Raum (eine Box mit Fenstern) zu schaffen, entweder die 3D-Modellierung beherrschen oder versuchen, etwas aus Quads zusammenzusetzen. In letzter Zeit ist ProBuilder völlig kostenlos, aber es ist auch ein vereinfachtes 3D-Modellierungspaket. Ich wollte ein einfaches Tool, mit dem wir schnell Umgebungen wie Räume mit Fenstern und korrekten UV-Strahlen erstellen können. Vor langer Zeit habe ich ein Plugin für Unity entwickelt, mit dem Sie mithilfe einer 2D-Zeichnung schnell Prototypen für Umgebungen wie Wohnungen und Räume erstellen können. Jetzt habe ich beschlossen, es in OpenSource zu integrieren. Anhand seines Beispiels analysieren wir, wie Sie den Editor erweitern können und welche Tools dafür vorhanden sind. Wenn Sie interessiert sind, willkommen bei Katze. Ein Link zum Projekt am Ende ist wie immer beigefügt.

Unity3d verfügt über eine ausreichend breite Toolbox, um die Funktionen des Editors zu erweitern. Dank Klassen wie
EditorWindow sowie der Funktionalität von
Custom Inspector ,
Property Drawer und
TreeView (+
UIElements sollten bald erscheinen) ist es einfach, Frameworks mit unterschiedlichem Komplexitätsgrad auf dem Gerät zu erstellen.
Heute werden wir über einen der Ansätze sprechen, mit denen ich meine Lösung entwickelt habe, und über einige interessante Probleme, mit denen ich konfrontiert war.
Die Lösung basiert auf der Verwendung von drei Klassen, z. B.
EditorWindow (alle zusätzlichen Fenster),
ScriptableObject (Datenspeicherung) und
CustomEditor (zusätzliche Inspektorfunktionalität für Scriptable Object).
Bei der Entwicklung von Editorerweiterungen ist es wichtig, das Prinzip einzuhalten, dass Unity-Entwickler die Erweiterung verwenden. Daher sollten die Schnittstellen klar, nativ und in den Unity-Workflow integriert sein.
Sprechen wir über interessante Aufgaben.
Damit wir einen Prototyp erstellen können, müssen wir zunächst lernen, wie man Zeichnungen zeichnet, aus denen wir unsere Umgebung generieren. Dazu benötigen wir ein spezielles EditorWindow-Fenster, in dem wir alle Zeichnungen anzeigen. Im Prinzip wäre es möglich, in SceneView zu zeichnen, aber die ursprüngliche Idee war, dass Sie beim Abschluss der Lösung möglicherweise mehrere Zeichnungen gleichzeitig öffnen möchten. Im Allgemeinen ist das Erstellen eines separaten Fensters in einer Einheit eine ziemlich einfache Aufgabe. Dies finden Sie in den
Unity-Handbüchern. Das Zeichenraster ist jedoch eine interessantere Aufgabe. Zu diesem Thema gibt es mehrere Probleme.
Unity verfügt über mehrere Stile, die sich auf die Fensterfarben auswirken.Tatsache ist, dass die meisten Benutzer der Pro-Version von Unity ein dunkles Thema verwenden und in der kostenlosen Version nur die helle Version verfügbar ist. Die im Zeichnungseditor verwendeten Farben sollten jedoch nicht mit dem Hintergrund verschmelzen. Hier können Sie zwei Lösungen finden. Das Schwierige ist, eine eigene Version der Stile zu erstellen, diese zu überprüfen und die Palette für die Version des Geräts zu ändern. Und das Einfache ist, den Fensterhintergrund mit einer bestimmten Farbe zu füllen. Bei der Entwicklung wurde beschlossen, einen einfachen Weg zu verwenden. Ein Beispiel dafür ist das Aufrufen eines solchen Codes in der OnGUI-Methode.
Eine bestimmte FarbeGUI.color = BgColor; GUI.DrawTexture(new Rect(Vector2.zero, maxSize), EditorGUIUtility.whiteTexture); GUI.color = Color.white;
Im Wesentlichen haben wir nur die BgColor-Farbtextur auf das gesamte Fenster gezeichnet.
Zeichnen und verschieben Sie das RasterHier wurden mehrere Probleme gleichzeitig aufgedeckt. Zuerst mussten Sie Ihr Koordinatensystem eingeben. Tatsache ist, dass wir für eine korrekte und bequeme Arbeit die GUI-Koordinaten des Fensters in den Koordinaten des Rasters neu berechnen müssen. Hierzu wurden zwei Konvertierungsmethoden implementiert (im Wesentlichen sind dies zwei gemalte TRS-Matrizen).
Konvertieren von Fensterkoordinaten in Bildschirmkoordinaten 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); }
Dabei ist
_ParentWindow das Fenster, in dem das Raster
gezeichnet wird,
_Offset die aktuelle Position des Rasters und
_Zoom der Approximationsgrad.
Zweitens benötigen wir zum Zeichnen der Linien die
Handles.DrawLine- Methode. Die Handles-Klasse verfügt über viele nützliche Methoden zum Rendern einfacher Grafiken in Editorfenstern, Inspector oder SceneView. Zum Zeitpunkt der Plugin-Entwicklung (Unity 5.5) hat
Handles.DrawLine Speicher zugewiesen und im Allgemeinen recht langsam gearbeitet. Aus diesem Grund wurde die Anzahl der möglichen Zeilen für das Rendern durch die Konstante
CELLS_IN_LINE_COUNT begrenzt, und beim Zoomen wurde auch die LOD-Ebene festgelegt, um im Editor akzeptable fps zu erzielen.
Gitterzeichnung 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)) ); } }
Fast alles ist startbereit. Seine Bewegung wird sehr einfach beschrieben. _Offset ist im Wesentlichen die aktuelle Position der Kamera.
Gitterbewegung 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; }
Im Projekt selbst können Sie sich mit dem Fenstercode im Allgemeinen vertraut machen und sehen, wie Schaltflächen zum Fenster hinzugefügt werden können.
Wir gehen weiter. Zusätzlich zu einem separaten Fenster zum Zeichnen von Zeichnungen müssen wir die Zeichnungen irgendwie selbst speichern. Die interne Unity-Serialisierungs-Engine, das Scriptable Object, eignet sich hervorragend dafür. Tatsächlich können Sie die beschriebenen Klassen als Assets im Projekt speichern, was für viele Entwickler von Einheiten sehr praktisch und nativ ist. Zum Beispiel der Teil der Apartment-Klasse, der für das Speichern von Layoutinformationen im Allgemeinen verantwortlich ist
Teil der Apartmentklasse 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
Im Editor sieht es in der aktuellen Version folgendermaßen aus:

Hier wurde CustomEditor natürlich bereits angewendet, aber Sie können trotzdem feststellen, dass Parameter wie _Dimensions, Height, IsGenerateOutside, OutsideMaterial und PlanImage im Editor angezeigt werden.
Alle öffentlichen Felder und mit [SerializeField] gekennzeichneten Felder werden serialisiert (dh in diesem Fall in einer Datei gespeichert). Dies ist sehr hilfreich, wenn Sie Zeichnungen speichern müssen. Wenn Sie jedoch mit ScriptableObject und allen Editorressourcen arbeiten, müssen Sie berücksichtigen, dass es besser ist, die AssetDatabase.SaveAssets () -Methode aufzurufen, um den Status der Dateien zu speichern. Andernfalls werden die Änderungen nicht gespeichert. Wenn Sie das Projekt einfach nicht mit Ihren Händen speichern.
Jetzt analysieren wir teilweise die ApartmentCustomInspector-Klasse und wie sie funktioniert.
Klasse 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); } } }
CustomEditor ist ein sehr leistungsfähiges Tool, mit dem Sie viele typische Aufgaben für die Erweiterung des Editors elegant lösen können. In Verbindung mit ScriptableObject können Sie einfache, bequeme und intuitive Editorerweiterungen erstellen. Diese Klasse ist etwas komplizierter als nur das Hinzufügen von Schaltflächen, da Sie in der ursprünglichen Klasse sehen können, dass das Feld [SerializeField] private List _Rooms serialisiert wird. Das Anzeigen im Inspektor erstens zu nichts und zweitens - dies kann zu unvorhergesehenen Fehlern und Zeichnungszuständen führen. Die OnInspectorGUI-Methode ist für das Rendern des Inspektors verantwortlich. Wenn Sie nur Schaltflächen hinzufügen müssen, können Sie die DrawDefaultInspector () -Methode aufrufen, und alle Felder werden gezeichnet.
Die erforderlichen Felder und Schaltflächen werden dann manuell gezeichnet. Die EditorGUILayout-Klasse selbst verfügt über viele Implementierungen für eine Vielzahl von Feldtypen, die von der Einheit unterstützt werden. Das Rendern von Schaltflächen in Unity ist jedoch in der GUILayout-Klasse implementiert. In diesem Fall funktioniert die Verarbeitung zum Drücken von Tasten. OnInspectorGUI - wird bei jedem Benutzereingabe-Mausereignis ausgeführt (Mausbewegung, Mausklicks im Editorfenster usw.). Wenn der Benutzer auf die Schaltfläche im Feld klickt, gibt die Methode true zurück und verarbeitet die Methoden, die sich im if 'von Ihnen beschrieben' befinden. a. Beispielsweise:
Schaltfläche zur Netzgenerierung private void GenerateButton() { if (GUILayout.Button( "Generate Mesh" )) { MeshBuilder.GenerateApartmentMesh(_ThisApartment); } }
Wenn Sie auf die Schaltfläche Netz generieren klicken, wird die statische Methode aufgerufen, die für das Generieren eines Netzes eines bestimmten Layouts verantwortlich ist.
Zusätzlich zu diesen grundlegenden Mechanismen, die beim Erweitern des Unity-Editors verwendet werden, möchte ich ein sehr einfaches und sehr praktisches Tool separat erwähnen, über das viele aus irgendeinem Grund vergessen - Auswahl. Auswahl ist eine statische Klasse, mit der Sie die erforderlichen Objekte im Inspektor und in ProjectView auswählen können.
Um ein Objekt auszuwählen, müssen Sie nur Selection.activeObject = MyAwesomeUnityObject schreiben. Und das Beste daran ist, dass es mit ScriptableObject funktioniert. In diesem Projekt ist er für die Auswahl einer Zeichnung und von Räumen in einem Fenster mit Zeichnungen verantwortlich.
Vielen Dank für Ihre Aufmerksamkeit! Ich hoffe, dass der Artikel und das Projekt für Sie nützlich sind und Sie in einem der Ansätze zur Erweiterung des Unity-Editors etwas Neues für sich lernen. Und wie immer - ein
Link zum GitHub-Projekt , wo Sie das gesamte Projekt sehen können. Es ist immer noch ein wenig feucht, aber es ermöglicht Ihnen bereits, einfach und schnell Pläne in 2d zu machen.