Unity 6.3
0 онлайн 85 гостей 3 в системе
Вход

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

Содержание

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

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

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

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

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

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

Механизм обновления Описание Когда это происходит Влияние на производительность
Разрешение стиля Определяет окончательный вид элементов путем применения селекторов и стилей USS Срабатывает при изменении классов или стилей — например, при добавлении стилевого класса или смене цвета Большие или глубоко вложенные иерархии делают этот процесс дорогостоящим.
Пересчет макета Настраивает размер и положение элементов, чтобы они правильно вписывались в иерархию UI Приводится в действие изменениями размера, положения или выравнивания элемента, e.g., изменением размера панели или перемещением элементов Частые обновления макета могут быть дорогостоящими. Используйте преобразования для анимации вместо изменения позиций напрямую.
Обновление вершинного буфера Обновляет геометрические формы, используемые для отображения UI элементов, такие как прямоугольники или закругленные углы Приводится в действие при изменении геометрии элемента, например, при добавлении закругленных углов или изменении границ Обновление буферов вершин ресурсоемко. Избегайте частых изменений геометрии.
Изменения состояния рендеринга Изменяет состояния рендеринга, такие как текстуры и режимы смешивания, необходимые для рисования элементов Приводится в действие такими функциями, как маскировка или уникальные текстуры, которые нарушают пакетную обработку Чрезмерные изменения состояния увеличивают накладные расходы CPU. Оптимизируйте это пакетированием и ограничением числа уникальных текстур или масок.

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

Дозовые элементы

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

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

Разрыв партий снижает производительность.
Разрыв партий снижает производительность.

Для поддержания высокой производительности, важно структурировать ваш UI, чтобы свести к минимуму эти перерывы.

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

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

Вертикальные буферы

В UI Toolkit, вершинные буферы хранят геометрию (вершины), необходимую для рендеринга вашего UI. Когда UIDocument создает Панель во время выполнения, он предварительно выделяет один вершинный буфер для обработки визуальных элементов. Представьте этот буфер как «распределитель кучи» для визуальных элементов, динамически выделяющий память по мере добавления элементов в UI.

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

Чтобы решить эту проблему, вы можете настроить Vertex Budget в панели Настройки для настройки начального размера вершинного буфера. Значение по умолчанию 0, что позволяет Unity автоматически определять размер. Однако для сложных UIs увеличение этого значения вручную может повысить производительность, уменьшив количество вызовов рисования.

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

Этот UI требует более одного вызова draw.
Этому интерфейсу требуется больше одного вызова отрисовки.

Увеличение бюджета вершин до 20 000 вершин, например, может означать, что framebuffer может поместить элементы UI в один вызов рисования. Это делает наш пример UI более эффективным, изменяя одну настройку.

Увеличение бюджета вершины может уменьшить число вызовов.
Увеличение бюджета вершины может уменьшить число вызовов.
Настройка бюджета вершины восстанавливает один вызов игры.
Настройка бюджета вершины восстанавливает один вызов игры.

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

Убер-шайдер и ограничение на восемь текстур

UI Toolkit консолидирует всю функциональность UI в один универсальный “uber shader”. Вместо того, чтобы полагаться на множество вариантов шейдеров, этот шейдер использует динамическое ветвление для выбора подходящего пути рендеринга во время выполнения. Это снижает нагрузку CPU, минимизируя переключения шейдеров, но добавляет некоторую стоимость GPU из-за логики ветвления.

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

Этот пример UI содержит восемь текстур.
Этот пример UI содержит восемь текстур.
Uber shader рендируется как один draw call.
«Убершейдер» отрисовывается одним вызовом отрисовки.

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

Вот что произойдет, если вы превысите предел восьми текстур. Один рисунок вызов теперь намного больше:

Слишком много текстур разрушает пакеты.
Слишком много текстур разрушает пакеты.

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

Динамические текстурные атласы

