Расширение Редактора Unity С Помощью Окна Редактора, Скриптового Объекта И Пользовательского Редактора

Всем привет! Меня зовут Гриша и я основатель CGDevs. Сегодня я хотел бы поговорить о расширениях редактора и рассказать об одном из моих проектов, который я решил опубликовать на OpenSource. Unity — отличный инструмент, но у него есть небольшая проблема.

Новичку, чтобы сделать простую комнату (коробку с окнами), нужно либо освоить 3D-моделирование, либо попробовать собрать что-нибудь из четырехугольников.

Недавно ProBuilder стал полностью бесплатным, но это также упрощенный пакет для 3D-моделирования.

Мне нужен был простой инструмент, который позволил бы мне быстро создавать такие среды, как комнаты с окнами и правильными UV-развертками.

Довольно давно я разработал плагин для Unity, который позволяет быстро создавать прототипы таких сред, как квартиры и комнаты, с помощью 2D-чертежа, и теперь я решил выложить его на OpenSource. На его примере мы рассмотрим, как можно расширить редактор и какие инструменты для этого существуют. Если вам интересно, добро пожаловать под кат. Ссылка на проект, как всегда, находится в конце.



Расширение редактора Unity с помощью окна редактора, скриптового объекта и пользовательского редактора

Unity3d имеет достаточно широкий набор инструментов для расширения возможностей редактора.

Благодаря таким занятиям, как Окно редактора , а также функциональность Пользовательский инспектор , Ящик свойств И В виде дерева (+ скоро должен появиться UIElements ) поверх Unity легко создавать собственные фреймворки разной степени сложности.

Сегодня мы поговорим об одном из подходов, который я использовал при разработке своего решения и о парочке интересных проблем, с которыми мне пришлось столкнуться.

Решение основано на использовании трёх классов, таких как Окно редактора (все дополнительные окна), СкриптаблеОбъект (хранение данных) и Пользовательский редактор (дополнительная функциональность инспектора для Scriptable Object).

При разработке расширений редактора важно стараться придерживаться принципа, согласно которому разработчики Unity будут использовать расширение, поэтому интерфейсы должны быть понятными, нативными и вписываться в рабочий процесс Unity. Поговорим об интересных задачах.

Чтобы нам что-то прототипировать, в первую очередь нам нужно научиться рисовать чертежи, из которых мы будем генерировать нашу среду.

Для этого нам понадобится специальное окно EditorWindow, в котором мы будем отображать все рисунки.

В принципе, в SceneView можно было бы рисовать, но исходная идея заключалась в том, что при доработке решения может возникнуть желание открыть несколько рисунков одновременно.

В целом создание отдельного окна в Unity — достаточно простая задача.

Об этом вы можете прочитать в Руководства по Юнити.

А вот прорисовка сетки — более интересная задача.

В этой теме есть несколько проблем.

Unity имеет несколько стилей, влияющих на цвета окон.

Дело в том, что большинство людей, использующих Pro-версию Unity, используют темную тему, а в бесплатной версии доступна только светлая тема.

Однако цвета, используемые в редакторе рисунков, не должны сливаться с фоном.

Здесь можно придумать два решения.

Самое сложное — сделать свою версию стилей, проверить ее и изменить палитру под версию юнита.

А все просто – залейте фон окна определенным цветом.

В ходе разработки было решено использовать простой путь.

Примером того, как это можно сделать, является вызов следующего кода в методе OnGUI. Рисуем определенным цветом

  
  
  
  
  
  
   

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

По сути, мы просто нарисовали текстуру BgColor на всём окне.



Расширение редактора Unity с помощью окна редактора, скриптового объекта и пользовательского редактора

Рисование и перемещение сетки Здесь возникло сразу несколько проблем.

Сначала нужно было ввести собственную систему координат. Дело в том, что для корректной и удобной работы нам необходимо пересчитать координаты окна GUI в координаты сетки.

Для этого были реализованы два метода преобразования (по сути это написанные две TRS-матрицы) Преобразование координат окна в координаты экрана

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

Где _ParentWindow — это окно, в котором мы будем рисовать сетку, _Компенсировать - текущая позиция сетки, и _Увеличить — степень приближения.

Во-вторых, для рисования линий нам нужен метод Handles.DrawLine .

Класс Handles содержит множество полезных методов для рендеринга простой графики в окнах редактора, инспекторе или SceneView. На момент разработки плагина (Unity 5.5) Handles.DrawLine – выделял память и вообще работал довольно медленно.

По этой причине количество возможных линий было ограничено константой.

CELLS_IN_LINE_COUNT , а также сделал «уровень LOD» при масштабировании, чтобы добиться приемлемого fps в редакторе.

