[EXP] Unity 3D кастомный Inspector для MonoBehaviour. UnityEditor Scripting P.1
Вступление
В проекте, над которым я сейчас работаю встала задачка - нужно создавать, хранить и балансировать карточки, дающие игроку бафы/дебафы. К тому же, есть мысль создавать отдельные наборы карточек (по случаю праздников, например) и заменять их, когда душе угодно. Предвещая неудобства и технические запутанности при пользовании или масштабировании, я принял решение "очеловечить" процесс, создав кастомный инструмент с использованием ScriptableObject, работа с которым ведется в инспекторе.
Собственно, это и сподвигло меня на написание данной статьи, в которой я затрону создание кастомного инспектора с классом MonoBehaviour, отображение/сохранение публичных и сериализованных переменных, а также некоторые классы и методы, связанные с визуальным представлением компонентов инспектора.
Хочется поделиться опытом, а может кто-то еще что-нибудь интересного подскажет. Тема весьма запутанная, но результат иногда - отвал башки, так что запаситесь терпением и решите нужно ли это вам вообще)
Зачем кастомный инспектор?
Окно инспектора является одной из наиболее базированных вещей в Unity, ведь благодаря ему мы можем лицезреть и настраивать присущие конкретному объекту компоненты или же другие составляющие движка: модели, звуки, текстуры, материалы и т.д.
В общем, инспектором мы пользуемся часто. А по сути своей это то, как движок технически и визуально представляет нам классы типа MonoBehaviour и ScriptableObject.
Как правило, Unity разработчику достаточно обычного инспектора. Но иногда его все же стоит сделать кастомным в виду некоторых обстоятельств:
• Скрипт требует реализации дополнительного функционала;
• Скрипт планируется ре-использовать и уже есть основание полагать, что он станет полноценным тулом студии;
• Скрипт уже стал очень многофункциональным и запутанным, а в команде есть люди, которых нельзя пугать кодом и неоднозначными переменными;
• Просто есть желание слегка по-Марксистки насладиться результатами своего труда. Пользоваться скриптом, который красив и снаружи, и внутри;
• И еще миллиард причин, почему инспектор стоит сделать кастомным!..
Но инспектор - лишь верхушка айсберга. Ко всему прочему, движок юнити предоставляет возможность свободно использовать API (UnityEngine, UnityEditor), тем самым открывая доступ к широкому функционалу разработки собственных полноценных инструментов для управления работы движка, ресурсов и способов визуального представления. Например, можно создать тул, который будет искать текстуры в папке проекта и сортировать их на "POT"/"NPOT", при этом будет открываться в отдельном окне через вкладку Tools на панели управления. Надо - открыл, надо - закрыл.
Как создать кастомный инспектор
1. Как я уже говорил, инспектор взаимодействует с классами типа MonoBehaviour и ScriptableObject. Поэтому нам нужен класс, который наследуется от них. (В данной статье речь будет идти о MonoBehaviour)
Вот тестовый класс Player, над которым будем ставить опыты:
2. Для того, чтобы сделать инспектор кастомным, нам нужно создать дополнительный класс PlayerEditor, который:
• Наследуется от Editor;
• Использует библиотеку UnityEditor;
• Имеет атрибут [CustomEditor(typeof(class Name))].
Все что связанно с PlayerEditor следует заключить в
Кто-то создает класс кастомного Editor в отдельном файле и кладет в папку "Editor", но для маленьких скриптов я предпочитаю использовать оба класса в одном файле.
Все атрибуты есть на офф. сайте с документацией.
В данном случае используются два (оба относятся к UnityEditor API):
• CustomEditor (обязательный) - сообщает редактору юнити, что это кастомный Editor и для какого типа он используется;
• CanEditMultipleObjects - позволяет редактировать сразу несколько объектов на сцене.
3. Теперь в классе Editor объявим переменные типа Player, SerializedObject и SerializedProperty.
Player - будет экземпляр нашего класса.
SerializedObject - класс UnityEditor, который используется для редактирования сериализованных переменных. (Сериализует экземпляр нашего класса?)
SerializedProperty - класс UnityEditor, который используется для редактирования сериализованных переменных. (Создает ссылку на сериализованную переменную сериализованного экземпляра нашего класса?)
Грубо говоря, чтобы менять приватные переменные с атрибутом [SerializeField], иметь возможность использовать отмену действия "ctrl+z" и сохранять изменения физически на диск, нам будут нужны SerializedObject и SerializedProperty.
4. Далее нужно переопределить метод OnInspectorGUI(), в котором мы будем отрисовывать визуальный стиль и реализуем сохранение значений переменных.
Тут есть важный нюанс - необходимо самостоятельно оповещать Unity, когда произошли изменения как в публичных, так и в приватных (сериализованных) переменных.
В MonoBehaviour это делается при помощи:
• EditorUtility.SetDirty(_playerScript) - для публичных;
• _serializedPlayerScript.Update() - для сериализованных;
• _serializedPlayerScript.ApplyModifiedProperties() - для сериализованных.
5. Наконец-то можно перейти к отрисовке инспектора...
Для отрисовки мы можем воспользоваться следующими классами:
• EditorGUILayout - автоматически компонуемый класс UnityEditor;
• EditorGUI - класс UnityEditor, настраиваемый вручную;
• GUILayout - автоматически компануемый класс UnityEngine;
• GUI - класс UnityEngine для Unity GUI;
• GUIContent - класс UnityEngine, определяет что отрисовывать в качестве контента;
• GUIStyle - необязательный класс UnityEditor для настройки кастомного стиля конкретного элемента (позиция, шрифт, размер шрифта и т.д.)
В каждом классе достаточно интересных методов и многие довольно схожи, чтобы их спутать. Тут просто стоит зайти в официальную документацию.
Для отрисовки пишем методы последовательно друг за другом. Начнем с отображения текста:
EditorGUILayout.Space() или EditorGUILayout.Space(float width) - создает отступ;
EditorGUILayout.LabelField("text", new GUIStyle(GUI.skin.label) { alignment = TextAnchor.MiddleCenter, fontStyle = FontStyle.Bold, fontSize = 16 }) - создает заголовок и определяет для него новый стиль с позиционированием текста по центру, жирным шрифтом и размером 16;
EditorGUILayout.LabelField("", GUI.skin.horizontalSlider) - располагает заголовок с пустым текстом и стилем фона от слайдера (получается разделительная полоска).
EditorGUILayout.HelpBox("Напоминание, Подсказка, Ошибка", MessageType.None) - еще один способ представления текста пользователю. MessageType.Info - появится желтый знак. Также есть MessageType.Warning и MessageType.Error.
Теперь добавим отображение переменных:
_playerScript.a = EditorGUILayout.IntField(new GUIContent("Переменная А", "Переменная типа int"), _playerScript.a) - указывает, что это поле для записи и отображения int переменных (IntField), в качестве контента будет текст с названием и подсказкой при наведении мыши. В качестве значения переменная a экземпляра _playerScript. Обратите внимание, что поле должно соответствовать типу переменной (IntField, TextField, FloatField).
_playerScript._moveSpeed = EditorGUILayout.Slider(new GUIContent("Скорость игрока", "Настраиваемая скорость игрока"), _playerScript._moveSpeed, 1, 10) - тут вместо обычного поля ввода будет слайдер с значениями от 1 до 10. Также используется название и подсказка.
_serializedHealth.floatValue = EditorGUILayout.FloatField(new GUIContent("Здоровье", "Уровень здоровья игрока"), _serializedHealth.floatValue) - теперь подошли к сериализованному параметру. Обратите внимание, что обращаясь нужно указать тип переменной после точки _serializedHealth.floatValue.
Добавим еще пару приколов (картинку и выпадающий список):
showVariables = EditorGUILayout.Toggle(new GUIContent("Показать что-то", "Нажмите, чтобы узреть какие-то подкапотные переменные."), showVariables) - эта строка, которая отображает Toggle для showVariables (переменная showVariables объявлена вне метода, она принадлежит классу PlayerEditor).
Теперь в инспекторе появилась галочка. И если значение true, то переменные a, b и c будут отрисовываться, иначе - нет.
GUI.DrawTexture(rect, cover, ScaleMode.ScaleToFit) - отобразит текстуру/картинку, размеров rect, растянув до краев.
Также, можно добавить кнопки:
if (GUILayout.Button("Восстановить здоровье", new GUIStyle(GUI.skin.button) { alignment = TextAnchor.MiddleCenter, fixedHeight = 30 })) - строка отображает кнопку с кастомным стилем и проверяет была ли она нажата.
GUILayout.BeginHorizontal() и GUILayout.EndHorizontal() - весь контент, располагающийся между этими двумя методами, будет являться одной горизонтальной группой и компоноваться на одной линии друг за другом
Заключение
Вот такой кастомный инспектор получился) Надеюсь, вышло не слишком запутанно - старался максимально по-человечески писать и это все еще поверхностная информация - нора-то глубока...
Стоит ли игра свеч, каждый решает сам. Мне же кажется, это весьма полезно, если подходить к этому с профессиональной точки зрения - если скрипт весьма многофункциональный, писать доку и делать для него опрятную человеческую обертку, чтобы будущий вы или ваши коллеги могли в этом разобраться по щелчку пальца - ну,.. это круто. Стоит только уделить этому недельку-две и ваш внутренний геймдизайнер возликует.
В будущем планирую написать статью про свой тул на ScriptableObjects для создания и хранения карточек бафа/дебафа и про кастомный редактор малой кровью. Так что, если статья была вам полезна, дайте знать и подписывайтесь. Может кто-то поделится своим опытом разработки кастомных инструментов или редакторов, буду рад.
На этом все, спасибо) Берегите себя.