Нечистая игра
Задумывались ли вы, насколько внутренне «хаотичным» бывает код успешных видеоигр? Тем не менее, это не мешает им расходиться миллионными тиражами, создаваться одиночками на простеньких браузерных движках, собираться на Lua поверх библиотек для геймджемов или вовсе лепиться подростками в бесплатных версиях Unity. С другой стороны, наверняка каждый из вас знает образцовые кастомные движки с ECS, job-системами, собственной графикой и продвинутой рефлексией — только вот игра, написанная на таком технологическом шедевре, пылится в вашем бэклоге годами, и вы вряд ли провели в ней хотя бы пару часов.
Примерно полгода назад старые знакомые попросили меня помочь с доработкой и релизом их игры в Steam. До этого команда занималась нефтяным бизнесом, но, скопив солидный капитал, они решили оставить яркий след в индустрии развлечений. Честно говоря, я уже отвык от столь наивного подхода к коду и примитивных инженерных решений. Это стало забавным культурным шоком, быстро вернувшим меня с небес корпоративных стандартов на грешную землю. Сразу оговорюсь: простая архитектура вовсе не индульгенция для откровенно слабого кода. Это скорее осознанный выбор того, где именно вы можете позволить себе сложность. Ваш когнитивный ресурс строго ограничен размером «оперативной памяти» в голове, поэтому тратить его стоит исключительно на те элементы, которые действительно важны для игрока.
Начнем с того, что избыточное усложнение редко проистекает от лени или глупости. Как правило, это результат усердной и интеллектуальной работы. Человек изучает толковые статьи, смотрит глубокие доклады с конференций, набирается опыта в сильных студиях и искренне старается делать всё «по науке». Вот только эта «правильная» методология рождается в контексте масштабных или высокоспециализированных проектов, где иные подходы уже не работают из-за жесткой культуры производительности или, если хотите, «проклятия масштаба».
Все эти лекции про data-oriented design, эффективное выравнивание структур под кэш-линии процессора (где счет идет буквально на 64 байта), виртуальные вызовы и косвенную адресацию, разрушающую предсказатель переходов, — чистая правда. Но озвучивают её специалисты, у которых на мониторе единовременно функционируют сто тысяч сущностей. Я говорю вполне серьезно: в нашем текущем проекте мы уже умудрились исчерпать диапазон uint16 для идентификаторов ECS, при этом удерживая жесткий бюджет в 16 миллисекунд на весь кадр, включая рендеринг, физику и аудио для всей этой безумной массы объектов (хотя, конечно, далеко не каждого нужно обновлять каждый кадр). У моих же знакомых в обычном топ-даун шутере бегало всего около двух сотен врагов, но игра всё равно периодически подлагивала.
Бюджет кадра при 60 fps: 16.6 мс
200 сущностей, наивный Update()
с виртуальным вызовом: ~200 × 50 нс = 0.01 мс
плюс промахи кеша на каждой: ~200 × 200 нс = 0.04 мс
Итого игровая логика: ~0.05 мс из 16.6 мс
Тот же кадр, реальные расходы:
рендер и отправка команд: 4–8 мс
физика: 1–3 мс
GC-пауза в неудачный кадр: 2–15 мс ← вот здесь боль
Иными словами, вы можете ускорить свою игровую логику хоть в двадцать раз, но не заметите прироста ни единого кадра в секунду, если основные задержки кроются в частых аллокациях памяти внутри горячих циклов. И знаете, как они решили проблему? Просто снизили целевую частоту кадров до 30 в секунду — о чудо, микрофризы исчезли, потому что движок перестал захлебываться от непосильных задач. Хотя, признаюсь, я немного лукавлю: до показа игры издателю оставалось ровно два дня, так что копаться в кишках кода было уже рискованно — можно было попросту сорвать презентацию.
Карго-культ интерфейсов

