Краткий обзор нового Unity UI с примерами организации интерфейса

Пожалуй, для многих новость о выходе новой системы Unity UI (далее просто UI) не показалась чем-то значимым. Как минимум — по причине её сырости, как максимум — из-за существования NGUI. Я поначалу не стал исключением, но открытие исходного кода UI системы (1) под либеральной лицензией MIT/X11, а так же энтузиазм разработчиков Unity Technologies заставили меня изменить мнение.
На мой взгляд, новый UI принесет нам довольно много плюсов:
- Вполне достойный инструмент из коробки;
- Возможность более глубоко понимать работу UI благодаря наличию исходников;
- Общепринятый механизм внесения изменений в исходный код UI — fork/pull request;
- Здоровая конкуренция между разными системами UI в конечном итоге принесет свои плоды в виде более качественных и удобных инструментов для Unity, а возможно и демпинг цен;
- Тесное взаимодействие команды разработки UI и ядра Unity уже сейчас приносят плоды в виде оптимизаций производительности, да и в будущем надеюсь они будут идти нога в ногу.
Давайте перейдем к конкретике. Начну с общего представления, сформировавшегося после раскуривания скудной документации, просмотра видео-уроков и раскопки исходного кода.
Концепция UI
Подход к организации интерфейса переработан впринципе. То есть если вы ранее работали с GUI, в новом UI вы почти не найдете ничего похожего. И наоборот — если вы работали с NGUI, то найдете очень много общего. В целом ее можно назвать типичной для сред с инструментами визуальной разработки.
В основе понимания новой концепции лежат три компонента: Canvas, Rect Transform и Event Trigger.
Canvas — является контейнером для всех элементов UI и определяет режим рендера. Таких контейнеров на сцене может быть более одного (3).
Rect Transform — этот компонент позволяет задать положение и размер игрового объекта, используя удобные визуальные контролы. Он вводит понятия ширины и высоты, а не только масштаб (4).
Unity Event — доработанная система событий, а конкретно компонент Event -> Event Trigger который включает компоненты визуального управления вызовом событий (5).
По мере знакомства с UI я создавал для себя возможные варианты использования. Давайте на них и разберем эту всю кухню. В конце статьи вы можете найти демо-проект, на который я буду ссылаться в тексте. К сожалению, этим примерам далеко до готовых рецептов, так как они не достаточно проработаны, плюс заточены под 2D игры, хотя местами вполне подойдут и для 3D.
Пример #1
Цели:
1. Тайловый фон;
2. Фиксированное положение контролов в независимости от разрешения и соотнешения сторон экрана;
3. Контролы должны быть пропорциональны размеру экрана.
Вариант решения:
1. Добавляем на сцену Canvas и выбираем режим рендера Screen space — Overlay, этот режим будет автоматически подстраивать его размер под размер камеры;
2. Добавим тайловый фон, для этого нам всего-то и нужно, что добавить нашему Canvas новый UI компонент — Image, выбрать нужный спрайт и изменить поле Image Type в значение Tiled;
3. Выбираем в окне Game удобное нам разрешение, например, 480×800 и добавляем на наш Canvas — контейнер необходимые контролы, выставляем им нужные позиции;
4. Далее идет новая магия UI, под названием якоря! Название говорит само за себя, они позволяют закреплять ряд характеристик игрового объекта, используя компонент Rect Transform. Изменить их можно в окне редактирования сцены, включив новый режим, либо в инспекторе. Обратите внимание, что иконка в инспекторе компонента Rect Transform — кликабельна и открывает окно выбора предустановленных положений и вариантов поведения, а если зажать кнопку Shift, то еще и положение точки опоры;
5. Довершает картину один из компонентов Canvas, который добавляется к нему по умолчанию Layout -> Canvas Scaler, а именно один из его режимов — Scale With Screen Size. Этот режим позволяет задать Reference Resolution, в нашем случае это будет 480×800.

Пример #2
Цели:
1. Ориентация строго портретная;
2. Универсальный для всех соотношений сторон фон, который всегда отображается на весь экран, а если необходимо обрезается только снизу;
3. Фиксированное положение контролов в независимости от разрешения и соотнешения сторон экрана;
4. Контролы должны быть пропорциональны размеру экрана.

