Мы используем куки, чтобы пользоваться сайтом было удобно.
Хорошо
to the top

Вебинар: Зачем тестировщику нужна безопасность? - 15.04

>
>
Game++. Часть 1.1: С++, движки и...

Game++. Часть 1.1: С++, движки и архитектуры

09 Апр 2026

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

Почему С++?

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

С одной стороны, C++ предоставляет полный контроль над системными ресурсами. Разработчики могут управлять каждым байтом памяти, оптимизировать использование кэша процессора (ред. Здесь и далее оставлен авторский вариант написания этого термина), выстраивать системы синхронизации и распараллеливания под конкретную архитектуру. Создавать код, который эффективно работает на "слабом" железе, консолях прошлого поколения или мобильных устройствах. Вручную управлять памятью и размещением данных для достижения максимальной эффективности кэша, что становится решающим фактором в борьбе за производительность.

С другой стороны, современный C++ предоставляет мощные высокоуровневые абстракции для построения сложных систем. Классы, наследование, шаблоны, контейнеры, умные указатели, RAII, лямбды — весь этот арсенал позволяет создавать масштабные архитектуры с модульной логикой, системами сущностей, событийными механизмами и при разумном использовании без злоупотребления перегрузками и метапрограммированием код остаётся выразительным и поддерживаемым.

Альтернативные высокоуровневые языки — Python, JavaScript, C# или Java — обладают несомненными преимуществами, вроде автоматического управления памятью, удобного "сахарного синтаксиса", богатой стандартной библиотеки и на порядок лучшей читабельности. Именно поэтому они популярны среди дизайнеров, технических художников и UI-программистов для решения скриптовых задач в играх. Однако эти языки имеют критические недостатки для разработки игровых движков. Зависимость от виртуальных машин или сред выполнения, непредсказуемое поведение сборщика "мусора", который может заблокировать выполнение в самый неподходящий момент, невозможность точного контроля времени выполнения операций, зависимость от внешних решений и неоптимальные схемы размещения данных в памяти — всё это делает их неприменимыми для подсистем игрового движка.

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

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

Что такое игровой движок?

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

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

Джон Кармак сказал, что лучший способ [создания игр] — написать собственный движок ("The right move is to build your own engine"), на что многие возразят: это вовсе не так просто. Но "папа" Doom'a известен не только своим вкладом в разработку игровых движков, но и тем, что довольно часто высказывался критически о развитии игровых движков в целом, и о преимуществах создания собственных технологических решений вместо использования готовых. Эта философия, которой он придерживается на протяжении всей карьеры, была связана с тем, что собственный движок даёт разработчикам полный контроль над технологией и возможность создавать решения, заточенные под конкретные нужды их проектов.

Но, знаете, каждый по-своему прав — универсального ответа нет, всё зависит от конкретной ситуации. Может быть, создание собственного движка не так уж и сложно, как кажется? Зачем вообще это делать? И что вообще значит "создать игровой движок"?

Я работал с разными игровыми движками, и сделал с нуля парочку своих, а ещё два до них ушли в мусорную корзину. На первом (третьем) было выпущено три коммерческих игры, но без особого успеха и в итоге движок был продан одной питерской студии за "бешеную толпу вечнозеленых енотов" 15 лет назад. Видел, как разработчики используют другие движки общего назначения, как жалуются на проблемы и рассказывают о своих собственных решениях. Изучал и изучаю исходный код других (открытых и не очень) движков, хотя, признаюсь, мой опыт может быть несколько предвзятым или односторонним — всегда хотел делать игры-стратегии, но предлагали почему-то преимущественно шутеры.

Первый главный вопрос — зачем?

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

...во-первых, во-вторых и в-третьих — это очень весело, обещаю

Разработка движка — это как LEGO, только для взрослых (и чаще всего в одиночку). Вы можете прикручивать рендеринг, систему частиц, "горячую" загрузку скриптов, вы будете смеяться, удивляться, иногда, возможно, даже ругаться, но скучно точно не будет.

...в-четвертых — это опыт, за который компании готовы платить хорошие деньги

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

...вы точно очень многому научитесь в процессе