Рендеринг сетки

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

Почти все готово для сетки.

Его движение описывается очень просто.

_Offset — это, по сути, текущая позиция «камеры».

Движение сетки

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

В самом проекте вы можете ознакомиться с кодом окна в целом и посмотреть, как можно добавить кнопки в окно.

Давайте двигаться дальше.

Помимо отдельного окна для рисования чертежей нам необходимо как-то хранить сами рисунки.

Для этого отлично подходит внутренний механизм сериализации Unity, Scriptable Object. По сути, он позволяет хранить описанные классы как активы в проекте, что очень удобно и нативно для многих разработчиков юнитов.

Например, часть класса Apartment, отвечающая за хранение информации о планировке в целом.

Часть класса апартаментов

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

В редакторе в текущей версии это выглядит так:

Расширение редактора Unity с помощью окна редактора, скриптового объекта и пользовательского редактора

Здесь, конечно, CustomEditor уже применен, но тем не менее можно заметить, что в редакторе отображаются такие параметры, как _Dimensions, Height, IsGenerateOutside, OutsideMaterial и PlanImage. Все общедоступные поля и поля с пометкой [SerializeField] сериализуются (то есть в данном случае сохраняются в файл).

Это очень помогает, если вам нужно сохранить чертежи, но при работе со ScriptableObject, да и вообще со всеми ресурсами редактора, вы должны помнить, что для сохранения состояния файлов лучше вызывать метод AssetDatabase.SaveAssets().

В противном случае изменения не сохранятся.

Если только вы не сохраните проект вручную.

Теперь давайте частично рассмотрим класс ApartmentCustomInspector и то, как он работает. Класс 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 — очень мощный инструмент, который позволяет элегантно решать многие распространенные задачи расширения редактора.

В паре со ScriptableObject позволяет создавать простые, удобные и понятные расширения редактора.

Этот класс немного сложнее, чем простое добавление кнопок, поскольку в исходном классе вы можете видеть, что закрытое поле List _Rooms [SerializeField] сериализуется.

Отображение его в инспекторе, во-первых, бесполезно, а во-вторых, может привести к неожиданным ошибкам и состояниям чертежа.

За отрисовку инспектора отвечает метод OnInspectorGUI, и если вам нужно просто добавить кнопки, то в нем можно вызвать метод DrawDefaultInspector() и все поля будут отрисованы.

Затем вручную рисуются необходимые поля и кнопки.

Сам класс EditorGUILayout имеет множество реализаций для различных типов полей, поддерживаемых Unity. А вот отрисовка кнопок в Unity реализована в классе GUILayout. Как в этом случае работает обработка нажатия кнопок? OnInspectorGUI — реагирует на каждое событие пользовательского ввода с помощью мыши (перемещение мыши, нажатие клавиш мыши внутри окна редактора и т. д.).

Если пользователь щелкает мышью в ограничивающей рамке кнопки, то метод возвращает true и обрабатывает методы, которые находятся внутри if', который вы описали.

Например: Кнопка создания сетки

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

При нажатии на кнопку Generate Mesh вызывается статический метод, отвечающий за генерацию сетки определенного макета.

Помимо этих основных механизмов, используемых для расширения редактора Unity, хотелось бы отдельно упомянуть очень простой и очень удобный инструмент, о котором почему-то многие забывают — Выбор.

Selection — статический класс, позволяющий выбирать нужные объекты в Inspector и ProjectView. Для того, чтобы выбрать объект, вам просто нужно написать Selection.activeObject = MyAwesomeUnityObject. И самое приятное то, что он работает со ScriptableObject. В этом проекте он отвечает за выбор чертежа и комнат в окне чертежа.

Спасибо за внимание! Надеюсь, вы найдете статью и проект полезными и узнаете что-то новое, используя один из подходов к расширению редактора Unity. И как всегда - ссылка на проект GitHub , где вы можете просмотреть весь проект. Он еще немного сырой, но тем не менее уже позволяет просто и быстро делать макеты в 2D. Теги: #игры #Разработка игр #gamedev #разработка игр #C++ #unity #.

NET #gamedev #unity3d #unity #расширение редактора Unity #расширения редактора Unity

Вместе с данным постом часто просматривают:

Автор Статьи


Зарегистрирован: 2019-12-10 15:07:06
Баллов опыта: 0
Всего постов на сайте: 0
Всего комментарий на сайте: 0
Dima Manisha

Dima Manisha

Эксперт Wmlog. Профессиональный веб-мастер, SEO-специалист, дизайнер, маркетолог и интернет-предприниматель.