Games

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

THOMAS KROGH-JACOBSEN / UNITY TECHNOLOGIESProduct Marketing Core Tech
Jun 23, 2021|15 Мин
Оптимизируйте производительность мобильных игр: Советы по профилированию, памяти и архитектуре кода от лучших инженеров Unity
Эта веб-страница была переведена с помощью машинного перевода для вашего удобства. Мы не можем гарантировать точность или надежность переведенного контента. Если у вас есть вопросы о точности переведенного контента, обращайтесь к официальной английской версии веб-страницы.
Наша команда Accelerate Solutions знает исходный код изнутри и поддерживает множество клиентов Unity, чтобы они могли получить максимальную отдачу от движка. В своей работе они глубоко погружаются в проекты создателей, чтобы помочь определить точки, где производительность может быть оптимизирована для повышения скорости, стабильности и эффективности. Мы встретились с этой командой, состоящей из самых старших инженеров-программистов Unity, и попросили их поделиться своим опытом в области оптимизации мобильных игр.

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

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

Хотите посмотреть все серии прямо сейчас? Скачайте полную электронную книгу бесплатно.

Давайте покопаемся!

Профилирование

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

Профилируйте рано, часто и на целевом устройстве

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

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

Наряду с Unity Profiler вы можете использовать нативные инструменты для iOS и Android для дальнейшего тестирования производительности на соответствующих движках:

Некоторые аппаратные средства могут использовать преимущества дополнительных инструментов профилирования (например, Arm Mobile Studio, Intel VTune и Snapdragon Profiler). Дополнительные сведения см. в разделе Профилирование приложений, созданных с помощью Unity.

Сосредоточьтесь на оптимизации нужных областей

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

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

Поймите, как работает профилировщик Unity

Unity Profiler поможет вам обнаружить причины любых лагов или зависаний во время выполнения и лучше понять, что происходит в конкретном кадре или в конкретный момент времени. По умолчанию включите дорожки процессора и памяти. Вы можете отслеживать дополнительные модули Profiler, такие как Renderer, Audio и Physics, если это необходимо для вашей игры (например, геймплей с большим содержанием физики или музыки).

Используйте Unity Profiler для проверки производительности и распределения ресурсов вашего приложения.
Используйте Unity Profiler для проверки производительности и распределения ресурсов вашего приложения.

Создайте приложение на своем устройстве, установив флажки Development Build и Autoconnect Profiler, или подключите его вручную, чтобы ускорить запуск приложения.

Создание настроек в редакторе

Выберите целевую платформу для профилирования. Кнопка Record (Запись) отслеживает несколько секунд воспроизведения вашего приложения (по умолчанию 300 кадров). Перейдите в Unity > Preferences > Analysis > Profiler > Frame Count, чтобы увеличить этот показатель до 2000, если вам нужна более длительная съемка. Хотя это означает, что редактору Unity приходится выполнять больше работы на процессоре и занимать больше памяти, это может быть полезно в зависимости от вашего конкретного сценария.

Это профилировщик на основе инструментария, который профилирует тайминги кода, явно обернутые в ProfileMarkers (например, методы MonoBehaviour's Start или Update, или определенные вызовы API). Кроме того, при использовании Deep ProfilingsettingUnity может профилировать начало и конец каждого вызова функции в коде вашего скрипта, чтобы точно определить, какая часть приложения вызывает замедление.

Просмотр Timeline в редакторе
Используйте вид Timeline, чтобы определить, привязаны ли вы к CPU или GPU.

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

Щелкните в окне, чтобы проанализировать конкретный кадр. Далее используйте вид Timeline или Hierarchy для выполнения следующих действий:

  • Timeline показывает визуальную разбивку времени для конкретного кадра. Это позволит вам наглядно увидеть, как деятельность связана друг с другом и с различными потоками. Используйте этот параметр, чтобы определить, привязаны ли вы к CPU или GPU.
  • Иерархия показывает иерархию маркеров профиля, сгруппированных вместе. Это позволяет сортировать образцы по временным затратам в миллисекундах(Time ms и Self ms). Вы также можете подсчитать количество вызовов функции и управляемую кучу памяти(GC Alloc) на кадре.
Сортировка маркеров профиля по затратам времени
Представление "Иерархия" позволяет сортировать маркеры профиля по временным затратам.

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

Прежде чем оптимизировать что-либо в проекте, сохраните файл данных Profiler .datafile. Внесите изменения и сравните сохраненные .данные до и после модификации. Используйте этот цикл для повышения производительности: профилируйте, оптимизируйте и сравнивайте. Затем промойте и повторите.

Использование Profile Analyzer

Этот инструмент позволяет объединить несколько кадров данных Profiler, а затем найти интересующие вас кадры. Хотите посмотреть, что происходит с профилировщиком после внесения изменений в проект? Представление "Сравнение" позволяет загружать и различать два набора данных, чтобы протестировать изменения и улучшить их результат. Profile Analyzer доступен через менеджер пакетов Unity.

Более глубокий взгляд на Profile Analyzer в редакторе
Еще глубже погрузитесь в данные о кадрах и маркерах с помощью Profile Analyzer, который дополняет существующий Profiler.

Работайте в рамках определенного бюджета времени на кадр

Каждый кадр будет иметь временной бюджет, основанный на целевой частоте кадров в секунду (FPS). В идеале приложение, работающее со скоростью 30 кадров в секунду, должно обеспечивать примерно 33,33 мс на кадр (1000 мс / 30 кадров в секунду). Аналогично, цель в 60 FPS оставляет 16,66 мс на кадр.

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

Учет температуры устройства

Однако для мобильных устройств мы не рекомендуем постоянно использовать это максимальное время, так как устройство может перегреться, а ОС - снизить температуру CPU и GPU. Мы рекомендуем использовать только около 65 % доступного времени, чтобы оставить время на передышку между кадрами. Типичный бюджет кадра составляет примерно 22 мс на кадр при 30 fps и 11 мс на кадр при 60 fps.

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

Если устройство работает в горячем режиме, Profiler может воспринять и сообщить о низкой производительности, даже если это не является причиной для длительного беспокойства. Для борьбы с перегревом профилирования используйте профилирование короткими сериями. Это охлаждает устройство и имитирует реальные условия. Мы рекомендуем охладить устройство в течение 10-15 минут перед повторным профилированием.

Определите, на каком уровне вы работаете - на GPU или на CPU

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

  • Если вы видите маркер Gfx.WaitForCommands , это означает, что поток рендеринга готов, но, возможно, вас ожидает узкое место в основном потоке.
  • Если вы часто встречаете Gfx.WaitForPresent, это означает, что основной поток был готов, но ждал, пока GPU представит кадр.
Память

Unity использует автоматическое управление памятью для пользовательского кода и скриптов. Небольшие фрагменты данных, такие как локальные переменные со значением, выделяются в стеке. Более крупные фрагменты данных и долгосрочные хранилища выделяются в управляемую кучу.

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

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

Обзор встроенного редактора Memory Profiler
Захват, проверка и сравнение моментальных снимков в Memory Profiler.

Использование Memory Profiler

Это отдельное дополнение (доступно в виде пакета Experimental или Preview в менеджере пакетов) может сделать снимок памяти управляемой кучи, чтобы помочь вам выявить такие проблемы, как фрагментация и утечки памяти.

Щелкните в представлении Tree Map, чтобы отследить переменную до нативного объекта, удерживающего память. Здесь вы можете выявить общие проблемы с потреблением памяти, например, слишком большие текстуры или дублирующиеся активы.

Узнайте, как использовать Memory Profiler в Unity для улучшения использования памяти. Вы также можете ознакомиться с нашей официальной документацией по Memory Profiler.

Уменьшить влияние сборки мусора (GC)

Unity использует сборщик мусора Boehm-Demers-Weiser, который останавливает выполнение кода вашей программы и возобновляет нормальное выполнение только после завершения своей работы.

Помните о некоторых ненужных выделениях из кучи, которые могут привести к скачкам GC:

  • Струны: В C# строки являются ссылочными типами, а не типами значений. Сократите ненужное создание и манипулирование строками. Избегайте разбора строковых файлов данных, таких как JSON и XML; вместо этого храните данные в ScriptableObjects или форматах MessagePack или Protobuf. Используйте класс StringBuilder, если вам нужно создавать строки во время выполнения программы.
  • Вызовы функций Unity: Некоторые функции создают выделения из кучи. Кэшируйте ссылки на массивы, а не выделяйте их в середине цикла. Кроме того, воспользуйтесь некоторыми функциями, которые позволяют не генерировать мусор. Например, используйте GameObject.CompareTag вместо того, чтобы вручную сравнивать строку с GameObject.tag (так как возвращение новой строки создает мусор).
  • Бокс: Избегайте передачи переменной с типом значения вместо переменной с типом ссылки. При этом создается временный объект, а потенциальный мусор, который появляется вместе с ним, неявно преобразует тип значения в тип объекта (например, int i = 123; object o = i). Вместо этого постарайтесь предоставить конкретные переопределения с типом значения, которое вы хотите передать. Для этих переопределений также можно использовать генерики.
  • Корутины: Хотя yield не производит мусор, создание нового объекта WaitForSeconds производит. Кэшируйте и повторно используйте объект WaitForSeconds вместо того, чтобы создавать его в строке yield.
  • LINQ и регулярные выражения: И в том, и в другом случае мусор генерируется из закулисного бокса. Избегайте LINQ и регулярных выражений, если для вас важна производительность. Пишите циклы for и используйте списки в качестве альтернативы созданию новых массивов.

По возможности, вовремя собирайте мусор

Если вы уверены, что остановка сборки мусора не повлияет на определенный момент игры, вы можете запустить сборку мусора с помощью System.GC.Collect.

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

Используйте инкрементный сборщик мусора, чтобы разделить нагрузку на GC

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

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

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

При профилировании вы увидите пользовательский код вашего проекта в контуре PlayerLoop (а компоненты редактора - в контуре EditorLoop).

Увеличенный вид профилировщика
Профилировщик покажет ваши пользовательские скрипты, настройки и графику в контексте выполнения всего движка.
Вид цикла PlayerLoop

Познакомьтесь с PlayerLoop и жизненным циклом скрипта.

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

Понимание цикла проигрывателя Unity PlayerLoop

Убедитесь, что вы понимаете порядок выполнения цикла кадров Unity. Каждый скрипт Unity запускает несколько функций событий в заранее определенном порядке. Вы должны понимать разницу между функциями Awake, Start, Update и другими, которые создают жизненный цикл сценария.

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

Сведите к минимуму код, выполняющийся каждый кадр

Подумайте, должен ли код выполняться каждый кадр. Уберите ненужную логику из Update, LateUpdate и FixedUpdate. Эти функции событий - удобное место для размещения кода, который должен обновляться каждый кадр, при этом извлекается вся логика, которая не должна обновляться с такой частотой. По возможности выполняйте логику только тогда, когда ситуация меняется.

Если вам необходимо использовать Update, подумайте о том, чтобы запускать код каждые n кадров. Это один из способов применения временной нарезки - распространенной техники распределения большой нагрузки на несколько кадров. В этом примере мы запускаем функцию ExampleExpensiveFunction раз в три кадра:

Неизвестный тип блока "codeBlock", укажите для него сериализатор в свойстве `serializers.types`.

Избегайте тяжелой логики при запуске/пробуждении

Когда загружается первая сцена, эти функции вызываются для каждого объекта:

  • Проснись
  • OnEnable
  • Начало

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

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

Избегайте пустых событий Unity

Даже пустые MonoBehaviour требуют ресурсов, поэтому следует удалить пустые методы Update или LateUpdate.

Если вы используете эти методы для тестирования, используйте директивы препроцессора:

Неизвестный тип блока "codeBlock", укажите для него сериализатор в свойстве `serializers.types`.

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

Удаление утверждений журнала отладки

Операторы журнала (особенно в Update, LateUpdate или FixedUpdate) могут снижать производительность. Перед созданием сборки отключайте утверждения журнала.

Чтобы сделать это проще, подумайте о создании атрибута Conditional вместе с директивой препроцессинга. Например, создайте пользовательский класс следующим образом:

Неизвестный тип блока "codeBlock", укажите для него сериализатор в свойстве `serializers.types`.

Представление о ENABLE_LOG
Добавление пользовательской директивы препроцессора позволяет разделить скрипты на части.

Создайте сообщение в журнале с помощью своего пользовательского класса. Если вы отключите препроцессор ENABLE_LOG в настройках игрока, все ваши сообщения Log исчезнут одним махом.

Используйте хэш-значения вместо строковых параметров

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

При использовании метода Set или Get в аниматоре, материале или шейдере используйте метод с целочисленным значением вместо метода со строковым значением. Строковые методы просто выполняют хеширование строк, а затем передают хешированный идентификатор методам с целочисленными значениями.

Используйте Animator.StringToHash для имен свойств аниматоров и Shader.PropertyToID для имен свойств материалов и шейдеров.

Выберите правильную структуру данных

Выбор структуры данных влияет на эффективность, поскольку итерации выполняются тысячи раз за кадр. Не знаете, что использовать для коллекции - список, массив или словарь? Следуйте руководству MSDN по структурам данных в C# в качестве общего руководства по выбору правильной структуры.

Избегайте добавления компонентов во время выполнения

Вызов AddComponent во время выполнения программы сопряжен с некоторыми издержками. Unity Ads должна проверять наличие дубликатов или других необходимых компонентов при добавлении компонентов во время выполнения.

Инстанцирование Prefab с уже установленными компонентами, как правило, более эффективно.

Кэш GameObjects и компонентов

GameObject.Find, GameObject.GetComponent и Camera.main (в версиях до 2020.2) могут быть дорогими, поэтому лучше избегать их вызова в методах Update. Вместо этого вызывайте их в Start и кэшируйте результаты.

Вот пример, демонстрирующий неэффективное использование повторного вызова GetComponent:

Неизвестный тип блока "codeBlock", укажите для него сериализатор в свойстве `serializers.types`.

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

Неизвестный тип блока "codeBlock", укажите для него сериализатор в свойстве `serializers.types`.

Используйте пулы объектов

Instantiate and Destroy может генерировать мусор и сборку мусора (GC), и в целом является медленным процессом. Вместо того чтобы регулярно создавать и уничтожать GameObjects (например, стрелять пулями из пистолета), используйте пулы предварительно выделенных объектов, которые можно повторно использовать и перерабатывать.

Увеличенный взгляд на ObjectPool
В этом примере ObjectPool создает 20 экземпляров PlayerLaser для повторного использования.

Создавайте многоразовые экземпляры в тот момент игры (например, во время экрана меню), когда скачки процессора менее заметны. Отслеживайте этот "пул" объектов с помощью коллекции. Во время игры просто включайте следующий доступный экземпляр, когда это необходимо, отключайте объекты вместо того, чтобы уничтожать их, и возвращайте их в пул.

Увеличенный взгляд на иерархию SampleScene
Пул объектов PlayerLaser неактивен и готов к стрельбе.

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

Здесь вы узнаете, как создать простую систему пула объектов в Unity .

Использование объектов со сценариями

Храните неизменяемые значения или настройки в ScriptableObject, а не в MonoBehaviour. ScriptableObject - это актив, живущий внутри проекта, который нужно настроить только один раз. Он не может быть напрямую присоединен к GameObjects.

Создайте поля в ScriptableObject для хранения значений или настроек, а затем ссылайтесь на ScriptableObject в своих MonoBehaviour.

Блок-схема, показывающая объект ScriptableObject под названием Inventory, содержащий настройки для различных GameObjects
ScriptableObject под названием Inventory содержит настройки для различных GameObjects

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

Посмотрите это руководство "Введение в ScriptableObjects", чтобы узнать, как ScriptableObjects может помочь вашему проекту. Вы также можете найти соответствующую документацию здесь.

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

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

Обложка книги "Оптимизируйте производительность мобильных игр"

Скачайте нашу электронную книгу

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

Следите за дальнейшими советами по производительности

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