Unity 6.3
0 онлайн 55 гостей 3 в системе
Вход
UI Toolkit для продвинутых разработчиков Глава 13 из 14 Оригинал, стр. 130

Оптимизация производительности

Русский

Оптимизация производительности

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

К счастью, большинство таких проблем решается оптимизацией. В Unity 6 UI Toolkit заметно усовершенствован и уже в стандартной конфигурации работает быстрее, однако по-настоящему эффективный интерфейс всё равно требует усилий разработчика.

Во многом задача сводится к устранению лишних накладных расходов и сокращению количества вызовов отрисовки. Рассмотрим несколько способов оптимизации UI Toolkit в Unity 6.

художников |

Механизмы обновления

Визуальное дерево использует несколько механизмов обновления.

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

Описание

Когда срабатывает

Влияние

Разрешение стилей

Определяет окончательный вид элементов, применяя селекторы и стили USS.

При изменении классов или стилей, например при добавлении класса стиля или смене цвета.

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

Перерасчёт компоновки

Корректирует размер и положение При изменении размера, положения элементов в иерархии интерфейса. или выравнивания элемента, например размера панели или положения элементов.

Частые обновления компоновки требуют значительных ресурсов. Для анимации используйте преобразования.

Обновление буфера вершин

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

Обновление буферов вершин ресурсоёмко. Избегайте частых изменений геометрии.

Изменение состояния рендеринга

Меняет необходимые для отрисовки состояния, например текстуры и режимы смешивания.

Частая смена состояний повышает нагрузку на CPU. Применяйте пакетную обработку и ограничивайте уникальные текстуры и маски.

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

Разумеется, стоимость этих операций зависит от частоты и масштаба изменений элементов интерфейса.

художников |

Пакетная обработка элементов При отрисовке интерфейса для каждого визуального элемента необходимо отправить инструкции на GPU. UI Toolkit оптимизирует эти вызовы отрисовки с помощью пакетной обработки: элементы с одинаковыми требованиями к GPU объединяются и обрабатываются вместе. Это значительно сокращает затраты на обмен данными с GPU, подобно пакетной обработке вызовов отрисовки для GameObject.

Для эффективного объединения элементы должны использовать одно и то же состояние GPU: одинаковые шейдеры, текстуры, данные сетки и другие параметры. Например, последовательность текстовых элементов с одним шрифтом и стилем можно обработать одним пакетом. Если вставить между ними изображение, потребуются другие параметры GPU и будет создан новый пакет. Разрыв пакетов снижает производительность.

Каждый такой разрыв пакета немного снижает эффективность. Для высокой производительности структуру интерфейса следует организовать так, чтобы свести число разрывов к минимуму. Поскольку каждый пакет может породить один или несколько вызовов отрисовки на GPU, меньшее число пакетов обычно означает меньшие накладные расходы и более высокую производительность. Далее рассмотрим способы сократить количество пакетов и добиться стабильной работы интерфейса.

Буферы вершин В UI Toolkit буферы вершин хранят геометрию, необходимую для отрисовки интерфейса. Когда UIDocument создаёт Panel во время выполнения, для визуальных элементов заранее выделяется один буфер вершин. Его можно представить как распределитель памяти в куче: по мере добавления элементов в интерфейс он динамически выделяет им память. Если интерфейс превышает ёмкость буфера, создаются дополнительные буферы. Это может раздробить пакеты, увеличить число вызовов отрисовки и в итоге снизить производительность. Начальный размер буфера настраивается параметром Vertex Budget в Panel Settings. Значение по умолчанию 0 позволяет Unity определить размер автоматически. Однако для сложных интерфейсов ручное увеличение этого значения иногда повышает производительность за счёт сокращения числа вызовов отрисовки.

художников |

Рассмотрим пример. Интерфейс содержит множество элементов, которые не помещаются в один буфер вершин. Frame Debugger показывает, что в результате вместо одного вызова отрисовки выполняются два.

Для этого интерфейса требуется несколько вызовов отрисовки.

Если, например, увеличить Vertex Budget до 20 000 вершин, элементы интерфейса могут поместиться в одном буфере и отрисоваться за один вызов. Таким образом, изменение единственного параметра повышает эффективность интерфейса в нашем примере.

Увеличение Vertex Budget может сократить число вызовов отрисовки.

художников |

Настройка Vertex Budget восстанавливает отрисовку одним вызовом.

Для сложных интерфейсов ручное увеличение этого значения может повысить производительность благодаря сокращению числа вызовов отрисовки, но избегайте избыточного выделения памяти. С помощью Frame Debugger и Unity Profiler найдите оптимальный баланс между объёмом памяти и количеством вызовов отрисовки.