Крупные игровые студии питают особую страсть к интерфейсам. Какой-нибудь условный IAudioSystem существует вовсе не ради эстетики (хотя выглядит красиво). Просто у их движка имеется с десяток целевых платформ и три разных звуковых бэкенда, которые дописываются параллельно с самой игрой. Интерфейс в таком случае — лишь способ избавить разработчиков от необходимости постоянно дергать на код-ревью специалистов из смежных отделов.
В камерной же команде из трех человек, ориентированной строго на одну платформу, подобный интерфейс будет иметь ровно одну-единственную реализацию плюс унылую заглушку (мок), которую никто даже не запускает, поскольку полноценных тестов в проекте нет (да-юн, классическая боль небольших коллективов). Но вы всё равно платите полную стоимость сложной абстракции, позаимствованную из мира огромных движков, не получая взамен ни единого реального преимущества. Стоило ли оно того? Зато теперь мы с восторгом слушаем на конференциях умные доклады от инженеров звуковых подсистем, мечтающих продать нам свой очередной IAudioSystem.
Что видит читатель доклада с GDC:
IWeaponSystem ──► WeaponSystemPS5
├──► WeaponSystemXbox
└──► WeaponSystemPC
(три платформы, три команды, общий контракт)
Что получается в проекте на троих:
IWeaponSystem ──► WeaponSystem
└──► WeaponSystemMock // не используется с 2023 года
(одна реализация, два лишних файла, минус переход по коду)
Отдельный вид творческого помешательства — проектировать масштабные системы с расчетом на десять тысяч объектов еще до того, как проект вообще обрел хоть какие-то очертания. А если эти объекты не появятся вовсе? Что ж, вы получите свою порцию дофамина от написания изящного кода, который в итоге никому не пригодится. Писать пулы объектов, кастомные чанки памяти и SoA-структуры чертовски приятно: задача четко формализована, и в конце всегда есть осязаемый результат. А вот разбираться, почему в вашу игру откровенно скучно играть — процесс мучительный, и метрик там никаких нет. Вот программисты с чистой совестью и упоением оптимизируют проект, в который еще никто толком не играл. Мой вам совет: не давайте программистам единолично определять геймдизайн — пусть этим занимаются специалисты по уровням, монстрам и механикам.
А что сделали те, у кого получилось

Лука Галанте, бывший разработчик игровых автоматов, в начале 2021 года бросил денежную, но унылую работу и на легком HTML5-фреймворке Phaser (вдохновившись мобильной Magic Survival, если мне не изменяет память) создал Vampire Survivors. Выход в ранний доступ в декабре того же года мгновенно превратил игру в один из главных хитов десятилетия, потеснивший многие высокобюджетные блокбастеры и заложивший фундамент нового жанра. И лишь в 2023 году проект переписали на Unity ради производительности и поддержки консолей. Сначала игра нашла свою аудиторию на простейшем веб-стеке, и лишь потом обросла «взрослым» фундаментом.
Соло-разработчик LocalThunk потратил два с половиной года на движке Löve (изначально ради красивой строчки в резюме при поиске работы дизайнером), создавая карточную игру Balatro, тираж которой к январю 2025 года перевалил за пять миллионов копий. Löve представляет собой связку Lua и тонкой обертки над SDL — там нет ни визуального редактора, ни сцен, ни инспектора объектов. Моддеры, изучившие файлы игры, обнаружили внутри типичный Lua с огромным количеством легаси-конструкций, функциями длиной в три тысячи строк и практически полным отсутствием комментариев.
Stardew Valley, Undertale, Minecraft, Unturned, Phasmophobia (XNA, GameMaker, LWJGL, Unity, Unity) — все эти феноменальные хиты создавались силами одного-двух человек. Никаких уникальных движков собственной разработки, никакой архитектурной экзотики и сплошной «душный» код. Зато авторы направили всю энергию на наполнение игры контентом, а не на возведение сложной инфраструктуры.
И чтобы не складывалось иллюзий, будто выбор простых технологий ничего не стоит: когда те же авторы Vampire Survivors мигрировали с Phaser на Unity, этот процесс занял целый год работы команды, разросшейся к тому моменту до десяти человек. Простой стек экономит ресурсы на старте, но проблемы не исчезают бесследно — за всё приходится платить позже, когда у вас уже появятся и деньги, и люди. Считаю ли я это честной сделкой? Безусловно. Но это именно сделка, а не манна небесная.
Берите скучный стек