Переключение между несколькими текстурами может заставить UI Toolkit разорвать пакеты, увеличивая вызов рисования и снижая производительность. Общим решением этой проблемы является текстура atlasing, которая объединяет меньшие текстуры в одну большую текстуру.

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

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

Образец Dragon Crashers использует 2D Sprite Atlas.
Образец Dragon Crashers использует 2D Sprite Atlas.

Динамический атлас текстур UI Toolkit эффективно объединяет несколько изображений в одну текстуру, сокращая изменения состояния текстур. Параметры атласа настраиваются на панели Settings, а раскладку атласа можно посмотреть в просмотрщике динамических атласов (доступен в окне отладчика UI Toolkit).

Используйте просмотрщик динамических атласов в Frame Debugger.
Используйте просмотрщик динамических атласов в Frame Debugger.

Обратите внимание, что если ваш UI претерпевает значительные изменения (например, добавление и удаление многих текстур с течением времени), атлас может стать фрагментированным. В таких случаях ResetDynamicAtlas API может вернуть атлас в его исходное состояние.

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

Маскировка

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

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

UI Toolkit поддерживает два типа маскировки:

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

  • Закругленные углы и сложные маски (буфер шаблона): Закругленные углы и другие сложные формы требуют операций буфера шаблона, потенциально разрушающих пакеты на каждом уровне маскировки. Этот метод поддерживает до семи вложенных уровней маскировки.

Маски прямоугольных и закругленных углов
Маски прямоугольных и закругленных углов

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

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

  • Минимизация глубины вложения масок. Сохранение масок плоскими в иерархии обеспечивает меньшее количество перерасчетов шаблона.

  • Когда это возможно, используйте одну маску над родительским элементом вместо нескольких масок над дочерними элементами.

  • Когда без нескольких слоёв маскирования не обойтись, примените подсказку использования Mask Container, чтобы оптимизировать настройку состояния трафарета. Однако применяйте её осторожно, чтобы не разрывать пакеты.

Используйте подсказку использования Контейнера маски.
Используйте подсказку использования Контейнера маски.

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

Анимации и переходы

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

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

Преобразования Animate вместо свойств макета.
Преобразования Animate вместо свойств макета.

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

Подсказка DynamicTransform инструктирует UI Toolkit обрабатывать изменения позиции и преобразования на GPU, обходя дорогостоящие перерасчеты данных вершин.

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

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

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

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

Привязка данных во время выполнения

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

Некоторые методы могут оптимизировать этот процесс:

Сумки для свойства и генерация источников

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

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

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

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

Отслеживание изменений

Привязка данных времени выполнения включает два интерфейса, оптимизирующих частоту обновления привязок данных:

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

  • INotifyBindablePropertyChanged: этот интерфейс запускает обновления только при изменении конкретных значений свойств.

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

Этот ScriptableObject показывает пример, который реализует эти оптимизации:

