Оптимизация игр на 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.
Некорректные данные при запуске Deep Profiling
Не используйте эту опцию, пока не узнаете, когда это нужно делать. Deep Profile можно включить, если есть проблема с конкретной частью программы: это поможет определить, какая часть кода вызывается. При этом не стоит ориентироваться на полученные тайминги, потому что глубокое профилирование слишком сильно перегружает небольшие методы, странным образом искажая все результаты. В качестве альтернативы или дополнения к глубокому профилированию можно расставлять собственные маркеры профиля.
Использование кастомных маркеров профиля:
Некорректные данные из-за физики в 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: он упрощает сравнение данных в профиле. Это поможет обнаружить пики загрузки или другие отличия между разными билдами/конфигурациями настроек. То есть этот инструмент отмечает все произведенные улучшения.

Profile Analyzer экономит много работы: выберите две области, и он автоматически сообщит, в чем разница.
2. Базовая производительность
Сначала мы обычно игнорируем пики загрузки и сосредотачиваемся на том, чтобы базовая производительность была в пределах нормы. Здесь главное довести «нормальный» игровой цикл до приемлемого уровня, будь то 30 кадров в секунду (мобильные платформы, Switch), 60 или даже 120 (VR).
Используйте профайлер Unity, чтобы понять, что снижает базовую производительность, и решить только самые важные проблемы. Timeline помогает увидеть, как работает игра с точки зрения производительности.

Режим отображения 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, а также динамический и статический батчинг.
Отладчик кадров также помог нам отследить неприятные ошибки, из-за которых объекты или весь экран становились черными. Прокручивая вызовы отрисовки, можно точно узнать, когда и как что-то отображается.

Прочее
Помимо Occlusion Culling есть еще две настройки, которые стоит проверить перед использованием.
Нам пришлось отключить их на многих платформах из-за сбоев Unity (Particle jobs содержат ошибки). Как обычно: перед включением проверьте не страдает ли от этого производительность.
Сотрудники Unity упоминали, он полезен только при работе на старых устройствах. Динамический батчинг тоже может сильно нагружать ЦП, и выгода от видеокарты не окупится. Пока не смог оценить явные плюсы и минусы этой настройки.
Нужны ли другие инструменты
Есть и другие инструменты, которые использовались раньше: от PIX (Xbox) до Intel VTune. Однако современный профайлер Unity предлагает все, что необходимо для внесения наиболее важных изменений. Для некоторых конкретных платформ можно использовать дополнительные инструменты (PIX, XCode, Android Studio и другие), чтобы упростить доступ к информации об устройстве. Но по моему опыту, встроенных инструментов Unity достаточно для выполнения оптимизации.
Отдельно про ограничение со стороны ЦП
У всех наших игр на Unity ЦП становится узким местом всей системы. Если проблемы с видеокартой и возникают, то обычно из-за того, что мы не провели какую-то базовую оптимизацию. Дело в том, что Unity очень много задач отправляет на один тред ЦП, хотя современные процессоры часто имеют по восемь ядер.
Невозможно использовать всю фактическую мощность CPU. Поэтому такие функции, как Graphic Jobs, очень важны. Burst/Jobs/DOTS должны стать решениями этой проблемы в будущем. Но на сегодняшний день мы еще не нашли способ применить их с пользой для наших игр.

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