Выбор готового коммерческого движка часто ошибочно воспринимают как распишущуюся в творческой импотенции капитуляцию — хотя это мнение до сих пор активно транслируют на профильных конференциях. Впрочем, когда я сам запускаю очередную поделку на Unreal и с трудом отличаю её от десятка таких же релизов прошлой недели, меня тоже начинают терзать смутные сомнения.
Тем не менее, прагматичный выбор скучного проверенного стека — это в первую очередь грамотное управление рисками. Такие платформы, как Unity, Unreal или Godot, давно отладили подгрузку ресурсов, обработку аудио, интеграцию геймпадов и сборку билдов. Что еще важнее для разработчиков, целящихся на консоли — там уже настроены нормальные платформенные бэкенды, избавляющие от головной боли при сертификации. У той же Microsoft есть целый департамент, специализирующийся исключительно на проверке игр на Unity.
И еще один нюанс: выбирая чужой движок, вы автоматически разделяете риски бизнес-решений его создателей. Если топ-менеджменту крупной IT-компании вдруг перестанет хватать денег на новую яхту, они без колебаний выкатят очередное гениальное нововведение — например, плату за каждую установку приложения через Play Store. Потом, конечно, поймут абсурдность ситуации и пойдут на попятную, но уровень доверия будет подорван окончательно, и многие скажут: «Спасибо, пожалуй, воздержимся». Преимущество скучного стека заключается в колоссальной экономии времени разработки. Но будьте готовы к тому, что в один прекрасный день совет директоров чужой корпорации решит изменить правила игры, и вам придется с этим смириться.
Монолит лучше связки систем
Этот тезиск обычно вызывает самую бурю эмоций, когда я рекомендую командам отказаться от разветвленных многоуровневых подсистем. В игровой индустрии массивный, прямолинейный класс, выполняющий сразу множество задач, часто оказывается куда эффективнее сложной композиции из десятка микросущностей. И дело вовсе не в качестве написания кода — просто геймдизайн меняется столь стремительно, что возможность быстро внести правку начинает цениться выше академической чистоты и легкости чтения. Когда проект завоюет популярность и обзаведется преданной армией фанатов, вы всегда успеете заняться рефакторингом и философскими поисками идеальной архитектуры. А если полученный вариант стыдно демонстрировать на код-ревью… ну что ж, за стыд еще никто не умирал, зато нужная игровая механика появится в игре буквально через полчаса.
class GameManager {
Player player;
WaveSpawner spawner;
UpgradeList upgrades;
float runTime;
void Update(float dt) {
runTime += dt;
spawner.SpawnIfNeeded(runTime);
player.Update(dt);
enemies.UpdateAll(dt);
CheckCollisions();
if (player.xp >= nextLevelXp) OpenUpgradeScreen();
hud.Refresh(player, runTime);
}
}
А вот как выглядел бы тот же функционал, сделанный «по канонам» (EventBus + ServiceLocator + IUpgradeProvider + TimeScaleService): каждая мелкая правка превращалась бы в многочасовое расследование. Пришлось бы выяснять, кто именно подписан на событие повышения уровня игрока, в какой последовательности отрабатывают подписчики, почему TimeScaleService уже успел повлиять на физику снарядов, и городить четвертый по счету вспомогательный сервис, чтобы эту вакханалию исправить.
Любая абстракция — это ставка на то, что вы способны безошибочно предсказать вектор изменений проекта на несколько месяцев или даже лет вперед. В системном софте или графических движках траектория развития очевидна, но в игровой логике угадать вектор практически нереально — особенно для небольшой команды в поиске своей ниши, где геймдизайнер меняет требования едва ли не после каждого плейтеста. В таких условиях красивая и гибкая архитектура становится непреодолимым барьером, который сначала приходится разбирать, а затем собирать заново. Разумеется, монолитный менеджер легко может выродиться в раздутый глобальный синглтон, вызываемый из сотен разных мест. И тогда начнется настоящий кошмар — но с этой проблемой вы будете разбираться уже после релиза, если, конечно, до него дойдете…
Синхронно и линейно — это нормально

