
...или почему игровые движки всегда заканчивают одинаково.
Вы задумывались когда-нибудь о том, что самый дорогой движок в индустрии написан так, что его проще выбросить и написать заново, чем понять и починить. Или что самая успешная студия десять лет жила на коде, который сами авторы называли «отколбашенным» и в русском языке есть более подходящее понятие, начинается на г... заканчивается на ...код. А правильный с точки зрения архитектуры движок умирает в безвестности, потому что деньги у компании на его развитие закончились. Если не узнали примеры, то в конце статьи будет пояснение.
Любой движок, на котором есть "живые" игры проходит через четыре стадии: «хоть как-то, работает», «работает, но стыдно показать», «не стыдно показать, но никто не знает как работает», и «работает на отдельном языке, который мы сами же и придумали». Потом круг замыкается, и начинается новый виток истории, потому что команда осознает, что дальше так жить нельзя.
Сразу оговорюсь, что это во многом пересказ чужих мыслей, докладов с GDC, лекций про паттерны, и общение с коллегами из больших студий. Отсылка к словам и блогам двадцатилетней давности тут не для ностальгии, а для дела, потому что в интернете информация живёт недолго, блоги нулевых уже наполовину выпилены, а выводы пятнадцатилетней давности до сих пор актуальны до неприличия.
Архитектуру и дизайн всегда делают разные люди
Первое, что стоит понять про игровые движки, так что архитектура движка и его дизайн (в смысле "проектирование систем") это не синонимы, а архитектор с проектировщиком это реально разные должности и разные люди и тогда получается хорошо, но иногда их совмещает кто-то один бородатый и получается как получается, как в том анекдоте... что если вести девушку и целовать машину, то оба дела делаются плохо.
Потому что архитектура, это про процессы и результаты, а дизайн систем почти всегда про решения и связи. И главное в работе дизайнера это не написать хорошо вместо плохо, дизайн начинается там, где оба крайних варианта хорошие, но тянут в разные стороны, тогда у выбора будут далекоидущие последствия, которые аукнутся года через три. А в первом прототипе движка обычно все плохо, и дизайнер тут обычно катится рядышком пятым колесом, активно мешая команде. Работа дизайнера сделать так, чтобы по всем осям разработки получалось минимальное сопротивление при написании кода и создании контента. Код и контент находятся на разных полюсах разработки, поэтому очень часто это сложно, иногда невозможно.
Гранулярность: гибкость <───────●──────> простота Связанность: несвязанность <──●───────> запутанность (направо не ходить) Гибкость: приложение <─────●──────> библиотека Связность: удобство <───────●──────> ортогональность
Потому что силы, которые тянут ползунок туда или сюда, почти никогда не про программирование, а про деньги и людей: cколько у вас программистов, сколько артистов, кто кому платит зарплату и кто на ком будет экономить. Архитектор же проектирует систему систем, и ему нет дела до вас, денег и людей... конечно, если это хороший архитектор систем.
Паттерны не про язык программирования
Маленькое лирическое отступление. «Паттерн» часто переводят как «шаблон», но шаблон это жёсткая форма, не допускающая вариаций и прижившаяся в русском языке. В английском для разных описательных частей и вариаций разработки применяют несколько слов, что значительно разнообразит и речь, и контекст понимания, и собственно доклады на конференциях.
Такие разные паттерны
pattern не фиксирует форму, а описывает силы (forces) и то, как они разрешаются, оставляя конкретную реализацию открытой. GoF унаследовали слово, но слегка выхолостили смысл и их паттерны куда ближе к шаблонам.
template жёсткая форма с дырками для подстановки. C++ template будет предельным случаем, когда инстанцирование детерминировано, вариаций тут нет, только по типу-параметру.
idiom как низкоуровневая, языко-специфичная конвенция («RAII», «CRTP», «copy-swap»). Устоявшийся оборот речи в конкретном языке, и ближе всего к лингвистическому смыслу слова как узнаваемый оборот.
blueprint это план, который надо выполнить буквально, т.е. инженерный чертёж.
archetype как прообраз, из которого варианты порождаются через отклонение, а не через подстановку. archetype ближе к «видению», чем к шаблону, но в другую сторону и это скорее источник решение, а не сеть похожих решений.
motif используется как повторяющийся элемент, узнаваемый, но без претензии на «решение проблемы». Больше распространен в музыке, но и в технической документации я тоже его встречал при описании систем.
schema абстрактная структура данных/отношений, из области баз данных и больше не про решение проблемы, а про форму организации данных.
convention договорённость, ничем не мотивированная кроме соглашения нескольких людей («naming convention»).
paradigm на порядок выше по уровню абстракции, целый способ мышления (ООП, функциональщина).
Многие эти слова в русском комьюнити заменяют на одно "паттерн". Менять терминологию, конечно, уже поздно, но в голове держите что вместе с паттерном надо читать и контекст, где это слово применяют. Когда говорят "паттерны проектирования", то обычно вспоминают GoF с их фабриками, синглтонами и прочим зверинцем, но почти всё оттуда это design for change, то есть мы уже внутри картинки мира «как нам это программировать» и эти паттерны больше template, blueprint и idiom, нежели сами паттерны.
Но как бы ни хорош был архитектор и дизайнером, угадать масштаб будущей катастрофы с первого раза почти невозможно, поэтому большинство стартует с того, что любовно называют быстро(тут другое слово)-кодингом, он же «кодим как в 1999-м», а потом начинается самое интересное, потому при кажущемся просторе развития, путей по которым идут движки всего четыре. В жизни любой студии, которая пишет свой движок (назовем это Фаза-0), наступает момент «а давайте сделаем конструктор», и мы переходим в Фазу-1...
Все есть объект...