Это поможет вам лучше понимать другие движки, и не только движки. У вас появится реальное инженерное чутье, понимание, что значит "тяжёлые алгоритмы", почему плохо грузить текстуры в кадре, что такое cache-friendly структуры данных, спроектированные для максимально эффективного использования кэш-памяти процессора.

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

...кстати, про мусорное ведро

Первый движок уйдёт туда, и, по опыту, второй тоже. Это нормально и это часть обучения. Есть такое понятие, как правило "трёх систем": первая — проба, вторая — попытка сделать всё "правильно", третья — та, что начинает работать. Поэтому смело пишите код, удаляйте неудачные решения и начинайте заново. Каждая "неудачная" версия — это не потерянное время, а бесценный опыт, который приближает вас к той самой третьей системе, которая действительно заработает.

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

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

...всё можно и нужно переделать под свои желания и нужды

Хотите hot-reload конфигов на JavaScript? Придётся разобраться в JavaScript и даже немного в его виртуальной машине. Надоел устаревший способ загрузки текстур? Сделаете свой runtime-атлас из файлов прямо на диске. Здесь нет ограничений — кроме ваших навыков и времени. Странные желания — это хорошо. Они формируют уникальные решения.

...полный контроль над тем, как всё работает внутри

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

...можно использовать любимый язык программирования

Нравится Rust? Используйте его. Любите C++? Welcome to pain club. Кто-то пишет движки на Go. Я видел движки на Lua, на Python, на Haskell. Даже на Erlang. На COBOL не встречал, но верю, что где-то и он есть. Главное, чтобы вы знали, зачем выбрали именно этот язык.

...можно сделать крошечный дистрибутив

Вот, например, бинарник моего хобби-проекта занимает около 2 МБ. В него упакованы скрипты, конфиги, даже часть ассетов. Остальное подтягивается с диска. Для небольшой игры это — роскошь и мечта, особенно на фоне движков, где только "Hello World" весит 200 МБ.

...можно выложить движок в открытый доступ

И это отличная идея. Во-первых, посмотреть, как его используют другие. Во-вторых, найти тех, кто его смог собрать. Таких будет мало. Примерно 0,1% от тех, кто скачал. Но именно этих людей можно звать в команду. Epic Games, кстати, именно так и делали — активные коммиттеры в Unreal становились официальными сотрудниками.

...и даже продать его

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

...вы никогда не будете разочарованы, что разработчики движка не реализовали нужную вам функцию

Потому что здесь разработчик — вы. Хотите новую систему навигации? Делайте. Надо расширить систему сохранений? Пожалуйста. И нет багрепортов, где вам ответят: "Запланировано на Q5 следующего года"

...и наконец: технические собеседования

Когда у вас есть собственный движок, разговор почти всегда переходит в интересное русло. Вы уже не будете просто отвечать на вопросы, вы можете (и со знанием дела) задавать их сами: "А как у вас устроено обновление игровых объектов? Как реализована загрузка ассетов?" Люди начинают слушать, потому что знают — перед ними не просто программист, а инженер, который создал своё решение и не боится показывать эти знания.

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

Разработчики игр всегда жалуются на движки, какими бы крутыми и удобными они ни были: то не работает, это ломалось, это новое не поддерживает вот то старое, а то старое просили добавить ещё десять лет назад, и до сих пор ничего не добавили. Конечно, у всего этого есть причины — если вы когда-нибудь работали в крупной компании, вы точно меня поймёте. Но всегда легче винить кого-то другого, а не себя. Если тебе так нужна эта функция возьми и напиши, разве нет? Нет, потому что это не твой движок. Движок чужой, а вот разочарование твоё. Это ожидаемо. Как ожидаемо что нужная "фича" в своём движке будет добавлена, когда она действительно понадобится.

