Оптимизация Unity UI
В этой статье разбираются вопросы оптимизации UI-элементов проектов, сделанных в Unity. На основании информации из официальной документации и личного опыта я постарался наглядно объяснить принципы работы UI-элементов. Также здесь вы найдёте практические советы, которые помогут улучшить производительность вашего проекта в том, что касается пользовательского интерфейса.
- Терминология
- Вводная
- Отрисовка Unity UI
- Перестроение интерфейса пользователя
- Батчинг мешей
- Общие советы по оптимизации UI
- Работа с текстом и его оптимизация
- Атласы Спрайтов
- Резюмируем способы оптимизации
- Источники
Терминология
UI-элементы — все элементы в Unity, относящиеся к построению пользовательского интерфейса. Сюда относятся, например: кнопка, текст, картинка, выпадающее меню и др.
Холст (canvas) — базовый элемент UI, являющийся контейнером для остальных элементов.
Меш (mesh) — совокупность параметров описывающих 3D-модели.
Квад (quad) — это меш, который представляет из себя четырехугольник.
Батчинг (batching) — объединение мешей объектов в один большой меш для более быстрой отрисовки.
Вызов отрисовки (draw-call) — команда на отрисовку от движка к графическому API (например, OpenGL или Direct3D).
Transparent queue — очередь отрисовки прозрачных объектов.
Альфа-смешивание (Alpha blending) — алгоритм смешивания пикселей по альфа-каналу для получения изображения с прозрачностью.
Атлас (Atlas) — вид ресурсов, который объединяет несколько текстур в одну.
Вводная
В оптимизация UI нет универсальных правил, работающих в любой ситуации. Всё сводится к нахождению баланса между стоимостью батчинга и количеством вызовов отрисовки. Можно выделить четыре основные проблемы:
- слишком большая загрузка GPU (превышение нагрузки на отрисовку);
- слишком большая нагрузка на CPU при перестроении холста;
- слишком много изменяющихся элементов, приводящих к перестроению холста;
- слишком большая нагрузка на CPU при генерации мешей (обычно связанных с текстом).
Отрисовка Unity UI
Базовым элементом пользовательского интерфейса Unity является холст. Он отвечает за генерацию, сортировку и отрисовку мешей дочерних элементов интерфейса. Все элементы UI должны быть дочерними к какому либо холсту, в противном случае они не будут отображаться в игре.
Отрисовка происходит от самого дальнего к самому ближнему объекту от камеры (back-to-front) в Transparent queue с альфа-смешиванием.
Отдельно надо отметить, что прозрачность UI элементов НЕ влияет на производительность. Даже если элемент полностью состоит из “непрозрачных” пикселей, он все равно будет отрисован с использованием альфа-смешивания.
Также важно понимать, что при отрисовке обрабатываются все пиксели всех активных элементов. Это не зависит от того, видны они, перекрыты другими объектами или вообще полностью прозрачны.
Перестроение интерфейса пользователя
Перестроение интерфейса пользователя — это многоэтапный процесс, включающий в себя построение мешей каждого элемента UI, и попытка батчинга этих мешей для того, чтобы минимизировать количество отрисовок (draw calls).
Перестроение происходит в четыре этапа:
- Анализируются структуры элементов.
- Перестраиваются меши всех активных элементов, включая элементы с нулевой прозрачностью.
- Перестраиваются материалы для батчинга мешей элементов.
- Все элементы отрисовываются согласно своей очерёдности.
Перестроенный холст кешируется и переиспользуется до тех пор, пока один из элементов в холсте не помечается как изменённый.
Изменёнными (dirty) помечаются объекты, которые были активированы или деактивированы; у которых изменился материал, позиция, масштаб, поворот; изменилось текстовое значение у текстового компонента; производилось переназначение родителя и т.д.
В этом случае заново происходит перестроение холста, содержащего хотя бы один изменённый элемент. Правда, это относится только к тому холсту, в котором находится элемент. То есть изменения элементов в дочерних холстах не влияют на родительские.
Чем больше элементов в холсте, тем больше будут издержки на анализ и сортировку объектов.
Батчинг мешей
Объединение мешей, или батчинг, помогает снизить нагрузку на GPU, уменьшая количество вызовов отрисовки. В процессе батчинга меши сортируются по глубине и проверяются на перекрытие. При проходе от дальнего элемента к ближнему (или от верхнего элемента к нижнему в иерархии) в рамках одного холста объекты с одинаковыми материалами или текстурами объединяются в один меш. Для этого между ними не должно быть объектов с иными материалами. Также объекты с иными материалами не должны перекрывать запекаемые объекты своими габаритными контейнерами. Операция батчинга мультипоточная, её производительность значительно разнится в зависимости от количества ядер у процессора.
Текст может батчиться с другим текстом, если у них один и тот же шрифт. При этом неважно, выбраны одинаковые или разные настройки размера шрифта и стили. Если же шрифты разные, то текст не будет батчиться.
Также надо учитывать, что текст может перекрыть объект своим габаритным контейнером, и такое перекрытие можно легко пропустить.
Рассмотрим пример. Есть три объекта A, B и C, расположенные в иерархии таким образом:
На изображении слева объекты A и C будут объединены, т.к. имеют один и тот же материал и не пересекаются с объектом B. На изображении справа объекты A и C не будут объединены, т.к. имеется пересечение с объектом B.
Общие советы по оптимизации UI
Прежде чем начинать оптимизацию, настоятельно рекомендуется провести профайлинг UI. Это поможет определить узкие места, приводящие к потере производительности (если они есть). Для профайлинга существует множество инструментов, как встроенных в Unity (Unity Profiler), так и сторонних. Но вопросы профайлинга UI в этой статье мы разбирать не будем.
Вот что можно посоветовать для оптимизации UI в Unity:
- Отключайте невидимые объекты. Если элемент перекрыт непрозрачным элементом, то требуется отключить его GameObject или родительский GameObject перекрываемого элемента. При этом элементы интерфейса, у которых альфа выставлена в 0, всё равно будут отрисованы. У таких объектов нужно включить Cull Transparent Mesh в компоненте Canvas Renderer или просто отключить невидимые объекты.
- Отключайте объекты мира, скрытые непрозрачным интерфейсом. Если интерфейс перекрывает не весь мир, можно сохранить его в Render Texture, а мировую камеру отключить.
- Минимизируйте количество пикселей для отрисовки. Объединяйте по возможности несколько изображений в одно. К примеру, имеет смысл делать кнопки одним спрайтом, а не отдельными слоями с подложкой, обводкой, телом кнопки и т.п. Это уменьшит гибкость работы с такими элементами и может привести к засорению ресурсов, так что надо искать компромисс.
- Избегайте пустых элементов, которые служат лишь для для организации структуры (не используйте элементы как названия «папок» в иерархии документа).
- Избегайте пересечения объектов, не способных запечься друг с другом. Если возможно, то лучше изменить положение в иерархии, размер контейнера или позицию перекрывающихся и не запекающихся элементов.
- Используйте для динамических элементов отдельные или вложенные холсты. Так вы минимизируете затраты на сортировку и перестройку структуры холста, содержащего большое количество элементов. Вложенные холсты более удобны, т.к. наследуют настройки родительского холста. В то же время, при изменении родительского холста перестроятся и все вложенные. Такое довольно редко, но происходит (например при смене разрешения экрана). Учитывайте, что объекты из разных холстов или вложенных холстов НЕ запекаются для совместной отрисовки. Рекомендуется разделять холсты по регулярности обновления элементов. Статичные элементы нужно поместить в отдельный холст, тогда они будут отрисовываться лишь один раз. Если есть элементы, которые постоянно меняются, то лучше их объединить в другой холст, т.к. они все равно будут вызывать перестройку друг друга. Изменяющиеся объекты также можно разделить на несколько холстов по частоте обновления. Например, элементы, обновляющиеся каждый кадр положить в один холст, а элементы, обновляющиеся реже — в другой.
- Отключение Pixel Perfect ощутимо повысит производительность. Особенно это актуально для постоянно обновляющихся объектов с большим количеством элементов (например, скролящийся инвентарь).
Холст
Вложенный холст - При необходимости отключить холст не отключайте объект, содержащий его (через функцию SetActive). При следующем включении элементы этого холста помечаются как изменённые и происходит перестроение всех элементов. Лучше выключить сам компонент холста, тогда вся структура и запечённые данные не изменятся и при следующем включении компонента холста просто начнут свою отрисовку.
- У холстов, у которых в параметре Render Mode выбраны Screen Space — Camera или World Space, всегда устанавливайте камеру. Если её не установить, то система пользовательского интерфейса в каждом кадре будет производить её поиск через Object.FindObjectWithTag, чтобы найти Camera.main, а это скажется на производительности.
- Чтобы не создавать несколько одинаковых текстур, можно создать одну в оттенках серого и “красить” её через компонент Image, выбрав нужный цвет.
- Оставляйте флаг Raycast Target только на элементах, нуждающихся в событиях ввода, и снимайте у остальных. По умолчанию Raycast target включён на многих элементах (Image, Text и т.д.). Это затрудняет и замедляет работу Raycaster-компонента, который обрабатывает события ввода в UI Unity. При клике или тапе он проходит по всей иерархии элементов и ищет все графические компоненты с выставленным флагом Raycast Target, затем проверяет их на возможность событий ввода и после успешного прохождения проверок добавляет в список попаданий. После этого список попаданий сортируется по глубине, отбрасываются объекты, находящиеся за пределами экрана. В результате выдаёт окончательный список попаданий.
В Unity UI многие компоненты (Image, Text и т.д.) имеют флаг Raycast Target.
Raycast Target у компонента изображения.
У TextMeshPro он спрятан во вкладке Extra Settings.
Для TextMeshPro можно задать установку Raycast Target по умолчанию в Player Settings -> TextMest Pro -> Settings -> Enable Raycast Target.
Элементы проходят проверку, когда:
- включён Raycast Target;
- включён и активирован сам элемент;
- точка ввода попадает в пересечение с RectTransform.
Также имеет смысл убирать флаг Raycast Target с дочерних элементов, если корневой объект уже имеет его и полностью перекрывает своей геометрией дочерние элементы. Например, стандартная кнопка Unity UI.
Изображение своей формой полностью перекрывает текст, в этом случае можно снять Raycast Target с компонента текста.
Если все элементы холста не ждут событий ввода, то можно удалить компонент Graphic Raycaster с холста/вложенного холста.
Работа с текстом и его оптимизация
Текст в интерфейсе Unity состоит из сеток, в которых на каждый символ создаётся свой квад (quad). Меш перестраивается каждый раз, когда изменяется значение текста. Также перестроение происходит, если был выключен и повторно включён текстовый компонент или его родитель.
По умолчанию шрифты в Unity добавляются как динамические. Для каждого динамического шрифта, используемого в текстовом компоненте на сцене, создаётся свой атлас. В этот атлас включаются только используемые символы. Например, если текстовое поле содержит текст “New Text”, то созданный для него атлас будет содержать символы “N”, ”e”, “w”, “T”, “x” и “t”. Для каждого символа, отличающегося по размеру или стилю, будет создано своё представление в атласе.
Текст на сцене
Атлас шрифта
Если в процессе выполнения программы содержимое текстового компонента изменится и там появятся символы, которые отсутствуют в атласе, будет вызвана перестройка всего атласа. В случае, если в текстуре атласа есть свободное место, то необходимые символы будут просто добавлены туда. При этом символы, которые в данный момент не используются, не удалятся. Если в атласе недостаточно места для новых символов, его размер будет удвоен и заполнен заново на основе используемых символов в активных текстовых компонентах.
Если для проекта задано строго определённое количество символов, например только латинский алфавит, то стоит использовать статичные шрифты, которые хранятся в памяти постоянно. Если в вашем проекте допускается большое количество символов, то лучше всё же остановиться на динамических шрифтах.
Можно также повысить производительность, если заменить текстовый компонент на спрайт. Например, появляющиеся в игре цифры (счёт) можно сделать, используя спрайты из одного атласа, содержащего набор только необходимых символов. В этом случае не будет издержек на перестроение холста и атласа шрифта.
Использование резервных шрифтов (Fallback fonts), перечисленных в поле Font Names в настройках шрифта, приводит к увеличению используемой памяти. Особенно это заметно на пиктографических шрифтах.
Не рекомендуется использовать Best Fit, т.к. эта опция приводит к быстрому переполнению атласа и вызывает его перестройку.
Best fit игнорирует настройки размера шрифта и пытается уместить текст в габаритный прямоугольник текстового компонента.
Теперь пара слов о TextMeshPro (TMP), популярной замене стандартному текстовому компоненту Unity. TextMeshPro также перестраивает свою сетку каждый раз, когда значение текста меняется. Однако TMP-текст не использует динамические шрифты. Для него заранее генерируются атласы шрифтов, в которые включаются все необходимые символы. Если для какого-то текста на сцене нет символа в назначенном на его компонент шрифте, то TMP начинает искать в резервных шрифтах. Если и там ничего не находится, то TMP будет пытаться найти этот символ во всех загруженных шрифтах.
Best Fit в TMP не создаёт проблем, как обычный текстовый компонент, так что его вполне можно использовать.
Большое количество шрифтов с разными локализациями или большие атласы шрифтов могут занимать много памяти. Поэтому лучше использовать предварительную загрузку только необходимых для конкретной локализации шрифтов.
В World Space рекомендуется использовать TextMeshPro вместо TextMeshProUGUI.
TextMeshProUGUI используется в холстах.
Атласы Спрайтов
Спрайтовые атласы — это вид ресурсов, который объединяет несколько текстур в одну. Они позволяют снизить количество отрисовок (draw calls) и повысить производительность.
Создание атласа.
Создаём атлас: Asset > Create > Sprite Atlas.
Выделяем атлас и помещаем необходимые sprite в Objects for Packing.
Нажимаем Pack Preview для предпросмотра атласа.
Нужно учитывать, что даже если на сцене используется один или несколько спрайтов из атласа, то атлас всё равно будет загружен целиком. Поэтому нет смысла пытаться внедрить все изображения в один гигантский атлас, который на устройствах с небольшой оперативной памятью может занять её ощутимую часть. Лучше разбить на какие-то более мелкие атласы, например на атлас пользовательского интерфейса меню игры и атлас интерфейса игрового режима.
Избегайте больших пустых мест в атласе, чтобы не занимать лишнее место в памяти. Для этого можно изменить размер атласа или добавить дополнительные изображения, чтобы максимально заполнить пустое пространство.
Для изображений, которые не попали в атлас, необходимо выбрать правильные настройки.
К каждому формату сжатия имеются требования, при соблюдении которых оно будет эффективно работать. Самые частые требования к изображениям:
- ширина и высота должны быть кратными степени двойки;
- ширина и высота должны быть кратными 4;
- оба предыдущих пункта вместе.
В противном случае текстура не будет сжата и возникнут дополнительные затраты памяти устройства.
При выборе формата сжатия Unity подскажет, если какие-то требования для выбранного формата не будут выполнены.
Также не забывайте, что форматы отличаются от целевого устройства. Дополнительную информацию и рекомендации по форматам можно посмотреть в официальной документации:
Резюмируем способы оптимизации
- Отключать невидимые и прозрачные объекты
- Минимизировать количество элементов
- Избегать пересечения объектов, не способных запечься друг с другом
- Распределять элементы по холстам в зависимости от частоты обновления
- У холстов с часто обновляющимися элементами отключить Pixel Perfect
- Отключить Raycast Target у не кликабельных элементов
- Удалить компонент Graphic Raycaster на холсте, у которого все элементы не кликабельны
- Для обычных текстовых компонентов не использовать Best Fit
- Для TextMeshPro В World Space использовать TextMeshPro вместо TextMeshProUGUI. TextMeshProUGUI используется в холстах
- Устанавливать камеру в настройках холста, если надо
- Отключать холсты через отключение компонента холста
- Использовать спрайты с градацией серого и раскрашивать настройками компонента изображения
- Использовать спрайтовые атласы
- Использовать правильный размер и формат сжатия для текстур, не попавших в атлас
Источники:
Исчерпывающая информация о оптимизации Unity UI
Ещё один официальный источник по этой теме, содержит информацию в более сжатой форме