Универсальный шейдер и ограничение в восемь текстур UI Toolkit объединяет всю функциональность отрисовки интерфейса в одном универсальном uber-шейдере. Вместо нескольких вариантов шейдера он использует динамическое ветвление, выбирая нужный путь рендеринга во время выполнения. Благодаря меньшему числу переключений шейдеров снижается нагрузка на CPU, хотя логика ветвления несколько увеличивает стоимость обработки на GPU. Одна из важных возможностей этого шейдера - поддержка до восьми текстур в одном пакете. Поэтому элементы с разными текстурами можно отрисовать одним вызовом. На следующем изображении показан интерфейс, состоящий из восьми разных текстур:

художников |

Интерфейс в этом примере содержит восемь текстур.

Универсальный uber-шейдер отрисовывает их одним вызовом.

художников |

Как видно в Frame Debugger, Unity отрисовывает пример интерфейса с восемью текстурами одним вызовом. Однако при превышении этого ограничения система вынуждена разделять элементы на отдельные пакеты, увеличивая накладные расходы. Ниже показано, что происходит при использовании более восьми текстур: вместо одного вызова отрисовки выполняется сразу несколько.

Избыток текстур приводит к разрыву пакетов.

Чтобы обойти это ограничение, UI Toolkit предоставляет инструменты оптимизации текстур. Например, объединение текстур в атласы позволяет удержать их число в поддерживаемых пределах, сохранить эффективность пакетной обработки и сократить количество вызовов отрисовки.

Динамические атласы текстур Переключение между множеством текстур может вынудить UI Toolkit разбивать пакеты, увеличивая число вызовов отрисовки и снижая производительность. Распространённое решение - атлас текстур, объединяющий несколько небольших текстур в одну крупную. Если вы знакомы с 2D Sprite Atlas, то уже знаете эффективный способ повысить производительность. Когда несколько спрайтов упакованы в атлас, Unity обрабатывает их как одну текстуру, что сокращает разрывы пакетов и вызовы отрисовки.

2D Sprite Atlas без труда интегрируется с UI Toolkit и отлично подходит для статического или заранее подготовленного содержимого. Но у него есть ограничения: например, он не поддерживает текстуры, создаваемые во время выполнения. Кроме того, атлас требует предварительной настройки и компоновки спрайтов, что может занять немало времени.

художников |

В примере Dragon Crashers используется 2D Sprite Atlas.

Динамический атлас текстур UI Toolkit эффективно объединяет несколько изображений в одну текстуру, сокращая число изменений состояния. Параметры атласа настраиваются в Panel Settings, а его компоновку можно просмотреть в Dynamic Atlas Viewer, доступном в окне UI Toolkit Debugger.

Используйте Dynamic Atlas Viewer в отладчике UI Toolkit.

Если интерфейс значительно меняется со временем, например в него часто добавляются и из него удаляются текстуры, атлас может фрагментироваться. В таком случае API ResetDynamicAtlas восстановит его исходное состояние. В UI Toolkit можно одновременно использовать 2D Sprite Atlas и динамические атласы текстур. Атласы спрайтов лучше всего подходят для статического, заранее подготовленного содержимого, а динамические атласы - для интерфейса, содержимое которого формируется во время выполнения.

художников |

Маскирование UI Toolkit создаёт маски - области, показывающие или скрывающие части элементов интерфейса, - с помощью буфера трафарета. Поскольку этот буфер входит в состояние GPU, изменение параметров маски может привести к разрыву пакетов. Иерархическое наложение маскированных элементов дополнительно усложняет обработку: на каждом уровне вложенности буфер трафарета должен отслеживать ещё одно состояние, что повышает нагрузку на GPU. UI Toolkit поддерживает два типа маскирования: - Прямоугольное маскирование. Прямоугольные маски используют операции на уровне шейдера, сохраняя целостность пакетов без изменения состояния GPU. Буфер трафарета при этом не задействуется, поэтому глубина вложенности прямоугольных масок не ограничена. - Скруглённые углы и сложные маски (буфер трафарета). Для скруглённых углов и других сложных форм требуются операции с буфером трафарета, которые могут разрывать пакет на каждом уровне маскирования. Поддерживается до семи уровней вложенности.

Прямоугольные маски и маски со скруглёнными углами

Чтобы повысить производительность маскированных элементов:

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

- Сведите глубину вложенности масок к минимуму. Плоская иерархия требует меньше перерасчётов буфера трафарета. - По возможности применяйте одну маску к родительскому элементу вместо нескольких масок у дочерних элементов. - Если без нескольких уровней маскирования не обойтись, примените указание по использованию Mask Container, чтобы оптимизировать настройку состояния буфера трафарета. Используйте его умеренно, поскольку оно может привести к разрыву пакетов.

