Всем привет! Меня зовут Гриша и я основатель CGDevs. Сегодня я хотел бы поговорить о расширениях редактора и рассказать об одном из моих проектов, который я решил опубликовать на OpenSource. Unity — отличный инструмент, но у него есть небольшая проблема.
Новичку, чтобы сделать простую комнату (коробку с окнами), нужно либо освоить 3D-моделирование, либо попробовать собрать что-нибудь из четырехугольников.
Недавно ProBuilder стал полностью бесплатным, но это также упрощенный пакет для 3D-моделирования.
Мне нужен был простой инструмент, который позволил бы мне быстро создавать такие среды, как комнаты с окнами и правильными UV-развертками.
Довольно давно я разработал плагин для Unity, который позволяет быстро создавать прототипы таких сред, как квартиры и комнаты, с помощью 2D-чертежа, и теперь я решил выложить его на OpenSource. На его примере мы рассмотрим, как можно расширить редактор и какие инструменты для этого существуют. Если вам интересно, добро пожаловать под кат. Ссылка на проект, как всегда, находится в конце.
Unity3d имеет достаточно широкий набор инструментов для расширения возможностей редактора.
Благодаря таким занятиям, как Окно редактора , а также функциональность Пользовательский инспектор , Ящик свойств И В виде дерева (+ скоро должен появиться UIElements ) поверх Unity легко создавать собственные фреймворки разной степени сложности.
Сегодня мы поговорим об одном из подходов, который я использовал при разработке своего решения и о парочке интересных проблем, с которыми мне пришлось столкнуться.
Решение основано на использовании трёх классов, таких как Окно редактора (все дополнительные окна), СкриптаблеОбъект (хранение данных) и Пользовательский редактор (дополнительная функциональность инспектора для Scriptable Object).
При разработке расширений редактора важно стараться придерживаться принципа, согласно которому разработчики Unity будут использовать расширение, поэтому интерфейсы должны быть понятными, нативными и вписываться в рабочий процесс Unity. Поговорим об интересных задачах.
Чтобы нам что-то прототипировать, в первую очередь нам нужно научиться рисовать чертежи, из которых мы будем генерировать нашу среду.
Для этого нам понадобится специальное окно EditorWindow, в котором мы будем отображать все рисунки.
В принципе, в SceneView можно было бы рисовать, но исходная идея заключалась в том, что при доработке решения может возникнуть желание открыть несколько рисунков одновременно.
В целом создание отдельного окна в Unity — достаточно простая задача.
Об этом вы можете прочитать в Руководства по Юнити.
А вот прорисовка сетки — более интересная задача.
В этой теме есть несколько проблем.
Unity имеет несколько стилей, влияющих на цвета окон.
Дело в том, что большинство людей, использующих Pro-версию Unity, используют темную тему, а в бесплатной версии доступна только светлая тема.
Однако цвета, используемые в редакторе рисунков, не должны сливаться с фоном.
Здесь можно придумать два решения.
Самое сложное — сделать свою версию стилей, проверить ее и изменить палитру под версию юнита.
А все просто – залейте фон окна определенным цветом.
В ходе разработки было решено использовать простой путь.
Примером того, как это можно сделать, является вызов следующего кода в методе OnGUI. Рисуем определенным цветом
По сути, мы просто нарисовали текстуру BgColor на всём окне.GUI.color = BgColor; GUI.DrawTexture(new Rect(Vector2.zero, maxSize), EditorGUIUtility.whiteTexture); GUI.color = Color.white;
Рисование и перемещение сетки
Здесь возникло сразу несколько проблем.
Сначала нужно было ввести собственную систему координат. Дело в том, что для корректной и удобной работы нам необходимо пересчитать координаты окна 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
В редакторе в текущей версии это выглядит так:
Здесь, конечно, 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
-
Остерегайтесь Шпионского По
19 Oct, 24 -
Брауи
19 Oct, 24 -
В Чечне Создана Авиакомпания
19 Oct, 24 -
Глобальный Конкурс Aws Для Стартапов
19 Oct, 24 -
Они Крадут
19 Oct, 24