Content Oriented Web
Make great presentations, longreads, and landing pages, as well as photo stories, blogs, lookbooks, and all other kinds of content oriented projects.
Блог xyz school

Как правильно оптимизировать игровой код

Александр Балакшин, работавший над Rainbow Six Siege в роли ведущего геймплей-программиста, объясняет, с чего лучше начинать оптимизацию кода, и какие места в нём лучше лишний раз не трогать.

Автор: Александр Балакшин

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

Данные о производительности нужно снимать сразу с целевых платформ. То, что на машине разработчиков уверенно показывает 60 кадров в секунду, устроит «слайд-шоу» на консолях. В этом плане самая удобная платформа — Xbox One. Если что-то пошло на ней, значит спокойно запустится и на PlayStation 4, и на рекомендуемой конфигурации ПК.
Unreal Insights — профилировщик для Unreal Engine
Источник
В оптимизации ААA-проектов важно, чтобы код «дружил» с кэшем. То есть, код должен минимизировать загрузку из оперативной памяти, а ещё — эффективно использовать то, что уже находится в кэше процессора. При этом порой приходится думать не только о кэше данных, но и о кэше инструкций.

Например, разработчики Sea of Thieves смогли избавиться от проблем с кэшем инструкций с помощью агрегации вызовов tick-функции по типу обновляемых компонентов. Это ещё и помогло им поднять FPS с пяти кадров в секунду до приемлемых значений. Крайне интересный доклад на эту тему можно посмотреть здесь.
Прошлое поколение консолей плохо справлялось с branch prediction, поэтому программистов заставляли минимизировать количество ветвлений в коде — тех самых branch, — чтобы избежать потенциальных проблем с кэшем. Сейчас чем-то подобным приходиться заниматься только при написании шейдеров — из-за особенностей архитектуры современных GPU, — или при создании совсем уж высокооптимизированных систем самого движка .

Оптимизируя кэш данных, нужно внимательно следить за типами коллекций. Например, красно-чёрное дерево может быть гораздо менее эффективно, чем комбинация двух простых массивов, даже если задача — найти значение по ключу. Не стоит забывать и про выравнивание для структур и классов, особенно если те используются чаще всего.
Если приходится работать с объектно-ориентированными фреймворками, лучше использовать как можно меньше динамических приведений типов. В игровых движках обычно используется своя версия RTTI (runtime type introspection) вместо стандартной из С++, — но это всё равно очень ресурсозатратный процесс. Под него трудно рассчитать время, а потом ещё приходится разбираться с парой-тройкой кэш-промахов.

Когда целевой тип известен, лучше использовать статический каст. Но если без динамического приведения не обойтись, то имеет смысл сохранить результат хотя бы как переменную в функции. А ещё лучше — как член класса, если есть такая возможность. Кстати, именно поэтому стоит свести к минимуму получение компонент у того или иного объекта, если работать приходится с соответствующей моделью.
Создавая конкретные игровые функции, нужно контролировать количество и качество (синхронные/асинхронные) пространственных проверок — рейкастов (лучей для определения коллизий), шейпкастов, оверлап-тестов. В разработке современных игр они применяются достаточно часто: в стрельбе, перемещении, паркуре, инверсной кинематике и во многом другом.

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

Обновление всех игровых компонентов можно делить на группы. Иногда каждый отдельный кадр требует запуск своего Update. В других случаях обновление достаточно производить раз в секунду — например, когда враги находятся далеко от игрока. А где-то его вообще можно отключить до активации нужных событий.

Кстати, в Unreal Engine 4 недавно добавили удобный функционал для таких задач — the significance manager. Он, наряду с функцией animation sharing, успешно доказал свою эффективность в Fortnite. На эту тему есть крайне интересное выступление с GDC 2018.
Для сетевых игр также нужно обращать внимание на объём реплицируемых данных. Когда на сервер отправляются 500 Кб в кадр, это кажется мелочью. Но при игре в 60 FPS трафик составит уже 30 Мб/сек (240 мегабит/сек) — и это если не считать репликации другим клиентам. А ведь дешёвый и быстрый интернет есть далеко не везде.

Процесс оптимизации включает в себя не только код, но и контент — он уже ложится на плечи художников. Им нужно следить за соответствием моделей и текстур бюджетам. Последние, в свою очередь, регламентирует технический арт-директор. А ещё нужно помнить о «сложности» скелетов и форме коллизий.

В коллизиях нужно использовать как можно более простые примитивы — капсулы, сферы и параллелепипеды (box'ы). Произвольные выпуклые многогранники лучше оставить до тех случаев, когда без них никак не обойтись. Если с ними не перебарщивать, то фаза обновления физики и определения пройдёт как по маслу.
Наконец, нужно помнить и о тех местах в коде, которые лучше не трогать. Для этого нужно опять обратиться к данным профилировщика. Бывает так, что какое-то место в коде работает идеально, но всё равно кажется неоптимизированным. Если тесты не выявляют в нём никаких проблем, то лучше не тратить на изменение кода неделю-другую, а оставить всё как есть. Получится экономия и личного времени, и денег работодателя. А это — самая главная оптимизация.

Если хочется сильнее углубиться в тему, можно посмотреть крайне интересный доклад Николаса Флёри на CppCon 2016 — там он рассказывает, как оптимизирована Tom Clancy's Rainbow Six Siege. Ещё посоветую выступление Эрла Хаммонда на GDC 2018 года, — из него можно узнать о реализации определения столкновений в Titanfall 2. Это уже для тех, кто любит погорячее: в ролике присутствуют математика, векторизация и деление на 0.
Понравилась статья?
Хочешь получать лучшие статьи
от XyZ раз в неделю?
Подпишись на рассылку XyZ
Нажимая на кнопку, вы соглашаетесь с условиями обработки данных