Десятилетиями нам рассказывают, что есть только два пути: громоздкие иерархии ООП или стерильная бюрократия ECS. Нас заставили поверить в то, что создание игр — это выбор между анархией и диктатурой.
Это ложь. Оба этих пути — ненужные усложнения. Есть прямой и эффективный способ, который мы променяли на модные, но непрактичные примочки. Эта статья — о том, как вернуться к здравому смыслу.

Как нам продали «серебряную пулю» ООП
Целые поколения разработчиков учили моделировать мир как иерархию классов. В институтских курсах и учебниках их гоняли по UML и «правильным» диаграммам: наследование, полиморфизм, интерфейсы. Легендарный пример — «объектная модель микроволновки»: класс Microwave
, подклассы состояний, входов, кнопок, таймеров. Идея проста — всё в мире «есть объект», значит, мир можно объяснить деревом наследования.
Дальше случилось ожидаемое. «ООП головного мозга» переехало в игровую разработку. «Минотавр — это Humanoid
, Humanoid
это Creature
, а Creature
— это GameObject
, у каждого Humanoid
есть Inventory
, а в Inventory
лежат Item
, а Item
— это... ещё одна ветка дерева». Любая новая механика в RPG — квестовая роль, новая аура, модификатор предмета — врезается в дерево не по оси, ломая предположения, исходя из которых дерево строили. Базовые классы пухнут, инкапсуляция мешает системным проходам, а код разваливается на исключения и костыли.
Второй аспект «ООП головного мозга» — ритуал «состояние объекта меняет сам объект через свои методы». На бумаге это красиво: инкапсуляция, инварианты, «правильные» точки входа. На практике в RPG это превращает прямую, настольную логику в головоломку о том, «кто внутри кого вызывает кого».
В настольной RPG игрок двигает фишку, бросает кубик, вычитает броню цели, записывает урон на листе — последовательность очевидна и внешняя по отношению к «объектам». В ООП-версии мы вдруг пытаемся «наносить урон изнутри меча»: метод Sword.hit(target)
лезет за параметрами атакующего, целится в защитника, спрашивает у «брони по которой ударили» модификатор, затем уведомляет target.takeDamage(...)
. А если урон идёт от «огненной ауры» на перчатках? Кто «владеет» действием — меч, рука, аура или персонаж? Где должен жить код? Как не нарушить инварианты десятка объектов одновременно?
Вместо простого внешнего правила «рассчитать урон по данным атакующего, защищающегося и типа попадания» мы создаём клубок взаимодействий методов, которым нужно «легально» менять чужое состояние. Любая новая механика (контрудар, шипы на броне, сопротивляемость части урона) приводит к комбинаторному взрыву количества мест, куда надо «проползти» и аккуратно вставить вызовы. Поток расчёта перестаёт быть видимым — он размазан по «правильным» классам и их методам. Так ритуал инкапсуляции начинает мешать самой сути игры: прозрачному, детерминированному конвейеру правил.
ООП отлично подходит как способ описания мира, но как «серебряная пуля» для игр он породил «деревья смерти». Реальные игры — это композиция данных и потоков обработки, а не учебная иерархия «микроволновки» с кнопками и дверцей.
А финальный акт трагедии выглядел так: команды, которые дотаскивали RPG до релиза, вынужденно «снимали корону» с ООП и сводили множество классов в один или несколько god-object — «менеджеров всего». В этих классах оказывались почти все данные и почти весь код боёвки/эффектов/квестов. Там, где раньше планировались «красивые виртуальные вызовы», появлялись switch
/if
‑цепочки по типам и флагам: быстрее, прозрачнее и ремонтопригоднее в дедлайны, чем разгонять сигналы по дереву наследования и ловить сайд‑эффекты инкапсуляции. Это важный симптом: даже убеждённые ООП‑практики в критический момент переходят к явному конвейеру и таблицам решений — потому что так проще контролировать логику игры.
Новая «пуля»: как ECS из оптимизации превратили в религию
Естественная реакция на гибельные деревья — отделить данные от логики. Ранние практики и публикации по ECS честно об этом: избавиться от «толстого базового класса», дать дизайнерам свободу комбинировать свойства через компоненты и получить предсказуемые линейные проходы по данным. Там ECS — это способ оптимизации доступа к памяти и ускорения итераций.
Что именно подразумевалось под «ранним» ECS:
— Сущность (Entity) — лишь уникальный целочисленный ID. В сущности нет методов/данных.
— Компонент (Component<T>) — только данные (plain struct).
— Система (System) — обычная функция/функтор, которая объявляет требования к компонентам и выполняется в фиксированном порядке кадра. Система не хранит состояние игры, а действует над данными компонентов. Порядок выполнения систем — детерминирован: например, Input → Physics → Gameplay → Animation → Rendering. Задача ECS — ускорять внутри каждого шага массовые однотипные операции, а не скрывать порядок.
Этот «ранний» подход хорош тем, что возвращает контроль к данным и последовательным проходам: простые структуры, плотные контейнеры, детерминированный порядок систем. Он отлично масштабируется на массовые однотипные операции (позиции, физика, эффекты по срезам), упрощает профилирование и дебаг, а дизайнерам даёт гибкость — поведение задаётся комбинацией компонентов, без переписывания иерархий.
У него, впрочем, есть и недостатки, вот некоторые из них: структурные изменения (add/remove) стоят дороже при архетипном подходе — приходится использовать снимки/буферы команд, чтобы не ломать итерацию; запросы по нескольким компонентам — это всегда выбор компромисса между локальностью и простотой (sparse-set быстрее модифицируется, но хуже бьёт в кэш, архетипы локальны, но тяжелее на миграциях).
Люди любят догмы. Из инструмента сделали догмат: «на любое явление — компонент, на любую реакцию — система», на любое свойство — компонент. В результате простое событие «горение» размазывается по BurningComponent
, BurningDamageComponent
, IgniteEvent
, BurningSystem
, DamageOverTimeSystem
, «тегу одного кадра» и буферу команд, только чтобы не нарушить «чистоту». Структурные изменения дорожают, последовательность систем превращается в скрытый протокол, а «архитектура ради архитектуры» начинает доминировать над геймдизайном.
Да, ECS может помочь ускорить обработку больших массивов данных (в основном за счёт уменьшения размера массива до состояния, когда он помещается в кэш). Но догматическое требование «всё разнести на компоненты и сотни микросистем» доводит подход до абсурда. Там, где нужен один ясный проход по данным и пара локальных структур, возникает «Скрытый протокол порядка систем»: поведение проекта зависит от последовательности апдейтов, что-то происходит сразу, а что-то, что тоже должно происходить мгновенно, происходит только через несколько кадров. Меняем порядок апдейтов — получаем регрессии класса «эффект сработал до урона», «статус сняли раньше проверки». Да, любой пайплайн имеет порядок, но в ECS он размазан по файлам регистраций и систем, плохо обозревается и тестируется изолированно.
Часто происходит «Взрыв микросистем»: требование «компоненты — только данные» толкает к дроблению поведения на десятки «малых систем». На бумаге это красиво («single responsibility»), на практике — каша из крошечных этапов, которые трудно держать в голове. Удобство рефакторинга покупается потерей когерентной картины.
Реальный выигрыш ECS — там, где много однотипных апдейтов каждый кадр (RTS с тысячами юнитов). В RPG и экшен-адвенчурах значимая часть логики — события, триггеры, эффекты, квестовые проверки, то есть «редкие изменения» и «короткие срезы». Там накладные расходы на инфраструктуру ECS могут превышать выигрыш от кэш‑локальности.
Простая реакция на событие («промах по летуну замедляет стрелка») превращается в цепочку новых компонент и микросистем «ради чистоты», вместо одного явного шага в конвейере тика. Когда код всё чаще объясняют словами «так требует наш фреймворк», а не «так проще и надёжнее», это красный флаг.
Сначала было ООП, где нам предложили идею, что фишка на доске должна сама себя двигать. Это породило монстров — хрупкие иерархии классов, где логика и данные были спутаны в один неразрывный ком. Их разбили на мелкие компоненты, и продолжили растирать компоненты в пыль, дойдя до полного абсурда. Отладка превратилась в кошмар. Как же быть?
Вернуться к истокам
ECS и ООП — инструменты. Они не обязаны становиться религией. Когда «плюсы» превращаются в ритуалы, пора положить код на стол и спросить: «Какие данные и в каком порядке мне реально нужно обрабатывать в каждом кадре?»
Посмотрите на настольную RPG: движение фишек по полю и записи на бумаге — это модель «плотные общие данные + редкие расширения». Часто используемые значения (позиция, очки здоровья, инициатива) лежат «в таблице на столе» — их видно всем и они обновляются каждый ход. Редкие свойства (ядовитая кровь, разовая метка заклинания, долговременный дебафф) фиксируются отдельно — условно «на полях листа» — и учитываются только когда актуальны.
Ровно так и должен выглядеть практичный рантайм:
- держим «скелет» сущности с часто используемыми полями в одном плотном массиве,
- добавляем при необходимости редкие/большие блоки данных динамически из пулов и обращаемся к ним только из тех функций, которым это нужно.
Это перекликается с тем, как писали игры в 80-е, когда места на диске у ЭВМ было меньше, чем объём кэша у современного процессора: один или несколько массивов записей фиксированного формата, детерминированный «главный цикл», минимум динамики и максимум последовательных проходов по данным. Тогда экономить память было нужно, чтобы игру в принципе стало возможно создать, сегодня, чтобы игровой сервер был экономически эффективным.
То есть «игровой мир» — это один или несколько массивов записей фиксированной длины; «логика» — один или несколько проходов по этим массивам с понятным порядком шагов. Минимум динамических структур, максимум последовательного доступа к памяти.
Это не «олдскул ради олдскула». Это модель, продиктованная ограничениями: простые структуры, предсказуемые проходы, данные «рядом». Ровно это мы возвращаем — только вместо «одного монолитного массива» даём скелету сущности плотные «горячие» поля, а редкие/большие части выносим в отдельные массивы/пулы. Получается современная версия той же идеи: данные линейны там, где это важно, а код читабелен и предсказуем. Это те самые соображения, которые легли в основу ECS, но воплощённые не в обобщённом шаблонном виде, а простым и эффективным способом, позволяющим не плодить сущности без надобности.
Идея не про «лопату на Си», а про аккуратный, предсказуемый поток данных без лишней инфраструктуры.
Там, где в ECS добавили бы тег/компонент и дождались бы безопасной фазы, в этой модели мы используем обычные очереди для отложенных действий и детерминированный порядок вызовов функций. Например: применяем урон/эффекты заклинаний, изменяя HP и складывая всех персонажей, которые могли умереть, в список для отложенной проверки, потом проверяем смерть и убираем умерших персонажей. Никаких структурных перестроек по ходу.
Такой конвейер и прозрачен, и легко расширяем. Можно смотреть на это как на прадедушку ECS и ООП.
Выделите «скелет» сущности: активность, позиция, базовое здоровье — всё, что нужно почти всегда. Поместите в единый массив структур Unit
.
Вынесите опциональные блоки данных (физика, магия, инвентарь) в пулы. В Unit
оставьте только указатели. Не плодите «мини‑указатели» без нужды.
Забудьте о "системах". Логика — это простая функция, которая проходит по главному массиву юнитов. Пишите обновление мира в одном месте в виде нескольких проходов, сохраняйте явный порядок вызовов.
Заведите простые очереди для отложенных событий вместо структурных изменений «на лету».
Почему это лучше?
Давайте сравним предложенный подход с альтернативами.
* Против "God-Object": Это не лапша‑код. Это чётко структурированный подход: одна главная структура данных, пулы для опциональных данных и набор независимых функций‑систем. Всё просто, предсказуемо и легко для понимания. Нет выворачивания естественной логики наизнанку, когда мечи считают урон.
* Против ECS: Простой подход позволяет избавиться от всего бойлерплейта. Не нужно создавать файлы, регистрировать типы. Нужно новое поведение? Добавьте параметр или указатель на блок параметров в Unit
и допишите код обновления. Всё. Вы сфокусированы на игре, а не на архитектуре.
* А как же производительность? Благодаря пулам, все MagicData
лежат в памяти подряд. Когда ваша функция ProcessMagic
бежит по юнитам, она обращается к данным, которые находятся рядом в кэше. Да, есть один лишний переход по указателю, но вы избавлены от сложной машинерии ECS по сопоставлению Entity ID с индексами в десятках разных контейнеров. Мы получаем не меньшую производительность, но с на порядок меньшей сложностью.
Не нужно ходить кругами. Пришло время прекратить строить архитектурные соборы и начать писать игры. Этот прямой, прагматичный подход — и есть тот самый здравый смысл, который вы потеряли.