Узнайте, как оптимизировать производительность пользовательского интерфейса в Unity, используя это подробное руководство с многочисленными экспериментами, практическими советами и тестами производительности, подтверждающими его эффективность!
Привет! Меня зовут Сергей Бегичев, я разработчик клиентских приложений в Pixonic (MY.GAMES). В этой статье я расскажу об оптимизации пользовательского интерфейса в Unity3D. Хотя рендеринг набора текстур может показаться простым, он может привести к серьезным проблемам с производительностью. Например, в нашем проекте War Robots неоптимизированные версии пользовательского интерфейса составляли до 30% от общей нагрузки на процессор — поразительная цифра!
Обычно эта проблема возникает при двух условиях: во-первых, когда есть много динамических объектов, и во-вторых, когда дизайнеры создают макеты, которые отдают приоритет надежному масштабированию в разных разрешениях. Даже небольшой пользовательский интерфейс может генерировать заметную нагрузку в таких обстоятельствах. Давайте рассмотрим, как это работает, выявим причины нагрузки и обсудим возможные решения.
Сначала давайте рассмотрим
Хотя пункты 2 и 3 интуитивно понятны, остальные рекомендации может быть проблематично вообразить на практике. Например, совет «разделить холсты на подхолсты» безусловно ценен, но Unity не дает четких указаний относительно принципов, лежащих в основе этого разделения. Если говорить обо мне, то с практической точки зрения я хочу знать, где имеет наибольший смысл внедрять подхолсты.
Рассмотрите совет «избегать групп макетов». Хотя они могут способствовать высокой нагрузке на пользовательский интерфейс, многие крупные пользовательские интерфейсы поставляются с несколькими группами макетов, и переделка всего может занять много времени. Более того, дизайнеры макетов, которые избегают групп макетов, могут обнаружить, что тратят значительно больше времени на свои задачи. Поэтому было бы полезно понять, когда следует избегать таких групп, когда они могут быть полезны и какие действия следует предпринять, если мы не можем их устранить.
Эта неоднозначность рекомендаций Unity является основной проблемой — часто неясно, какие принципы следует применять к этим предложениям.
Для оптимизации производительности пользовательского интерфейса важно понимать, как Unity создает пользовательский интерфейс. Понимание этих этапов имеет решающее значение для эффективной оптимизации пользовательского интерфейса в Unity. Мы можем в общих чертах выделить три ключевых этапа в этом процессе:
Layout . Изначально Unity упорядочивает все элементы пользовательского интерфейса на основе их размеров и назначенных позиций. Эти позиции рассчитываются относительно краев экрана и других элементов, образуя цепочку зависимостей.
Пакетирование . Далее Unity группирует отдельные элементы в пакеты для более эффективного рендеринга. Рисование одного большого элемента всегда эффективнее, чем рендеринг нескольких более мелких. (Для более глубокого погружения в пакетирование см.
Рендеринг . Наконец, Unity рисует собранные партии. Чем меньше партий, тем быстрее будет процесс рендеринга.
Хотя в этом процессе задействованы и другие элементы, именно эти три этапа обуславливают большинство проблем, поэтому сейчас давайте сосредоточимся на них.
В идеале, когда наш пользовательский интерфейс остается статичным (то есть ничего не движется и не меняется), мы можем построить макет один раз, создать один большой пакет и эффективно отобразить его.
Однако если мы изменим позицию хотя бы одного элемента, нам придется пересчитать его позицию и перестроить затронутую партию. Если другие элементы зависят от этой позиции, нам придется пересчитать и их позиции, что вызовет каскадный эффект по всей иерархии. И чем больше элементов требуют корректировки, тем выше становится нагрузка на партию.
Таким образом, изменения в макете могут вызвать волновой эффект во всем пользовательском интерфейсе, и наша цель — минимизировать количество изменений. (В качестве альтернативы мы можем попытаться изолировать изменения, чтобы предотвратить цепную реакцию.)
В качестве практического примера, эта проблема особенно выражена при использовании групп макетов. Каждый раз, когда макет перестраивается, каждый LayoutElement выполняет операцию GetComponent, которая может быть весьма ресурсоемкой.
Давайте рассмотрим ряд примеров, чтобы сравнить результаты производительности. (Все тесты проводились с использованием Unity версии 2022.3.24f1 на устройстве Google Pixel 1.)
В этом тесте мы создадим группу макетов, состоящую из одного элемента, и проанализируем два сценария: один, в котором мы изменяем размер элемента, и другой, в котором мы используем свойство FillAmount.
Изменения RectTransform:
Изменения FlllAmount:
Во втором примере мы попробуем сделать то же самое, но в группе макета с 8 элементами. В этом случае мы все равно изменим только один элемент.
Изменения RectTransform:
Изменения FlllAmount:
Если в предыдущем примере изменения в RectTransform привели к нагрузке на макет в 0,2 мс, то на этот раз нагрузка увеличивается до 0,7 мс. Аналогично нагрузка от пакетных обновлений увеличивается с 0,65 мс до 1,10 мс.
Хотя мы все еще изменяем только один элемент, увеличенный размер макета существенно влияет на нагрузку во время перестройки.
Напротив, когда мы настраиваем FillAmount элемента, мы не наблюдаем увеличения нагрузки, даже при большем количестве элементов. Это происходит потому, что изменение FillAmount не запускает перестроение макета, что приводит лишь к небольшому увеличению нагрузки пакетного обновления.
Очевидно, что использование FillAmount является более эффективным выбором в этом сценарии. Однако ситуация становится более сложной, когда мы меняем масштаб или положение элемента. В этих случаях сложно заменить встроенные механизмы Unity, которые не вызывают перестроение макета.
Вот где в игру вступают SubCanvases. Давайте рассмотрим результаты, когда мы инкапсулируем изменяемый элемент в SubCanvas.
Мы создадим группу макета с 8 элементами, один из которых будет размещен в SubCanvas, а затем изменим его преобразование.
Изменения RectTransform в SubCanvas:
Как показывают результаты, инкапсуляция одного элемента в SubCanvas практически устраняет нагрузку на макет; это происходит потому, что SubCanvas изолирует все изменения, предотвращая перестройку на более высоких уровнях иерархии.
Однако важно отметить, что изменения в пределах холста не повлияют на расположение элементов за его пределами. Поэтому, если мы слишком сильно расширим элементы, существует риск того, что они могут перекрывать соседние элементы.
Давайте продолжим, заключив 8 элементов макета в SubCanvas:
Предыдущий пример показывает, что, хотя нагрузка на макет остается низкой, пакетное обновление удвоилось. Это означает, что, хотя разделение элементов на несколько SubCanvases помогает снизить нагрузку на сборку макета, оно увеличивает нагрузку на пакетную сборку. Следовательно, это может привести нас к общему отрицательному эффекту.
Теперь проведем еще один эксперимент. Сначала создадим группу макета из 8 элементов, а затем изменим один из элементов макета с помощью аниматора.
Аниматор изменит RectTransform на новое значение:
Здесь мы видим тот же результат, что и во втором примере, где мы все изменили вручную. Это логично, потому что не имеет значения, что мы используем для изменения RectTransform.
Аниматор изменяет RectTransform на похожее значение:
Аниматоры ранее сталкивались с проблемой, когда они постоянно перезаписывали одно и то же значение в каждом кадре, даже если это значение оставалось неизменным. Это непреднамеренно запускало перестроение макета. К счастью, более новые версии Unity решили эту проблему, избавив от необходимости переключаться на альтернативные
Теперь давайте рассмотрим, как изменение текстового значения ведет себя в группе макета с 8 элементами и вызывает ли это перестройку макета:
Мы видим, что перестройка также запущена.
Теперь изменим значение TextMechPro в группе макета из 8 элементов:
TextMechPro также запускает перестройку макета, и, похоже, это даже создает большую нагрузку на пакетную обработку и рендеринг, чем обычный Text.
Изменение значения TextMechPro в SubCanvas в группе макета из 8 элементов:
SubCanvas эффективно изолировал изменения, предотвращая перестроение макета. Тем не менее, хотя нагрузка на пакетные обновления снизилась, она остается относительно высокой. Это становится проблемой при работе с текстом, поскольку каждая буква рассматривается как отдельная текстура. Изменение текста, следовательно, влияет на несколько текстур.
Теперь давайте оценим нагрузку, возникающую при включении и выключении GameObject (GO) в группе макета.
Включение и выключение GameObject внутри группы макета из 8 элементов:
Как мы видим, включение или выключение GO также запускает перестройку макета.
Включение GO внутри SubCanvas с группой макетов из 8 элементов:
В этом случае SubCanvas также помогает снять нагрузку.
Теперь давайте проверим, какова будет нагрузка, если мы включим или выключим весь GO с помощью группы макетов:
Как показывают результаты, нагрузка достигла своего самого высокого уровня. Включение корневого элемента запускает перестройку макета для дочерних элементов, что, в свою очередь, приводит к значительной нагрузке как на пакетирование, так и на рендеринг.
Итак, что мы можем сделать, если нам нужно включить или отключить целые элементы пользовательского интерфейса, не создавая при этом чрезмерной нагрузки? Вместо включения и отключения самого GO, вы можете просто отключить компонент Canvas или Canvas Group. Кроме того, установив альфа-канал Canvas Group на 0, можно добиться того же эффекта, избежав при этом проблем с производительностью.
Вот что происходит с нагрузкой, когда мы отключаем компонент Canvas Group. Поскольку GO остается включенным, пока холст отключен, макет сохраняется, но просто не отображается. Такой подход не только обеспечивает низкую нагрузку на макет, но и значительно снижает нагрузку на пакетирование и рендеринг.
Далее давайте рассмотрим влияние изменения SiblingIndex в группе макета.
Изменение SiblingIndex внутри группы макета из 8 элементов:
Как видно, нагрузка остается значительной, 0,7 мс для обновления макета. Это ясно указывает на то, что изменения в SiblingIndex также вызывают перестройку макета.
Теперь давайте поэкспериментируем с другим подходом. Вместо того, чтобы менять SiblingIndex, мы поменяем местами текстуры двух элементов в группе макета.
Перестановка текстур двух элементов в группе макета из 8 элементов:
Как видим, ситуация не улучшилась, а наоборот, ухудшилась. Замена текстуры также вызывает перестроение.
Теперь давайте создадим пользовательскую группу макета. Мы построим 8 элементов и просто поменяем местами два из них.
Пользовательская группа макетов из 8 элементов:
Нагрузка действительно значительно снизилась — и это ожидаемо. В этом примере скрипт просто меняет местами позиции двух элементов, устраняя тяжелые операции GetComponent и необходимость пересчета позиций всех элементов. В результате для пакетирования требуется меньше обновлений. Хотя этот подход кажется панацеей, важно отметить, что выполнение вычислений в скриптах также вносит вклад в общую нагрузку.
По мере того, как мы усложняем нашу группу макетов, нагрузка неизбежно увеличится, но это не обязательно отразится в разделе Layout, поскольку вычисления происходят в скриптах. Поэтому крайне важно самостоятельно отслеживать эффективность кода. Однако для простых групп макетов индивидуальные решения могут быть отличным вариантом.
Перестройка макета представляет собой значительную проблему. Чтобы решить эту проблему, мы должны определить ее основные причины, которые могут быть разными. Вот основные факторы, которые приводят к перестройкам макета:
Важно выделить несколько аспектов, которые больше не вызывают проблем в новых версиях Unity, но вызывали их в более ранних: перезапись одного и того же текста и многократная установка одного и того же значения с помощью аниматора.
Теперь, когда мы определили факторы, вызывающие перестройку макета, давайте обобщим наши варианты решений:
Оберните GameObject (GO), который запускает перестроение, в SubCanvas. Такой подход изолирует изменения, не давая им влиять на другие элементы выше по иерархии. Однако будьте осторожны — слишком много SubCanvas могут значительно увеличить нагрузку на пакетирование.
Включайте и выключайте SubCanvas или Canvas Group вместо GO. Используйте пул объектов вместо создания новых GO. Этот метод сохраняет макет в памяти, позволяя быстро активировать элементы без необходимости перестроения.
Используйте шейдерные анимации. Изменение текстуры с помощью шейдера не вызовет перестройку макета. Однако имейте в виду, что текстуры могут перекрываться с другими элементами. Этот метод эффективно служит той же цели, что и использование SubCanvases, но требует написания шейдера.
Замените группу макетов Unity на пользовательскую группу макетов. Одна из ключевых проблем с группами макетов Unity заключается в том, что каждый LayoutElement вызывает GetComponent во время перестроения, что является ресурсоемким. Создание пользовательской группы макетов может решить эту проблему, но у нее есть свои сложности. Пользовательские компоненты могут иметь особые эксплуатационные требования, которые вам необходимо понимать для эффективного использования. Тем не менее, этот подход может быть более эффективным, особенно для более простых сценариев групп макетов.