Используйте указание по использованию Mask Container.

художников |

Наконец, оцените результат оптимизации в Frame Debugger и убедитесь, что отрисовка и пакетная обработка выполняются эффективно.

Анимации и переходы Переходы USS в UI Toolkit позволяют создавать простую анимацию свойств, однако изменение параметров компоновки, например размера или положения, может запустить дорогостоящий перерасчёт. Снизить нагрузку можно несколькими способами.

Прежде всего используйте анимацию преобразований вместо изменения свойств компоновки. Не анимируйте width, height, top или left применяйте преобразования translate, scale и rotate. Эти операции обрабатываются непосредственно на GPU и не требуют перерасчёта компоновки, поэтому анимация получается плавнее.

Анимируйте преобразования, а не свойства компоновки.

Для любого анимируемого визуального элемента можно также включить указания по использованию. Указание DynamicTransform предписывает UI Toolkit обрабатывать изменения положения и преобразований на GPU, не выполняя дорогостоящий перерасчёт данных вершин.

художников |

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

Указания по использованию доступны для каждого визуального элемента.

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

Наконец, контролируйте производительность анимации с помощью Unity Frame Debugger: этот инструмент поможет проверить, что оптимизация даёт ожидаемый результат.

художников |

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

Контейнеры свойств и генерация исходного кода Контейнер свойств (property bag) - вспомогательный объект для эффективного обхода и изменения данных определённого типа. По умолчанию Unity создаёт контейнер с помощью рефлексии при первом обращении к типу. Такой подход удобен, но вносит небольшие накладные расходы во время выполнения: генерация выполняется отложенно, если контейнер ещё не зарегистрирован. Для повышения производительности можно включить генерацию кода контейнеров свойств. Пометьте тип атрибутом [Unity.Properties.GeneratePropertyBag] и убедитесь, что для сборки также включена генерация кода. Тогда Unity создаст и зарегистрирует контейнер на этапе компиляции, исключив рефлексию во время выполнения. Подробнее см. в документации по контейнерам свойств.

Атрибут GeneratePropertyBag оптимизирует тип целиком, а CreateProperty у отдельных свойств позволяет Unity сгенерировать код привязки на этапе компиляции. Системе не приходится искать и связывать свойства через рефлексию во время выполнения, поэтому привязка данных работает быстрее и эффективнее.

Во многих случаях для её оптимизации достаточно одного [CreateProperty]. Если тип нуждается и в других оптимизациях, например в эффективной сериализации или частом обходе всех свойств, сочетание [CreateProperty] и [GeneratePropertyBag] обеспечивает наилучшую общую производительность.

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

художников |

Ниже показан пример ScriptableObject, реализующего эти оптимизации:

C#
[CreateAssetMenu(fileName = "CarData", menuName = "Scriptable Objects/CarData"), GeneratePropertyBag] public class CarData : ScriptableObject, INotifyBindablePropertyChanged, IDataSourceViewHashProvider {
    private long _version;
    [SerializeField, DontCreateProperty] string _name;
    public event EventHandler<BindablePropertyChangedEventArgs> propertyChanged;
    [CreateProperty] public string Name {
        get => _name;
        set { _name = value; _version++; Notify(); }
    }
    void Notify([CallerMemberName] string property = "") {
        propertyChanged?.Invoke(this, new BindablePropertyChangedEventArgs(property));
    }
    public long GetViewHashCode() => _version;
}

Класс из примера использует [GeneratePropertyBag], чтобы создать контейнер свойств на этапе компиляции, и [CreateProperty], чтобы оптимизировать привязку свойства Name во время выполнения. Для отслеживания изменений класс реализует INotifyBindablePropertyChanged. Метод Notify вызывает событие propertyChanged при каждом обновлении Name, сообщая об изменении интерфейсу и всем подписчикам. Класс также реализует IDataSourceViewHashProvider. Метод GetViewHashCode возвращает версионный хеш, который увеличивается при каждом изменении Name, поэтому обновления легко обнаружить.

художников |

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

Обновление привязок

Обновление компоновки

Стоимость рендера

Расчёт стилей

Стоимость смены

Память стилей

Память мешей

Opacity: 0

Высокая

Низкая

Полная

За экраном

Средняя

Низкая

Полная

Visibility: Hidden

Средняя

Средняя

Трафарет

Display: None

Нет

Нет

Нет

Средняя

Полная

Удаление из иерархии

Нет

Нет

Нет

Нет

Высокая

Нет

Нет

Переключайте видимость элементов разными способами.

Если установить opacity в 0 или переместить элемент за пределы экрана, он по-прежнему обрабатывается GPU и системой компоновки, поэтому стоимость отрисовки остаётся средней или высокой. Эти способы подходят для переходов, но не сокращают расход памяти и затраты на компоновку. Если присвоить свойству visible значение false, элемент не отрисовывается, но остаётся частью компоновки. Это компромиссный вариант для временного скрытия элемента, при котором сохраняются связанные с ним затраты памяти. Более эффективный способ - установить style.display в DisplayStyle.None: и отрисовка, и обновление компоновки полностью прекращаются. Однако при повторном отображении элемента компоновку придётся пересчитать. Редко появляющиеся элементы, например диалоговые окна и панели настроек, можно удалить из иерархии методом RemoveFromHierarchy, чтобы исключить постоянные накладные расходы. Учтите, что при повторном добавлении возникнет более заметный всплеск нагрузки, поскольку компоновку придётся построить заново.

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

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

художников |

Снизить влияние избыточной отрисовки на производительность помогут следующие приёмы:

- Чтобы полностью скрыть элемент, используйте style.display = DisplayStyle.None вместо style.opacity = 0, при котором прозрачный элемент всё равно отрисовывается. - Не накладывайте без необходимости несколько элементов друг на друга: удаляйте или скрывайте те, которые полностью перекрыты. - Для прокручиваемого содержимого реализуйте виртуализацию с помощью ListView. Этот элемент эффективно отрисовывает только видимые на экране строки.

- Также можно задать style.overflow = Overflow.Hidden, чтобы обрезать содержимое по границам заданной области и не отрисовывать невидимые фрагменты.

Управление памятью Файлы USS и UXML содержат прямые ссылки на шрифты, текстуры и другие ресурсы. При загрузке таких файлов в память загружаются и все ресурсы по ссылкам, что может увеличить её расход. Ниже показаны ресурсы, на которые ссылается пример USS:

USS содержит ссылки на ресурсы.

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

художников |

Для оптимизации использования ресурсов применяйте следующие стратегии:

- Используйте AssetBundle или Addressables. По возможности загружайте только те документы интерфейса и таблицы стилей, которые нужны в текущей сцене или контексте. Это помогает контролировать расход памяти. - Выгружайте ненужные ресурсы. Если элемент или документ интерфейса больше не используется, удалите его из иерархии методом RemoveFromHierarchy, а затем выгрузите через Addressables.Release или AssetBundle.Unload(true), освободив память для других операций. - Загружайте части сложного интерфейса выборочно. Разделите крупные UXML- или USS-файлы на небольшие модульные шаблоны VisualTreeAsset и загружайте их динамически по мере необходимости. Если в память попадают только ресурсы видимых элементов, её расход остаётся низким.

Инструменты профилирования Unity предоставляет несколько инструментов для поиска и устранения проблем с производительностью интерфейса. Unity Profiler, UI Toolkit Debugger и Frame Debugger незаменимы при диагностике: с их помощью можно анализировать вызовы отрисовки, пакеты и дорогостоящие операции перерасчёт компоновки, обновление стилей и изменение буфера вершин. Для более подробного отслеживания изменений интерфейса используйте метод SetPanelChangeReceiver из Panel Settings. Он позволяет получать уведомления об изменениях и определять их источник. Метод доступен только в редакторе и отладочных сборках, но помогает выявить конкретное поведение интерфейса, вызывающее замедление. Ниже приведён скрипт, который записывает в журнал каждое изменение интерфейса:

C#
using UnityEngine;
using UnityEngine.UIElements;
public class PanelChangeReceiver : MonoBehaviour, IDebugPanelChangeReceiver {
    [SerializeField] PanelSettings m_PanelSettings;
    void Awake() {
        m_PanelSettings.SetPanelChangeReceiver(this);
    }
    void OnDestroy() {
        m_PanelSettings.SetPanelChangeReceiver(null);
    }

художников |

C#
public void OnVisualElementChange(VisualElement element, VersionChangeType changeType) {
    Debug.Log($"{element.name}
    {changeType}");
}
}

Добавьте этот компонент к GameObject и задайте PanelSettings в Inspector. Метод OnVisualElementChange срабатывает при любом изменении визуального элемента, например его компоновки, стиля или преобразования, и выводит сообщение в консоль. Так можно понять, какая часть интерфейса изменяется в данный момент.

Улучшения производительности в Unity 6 В Unity 6 реализован широкий набор оптимизаций, обеспечивающих плавную и отзывчивую работу как в редакторе, так и во время выполнения: - Распространение событий. Правила распространения событий упрощены: теперь их легче понять, а обработка выполняется вдвое быстрее. - Улучшенная генерация сеток. Геометрия стандартных элементов теперь создаётся в системе заданий, векторный API переведён на нативную реализацию, а генерация текста распараллелена. - API пользовательской геометрии. Новый общедоступный API позволяет разработчикам создавать собственную геометрию с тем же уровнем производительности и строить высокооптимизированные компоненты интерфейса.

- Производительность компоновки в глубоких иерархиях. Улучшенное кэширование вычислений компоновки заметно ускоряет работу глубоких иерархий и делает взаимодействие плавнее. - Оптимизированный TreeView для больших наборов данных. Элемент TreeView, ранее неэффективный на больших объёмах данных, получил новый высокопроизводительный внутренний механизм, специально предназначенный для Entities.

Дополнительные материалы по оптимизации производительности - Unite 2024: «Максимальная производительность с UI Toolkit». - Электронная книга: «Полное руководство по профилированию игр Unity». - Электронная книга: «Оптимизация производительности игр для мобильных устройств, XR и веб-платформ в Unity». - Электронная книга: «Оптимизация производительности игр для консолей и ПК в Unity».

English

Optimizing performance

Building a sophisticated game UI often means managing a large hierarchy of onscreen elements. With hundreds of elements in play, this can cause technical challenges. Even subtle inefficiencies can build up into stutters or hitches at runtime – and those can negatively impact the player experience. The good news is that most of these challenges can be addressed through some optimization. While Unity 6 brings significant UI Toolkit improvements for better out-of-thebox performance, a truly efficient user interface still takes some effort on the part of the developer. Much of this work often comes down to eliminating unnecessary overhead and reducing draw calls. Let’s explore some tips for optimizing UI Toolkit in Unity 6 to help you get the most out of it.

Update mechanisms

The visual tree includes several update mechanisms.

The visual tree contains several update mechanisms that respond to changes in styles, layout, or content at runtime. Any one of these update mechanisms can affect performance. The following table provides a summary of when they occur, and each one’s impact on performance. Update mechanism

Description

When it happens

Performance impact

Style resolution

Determines the final appearance of elements by applying USS selectors and styles

Triggered when classes or styles are changed, such as adding a style class or modifying a color

Large or deeply nested hierarchies make this process expensive. Minimize frequent changes.

Layout recalculation

Adjusts the size and position of elements to fit correctly within the UI hierarchy

Triggered by changes to element size, position, or alignment, e.g., resizing a panel or moving elements

Frequent layout updates can be costly. Use transforms for animations instead of altering positions directly.

Vertex buffer updates

Updates geometric shapes used to render UI elements, like rectangles or rounded corners

Triggered when an element’s geometry changes, such as adding rounded corners or modifying borders

Updating vertex buffers is resource-intensive. Avoid frequent geometry changes.

Rendering state changes

Changes rendering states like textures and blending modes required to draw elements

Triggered by features like masking or unique textures, that disrupt batching

Excessive state changes increase CPU overhead. Optimize by batching and limiting unique textures or masks.

The cost of these operations, of course, depends on how often and how extensively you modify UI elements.

Batching elements When rendering a user interface, every visual element requires instructions to be sent to the GPU. UI Toolkit optimizes these draw calls through batching. This groups visual elements with identical GPU requirements together so they can be processed together. Batching significantly reduces the communication overhead with the GPU, much like draw call batching with GameObjects.

Breaking batches reduces performance.

To batch elements efficiently they must share the same GPU state – the same shaders, textures, mesh data, and other GPU-specific parameters. For example, a sequence of text elements using the same font and style can be batched together. However, inserting an image between them requires different GPU settings. That forces a new batch.

Every batch "break" like this introduces a small inefficiency. To maintain high performance, it’s essential to structure your UI to minimize these breaks. Since every batch may issue one or more draw calls to the GPU, fewer batches generally mean reduced overhead and better performance. In the next sections, let’s explore techniques to optimize your UI’s batch count and achieve consistent performance.

Vertex buffers In UI Toolkit, vertex buffers store the geometry (vertices) needed to render your UI. When a UIDocument creates a Panel at runtime, it pre-allocates a single vertex buffer to handle the visual elements. Think of this buffer as a "heap allocator" for visual elements, dynamically allocating memory as elements are added to the UI. If the UI exceeds the capacity of the vertex buffer, additional buffers are created. This can fragment batching, increase the number of draw calls, and ultimately reduce performance. To address this, you can adjust the Vertex Budget in the Panel Settings to configure the initial size of the vertex buffer. The default value is 0, allowing Unity to determine the size automatically. However, for complex UIs, manually increasing this value can improve performance by reducing the number of draw calls.

Here’s an example. This UI contains a lot of elements that can’t fit within a single vertex buffer. The Frame Debugger shows that this results in two draw calls instead of one.

This UI requires more than one draw call.

Increasing the Vertex Budget to a value to 20,000 vertices, for instance, may mean that the framebuffer can fit the UI elements into a single draw call. This makes our example UI more efficient by changing one setting.

Increasing the Vertex Budget may reduce draw calls.

Adjusting the Vertex Budget restores the one draw call.

For complex UIs, manually increasing this value may improve performance by reducing draw calls, but be careful of over-allocating memory. Use the Frame Debugger and Unity Profiler to find the best balance between memory usage and number of draw calls.

Uber shader and eight-texture limit UI Toolkit consolidates all UI rendering functionality into a single versatile "uber shader." Rather than rely on multiple shader variants, this shader uses dynamic branching to select the appropriate rendering path at runtime. This reduces CPU overhead by minimizing shader switches but does add some GPU cost due to the branching logic. One feature that makes this shader powerful is its support for up to eight textures within the same batch. This allows elements with different textures to render in the same draw call. In the image (in the next page?) you can see a UI consisting of eight different textures:

This example UI contains eight textures.

The "uber shader" renders as one draw call.

As shown in the Frame Debugger, Unity renders an example UI using one draw call for up to eight textures. However, exceeding the eight-texture limit forces the batching system to split into separate batches, increasing overhead. Here’s what happens if you exceed the eight-texture limit. The one draw call is now many more:

Too many textures break the batches.

To mitigate this limitation, UI Toolkit provides tools to optimize texture usage. For example, consolidating textures into atlases helps keep the number of textures within the supported limit, preserving batch efficiency and reducing draw calls.

Dynamic texture atlases Switching between multiple textures can force UI Toolkit to break batches, increasing draw calls and reducing performance. A common solution to this problem is texture atlasing, which combines smaller textures into a single larger texture. If you’re familiar with the 2D Sprite Atlas, you already know an effective way to improve performance. By packing multiple sprites into a sprite atlas, Unity treats them as a single texture, reducing batch breaks and draw calls. The 2D Sprite Atlas integrates seamlessly with UI Toolkit, making it a great choice for static or pre-defined content. However, the 2D Sprite Atlas has limitations, such as the inability to handle runtime-generated textures. It also requires some setup and sprite layout ahead of time, which can be timeconsuming.

The Dragon Crashers sample uses a 2D Sprite Atlas.

UI Toolkit’s dynamic texture atlas effectively merges multiple images into one texture, reducing texture state changes. You can configure atlas settings in Panel Settings and visualize the atlas layout in the Dynamic Atlas Viewer (available in the UI Toolkit Debugger window).

Use the Dynamic Atlas Viewer in the Frame Debugger.

Note that if your UI undergoes extensive changes (such as adding and removing many textures over time), the atlas may become fragmented. In such cases, the ResetDynamicAtlas API can restore the atlas to its initial state. Remember that you can use the 2D Sprite Atlas and dynamic texture atlases side-by-side within UI Toolkit. Sprite Atlases are ideal for static, predefined content, while dynamic atlases excel in situations where UI content is runtime-driven.

Masking UI Toolkit uses the stencil buffer to create masks – areas that show or hide parts of UI elements. Since the stencil buffer is part of the GPU state, changing mask settings can force UI Toolkit to break batches. Be aware that hierarchically layering masked elements adds complexity, as each nested depth requires the stencil buffer to track additional states. That increases the GPU workload. UI Toolkit supports two types of masking: —

Rectangular-based masking: Rectangular masks use shader-based operations, preserving batch consistency without GPU state changes. This technique doesn’t use the stencil buffer, so you can nest rectangular masks without depth limits.

Rounded Corners and Complex Masks (stencil buffer): Rounded corners and other complex shapes require stencil buffer operations, potentially breaking batches at each masking level. This technique supports up to seven nested levels of masking.

Rectangular versus rounded corners masks

To optimize performance for masked elements: —

Use rectangular masks when possible to avoid stencil operations.

Minimize the nesting depth of masks. Keeping masks flat in the hierarchy ensures fewer stencil recalculations.

When possible, use a single mask over a parent element instead of multiple masks over child elements.

When multiple masking layers are unavoidable, apply the Mask Container usage hint to optimize stencil state setup. However, use this sparingly to prevent batch breaks.

Use a Mask Container usage hint.

Finally, verify the impact of these optimizations using the Frame Debugger to ensure efficient rendering and batching.

Animations and transitions While UI Toolkit’s USS transitions offer simple property animations, changing layout properties like size or position can trigger expensive layout recalculations. To optimize animations and reduce performance overhead, you can try several strategies. First, prioritize transform-based animations over layout property changes. Instead of animating properties like width, height, top, or left, use translate, scale, or rotate transforms. These operations are processed directly on the GPU, avoiding the need for layout recalculations. That can result in smoother animations.

Animate transforms instead of layout properties.

You can also enable usage hints for any visual element that needs to be animated. The DynamicTransform hint instructs UI Toolkit to handle position and transform updates on the GPU, bypassing expensive vertex data recalculations.

For parent containers with multiple animated children, the GroupTransform hint can significantly reduce overhead. It applies a single transform to the parent, which the GPU efficiently propagates to all child elements, optimizing animations for large groups.

Usage hints are available for each visual element.

Also, as a general rule, avoid switching classes for style changes in large hierarchies during animations. Class changes can trigger extensive style recalculations, especially in complex UI structures. Instead, update styles directly using inline property changes to minimize computational costs. Finally, monitor animation performance using Unity’s Frame Debugger. This tool allows you to verify that these optimizations are working as intended.

Runtime data binding Runtime data binding in Unity simplifies updating UI elements by ensuring they automatically reflect changes in the underlying data. This eliminates the need for manual updates, making UI development more efficient and maintainable. Some techniques can optimize this process:

Property bags and source generation A property bag is a companion object that enables efficient traversal and manipulation of a type’s data. By default, Unity generates property bags using reflection the first time a type is accessed. While this reflective approach is convenient, it introduces a small runtime overhead because it happens lazily – only when the property bag has not been registered yet. To improve performance, you can enable code generation for property bags. Tag the type with [Unity.Properties.GeneratePropertyBag] and ensure the assembly is also tagged for code generation. Unity will then generate and register the property bag at compile time, eliminating the need for reflection during runtime. For more details, refer to the Property bags documentation. While the GeneratePropertyBag attribute optimizes an entire type, adding the CreateProperty attribute to individual properties allows Unity to generate binding code at compile time. This removes the need for runtime reflection to discover and connect properties, ensuring faster and more efficient data binding. In many cases, using the [CreateProperty] alone is enough to optimize runtime data binding. However, if the type requires additional optimizations, like efficient serialization or frequent traversal of all its properties, combining [CreateProperty] with [GeneratePropertyBag] provides the best overall performance.

Change Tracking Runtime data binding includes two interfaces that optimize how often the data bindings can update: —

IDataSourceViewHashProvider: This interface provides hash-based equality checks and ensures that the bindings are updated only when the data has changed meaningfully.

INotifyBindablePropertyChanged: This interface triggers updates only when specific property values change.

These interfaces are especially valuable for complex UIs, preventing unnecessary updates when data hasn’t meaningfully changed.

This ScriptableObject shows a sample that implements these optimizations:

C#
[CreateAssetMenu(fileName = "CarData", menuName = "Scriptable Objects/CarData"), GeneratePropertyBag] public class CarData : ScriptableObject, INotifyBindablePropertyChanged, IDataSourceViewHashProvider {
    private long _version;
    [SerializeField, DontCreateProperty] string _name;
    public event EventHandler<BindablePropertyChangedEventArgs> propertyChanged;
    [CreateProperty] public string Name {
        get => _name;
        set { _name = value; _version++; Notify(); }
    }
    void Notify([CallerMemberName] string property = "") {
        propertyChanged?.Invoke(this, new BindablePropertyChangedEventArgs(property));
    }
    public long GetViewHashCode() => _version;
}
This example class uses [GeneratePropertyBag] to generate a property bag at compile time and [CreateProperty] to optimize runtime data binding for the Name property. For change tracking, it implements INotifyBindablePropertyChanged. The Notify method triggers the propertyChanged event whenever Name is updated. This signals changes to the UI and informs any listeners. The class also implements IDataSourceViewHashProvider. The GetViewHashCode method returns a versioned hash that increases each time Name changes, making it easy to detect updates.

Showing and hiding elements When hiding UI elements, simply changing opacity or moving them off-screen isn’t always the best for performance. Even when hidden, these elements still participate in layout calculations, style updates, and data binding operations, potentially impacting performance. UI Toolkit has a few different ways to hide an element, each with its trade-offs. See this table for a summary.

Toggle elements using different methods.

When hiding UI elements, setting the opacity to 0 or moving them off-screen keeps them visible to the GPU and layout system, with medium to high render costs. These methods are useful for transitions but do not reduce memory or layout overhead. Setting an element’s visible property to false prevents rendering but keeps it as part of the layout. This is a compromise that temporarily hides the element while using stencil memory. For more efficient performance, setting the style.display attribute to DisplayStyle.None stops rendering and layout updates entirely. However, this also involves a cost to recalculate the layout when toggling the element back on. For elements that appear infrequently, like dialog boxes or settings panels, simply remove them from the hierarchy with RemoveFromHierarchy to reduce ongoing overhead. Just be aware that this incurs a higher performance spike when the element is re-added since the layout must be fully rebuilt. Choose methods based on how frequently elements need to be toggled. Then, balance shortterm rendering needs with long-term performance.

Overdraw UI Toolkit renders elements with transparency, which can result in significant overdraw when elements overlap, as each pixel may be processed multiple times. This becomes especially costly with UI Toolkit’s uber shader, which adds complexity to each layer of overlapping elements. Stacking multiple layers of transparent or semi-transparent elements can further impact performance.

Several strategies can help mitigate the performance impact of overdraw: —

Use style.display = DisplayStyle.None to hide elements completely instead of style.opacity = 0, which still renders them as transparent.

Rather than stacking multiple elements on top of each other, remove or hide any elements that are completely obscured.

When working with scrollable content, implement virtualization through ListViews. ListViews can efficiently render only the visible elements on-screen.

You can also set style.overflow = Overflow.Hidden to clip content to specific areas, reducing unnecessary rendering outside visible bounds.

Memory management USS and UXML files reference fonts, textures, and other assets directly. Loading these files pulls all referenced assets into memory, potentially increasing memory usage. Here you can see the assets referenced from an example USS:

A USS references assets.

When these assets are imported, they immediately consume memory – even when not in use. This can lead to inefficient memory use if assets aren’t managed properly.

To optimize asset usage, consider these strategies: —

Use Asset Bundles or Addressables: When possible, only load the UI documents and style sheets required for a particular scene or context. This can help keep memory consumption in check.

Unload assets when not needed: If a UI element or document is no longer in use, remove it from the hierarchy using RemoveFromHierarchy. Then, unload it using Addressables.Release or AssetBundle.Unload(true) to free up memory for other operations.

Selective loading for complex UIs: Break large UXML or USS files into smaller, modular templates (VisualTreeAssets) and load them dynamically as needed. Only loading resources for visible elements helps keep memory usage low.

Profiling tools Unity provides several tools to identify and resolve UI performance issues in your application. The Unity Profiler, UI Toolkit Debugger, and Frame Debugger are essential for diagnosing performance issues. These tools help you analyze draw calls, batches, and expensive operations like layout recalculations, style updates, and vertex buffer changes. For a more granular view of UI changes, use the SetPanelChangeReceiver method from the Panel Settings. This allows you to listen for changes to your UI and track their source. While limited to the Editor and development builds, it is useful for isolating specific UI behaviors that might be causing slowdowns. Here’s an example script that logs every change to the UI:

C#
using UnityEngine;
using UnityEngine.UIElements;
public class PanelChangeReceiver : MonoBehaviour, IDebugPanelChangeReceiver {
    [SerializeField] PanelSettings m_PanelSettings;
    void Awake() {
        m_PanelSettings.SetPanelChangeReceiver(this);
    }
    void OnDestroy() {
        m_PanelSettings.SetPanelChangeReceiver(null);
    }
C#
public void OnVisualElementChange(VisualElement element, VersionChangeType changeType) {
    Debug.Log($"{element.name}
    {changeType}");
}
}

Simply attach this to a GameObject and set the PanelSettings in the Inspector. The OnVisualElementChange method triggers whenever a visual element undergoes a change (e.g., layout, style, transform) and logs a console message. This can help you understand what aspect of the UI is currently being modified.

Unity 6 performance enhancements Unity 6 introduces a wide array of performance improvements to ensure a smooth and responsive experience in both the Editor and runtime environments: —

Event dispatching: Event dispatching rules have been simplified, making them easier to understand and twice as fast.

Mesh generation enhancements: Key improvements include jobified geometry generation for classic element geometry and a transition of the vector API to a native implementation. Text generation is also now parallelized.

Custom Geometry API: A new public API enables developers to generate custom geometry with the same level of performance, allowing for highly optimized UI components.

Deep Hierarchy Layout Performance: Improved caching of layout computations significantly boosts performance in deep hierarchies, providing a smoother user experience.

Optimized TreeView for Large Datasets: The TreeView control, previously inefficient with large datasets, has been enhanced with a new high-performance backend specifically for Entities.

More performance optimization resources Unite 2024: Getting the best performance with UI Toolkit E-book: The ultimate guide to profiling Unity games E-book: Optimize your game performance for mobile, XR, and the web in Unity E-book: Optimize your game performance for consoles and PCs in Unity