Заключение
Надеюсь, эта статья поможет вам с оптимизацией или хотя бы подскажет, как выбрать более эффективный подход к профилированию. Удачи с разработкой.
- оптимизация
- профилирование производительности
- unity
- геймдев
- unity profiler
Практическое руководство по оптимизации для мобильных
Это руководство предназначено для новичков в мобильном геймдеве. Для тех, кто испытывает трудности при планировании и прототипировании новой мобильной игры (или портировании уже существующего проекта). Также этот раздел будет полезен в качестве справки для каждого, кто делает мобильные или браузерные игры (с целевой платформой — старые ПК или нетбуки).
Оптимизация вообще широкая тема, и то как вы ее сделаете, целиком зависит от вашей игры, поэтому данное руководство следует рассматривать как некое введение или ссылку, а не пошаговое руководство.
Мобильные устройства не созданы одинаковыми
Информация здесь предполагает аппаратное обеспечение на уровне чипсета Apple A4, который используется в оригинальных iPad, iPhone 3GS и третьем поколении iPod Touch. Из Android предполагается устройство подобное Nexus One, или большинства устройств, работающих на Android 2.3 Gingerbread. В основном, эти устройства были выпущены в начале 2010 года. Эти устройства старее, медленнее современных, но так как они составляют большую часть рынка, их также следует поддерживать.
Есть очень быстрые и очень медленные телефоны. Вычислительные мощности мобильных устройств растут с потрясающей скоростью. Для нового поколения мобильной GPU, быть в 5 раз быстрее своего предшественника — обычное дело. Скорость мобильных устройств уже сравнима со скоростью ПК.
Для обзора технических характеристик мобильных устройств от Apple, см. Hardware.
Если вы хотите разрабатывать под мобильные устройсва, которые станут известными в будущем, или эксклюзивные high end устройства прямо сейчас, вы можете это сделать. См. Мобильные устройства будущего.
Очень низкая производительность (например, iPhone 3G или первое и второе поколение iPod touches) требует особого внимания к оптимизации. В противном случае могут возникнуть проблемы когда покупатели, не обновившие устройства, будут покупать ваши приложения. Если же вы делаете бесплатное приложение, можно не беспокоится о поддержке старых устройств.
Оптимизацию не следует считать последней стадией разработки проекта
Британский ученый Майкл А. Джексон часто цитируется своими Правилами оптимизации программ:
_Первое правило оптимизации программы: не делаете ее. Второе правило оптимизации программы (только для экспертов!): не делайте ее пока что.
Он обосновал это тем, что учитывая рост скорости компьютеров, ваша программа будет достаточно быстрой. Кроме того, если вы попытаетесь слишком много оптимизировать, то сильно усложните код, ограничите себя и создадите много ошибок.
Однако, если вы разрабатываете мобильные игры, есть еще одно мнение: аппаратное обеспечение, представленное сейчас на рынке, сильно ограничено по сравнению с компьютерами, которые мы используем для работы. Поэтому высок риск того, что ваша ваша игра не будет работать на большинстве устройств и оптимизацию рекомендуют делать с самого начала разработки.
В данном руководстве мы постараемся указать ситуации, когда оптимизация сыграет большую роль в производительности, по сравнению с обратными ситуациями, когда оптимизация большого значения не имеет.
Оптимизация: Не только для программистов
Художникам тоже полезно знать ограничения платформы и методы, которые используются для того, чтобы их обойти. Зная это, они могут принимать креативные решения, которые в итоге сэкономят их труд.
- На художника ложится большая ответственность. Если дизайн игры предполагает атмосферность и освещение, их можно нарисовать в текстурах вместо запекания.
- Каждый раз, когда что-либо может быть запечено, художники могут готовить контент для выпекания, вместо рендеринга в реальном времени. Это позволяет им игнорировать технические ограничения и работать свободно.
Планируйте игру так, чтобы во время исполнения она работала “плавно”.
Эти две страницы детально описывают основные тенденции в игровой производительности и объясняют, как лучше спланировать оптимизацию своей игры или как интуитивно выявить места, нуждающиеся в оптимизации (в случае, если игра уже вышла в продакшн).
- Практические методы для оптимизации рендеринга
- Практические методы для оптимизации скриптинга и геймплея
Профилируйте на ранней стадии и почаще
Профилирование важно, потому что оно поможет выяснить, какие оптимизации действительно приведут к большому приросту производительности, а какие являются пустой тратой вашего времени. Благодаря тому, что рендеринг обрабатывается на отдельном чипе (GPU), отрисовка одного кадра занимает в два раза меньше времени (только GPU, а не CPU + GPU). Это означает, что если CPU замедляет работу, оптимизация ваших шейдеров вообще не повысит частоту кадров, и если GPU замедляет работу, не помогут оптимизация физики и скриптов.
Часто бывает так, что разные части игры и разные ситуации работают по разному, так что одна часть игры может привести к 100 миллисекундным кадрам полностью из скрипта, а другая может привести в замедлению игры, потому что в данный момент что нибудь рендерится. Поэтому, если вы собираетесь оптимизировать свою игру, нужно по крайней мере выявить узкие места.
Внутренний Профайлер
Профайлер в Unity в основном используется при ориентации на iOS и Android. См. Руководство по профайлеру для основных инструкций по его использованию.
Внутренний Профайлер
Внутренний профайлер выкидывает текст каждые 30 кадров. Это поможет вам выяснить, какие аспекты вашей игры замедляют ее, будь то физика, скрипты, визуализация, но без множества деталей (например, только название скрипта или визуализации).
См. Встроенный Профайлер для подробной информации о том, как это работает и включается.
Профайлер с рендерингом
Профайлер без рендеринга
Продолжаем оптимизировать мобильные игры на Unity. Используем профайлер, а также смотрим, куда лезть не стоит
Постоянно просматриваю сотни новых проектов и вижу новые пробелы в производительности даже в гиперказуальных играх, причем практически во всех. В прошлой статье я описал четыре простых шага и несколько рекомендаций, как оптимизировать практически любую простую игру на Unity под слабые устройства, затратив на это минимум ресурсов.
Сегодня рассмотрим еще несколько нюансов, которые могут облегчить разработку или ревью вашего проекта, чтобы приблизить дату релиза и избежать разочарования игроков от плохой производительности.
Также поговорим, на что не стоит обращать внимания в небольших проектах, чтобы не тратить лишнее время ради пары процентов производительности.
Начнем с основного инструмента, который сильно может облегчить жизнь при поиске слабых мест.
1. Профайлер
Встроенный в Unity профайлер я всегда использую как стартовую точку. Здесь все интуитивно, даже если пользуешься им первый раз, поэтому игнорировать его точно не стоит. Он наглядно показывает части кода, которые тормозят.
Проблемы обычно возникают, когда код писал кто-то другой — тут придется разобраться, что он делает и как это исправить.
Если профайлер Unity не дал однозначных ответов, переходим к нативным профайлерам — Android Studio или Xcode, в зависимости от платформы.
В более сложных, непонятных ситуациях переходим на специализированные, например, Snapdragon Profiler, Arm Mobile Studio и так далее, в зависимости от девайсов под рукой. Функционал у таких профайлеров плюс-минус одинаковый, просто они под разное «железо».
2. Баннерная реклама
Не так давно был случай, когда я использовал профайлер не совсем корректно. По запросу от команды проводился анализ игры. Вводная была такая: стабильно низкий FPS. И так как речь шла о нем, я проводил замеры не со старта игры, а через некоторое время, и внимательно изучал проект на «плато». Я заметил несколько проблем и почти два дня составлял рекомендации по оптимизации. Выписал огромное количестве рецептов, но по итогу производительность выросла на 10%, что очень мало.
Дальнейший анализ показал, что проблема на самом деле существовала первые 15-20 секунд после старта игры из-за загрузки баннерной рекламы. Как только баннер прогружался — лаги прекращались.
Баннер — это, по сути, браузер, запущенный внутри игры. А браузер довольно тяжелое приложение, которое используется просто, чтобы показать маленькую картинку или анимацию. Пока он загружается, игра может лагать. Такой подход используют абсолютное большинство рекламных сетей. Поэтому, если бы кто-то захотел избавиться от проблемы, то ему бы пришлось написать свой плагин и создать свою компанию, которая продает рекламу.
Поэтому выходов из такой ситуации всего два:
- Выключить баннерную рекламу на слабых устройствах и потерять часть монетизации.
- Смириться с некоторыми лагами первые 30 секунд (в идеале попытаться как-то это замаскировать), пока баннер загружается.
В целом, все рекламные плагины вызывают лаги, и общая рекомендация — подгружать плейсменты рекламы по очереди, немного размазывая лаг во времени.
3. Отсечение того, что за кадром
Случается, что камера смотрит вниз, где два игровых персонажа, а вокруг гуляет еще десяток невидимых «лишних», которые отнимают ресурсы. Обычно это не критично, потому что на небольших проектах мало объектов, все они попадают в камеру и нет больших уровней.
Если же уровень большой и объектов действительно много, не забывайте отключать все, что не видно. Например, у анимированных объектов (CullingMode) есть три опции:
- анимировать всегда (AlwaysAnimate);
- анимировать, только когда игрок их видит (CullCompletely);
- анимировать только физику, если ты их не видишь (AnimatePhysics).
По дефолту в Unity стоит «анимировать всегда», поэтому в большинстве случаев эту галочку нужно отключать.
4. 3D-модели и текстуры
Для небольших проектов этот пункт не особо актуален. Ситуации встречаются двух типов:
- Колоссальные ошибки. Например, разработчик сделал партикл-систему из сфер, в которых по полторы тысячи полигонов в каждой. Не надо так.
- Всё в пределах нормы, и оптимизация не стоит потраченного времени.
5. Организация проекта
Беспорядок в организации файлов, дублирование ассетов и текстур, нечитабельные имена и вот это все банально замедляют разработку. Хотя в казуальных проектах от сторонних студий это последнее, на что стоит обращать внимание.
Осложняет ситуацию еще и то, что каждый случай индивидуальный. У разных команд свои сильные и слабые стороны. Если кто-то привык к определенному порядку и иерархии проекта, то пытаться себя перестроить — это потратить уйму времени, сил и не факт, что станет лучше.
Я не даю рецептов в этом плане и не лезу в чужие процессы без крайней необходимости. Хорошо, когда проект с самого начала грамотно организован, но в статье мы все-таки говорим про технические ошибки.
Пример оптимизации мидкор-проекта
Теперь рассмотрим конкретный кейс из прошлого с примерами советов по оптимизации одного проекта.
Это была мидкорная игра с кораблями на 10 игроков. Но ее проблема заключалась в том, что на средних по производительности устройствах FPS падал до 5 кадров в секунду. И причин тому нашлось несколько.
Скиннинг моделей
Непомерно много на проекте отнимал скиннинг 3D-моделей — парусов и моряков на палубе было очень много. Unity умеет делать скиннинг на GPU для ускорения, но даже это не всегда помогает, особенно на слабых устройствах где и так GPU очень слаб.
В данном кейсе скиннинг занимал 60 миллисекунд, и уже это приводило к ограничению в 12 FPS, без учёта рендера кадра и прочего.
- Для начала стоит на всех аниматорах, которые не влияют на симуляцию, а только визуальны, поставить CullingMode на CullCompletely (см. раздел №3 выше). И хорошо бы ещё и на определённом расстоянии от камеры их тоже выключать, даже если видно.
- Уменьшить количество скин мешей в принципе.
- Уменьшить количество костей на вершину, иногда сделать на каждый треугольник по одной-две кости, если это не очень критично влияет на визуальную красоту.
- Уменьшить количество вершин на моделях.
- Можно переделать анимации и так далее (например, уменьшить количество костей).
Очистка буфера кадра
При рендрере первой камеры (или на старте рендера через SRP) нужно чистить весь буфер кадра, а не только Z и стенсил. Если что-то забыть — ломается маркер начала кадра, что на мобильных GPU критично и некоторые вещи, которые сделаны на аппаратном уровне, могут отрабатывать не так, как ожидается.
Форматы текстур
Хорошие и модные форматы текстур могут не поддерживаться конкретными устройствами. В таких случаях Unity показывает, что они должны весить, например, 0.7 Мб, а на девайсе по факту выходит 2.7 Мб.
Дело в том, что если какой-то оригинальный формат текстур не поддерживается на конкретном девайсе, то Unity распаковывает его в другой формат и использует в несжатом виде. Получается, что текстуры должны весить мало (как показывает Unity), а на самом девайсе они весят в несколько раз больше. Всё потому, что встроенный профайлер показывает только расчетный размер текстуры, если на телефоне будет поддерживаться такой формат.
Чтобы узнать истинный размер текстуры — нужны нативные профайлеры, о которых я упоминал выше.
Аллокации
Большое количество аллокаций (выделений памяти).
Обычно считается, сколько делается аллокаций на один кадр. И если их там больше, условно, чем 5-10 Кб на один кадр, то стоит задуматься над поиском и ликвидацией этих мест.
В итоге
Добились 20-30 FPS на слабых устройствах, вместо изначальных 5.
Четыре простых шага, как оптимизировать производительность мобильной игры на Unity
Изучив множество прототипов, я практически не встретил ни одного, который был бы оптимизирован под слабые устройства, даже когда дело касается Hyper Casual. Даже если на мощных устройствах у вас стабильно хорошая производительность, то постобработка, неправильные настройки графики и теней могут уничтожить FPS в пару кликов — а в релизе такие ошибки критичны.
Поэтому, когда мне приносят очередной проект, я стараюсь дать разработчикам самые быстрые и эффективные решения без необходимости переделывать половину игры. Ниже приведу одни из самых распространенных и простых ошибок в оптимизации, которые можно исправить, потратив минимум ресурсов.
Это особенно актуально для ГК-проектов, которые разрабатываются и тестируются быстро на реальной аудитории, но должны плавно запускаться на максимум устройствах.
Очевидная причина проблем с оптимизацией в том, что мощность графического процессора мобильного девайса несравнима с видеокартой ПК, GPU флагманов может быть в десятки раз мощнее, чем у массовых и дешевых девайсов, а постобработка подразумевает дополнительные операции с отрендеренным изображением каждый кадр, то есть 30-60 раз в секунду.
При этом мобильные игры не всегда требуют высокого качества изображения, хотя бы из-за размеров экрана. Но разработчики все равно часто используют приятные глазу эффекты, знакомые пользователям консолей и ПК: блум, цветокоррекция, антиалиасинг и так далее.
В итоге игра начинает тормозить и нужно искать причину.
Причина №1. Антиалиасинг и желание сделать красиво
Антиалиасинг нужен для устранения эффекта «лесенки» на краях объектов. Красиво, и на устройствах выше среднего это практически «бесплатно», но на слабых может сильно испортить игровой опыт.
Выходом могла бы стать настройка под разные девайсы, но по опыту могу сказать, что так делают немногие разработчики Hyper Casual. Поэтому проще и быстрее полностью отказаться от антиалиасинга, разница от наличия которого может быть незаметна на экране мобильного девайса.
Дальше есть два пути, как решить вопрос цветокоррекции.
- Это можно сделать на стороне художников, но перерисовка текстур требует много ресурсов, поэтому отбрасываем вариант.
- Пройтись по всем шейдерам в проекте и вставить туда пару строчек, которые делают цветокоррекцию. В таком случае цветокоррекция получается практически «бесплатной». Но если шейдеров слишком много, и они, например, покупные, а разработчик не понимает, что куда вставлять — могут возникнуть проблемы.
Фишка в том, что если мы хотим сделать цветокоррекцию, но не хотим вставлять ее в шейдер, то рендер нельзя сделать сразу на экран. Надо сначала нарисовать всю игру куда-то во внутреннюю текстуру, потом пройтись по ней с цветокоррекцией и только потом вывести на экран. Из-за этой многоступенчатости FPS на слабых устройствах сразу падает в несколько раз.
Еще один нюанс: вставить цветокоррекцию в шейдер можно только при небольшом количестве источников света. Для большинства мобильных игр это не актуально, но на всякий случай поясню.
В шейдер нельзя добавить километры кода, так как есть ограничения. Туда можно вписать, например, 10 источников света, чтобы они обрабатывались в этом шейдере. Но если нужно больше, то придется этот объект нарисовать еще раз, а потом приплюсовать освещение от новых источников света. С таким вторым проходом цветокоррекция в шейдере уже не сработает, она будет применяться криво.
Причина №2. Физика и желание всё сделать «честно»
Кейс: на сцене одновременно взрывается 50 объектов, что сильно нагружает устройство.
Решение простое: не обязательно все взрывать по-честному — ближе к камере можно применить качественные и проработанные эффекты, а для всего, что расположено по краям или далеко, сделать более простой эффект в виде дыма или вовсе ничего не проигрывать.
Или другой пример из моей практики:
Есть игра, где бежит 3D-человечек и у него есть два варианта смерти — от разрезания по вертикали циркулярной пилой или по горизонтали крутящимися ножами. Чтобы это реализовать, можно поставить тяжелый плагин, который прямо во время игры берет меш человечка, его правильно разрезает на несколько объектов, зашивает «дыру», которая образовалась, и навешивает на каждую часть физику. Это происходит очень медленно и может тормозить даже на ПК. А что если в игре 20 человечков, которые синхронно попадают под нож?
В этом случае можно просто сделать два префаба: один разрезанный горизонтально, другой — вертикально. Сохранить их, просчитать заранее и использовать в игре уже готовое.
Причина №3. Большое количество независимых объектов
Кейс: окружение сцены состоит из кубов, к которым применены разные материалы с разными шейдерами, или просто из большого количества кубов. При достижении 10 000 кубов в сцене игра начинает заметно тормозить, так как каждый из объектов требует один вызов отрисовки (drawcall) на видеокарте.
Поможет объединение мешей (батчинг) объектов в один большой меш для более быстрой отрисовки в сцене с множеством одинаковых объектов. Это можно сделать инструментами Unity, но лучше вручную.
При этом статический батчинг (Static Batching) хуже склеивания в один меш, если у вас на сцене много маленьких объектов по паре треугольников. Потому что тогда Unity будет рендерить статик батчинг как один и тот же меш, но по кусочку с кучей вызовов. То есть 10 000 кубов будут рендериться в 10 000 drawcall’ов.
Решение: использовать один шейдер со всеми необходимыми параметрами для материалов, убедиться что объекты окружения помечены как статические, и что в проекте используется батчинг, который группирует эти объекты во время рендеринга и отрендерит их все за один вызов отрисовки.
Причина №4. Реалтайм тени и освещение
Основная причина, почему его не стоит использовать: расчеты освещения происходят во время работы приложения и нагружают устройство.
Решение: использование в шейдерах окружения заранее просчитанные карты освещения, которые представляют собой битмап. Целевое устройство, на котором будет запускаться игра, будет загружать карту освещения один раз, вместо того, чтобы пересчитывать её постоянно во время игры.
Полностью запеченный свет (максимальная производительность):
Один каскад теней без сглаживания, подогнанный под размеры сцены (х2 по ресурсам по сравнению с первым вариантом):
Стандартно настроенные тени с несколькими каскадами + сглаживание (х2.5 от первого варианта):
Дополнительно. Tips and tricks
Ко всему вышесказанному рекомендую сделать еще несколько вещей, чтобы ваш прототип стал еще ближе к успешному релизу.
- Уберите у всех мешей Light и Reflection пробы, если вы ими не пользуетесь — они отжирают немного производительности, даже если в шейдере нет никаких ссылок на них.
- Число итераций у Physics Solver можно уменьшить с 6 до 2 (но надо проверить, не повлияет ли это на геймплей), а частоту апдейта физики с 50 до 10-30 (примерно 30-50% от целевого FPS). Положения объектов будут интерполироваться, так что визуально никаких дерганий быть не должно.
- Меш коллайдеры это очень дорого, особенно когда меши колизятся с мешами. Лучше замените их на сферы, капсулы, кубы и так далее. Для примера вот сколько стоит посчитать коллизию между объектами на ПК (на смартфонах результаты будут еще хуже):
- Не вешайте очень много Rigidbody на большое количество трансформов — могут возникнуть большие затраты на синхронизацию физика <> трансформ.