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

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

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

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

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

Давайте углубимся!

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

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

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

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

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

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

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

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

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

Конечно, не каждая оптимизация, описанная здесь, будет применима к вашему приложению. Что-то, что хорошо работает в одном проекте, может не подойти вашему. Определите настоящие узкие места и сосредоточьтесь на том, что приносит пользу вашей работе.

Поймите, как работает Profiler Unity

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

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

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

Настройки сборки в редакторе

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

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

Просмотр временной шкалы в редакторе
Используйте просмотр временной шкалы, чтобы определить, ограничены ли вы по ЦП или по ГП.

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

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

  • Временная шкала показывает визуальное распределение времени для конкретного кадра. Это позволяет вам визуализировать, как действия соотносятся друг с другом и между различными потоками. Используйте эту опцию, чтобы определить, ограничены ли вы по ЦП или по ГП.
  • Иерархия показывает иерархию ProfileMarkers, сгруппированных вместе. Это позволяет вам сортировать образцы по временным затратам в миллисекундах (Время мс и Собственное время мс). Вы также можете подсчитать количество Вызовов к функции и управляемую память кучи (GC Аллокация) на кадре.
Сортировка ProfileMarkers по затратам времени
Представление иерархии позволяет сортировать ProfileMarkers по затратам времени.

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

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

Используйте Анализатор профиля

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

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

Работайте с конкретным бюджетом времени на кадр

Каждый кадр будет иметь бюджет времени, основанный на ваших целевых кадрах в секунду (fps). В идеале приложение, работающее на 30 fps, позволит примерно 33.33 мс на кадр (1000 мс / 30 fps). Аналогично, цель в 60 fps оставляет 16.66 мс на кадр.

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

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

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

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

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

Определите, ограничен ли вы ЦП или ГП

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

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

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

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

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

Взгляд на профайлер памяти в редакторе
Захватывайте, проверяйте и сравнивайте снимки в профайлере памяти.

Используйте профайлер памяти

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

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

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

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

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

Будьте внимательны к определенным ненужным выделениям кучи, которые могут вызвать всплески 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. Используйте Анализатор профиля, чтобы проверить его пользу для вашего приложения.

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

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

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

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

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

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

Поймите Unity PlayerLoop

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

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

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

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

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

private int interval = 3;

void Update()
{
    if (Time.frameCount % interval == 0)
    {
        ExampleExpensiveFunction();
    }
}

Избегайте тяжелой логики в Start/Awake

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

  • Awake
  • OnEnable
  • Start

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

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

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

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

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

#if UNITY_EDITOR
void Update()
{
}
#endif

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

Удалить отладочные сообщения

Сообщения журнала (особенно в Обновление, Позднее обновление или Фиксированное обновление) могут замедлить производительность. Отключите ваши Сообщения журнала перед созданием сборки.

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

public static class Logging
{
    [System.Diagnostics.Conditional("ENABLE_LOG")]
    static public void Log(object message)
    {
        UnityEngine.Debug.Log(message);
    }
}
Вид ENABLE_LOG
Добавление пользовательской директивы предварительной обработки позволяет вам разделить ваши скрипты.

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

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

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

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

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

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

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

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

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

Инстанцирование префаба с уже настроенными необходимыми компонентами, как правило, более производительно.

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

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

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

void Update()
{
    Renderer myRenderer = GetComponent<Renderer>();
    ExampleFunction(myRenderer);
}

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

private Renderer myRenderer;

void Start()
{
    myRenderer = GetComponent<Renderer>();
}

void Update()
{
    ExampleFunction(myRenderer);
}

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

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

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

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

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

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

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

Используйте ScriptableObjects

Храните неизменяемые значения или настройки в ScriptableObject вместо MonoBehaviour. ScriptableObject — это актив, который существует внутри проекта, который нужно настроить только один раз. Его нельзя напрямую прикрепить к GameObject.

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

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

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

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

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

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

Обложка электронной книги, "Оптимизация производительности вашей мобильной игры"

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

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

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

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