Событийные шины, реактивное программирование и асинхронность в геймдеве обычно продвигают под знаменами «слабой связанности компонентов». Подобные концепции охотно навязывают тем же ребятам, которые пишут абстрактные звуковые системы и имеют в запасе лишний год на их осмысление. Но независимый инди-разработчик вместе с этим набором получает лишь одну проблему: полную невозможность восстановить последовательность выполнения игрового кадра путем простого чтения кода. Классический пример бага в такой системе — урон персонажу рассчитывается уже после того, как интерфейс обновил шкалу здоровья, из-за чего игрок на мгновение видит неактуальные цифры, а на слабом железе этот рассинхрон длится еще дольше. Подобных неявных зависимостей со временем становится всё больше, и для контроля над ними приходится нанимать отдельного человека.
// скучно, зато весь кадр помещается в голову
void Tick(float dt) {
input.Poll();
player.Move(dt);
enemies.Move(dt);
physics.Step(dt);
combat.ResolveDamage(); // урон считается здесь и только здесь
world.RemoveDead();
hud.Refresh(); // UI всегда видит финальное состояние
}
В событийно-ориентированной архитектуре порядок выполнения зависит исключительно от того, какой скрипт успел подписаться на рассылку раньше. А этот порядок, в свою очередь, определяется моментом загрузки сцены, который легко нарушить простым добавлением нового префаба. Стоит вам столкнуться с необходимостью вручную настраивать очередность выполнения скриптов — и вы окончательно потеряли контроль над кадром, вынужденя изобретать посредников и создавать фиктивные сущности. Запомните: один прямолинейный вызов метода Tick в большинстве случаев куда надежнее и дешевле любых архитектурных наворотов.
Да, за простоту тоже приходится платить: линейный Tick неизбежно распухает вместе с проектом, превращаясь в простыню из двухсот вызовов, половина из которых — условные ветвления. Это неприятно, но такой код всё еще можно прочитать сверху вниз за пару минут, в отличие от запутанного графа подписок, который удается распутать лишь в отладчике и только в ходе конкретной игровой сессии.
Ложная сложность

