Memperluas Editor Persatuan dengan Jendela Editor, Objek Skrip, dan Editor Kustom

Halo semuanya! Nama saya Grisha dan saya adalah pendiri CGDevs. Hari ini saya ingin berbicara tentang ekstensi editor dan berbicara tentang salah satu proyek saya, yang saya putuskan untuk posting di OpenSource.

Unity adalah alat yang hebat, tetapi memiliki sedikit masalah. Untuk seorang pemula, untuk membuat ruangan sederhana (kotak dengan jendela), seseorang harus menguasai pemodelan 3D, atau mencoba merakit sesuatu dari paha depan. Baru-baru ini telah menjadi ProBuilder gratis, tetapi juga paket pemodelan 3D yang disederhanakan. Saya ingin alat sederhana yang memungkinkan kita untuk dengan cepat menciptakan lingkungan seperti kamar dengan jendela dan memperbaiki UV. Beberapa waktu yang lalu saya mengembangkan satu plugin untuk Unity, yang memungkinkan Anda untuk dengan cepat membuat prototipe lingkungan seperti apartemen dan kamar menggunakan gambar 2D, dan sekarang saya memutuskan untuk meletakkannya di OpenSource. Menggunakan contohnya, kami akan menganalisis bagaimana Anda dapat memperluas editor dan alat apa yang ada untuk ini. Jika Anda tertarik, selamat datang di kucing. Tautan ke proyek di akhir, seperti biasa, terlampir.



Unity3d memiliki kotak alat yang cukup lebar untuk memperluas kemampuan editor. Berkat kelas seperti EditorWindow , serta fungsi Custom Inspector , Property Drawer dan TreeView (+ UIElements akan segera muncul), mudah untuk membangun kerangka kerja Anda dengan berbagai tingkat kompleksitas di atas unit.

Hari ini kita akan berbicara tentang salah satu pendekatan yang saya gunakan untuk mengembangkan solusi saya dan tentang beberapa masalah menarik yang harus saya hadapi.



Solusinya didasarkan pada penggunaan tiga kelas, seperti EditorWindow (semua jendela tambahan), ScriptableObject (penyimpanan data) dan CustomEditor (fungsionalitas inspektur tambahan untuk Objek Scriptable).

Saat mengembangkan ekstensi editor, penting untuk mencoba mematuhi prinsip bahwa pengembang Unity akan menggunakan ekstensi, sehingga antarmuka harus jelas, asli dan sesuai dengan alur kerja Unity.

Mari kita bicara tentang tugas yang menarik.

Agar kita dapat membuat prototipe sesuatu, pertama-tama kita perlu belajar cara menggambar dari mana kita akan menghasilkan lingkungan kita. Untuk melakukan ini, kita memerlukan jendela EditorWindow khusus, di mana kita akan menampilkan semua gambar. Pada prinsipnya, mungkin untuk menggambar di SceneView, tetapi ide awalnya adalah bahwa ketika menyelesaikan solusi, Anda mungkin ingin membuka beberapa gambar pada saat yang sama. Secara umum, membuat jendela terpisah di sebuah unit adalah tugas yang cukup sederhana. Ini dapat ditemukan di manual Unity. Tetapi kotak gambar adalah tugas yang lebih menarik. Ada beberapa masalah dalam hal ini.

Unity memiliki beberapa gaya yang memengaruhi warna jendela.

Faktanya adalah bahwa kebanyakan orang menggunakan versi Pro dari Unity menggunakan tema gelap, dan hanya versi ringan yang tersedia dalam versi gratis. Namun, warna yang digunakan dalam editor gambar tidak boleh menyatu dengan latar belakang. Di sini Anda dapat menemukan dua solusi. Yang sulit adalah membuat versi sendiri dari gaya, memeriksanya dan mengubah palet untuk versi unit. Dan hal yang sederhana adalah mengisi latar belakang jendela dengan warna tertentu. Dalam pengembangannya, diputuskan untuk menggunakan cara yang sederhana. Contoh bagaimana ini dapat dilakukan adalah dengan memanggil kode tersebut dalam metode OnGUI.

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



Intinya, kami hanya menggambar tekstur warna BgColor ke seluruh jendela.



Gambar dan pindahkan kisi

Di sini beberapa masalah terungkap sekaligus. Pertama, Anda harus memasuki sistem koordinat Anda. Faktanya adalah bahwa untuk pekerjaan yang benar dan nyaman kita perlu menghitung ulang koordinat GUI jendela dalam koordinat grid. Untuk ini, dua metode konversi diterapkan (pada dasarnya, ini adalah dua matriks TRS yang dicat)

Mengkonversi koordinat jendela ke koordinat layar
 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); } 



di mana _ParentWindow adalah jendela di mana kita akan menggambar grid, _Offset adalah posisi saat ini dari grid, dan _Zoom adalah tingkat perkiraan.

Kedua, untuk menggambar garis kita perlu metode Handles.DrawLine . Kelas Handles memiliki banyak metode yang berguna untuk merender gambar sederhana di jendela editor, inspektur, atau SceneView. Pada saat pengembangan plugin (Unity 5.5), Handles.DrawLine - mengalokasikan memori dan umumnya bekerja cukup lambat. Karena alasan ini, jumlah baris yang mungkin untuk render dibatasi oleh konstanta CELLS_IN_LINE_COUNT , dan juga "level LOD" dibuat pada zoom untuk mencapai fps yang dapat diterima dalam editor.