Здесь обычно есть наколбашенный код одной-двух игр, где там всё сделано классами. Чтобы все было сделано классами, надо иметь GameObject, он же GodObject, который знает всё обо всех. И есть второй Бог, он жеGameLevel, который передаётся повсюду и тоже знает всё. Сейчас всё связано со всем, наш ГоблинВожак знает как работает солнце в небе, потому что солнце тоже GameObject
class GoblinLeader : public Goblin { /* параметры + поведение */ }; // иерархия примерно такая: // GoblinLeader -> Goblin -> Enemy -> AiObject -> MoveableObject -> GameObject
Программист про гоблина-вожака тоже знает всё, кто он (это зашито в наследовании), какой он (значения полей) и что делает (код методов) и это хорошо. Но "кто" и "что-делает" слиплись намертво, потому что класс это данные плюс код в одном флаконе, и это плохо.
Фаза-1 очень кривая, зато работает, и работает долго. На тему GodObject есть прекрасная байка из "Блицкрига" от самих разработчиков, когда в какой-то момент нейтральные свиньи отнаследовались от боевого юнита и, увидев врага, патриотично лезли за оружием, которого у них не было, а патроны к нему были из-за неиниченной переменной, а так как оружия у них было, то они массово нулпоинтерели и крашили игру. Починили краш, выдав свиньям парабеллум, но забрав патроны, т.е. проинитив переменную в 0.
В этой байке весь смысл фазы-1, когда весь движок живёт наследованием от одного класса, и баги лечатся наследованием же. Дальше начинается фаза-2 «дальше так жить нельзя», но как я уже сказал, она может отстоять по времени очень далеко. Настолько далеко, что могут пройти годы, пока команда найдет в себе силы, время и деньги для перехода в фазу-2. Можете посмотреть на Wargaming с его более чем десятью лет в проде, кучей игр под все платформы, еженедельных апдейтов... и как они сами признают, движок находится все еще в фазе-1, полёт нормальный.
... но не все можно объектом сделать
Наконец набирается критическая масса «так жить нельзя» и выделяется команда, которая начинает наводить красоту. Появляются модули, менеджеры, стройные классы, появляется динамика и вместо жёсткого наследования начинаются вопросы «а есть ли у объекта компонент с таким именем?».
GameObject* player = scene.findGameObject("Player"); if (!player->hasComponent("ControlComponent")) player->addComponent(new ControlComponent()); player->update();
GameObject худеет до контейнера компонентов, связность кода падает, связанность систем растет. Когда связность падает это всегда хорошо, а плохо то, что взамен прилетает обилие приведений типов, поиск подобъектов руками, умные указатели повсюду (потому что отличить целую сущность от её части теперь невозможно) и messaging, это такой механизм который позволяет вам сообщить объекту что он уже мертв, а как он будет мертв и кто его удалит решает уже другая система. А самый треш, как обычно, всегда оседает в системе GUI, фраза "так исторически сложилось", как раз больше всего про GUI.
Обычно фаза-2 занимает около года или двух, потому что её делают программисты. Год, на самом деле, это очень-очень мало для того чтобы мигрировать большой проект, и чем меньше по времени оказась фаза-2, тем лучше код был перед чисткой. Для примера у Unreal миграция заняла почти 7 лет, с 2007 года, когда они объявили, что будут двигаться в сторону конфигов-кода, до 2014 года, когда это всё наконец заработало.
... но можно сделать конфигом

