Чем плох и хорош канвас unity
Организация интерфейса в Unity с UI Canvas
В Unity есть хорошая система для создания пользовательского интерфейса UI Canvas. По ней написано довольно много обучающего материала, но большинство гайдов рассказывает только о том, какие кнопки нажать и какой код написать, чтобы все заработало. В качестве примеров обычно приводится небольшой интерфейс состоящий из пары окон: главное меню, настройки. Однако в играх бывает гораздо больше окон и когда их становится уже хотя бы десяток, возникает необходимость какой-либо их организации. В рамках этой статьи я хочу рассказать как я решаю эту проблему.
Для начала нужно выделить отдельные окна в вашем интерфейсе. Под окном я подразумеваю панель с некоторыми контролами.
У каждого окна должен быть корневой объект, который будет содержать все контролы. Этот объект будет представлять окно как единое целое. Например есть панель на которой находятся элементы управления, логично сделать эти элементы дочерними по отношению к панели. К каждому окну прикрепляется компонент, котрый является наследником абстрактного класса Window.
Window
Window написан исходя из того, что у окон могут быть дочерние окна, т.е. те окна которые могут быть открыты из текущего окна при этом в один момент времени может быть открыто одно дочернее окно, а все остальные должны быть закрыты. С этой целью класс содержит свойство CurrentWindow в котором хранится ссылка на открытое на данный момент окно. А также есть событие OnOpen которое сообщает об открытии окна. Метод ChangeCurrentWindow() можно подписать на это событие у дочерних окон, чтобы в любой момент времени было открыто одно дочернее окно, он закрывает открытое дочернее окно и меняет ссылку на текущее открытое окно, ниже я приведу пример реализации. Также в классе есть методы SelfClose() и SelfOpen(), эти методы отвечают за то как будет открываться и закрываться окно. В методе Awake() происходит регистрация окна в UIManager, т.е. добавляется ссылка на окно.
UIManager
Далее перейдём к классу UIManager, он является sigleton-классом для того чтобы все окна могли к нему обращаться при необходимости. Как видно в нём есть список окон Windows, именно в нем хранятся ссылки на все окна на сцене. InitUI() нужен чтобы что-либо инициализировать, например, если вы хотите создавать окна из префабов, то здесь вы можете сделать их инстансы.
В методе Start() вы можете открыть окна которые должны быть открыты с самого начала или наоборот закрыть ненужные. Метод Get() позволяет получить ссылку на конкретное окно.
SettingsWindow
В качестве примера приведу свою реализацию окна настроек:
Из окна настроек я могу открыть 4 других окна, чтобы открыто было только одно окно, я подписываю метод ChangeCurrentWindow() окна настроек на события OnOpen дочерних окон, таким образом открытые окна закрываются, при открытии других. Реализации SelfOpen() и SelfClose() просто активируют или деактивируют окно.
Таким образом получается легко расширяемая система UI, нет необходимости вручную добавлять ссылки на окна в инспекторе или где-либо еще, чтобы добавить новое окно, нужно всего лишь создать соответствующий класс и унаследовать его от Window. У такой системы есть пара минусов, это то, что чем больше окон, тем больше классов необходимо будет создать и то, что поиск ссылки на окно происходит путем перебора массива ссылок, но на мой взгляд эти недостатки окупаются преимуществами.
Сanvas (Полотно)
Canvas (полотно) – это область, внутри которой находятся все элементы UI (пользовательского интерфейса). Полотно – это игровой объект (Game Object), с добавленным к нему компонентом Canvas. Все элементы UI должны быть дочерними этому Canvas.
Область Canvas отображается в виде прямоугольника в окне Scene View. Это облегчает процесс расположения элементов UI без необходимости видеть игровое окно (Game View).
Canvas uses the EventSystem object to help the Messaging System.
Порядок отрисовки элементов
Элементы UI на Canvas появляются в том же порядке, в каком они расположены в иерархии. Первый дочерний элемент отрисовывается первым, второй – за ним и так далее. Если два элемента UI накладываются друг на друга, добавленный позднее будет поверх того, что был добавлен ранее.
Чтобы изменить то, какой элемент будет находится поверх остальных, просто поменяйте местами элементы в иерархии путем перетаскивания. Порядком также можно управлять при помощи скриптинга, используя следующие методы компонента Transform: SetAsFirstSibling, SetAsLastSibling и SetSiblingIndex.
Режимы отображения
Этот режим отображения помещает элементы интерфейса на экран поверх сцены. Если меняется размер экрана или его разрешение, полотно автоматически примет нужный размер вместе с ним.
Интерфейс на Canvas в пространстве экрана в режиме перекрытия
Интерфейс на Canvas в пространстве экрана в режиме камеры
Пространство игрового мира (World Space)
При этом режиме отображения Canvas ведет себя также, как и любой другой объект на сцене. Размер Canvas может быть задан вручную при помощи Rect Transform, а элементы интерфейса будут отображаться перед или за другими объектами на сцене, в зависимости от их трехмерного расположения. Этот режим удобен для тех интерфейсов, которые предполагаются как часть игрового мира (diegetic interfaces).
Интерфейс на Canvas в пространстве игрового мира
Холст (Canvas)
Компонент Canvas представляет собой абстрактное пространство, в котором производится настройка и отрисовка UI. Все UI-элементы должны быть потомками игровых объектов, к которым присоединен Canvas. Когда вы создаете UI-элемент из пункта меню (GameObject > Create UI), Canvas будет добавлен автоматически, если его нет в сцене.
Свойства
Подробности
Одного холста для всех UI-элементов вполне достаточно, но и несколько холстов в сцене допустимо. Также, возможно использование нескольких холстов, когда один выставляется дочерним элементом другого, для оптимизации. Вложенный холст использует тот же режим рендеринга (Render Mode), что и родитель.
В этом режиме холст масштабируется для заполнения всего экрана, а затем рисуется напрямую, не ссылаясь на сцену или камеру (интерфейс написуется даже если в сцене вообще нет камеры). Если размер или разрешение экрана меняются, интерфейс автоматически перемасштабируется. Интерфейс рисуется поверх любой другой графики, такой как вид из камеры.
Интерфейс рисуемый поверх объектов сцены
В этом режиме, холст отображается как если бы он был нарисован на плоском объекте, на некотором расстоянии заданной камеры. Экранный размер интерфейса не меняется с расстоянием, т.к. он всегда масштабируется чтобы в точности заполнять пирамиду видимости камеры (camera frustum). Если размер или разрешение экрана, или пирамида видимости, изменяются – интерфейс автоматически перемасштабируется, чтобы помещаться. Любые 3д объекты сцены, расположенные ближе к камере, чем плоскость интерфейса, будут отрисованы “над” интерфейсов, в то время как остальные объекты, находящиеся за плоскостью, будут загорожены.
Интерфейс в режиме Camera mode с объектами сцены спереди
World Space (пространство мира)
Интерфейс в пространстве мира, пересекающийся с объектами сцены
Холст (Canvas)
Компонент Canvas представляет собой абстрактное пространство, в котором производится настройка и отрисовка UI. Все UI-элементы должны быть потомками игровых объектов, к которым присоединен Canvas. Когда вы создаете UI-элемент из пункта меню (GameObject > Create UI), Canvas будет добавлен автоматически, если его нет в сцене.
Свойства
Подробности
Одного холста для всех UI-элементов вполне достаточно, но и несколько холстов в сцене допустимо. Также, возможно использование нескольких холстов, когда один выставляется дочерним элементом другого, для оптимизации. Вложенный холст использует тот же режим рендеринга (Render Mode), что и родитель.
В этом режиме холст масштабируется для заполнения всего экрана, а затем рисуется напрямую, не ссылаясь на сцену или камеру (интерфейс написуется даже если в сцене вообще нет камеры). Если размер или разрешение экрана меняются, интерфейс автоматически перемасштабируется. Интерфейс рисуется поверх любой другой графики, такой как вид из камеры.
Интерфейс рисуемый поверх объектов сцены
В этом режиме, холст отображается как если бы он был нарисован на плоском объекте, на некотором расстоянии заданной камеры. Экранный размер интерфейса не меняется с расстоянием, т.к. он всегда масштабируется чтобы в точности заполнять пирамиду видимости камеры (camera frustum). Если размер или разрешение экрана, или пирамида видимости, изменяются – интерфейс автоматически перемасштабируется, чтобы помещаться. Любые 3д объекты сцены, расположенные ближе к камере, чем плоскость интерфейса, будут отрисованы “над” интерфейсов, в то время как остальные объекты, находящиеся за плоскостью, будут загорожены.
Интерфейс в режиме Camera mode с объектами сцены спереди
World Space (пространство мира)
Интерфейс в пространстве мира, пересекающийся с объектами сцены
Профилирование Unity UI: кто портит мой батчинг?
Вы потратили бесконечное количество времени на оптимизацию Unity UI. Но для того, чтобы вызвать торможения, достаточно небольшой модификации крошечного атрибута почти невидимого элемента UI Canvas. И когда такое случается, даже профилирование Unity UI не спасёт вас от снижения FPS. Вы готовы долгому исправлению ошибок?
Именно это и произошло в моём последнем проекте.
Я упорно работал над оптимизацией нескольких панелей UI в порте нашей игры на Oculus Quest. В основном задача сводилась к снижению уровня перерисовки (overdraw) до приемлемых величин, чтобы GPU мог справляться с самым главным — реальным 3D-рендерингом.
Так я работал над оптимизацией Unity UI не меньше месяца, и со временем добился чертовски неплохого прогресса.
На каком-то этапе UI стал настолько оптимизированным, что едва влиял на тайминги GPU. Реализованные мной техники затемнения непрозрачного UI компенсировали большую часть перерисовок, вызванных наслоением UI (элементами, отрисовываемыми поверх других элементов).
Итак, у меня получилась сверхоптимизированная гибридная система UI, которая по сути перекрывала отрисовываемые под ней 3D-элементы. Стало очень легко отбрасывать рендеринг этих перекрытых фрагментов.
Однако работа ещё была далека от завершения.
Когда я подключил Unity UI Profiler, моё внимание привлекла одна вещь.
Я увидел, что перегруженный ЦП тратит в каждом кадре более 1 мс на рендеринг UI. Это куча времени для платформы, которая даёт тебе бюджет в 13 мс на выполнение всей игры: физики, логики, 3D-рендеринга, ввода, VR и сетевого кода.
И ведь бывали случаи, когда UI «убивал» производительность ЦП ещё сильнее.
Unity UI: затратные батчи сборки
И это говорит об одном: UI можно оптимизировать под GPU, но это не обязательно означает оптимизацию под ЦП.
На самом деле, при рендеринге Unity UI задачи ЦП и GPU сильно отличаются. Не удивительно, что я рекомендую подходить к оптимизации ЦП и GPU по-разному, и об этом говорилось в моём предыдущем посте про оптимизацию Unity UI.
Дальнейшее профилирование Unity UI выявило очевидную проблему: UI постоянно воссоздавался в каждом новом кадре, т.е. в каждом кадре происходила перестройка Canvas (Canvas Rebuild).
Постоянная нагрузка на ЦП длительностью 1 мс… ой-ёй.
Но зачем Unity так со мной поступает? Я думал, что Unity кэширует все Canvas интерфейса.
На самом деле да, так и есть. Unity эффективно кэширует Canvas, чтобы они собирались только один раз.
Однако проблема возникает, когда ты меняешь свойства любого из элементов UI в Canvas — цвет, позицию, и так далее.
То есть все любимые нами анимации, например, эффекты кнопок при наведении курсора, убивают производительность, а вы, возможно, этого и не знаете.
Когда происходит изменение свойства UI, Unity выполняет знаменитую Canvas Rebuild, которая рушит производительность игры.
Canvas Rebuild интерфейса заставляет движок Unity итеративно обходить все элементы UI этого Canvas для генерации оптимизированного списка вызовов отрисовки (множества вершин, цветов, материалов и т.д.). И на Canvas Rebuild уходит больше времени, чем требуется Seat Panda, чтобы разогнаться с нуля до 60 миль/ч.
Осознав, что мы постоянно страдаем от Canvas Rebuild, логично будет задать вопрос…
Почему мы страдаем от Canvas Rebuilds и что с этим можно сделать?
Чтобы ответить на этот простой вопрос, мне пришлось потратить больше 5 часов на изучение темы и работу с Unity UI Profiler.
1. Профилирование Unity UI: всё хорошо, пока.
Представим, что у нас есть простой UI.
Этот UI ничего не делает, просто находится на экране и раздражает игрока, пытающегося что-нибудь сквозь него разглядеть.
Как набор из 350 с лишним изображений в Grid Layout Group это выглядит так:
Пример профилирования Unity UI
И всё в порядке, даже несмотря на 350 с лишним изображений. В стандартном случае UI отрендерится всего за два вызова отрисовки (draw call), потому что в нём всего два уникальных изображения, не находящихся в спрайтовом атласе.
По сути, в профилировщике я могу увидеть, что со стороны ЦП практически нет никакой нагрузки. Бо́льшую часть времени на UI тратится меньше 0.01 мс, что чертовски здорово.
Профилирование Unity UI: загадочный всплеск
(… Бо́льшую часть времени)
Постойте-ка, а откуда взялся этот всплеск ресурсов ЦП в конце графика?
2. Профилирование Unity UI: появляется внезапная Canvas Rebuild!
Что же произошло в конце Unity Profile? Очень странно, всего за секунду затраты ЦП на Unity UI стали в два с лишним раза больше.
Я хочу сыграть в игру
Найдите два различия в показанных ниже примерах (возможно, чтобы увидеть лучше, придётся открыть изображения в отдельном окне).
Профилирование Unity UI: малозатратный Canvas
Профилирование Unity UI: Canvas Rebuild
Даю вам пять секунд, чтобы разобраться.
5, 4… ну ладно, вот подсказка, чтобы упростить задачу:
Профилирование Unity UI: излишние затраты на Canvas Rebuild
PostLateUpdate.UpdateRectTransform и UGUI.Rendering.UpdateBatches сильно хотят стать главными звёздами этого шоу.
Что делают эти области?
Чёрт, да мы даже не знаем, был ли это всего один атрибут, или все сразу. Был ли это один объект или несколько? И какие именно? В этом-то и проблема: мы не знаем.
Теперь мы вроде бы знаем, что происходит, и находимся на верном пути. Но как избежать этой перестройки Canvas? Что их вызывает?
Нам нужна более конкретная информация…
Подведём итог
3. Ищем вредителя: политически некорректный способ грубого перебора
Нам по-прежнему нужно ответить на следующий вопрос:
Что вызывает эти Canvas Rebuild?
Оказывается, быстрого способа выяснить это не существует, особенно при большой иерархии Canvas.
Но для начала я покажу вам метод грубого перебора для нахождения причины Canvas Rebuild.
1. Пусть Unity UI Profiler продолжает записывать показания
Отфильтруем метрики, чтобы можно было сосредоточится на самом важном: рендеринге, скриптах и UI.
Отслеживайте исходную отметку, чтобы понимать затраты на текущую схему, в которую должны входить и затратные Canvas Rebuild.
2. Деактивируйте Game Objects из UI и сравните показания
Выберите группу game objects и деактивируйте их.
Сравните показатели производительности.
Если показания сильно не улучшились, то продолжайте деактивировать game objects, пока не увидите значительное улучшение.
3. Найдите, что изменяет его свойства
Нам удалось выделить объект, вызывающий Canvas Rebuild. Но что конкретно становится их причиной?
Возможно, его масштаб меняется через скрипт? Или его позиция меняется анимацией?
Удобно будет нажать правой клавишей на RectTransform и выбрать «Find References in Scene«
Когда вы узнаете, что вызывает перестройку Canvas, сделайте с этим что-нибудь, например, отключите анимации или преобразования.
Рубен, но как мне использовать этот метод при огромной иерархии UI? Не рассказывай мне чушь
Я говорил, что процесс не будет ни быстрым, ни интересным, но игроки будут вам благодарны.
В этом и дело. Во-первых, наличие огромной иерархии — неидеальная ситуация. Именно такие массивные и глубокие иерархии делают Canvas Rebuild такими затратными для ЦП.
Но большие и вложенные иерархии UI могут (и будут) возникать, поэтому ожидайте, что Canvas Rebuild будут сильнее всего бить по самому важному: по игровому процессу.
Хотя метод грубого перебора помогает найти источник перестройки Canvas, в долгосрочной перспективе он плохо масштабируется.
Став более профессиональным в оптимизации UI, я создал инструмент, дающий все необходимые мне ответы, чтобы проект соответствовал ожиданиям игроков…
Профилирование Canvas Rebuild
4. Бонус: расширение возможностей Unity Profiler по оптимизации UI
Надеюсь, вам уже понятно, насколько частыми и мешающими могут быть Canvas Rebuild.
Эти перестройки, заразившие мою игру, отбирали целых 10% всего бюджета ЦП!
Как мы видели, существует метод грубого перебора для поиска источников Canvas Rebuild. Возможно, вы справитесь с ними, воспользовавшись стратегиями, перечисленными в моём посте об оптимизации Unity UI.
Но такой подверженный ошибкам процесс ни за что бы не удовлетворил настоящего гуру. Вы можете потратить несколько дней, борясь с Canvas Rebuild, но в самый неожиданный момент они вернутся, чтобы пропасть, как только вы подключите Unity UI Profiler.
Если вы разрабатываете игру для VR, это становится критически важным. Мы не хотим, чтобы Canvas перестраивались в UI мирового пространства. Если от перестройки не избавиться, то ваши игроки скорее всего этого не выдержат.
Ладно, я понял, нужно избавляться от Canvas Rebuilds. Но Unity Profiler не даёт по ним почти никакой информации! Что ты можешь посоветовать?
Рад что вы спросили. Оказывается, мы можем убедить Unity Profiler представлять нам полезную информацию о том, что мешает производительности UI.
Мы с вами можем расширить функциональность Unity UI Profiler. Для этого нужно модифицировать публично доступный исходный код Unity UI. Добравшись до исходного кода, нужно будет найти функции кода, в которых происходят Canvas Rebuilds. Затем нам потребуется API-магия с BeginSample и EndSample профилировщика.
Если вы работаете в Unity 2019.1 или более ранней версии, то исходный код Unity UI выложен в репозитории Bitbucket. Также там есть руководство по скачиванию, установке и модифицированию.
Что посоветую я? Пользоваться более новой версией Unity, не ниже 2019.2.0. Новые версии Unity по умолчанию поставляются с исходным кодом UI, поскольку система UI теперь является частью менеджера пакетов (package manager). Это самый беспроблемный способ.
Вот список частей кода, которые я обнаружил в процессе своих исследований, куда можно добавить вызовы Profiling API:
Unity UI: исходный код профилирования
Удобно для художника/дизайнера? Нет.
Именно поэтом я написал небольшое Unity Extension с открытым исходным кодом, расширяющее возможности Unity Profiler.
Этот бесплатный инструмент позволит нам быстро переключаться между разными режимами профилирования, чтобы обеспечить максимальную производительность игры.
Что самое лучшее в этом расширителе Unity Profiler? Он работает за пределами редактора, по сути избавляя вас от всей головной боли при профилировании UI для Android и других платформ.
Вот он, вся его мощь управляется всего двумя кнопками: