Unity 6.3
0 онлайн 86 гостей 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».