Создание архитектуры UI на Unity

Для мобильных игр и не только.

Разработчик Янко Оливейра (Yanko Oliveira) поделился в блоге на Gamasutra своим опытом и советами по выстраиванию архитектуры пользовательского интерфейса для мобильных игр на Unity.

Предисловие

При разработке мобильного проекта, особенно F2P, не обойтись без метаигры, которая чаще всего гораздо сложнее основной геймплейной петли. А это значит много работы над пользовательским интерфейсом.

UI мало кто любит заниматься, поэтому разработчики часто выбирают уже существующие архитектуры и упрямо защищают их в жарких спорах. На деле нет какой-то идеальной архитектуры, подходящей для всех.

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

Если вкратце, эта архитектура — нечто вроде менеджера окон с историей и управлением потоком данных. Её легко адаптировать под конкретный проект.

Немного о UI на Unity

Он неплох, но иногда может быть ужасным

Многие необходимые вещи уже встроены в Unity, поэтому разработка интерфейса в нём — довольно простая задача, если понимаешь, как что работает. Однако некоторые сомнительные решения и небольшие детали могут вызывать фрустрацию.

Иногда придётся создавать свои компоненты интерфейса. При этом нужно помнить о целесообразности этих действий — возможно, то, что вам нужно, уже есть на движке.

Когда пишете код, создавайте новое

Расширить базовые классы UI и добавить дополнительные функции существующим компонентам — лёгкий путь, но опасный. Есть большая вероятность, что в результате придётся делать больше работы, чем если бы вы создали новые компоненты и классы.

От холстов зависит, какие части интерфейса нужно обновить

Когда вы что-то меняете в иерархии, весь меш обновляется. То есть, если весь интерфейс — это один холст (canvas), и в одном маленьком текстовом поле каждый кадр меняется число, то весь холст будет заново отстраиваться каждый кадр.

В идеале нужно иметь все статичные элементы на одном холсте, а динамические — на другом, но чаще всего это невозможно. Нужно искать баланс между оптимизацией и временем, затраченным на работу.

Используйте якоря вместо абсолютного позиционирования

Отзывчивый интерфейс легче адаптировать для разных разрешений. Также не забудьте выставить референсное разрешение в CanvasScaler, иначе потом придётся всё перестраивать. Я использую 1920*1080.

Используйте transform.SetParent(parent, false) вместо transform.parent

Это распространённая ошибка. Её симптом — элементы выглядят нормально, когда вы передвигаете их в редакторе, но меняют положение при рендеринге.

Глоссарий

Экран (screen) — отдельная замкнутая часть интерфейса. Они бывают двух видов: панели (panels) и диалоги (dialogs).

Одновременно могут быть открыты несколько панелей и диалогов, но только один диалог может быть интерактивным в единицу времени. Панели могут быть только открыты или закрыты, а диалоги обладают историей.

Виджет (widget) — часть экрана, которую можно использовать несколько раз.

Слой (layer) — то, в чём находится и чем контролируется экран.

  1. 1 — диалог выбора уровня;
  2. 2 — панель, отображающая количество ресурсов пользователя;
  3. 3 — навигационная панель;
  4. 4 — виджет. Значения в нём могут динамически меняться, и могут быть использованы в других элементах (к примеру, в диалоге с достижениями).

Иерархия

Вот приблизительное устройство иерархии элементов интерфейса (в скобках перечислено, что в них обычно содержится).

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 или, как я, создать прозрачный прямоугольник поверх всего.

Вот ATransitionComponent.

Их можно выставить в полях transition in или transition out в любом AUIScreenController. В нём также есть два события, к которым регистрируется слой и предупреждает, когда анимация закончилась. Это позволяет обеспечивать плавность анимаций по времени и блокировать экран: когда диалог открывается или закрывается, он вызывает блокировку интерфейса и включает анимацию. Блок снимается, когда анимация закончилась.

Благодаря этому можно создать несколько типов переходов, и они могут быть напрямую настроены художниками. Для более сложных анимаций можно создать AScreenTransition, который работает с Animator.

Самое плохое, по моему мнению, что даже для самых простых движений нужен набор анимаций и контроллер Animator. В Unity всё зависит от иерархии и названий, так что даже для создания элементарного затухания экрана нужно сильно постараться. К счастью, теперь есть SimpleAnimationComponent, для которого не нужен контроллер Animator.

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

 
Источник: DTF

Читайте также