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

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

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

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

Предисловие

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Глоссарий

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

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

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

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

Создание архитектуры UI на Unity
  1. Диалог выбора уровня.
  2. Панель, отображающая количество ресурсов пользователя.
  3. Навигационная панель.
  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 контролирует историю и очерёдность диалогов. Вот код базового класса.

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

Каждому экрану можно добавить параметр Properties. У этих параметров класс [System.Serializable], так что их можно менять прямо в префабе. Экран появляется при регистрации, когда префаб привязывается к слою. Экран привязан к ScreenID.

Вот отрывок из AUIScreenController.

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

UI Manager получает запросы методов (method calls) и направляет их к нужным слоям. Я обычно добавляю ещё ярлыки для отдельных вещей, которые точно пригодятся, вроде UICamera или MainCanvas.

Никакой код не должен прямо взаимодействовать с кодом экранов или слоёв, всё должно происходить через UI Manager.

Контроль анимации и плавности

Animator — мощный инструмент, если нужно анимировать гуманоидного персонажа, но для интерфейса он не очень подходит. Разве что анимации должны быть очень простыми: тогда можно дописать код к стейт-машинам (state machines) Animator.

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

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

Вот ATransitionComponent.

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

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

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

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

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

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

2323
12 комментариев

Для простых анимаций, типа ухода в прозрачность, скейл, поворот и перемещение отлично подходят tween-engines, типа DOTween PRO (не реклама). Отпадает нужда в Animator’e, ассетах анимации и стейт машине, твины вызываются одной строчкой кода и могут быть объединены в последовательности (sequences).

5
Ответить

С твинами нужно быть аккуратнее. Тот же DotTween генерить кучу мусора. Ассинхронные процессы и множество колбеков приведет к очень запутанным багам и утечкам. Так что стейт машина более надежна. А твины только для внутренных мелких анимаций виджетов.

3
Ответить

Все такие статьи пустоваты имхо, ибо

1) норм перцам что зафигачили пару сотен интерфейсов, тут ничего нового
2) новичкам тут мне кажется непонятно 90% информации, ибо это теория которая непонятна в отрыве от практики.

ЗЫ я долго думал над эффективным способом подачи материла для дтф. И мне кажется что это

1) немножко общей теории
2) пример на конкретном случае, пошаговый
3) отсылки к более углубленным материалам, где заинтересовавшиеся могут погрузиться (например подборка видеотуторов на тему).

6
Ответить

Согласен. Без реального проекта это просто вода, ничего толком не понятно. Вот бы своими руками пощупать, посмотреть, как в редакторе настроено, как в коде прописано.

Ответить

Описанная система запутанная. Тот костыль с прозрачным квадратом "пока анимация не закончится" говорит о том что машины состояний используются не правильно.

2
Ответить

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

Также, непонятно, почему не стоит наследоваться от существующих компонентов, особенно базовых вещей типа selectable. Конечно, механика работы компонентов документирована не всегда хорошо - но слава б-гу, весь Unity UI открыт на bitbucket для каждой версии Unity, код легко читаем и понять, что именно происходит, не предоставляет труда.

Кроме этого, если у вас много интерфейсов, пожалуйста, не забывайте о юнит-тестах! Система, которая в Unity не совсем корректно называется integration testing (та, которая, в отличие от "unit testing", может работать со сценами и gameObject'ами) просто прекрасно подходит для тестирования всего-всего-всего интерфейса, чтобы ловить поломки на самом раннем этапе.

Ну и, наконец, код игры не должен отправлять UI никаких запросов! Используйте наконец нормальный MVC и просто подписывайтесь на изменениях данных, которые получите при инициализации - это и делает разные компоненты игры менее зависимыми друг от друга, и разнообразное тестирование (как автоматическое, так и ручное, для художников и дизайнеров) делает гораздо проще.

1
Ответить

Хорошо сказал )

Ответить