В начале 2000-х термин "игровой движок" уже прочно вошёл в обиход разработчика, но большая часть проектов всё равно была, что называется, "самописной", и абсолютная часть технологий писалась с нуля: от собственного рендера до самодельной анимационной системы и "рукописной" физики. Тогда было обычным делом собирать движок под конкретную игру, заточенный под её жанр, платформу и технические ограничения. С тех пор индустрия сильно изменилась: игры стали массовым, коммерчески зрелым продуктом, а технологии разработки — более стандартизированными и модульными. Современные игровые движки — будь то Unreal Engine, Unity, CryEngine, Source, или внутренние инструменты крупных студий — стали полноценными наборами SDK, которые позволяют строить не просто один проект, а целую линейку игр, часто даже разных жанров, на одной технологической базе.

Хотя реализации движков могут сильно различаться, к середине 2000-х сформировался более-менее устойчивый набор ключевых подсистем: графика, физика, анимация, аудио, ИИ, UI, системная логика, репликация и сеть, внутриигровой скриптинг, системы данных и т. д. Более того, даже внутри этих компонентов начали появляться определённые паттерны и архитектурные подходы, которые повторяются от проекта к проекту — будь то ECS (Entity-Component System), behavior trees (BT), deferred rendering или система синхронизации с возможностью отката состояния. Но информацию по каждой из этих тем приходилось собирать по крупицам: здесь статья, там доклад с GDC или кусок кода с GitHub.

Были книги по графике, по физике, по архитектуре ИИ, но всё это — разрозненные знания. Практически не было изданий, которые бы связывали всё вместе — рассказывали не только о том "что это", но и о том "как всё это живёт под одной крышей", как проектировать архитектуру игры, учитывая технические ограничения и производственные реалии, как это сделано в GEA.

Второй главный вопрос — а надо ли?

Свой проект — это всегда тяжело и требует много времени, реально много времени. Разумеется, многое зависит от конечных целей, но не стоит ожидать, что вы за год напишете клон Unreal Engine, особенно учитывая, что этот движок создаёт уже вот лет двадцать команда высококвалифицированных инженеров, а сейчас ещё и комьюнити. В собственном движке постоянно будет чего-то не хватать. Нужно будет потратить долгие годы упорной работы, чтобы достичь хотя бы части функциональности и удобства использования, которыми обладают зрелые движки.

Вы никогда не сможете соперничать с крупными индустриальными движками вроде того же Unreal, Unity, СryEngine и даже Godot. Разве что вы — большая и богатая студия, но тогда эта книга вам, скорее всего, не особо нужна, поскольку у вас уже есть свой движок, или вы используете что-то из вышеперечисленного.

Тут надо понимать: затраты на разработку собственного движка никогда не окупятся, не будет такого момента, что со вчерашнего дня движок стал приносить прибыль. Нет, даже у топовых студий это убыточная статья. Деньги зарабатывают игры, а не движки... Из отчёта того же Unreal, затраты на разработку движка за последние только четыре года составили больше 160 млн долл., фактически эпики каждый год делают новую AAA-игру, только мы этого не видим.

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

Совет

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

Либы + cmake != движок

Вы дочитали до этого места, значит, я вас не убедил оставить это неблагодарное занятие, тогда идём дальше...

Являются ли SDL/SFML/Allegro игровыми движками, хотя на них сделано бессчётное число инди-игр? Точно нет — это библиотеки для обёртки над платформенным кодом, хотя они дают возможность сделать очень многое, что умеет базовый игровой движок. А Vulkan/DX12? Точно нет — это графические API. А FMOD/WWISE?

Тоже нет — но они позволяют реализовать двух/трёхмерную сцену, правда, звуковую. Итак, делаем вывод:

Либы + API != движок

А теперь представьте себе маленький проект про истребление пауков и строительство базы на другой планете, который объединяет Allegro, DX11 и stb (набор разных header-only решений на все случаи жизни), чтобы рисовать на экране движущиеся спрайты. Скорее всего, вы узнали в этом описании игру Factorio, именно с демо-версии на Allegro и пары спрайтов всё начиналось:

Allegro + API + stb + talent == Factorio

Будет ли это игровым движком? Тоже нет, хотя очень близко — игровой движок появляется, когда есть игра (или игры), сделанные на нём. Пока игр нет, это всего лишь набор библиотек, как-то слепленных вместе через CMake. Может быть, это просто сочетание графики, анимации и обработки пользовательского ввода? Значит, итог таков:

Factorio != движок && Factorio == игра

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

Вам действительно нужны сложные редакторы материалов и километровые шейдеры, бесшовная геометрия и глобальное освещение для неё, 3D-физика с обратной кинематикой и поддержка сетевой игры, если вы делаете 2D-платформер с пиксельной графикой?

У Unreal Engine всё это есть и в неплохом качестве, но нужно ли всё это вам? Ведь оно требует опыта, привыкания и умения настроить и пользоваться. Всё, что потребуется платформеру, — это обрабатывать ввод, рендерить спрайты, возможно даже отображать "сырые" .png на экране и воспроизводить звук. Всё это пишется за пару дней по открытым урокам на ютубе. И эти вечера будут потрачены с пользой, вместо того чтобы разбираться с контроллером персонажа в редакторе. И вот вы перешли к самому главному, ради чего это всё затевалось, — к созданию игры.

А если вы хотите создать ААА-хит с топовой графикой, физикой и всем прочим? Тогда да, вам понадобятся все эти сложнейшие системы. И пара десятков свободных лет жизни или хорошая команда толковых инженеров, чтобы ежедневно работать только над движком и всё это реализовать. Да, современные игровые движки безнадёжно далеко ушли от ретро-платформера.

Тем не менее, большинство игровых движков имеет архитектуру, которая объединяет все эти подсистемы в единое целое. Необязательно все, пока необязательно, но надо понимать, что ваш конкретный движок может содержать лишь часть из этого списка и при этом оставаться игровым движком. Не каждой игре нужны все функции сразу, иначе бы ни одна инди-игра не увидела свет. Движок может быть маленьким, конкретно под вашу игру. Но давайте будем честными — мы избалованы возможностями больших движков, переносим эти ожидания и ждём того же поведения от всех других.

Полезности

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

Когда у тебя уже что-то движется по экрану, неизбежно приходит следующий вопрос — обновление логики. Наивный подход — всё в функции обновления (Game::Update()), кто успел, тот и обновился. Потом появляется приоритет обновления, потом зависимости между системами, далее возникает вопрос: а если физика идёт отдельно, как быть с логикой, завязанной на столкновения? Даже если игра маленькая — логику лучше проектировать как независимую систему, а не как цепочку "костылей", зависимых от порядка в массиве элементов. Лучше, когда компоненты не знают друг о друге ничего — они подписываются на события, и им всё равно, кто их вызывает.

Дальше возникает вечная проблема всех: система сцен/уровней. Вроде бы очевидно: есть меню, есть игра, есть катсцена. Но когда уровень загружается внутри уровня, а потом один режим игры переключается в другой без выхода в меню, когда нужно "заморозить" сцену, показать поверх обучение, а потом вернуться обратно — вот тогда система сцен начинает рассыпаться, если она не была заложена изначально. Сцена — это не просто список объектов, это управляемый контейнер логики, данных, зависимостей и переходов.

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

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

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

Программирование

Это может показаться странным, но для реализации игровой логики классическое программирование не является обязательным. Достаточно посмотреть на системы визуального программирования (Blueprints, VisualScript, ICS), которые существуют уже десятилетия и позволяют создавать сложные механики без написания самого кода. Это особенно популярно среди дизайнеров и художников, которые хотят быстро прототипировать идеи, не погружаясь в синтаксис языков программирования.

Дизайнеры тоже пишут код, визуальное программирование — это полноценный вид программирования, просто слова class, if, for выглядят по-другому. Однако суть не в терминах, а в практических аспектах. Реализация визуального скриптинга в движке требует создания специализированных инструментов: редакторов узлов, визуального дебаггера, систем визуализации потоков данных, оптимизаторов и много чего ещё. Это значительно усложняет разработку движка, например, в Unreal Engine за Blueprints стоит огромный объём низкоуровневого кода на C++, сравнимый с рендером или движком анимаций.

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

Главное окно (Main window)

