Редактор Unity знаменит не только тем, что в нем несложно научится делать игры используя уже имеющиеся инструменты, но и также возможностью дополнять сам редактор, улучшая его инструменты или создавая новые конкретно под проект. Чаще всего расширяют редактор именно с целью создания новых инструментов для удобства работы в нем.
Перед началом работы над расширением редактора, необходимо усвоить одно главное правило:
- Работа в редакторе и работа игры это два разных процесса. Процесс, когда вы запускаете игру после сборки проекта, обычно называют Runtime. Во время этого процесса редактор и его инструменты недоступны. Все элементы, созданные в процессе расширения редактора, также будут недоступны в режиме Runtime, поэтому необходимо заранее разделять области, где вы будете работать с редактором, а где с объектами игры.
Все области редактора или окна, которые вы видите при запуске Unity, имеют свою определенную роль. В окне Hierarchy находятся все объекты сцены, в окне Project – все файлы проекта, а в окне Inspector – настройки и свойства объектов. Также можно добавлять новые окна если необходимо, к примеру – Scene, Game или Profiler, но в этой статье мы будем заниматься только расширением окна “инспектора” (Inspector).
Часто бывает так, что в крупных проектах могут встречаться объекты с очень большим набором самых разных параметров, данных и свойств. Управлять всеми этими свойствами бывает очень трудно, так как просто теряешься в них или забываешь, какой параметр за что отвечает. Для примера, создадим простой скрипт юнита Unity и наполним скрипт данными.
- public class Unit : MonoBehaviour {
- public Sprite icon;
- public string description = “Описание”;
- public float minHealth = 0;
- public float maxHealth = 100;
- public bool rangedUnit;
- public int rangeAttack = 100;
- public AttackType type;
- public float damage = 10;
- }
Здесь есть параметр иконки юнита в виде Sprite, описание юнита в виде текста string, минимальный и максимальный запас здоровья юнита, радиус атаки, урон и тип атаки юнита.
- public enum AttackType {
- Magic = 1,
- Siege = 2,
- Heavy = 4,
- Chaos = 8
- }
Тип атаки AttackType – это отдельное перечисление enum, которое будет указывать, какой тип урона наносит юнит. Так как юниту можно будет указать несколько типов атак, в редакторе представим перечисление в виде маски. Теперь сохраним этот скрипт и закинем какому нибудь объекту на сцене. Вот так выглядит компонент Unit со всеми настройками.
Все же, пока не так сложно разобраться, какой параметр за, что отвечает, потому что их не так много, но с помощью расширения редактора можно добиться намного более удобного вида компонента для работы, например такого.
Насколько сразу стало все понятно, разные настройки юнита вынесены в разные области.
Редактор для компонента
Настройки каждого компонента в инспекторе отображаются с помощью стандартного редактора, к которому необходимо получить доступ и перенастроить его. У нас уже есть компонент юнита Unit. Чтобы изменить его редактор, необходимо создать специальный скрипт, управляющий его отображением в инспекторе.
Перед тем как создать скрипт редактора, нужно вспомнить главное правило разделения процессов редактора и Runtime’а, для этого достаточно использовать одну из специальных папок редактора – Editor. Эта специальная папка Editor, как и другие специальные папки по типу Resource или StreamingAssets выполняет отдельные функции в редакторе Unity. В папках Editor должны находится только те объекты, которые используются только в редакторе. Там могут находится скрипты для расширения, изображения, другие файлы. Задача этой папки в том, чтобы эти файлы не попали в конечную сборку игры и тем самым не засоряли ее объектами, которые никогда не используются в самой игре.
Поэтому создаем в проекте специальную папку Editor и новый скрипт в ней, для расширения компонента Unit. По негласному правилу среди разработчиков, принято называть скрипты расширения также, как и названия их компонентов, только добавляя в конце *Editor. В нашем случае назовем этот скрипт UnitEditor.
- using System.Collections;
- using System.Collections.Generic;
- public class UnitEditor : MonoBehaviour {
- }
Сейчас этот скрипт наследуется от класса MonoBehaviour и представляет собой обычный компонент. Для того, чтобы сделать из него “новый редактор”, необходимо унаследовать его от класса “расширения” Editor. Находится этот класс в специальном пространстве имен UnityEditor. Добавить это пространство можно, дописав в самом верху скрипта два слова: using UnityEditor;
- using System.Collections;
- using System.Collections.Generic;
- using UnityEditor;
- public class UnitEditor : Editor {
- }
Готово, новое пространство подключено и скрипт унаследован от класса Editor. Далее необходимо указать, какой именно компонент необходимо расширить. Для этого используем стандартный атрибут CustomEditor.
- [CustomEditor(typeof(Unit))]
- public class UnitEditor : Editor {
- }
В атрибуте CustomEditor указывается название компонента, к которому необходимо подключить новый редактор. В нашем случае просто указывается компонент юнита – Unit.
Расширение инспектора
Как и для всех остальных областей редактора Unity, панель инспектора (Inspector) имеет в классе Editor свой специальный метод для работы, называется он OnInspectorGUI. Именно этот метод отвечает за то? как будут выглядеть настройки компонента в окне инспектора.
- [CustomEditor(typeof(Unit))]
- public class UnitEditor : Editor {
- public override void OnInspectorGUI() {
- }
- }
То, что мы будем видеть в окне инспектора, будет сначала обрабатываться здесь – в методе OnInspectorGUI. Этот метод виртуальный, так как он изначально уже имеет свой стандартный редактор, и после того как мы его перегрузили, сохраняем скрипт, и видим в окне инспектора, что все настройки компонента Unit исчезли.
Для того, чтобы “нарисовать” новый редактор компонента Unit, используется специальный GUI. Тем, кто работал раньше с GUI, известно, что это набор статических методов для отображения процедурного графического интерфейса. К примеру, попробуем вывести какой то текст в окне инспектора. Для этого воспользуемся обычным статическим методом Label класса GUILayout.
- [CustomEditor(typeof(Unit))]
- public class UnitEditor : Editor {
- public override void OnInspectorGUI() {
- GUILayout.Label(“Hello inspector”);
- }
- }
Сохраняем скрипт и видим в окне настройки компонента текст Hello inspector.
Теперь займемся созданием новых настроек для компонента Unit. Возвращаемся снова в метод OnInspectorGUI и для начала определим объект редактора как тип Unit.
- [CustomEditor(typeof(Unit))]
- public class UnitEditor : Editor {
- public override void OnInspectorGUI() {
- Unit unit = this.target as Unit;
- }
- }
Для этого воспользуемся преобразованием переменной target в тип Unit, это необходимо сделать, чтобы четко дать понять редактору, с каким объектом мы работаем, ведь изначально редактор не может знать, какой компонент к нему обращается, хоть мы это и указали в атрибуте CustomEditor. Далее возьмем переменную description объекта Unit и “нарисуем” для нее новый тип настроек, так чтобы теперь вмешалось больше текста чем раньше.
- [CustomEditor(typeof(Unit))]
- public class UnitEditor : Editor {
- public override void OnInspectorGUI() {
- Unit unit = this.target as Unit;
- unit.description = EditorGUILayout.TextArea(this.unit.description, GUILayout.Height(50));
- }
- }
Сохраняем скрипт и смотрим результат в инспекторе. Для примера в левой части изображения показан старый вид настройки переменной description, а новый вид справа.
Статический метод TextArea позволяет отображать простую переменную текста как целую область, в которой намного более удобно этот текст редактировать. Все необходимые методы для работы с инспектором в редакторе находятся в классе EditorGUI и EditorGUILayout.
Теперь перейдем к настройке переменных количества жизней, для этого воспользуемся еще одним новым методом Slider класса EditorGUILayout.
- public override void OnInspectorGUI() {
- Unit unit = this.target as Unit;
- unit.description = EditorGUILayout.TextArea(this.unit.description, GUILayout.Height(50));
- string text = “Мин. здоровья: ”;
- unit.minHealth = EditorGUILayout.Slider(text, unit.minHealth, 0, unit.maxHealth);
- text = “Мак. здоровья: “;
- unit.maxHealth = EditorGUILayout.Slider(text, unit.maxHealth, unit.minHealth, 1000);
- }
Раз теперь у нас есть доступ к настройкам редактора компонента, то можем указать небольшую зависимость: минимальный запас здоровья не может быть больше, чем максимальный, а максимальный запас здоровья не может быть меньше минимального.
Сохраняем и смотрим результат.
Далее поработаем с переключаемыми параметрами редактора и настройкой дальности атаки. По условию, если юнит может проводить атаки дальнего боя, то ему необходимо настроить эту саму дальность атаки. Для этого в компоненте Unit есть две переменные: boolean переменная, rangedUnit для обозначения, что объект является юнитом дальнего боя, и числовая переменная int rangeAttack для настройки самой дальности атаки.
Чтобы отобразить в редакторе эти две настройки, воспользуемся “групповым” переключателем. Этот переключатель не будет давать возможности изменить дальность атаки юниту, пока он является юнитом ближнего боя и наоборот.
public override void OnInspectorGUI() {
- Unit unit = this.target as Unit;
- unit.description = EditorGUILayout.TextArea(this.unit.description, GUILayout.Height(50));
- string text = “Мин. здоровья: ”;
- unit.minHealth = EditorGUILayout.Slider(text, unit.minHealth, 0, unit.maxHealth);
- text = “Мак. здоровья: “;
- unit.maxHealth = EditorGUILayout.Slider(text, unit.maxHealth, unit.minHealth, 1000);
- text = “Включить дальнобойность: “;
- unit.rangedUnit = EditorGUILayout.BeginToggleGroup(text, unit.rangedUnit);
- text = “Радиус атаки: “;
- unit.rangeAttack = EditorGUILayout.IntSlider(text, unit.rangeAttack, 100, 1000);
- EditorGUILayout.EndToggleGroup();
- }
Снова сохраняем скрипт и смотрим результат.
Метод BeginToggleGroup позволяет блокировать изменения любых переменных, находящихся в его области. Главное при работе с такими областями – не забывать их закрывать, в нашем случае после метода IntSlider, где настраивается дальность атаки юнита, метод EndToggleGroup закрывает область переключателя, тем самым не влияя на другие переменные в новом редакторе. Далее переходим к настройкам разного типа урона, для чего воспользуемся специальным редактором масок.
- public override void OnInspectorGUI() {
- Unit unit = this.target as Unit;
- unit.description = EditorGUILayout.TextArea(this.unit.description, GUILayout.Height(50));
- string text = “Мин. здоровья: ”;
- unit.minHealth = EditorGUILayout.Slider(text, unit.minHealth, 0, unit.maxHealth);
- text = “Мак. здоровья: “;
- unit.maxHealth = EditorGUILayout.Slider(text, unit.maxHealth, unit.minHealth, 1000);
- text = “Включить дальнобойность: “;
- unit.rangedUnit = EditorGUILayout.BeginToggleGroup(text, unit.rangedUnit);
- text = “Радиус атаки: “;
- unit.rangeAttack = EditorGUILayout.IntSlider(text, unit.rangeAttack, 100, 1000);
- EditorGUILayout.EndToggleGroup();
- text = “Тип атаки: “;
- unit.type = (AttackType)EditorGUILayout.EnumFlagsField(text, unit.type);
- }
Можно сохранить и посмотреть результат.
Теперь юниту можно выбрать сразу несколько типов атак. Метод EnumFlagsField позволяет задавать перечислению enum сразу несколько значений, именно по такому же принципу настраиваются камеры и их параметр Culling mask.
Осталось настроить всего две переменных класса Unit, это количество урона и иконку. Так как переменная damage имеет числовой тип float, то и редактор тоже будет использовать метод FloatField для дробных числовых переменных.
- public override void OnInspectorGUI() {
- Unit unit = this.target as Unit;
- unit.description = EditorGUILayout.TextArea(this.unit.description, GUILayout.Height(50));
- string text = “Мин. здоровья: ”;
- unit.minHealth = EditorGUILayout.Slider(text, unit.minHealth, 0, unit.maxHealth);
- text = “Мак. здоровья: “;
- unit.maxHealth = EditorGUILayout.Slider(text, unit.maxHealth, unit.minHealth, 1000);
- text = “Включить дальнобойность: “;
- unit.rangedUnit = EditorGUILayout.BeginToggleGroup(text, unit.rangedUnit);
- text = “Радиус атаки: “;
- unit.rangeAttack = EditorGUILayout.IntSlider(text, unit.rangeAttack, 100, 1000);
- EditorGUILayout.EndToggleGroup();
- text = “Тип атаки: “;
- unit.type = (AttackType)EditorGUILayout.EnumFlagsField(text, unit.type);
- text = “Кол-во урона: “;
- unit.damage = EditorGUILayout.FloatField(text, unit.damage);
- }
Все поля, которые мы настраивали ранее, имеют простейший тип, это float, int, string и boolean, иконка юнита имеет тип Sprite, который в свою очередь является Unity объектом. Для любых unity объектов есть специальный редактор полей, называется он ObjectField. Попробуем добавить поле иконки перед полем редактирования описания в самом верху метода OnInspectorGUI.
- public override void OnInspectorGUI() {
- Unit unit = this.target as Unit;
- unit.icon = EditorGUILayout.ObjectField(unit.icon, typeof(Sprite), false) as Sprite;
- unit.description = EditorGUILayout.TextArea(this.unit.description, GUILayout.Height(50));
- string text = “Мин. здоровья: ”;
- unit.minHealth = EditorGUILayout.Slider(text, unit.minHealth, 0, unit.maxHealth);
- text = “Мак. здоровья: “;
- unit.maxHealth = EditorGUILayout.Slider(text, unit.maxHealth, unit.minHealth, 1000);
- text = “Включить дальнобойность: “;
- unit.rangedUnit = EditorGUILayout.BeginToggleGroup(text, unit.rangedUnit);
- text = “Радиус атаки: “;
- unit.rangeAttack = EditorGUILayout.IntSlider(text, unit.rangeAttack, 100, 1000);
- EditorGUILayout.EndToggleGroup();
- text = “Тип атаки: “;
- unit.type = (AttackType)EditorGUILayout.EnumFlagsField(text, unit.type);
- text = “Кол-во урона: “;
- unit.damage = EditorGUILayout.FloatField(text, unit.damage);
- }
Первый параметр, который указывается в методе ObjectField – это сама переменная иконки. Далее необходимо указать тип объекта, в нашем случае это Sprite и последний boolean параметр “запрещает”, либо “разрешает” добавлять объекты со сцены.
Чтобы поле иконки выглядело лучше, можно указать минимальные размеры самого поля через GUILayout.Width, GUILayout.Height и добавить выравнивание по горизонтали вместе с полем описания.
public override void OnInspectorGUI() {- Unit unit = this.target as Unit;
- GUILayout.BeginHorizontal();
- unit.icon = EditorGUILayout.ObjectField(unit.icon, typeof(Sprite), false, GUILayout.Width(50), GUILayout.Height(50)) as Sprite;
- unit.description = EditorGUILayout.TextArea(this.unit.description, GUILayout.Height(50));
- GUILayout.EndHorizontal();
- string text = “Мин. здоровья: ”;
- unit.minHealth = EditorGUILayout.Slider(text, unit.minHealth, 0, unit.maxHealth);
- text = “Мак. здоровья: “;
- unit.maxHealth = EditorGUILayout.Slider(text, unit.maxHealth, unit.minHealth, 1000);
- text = “Включить дальнобойность: “;
- unit.rangedUnit = EditorGUILayout.BeginToggleGroup(text, unit.rangedUnit);
- text = “Радиус атаки: “;
- unit.rangeAttack = EditorGUILayout.IntSlider(text, unit.rangeAttack, 100, 1000);
- EditorGUILayout.EndToggleGroup();
- text = “Тип атаки: “;
- unit.type = (AttackType)EditorGUILayout.EnumFlagsField(text, unit.type);
- text = “Кол-во урона: “;
- unit.damage = EditorGUILayout.FloatField(text, unit.damage);
- }
Ну вот так на много лучше и удобней. Таким образом можно настраивать любые поля и свойства компонента, достаточно получить доступ к редактору объекта.
Заключение
Расширять редактор можно для любых unity объектов и компонентов, и даже не обязательно для MonoBehaviour скриптов, но и для уже существующих объектов в unity: Transform, Collider и другие. К примеру, для удобства работы с Transform’ом в 2D редакторе можно его просто улучшить также, как сделали этот с компонентом Unit.
Вы в уроке не показали как разбивать на области как в третьем скриншоте. Кроме этого – вы не отвечаете на почту 🙁
Отвечаем на почту, но не мгновенно, а в течении пары дней. Пожалуйста, дождитесь ответа.
Приветствую.. подскажите, а как обновить инспектор? Я сделал кнопку для рандомизации AnimationCurve, только поле остается пустым в инспекторе, а при нажатии на поле открывается сама кривая(т.е. она есть, но её не видно в миниатюре). Проблема решается путем переключения на другой объект сцены и обратно, но хотелось бы ,чтобы изменения были видны в инспекторе сразу
Проконсультируйтесь пожалуйста через my.first.unity.help@gmail.com
Ето все интересно но у меня проблема в том что ети компоненты скорости жизни и так далее не появляются .
Проконсультируйтесь пожалуйста индивидуально через поддержку сайта: support@unity3dschool.com