Наш телеграм канал
Будьте вкурсе новостей науки и IT
Оптимизация игр на Unity: проверенный в деле план

Оптимизация игр — отдельная головная боль разработчиков, процесс, который может идти бесконечно. Нужно учесть загрузку процессора, видеокарты и не потерять FPS. Нашли статью, автор которой 13 лет разрабатывает на Unity и делится советами по оптимизации. Под катом есть пошаговый план, как сделать проект на Unity более производительным.

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

Распространенные ошибки

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

Аврал за несколько дней до дедлайна 

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

Отсутствие плана

Нельзя заниматься профилированием и оптимизацией без плана. Нет смысла работать вслепую и оптимизировать код или арт, не определив узкие места.

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

Программисты порой оптимизируют отдельные куски кода — например, оптимизируют UI с помощью цикла foreach (с 10 мс до 3 мс). А художники рассчитывают полигоны. И то, и другое — улучшение, но зачастую эти действия не дают заметных результатов. Лучше сосредоточиться на опыте игрока, ведь в конечном итоге — это единственный важный результат.

Некорректные данные при включении GPU Profiler

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

Определить кто задерживает исполнение программы — GPU или CPU — можно, используя Timeline профайлера CPU:

  • Gfx.WaitForPresent: ограничения GPU, CPU ожидает ответа от GPU;

  • Gfx.WaitForCommands: ограничения CPU, GPU ожидает ответа от CPU.

«Сбор данных профайлера GPU может сильно перегружать систему. 
Закройте эту графу, если эти данные вам не нужны», — я солидарен с Unity. 
«Сбор данных профайлера GPU может сильно перегружать систему. Закройте эту графу, если эти данные вам не нужны», — я солидарен с Unity. 

Некорректные данные при запуске Deep Profiling

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

Использование кастомных маркеров профиля:

UnityEngine.Profiling.Profiler.BeginSample("MyHeavyCode - Top");
..
UnityEngine.Profiling.Profiler.EndSample();

Некорректные данные из-за физики в FixedUpdate

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

Сложности с сетевым решением

Программисты порой полагают, что сетевое решение (например, Photon) сильно снижает производительность. Они видят, что из-за него происходят пики загрузки ЦП, но забывают заглянуть поглубже в стек вызовов. Сетевой инструмент запускает методы, вызываемые из сети (так называемые RPC), а они — часть вашего собственного кода, которая не имеет к сети никакого отношения. В таких ситуациях нужно оптимизировать RPC-методы и/или распределить их рабочую нагрузку.

План оптимизации

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

Подготовка

1. Возьмите за ориентир самую слабую платформу.

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

2. Сделайте так, чтобы вам было удобно.

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

Общие рекомендации

1. Автоматизируйте билды, чтобы они собирались в один клик.

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

2. Ускорьте билды.

Обеспечьте возможность собирать более быстрые и менее объемные билды (например, с одним уровнем и одной машиной в гоночной игре).

3. Отключите обфускацию, если она используется.

Рекомендации по платформам

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

Ускорьте настройку компилятора Il2CPP

Используйте Release или даже Debug, если компиляция кода не занимает слишком много времени.

PlayerSettings.SetIl2CppCompilerConfiguration(group, mode); 
Il2CppCompilerConfiguration.Master //Slow build, Quick performance
Il2CppCompilerConfiguration.Release //Medium build time, Good runtime performance
Il2CppCompilerConfiguration.Debug //Quickest build, slowest runtime performance

Петля оптимизации

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

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

1. Документирование производительности

Фиксируйте производительность вашей игры, желательно на тех этапах, которые легко воспроизвести. Задокументируйте результаты и сохраните данные профилирования. Стоит записать уровень загрузки CPU (и GPU), чтобы оценить прогресс. Я часто также отслеживаю загрузку памяти, чтобы подготавливать эффективные новые билды и при необходимости сокращать объем используемой памяти.

Можно использовать Profile Analyzer: он упрощает сравнение данных в профиле. Это поможет обнаружить пики загрузки или другие отличия между разными билдами/конфигурациями настроек. То есть этот инструмент отмечает все произведенные улучшения.

Window > Analyse > Profile Analyzer
Window > Analyse > Profile Analyzer

Profile Analyzer экономит много работы: выберите две области, и он автоматически сообщит, в чем разница. 

2. Базовая производительность

Сначала мы обычно игнорируем пики загрузки и сосредотачиваемся на том, чтобы базовая производительность была в пределах нормы. Здесь главное довести «нормальный» игровой цикл до приемлемого уровня, будь то 30 кадров в секунду (мобильные платформы, Switch), 60 или даже 120 (VR).

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

Window > Analysis > Profiler
Window > Analysis > Profiler

Режим отображения Timeline куда полезнее, чем Hierarchy. Timeline показывает порядок задач и их зависимость друг от друга. Стоит обратить внимание на следующие моменты:

  • скрипты/плагины, запускающие тяжелый код в Update, FixedUpdate, LateUpdate и т.д.;

  • аудио: звук должен давать не больше 5% нагрузки на процессор (убедитесь, что вы не воспроизводите звуки, которые не слышны);

  • неэффективная реализация пользовательского интерфейса и как следствие перегрузка процессора (избегайте большого количества перерисовок);

  • запуск анимаций, которые не видны;

  • оптимизация настроек Physics Fixed Deltatime: не слишком мало (с ошибками в физике) и не слишком много (слишком сильная потеря производительности). Используйте FixedUpdate() только для того кода, который должен работать во время физики, поскольку этот метод сильно сказывается на FPS.