Вариант решения:
1. Добавляем на наш Canvas — контейнер объект UI -> Image, выбираем нужный спрайт и тип изображения Simple;
2. Вся магия сводится к расположению якорей и точки опоры таким образом, что бы они лежили по центру верхнего ребра Canvas;
3. В компонент Canvas Scaler нужно выставить значение Match равное 0.
Пример #3
Цели:
1. Ориентация строго портретная;
2. Тайловый фон, который не скейлится;
3. Фиксированное по размеру игровое поле, например 400×400;
4. Фиксированное положение контролов в независимости от разрешения и соотнешения сторон экрана;
5. Контролы должны быть пропорциональны размеру экрана.

Вариант решения:
1. Можно, конечно, реализовать фон как Sky Box или игровым объектом типа Quad с затайленой текстурой и размерами заведомо больше экрана. Но мы пойдем UI-ным путем и создадим Canvas, аналогичный примеру #1, только выставим тип рендера Screen Space — Camera и пропишем нашу главную камеру в поле Render Camera;
2. Теперь создадим еще один Canvas с типом рендера World Space, пропишем камеру и поставим значение Order in Layer равное 1, что бы этот слой рисовался поверх предыдущего Canvas у которого это значение равно 0;
3. Советую вам при рендере World Space _заранее_ ставить значение масштаба Canvas таким образом, что бы у вас не было проблем с размером шрифтов;
4. Контролы раставляем уже известным нам способом.
Пример #4
Цели:
1. Создать скроллер;
2. Контент не должен быть виден за пределаеми скролера;
3. Он должен скейлиться вместе с экраном и липнуть к нижнему краю.

Вариант решения:
1. Нам необходим игровой объект которому мы добавим компонент UI -> Scroll Rect, самое важное поле которого — Content, задав которое мы указываем контейнер содержащий игровые объекты (например надцать изображений игровых уровней размером 100х100, которые заведомо не влезают в размер экрана);
2. Так же нам будет полезен компонент UI -> Mask (обратите внимание, что также необходимо вставлять пустой компонент Image), который клипнет контент выходящий за пределы игрового объекта, на который он добавлен;
3. Можно так же создать объект UI -> Scrollbar и привязать его к нашему Scroll Rect
Пример #5
Несколько мелкий приятностей:
1. Как расширить список обрабатываемых событий, например обрабатывать события StartDrag, EndDrag и т.д. используя компонент Event Trigger;
2. Применение элемента UI -> Toggle для реализации наград в виде Звезд с удобным скриптингом вида isOn = true;
3. Простейший вариант Попап окошек с использованием компонента Canvas Group и старого доброго аниматора без единой строчки кода.
Конечно, эти примеры покрывают лишь малую часть возможных задач и реализаций, но я надеюсь, они помогут вам выбрать удобное направление для поисков решения или, как минимум, песочницу, в которой можно попробовать разные варианты.
UI: Пользовательский интерфейс
Графический интерфейс пользователя (англ. Graphic User Interface, GUI) — это метод взаимодействия пользователя с игрой, в котором элементы интерфейса (меню, кнопки, значки, списки и т. п.), представленные пользователю на экране, выполнены в виде графических изображений.

