Мы используем куки, чтобы пользоваться сайтом было удобно.
Хорошо
to the top
>
>
>
Каков C++ в gamedev'e?

Каков C++ в gamedev'e?

12 Янв 2026
Автор:

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

Мы опубликовали и перевели эту статью с разрешения правообладателя. Автор статьи — Сергей Кушниренко.

Хотел написать продолжение к статье Что почитать игровому программисту? про использование С++ в игровых движках, но размышления свернули куда-то не туда.

Заворожённо смотрю, как и какими темпами идёт развитие языка в последние годы, и понимаю, что получить и особенно применить возможности С++20/3 в разработке игр и движков получится хорошо, если с опозданием лет эдак в пять, — как раз на следующее поколение консолей, — если вообще получится. Сейчас плюсы в игрострое зависли где-то между 14 и 17 стандартом: Sony только-только выкатила свою версию компилятора с полной поддержкой 17 стандарта, а учитывая реактивность игровых студий в изменении кор пайплайнов, что-то новое начнут только в новых проектах. Менять коня, т.е. компилятор посреди разработки игры равносильно стрельбе не только по ногам себе, но и соседям программистам: работает — не чини.

Если смена компилятора и стандарта не даст гарантированного прироста скорости работы больше 5%, то бюджет и людей я не одобрю. (с)

Знакомство с кодовой базой больших движков даёт понимание уровня и объёмов кода в продакшене и в тулзах, и ситуация вырисовывается такая, что эти объёмы стали в индустрии, что называется, "too big to fall". Т.е. написать что-то новое уровня движков вроде Unity/Unreal/Dagor на другом языке, будь он хоть в тысячу раз безопаснее и в десять раз быстрее, не получится, но попытки, конечно, делаются. И чем дальше продолжается поддержка существующих проектов на плюсах, тем меньше возможности выбора остаётся.

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

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

  • Вендоры платформ (Sony, Microsoft, Nintendo) дают API на С/C++. Объём кодовой базы их ОС и SDK намного больше, чем у игровых движков, использовать что-то альтернативное просто не получится — затраты на переделку похоронят даже Нинку с её безразмерными бюджетами.
  • Портирование игр между платформами возможно только на языках C/C++, причину я написал выше, никакого другого общего языка между платформами нет.
  • Компиляторы для плюсов оптимизировались десятилетиями, и чтобы получить соответствующую производительность на другом языке, им тоже придётся пройти этот путь, — пусть не десятилетия, потому что база уже есть, но годы точно. Писать быстрый относительно высокоуровневый платформенный код на чём-то отличном от C/C++ сейчас просто не получится.
  • Legacy — наследованный код, от него никуда не деться, его надо сопровождать и чинить, фиксить баги. А ещё надо понимать, что этот код делает, и иногда проще написать некоторую часть с нуля, чем дорабатывать то, что имеется, но не всегда на это есть люди и время.
  • Language pain — англоязычный термин (не нашёл, к сожалению, аналога в русском сообществе) в среде разработчиков игр, который точно описывает, почему индустрия не слезет с плюсов ближайшие лет десять точно. Vendor lock обоснован не только применением железа конкретного производителя, но и парадигмой выбранного языка разработки. И у каждого производителя она своя. Отдавать даже 1% рынка никто не будет, и свой язык программирования только увеличивает присутствие вендора. Учитывая, что потеря одного процента — это упущенные десятки млрд убитых енотов, затраты на его разработку даже в 10% от этой прибыли с лихвой окупаются.
  • Шейдеры. Выделю эти языки отдельно, хотя они очень близки по своей сути к C, это уже часть платформы, без которой игру вы не сделаете. И если в плане общего языка разработки С++ является как бы "философским камнем", способным переплавлять общие идеи в работающий код под любую платформу, то для низкоуровневого высокопроизводительного кода такого общего компонента нет. И, скорее всего, никогда не будет. Ну просто не получится отрисовать ничего на экране. Некоторое время это место занимал OpenGL, но его общими усилиями искоренили почти везде.

Но, что самое интересное, сам основной язык разработки игровых движков стал неоднородным — его можно разделить на низкий, средний и высокоуровневый С++, и каждый имеет свои особенности.

Hardware/Baremetal/Hardcore C++

Используется для числодробилок и работе с большими объёмами вычислений.