Большинство игр требуют окно для работы целиком. Даже браузерные игры здесь не исключение: окно может быть либо вся веб-страница целиком, либо конкретный элемент <canvas>, внутри которого исполняется игра. Создание окна обычно происходит через платформенно-зависимые механизмы, эти низкоуровневые интерфейсы, как правило, выглядят громоздкими и неудобными, более того, для каждой платформы (Windows, Linux, macOS, iOS, Android) придётся писать отдельный код для создания окна. Но такой код пишется один раз, и потом к нему уже никто не возвращается.

Помимо окна, обычно присутствует контекст графического API (OpenGL, Vulkan, DirectX), который тоже создаётся через страшные платформо-зависимые вызовы. Но опять же написать их придётся всего один раз, или взять готовую библиотеку, которая возьмёт это всё это на себя (SDL2, GLFW, SFML или любую из тысяч других библиотек). После создания окна, на следующем шаге требуется организовать игровой цикл — именно он будет обрабатывать события, обновлять игровую логику и перерисовывать экран.

Игровой цикл

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

int main()
{
  createWindow(); init();

  while (isRunning())
  {
    handle(inputEvent());
    updateFrame();
    drawFrame();
  }

  shutdown();
  destroyWindow();
}

Игровой цикл заставляет игру работать, без него функция main просто завершилась бы, и игра закрылась. Существует множество способов представления цикла в API движка. Он может быть полностью явным, когда пользователь движка где-то в своей функции main пишет while(engine::isRunning()). Он может быть скрыт внутри движка, например, в какой-нибудь функции engine::run(), которая, в свою очередь, вызывает некоторые функции, предоставляемые пользователем (колбэки). Что бы вы ни выбрали, вам всё равно будет нужен игровой цикл.

Пользовательский ввод

Это одна из базовых составляющих любой игры, даже если игроку придётся всего лишь "тапать" хомяка. На самом базовом уровне любой ввод обрабатывается с помощью платформенно-зависимых API вроде WinAPI, X11, Cocoa, но работать с ними неудобно: они не сложные, но очень уж многословные и требуют индивидуального кода для каждой поддерживаемой платформы. Но почти все эти API предоставляют возможность вытащить события в функцию обработки событий вроде Game::pollEvents(), с помощью которой уже можно последовательно получать события от пользователя.

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

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

while (engine.isRunning())
{
  Event event;

  while (engine.pollEvent(event))
  {
    switch (event.type)
    {
      case Event::MouseMove: 
        application.onMouseMove(event.mouseX, event.mouseY);
        break;

      case Event::KeyPressed:
        application.onKeyPressed(event.keyCode);
        break;

      case Event::WindowClose:
        engine.quit(); 
        break;
    }
  }

  engine.update(); // Логика игры
  engine.render(); // Рендеринг кадра
}

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

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

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

void onMouseMove(MouseEvent event) {
  application.onMouseMove(event.mouseX, event.mouseY);
}

void onKeyPressed(KbEvent event) {
  application.onKeyPressed(event.keyCode);
}

void onWindowClose(AppCloseEvent ev) {
  engine.quit();
}

int main()
{
  engine.subscribe(onMouseMove);
  engine.subscribe(onKeyPressed);
  engine.subscribe(onWindowClose);

  while (engine.isRunning()) {
    Event event; updateFrame();
    engine.update(); // Логика игры
    engine.render(); // Рендеринг кадра
  }
}

Графика

Окно у нас уже есть. Мы даже умеем как-то реагировать на мышку и клавиатуру. Но... показать эту реакцию мы пока не можем. Пора заняться графикой. Честно, мне 3D-графика всегда давалась с трудом, постоянно завидовал своему товарищу с ником @megai2, который с лёгкостью пишет рендеры подо что угодно, от плойки до мобилок. Большинство библиотек для создания окон, таких как SDL, уже содержат поддержку 2D-графики — можно рисовать прямо в окне, можно текстурами, можно вручную заливая пиксели любыми цветами. Примитивно, но честно.