А дальше начинает происходить магия, на которую уже не в силах повлиять программисты, которые её написали. Если у нас уже есть описание объекта в данных, зачем вообще держать класс GoblinLeader в коде? Уберём. Пусть объект собирается из внешнего файла.
// больше нет класса GoblinLeader, есть описание { "GoblinLeader": { "EnemyComponent": { /* ... */ }, "MoveableComponent": { /* ... */ }, "BossComponent": { /* ... */ } }} // ^ в файл уехали не только константы, но и сам рецепт сборки объекта
Тут обычно приезжает грузовик с граблями и оказывается что описание простого окошка уехало куда-то в XML и теперь этим XML заведует Ваня-Джон-Вапур, которому тоже надо платить деньги, а он за эти деньги будет писать координаты в XML. И Ваня-джун-программист становится внезапно техническим-художником, а вся красивая ООП-модель приложения уезжает в метамодель оконной системы, которую настраивают другие люди и приложение, с точки зрения программиста, становится чужим и незнакомым.
Программист теперь не знает «кто», и гоблина-вожака в коде нет, есть его отдельные части, и даже эти части не полные, а собираются из каких-то других частей. А для кода отрисовки вожак и вовсем ничем не отличается от камня или свиньи, и вполне может хранить парабелум без патронов, или даже патроны без парабелума отдельно и без желания скрашить игру. Зато гейм-дизайнер собирает из компонентов то, что программист и не задумывал, и чинит свиней без программиста, а иногда и быстрее него.
Характерным признаком приближения к фазе-3 будет момент, когда вы знаете компоненты по отдельности, смотрите на что-то в игре и искренне не понимаете, как дизайнеры вообще ухитрились это собрать. С организационной стороны студия в этот момент делится на отделы, обрастает спринтами, митингами и зонами ответственности, за которые никто не хочет браться, потому что «это в чужой зоне» и появляются отдельные профессии вроде верстальщика интерфейса, тестировщика монстров, CI-инженера и технического-дизайнера для уровней и локаций.
И здесь, в этой фазе, заканчивается программирование, код на C++ и начинается полёт фантазии дизайнеров, за что все так любят Unreal. Но взамен прилетает куча проблем и вопросов как этот полет поддерживать. Как сериализовать? Как отлаживать логику, уехавшую в данные? Как мержить два конфликтующих файла настроек уровня? Все вопросы, на которые в мире чистого C++ давно были ответы, приходится решать заново.
А еще вылезает обязанность, которую ненавидят абсолютно все программисты без исключения - переписывать за геймдизайнерами эту логику обратно на C++, когда те что-то запороли, либо у них не работает, либо работает адски медленно. И получаются позиции вроде технических-дизайнеров, то есть слономухов, потому что выделить это в отдельную специальность невозможно, а переписывать чужой плохой код (а чужой код всегда плохой) никто добровольно не пойдёт.
... через свой язык программирования

