Gamedev Дмитрий Мучкин
4 008

Создание архитектуры 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. Диалог выбора уровня.
  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 контролирует историю и очерёдность диалогов. Вот код базового класса.

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

#опыт #unity

Материал опубликован пользователем. Нажмите кнопку «Написать», чтобы поделиться мнением или рассказать о своём проекте.

Написать
{ "author_name": "Дмитрий Мучкин", "author_type": "editor", "tags": ["\u043e\u043f\u044b\u0442","unity"], "comments": 12, "likes": 33, "favorites": 48, "is_advertisement": false, "subsite_label": "gamedev", "id": 14464, "is_wide": false }
{ "id": 14464, "author_id": 6322, "diff_limit": 1000, "urls": {"diff":"\/comments\/14464\/get","add":"\/comments\/14464\/add","edit":"\/comments\/edit","remove":"\/admin\/comments\/remove","pin":"\/admin\/comments\/pin","get4edit":"\/comments\/get4edit","complain":"\/comments\/complain","load_more":"\/comments\/loading\/14464"}, "attach_limit": 2, "max_comment_text_length": 5000, "subsite_id": 64954 }

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

Популярные

По порядку

Написать комментарий...
5

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

Ответить
1

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

Ответить
6

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

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

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

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

Ответить
0

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

Ответить
2

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

Ответить
1

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

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

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

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

Ответить
0

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

Ответить
0

Но вообще мне кажется это разные подходы не мешающие друг другу, можно и на ивентах строить и иметь некий менеджер данных, который выдаёт или с которого забирают данные. Например есть scriptable object в котором хранятся общие данные по игроку, проще оттуда брать чем собирать ивентами )

Ответить
0

Зависит от характера данных и того, насколько много нужно вообще абстракции проекту, конечно. Практики, о которых я пишу, маленькому раннеру точно не нужны.

Ответить

Комментарий удален

0

Используем аналогичную систему на работе - для создания, закрытия и управлением окнами, слоями и анимациями интерфейса. Сто лет как хочу причесать её и адаптировать под uGUI (мы используем NGUI), но руки никак не доходят. И всё время удивляюсь, заходя на asset store, почему до сих пор никто не выложил ни одного подобного инструмента. Ведь это же реальный мастхев для каждого разработчика.

Ответить
0

Забыли упомянуть само переключение межу канвасами через Canvas.enable а не GameObject.SetActive()

Ответить
0

Если есть некий стейт менеджер, то проще к сожалению переключать объекты, ибо не всегда интерфейс разбит на подканвасы и канвасы. Хотя это конечно плохой способ.

Ответить
0

Прямой эфир

[ { "id": 1, "label": "100%×150_Branding_desktop", "provider": "adfox", "adaptive": [ "desktop" ], "adfox_method": "createAdaptive", "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "ezfl" } } }, { "id": 2, "label": "1200х400", "provider": "adfox", "adaptive": [ "phone" ], "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "ezfn" } } }, { "id": 3, "label": "240х200 _ТГБ_desktop", "provider": "adfox", "adaptive": [ "desktop" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "fizc" } } }, { "id": 4, "label": "240х200_mobile", "provider": "adfox", "adaptive": [ "phone" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "flbq" } } }, { "id": 5, "label": "300x500_desktop", "provider": "adfox", "adaptive": [ "desktop" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "ezfk" } } }, { "id": 6, "label": "1180х250_Interpool_баннер над комментариями_Desktop", "provider": "adfox", "adaptive": [ "desktop" ], "adfox": { "ownerId": 228129, "params": { "pp": "h", "ps": "clmf", "p2": "ffyh" } } }, { "id": 7, "label": "Article Footer 100%_desktop_mobile", "provider": "adfox", "adaptive": [ "desktop", "tablet", "phone" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "fjxb" } } }, { "id": 8, "label": "Fullscreen Desktop", "provider": "adfox", "adaptive": [ "desktop", "tablet" ], "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "fjoh" } } }, { "id": 9, "label": "Fullscreen Mobile", "provider": "adfox", "adaptive": [ "phone" ], "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "fjog" } } }, { "id": 10, "label": "Native Partner Desktop", "provider": "adfox", "adaptive": [ "desktop", "tablet" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "fmyb" } } }, { "id": 11, "label": "Native Partner Mobile", "provider": "adfox", "adaptive": [ "phone" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "fmyc" } } }, { "id": 12, "label": "Кнопка в шапке", "provider": "adfox", "adaptive": [ "desktop", "tablet" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "fdhx" } } }, { "id": 13, "label": "DM InPage Video PartnerCode", "provider": "adfox", "adaptive": [ "desktop", "tablet", "phone" ], "adfox_method": "createAdaptive", "adfox": { "ownerId": 228129, "params": { "pp": "h", "ps": "clmf", "p2": "flvn" } } }, { "id": 14, "label": "Yandex context video banner", "provider": "yandex", "yandex": { "block_id": "VI-250597-0", "render_to": "inpage_VI-250597-0-1134314964", "adfox_url": "//ads.adfox.ru/228129/getCode?pp=h&ps=clmf&p2=fpjw&puid1=&puid2=&puid3=&puid4=&puid8=&puid9=&puid10=&puid21=&puid22=&puid31=&puid32=&puid33=&fmt=1&dl={REFERER}&pr=" } }, { "id": 15, "label": "Плашка на главной", "provider": "adfox", "adaptive": [ "desktop", "tablet", "phone" ], "adfox": { "ownerId": 228129, "params": { "p1": "byudo", "p2": "ftjf" } } }, { "id": 17, "label": "Stratum Desktop", "provider": "adfox", "adaptive": [ "desktop" ], "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "fzvb" } } }, { "id": 18, "label": "Stratum Mobile", "provider": "adfox", "adaptive": [ "tablet", "phone" ], "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "fzvc" } } } ]
Хидео Кодзима оказался алгоритмом
машинного обучения
Подписаться на push-уведомления