Допустим, движок заточен под 2D-игры с тайлами и спрайтами. Тогда, скорее всего, придётся реализовать что-то вроде drawSprite(image, x, y). Возможно, с поддержкой всяких приятностей: масштабирование, вращение, наложение цвета, обесцвечивание, альфа-блендинг. Внутри это может быть реализовано вручную — прямым копированием пикселей с какими-то расчётами или без преобразований (blitting). А может — через другую библиотеку вроде Cairo, которая внутри оказывается очень даже неплохого уровня рендером, способным если не на всё, то на очень многое. Или через OpenGL/DX/Vulkan, если душа просит хардкора и скоростей.

А можно вообще не специализироваться, и просто дать пользователю движка в руки универсальные API, как это делают большинство библиотек SDL/SFML/ Allegro — пускай сам ковыряется. В моей реализации для "Фараона" есть обертка над SDL (которая умеет в нативные GAPI), но полноценного рендерера в привычном смысле нет. Для каждого движка я писал собственную систему отрисовки заново. С одной стороны, это было непросто, с другой, я вдоволь поэкспериментировал с графикой.

Как бы ни была построена система отрисовки, рано или поздно, понадобится отображать текст. Для интерфейса, отладочной информации и прочего. Можно, конечно, отрисовывать буквы вручную пикселами, но лучше взять нормальную библиотеку для работы со шрифтами, например, FreeType. Для любителей приключений и юникода — HarfBuzz.

Ресурсы

Однажды ты заканчиваешь очередной уровень, запускаешь билд... и он запускается минуту. Потом две. Потом ты замечаешь, что сцена "весит" 2 ГБ, потому что каждое дерево — объект с уникальной копией текстуры и модели. И вот тогда приходит осознание: пора серьёзно заняться системой работы с ресурсами.

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

Поэтому пакуется всё: текстуры, модели, звуки, уровни, скрипты — в архивы. Собственный формат контейнера с индексом и сжатием — распространённое решение, но кто-то берёт готовые вроде Oodle, пакует в ZIP или делает виртуальную файловую систему, заточенную под особенности движка. Большие коммерческие движки, кстати, обычно идут именно по этому пути. Архивы удобно грузить, удобно кэшировать, и никто не лазит туда руками... кроме тех, кто очень хочет.

Можно, например, скомпилировать все ресурсы в один большой бинарный blob и "запечь" внутрь исполняемого файла, так делали и делают многие игры. Так делали XBox и Sony поначалу. Преимущества? Всё рядом, всё быстро — никаких "подгрузок", никаких путей к файлам — просто достаёшь байтовый массив и работаешь с ним, как с ресурсом. Иногда это спасало в разных проектах, по-настоящему спасало, и позволяло пройти сертификацию. А что бинарник 700 МБ, так это всё "кот виноват" и "джуны погромисты" понаписали шаблонов.

Но, конечно, есть и минусы. Не получится изменить ни один ассет, не перекомпилировав всю игру. Даже если просто хочется поменять цвет пиксела в текстуре — жди билд. А ещё это делает практически невозможным изменение поведения игры без доступа к её исходникам. Как игроки будут вставлять свои кастомные шляпы, если весь контент "запаян" в .exe-файл?

Можно хранить ресурсы как обычные файлы в папке assets/ рядом с исполняемым файлом. Тогда движок просто загружает нужный файл при запуске уровня, если мы заранее не озаботились перфомансом. Такой метод идеален для разработки: быстро, прозрачно, можно в любой момент зайти в папку, заменить текстуру и увидеть изменения в игре. А ещё это настоящий подарок для моддеров: бери, меняй, добавляй — всё открыто.

Как бы в итоге ни хранились ассеты, их нужно ещё как-то превратить из "сырых" байтов в осмысленные данные. Тут понадобятся форматные библиотеки, например stb_image, чтобы превратить PNG в массив пикселей. Или разные JSON-библиотеки — чтобы вытащить информацию о сущностях из .json-подобных конфигурационных файлов, если вы ещё не разочаровались в JSON как формате для таких файлов. В какой-то момент в команде приходит понимание, что чисто декларативный формат конфигов не покрывает все кейсы, и тогда в движок приходят VM (Virtual Machine) для JS/Lua/Squirrel/AngelScript, которые вроде как скриптовые языки, но используются для парсинга конфигов.

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

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