Всё вышесказанное имеет смысл лишь тогда, когда сэкономленные ресурсы направляются непосредственно на создание игры. К счастью, чаще всего так и происходит: разработчики высвобождают силы для проработки интересных механик и сочного геймплея, который в профессиональной среде принято называть «juice». На практике это достигается дюжиной простейших приемов: легкой тряской камеры, микропаузами в момент ударов, правильным масштабированием спрайтов, системой частиц и уникальным звуковым сопровождением для каждого действия. Ни один из этих элементов не требует навороченной архитектуры или сложных инженерных систем — зато им требуется масса времени на тонкую настройку баланса параметров и живое тестирование на игроках.
Сложная система управления пулом на десять тысяч юнитов требует глубокого профилирования и незаурядной инженерной смекалки, но она никак не гарантирует высокий FPS. Производительность кадров — величина нелинейная: падение частоты со 120 до 90 кадров отнимает всего 2.8 мс, в то время как просадка с 30 до 25 кадров съедает сразу 6.7 мс. По своей сути это совершенно разные порядки величин, и ваши сэкономленные крошечные доли миллисекунды дадут прирост оптимизации меньше процента, отняв при этом массу времени, поскольку ваша интуиция в поиске «узких мест» почти наверняка ошиблась еще до запуска профайлера.
Где техническая сложность действительно оправдана, так это в конвейерах сборки и тестирования. Настроенная автосборка с воспроизводимыми билдами, автоматизированными тестами и строгим соблюдением требований платформ (которые у Sony, Microsoft и Nintendo кардинально различаются) — всё это скучные вещи. Их не увидит игрок в трейлере, да и сами программисты с дизайнерами о них не думают. Но именно эта рутина отделяет стадию «мы сделали игру» от стадии «игра успешно вышла в свет». Создать проект — полдела, куда обиднее сорвать дедлайн или получить отказ в сертификации из-за некорректной обработки отключения геймпада, лишившись контракта с издателем. Согласитесь, это гораздо обиднее, чем упущенная возможность написать собственный ECS.
Где простая архитектура ломается
Было бы лукавством утверждать, будто достаточно писать код попроще — и все проблемы автоматически исчезнут. Это не так, ведь у предельной простоты есть свои издержки, и мириться с ними или нет — решать исключительно вам.
Примитивный код со временем неминуемо превращается в кашеобразное болото из-за полного отсутствия границ между модулями. Даже минимальный набор барьеров окупается всегда и везде, а стоит ровно ничего: храните игровые данные отдельно от логики, игровую логику — отдельно от визуального представления, а представление — отдельно от пользовательского интерфейса. Вы можете обойтись вообще без интерфейсов в коде, но жесткое правило «нельзя менять здоровье игрока напрямую из обработчика нажатия кнопки UI» убережет вас от хаоса.
Отдельная тема — система сохранений. Пожалуй, это единственный компонент, ради которого я готов оправдать заблаговременное проектирование. Здесь вы обязаны обеспечить безупречную обратную совместимость данных, а добиться её без продуманной архитектуры практически невозможно. Создать её придется в любом случае, ведь игрок, потративший на прохождение сорок часов и потерявший свой прогресс из-за битого сейва, обрушит на вас шквал гнева. А если таких игроков окажется не один, и даже не сотня?
Во всем остальном код может оставаться настолько примитивным, насколько вам позволяет профессиональная совесть. Можете поинтересоваться у создателя замечательной игры VVVVVV: проект потрясающий, но написан буквально из подручных материалов и «палок» (github.com/TerryCavanagh/VVVVVV/blob/master/desktop_version/src/Logic.cpp).

Архитектура это инструмент
Для конечного пользователя игра существует исключительно на экране и в его воображении. Ни один игрок не полезет в репозиторий проверять, использовали ли вы паттерн ECS, насколько «дурно пахнет» конкретный интерфейс и сколькими костылями подпирался GameManager во время рендеринга. Время разработчика — ресурс крайне дефицитный, и расходовать его следует не на абстрактную сложность, а на то, что видит человек по ту сторону монитора. Помните, что любое элегантное инженерное решение имеет свою цену. Вы когда-нибудь встречали в Steam рецензию вроде: «Геймплей великолепный, визуал потрясающий, но за использование синглтона в игровом цикле ставлю два балла из десяти»? Вот и я нет.
Глава HBO рассказал о периодичности выхода новых сезонов сериала о Гарри Поттере
Разработчики готовят проект для взлома PS5 на прошивках вплоть до версии 13.60
Team Ninja выпустила бесплатное демо Wo Long 2: Wings of Ember и объявила дату релиза игры
Для новой части «Обители зла» создали монстра весом 900 килограммов, которым управляли четыре человека
Кооперативная настольная игра на музыкальные ассоциации «Стереоразум» выйдет на русском языке
JoyToy открыла предзаказ на фигурку Эреба из Warhammer The Horus Heresy
JoyToy показала фигурку вестника-консула «Имперских Кулаков» из Warhammer: The Horus Heresy
Half-Life: Alyx получила крупное обновление для подготовки игры к Steam Frame от Valve