void frustum_for_box_occluder(
                         const TMatrix &to_box_space,
                         const Point3 box_corners[8],
                         const Point3 &eye,
                         plane3f out_frustum_planes[BOX_OCCLUDER_PLANES_MAX],
                         int *out_planes_count)
{
  Point3 box_eye = to_box_space * eye;

  G_ASSERT(to_box_space.det() > 0);

  unsigned index = unit_segment_classify(box_eye.x) * 1
                 + unit_segment_classify(box_eye.y) * 3
                 + unit_segment_classify(box_eye.z) * 9;
  G_ASSERT(index < 27);

  {
    // Rare case near_box, when the point is located very close to the cube.
    // Then the plane is chosen based on the closest face to the eye.
    bool near_box = likely_inside_m0505(box_eye.x)
                 && likely_inside_m0505(box_eye.y)
                 && likely_inside_m0505(box_eye.z);

    if (near_box)
    {
      float abs_x = fabsf(box_eye.x), 
            abs_y = fabsf(box_eye.y), 
            abs_z = fabsf(box_eye.z);

      int i0 = abs_x < abs_y, i1 = abs_y < abs_z, i2 = abs_z < abs_x;

      float max_coord = box_eye[gComparisonsToMaxCoordIndex[i0][i1][i2]];
      const BoxPointClassificationForOcclusion &cl =
        gBoxPointClassificationForOcclusion[
              gNearCubeFrontPlaneForOcclusion[i0][i1][i2][max_coord < 0]];

      *out_planes_count = 1;
      Plane3 p(box_corners[cl.mFrontPlane[0]], 
               box_corners[cl.mFrontPlane[1]], 
               box_corners[cl.mFrontPlane[2]]);

      out_frustum_planes[0] = v_ldu(&p.n.x);
      return;
    }
  }

  {
    // Common case. Planes are constructed based on index, 
       obtained from unit_segment_classify for x,y,z.
    const BoxPointClassificationForOcclusion &cl =
          gBoxPointClassificationForOcclusion[index];

    *out_planes_count = cl.mSidePlanesCount + 1;
    Plane3 p(box_corners[cl.mFrontPlane[0]],
             box_corners[cl.mFrontPlane[1]],
             box_corners[cl.mFrontPlane[2]]);
    out_frustum_planes[0] = v_ldu(&p.n.x);
    for (int i = 0; i < cl.mSidePlanesCount; ++i)
    {
      Plane3 p_(Plane3(eye, box_corners[cl.mSidePlanes[i][0]],
                            box_corners[cl.mSidePlanes[i][1]]));
      out_frustum_planes[i + 1] = v_ldu(&p_.n.x);
    }
  }
}