Интерфейс игры
Canvas
Все элементы интерфейса (кнопки, изображения) должны находиться внутри объекта Canvas. Canvas — это холст экрана, на котором размещаются элементы. Для создания холста в главном меню выбери GameObject → UI → Canvas. В окне Инспектор можно настроить параметры холста. Самая важная настройка — это Render Mode компонента Canvas. Он отвечает за режим отображения интерфейса.
Render Mode содержит следующие настройки на выбор:
- Screen Space — Overlay. Элементы интерфейса показываются поверх экрана. Стоит по умолчанию;
- Screen Space — Camera. Позволяет отображать интерфейс в определенной камере;
- World Space. В этом режиме элементы интерфейса располагаются в 3D пространстве.
Rect Transform
Все элементы интерфейса имеют настройки Rect Transfrom. Разберем их на примере кнопки (выбрав в главном меню GameObject → UI → Button).
Во время редактирования пользовательского интерфейса в Unity нужно включить режим 2D. Для этого на панели Scene нажми кнопку 2D:
![]()
Переключение режимов
Включи инструмент редактирования пользовательского интерфейса на панели инструментов или нажми кнопку T. С помощью этого инструмента можно перемещать, вращать и изменять размер элементов интерфейса.
Режимы редактирования
Выбери элемент интерфейса. Схватив за одну из точек по краям можно изменить размер элемента.
Anchor — это точка привязки элемента интерфейса на экране. Изменить расположение Anchor можно, если схватить за центр «цветка». Разберем пример, когда Anchor привязан к левому верхнему краю. Точка Anchor кнопки находится в левом верхнем углу. При изменении размера экрана – кнопка не изменяет свой размер и будет расположена на определенном расстоянии от левого верхнего угла. Следовательно, кнопка будет выглядеть большой на маленьком экране и маленькой на большом мониторе.
Пример 1 Пример 2
Для того, чтобы элемент интерфейса масштабировался — расположи «лепестки» Anchor так:
Пример 3 Пример 4
Button
Создать кнопку можно в главном меню GameObject → UI → Button. Кнопка состоит из двух объектов, которые можно посмотреть в окне Иерархия — формы и текста внутри нее:
Объекты кнопки
Сначала разберем настройки надписи на кнопке. Выбери Text на панели Hierarchy. В Inspector будут следующие настройки текста:
- Text — текст кнопки;
- Font Size — размер текста.
Теперь, рассмотрим настройки формы кнопки. Выбери объект Button (название этого объекта можно изменить). В окне Инспектор находятся следующие основные настройки:
- Normal Color. Цвет кнопки;
- Highlighted Color. Цвет кнопки при наведении на нее мыши;
- Pressed Color. Цвет нажатой кнопки.
Функции
Как ты уже знаешь, отдельным блокам кода можно задавать имена. Такой именованный блок кода называется функцией или методом (частный случай функций). Для примера создадим функцию с именем SayHello, которая выводит фразу Hello world! в консоль:
Обратите внимание, что функция не выполняется автоматически при создании. Чтобы функция выполнилась, ее требуется явно вызвать в желаемом месте. Для примера вызовем созданную функцию внутри функции Start():
Обработка события нажатия на кнопку
Для обработки события нажатия на кнопку необходимо выполнить два шага:
1. Создать функцию, которая должна выполняться при каждом нажатии.
2. Передать имя созданной функции в функцию AddListener() соответствующего события кнопки.
Весь код скрипта:
Обратите внимание, что имя функции может быть любым. Имя должно соответствовать действию, которое выполняет функция. А вот этот скрипт загрузит сцену с нужным именем по нажатию кнопки:
Создание кнопки со своим дизайном
Рассмотрим пример создания кнопки, состоящая из трех изображений: обычное состояние (Normal), состояние, когда на кнопку наведена мышь (Hovered) и состояние нажатия на кнопку (Pressed). Кнопки можно ⬇ скачать тут.

Нормальное состояние кнопки (Normal) и нажатая кнопка (Pressed)
Сначала нужно загрузить все изображения кнопки в проект, лучше использовать одно изображение, разделенное с помощью инструмента Sprite Editor. Выбери загруженное изображение и в окне Инспектор в параметре Texture Type выбери Sprite (2D and UI). После чего нажми кнопку Apply.

Изменение типа изображения
Теперь в параметре Sprite Mode выбери Multiple и нажми кнопку Sprite Editor, чтобы открылся редактор Sprite Editor. Выбери в меню Slice → Type → Automatic и нажми кнопку Slice. Unity сам найдет все кнопки и разметит их.

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