[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 для создания пакета свойств во время компиляции и CreateProperty для оптимизации привязки данных в режиме выполнения для Name свойства.

Для отслеживания изменений он реализует INotifyBindablePropertyChanged. Метод Notify вызывает событие propertyChanged каждый раз, когда Name обновляется. Это сигнализирует об изменениях UI и информирует любых слушателей.

Класс также реализует IDataSourceViewHashProvider. GetViewHashCode метод возвращает версионированный хэш, который увеличивается каждый раз Name изменения, что облегчает обнаружение обновлений.

Показать и скрыть элементы

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

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

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

При скрытии элементов UI, установка opacity на 0 или перемещение их вне экрана сохраняет их видимыми для GPU и системы макета, со средними или высокими затратами на рендеринг. Эти методы полезны для переходов, но не уменьшают нагрузку на память или макет.

Установка свойства visible элемента в false предотвращает отображение, но сохраняет его как часть макета. Это компромисс, который временно скрывает элемент при использовании памяти шаблона.

Для повышения производительности установка атрибута style.display на DisplayStyle.None полностью останавливает отображение и обновление макета. Однако это также связано с расходами на перерасчет макета при повторном включении элемента.

Для элементов, которые появляются редко, таких как диалоговые окна или панели настроек, просто удалите их из иерархии с помощью RemoveFromHierarchy, чтобы уменьшить текущие накладные расходы. Только помните, что это приводит к более высокому пику производительности при повторном добавлении элемента, так как макет должен быть полностью перестроен.

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

Овердрафт

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

Несколько стратегий могут помочь смягчить влияние овердрафта на производительность:

  • Используйте style.display = DisplayStyle.None, чтобы скрыть элементы полностью, вместо style.opacity = 0, который всё равно делает их прозрачными.

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

  • При работе с прокручиваемым контентом, реализуйте виртуализацию через ListViews. ListViews может эффективно рендировать только видимые элементы на экране.

  • Вы также можете установить style.overflow = Overflow.Hidden для обрезки контента в определенных областях, уменьшая ненужное отображение за пределами видимых границ.

Управление памятью

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

USS относится к ассетам.
USS относится к ассетам.

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

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

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

Этот метод позволяет отслеживать влияние изменений UI на производительность и определять, не вызывают ли узкие места в производительности отдельные элементы.

Инструменты профилирования

Unity предоставляет несколько инструментов для выявления и решения UI проблем производительности в вашем приложении.

Отладчики Unity Profiler, UI Toolkitи Frame Debugger необходимы для диагностики проблем с производительностью. Эти инструменты помогают анализировать вызовы рисования, пакеты и дорогостоящие операции, такие как перерасчеты макетов, обновления стилей и изменения буферов вершин.

Для более подробного просмотра изменений UI используйте метод SetPanelChangeReceiver из панели Настройки. Это позволяет просматривать изменения в вашем UI и отслеживать их источник. Хотя это ограничивается Редактором и сборками для разработки, это полезно для изолирования специфических поведения UI, которые могут вызывать замедление.

Вот пример скрипта, который регистрирует каждое изменение в UI:

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);
    }
    
    public void OnVisualElementChange(VisualElement element, VersionChangeType changeType)
    {
        Debug.Log($"{element.name} {changeType}");
    }
}

Просто подключите это к GameObject и установите PanelSettings в Inspector. Метод OnVisualElementChange запускается каждый раз, когда визуальный элемент подвергается изменению (e.g., layout, style, transform) и регистрирует сообщение консоли. Это может помочь вам понять, какой аспект UI в настоящее время изменяется.

Unity 6 улучшений производительности

Unity 6 вводит широкий спектр улучшений производительности для обеспечения плавного и чувствительного опыта как в редакторе, так и в среде исполнения:

  • Рассылка событий: Правила рассылки событий были упрощены, сделав их более понятными и в два раза быстрее.

  • Улучшения в генерации сетки: Основные улучшения включают в себя генерацию геометрии для классической геометрии элементов и переход вектора API к нативной реализации. Генерация текста также теперь параллельна.

  • Пользовательская геометрия API: Новый открытый API позволяет разработчикам формировать собственную геометрию с той же производительностью, что даёт возможность создавать сильно оптимизированные компоненты интерфейса.

  • Глубокая Hierarchy производительность макета: Улучшенное кэширование вычислений макета значительно повышает производительность в глубоких иерархиях, обеспечивая более плавный пользовательский опыт.

  • Оптимизированный TreeView для Больших Наборов Данных: Управление TreeView, ранее неэффективное с большими наборами данных, было улучшено новым высокопроизводительным бэкэндом специально для Объектов.

Дополнительные ресурсы

  • Сообщество: Объединение 2024: достижение наилучших результатов с UI Toolkit
  • 📖 Электронная книга: Окончательное руководство по профилированию Unity игр
  • 📖 Электронная книга: Оптимизация производительности игры для мобильных устройств, XR, и веб в Unity
  • 📖 Электронная книга: Оптимизировать производительность игры для консолей и PCs в Unity
  • 📖 Электронная книга: Оптимизировать производительность игры для консолей и PCs в Unity