Хорошим примером "такого С++" будут: подсистемы симуляции физики, рендер сцены, коллизии, системы балансирования нагрузки (Tasks/Workers) при использовании в многоядерных системах, анимация персонажей, обсчёт воды и частиц (https://github.com/NVIDIA-Omniverse/PhysX).

Или там, где нужно работать с пониманием особенностей платформы (железа) и оперировать такими понятиями как cache locality, branch prediction, упаковка и порядок данных в структурах. Если вы загляните в код этих систем, то выглядеть он будет как написанный на чистом С, с минимальными возможностями плюсов вроде перегрузки функций или наследования. Т.е. тут даже скорости обычных плюсов не хватает, и приходится идти на существенное ограничение возможностей, чтобы выжать ещё пару-тройку процентов производительности.

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

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

В одном из докладов на GDC по Uncharted, разработчики привели замеры, что 80% времени игра проводит в таком коде, и только 20% — в общем. Этот низкоуровневый код быстрее обычного в десятки если не сотни раз, и если скорости мешает архитектура и какие-то правила написания совершенного кода, то и архитектура, и правила идут лесом... Перефразирую выражение про капиталиста и 300% прибыли: рендер-программист ради 3% прироста спокойно сломает вам половину редактора, и это будут ваши проблемы, а не его.

Такой низкоуровневый код на недо-C++ неидеален, неудобен, пестрит всеми возможными антипаттернами, ходит по грани UB и насыщен персональными трюками отдельных людей, но он быстрый, и этого достаточно, чтобы его брали в прод. Могут ли другие языки, которые стремятся стать "лучшим С", т.е. сгенерировать код, что будет работать быстрее очень и очень большой вопрос. Как раз из-за красивостей, синтаксического сахара, проверок и ограничений такой код теряет до половины скорости работы. Хотите стрелять себе в ногу со скоростью автомата — да пожалуйста. А, и забыл ещё одно: скорее всего это код скомпилируется и заработает на другой платформе.

В одном из движков "подтекал" стриминг текстур — несильно, хватало на 2-3 часа игры. Пофиксить его не представлялось возможным, потому что этот код был сильно легаси, и попытки его починять приводили к статтерам во время игры. В итоге починили вот так: когда игра подходила к границе OOM, просто менялась дата создания файла сохранения на 2039 год, отчего Steam воспринимал это как ошибку и показывал системное сообщение. Потом починили нормально, пользователи, конечно, были недовольны, но списывали это на проблемы сети, Steam, компа, но не игры.

Ещё одна причина применения "такого C++" в том, что он позволяет хорошо контролировать производительность получаемого кода там, где это требуется, потому что можно примерно представить во что все эти конструкции скомпилятся в асме.

Middleware/Common С++/Templates

Далее, поднимаясь по слоям архитектуры, мы попадаем на уровень "обычного" С++. Этот код написан с применением классических "технологий" и алгоритмов, которые изобрели за время развития языка. Здесь располагается 80% кода, который задействован в софте. Сотни библиотек на разных языках, которые в том или ином виде предоставляют доступ через "С-интерфейс" к своим возможностям. Различные связки с кор языком ОС, например Java с JNI, Objective C++, виртуальные машины скриптовых языков.

Здесь же язык раскрывается как высокоуровневое средство проектирования, — заметьте, не язык написания кода, а именно средство для описания архитектуры приложения (OOD, DOD, DDD). Он позволяет не только выжать все соки из железа, наплевав на все правила хорошего кода, но и показать этот самый хороший код, устойчивый к ошибкам, утечкам, bound check access и защитой от джуна. К сожалению, во многих игровых движках здесь ещё остались ошмётки "ревущих" нулевых, когда плюсы вовсю использовались для написания игровой логики. Вы можете заметить это, например, по доступным исходникам анриала или дагора, где кор логика, связанная с игроком, частично присутствует на самом нижнем уровне объектов.

Ну и, конечно, язык предоставляет доступ к API библиотек. А при использовании некоторых хаков вроде privablic access, то и вообще к большей части скрытой от конечного пользователя функциональности. Но если вы думаете, что вот он — настоящий С++, то нет, здесь все ещё живут призраки "plain C": то там, то тут можно увидеть специально упрощённый функционал, чтобы этим уровнем могло пользоваться как можно больше людей.

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

Мы с радостью пожертвуем 10% продуктивности ради того, чтобы получить 10% дополнительной производительности".

Tim Sweeney (c)

Рисунок N1 — Если кто забыл, как он выглядит.

Выливается это в то, что в движок приходят виртуальные машины языков второго и третьего уровня, которые позволяют с одной стороны писать скоростные алгоритмы на уровне движка, а с другой — оградить дизайнеров от C++ в пользу чего-то более медленного, удобного и понятного. Сначала это была мода на затаскивание языка скриптов Lua/Js/Squirrel/"Напишите свое", чуть позже пришло время визуального программирования. Скрипты и визуал скрипты (blueprints) — это тоже всё не изобретение игростроя. Они пришли из мира робототехники, где цена ошибки значительно выше, и сама ошибка может привести не просто к вылету на рабочий стол, а к реальным повреждениям оборудования. Минусы такого подхода: то, что можно написать в 10 строках кода, займет 1000 строк за счёт написания обвязки, проверок, тулов и т.д.

Про снижение производительности и говорить не приходится, даже самая продвинутая Lua VM, как бы ни заявляли её разработчики, просаживает перф хорошо если только в два раза. Возможно, на каких-то синтетических тестах падение производительности составляет десять и меньше процентов, но в реальной игре код из этого теста выполнятся 0.1% времени работы. Это не так критично, как кажется на первый взгляд, потому что всё это компенсируется ростом скорости памяти и процессоров, и видеокарт. Но падение производительности оценивается не только в терафлопсах, сам язык Lua намного проще, чем плюсы. И люди — программисты и дизайнеры — также начинают думать и писать в парадигме упрощённого языка, просто потому что писать сложнее не надо, да и не всегда получается.

По моему опыту, код, переписанный со скриптовых языков обратно на C++, будет быстрее в 5+ раз. Обычно так и происходит, когда по результатам профилирования игры определяются медленные участки. Другие языки скриптов не сильно далеко ушли от Lua, внимание на котором в разработке было акцентировано как минимум лет десять, и за это время его очень прилично ускорили. C момента появления языка в далеком 1993 году производительность самой виртуальной машины безотносительно производительности железа выросла почти в десять раз. На картинке ниже приведены бенчмарки реализаций алгоритмов между разными версиями виртуальных машин языка Lua, красным для примера дано эталонное время работы алгоритма на языке C.

Необходимость создания биндингов из плюсов в скриптовый язык является ещё одним бутылочным горлышком при использовании связок с++ <-> скрипты, зачастую из-за необходимости копирования данных между уровнями представления. Потеря производительности на всех этапах делается, чтобы дать возможность программировать всем — от художника до дизайнера ИИ и системных механик — позволить ошибаться и писать полную дичь, не скрашив при этом редактор неосторожным движением шаловливых ручек.

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

Ещё дальше в этом плане шагнули Unity и Unreal, предоставив возможности визуального скриптования и редактирования объектов и логики прямо во время симуляции, что ещё больше снижает требования к базовым знаниям разработки в общем и к программированию в частности. Так, наверное, и должны разрабатываться игры, когда ты просто меняешь состояние игры прямо во время игры. Как и в случае с переходом от нативного кода к скриптам, так и от скриптов к визуальному программированию — это ещё больше замедляет общий код игры, но даёт ещё больше защиты от ошибок для команды. Теперь уже скрипты и ВМ выступают в роли фреймворка нижнего уровня, а на уровне визуальных скриптов вы на 95% защищены от возможности скрашить игру, при этом давая доступ ко всему функционалу движка — от шейдеров до анимаций и поведения NPC.

Однако это не гарантирует, что разработка будет легче. Я бы сказал, наоборот —разработка становится сложнее в общем, но эта сложность размазана между сотнями и тысячами элементов игры. Ну, конечно, можно факапить похуже и намного быстрее, чем в коде. Эта жесть из реального проекта, назовем такую сложность WTF/s(1). Честно, такое никто не будет ревьюить, — апрувнут не глядя, молитесь только, чтобы этот ГД довел своего монстра до релиза.

Рисунок N2 — WTF/s (n)

Рисунок N3 — WTF/s (n^2)

Рисунок N4 — Нинада так! WTF/s (80lvl)

Meta/Highlevel C++

Подбираемся к самой мякотке. Кроме обычного плюсового кода есть ещё небольшие части игрового движка, которые требуют использования самых навороченных языковых средств. Это RTTI, reflection, compile-time расчёты и средства кодогенерации, когда код игры вырастает из набора конфигов по заданным наборам правил.

RTTI по понятным причинам в 99% случаях выключают, но сама необходимость каста к нужному типу никуда не делась, поэтому почти всегда пишут свою погремушку.

Из-за отсутствия рефлексии в самом языке, её "изобретает" каждая вторая студия — у кого как получится. Готовой и проверенной схемы и технологии рефлексии нет — каждый фреймворк предлагает свои методы разметки кода, сериализации и биндингов.

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

Из известных "хороших" кодогенераторов могу отметить следующие:

  • Cхема данных на отдельном переносимом языке (flatbuffers).
  • Отдельный язык генерации данных и кода для работы с ними (Racket от Naughty Dogs) https://www.gdcvault.com/play/211/Adventures-in-Data-Compilation-and https://www.youtube.com/watch?v=oSmqbnhHp1c.
  • CppHeaderParser — python-библиотека из одного файла, которая умеет читать хедеры. Она очень простая, не ходит по #include, пропускает макросы, работает очень шустро и позволяет быстро встроить в пайплайн.
  • RTTR позволяет создавать и изменять типы, классы, методы и свойства объектов на языке C++ во время выполнения программы. Это может быть полезно для различных целей, таких как сериализация, создание скриптов, генерация пользовательских интерфейсов и многое другое.

Мысли опосля...

Возвращаясь в реальный мир после просмотра примеров из новых стандартов языка на YouTube или СppCon, когда лямбда, обернутая в memfunction, скользит по корутинам, и в очередной раз после бессонной ночи глядя в отладчик и исписанный блокнот с записями, обнаруживаю какую-нибудь странную строчку кода, из-за которой непонятно, как вообще это всё работало, в сотый раз задумываюсь над тем, что если такое написали в С++11, то как же изощрённо это могут сделать по-новому. И как долго потом будут эту багу искать. Игры все-таки пишут с какой-то целью, и просто переписывать код туда-сюда ради рефакторинга — плохая затея. Может и хорошо, что мы живём в своем маленьком С++ мирке, охраняемом святой троицей Sony, Microsoft и Nintendo, которые не пускают сюда драконов из комитета?

Хотите узнать больше?

Команда PVS-Studio ценит сообщество game developer-ов, поэтому не упускает возможности побольше рассказать о том, как можно улучшить рабочие процессы с помощью статического анализатора кода на реальных примерах. Предлагаем небольшую подборку статей:

Приглашаем также ознакомиться с вебинарами по оптимизации игр, где Сергей Кушниренко принимал участие в качестве эксперта:

Последние статьи:

Опрос:

book gost

Дарим
электронную книгу
за подписку!

Популярные статьи по теме


Комментарии (0)

Следующие комментарии next comments
close comment form