Десятилетиями нам рассказывают, что есть только два пути: громоздкие иерархии ООП или стерильная бюрократия 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 с индексами в десятках разных контейнеров. Мы получаем не меньшую производительность, но с на порядок меньшей сложностью.

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

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


  1. mrhearthstone
    08.08.2025 23:31

    Используйте функциональный - в чем проблема?


  1. Spyman
    08.08.2025 23:31

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

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

    Проблемы начинаются при реактивных штуках (если противник парировал атаку, даёт +100% к слеюущей) но они и в случае хранения данных в общей куче - точно такие же.

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

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


    1. vadimr
      08.08.2025 23:31

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


  1. vadimr
    08.08.2025 23:31

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


    1. MagisterAlexandr
      08.08.2025 23:31

      Здесь это где?


  1. VitalyZaborov
    08.08.2025 23:31

    Можно пример какой-то игры, написанной по таким принципам? Потому что сейчас кажется, что на бумаге всё хорошо, а на практике такой подход будет работать только для несложной адаптации настолки, где правила изначально были достаточно простые, чтобы их мог понять человек:

    Выделите «скелет» сущности: активность, позиция, базовое здоровье — всё, что нужно почти всегда. Поместите в единый массив структур Unit.

    Это будет работать, если взаимодействовать между собой будут только персонажи. Если же у вас можно нанести урон неодушевлённому объекту (фаербол ломает бочку) и неодушевлённый объект сам может быть источником урона (бочка взрывается), то у вас на сцене вместо десятка юнитов их будут сотни: каждое дерево, сундук и даже стул, на котором сидит персонаж - это теперь всё юниты. И их список постоянно меняется, изменяя наш массив: гоблин кинул в игрока гранату? Плюс юнит, потому что граната - это тоже юнит. И так у нас весь выигрыш в производительности растворяется о тысячи проверок каждый кадр "а нет ли у дерева мозгов и не надо ли обновить ему ИИ", потому что вместо массива компонентов Brain у нас указатели в Unit.

    пора положить код на стол и спросить: «Какие данные и в каком порядке мне реально нужно обрабатывать в каждом кадре?»

    Что если данных - миллион и порядок обработки не детерминирован? У вас, к примеру, диаблоид, где и на персонаже, и на противниках куча предметов с эффектами возврата урона, спавна проджектайлов на атаке и прочего вампиризма. И состав этих эффектов ещё и постоянно меняется в реальном времени. Тогда у вас одна функция расчёта урона растянется на тысячи строк. В таких случаях гораздо удобнее все эффекты атаки / защиты описать в виде классов и хранить их экземпляры в списках для каждого юнита. В кого-то прилетел урон? Проходим по спискам атакующего у цели чтобы посчитать его значение и запустить всю дополнительную логику.

    Минимум динамических структур, максимум последовательного доступа к памяти.

    Упарываться в оптимизацию игровой логики на уровне доступа к памяти в 2025 - ну такое себе. Сколько должно быть юнитов и как они должны себя вести чтобы это давало значимый прирост в сравнении хотя бы затратами на поиск пути для этих всех юнитов?


  1. CorruptotronicPervulator
    08.08.2025 23:31

     Кто «владеет» действием — меч, рука, аура или персонаж?

    Дамаг наносит не рука, аура и не персонаж. Оружие. Всегда оружие, остальное — модификаторы, которые должно опросить оружие.

    PS: если рука пустая, то оружие — «кулак», дробящее. Проще некуда.


  1. winorun
    08.08.2025 23:31

    В ООП-версии мы вдруг пытаемся «наносить урон изнутри меча»: метод Sword.hit(target) лезет за параметрами атакующего, целится в защитника, спрашивает у «брони по которой ударили» модификатор, затем уведомляет target.takeDamage(...). А если урон идёт от «огненной ауры» на перчатках? Кто «владеет» действием — меч, рука, аура или персонаж? Где должен жить код? Как не нарушить инварианты десятка объектов одновременно?

    Damage getDamage(Character * defender, Weapon * weapon){
    ....
    }

    wolf.DownHealth(getDamage(wolf,sword));
    wolf.DownHealth(getDamage(wolf,dager));

    И что здесь не так.


  1. JordanCpp
    08.08.2025 23:31

    Спасибо за большое количество кода, теперь мне все стало понятно. Текст отлично объясняет проблемы и даёт решение. Сарказм end.

    Неужели нельзя примеры кода добавить, было, стало и т.д


  1. JordanCpp
    08.08.2025 23:31

    Описывать код только текстом это знаете моветон. Я что должен предполагать, что вы имели ввиду и по тексту в голове код писать?


  1. CloudlyNosound
    08.08.2025 23:31

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

    Господи! Да кто с вами всё это сделал? Сколько вас там? А сколько их? Есть понимание, на каком континенте находится бункер, в котором вас держат?

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

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


  1. SergeyGershkovich
    08.08.2025 23:31

    Предложенный метод "Учётной системы" применяется для управления предприятиями. Объекты в справочнике участвуют в различных регистрах - состояниях (моделях). Вычисления в регистрах могут быть взаимосвязаны транзакциями. Такой подход победил ООП и не даёт разбить на микросервисы ERP системы.

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

    Учётная система - комплексный подход к оптимизации связей элеметов. Кажется не сложно продумать заранее:

    • Регистр состояний (перчатка новая, перчатка требует починки);

    • Регистр владения (какие перчатки какому герою принадлежат);

    • Регистр столкновения юнитов (для всех перчаток, мечей, кольчуг проверяется их пересечение в 3д модели);

    • Другие регистры.

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

    ВЫВОД1: Описанная проблема не архитектурная, а организационная. Когда в команде распределяетя работа:

    • Начинающие разобьются на независимые объекты;

    • Опытные уже думают о группах сервисов состояний объектов;

    • Прожаренные смогут возглавить разработку по упрощённым шаблонам, не распыляясь на детализацию.

    ВЫВОД2: Любой шаблон - устаревшая архитектура. Придет однажды новичек с новым объектом, сломает шаблон и все начнется сначала.


  1. BenGunn
    08.08.2025 23:31

    Пример автора использования ООП на примере RPG игры говорит лишь о том, что автор не может в ООП.