[Перевод] Персонализация IMGUI и редактора Unity. Часть первая

С момента выхода новой системы Unity UI прошло больше года, поэтому Ричард Файн решил написать о ее предшественнице — IMGUI.
На первый взгляд, это совсем нелогично. Зачем писать об устаревшей системе UI, если уже давно вышла новая? Что ж, новая система действительно предлагает широкие возможности настройки игровых интерфейсов, но если вы хотите добавить в редактор новые инструменты и функции, вам наверняка пригодится IMGUI.

59a333b26acd4d06a62c3f0a21e28c77.jpg
Приступая к работе

Итак, первый вопрос: что значит IMGUI? IMGUI расшифровывается как Immediate Mode GUI. Об этом стоит рассказать поподробнее. Существует 2 основных подхода к системам GUI: прямой (Immediate Mode GUI) и сохраненный (Retained Mode GUI).

При использовании сохраненного режима система запоминает информацию об интерфейсе. Разработчик добавляет различные элементы: метки, кнопки, слайдеры, текстовые поля, после этого данные сохраняются и используются для определения внешнего вида экрана, обработки событий и т. п. Когда вы изменяете текст на метке или перемещаете кнопку, вы фактически изменяете сохраненные в системе данные и создаете новое состояние системы. Когда вы взаимодействуете с интерфейсом, система запоминает ваши действия, но больше ничего не делает без дополнительного запроса. Unity UI работает именно в этом режиме. Вы создаете компоненты вроде UI.Labels или UI.Buttons, настраиваете их, а система берет на себя всё остальное.

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

В качестве отступления вы можете посмотреть отличный ролик Кейси Муратори, в котором он рассматривает основные принципы и преимущества прямого режима.

Обработка событий

У активного интерфейса IMGUI всегда существует обрабатываемое событие, например «нажатие кнопки мыши пользователем» или «необходимость перерисовки интерфейса». Вид текущего события можно узнать из значения Event.current.type.
Представим, что в окно интерфейса нужно добавить набор кнопок. В таком случае для обработки каждого события необходимо написать отдельный код. Схематически это можно изобразить так:

a289275bfd914ef4909adc81075ef5e1.png

Написание отдельных функций для каждого события занимает много времени, но по своей структуре они очень похожи. На каждом этапе мы будем выполнять действия с одними и теми же элементами (кнопка 1, 2 или 3). Конкретное действие зависит от события, но структура остается неизменной. Это значит, что вместо первого алгоритма можно сделать единую функцию:

eaf142fd0eec4a92983b2df52bb15496.png

Единая функция OnGUI вызывает функции библиотеки (например GUI.Button), которые выполняют различные действия в зависимости от вида обрабатываемого события. Ничего сложного!

Чаще всего используются события следующих пяти видов:

a464375fc7c44af1814d7f56575f766e.png

Ознакомиться с полным списком видов событий можно в документации EventType.
Каким образом стандартный элемент управления GUI.Button реагирует на события?

67f2f41c0a594b9f876ee192f87714d2.png

На практике всё не так просто. Например, кнопки должны реагировать на события, запускаемые нажатием клавиш. Кроме того, нужно добавить код, останавливающий ответ на MouseUp от любых кнопок, кроме той, на которую был наведен курсор в момент нажатия. В любом случае, если вызов GUI.Button происходит в одном и том же месте кода и сохраняет одинаковые координаты и содержание, фрагменты кода будут сообща определять поведение кнопки.

Для объединенной модели поведения интерфейса при различных событиях в IMGUI используется идентификатор управляющего элемента — сontrol ID. С помощью этого идентификатора можно выполнять единообразные обращения к элементам интерфейса при любом событии. Control ID присваивается каждому элементу интерфейса с нетривиальным интерактивным поведением. Присвоение выполняется в зависимости от порядка запросов, поэтому, если функции интерфейса будут вызваны в одинаковом порядке из разных событий, им будут присвоены одинаковые идентификаторы, а события будут синхронизированы.

Создание элементов интерфейса

Классы GUI и EditorGUI предоставляют библиотеку стандартных элементов интерфейса Unity. Их можно использовать для создания собственных классов Editor, EditorWindow или PropertyDrawer.

Начинающие разработчики часто пренебрегают классом GUI, предпочитая использовать EditorGUI. На самом деле элементы обоих классов одинаково полезны и могут совместно использоваться для расширения функционала редактора. Главное различие в том, что элементы EditorGUI нельзя использовать в игровых интерфейсах, так как они являются частью редактора, в то время как элементы GUI входят в состав самого движка.

Но как быть, если вам недостаточно ресурсов стандартных библиотек?

Давайте посмотрим, как выглядит уникальный элемент пользовательского интерфейса на примере этого демо-приложения (для просмотра требуется браузер с поддержкой WebGL, например последняя версия Firefox).