Фрагменты кнопки, которые не будут изменять размер
Выполни это действие для каждой кнопки. После этого можно закрыть окно и принять изменения.
Создай кнопку Выдели кнопку GameObject → UI → Button. Выбери ее и в окне Инспектор отредактируй следующие настройки:
- в параметре Source Image выбери спрайт кнопки в обычном состоянии (normal);
- в параметре Image Type выбери Sliced;
- в параметре Transition выбери Sprite Swap;
- в параметре Highlighted Sprite выбери спрайт подсвеченной кнопки (с нашими кнопками это normal состояние);
- в параметре Pressed Sprite выбери спрайт нажатой кнопки.
Image
GameObject → UI → Image позволяет показывать на переднем плане экрана изображения такие как карта, значок жизни, магии и прочее.
Добавь в проект изображение, которое будет использоваться в графическом интерфейсе. В Окне Инспектор изображения для выбери следующие настройки:
- в параметре Texture Type выбери Sprite (2D or UI);
- в параметре Sprite Mode выбери Single.
Добавь на сцену GameObject → UI → Image. На панели Inspector выбери следующие настройки:
- в параметре Source Image выбери загруженное изображение;
- в параметре Color можно перекрасить изображение в определенный цвет;
- в параметре Image Type выбери Simple. Если включить настройку Preserve Aspect, то изображение не будет деформироваться при изменении размера экрана. Если в настройке Image Type выбрать Tiled — изображение будет повторяться на экране.
Text
GameObject → UI → Text позволяет отображать текст в графическом интерфейсе (отображение диалогов, подсказок, надписей). Содержит следующие настройки:
- Text — текст, который будет отображаться на экране;
- Font — шрифт текста;
- Font Style — тип шрифта. Принимает следующие значения:
- Normal – обычный шрифт;
- Bold – применение полужирного начертания к тексту;
- Italic – применение курсивного начертания к тексту;
- Bold and Italic – применение курсивного и полужирного начертания к текст.
- Font Size — размер шрифта;
- Alignment — выравнивание текста;
- Color — цвет текста.
Текст можно менять с помощью скрипта. Это позволит в режиме реального времени отображать количество набранных очков, жизни, времени и т.п.
Создание архитектуры UI на Unity
Разработчик Янко Оливейра (Yanko Oliveira) поделился в блоге на Gamasutra своим опытом и советами по выстраиванию архитектуры пользовательского интерфейса для мобильных игр на Unity.
При разработке мобильного проекта, особенно F2P, не обойтись без метаигры, которая чаще всего гораздо сложнее основной геймплейной петли. А это значит много работы над пользовательским интерфейсом.
UI мало кто любит заниматься, поэтому разработчики часто выбирают уже существующие архитектуры и упрямо защищают их в жарких спорах. На деле нет какой-то идеальной архитектуры, подходящей для всех.
В этом материале я изложу один способ разработки интерфейса, который хорошо работает при создании систем средней и высокой сложности, и был проверен на нескольких выпущенных играх.
Если вкратце, эта архитектура — нечто вроде менеджера окон с историей и управлением потоком данных. Её легко адаптировать под конкретный проект.
Многие необходимые вещи уже встроены в Unity, поэтому разработка интерфейса в нём — довольно простая задача, если понимаешь, как что работает. Однако некоторые сомнительные решения и небольшие детали могут вызывать фрустрацию.
Иногда придётся создавать свои компоненты интерфейса. При этом нужно помнить о целесообразности этих действий — возможно, то, что вам нужно, уже есть на движке.
Расширить базовые классы UI и добавить дополнительные функции существующим компонентам — лёгкий путь, но опасный. Есть большая вероятность, что в результате придётся делать больше работы, чем если бы вы создали новые компоненты и классы.
Когда вы что-то меняете в иерархии, весь меш обновляется. То есть, если весь интерфейс — это один холст (canvas), и в одном маленьком текстовом поле каждый кадр меняется число, то весь холст будет заново отстраиваться каждый кадр.
В идеале нужно иметь все статичные элементы на одном холсте, а динамические — на другом, но чаще всего это невозможно. Нужно искать баланс между оптимизацией и временем, затраченным на работу.
Отзывчивый интерфейс легче адаптировать для разных разрешений. Также не забудьте выставить референсное разрешение в CanvasScaler, иначе потом придётся всё перестраивать. Я использую 1920*1080.
Это распространённая ошибка. Её симптом — элементы выглядят нормально, когда вы передвигаете их в редакторе, но меняют положение при рендеринге.
Экран (screen) — отдельная замкнутая часть интерфейса. Они бывают двух видов: панели (panels) и диалоги (dialogs).
Одновременно могут быть открыты несколько панелей и диалогов, но только один диалог может быть интерактивным в единицу времени. Панели могут быть только открыты или закрыты, а диалоги обладают историей.
Виджет (widget) — часть экрана, которую можно использовать несколько раз.
Слой (layer) — то, в чём находится и чем контролируется экран.
- Диалог выбора уровня.
- Панель, отображающая количество ресурсов пользователя.
- Навигационная панель.
- Виджет. Значения в нём могут динамически меняться, и могут быть использованы в других элементах (к примеру, в диалоге с достижениями).
Вот приблизительное устройство иерархии элементов интерфейса (в скобках перечислено, что в них обычно содержится).
UI [код UI Manager, основной холст]
— Камера
— Слой диалога (код слоя диалога)
— — Диалог А (контроллер диалога А)
— — Диалог B (контроллер диалога B)
— Слой панелей (код слоя панелей)
— — Панель A (контроллер панели A)
— — Панель B (контроллер панели B)
Unity отрисовывает элементы строго по иерархии, то есть, нижние элементы рендерятся в последнюю очередь. В этом примере панели всегда будут расположены поверх диалогов.
Во время настройки основного холста используйте Screenspace — Camera, а не Screenspace — Overlay. Она работает точно так же, и можно добавлять вещи вроде трёхмерных моделей, систем частиц и даже эффектов постобработки.
Префабы (prefab) очень помогают. Я обычно делаю по одному префабу для экранов и по несколько для виджетов. Что бы вы ни делали, старайтесь разделять элементы на как можно большее количество префабов.
В идеале ваш художник интерфейса должен работать в Unity. Так вы сможете сосредоточиться на программировании, вместо того, чтобы пытаться перенести творения художника в игру. Если он не знает движка, постарайтесь его научить.
Также можно сделать редактор для художника, но это нужно делать вместе с ним, потому что это ему работать в программе.
Спрайты лучше всегда переводить в атласы (atlas). Я пользуюсь встроенным Sprite Packer и складываю все спрайты для одного атласа в отдельную папку, чтобы все они попадали в Asset Bundle.
Также рекомендую работать над заготовками в сторонних программах как можно дольше, прежде чем запускать Unity, используя Balsamiq, Axure, InVision или Origami Studio, в зависимости от предпочтений художника.
Когда заготовки сделаны и одобрены, художник делает префаб экрана, который программисты добавляют в игру. Если времени на весь рабочий цикл нет, программисты могут делать собственные версии экранов, чтобы добавить нужные функции, а потом менять их на одобренные префабы.
Код системы интерфейса разделён на три части: главный фасад, контроллеры слоёв и контроллеры экранов. В моём примере иерархии было два слоя, в реальности их почти всегда больше. Можно каждый раз создавать новый слой, но я пользуюсь пара-слоями (para-layers): дополнительные объекты в иерархии, к которым код слоя привяжет все изменения экрана (Screen Transforms).
Все контроллеры слоёв — это вариации AUILayerController. У этого класса есть код для показа и скрытия экранов, и он общается с самими экранами. PanelLayer просто указывает, что он управляет панелями, и отправляет их к пара-слоям. DialogueLayer контролирует историю и очерёдность диалогов. Вот код базового класса.
Каждому экрану можно добавить параметр Properties. У этих параметров класс [System.Serializable], так что их можно менять прямо в префабе. Экран появляется при регистрации, когда префаб привязывается к слою. Экран привязан к ScreenID.
Вот отрывок из AUIScreenController.
UI Manager получает запросы методов (method calls) и направляет их к нужным слоям. Я обычно добавляю ещё ярлыки для отдельных вещей, которые точно пригодятся, вроде UICamera или MainCanvas.
Никакой код не должен прямо взаимодействовать с кодом экранов или слоёв, всё должно происходить через UI Manager.
Animator — мощный инструмент, если нужно анимировать гуманоидного персонажа, но для интерфейса он не очень подходит. Разве что анимации должны быть очень простыми: тогда можно дописать код к стейт-машинам (state machines) Animator.
Чаще всего в интерфейсе очень много анимаций для экранов, которые постоянно появляются и исчезают. При их настройке легко случайно создать условия для зависания при нажатии кнопки.
В коде экранов есть встроенные возможности для появления и исчезания, и во время анимации взаимодействие с ними заблокировано. Чтобы избежать проблем, можно тщательно контролировать EventSystem или, как я, создать прозрачный прямоугольник поверх всего.
Их можно выставить в полях transition in или transition out в любом AUIScreenController. В нём также есть два события, к которым регистрируется слой и предупреждает, когда анимация закончилась. Это позволяет обеспечивать плавность анимаций по времени и блокировать экран: когда диалог открывается или закрывается, он вызывает блокировку интерфейса и включает анимацию. Блок снимается, когда анимация закончилась.
Благодаря этому можно создать несколько типов переходов, и они могут быть напрямую настроены художниками. Для более сложных анимаций можно создать AScreenTransition, который работает с Animator.
Самое плохое, по моему мнению, что даже для самых простых движений нужен набор анимаций и контроллер Animator. В Unity всё зависит от иерархии и названий, так что даже для создания элементарного затухания экрана нужно сильно постараться. К счастью, теперь есть SimpleAnimationComponent, для которого не нужен контроллер Animator.
За годы разработки описанная здесь система интерфейса почти не изменялась. Решение не использовать существующие архитектуры окупилось: если мы хотели добавить что-то новое в UI, мы легко можем написать грубый локализованный код, а потом почистить его.
Для простых анимаций, типа ухода в прозрачность, скейл, поворот и перемещение отлично подходят tween-engines, типа DOTween PRO (не реклама). Отпадает нужда в Animator’e, ассетах анимации и стейт машине, твины вызываются одной строчкой кода и могут быть объединены в последовательности (sequences).
С твинами нужно быть аккуратнее. Тот же DotTween генерить кучу мусора. Ассинхронные процессы и множество колбеков приведет к очень запутанным багам и утечкам. Так что стейт машина более надежна. А твины только для внутренных мелких анимаций виджетов.
Все такие статьи пустоваты имхо, ибо
1) норм перцам что зафигачили пару сотен интерфейсов, тут ничего нового
2) новичкам тут мне кажется непонятно 90% информации, ибо это теория которая непонятна в отрыве от практики.
ЗЫ я долго думал над эффективным способом подачи материла для дтф. И мне кажется что это
1) немножко общей теории
2) пример на конкретном случае, пошаговый
3) отсылки к более углубленным материалам, где заинтересовавшиеся могут погрузиться (например подборка видеотуторов на тему).
Согласен. Без реального проекта это просто вода, ничего толком не понятно. Вот бы своими руками пощупать, посмотреть, как в редакторе настроено, как в коде прописано.
Описанная система запутанная. Тот костыль с прозрачным квадратом "пока анимация не закончится" говорит о том что машины состояний используются не правильно.
Такие практики подходят только простым интерфейсам без большого количества менающихся данных. В более сложных случаях необходимо отходить от якорей к использованию layout.
Также, непонятно, почему не стоит наследоваться от существующих компонентов, особенно базовых вещей типа selectable. Конечно, механика работы компонентов документирована не всегда хорошо — но слава б-гу, весь Unity UI открыт на bitbucket для каждой версии Unity, код легко читаем и понять, что именно происходит, не предоставляет труда.
Кроме этого, если у вас много интерфейсов, пожалуйста, не забывайте о юнит-тестах! Система, которая в Unity не совсем корректно называется integration testing (та, которая, в отличие от "unit testing", может работать со сценами и gameObject’ами) просто прекрасно подходит для тестирования всего-всего-всего интерфейса, чтобы ловить поломки на самом раннем этапе.
Ну и, наконец, код игры не должен отправлять UI никаких запросов! Используйте наконец нормальный MVC и просто подписывайтесь на изменениях данных, которые получите при инициализации — это и делает разные компоненты игры менее зависимыми друг от друга, и разнообразное тестирование (как автоматическое, так и ручное, для художников и дизайнеров) делает гораздо проще.