Страницы

суббота, 21 сентября 2013 г.

[Перевод] “0 – 60 fps за 14 дней!” Чему мы научились, оптимизируя игру на Unity3D.

Гарантией плавного и приятного процесса игры является высокая частота кадров и достижение 60 кадров в секунду на обычных iPhone и iPad устройствах было важным достижением в разработке нашей новой игры, Shadow Blade. (http://shadowblade.deadmage.com)

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

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

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

1 – Лицом к лицу со свирепым монстром по имени Garbage Collector.

Мы начинали разрабатывать игры на C/C++ и не привыкли к некоторым тонкостям разработки, которые предполагает автоматический Сборщик Мусора. Быть уверенным в том, что неиспользуемые объекты будут подчищены из памяти автоматически – здорово, до тех пор пока не столкнешься с реальной ситуацией, когда профайлер показывает пики нагрузки CPU, вызванные работой Сборщика Мусора, выполняющего свои прямые обязанности. Особенно это заметно на мобильных устройствах. Нашей первостепенной задачей стал поиск больших растрат памяти и принятие мер по их устранению. Вот некоторые из способов, которые мы применили:

  1. Избавление от объединений строк (тип string) в коде, так как это оставляет кучу мусора для GC.
  2. Замена всех циклов “foreach” на обычные “for” циклы. По некоторым причинам, каждая итерация цикла “foreach” генерировала 24 Байта мусорной памяти. Простейший цикл из 10 итераций занимал 240 Байт памяти, которую должен был подчистить GC, что было неприемлемо.
  3. Замена способа проверки тегов игровых объектов. Вместо “if (go.tag == “Enemy”)” мы стали использовать “if (go.CompareTag (“Enemy”)”. Когда мы вызываем свойство tag у игрового объекта, выделяется дополнительная память доя копии строки и это очень плохо, если вызовы происходят в цикле.
  4. Паттерн объектных пулов - великолепен! Мы создавали и использовали пулы для всех динамических игровых объектов, так что память не выделялась динамически в процессе игры, а когда объект переставал быть нужным он возвращался в пул.
  5. Отказ от использования LINQ команд, так как это приводит к выделению промежуточных буферов – пища для GC.

2 – Осторожность при использовании вызовов между скриптами на высоком уровне и нативным C++ кодом.

Весь игровой код приложения созданного на Unity3D – это скрипты, в нашем случае это был C#, которые обрабатываются с помощью исполняемой среды Mono. Любые взаимодействия с данными движка приводят к вызовам нативного кода из высокого уровня скриптов. Естественно это приводит к излишним  затратам производительности и уменьшение количества подобных вызовов являлось второй по приоритетности задачей. 

  1. Перемещение объектов по сцене всегда приводит к вызовам нативного кода и мы пришли к тому, что стали кэшировать трансформации объекта в течение кадра и посылать вызов только раз, для понижения избыточных нагрузок. Подобный прием мы использовали и в других похожих случаях, не только при перемещении и вращении объекта.
  2. Кэширование ссылок на собственные компоненты позволило избавиться от необходимости кажды раз получать доступ к компоненту через метод “GetComponent”, что так же приводит к вызовам нативного кода движка.

3 – Физика, физика и еще раз физика.


  1. Уменьшение времени физического шага до минимально возможного. В нашем случае мы не могли установить значение менее 16 миллисекунд.
  2. Уменьшение количества вызовов команд перемещения Character Controller. Перемещения Character Controller происходят синхронно и каждый вызов стоит значительных затрат производительности. Мы собирали все данные перемещения в течение кадра и применяли их единожды.
  3. Исключение использований “ControllerColliderHit”. Вызовы этого метода обрабатываются недостаточно быстро.
  4. Замена физической ткани(cloth) на Skinned Mesh на слабых устройствах. Конечно потребуется потратить время для поиска баланса между приличным видом результата и производительностью.
  5. Ragdoll'ы были отключены, так что они обрабатывались в физическом цикле и включались только по мере необходимости.
  6. Вызовы “OnInside” на триггерах нужно использовать аккуратно. Мы старались отказаться от их использований вообще.
  7. Слои вместо тегов. Объектам легко назначить слои и теги для доступа к определенным объектам, однако слои имеют некоторые преимущества, по крайней мере в производительности, когда приходится работать с физикой столкновений. В результате мы повышаем скорость физических расчетов и избавляемся от лишних выделений памяти.
  8. Уверенное «Нет!» меш-коллайдерам.
  9. Сокращение количества проверок столкновений с помощью лучей и сфер и использование информации от каждой проверки по максимуму.

4 – Давайте сделаем код AI быстрее!

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

  1. Множество запросов связанных с физических движком происходят из логики AI, например проверка видимости игрока. Цикл обновлений AI можно сделать реже, чем цикл графических обновлений, для снижения загрузки на CPU.

5 – Наибольшая производительность достигается там, где нет кода вообще!

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

  1. Используя свой алгоритм LOD, ИИ противников отключался, когда они находились далеко.
  2. Отключались двигающиеся платформы и препятствия, а так же их физические коллайдеры.
  3. Встроенная в Unity система остановки невидимых анимаций (animation culling)  помогла отключить анимацию на объектах, которые не рендерились.
  4. Такой же механизм использовался для систем частиц.

6 – Callback! А что у нас с пустыми callbacks?

Функции обратного вызова в Unity должны использоваться как можно реже. Даже пустые callback'и понижают производительность. В пустых callback нет смысла, однако они могут остаться в коде в следствие множества переписываний скриптов и рефакторинга.

7 – Могучие художники спешат на помощь.

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

  1. Использование общих материалов для игровых объектов и метка static приводит к тому, что они объединяются графической системой и понижают количество Draw Call'ов, а это крайне важно для производительности на мобильных устройствах
  2. Атласы текстур особенно полезны при отрисовке UI элементов.
  3. Квадратные текстуры, размером степени двойки и подходящий алгоритм сжатия – так же важный момент.
  4. Так как мы делали игру жанра side-scroller, наши художники заменили все фоновые меши на простые 2D плоскости.
  5. Light maps оказались ценным решением.
  6. Наши художники избавились от лишних вершин в мешах.
  7. Поиск приемлемого количества уровней Mip-map'ов текстур помогло повысить количество кадров на устройствах с различными разрешениями.
  8. Так же художники помогли в оптимизации, использовав объединение мешей.
  9. Наш аниматор старался использовать общие анимации между разными персонажами там, где это было возможно.
  10. Пришлось долго настраивать систему частиц, чтобы найти баланс между приемлемым видом и производительностью. Наиболее сложными оказались попытки уменьшить количество источников и прозрачностей.

8 – Необходимо сократить использование памяти, немедленно!

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

  1. Для различных устройств использовались различные размеры текстур, в особенности для UI и больших фонов. Shadow Blade использует универсальный билд, но разные ассеты загружаются в зависимости от размера и разрешения экрана при запуске игры.
  2. Нам пришлось поработать над тем, чтобы неиспользуемые ресурсы не загружались в память. На поздних этапах разработки мы обнаружили, что всякий ассет, даже если на него ссылается только префаб и он никогда не создается, полностью загружается в память.
  3. Помогло и удаление лишних полигонов  с мешей.
  4. Нам пришлось несколько раз переработать архитектуру жизненного цикла некоторых ассетов. Например, настройка времени загрузки/выгрузки ресурсов главного меню, ассетов в конце уровня или игровой музыки.
  5. С каждым игровым уровнем был связан отдельный пул объектов, собранный для его динамических объектов и оптимизированный для уменьшения использования памяти. Пулы объектов могут быть гибкими и содержать множество объектов на стадии разработки, однако, во время игры их содержимое должно быть определенным, если известны текущие требования.
  6. Необходимым оказалось хранение звуковых файлов сжатыми в памяти.

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

Игровой трейлер Shadow Blade: http://youtu.be/tgSXLVAwZJs

Комментариев нет :

Отправить комментарий