Цветные полосы прокрутки в демо связаны с плавающими переменными, имеющими значение от 0 до 1. Их можно использовать в Unity Inspector для отображения состояния отдельных частей игрового объекта, например космического корабля (предположим, значение 1 — «повреждения отсутствуют», а значение 0 — «критические повреждения»). Цвет полос изменяется в зависимости от значения, чтобы пользователь мог быстро разобраться в ситуации. Подобные элементы интерфейса легко создаются с помощью IMGUI.

Для начала необходимо решить, как будет выглядеть сигнатура функции. Нашему элементу понадобятся 3 составляющие, чтобы полностью охватить различные типы событий:

• Rect, определяющий координаты отрисовки элемента и считывания нажатий мыши;
• float, плавающая переменная, которую представляет цветная полоса;
• GUIStyle, содержащий необходимую информацию об отступах, шрифтах, текстурах и т. п. В нашем случае это будет текстура, используемая при отрисовке полосы. Далее мы рассмотрим этот параметр более детально.

Функция должна будет возвращать новое значение плавающего числа, установленное после перемещения ползунка. Это имеет смысл для событий, связанных с мышью, но не для событий перерисовки элемента, поэтому по умолчанию функция будет возвращать значение, переданное при вызове. В таком случае вызовы вида «value = MyCustomSlider (… value…)» не зависят от события, а значение переменной остается без изменений.

В итоге сигнатура функции принимает такой вид:

d78ebfb48cf742818b2188eaf0cce0d1.png

Приступим к реализации функции. Прежде всего нам нужно получить control ID, который будет использоваться при реакции на события, связанные с мышью. При этом, даже если текущее событие нас не интересует, для него все равно нужно запросить идентификатор, чтобы он не был присвоен другому элементу в контексте этого события, так как IMGUI присваивает идентификаторы в порядке получения запросов. Во избежание проблем с использованием разных идентификаторов для одного и того же элемента в контексте разных событий необходимо запрашивать их для всех типов событий (или не запрашивать ни для одного, если элемент не интерактивен, но это не наш случай).

96ffd4e8b37a47c28810c0a148b8ec7d.png

Параметр FocusType.Passive определяет роль элемента в навигации с помощью клавиатуры. Passive означает, что наша полоса не реагирует на ввод с клавиатуры. В противном случае используются Native или Keyboard. Более подробную информацию о параметре FocusType можно найти в соответствующей документации.

Теперь мы воспользуемся оператором ветвления, чтобы разделить код, необходимый для различных типов событий. Вместо того чтобы использовать Event.current.type напрямую, мы применим Event.current.GetTypeForControl (), присвоив ему control ID. Таким образом мы отфильтруем типы событий, чтобы, например, событие клавиатуры не ссылалось на неправильный элемент. Тем не менее такая фильтрация не универсальна, поэтому позднее нам придется добавить дополнительные проверки.

37c732d830d74c6faf6d21d655ace8d3.png

Итак, можно приступать к реализации поведений для разных типов событий. Начнем с отрисовки:

5b87b01b618442109dee6ce39dfbb620.png

На этом можно было бы остановиться и получить готовый элемент визуализации плавающих значений от 0 до 1 в режиме «только для чтения». Но давайте продолжим и сделаем его интерактивным.

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

Для этого мы воспользуемся переменной GUIUtility.hotControl, содержащей control ID, который в данный момент взаимодействует с мышью. IMGUI использует ее в функции GetTypeForControl (). Если она не равна нулю, события мыши отфильтровываются (при условии, что передаваемый control ID не совпадает со значением hotControl).
Установить и сбросить hotControl очень просто:

a7cd4dc9d6c5486493a5f05f6d55c6e9.png

Обратите внимание: если любой другой элемент является hotControl (например, GUIUtility.hotControl не равен нулю и содержит другой идентификатор), GetTypeForControl () не станет возвращать mouseUp/mouseDown, а просто проигнорирует эти события.
Теперь нужно создать код для изменения плавающей переменной в то время, пока зажата кнопка мыши. Проще всего закрыть ветвление и указать, что любое событие, связанное с мышью и происходящее, пока идентификатор элемента находится в hotControl (т. е. пока происходит перетаскивание и кнопка мыши еще не была отпущена), должно изменять значение переменной:

6d77889dcebd4f048baedb57ac4d8fbb.png

Два последних шага — установка GUI.changed и вызов Event.current.Use () — особенно важны для корректного взаимодействия внутри множества элементов IMGUI. Установка значения TRUE для GUI.changed позволяет использовать функции EditorGUI.BeginChangeCheck () и EditorGUI.EndChangeCheck () для проверки изменения значения переменной действиями пользователя. Значение FALSE лучше не использовать, чтобы не пропустить предыдущие изменения.

Наконец, наша функция должна вернуть новое значение плавающей переменной. Чаще всего оно будет отличаться от предыдущего значения:

0d93c6b296214ab6b4be73781f631b95.png

MyCustomSlider готов. У нас получился простой функциональный элемент IMGUI, который можно использовать в пользовательских редакторах, PropertyDrawers, EditorWindows и т. д. Но это еще не всё. Далее мы поговорим о том, как можно расширить его функционал, например добавить возможность мультиредактирования.

© Habrahabr.ru