Оптимизируем работу с физикой в Unity

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

Слои и Матрица Коллизий

Все игровые объекты в Unity, если не указано иного, создаются на слое Default, где всё со всем сталкивается, но это не очень эффективно. Однако мы можем обозначить, что с чем должно взаимодействовать, определив разные слои для разных типов объектов. Для каждого нового слоя добавляются свои строка и столбец в Матрице Коллизий, которая отвечает за определение коллизий между слоями. По умолчанию, когда вы добавляете новый слой, Матрица Коллизий определяет столкновения этого слоя со всеми остальными слоями. Путём правильной настройки слоев и Матрицы Коллизий вы избежите незапланированных столкновений и сможете тестировать события коллизий.

Для целей данной статьи была создана простая демо-сцена с 2 000 объектов (1 000 красных и 1 000 зелёных) внутри контейнера. Зелёные объекты должны взаимодействовать только сами с собой и со стенами (слой Wall). В одном из тестов все объекты принадлежат слою Default и столкновения производятся путём строкового сравнения тега игровых объектов на слушателе коллизий. В другом тесте объекты разделены на два слоя. Взаимодействие этих слоёв было настроено в Матрице Коллизий.

Рис. 1: Настройка Матрицы Коллизий

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

Рис. 2: Количество столкновений в течение 5 секунд

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

Рис. 3: Общие слои в сравнении с отдельными в данных профилировки

Как мы видим, существуют значительные различия во времени, которое процессор потратил на физику. На общем слое это ~27,7 мс, а на отдельных — ~17,6 мс.

Рейкастинг

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

Вот несколько советов по использованию рейкастинга:

  • Хоть это и очевидно, но нужно стремиться к наименьшему количеству лучей.
  • Не делайте лучи длиннее, чем это необходимо. Чем длиннее луч, тем больше объектов нужно проверять на столкновение с ним.
  • Не используйте рейкасты внутри метода FixedUpdate(). А в некоторых случаях даже использование их внутри метода Update() может быть излишним.
  • Будьте осторожнее с типами коллайдеров, которые используете. Рейкастинг с меш-коллайдером обходится довольно дорого.
    • Неплохое решение — создать дочерний объект с примитивными коллайдерами и повторить с их помощью приблизительную форму меша. Все дочерние коллайдеры у родительского Rigidbody ведут себя как составные коллайдеры.
    • Если вам очень нужно использовать меш-коллайдеры, то как минимум сделайте их выпуклыми.
  • Уточняйте, во что именно луч должен попадать, и по возможности конкретизируйте слой-маску в функции рейкаста.
    • Хоть это и описано в документации, но не забудьте, что то, что вы указываете в функции рейкаста, это не идентификатор слоя, а битовая маска.
    • Если вы хотите, чтобы луч сталкивался с объектом, который находится на слое с идентификатором 10, вам необходимо указать 1<<10 (битовый сдвиг влево на 10), а не просто 10.
    • Если вы хотите, чтобы луч врезался во всё, кроме объектов на слое 10, то просто используйте побитовый оператор дополнения (~), который инвертирует каждый бит в побитовой маске.

Вот простая демо-сцена, где объект стреляет лучами, которые сталкиваются только с зелёными объектами.

Рис. 4: Простая демо-сцена с рейкастами

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

Рис. 5: Зависимость производительности от количества лучей

Рис. 6: Зависимость производительности от длины лучей

А вот что будет, если поменять примитивные коллайдеры на меш-коллайдеры.

Рис. 7: Меш-коллайдеры на сцене (110 полигонов на коллайдер)

Рис. 8: Примитивные коллайдеры в сравнении с меш-коллайдерами в данных профилировки

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

Физика 2D vs 3D

Решите, какой физический движок лучше подходит для вашего проекта. Если вы разрабатываете 2D-игру или 2.5D (псевдотрёхмерность), то использование физического движка для 3D может быть избыточным. Дополнительное измерение в вашем проекте впустую загружает процессор. Вы можете посмотреть более детальные различия между двумя движками в статье.

Rigidbody

Компонент Rigidbody — это привычный компонент для физических взаимодействий между объектами. Даже когда вы работаете с коллайдером как с триггером, вам нужно добавить его на игровой объект для корректной работы событий OnTrigger. Игровые объекты, которые не имеют компонента Rigidbody, рассматриваются как статические коллайдеры. Это крайне важно, т. к. попытка сдвинуть статический коллайдер крайне неэффективна, ведь движок пересчитывает весь физический мир заново. К счастью, профайлер даст вам знать, если вы попытаетесь сдвинуть статический коллайдер, и выведет предупреждение во вкладке профайлера. В следующем тесте были убраны Rigidbody со всех движущихся объектов первой демо-сцены и засняты данные профилировки, чтобы продемонстрировать, как влияют попытки сдвинуть статические коллайдеры.

Рис.9: Предупреждение перемещения статических коллайдеров

Как вы можете видеть, около 2 000 предупреждений генерируется на каждом игровом объекте. Также средние затраты времени, потраченного ЦПУ на физику, возросли с ~17,6 мс до ~35,85 мс. Поэтому если мы двигаем игровой объект, важно добавить на него Rigidbody. Если вы хотите вручную контролировать его движение, то просто пометьте его как Kinematic в свойствах Rigidbody.

Фиксированные временные участки (Timestep)

Настройка значения фиксированных timestep в Time Manager непосредственно влияет на FixedUpdate() и частоту обновления физики. Изменяя это значение, вы можете достигнуть золотой середины между точностью и временем, затраченным ЦПУ на физику.

Заключение

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

Перевод статьи «Physics Best Practices»

Александр Ланский