Круг замыкается, теперь весь игровой код вынесен из движка, а сам движок превратился в виртуальную машину, а вся игра стала просто контентом и остается дать геймдизайнерам возможность создавать контент самим, без участия программистов.
ScriptEngine::ExecuteScript("GoblinLeader"); // "GoblinLeader" файл, где описаны и параметры, и логика поведения, плюсового кода 0.001%
Признак начала этой фазы узнаётся, когда компания снова пытается сблизить программистов и дизайнеров, появляются технические геймдизайнеры и программистам предлагают самим верстать окна. Но программисты хотят писать скрипты текстом, желательно умеющим всё то же, что C++, плюс то, чего он не умеет, а геймдизайнеры хотят окошки, ползунки и визуальное программирование. Перевешивает обычно та идеологиия, кого в команде больше: где в команде программистская культура, там делают через скрипты, где больше арт, там хотят компоненты.
... data-driven неизбежен
Совершенно нет. Это, наверное, главное, что стоит унести из всего текста и сам смысл data-driven это введение новых, обычно не-программистских специальностей, которые для маленькой команды, где сплошные программисты, будет просто вреден. Программисты будут долго и дорого поддерживать построенную вавилонскую башню, которая окупается только масштабом.
Если для окупаемости игры хватает трёх программистов и кода соответствующего качества, то у вас и будет три программиста и код соответствующего качества. И это нормально. Ранние состояния App Store, гиперказуалки, эпоха появления домашних ПК - всё это были примеры, где наводить data-driven-красоту было оверкиллом. К тому же сейчас не 2006 год и можно взять Unity, Unreal или Godot и использовать их хоть как data-driven-конструктор, хоть в чистом "щанакодим" стиле. А уж устроить кашу можно что в коде, что в данных... это вообще любимое занятие начинающих команд.
... to be continued
Двадцать лет назад умные люди уже описали этот путь от объектов к данным и обратно к языкам. Можно было бы подумать, что сейчас все давно пишут «как в фазе 4», но в реальности живые компании сидят на всех четырёх фазах одновременно, и это нормально, потому что фаза диктуется не модой, хотя и модой тоже... (вот ECS стал таким каргокультом), а деньгами и составом команды.
Унификация инструментов, общие движки, визуальные скрипты: всё это никуда не делось, но магия программирования из мира тоже не исчезла и программисты по-прежнему нужны, и игровые, и движковые, и технические, и ui, и скриптовые, и звуковые... и черт-еще-знает-какие.
Колесо Грегори продолжает крутиться и следом за желанием собрать всю игру на визуальном языке придёт желание получить производительность, а за ним появится желание лучше управлять схемой данных, и где-то на этом витке снова захочется свой DSL, или blueprint, или daslang и всё неминуемо начнётся с начала.
Материалы
Scott Bilas, A Data-Driven Game Object System, GDC 2002. Доклад по архитектуре Dungeon Siege, откуда вообще пошла мода на ECS в разработке. Слайды до сих пор лежат в открытом доступе на https://www.gamedevs.org/uploads/data-driven-game-object-system.pdf
Mick West, Evolve Your Hierarchy, Cowboy Programming (2007). Как деревья наследования упираются в стену и превращаются в композицию компонентов. У него же в блоге есть разбор, как продавать эту идею менеджменту, может кому пригодится. https://cowboyprogramming.com/2007/01/05/evolve-your-heirachy/
Adam Martin, серия Entity Systems Are the Future of MMOG Development, блог t-machine.org. Про переход от компонентов к чистому ECS (entity как ID, компонент как чистые данные).
Niklas Gray, Building a Data-Oriented Entity System, блог Bitsquid/Autodesk Stingray (2014, несколько частей) Разбор движка, который реально живёт в фазе data-driven, с цифрами по производительности. https://www.gamedeveloper.com/programming/the-story-behind-the-truth-designing-a-data-model
Mike Acton, Data-Oriented Design and C++, CppCon 2014. Идеологический полюс, противоположный ООП-иерархиям. https://github.com/CppCon/CppCon2014/blob/master/Presentations/Data-Oriented Design and C%2B%2B/Data-Oriented Design and C%2B%2B - Mike Acton - CppCon 2014.pptx
Jason Gregory, Game Engine Architecture (книга). Глава про game object model, где как раз сравниваются inheritance-based, component-based и data-driven подходы как последовательные стадии одного и того же выбора. https://www.gameenginebook.com/
Tony Albrecht, Pitfalls of Object Oriented Programming, GDC Australia 2009. Прямая критика ООП с точки зрения производительности. https://www.gamedevs.org/uploads/pitfalls-of-object-oriented-programming.pdf
Bjarne Rene, Component Based Object Management, Game Programming Gems 5. Инженерный взгляд на ECS.https://gameenginegems.com/gemsdb/book.php?id=7
Борис Баткин/C. Макеев, Component-oriented design on consoles. Стоит глянуть для понимания как устроен современный WarThunder. https://github.com/SergeyMakeev/ecs + https://daslang.io/
З.Ы. Так вот, обещанное пояснение про первый абзац.
Самый дорогой движок, который проще выбросить, чем понять и починить, это CryEngine, точнее его форк, на котором Крис Робертс уже больше десяти лет пилит Star Citizen и сотни миллионов долларов краудфандинга ушли не в последнюю очередь на то, чтобы переписывать движок под себя. Даже движок (движок, не сама игра) GTA VI оценивается индустрией "всего" в 220M$, почти в два раза дешевле чем Star Citizen.
Студия, которая десять лет прожила на г...коде это Mojang и оригинальная Java-кодовая база Minecraft, которую сообщество и сами разработчики в шутку называли "нельзя удалять строки", но это не помешало игре стать самой продаваемой в истории.
А архитектурно правильный движок, который умер потому что у компании кончились деньги на его развитие - это Bitsquid, позже переименованный в Autodesk Stingray с чистой data-oriented архитектурой с самого начала, о которой до сих пор с придыханием пишут в блогах матерые игроделы. Был закрыт в 2018-м, потому что рынок массово ушёл в Unity и Unreal, а идеальная архитектура без реальных проектов никому оказалась не нужна.