Также было бы неплохо время от времени создавать релизный билд. На нем можно проверить фактический FPS.

3. Пики загрузки

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

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

Мы вызвали: родной метод Main() платформы; вызов через Monobehaviour Update(); метод (для обновления состояний платформы и ее четырех контроллеров). Это заняло 2,0 мс, что очень много для целевого значения в 16 мс. В итоге удалось оптимизировать процесс примерно до 0,2 мс, распределив его так, чтобы он запускался только на каждый X-й кадр, ведь не было необходимости запускать его на каждый фрейм. А также мы стали обновлять состояние только одного контроллера за один кадр, а не всех четырех сразу. Используя значение Time.frameCount по модулю, можно легко распределить множество различных операций по 16 кадрам в секунду.

Недавно меня попросили помочь улучшить производительность игры другой студии. Базовая производительность была вполне удовлетворительной, но в процессе игры возникали лаги. Уровень был разделен на части, которые загружались и выгружались динамически: так разработчики хотели сократить нагрузку на графический процессор. Однако их узким местом стал ЦП. Вся игра весила около 2,5 Гб и полностью помещалась в памяти консоли. Чтобы исправить ситуацию, нужно было всего лишь прекратить динамическую загрузку/выгрузку фрагментов уровня и просто сохранить все в консоли.

4. Повтор

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

Что оптимизировать

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

Недостаток производительности GPU: динамическое разрешение 

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

Пики загрузки CPU 

Используйте Incremental Garbage Collection. Благодаря этой функции, возможно, не придется сокращать выделенные сборщики мусора. Часто пиковые загрузки ЦП возникают из-за его работы. Incremental Garbage Collection позволяет значительно уменьшить пики загрузки. Правда, на некоторых проектах нам пришлось отключить эту функцию на нескольких платформах из-за сбоев Unity (Switch — Unity 2019.4).

Недостаток производительности CPU 

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

GPU и CPU: технология рендеринга

Хотя на топовых платформах в наших играх часто используется Deferred Rendering, было доказано, что лучше переключиться на Forward Rendering на устройствах более низкого уровня (мобильные платформы, Switch, Xbox One, PS4). Преимущества в производительности Deferred становятся очевидными только при использовании многопиксельной подсветки.

Сейчас много говорят про новые технологии рендеринга — HDRP и URP, повышающие производительность. Но на практике мы не слышали, чтобы благодаря им игры выиграли в производительности (скорее, наоборот).

GPU и CPU: отладчик кадров

Необходимо использовать отладчик кадров Unity (Frame Debugger). Подобно Timeline в профайлере, этот инструмент помогает понять, как на самом деле работает ваша игра, визуализируя ее. Обработка вызовов отрисовки отнимает процессорное время. То есть — важно сократить вызовы отрисовки, использовать слияние шейдеров и/или материалов, GPU Instancing, а также динамический и статический батчинг.

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

Отладчик кадров Unity
Отладчик кадров Unity

Прочее

Помимо Occlusion Culling есть еще две настройки, которые стоит проверить перед использованием.

Graphic Jobs 

Нам пришлось отключить их на многих платформах из-за сбоев Unity (Particle jobs содержат ошибки). Как обычно: перед включением проверьте не страдает ли от этого производительность.

Динамический батчинг 

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

Нужны ли другие инструменты

Есть и другие инструменты, которые использовались раньше: от PIX (Xbox) до Intel VTune. Однако современный профайлер Unity предлагает все, что необходимо для внесения наиболее важных изменений. Для некоторых конкретных платформ можно использовать дополнительные инструменты (PIX, XCode, Android Studio и другие), чтобы упростить доступ к информации об устройстве. Но по моему опыту, встроенных инструментов Unity достаточно для выполнения оптимизации.

Отдельно про ограничение со стороны ЦП

У всех наших игр на Unity ЦП становится узким местом всей системы. Если проблемы с видеокартой и возникают, то обычно из-за того, что мы не провели какую-то базовую оптимизацию. Дело в том, что Unity очень много задач отправляет на один тред ЦП, хотя современные процессоры часто имеют по восемь ядер. 

Невозможно использовать всю фактическую мощность CPU. Поэтому такие функции, как Graphic Jobs, очень важны. Burst/Jobs/DOTS должны стать решениями этой проблемы в будущем. Но на сегодняшний день мы еще не нашли способ применить их с пользой для наших игр.

Таск менеджер типичной игры на Unity с задержкой в CPU 3 (основной тред Unity)
Таск менеджер типичной игры на Unity с задержкой в CPU 3 (основной тред Unity)

Смежные вопросы

Сокращение используемых объемов памяти и исправление OOM-сбоев

Работая над производительностью игры, вы столкнетесь со сбоями Out Of Memory или медленной загрузкой из-за неоптимизированного использования ресурсов. Хотя память не обязательно напрямую влияет на производительность, она все-таки важна. Оптимизацию использования памяти лучше всего проводить при подготовке оптимизационных билдов. Используйте Memory Profiler, чтобы точно определять занимаемые объемы. Совет: велика вероятность, что шейдеры съедают 50% памяти, тут стоит глубже погрузиться в правильное ограничение ключевых слов шейдера (на эту тему стоит написать отдельную статью).

Window > Analysis > Memory Profiler 
Window > Analysis > Memory Profiler 

Заключение

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