Gambar kotak
 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)) ); } } 



Hampir semuanya siap untuk grid. Gerakannya digambarkan dengan sangat sederhana. _Offset pada dasarnya adalah posisi "kamera" saat ini.

Pergerakan grid
  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; } 



Dalam proyek itu sendiri, Anda dapat membiasakan diri dengan kode jendela secara umum dan melihat bagaimana tombol dapat ditambahkan ke jendela.

Kita melangkah lebih jauh. Selain jendela terpisah untuk menggambar, kita perlu menyimpannya sendiri. Mesin serialisasi internal Unity, Scriptable Object, sangat bagus untuk ini. Bahkan, ini memungkinkan Anda untuk menyimpan kelas yang dijelaskan sebagai aset dalam proyek, yang sangat nyaman dan asli untuk banyak pengembang unit. Misalnya, bagian dari kelas Apartemen yang bertanggung jawab untuk menyimpan informasi tata letak secara umum

Bagian dari kelas Apartemen
  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 



Di editor, sepertinya ini di versi saat ini:



Di sini, tentu saja, CustomEditor telah diterapkan, namun demikian, Anda dapat melihat bahwa parameter seperti _Dimensi, Tinggi, IsGenerateOutside, OutsideMaterial, dan PlanImage ditampilkan di editor.

Semua bidang publik dan bidang yang ditandai dengan [SerializeField] diserialisasi (yaitu, disimpan dalam file dalam kasus ini). Ini sangat membantu jika Anda perlu menyimpan gambar, tetapi ketika bekerja dengan ScriptableObject, dan semua sumber daya editor, Anda harus ingat bahwa lebih baik memanggil metode AssetDatabase.SaveAssets () untuk menyimpan keadaan file. Kalau tidak, perubahan tidak akan disimpan. Jika Anda tidak menyimpan proyek dengan tangan Anda.

Sekarang kita akan menganalisis sebagian kelas ApartmentCustomInspector, dan bagaimana cara kerjanya.

Kelas ApartemenCustomInspector
  [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 adalah alat yang sangat kuat yang memungkinkan Anda untuk menyelesaikan banyak tugas khas secara elegan untuk ekstensi editor. Dipasangkan dengan ScriptableObject, ini memungkinkan Anda untuk membuat ekstensi editor yang sederhana, nyaman dan intuitif. Kelas ini sedikit lebih rumit daripada hanya menambahkan tombol, seperti yang dapat Anda lihat di kelas asli bahwa bidang privat [SerializeField] _Rooms daftar sedang serial. Menampilkannya di inspektur, pertama, ke tidak ada, dan kedua - ini dapat menyebabkan bug yang tidak terduga dan menggambar negara. Metode OnInspectorGUI bertanggung jawab untuk memberikan inspektur, dan jika Anda hanya perlu menambahkan tombol, Anda dapat memanggil metode DrawDefaultInspector () di dalamnya dan semua bidang akan ditarik.

Kolom dan tombol yang diperlukan kemudian digambar secara manual. Kelas EditorGUILayout sendiri memiliki banyak implementasi untuk berbagai jenis bidang yang didukung oleh unit. Tetapi tombol rendering di Unity diimplementasikan di kelas GUILayout. Cara pemrosesan tombol ditekan berfungsi dalam kasus ini. OnInspectorGUI - dijalankan pada setiap acara mouse input pengguna (gerakan mouse, klik mouse di dalam jendela editor, dll.) Jika pengguna mengklik tombol di dalam kotak, metode mengembalikan true dan memproses metode yang ada di dalam jika 'dijelaskan oleh Anda' a. Sebagai contoh:

Tombol generasi mesh
 private void GenerateButton() { if (GUILayout.Button( "Generate Mesh" )) { MeshBuilder.GenerateApartmentMesh(_ThisApartment); } } 


Ketika Anda mengklik tombol Generate Mesh, metode statis dipanggil, yang bertanggung jawab untuk menghasilkan mesh dari tata letak tertentu.

Selain mekanisme dasar yang digunakan ketika memperluas editor Unity, saya ingin secara terpisah menyebutkan alat yang sangat sederhana dan sangat nyaman, tentang yang untuk beberapa alasan banyak lupa - Seleksi. Seleksi adalah kelas statis yang memungkinkan Anda memilih objek yang diperlukan di inspektur dan ProjectView.

Untuk memilih objek, Anda hanya perlu menulis Selection.activeObject = MyAwesomeUnityObject. Dan bagian terbaiknya adalah ia bekerja dengan ScriptableObject. Dalam proyek ini, ia bertanggung jawab untuk memilih gambar dan kamar di jendela dengan gambar.

Terima kasih atas perhatian anda! Saya harap artikel dan proyek ini akan bermanfaat bagi Anda, dan Anda akan mempelajari sesuatu yang baru untuk diri Anda sendiri dalam salah satu pendekatan untuk memperluas editor Unity. Dan seperti biasa - tautan ke proyek GitHub , tempat Anda dapat melihat keseluruhan proyek. Ini masih sedikit lembab, tetapi bagaimanapun itu sudah memungkinkan Anda untuk membuat rencana dalam 2D ​​sederhana dan cepat.

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


All Articles