С момента выхода новой системы Unity UI прошло больше года, поэтому Ричард Файн решил написать о ее предшественнице – IMGUI.
На первый взгляд, это совсем нелогично. Зачем писать об устаревшей системе UI, если уже давно вышла новая? Что ж, новая система действительно предлагает широкие возможности настройки игровых интерфейсов, но если вы хотите добавить в редактор новые инструменты и функции, вам наверняка пригодится IMGUI.
Приступая к работе
Итак, первый вопрос: что значит IMGUI? IMGUI расшифровывается как Immediate Mode GUI. Об этом стоит рассказать поподробнее. Существует 2 основных подхода к системам GUI: прямой (Immediate Mode GUI) и сохраненный (Retained Mode GUI).
При использовании сохраненного режима система запоминает информацию об интерфейсе. Разработчик добавляет различные элементы: метки, кнопки, слайдеры, текстовые поля, после этого данные сохраняются и используются для определения внешнего вида экрана, обработки событий и т. п. Когда вы изменяете текст на метке или перемещаете кнопку, вы фактически изменяете сохраненные в системе данные и создаете новое состояние системы. Когда вы взаимодействуете с интерфейсом, система запоминает ваши действия, но больше ничего не делает без дополнительного запроса. Unity UI работает именно в этом режиме. Вы создаете компоненты вроде UI.Labels или UI.Buttons, настраиваете их, а система берет на себя всё остальное.
При использовании прямого режима система не запоминает информацию об интерфейсе, а вместо этого постоянно запрашивает и уточняет информацию о его элементах. Каждый из них должен быть задан с помощью вызова функции. Реакция на любое действие пользователя возвращается немедленно, без дополнительного запроса. Такая система неэффективна для пользовательского интерфейса игры и неудобна для художников из-за зависимости от кода. В то же время она хорошо подходит для редактора Unity, чей интерфейс и без того зависит от кода и не изменяется в реальном времени в отличие от игры. Прямой режим также позволяет изменять отображаемые инструменты в соответствии с новым состоянием системы.
В качестве отступления вы можете посмотреть отличный ролик Кейси Муратори, в котором он рассматривает основные принципы и преимущества прямого режима.
Обработка событий
У активного интерфейса IMGUI всегда существует обрабатываемое событие, например «нажатие кнопки мыши пользователем» или «необходимость перерисовки интерфейса». Вид текущего события можно узнать из значения Event.current.type.
Представим, что в окно интерфейса нужно добавить набор кнопок. В таком случае для обработки каждого события необходимо написать отдельный код. Схематически это можно изобразить так:
Написание отдельных функций для каждого события занимает много времени, но по своей структуре они очень похожи. На каждом этапе мы будем выполнять действия с одними и теми же элементами (кнопка 1, 2 или 3). Конкретное действие зависит от события, но структура остается неизменной. Это значит, что вместо первого алгоритма можно сделать единую функцию:
Единая функция OnGUI вызывает функции библиотеки (например GUI.Button), которые выполняют различные действия в зависимости от вида обрабатываемого события. Ничего сложного!
Чаще всего используются события следующих пяти видов:
EventType.MouseDown — Пользователь нажал кнопку мыши.
EventType.MouseUp — Пользователь нажал кнопку мыши.
EventType.KeyDown — Пользователь нажал клавишу.
EventType.KeyUp — Пользователь нажал клавишу.
EventType.Repaint — Необходимо перерисовать интерфейс
Ознакомиться с полным списком видов событий можно в документации EventType.
Каким образом стандартный элемент управления GUI.Button реагирует на события?
EventType.Repaint — Перерисовка кнопки в указанном прямоугольнике
EventType.MouseDown — Если координаты курсора совпадают с областью кнопки, установить флажок «кнопка нажата» и запустить перерисовку кнопки в нажатом виде.
EventType.MouseUp — Снять флажок «кнопка нажата» и запустить перерисовку кнопки в отпущенном виде. Если курсор находится в области кнопки, вернуть TRUE (кнопка была нажата и отпущена).
На практике всё не так просто. Например, кнопки должны реагировать на события, запускаемые нажатием клавиш. Кроме того, нужно добавить код, останавливающий ответ на 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 …)” не зависят от события, а значение переменной остается без изменений.
В итоге сигнатура функции принимает такой вид:
Приступим к реализации функции. Прежде всего нам нужно получить control ID, который будет использоваться при реакции на события, связанные с мышью. При этом, даже если текущее событие нас не интересует, для него все равно нужно запросить идентификатор, чтобы он не был присвоен другому элементу в контексте этого события, так как IMGUI присваивает идентификаторы в порядке получения запросов. Во избежание проблем с использованием разных идентификаторов для одного и того же элемента в контексте разных событий необходимо запрашивать их для всех типов событий (или не запрашивать ни для одного, если элемент не интерактивен, но это не наш случай).
Параметр FocusType.Passive определяет роль элемента в навигации с помощью клавиатуры. Passive означает, что наша полоса не реагирует на ввод с клавиатуры. В противном случае используются Native или Keyboard. Более подробную информацию о параметре FocusType можно найти в соответствующей документации.
Теперь мы воспользуемся оператором ветвления, чтобы разделить код, необходимый для различных типов событий. Вместо того чтобы использовать Event.current.type напрямую, мы применим Event.current.GetTypeForControl(), присвоив ему control ID. Таким образом мы отфильтруем типы событий, чтобы, например, событие клавиатуры не ссылалось на неправильный элемент. Тем не менее такая фильтрация не универсальна, поэтому позднее нам придется добавить дополнительные проверки.
Итак, можно приступать к реализации поведений для разных типов событий. Начнем с отрисовки:
На этом можно было бы остановиться и получить готовый элемент визуализации плавающих значений от 0 до 1 в режиме «только для чтения». Но давайте продолжим и сделаем его интерактивным.
Чтобы сделать использование элемента удобным, после захвата ползунка нажатием кнопки следует учитывать только горизонтальное движение мыши независимо от того, остается ли курсор в пределах полосы. При этом нужно сделать следующую вещь: пока пользователь не отпустит кнопку мыши, ее движение не должно влиять на другие элементы интерфейса.
Для этого мы воспользуемся переменной GUIUtility.hotControl, содержащей control ID, который в данный момент взаимодействует с мышью. IMGUI использует ее в функции GetTypeForControl(). Если она не равна нулю, события мыши отфильтровываются (при условии, что передаваемый control ID не совпадает со значением hotControl).
Установить и сбросить hotControl очень просто:
Обратите внимание: если любой другой элемент является hotControl (например, GUIUtility.hotControl не равен нулю и содержит другой идентификатор), GetTypeForControl() не станет возвращать mouseUp/mouseDown, а просто проигнорирует эти события.
Теперь нужно создать код для изменения плавающей переменной в то время, пока зажата кнопка мыши. Проще всего закрыть ветвление и указать, что любое событие, связанное с мышью и происходящее, пока идентификатор элемента находится в hotControl (т. е. пока происходит перетаскивание и кнопка мыши еще не была отпущена), должно изменять значение переменной:
Два последних шага – установка GUI.changed и вызов Event.current.Use() – особенно важны для корректного взаимодействия внутри множества элементов IMGUI. Установка значения TRUE для GUI.changed позволяет использовать функции EditorGUI.BeginChangeCheck() и EditorGUI.EndChangeCheck() для проверки изменения значения переменной действиями пользователя. Значение FALSE лучше не использовать, чтобы не пропустить предыдущие изменения.
Наконец, наша функция должна вернуть новое значение плавающей переменной. Чаще всего оно будет отличаться от предыдущего значения:
MyCustomSlider готов. У нас получился простой функциональный элемент IMGUI, который можно использовать в пользовательских редакторах, PropertyDrawers, EditorWindows и т. д. Но это еще не всё. Далее мы поговорим о том, как можно расширить его функционал, например добавить возможность мультиредактирования.
На первый взгляд, это совсем нелогично. Зачем писать об устаревшей системе UI, если уже давно вышла новая? Что ж, новая система действительно предлагает широкие возможности настройки игровых интерфейсов, но если вы хотите добавить в редактор новые инструменты и функции, вам наверняка пригодится IMGUI.
Приступая к работе
Итак, первый вопрос: что значит IMGUI? IMGUI расшифровывается как Immediate Mode GUI. Об этом стоит рассказать поподробнее. Существует 2 основных подхода к системам GUI: прямой (Immediate Mode GUI) и сохраненный (Retained Mode GUI).
При использовании сохраненного режима система запоминает информацию об интерфейсе. Разработчик добавляет различные элементы: метки, кнопки, слайдеры, текстовые поля, после этого данные сохраняются и используются для определения внешнего вида экрана, обработки событий и т. п. Когда вы изменяете текст на метке или перемещаете кнопку, вы фактически изменяете сохраненные в системе данные и создаете новое состояние системы. Когда вы взаимодействуете с интерфейсом, система запоминает ваши действия, но больше ничего не делает без дополнительного запроса. Unity UI работает именно в этом режиме. Вы создаете компоненты вроде UI.Labels или UI.Buttons, настраиваете их, а система берет на себя всё остальное.
При использовании прямого режима система не запоминает информацию об интерфейсе, а вместо этого постоянно запрашивает и уточняет информацию о его элементах. Каждый из них должен быть задан с помощью вызова функции. Реакция на любое действие пользователя возвращается немедленно, без дополнительного запроса. Такая система неэффективна для пользовательского интерфейса игры и неудобна для художников из-за зависимости от кода. В то же время она хорошо подходит для редактора Unity, чей интерфейс и без того зависит от кода и не изменяется в реальном времени в отличие от игры. Прямой режим также позволяет изменять отображаемые инструменты в соответствии с новым состоянием системы.
В качестве отступления вы можете посмотреть отличный ролик Кейси Муратори, в котором он рассматривает основные принципы и преимущества прямого режима.
Обработка событий
У активного интерфейса IMGUI всегда существует обрабатываемое событие, например «нажатие кнопки мыши пользователем» или «необходимость перерисовки интерфейса». Вид текущего события можно узнать из значения Event.current.type.
Представим, что в окно интерфейса нужно добавить набор кнопок. В таком случае для обработки каждого события необходимо написать отдельный код. Схематически это можно изобразить так:
Написание отдельных функций для каждого события занимает много времени, но по своей структуре они очень похожи. На каждом этапе мы будем выполнять действия с одними и теми же элементами (кнопка 1, 2 или 3). Конкретное действие зависит от события, но структура остается неизменной. Это значит, что вместо первого алгоритма можно сделать единую функцию:
Единая функция OnGUI вызывает функции библиотеки (например GUI.Button), которые выполняют различные действия в зависимости от вида обрабатываемого события. Ничего сложного!
Чаще всего используются события следующих пяти видов:
EventType.MouseDown — Пользователь нажал кнопку мыши.
EventType.MouseUp — Пользователь нажал кнопку мыши.
EventType.KeyDown — Пользователь нажал клавишу.
EventType.KeyUp — Пользователь нажал клавишу.
EventType.Repaint — Необходимо перерисовать интерфейс
Ознакомиться с полным списком видов событий можно в документации EventType.
Каким образом стандартный элемент управления GUI.Button реагирует на события?
EventType.Repaint — Перерисовка кнопки в указанном прямоугольнике
EventType.MouseDown — Если координаты курсора совпадают с областью кнопки, установить флажок «кнопка нажата» и запустить перерисовку кнопки в нажатом виде.
EventType.MouseUp — Снять флажок «кнопка нажата» и запустить перерисовку кнопки в отпущенном виде. Если курсор находится в области кнопки, вернуть TRUE (кнопка была нажата и отпущена).
На практике всё не так просто. Например, кнопки должны реагировать на события, запускаемые нажатием клавиш. Кроме того, нужно добавить код, останавливающий ответ на 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 …)” не зависят от события, а значение переменной остается без изменений.
В итоге сигнатура функции принимает такой вид:
public static float MyCustomSlider(Rect controlRect, float value, GUIStyle style)
Приступим к реализации функции. Прежде всего нам нужно получить control ID, который будет использоваться при реакции на события, связанные с мышью. При этом, даже если текущее событие нас не интересует, для него все равно нужно запросить идентификатор, чтобы он не был присвоен другому элементу в контексте этого события, так как IMGUI присваивает идентификаторы в порядке получения запросов. Во избежание проблем с использованием разных идентификаторов для одного и того же элемента в контексте разных событий необходимо запрашивать их для всех типов событий (или не запрашивать ни для одного, если элемент не интерактивен, но это не наш случай).
{
int controlID = GUIUtility.GetControlID (FocusType.Passive);
Параметр FocusType.Passive определяет роль элемента в навигации с помощью клавиатуры. Passive означает, что наша полоса не реагирует на ввод с клавиатуры. В противном случае используются Native или Keyboard. Более подробную информацию о параметре FocusType можно найти в соответствующей документации.
Теперь мы воспользуемся оператором ветвления, чтобы разделить код, необходимый для различных типов событий. Вместо того чтобы использовать Event.current.type напрямую, мы применим Event.current.GetTypeForControl(), присвоив ему control ID. Таким образом мы отфильтруем типы событий, чтобы, например, событие клавиатуры не ссылалось на неправильный элемент. Тем не менее такая фильтрация не универсальна, поэтому позднее нам придется добавить дополнительные проверки.
switch (Event.current.GetTypeForControl(controlID))
{
Итак, можно приступать к реализации поведений для разных типов событий. Начнем с отрисовки:
case EventType.Repaint:
{
// Work out the width of the bar in pixels by lerping
int pixelWidth = (int)Mathf.Lerp (1f, controlRect.width, value);
// Build up the rectangle that the bar will cover
// by copying the whole control rect, and then setting the width
Rect targetRect = new Rect (controlRect){ width = pixelWidth };
// Tint whatever we draw to be red/green depending on value
GUI.color = Color.Lerp (Color.red, Color.green, value);
// Draw the texture from the GUIStyle, applying the tint
GUI.DrawTexture (targetRect, style.normal.background);
// Reset the tint back to white, i.e. untinted
GUI.color = Color.white;
break;
}
На этом можно было бы остановиться и получить готовый элемент визуализации плавающих значений от 0 до 1 в режиме «только для чтения». Но давайте продолжим и сделаем его интерактивным.
Чтобы сделать использование элемента удобным, после захвата ползунка нажатием кнопки следует учитывать только горизонтальное движение мыши независимо от того, остается ли курсор в пределах полосы. При этом нужно сделать следующую вещь: пока пользователь не отпустит кнопку мыши, ее движение не должно влиять на другие элементы интерфейса.
Для этого мы воспользуемся переменной GUIUtility.hotControl, содержащей control ID, который в данный момент взаимодействует с мышью. IMGUI использует ее в функции GetTypeForControl(). Если она не равна нулю, события мыши отфильтровываются (при условии, что передаваемый control ID не совпадает со значением hotControl).
Установить и сбросить hotControl очень просто:
case EventType.MouseDown:
{
// If the click is actually on us...
if (controlRect.Contains (Event.current.mousePosition)
// ...and the click is with the left mouse button (button 0)...
&& Event.current.button == 0)
// ...then capture the mouse by setting the hotControl.
GUIUtility.hotControl = controlID;
break;
}
case EventType.MouseUp:
{
// If we were the hotControl, we aren't any more.
if (GUIUtility.hotControl == controlID)
GUIUtility.hotControl = 0;
break;
}
Обратите внимание: если любой другой элемент является hotControl (например, GUIUtility.hotControl не равен нулю и содержит другой идентификатор), GetTypeForControl() не станет возвращать mouseUp/mouseDown, а просто проигнорирует эти события.
Теперь нужно создать код для изменения плавающей переменной в то время, пока зажата кнопка мыши. Проще всего закрыть ветвление и указать, что любое событие, связанное с мышью и происходящее, пока идентификатор элемента находится в hotControl (т. е. пока происходит перетаскивание и кнопка мыши еще не была отпущена), должно изменять значение переменной:
if (Event.current.isMouse && GUIUtility.hotControl == controlID) {
// Get mouse X position relative to left edge of the control
float relativeX = Event.current.mousePosition.x - controlRect.x;
// Divide by control width to get a value between 0 and 1
value = Mathf.Clamp01 (relativeX / controlRect.width);
// Report that the data in the GUI has changed
GUI.changed = true;
// Mark event as 'used' so other controls don't respond to it, and to
// trigger an automatic repaint.
Event.current.Use ();
}
Два последних шага – установка GUI.changed и вызов Event.current.Use() – особенно важны для корректного взаимодействия внутри множества элементов IMGUI. Установка значения TRUE для GUI.changed позволяет использовать функции EditorGUI.BeginChangeCheck() и EditorGUI.EndChangeCheck() для проверки изменения значения переменной действиями пользователя. Значение FALSE лучше не использовать, чтобы не пропустить предыдущие изменения.
Наконец, наша функция должна вернуть новое значение плавающей переменной. Чаще всего оно будет отличаться от предыдущего значения:
return value;
}
MyCustomSlider готов. У нас получился простой функциональный элемент IMGUI, который можно использовать в пользовательских редакторах, PropertyDrawers, EditorWindows и т. д. Но это еще не всё. Далее мы поговорим о том, как можно расширить его функционал, например добавить возможность мультиредактирования.
afrokick
А есть какие-то отличия от Unity GUI, который был до 4.6 и остался сейчас? Или они просто переименовали в IMGUI чтобы не путались разработчики?
Leopotam
Это и есть старый гуй, его не переименовывали, он всегда так и звался — immediate gui.