Но ресурсы — это не только про разработку. Это ещё и про релиз. Поэтому система должна генерировать билды под разные платформы. Windows, Linux, консоли, Steam Deck — у каждой свои ограничения и требования к выравниванию данных, форматам, правилам упаковки. Где-то недопустимы файлы MP3, где-то не поддерживается определённый формат текстур.

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

Вывод звука

Это, если вы не знали, ад на платформенно-зависимых API. Уродливые, неинтуитивные, капризные, XBox вообще любит плеваться исключениями, если запишешь лишнее в буфер, а... впрочем, если положишь меньше, чем выделил памяти под буфер, тоже будет плеваться. Я не настоящий "сварщик", т. е. не аудио-программист, но и малой доли приключений хватило, чтобы понять: хочешь просто воспроизвести звук — приготовься страдать.

Или... можно взять библиотеку, которая спрячет весь этот кошмар за нормальным интерфейсом. Например, OpenAL или SDL, последнее время я использую вторую, потому что, она очень лояльна к ошибкам, даже если делаешь что-то совсем не так. Эти библиотеки обычно ожидают от движка аудио-колбэк — функцию, которую будет вызывать система (обычно из отдельного потока) десятки или даже сотни раз в секунду.

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

Чтобы всё это заработало как надо — нужен микшер. Не человек, конечно (хотя иногда и это бы не помешало), а аудиомикшер — библиотека, которая будет сводить многочисленные звуковые потоки в один, аккуратно контролировать их громкость, фильтровать резкие пики, добавлять эффекты и не давать звуку превратиться в "кашу", если всё проигрывается одновременно. Особенно если у тебя в игре внезапно начинается бой, где есть 15 звуков разных юнитов, фанфары и тревожная музыка.

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

Физика

Если честно... большинству игр физика вообще не нужна. Вот прямо настоящая физика — с силами, импульсами, трением и со всем этим академическим багажом. Большинству нужно нечто попроще: анимации, логика, скрипты — то, что создаёт впечатление физики, не имея к ней почти никакого отношения. Вот, например, Civilization VI — одна из самых сложных стратегий современности. Но физика там не нужна. Совсем. Или разные градостроительные симуляторы (например, SimCity) — банально, но дома не падают, дороги не трескаются, и никто не просчитывает вектор падения водонапорной башни. Просто потому, что оно не нужно. Зачем усложнять? Но и Сiv, и Cities Skylines зачем-то тащат зависимости на PhysX, а возможно не просто тащат, но и как-то его используют.

Платформер или 2D-RPG сверху — вся "физика" обычно ограничивается перемещением персонажа и простыми столкновениями со стенами, врагами и ящиками. Это пишется за пару вечеров, если ты, конечно, не залипаешь на каждую строчку, как это делал я, пытаясь разобраться. Главное — всё будет под контролем: если требуется, чтобы герой скользил как по льду, — пожалуйста. Надо, чтобы враги отскакивали при столкновениях нелепо, как резиновые утки — вперёд.

А если слишком много столкновений и всё начинает тормозить — можно оптимизировать. Пространственное хеширование, квадродеревья, и ограничение по дальности от ГГ. Но... если вы всё же упрямы и точно знаете, что потребуется реальная физика — берите готовую библиотеку: Box2D для 2D или Bullet для 3D. Не изобретайте велосипед, их и так будет предостаточно в самописном движке.

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

Автор — Сергей Кушниренко

Разработчик с более чем двадцатилетним опытом программирования и создания игр. Выпускник Национального исследовательского университета ИТМО. Начинал карьеру с разработки программного обеспечения для военно-морских тренажеров, навигационных систем и сетевых решений. Последние пятнадцать лет специализируется на разработке игр: в Electronic Arts занимался оптимизацией игр The Sims и SimCity BuildIt, в Gaijin Entertainment руководил переносом игр на платформы Nintendo Switch и Apple TV. Активно участвует в проектах с открытым исходным кодом, включая библиотеку ImSpinner и проект восстановления игры Pharaoh (1999).

Подписаться на рассылку
Хотите раз в месяц получать от нас подборку вышедших в этот период самых интересных статей и новостей? Подписывайтесь!
Популярные статьи по теме

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

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