В этой статье ведущий UI/UX художник Никита Кандыбин и технический UI художник Ольга Кинчак поделятся эффективными базовыми практиками по оптимизации Unity UI, которые используются в компании Banzai Games при создании игровых интерфейсов, а также укажут на подводные камни тех или иных решений в дизайне и верстке. Практически во всех перечисленных ниже пунктах можно обойтись без кода, настраивая компоненты непосредственно в редакторе, а что-то даже предусматривая заранее на стадии проектирования макетов интерфейса.
Нативный Unity UI или, как его еще называют в народе, uGUI представляет собой довольно гибкий и удобный инструмент для создания и редактирования игровых пользовательских интерфейсов. Однако недостаток знаний, осведомленности о специфике работы и неправильное использование возможностей игрового движка рано или поздно приводит к возникновению проблем. Будь то низкая частота кадров, высокая загрузка центрального процессора, перегрев устройства и прочие страшные слова для проекта, который хочет стать (или продолжать оставаться) успешным.
Для мобильных платформ с их ограничениями по железу вопросы производительности и оптимизации становятся камнем преткновения на пути к реализации всего желаемого. Тем не менее это вовсе не повод отказываться от Unity UI — система хороша, просто нужно научиться в ней готовить.
Особенности Canvas
Для начала разберемся немного в том, каким образом организован Unity UI. Менеджментом всего, что связано с отрисовкой пользовательского интерфейса в Unity, занимается Canvas (далее просто “канвас”), являющийся родительским объектом для всех дочерних элементов интерфейса в иерархии сцены.
Любое изменение элементов в канвасе, будь то цвет, размер, позиция, материал, текст, активное состояние и т.д. помечает эти объекты как грязные (dirty), заставляя канвас их перерисовывать. Данный процесс включает в себя анализ изменений, перестроение канваса и его дальнейшее кэширование до тех пор, пока хотя бы один из дочерних элементов не будет вновь помечен как грязный.
Перестроение происходит в несколько этапов: расчет положений объектов в лэйаутах, анализ оптимальных способов их отрисовки, применение масок и пересчет графики грязных объектов. Канвас формирует команды для рендера, которые движок Unity отправляет на GPU и в конечном счете визуализирует.
Каждый из перечисленных этапов связан с целым ворохом потенциальных проблем с оптимизацией, которые мы и постараемся осветить далее.
UI Kit на все случаи жизни
В процессе отрисовки макетов пользовательского интерфейса постепенно формируется UI Kit проекта — все разнообразие графических элементов (кнопок, панелей, фоновых изображений, арта, декора, иконок и т.д), а также правил их компоновки. В дальнейшем все эти элементы будут порезаны на ассеты, залиты в движок и станут конструктором для финальной сборки интерфейса игры. Очень важно заранее продумывать, что и в каком виде попадет в Unity и насколько ресурсоемким станет в итоге.
Использование в UI дизайне таких популярных методов как slicing, tiling и окрашивание является одним из наиболее эффективных и распространенных способов экономии и многократного переиспользования ресурсов проекта.
Так, например, slicing и tiling позволяют хранить исходную графику в размере намного меньшем, чем могут оказаться созданные с ее помощью элементы интерфейса. У объектов сохраняется постоянство формы и при грамотной реализации отсутствуют какие-либо видимые артефакты. Эти приемы особенно полезны при адаптивной верстке в зоопарке мобильных устройств, когда при изменении пропорций экрана интерфейс должен эстетично растягиваться или сужаться.
В свою очередь, окрашивание является прекрасным их дополнением. Вместо сохранения линейки разноцветных копий вы можете подкрасить всего один графический объект, чтобы получить гораздо больше вариаций. Кроме того, что данный метод дает возможность программно управлять различными цветовыми схемами, без необходимости для дизайнера менять что-либо в исходных файлах.
Комбинируя приемы и отрисованные вами ассеты, ничто не мешает создавать сложные интерфейсные конструкции из простых, украшая их универсальными декоративными элементами и тем самым максимально эффективно используя готовые ресурсы. Однако у такого подхода есть и свои нюансы.
Overdraw
И в данном случае нюансами является специфика пайплайна рендеринга интерфейса в Unity и понятие overdraw. Если очень коротко, то overdraw — термин, обозначающий ситуацию, при которой один и тот же пиксель перерисовывается несколько раз (т.е. выполняется лишняя работа по его заполнению).
Дело в том, что все элементы интерфейса в сцене Unity отрисовывает в Transparent по очереди, от самого дальнего к самому близкому, смешивая их цвета при прозрачности — по-умолчанию считая их прозрачными, даже если они таковыми и не являются. А это означает, что рендериться будут абсолютно все активные графические объекты в иерархии, независимо от того, загораживаются ли они визуально другими или нет. При этом большое количество перекрывающихся элементов может привести к чрезвычайному высокому количеству перерисовок пикселей, цвета которых нужно смешать, снижению скорости их заполнения (fill rate’a) и, как следствие, возникновению проблем с производительностью.
C помощью одноименного режима отображения “Overdraw” в редакторе Unity позволяет довольно удобно отслеживать такие вот узкие места и буквально видеть перерисовку в сцене. Пусть такая оптимизация на первый взгляд кажется довольно незначительной, но когда объектов на экране становится много (а в сложных интерфейсах их ОЧЕНЬ много) и все они начинают наслаиваться, даже такие мелочи могут дать существенный прирост к производительности.
В нижнем варианте реализации у кнопки FIGHT тень, подложка и рамка слиты в одну текстуру, в отличие от верхнего, где каждый из этих слоев является самостоятельным спрайтом. Идентичный визуальный результат — разный Overdraw.
Поэтому в процессе создания дизайна интерфейса важно понимать и помнить, что для решения проблем с overdraw пока что лучшим выходом является создание ассетов, которые объединяют в себе как-можно больше декоративных элементов в одной текстуре. Важно уметь балансировать в своих визуальных решениях. Ведь недостатком таких объединенных текстур может стать полная инвариантность их применения и увеличение размера текстурных атласов проекта.
Draw Calls и использование атласов
Все это многообразие графических элементов, из которых состоит интерфейс игры, движку нужно каким-то образом собрать и передать графическому процессору. Чтобы отрисовать объект в сцене, вызывается метод отрисовки draw call и данные о нем передаются GPU. Чем больше отправляется таких вот вызовов за один кадр, тем больше времени занимает процесс рендеринга картинки, что ведет к риску уменьшения частоты кадров (FPS).
Один из наиболее эффективных способов снизить количество вызовов метода отрисовки заключается в использовании текстурных атласов — упаковки ваших текстур в единую, более крупную. Дело в том, что Unity старается автоматически объединять (“батчить”) графику в один draw call, отвечающую определенным критериям, тем самым ускоряя отрисовку кадра. В нашем случае наиболее важным здесь является то, что сбатчиться смогут только те объекты, которые используют одинаковые материалы и общий компонент рендерера (Canvas Renderer — дефолтный рендерер для всей UI графики). Таким образом, если элементы интерфейса в сцене используют стандартный UI материал, то, объединив их графику в один общий текстурный атлас, мы сможем получить тот самый желанный один draw call на рендеринг всей группы.
Однако и тут есть свои моменты. Как уже упоминалось ранее, объекты в кадре отрисовываются от самых дальних к самым близким, то есть от верхних к нижним в иерархии сцены. Если Rect Transform одного графического объекта хоть немного пересечется с другим, оба будут считаться накладывающимися друг на друга. При этом если в сцене между объектами, которые могли бы влезть в один draw call, ненароком вклинится объект с другим материалом, то он прервет объединение.
Так, например, текст является одной из основных причин дополнительных вызовов отрисовки, так как имеет отличный от любой другой графики материал и всегда будет беспощадно прерывать батчинг. Более того, размеры текстовых мешей и контейнеров зачастую занимают намного большую площадь, чем само начертание символов, что очень легко упустить из виду. Поэтому крайне важно следить за порядком отрисовки разношерстных объектов, их наслоением и иерархией внутри элементов в сцене.
Отрисовка кадра. Объекты на экране появляются последовательно по иерархии, но неравномерно в связи с использованием разных материалов и прерыванием батчинга.
К преимуществам в использовании текстурных атласов можно отнести и то, что они позволят движку сжимать ассеты произвольных размеров, тем самым уменьшая их общий вес при хранении в памяти. Дело в том, что большинство алгоритмов сжатия требуют, чтобы размер текстур был кратен степени двойки (например, 128x128, 256x256, 512x512 и т.д). Атласы, являющиеся по-умолчанию таковыми, берут все вопросы по сжатию на себя, исключая необходимость подгонять всю исходную графику вашего интерфейса под вышеуказанный критерий.
Также при создании атласов очень важно руководствоваться не просто желанием удобно “складировать” ассеты, но делать это грамотно, эффективно, учитывая логику появления тех или иных элементов в интерфейсе вашей игры. Даже если на экране изображена всего одна малюсенькая иконочка размером в 16x16 пикселей, упакованная в атлас, в память устройства она потянет за собой всю его текстуру в 4К. Еще на этапе дизайна возможно прикинуть, какие элементы станут уникальными и для каких экранов, а какие будут являться сквозным конструктором для интерфейса игры.
Использование масок
Маски — часто применяемый в дизайне пользовательских интерфейсов инструмент. Однако при его использовании очень важно учитывать влияние масок как на overdraw, так и на батчинг.
Unity предлагает нам два по-разному работающих компонента из коробки на выбор: Mask и Rect Mask 2D. При использовании любого из них графика, вылезающая за пределы маски, все равно будет влиять на общий fill rate, хоть она и не отображается на экране. Поэтому старайтесь избегать ситуаций, в которых для достижения желаемого результата маской будет вырезан лишь небольшой участок крупного графического объекта. Возможно, в каких-то местах лучше не скупиться на ассеты и сохранить этот кусочек в виде отдельной картинки.
Что же касается влияния на draw calls, то здесь наши компоненты разнятся. Обе маски прерывают батчинг между замаскированными объектами и их не замаскированными соседями, но по разным причинам. Mask использует stencil буфер (буфер шаблона), создавая в рантайме другой материал для всех своих дочерних объектов. Из-за этого материала они не станут батчиться с другими элементами в сцене, но все еще смогут сбатчиться друг с другом и даже с другими такими же замаскированными объектами под другой Mask.
Rect Mask 2D материал не меняет, stencil не использует и дополнительный overdraw от графики маски не добавляет, что делает ее в сравнении более производительной. Тем не менее использование данного компонента все равно рвет батчинг, причем, в отличие от Mask, элементы интерфейса под разными Rect Mask 2D друг с другом батчиться уже не будут.
Еще одним ограничением нативных масок является их неумение работать с плавным изменением альфа-канала. К сожалению, на данный момент ни один из вышеуказанных компонентов не позволяет добиться эффекта софт маски (soft mask), поддерживающей градиенты и полупрозрачности. За желаемым результатом придется обратиться к сторонним решениям или попробовать реализовать их самим.
В Asset Store можно найти плагины, добавляющие функционал софт маски в редактор. Имея под рукой такой инструмент, ему сразу же находится повсеместное применение в дизайне — скроллы, анимации, визуальные эффекты, декор и т.д.
Все три варианта маскируют картинку, используя один общий исходный спрайт.
Однако не стоит сильно обольщаться. Использование софт масок — хоть и приятная глазу, но довольно тяжелая фича, особенно для мобильных устройств. Так, например, открытие большого списка UI элементов под такой маской на весь экран с красивыми мягкими краями, как-минимум, гарантированно вызовет неприятный фриз игры. В подобной ситуации решением для нашей команды оказалось минимизировать использование софт масок (там, где это нужно), искать обходные пути в виде перекрывающих градиентов (там, где это возможно) или разнести добавление элементов в список на несколько кадров, чтобы размазать просадку производительности от инициализации софт маски.
Работа с текстом
Под каждый текстовый символ нативный UI Text создает отдельный квад (quad), а значит вполне закономерно, что чем больше в интерфейсе текста — тем больше геометрии в сцене. При этом при внесении любых изменений в текстовые значения, повторном включении его компонентов или родительского объекта вся эта геометрия будет перестраиваться.
По умолчанию все шрифты, которые добавляются в Unity, отмечаются как динамические. При всем удобстве их использование имеет свою цену. Атлас динамического шрифта генерируется в рантайме и заполняется только используемыми на данный момент в текстовых компонентах сцены символами. Для каждого шрифта создается свой атлас, учитывающий все встречающиеся в сцене размеры текстовых символов. Каждое появление нового символа в интерфейсе инициирует обновление и перестройку атласа шрифта, что порой может негативно сказаться на производительности.
Более того, если в текущем атласе уже нет свободного места для новых текстовых глифов, Unity пересобирает его заново, удаляя не использующиеся в данный момент символы. А если и это не помогает, то увеличивает его размер.
Если в игре не используется огромное количество символов, их набор заранее предопределен, то вместо динамических шрифтов намного лучше использовать статические. В настройках шрифта в редакторе можно заранее сгенерировать атлас, который будет содержать только необходимые проекту символы и не будет изменяться динамически, минуя все упомянутые в предыдущем абзаце издержки.
Отдельного упоминания заслуживает функция Best Fit, позволяющая автоматически изменять размер шрифта, если текст не помещается в свой Rect Transform. Сами сотрудники Unity в официальных туториалах вспоминают ее с ужасом и не рекомендуют использовать — настолько все плохо, не оптимизировано и быстро перегружает шрифтовый атлас. Старайтесь заранее предусматривать максимально возможные размеры текстовых контейнеров и следить за тем, чтобы все везде помещалось, оставляя запас на все происки локализации.
Альтернативой UI Text может быть сторонний TextMesh Pro, который использует только статичные шрифты, да и его Best Fit работает гораздо лучше и экономнее. Однако минусом работы с данным компонентом может быть то, что под каждую локализацию и текстовый стиль придется создавать свой отдельный набор ассетов шрифтов. Тут уж каждый решает сам, что ему ближе и как удобней.
Блюр
Размытие картинки (блюр) уже много лет как любят и используют в своей работе дизайнеры пользовательских интерфейсов, но в Unity UI его готовой оптимизированной реализации попросту нет.
Большое количество реалтайм блюра, позволяющего оставаться картинке динамичной, в Unity — гарантированный способ убить производительность в мобильной игре. Гораздо легче переносится его статичная версия — создается скриншот экрана, размывается в необходимое количество проходов и используется в дальнейшем как текстуру. Существует множество сторонних ассетов, позволяющих реализовать оба подхода. Однако, если использование реалтайм блюра для интерфейса все-таки оправдано и критично, постарайтесь тщательно следить хотя бы за тем, чтобы материалы блюра батчились вместе, если на экране есть несколько участков с размытием.
Layout группы
Layout группы (Layout Groups) — крайне удобный инструмент в Unity UI, помогающий автоматически располагать произвольное количество элементов с заданой ориентацией, выравниванием и отступами. Однако использовать его нужно с осторожностью. Сортировка, поиск групп и расчет положений элементов при перерисовке атласа — довольно затратное дело, особенно если в сцене есть Layout группы, и тем более если они оказываются вложены друг в друга. Старайтесь всюду, где это возможно, обходится грамотным использованием анкоров и пивотов трансформа UI элементов, предпочитая их Layout группам.
Raycast Target
Галка Raycast Target на графических компонентах UI элементов означает, что последние могут отлавливать клики от пользовательской мыши или тапы на сенсорных экранах. Свежесозданному объекту с компонентами Image или Text редактор ставит эту галку по-умолчанию. Для того, чтобы определить, какой объект поймал “тычок” от пользователя, Graphic Raycaster, обрабатывающий события ввода в Unity UI, проходится по всему списку элементов в иерархии, помеченных как Raycast Target, и сортирует их очередность, рассчитывая пересечения и перекрытия одних объектов другими.
Если оставлять включенной галку только тем объектам, которым это действительно логически необходимо (например, кнопкам), можно значительно сократить список для обхода и сортировки, тем самым ускорив полезную работу движка.
Иерархия. Чем проще — тем проще
И Layout группы, и Graphic Raycaster в процессе работы непрерывно путешествуют по дереву иерархии сцены, от находящихся глубоко дочерних элементов к самому корню, в процессе поиска различных компонентов.
Суть оптимизации данного процесса заключается в том, чтобы стараться этот путь сокращать — держать иерархию максимально плоской, избегая вложений, когда без них можно обойтись. Создавать объекты-папки для менеджмента других элементов интерфейса возможно и более удобно для восприятия, но злоупотреблять ими все-таки не стоит, чтобы не усложнять структуру иерархии.
Разделение канвасов
В самом начале статьи мы уже выяснили, что канвас стремится перестраиваться на любой чих от своих дочерних объектов. Чтобы в этом процессе не участвовали вообще все элементы интерфейса, некоторые из них можно вынести в суб-канвас (sub-canvas). Такой канвас будет перестраиваться самостоятельно, независимо от других, тем самым никак их не загрязняя. Лучший способ использовать это состоит в том, чтобы отсортировать элементы пользовательского интерфейса на статические и динамические. Например, если у вас есть окно инвентаря со скроллом предметов, вынесенный в отдельный канвас, скролл будет перестраиваться самостоятельно, не тревожа лишний раз статичные элементы фона.
Казалось бы, вот оно — решение всех проблемы с ненужными перестроениями. Но у всего есть свои “но”. Контролировать большое количество канвасов может стать банально неудобно. Также очень важно помнить, что элементы принадлежащие разным канвасам гарантированно не будут батчиться друг с другом. Поэтому, принимая структурные решения, разделением элементов интерфейса на разные канвасы лучше пользоваться осторожно и обязательно проверять в Профилировщике (Profiler), действительно ли такой подход может дать заметный прирост к производительности.
В наших проектах в отдельный канвас выносятся только элементы для фонового изображения, которое является общим для большинства экранов в интерфейсе игры и чаще всего статично.
И вот мы все это учли, провели работу над ошибками и получили более-менее оптимизированный статичный интерфейс. Пришло время добавить ему движения, но и тут можно с легкостью наломать дров.
Particle System
Тот, кто когда-нибудь пытался добавить в интерфейс системы частиц (Particle System), прекрасно знает, что это как смешивать жидкости разной плотности — одна обязательно будет отображаться над другой, как ты не крутись.
Стандартные частицы в Unity не учитывают сортировку объектов в канвасе и не умеют работать с масками. Поэтому на помощь опять приходят сторонние компоненты из Asset Store, которые решают вышеперечисленные вопросы, но при этом зачастую увеличивают ресурсную стоимость системы частиц. Следовательно, требования по производительности к частицам в пользовательском интерфейсе куда-более строгие, чем, скажем, на локациях или для персонажей. Кроме того, большое количество крупных наслаивающихся друг на друга частиц может значительно повысить overdraw.
Бывает и такое, что альтернативой системе частиц для достижения определенных визуальных эффектов и вовсе могут стать простые шейдеры (например, со скроллом или искажением текстуры), являющиеся более экономным и оптимизированным решением в вопросе перерисовок.
Анимация
Рассмотрим частую ситуацию. Необходимо заанимировать кнопку и все ее стейты — Unity любезно предлагает нам Animation Controller (далее просто “аниматор”) с базовым суповым набором анимаций для разных состояний, которые останется только отредактировать, да еще и без необходимости запуска рантайма для проверки результата. Удобно? Очень удобно. И снова коварная дефолтная ловушка. Ведь каждый кадр аниматор будет перерисовывать этот объект, помечая как грязный, даже если на экране в данный момент вообще не проигрывается никакая анимация.
Потому для случаев, когда анимируемые элементы интерфейса визуально большую часть времени остаются статичны, нативный аниматор лучше не использовать. Заменой ему в данном вопросе может послужить система tween-анимаций. Например, использование распространяемого бесплатно плагина DOTween. Однако за увеличение производительности придется заплатить необходимостью писать анимации в виде скриптов уже без удобного наглядного таймлайна, как у аниматора из коробки Unity.
Пример анимации произвольного элемента интерфейса...
… и то, как данная анимация может быть реализована с помощью абсолютно разных подходов.
Пока не разошлись
На этом, пожалуй, остановимся. Тема действительно очень обширна — мы коснулись лишь вопросов, которые лежат на поверхности и не требуют вмешательства со стороны кода… Но, как известно, совершенству нет предела.
Оптимизация — это всегда про выбор, про поиск баланса между разными, а порою даже и противоречащими друг другу решениями. Не стоит впадать в крайности с каким-либо из ее методов без предварительного анализа и уверенности в том, что ваши действия гарантированно приведут к положительному результату.
Не нужно сразу ужиматься и делать высоко оптимизированный серый квадрат на сером фоне, отказывая игре во “вкусном” интерфейсе. Тем не менее, каждый раз копая глубже, у вас появляется возможность избежать все большего количества проблем еще на старте. Ситуации варьируются от проекта к проекту, от цели к цели, но общие принципы всегда остаются прежними.
Ребята в Unity, конечно, тоже не сидят сложа руки. Официальные гайды на Unity Learn пополняются хорошими статьями про оптимизацию UI, а новые версии движка потихоньку, но исправляют проблемы старых. Судя по последним анонсам, Unity уже готовят абсолютно новый унифицированный инструмент для редактирования пользовательских интерфейсов — на первый взгляд, очень уж напоминающий CSS для web-разработки. В новой системе обещают полностью переработать отрисовку интерфейса и значительно улучшить производительность. Что ж, посмотрим, что в итоге получится. А пока работаем с тем, что имеем.
Почитать / посмотреть / послушать:
Гайд на Unity Learn
Советы от пользователей в блогах
О том как работает батчинг
Лекция по оптимизации (про UI — во второй половине)
Кому интересно — новый Unity UI редактор
Подпишитесь на страницы Banzai Games в соцсетях: Facebook, Vkontakte, Instagram, LinkedIn
В команду Banzai Games требуется Senior Unity Developer. Подробнее о вакансии можно прочитать здесь.