Подходы к кодингу игр: дата-ориентированный, ООП чистый и компонентный

 Когда приступаешь к разработке игр, то первым делом сталкиваешься с тем, какую парадигму выбрать.


Можно просто взять и писать игру как она есть. Это классический и самый простой подход.

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

Посмотрим примеры VVVVV и  Undertale. Оба автора рассказывают, что у них было по 800 веток в главном switch блоке.

Расскажи такое профессиональному программисту и он будет в ужасе.

Но игры успешные, и хорошо продаются.

Из этого сделаем вывод - мелкие проекты не нужно "оверинжинирить". Прямой подход самый подходящий.


----------------------------------------------------------------------------------------------------

2. Дата ориентированный

Но как только команда разрастается до 3-5 человек. И среди них появляются не программисты. Тогда возникает потребность в каких-либо инструментах.

 И организацию кода надо уже продумывать хотя бы модульную. 

Часто пляшут от структур данных (массивы структур, или структура массивов)

 

Самый простой из инструментов - текстовый файл.

Помню, когда работал в Nekki над Shadow Fight 3 - там как раз скрипты и весь гейм-дизайн сценариев прописан в обычном файле на своём псевдоязыке. И геймдизайнеры сами его правят.

Программисты лишь добавляют новые теги и параметры.


Так вот этот подход для однородных игр (мало типов объектов - 10-20) самый быстрый. Ты просто пробегаешь по массиву и обрабатываешь объекты, после процедура рендера показывает изменения в игровом мире, представленным массивом.


Самый яркий представитель этого подхода - Джон Блоу. В его выступлениях почерпнул, что как раз дата-ориентированность позволила ему создать Braid быстрее. Да и работает он хорошо.


Сейчас даже другие движки начинают задумываться над такой оптимизацией кода.

Пример: Unity и его DOTS система - обеспечивающая 50-100 тысяч одинаковых юнитов против 10-15 на прошлом методе (компоненты для отдельных поведений и префабы).

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

Минусов у такого подхода тоже много - расширять типы объектов сложно (всё в структурах заложено и под конец они просто расширяются до неприличных размеров, а если использовать указатели - то скорость падает и основное преимущество уже не так очевидно)


---------------------------------------------------------------------------------------------------------------------

3. Классический ООП подход

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

Ты просто моделируешь её через ООД (объект-ориентированный-дизайн), и потом каждую сущность и сообщения программисты делают сами. Скорость достигается за счёт модульной архитектуры, что вытекает из ООД.

Благодаря ООП и полиморфизму множество типов объектов (до 100 видов) вполне можно поддерживать, и расширяемость хороша.

А если смоделировать мир как "Black Box" - то объекты будут сами с ним взаимодействовать, минимизируя взаимодействие друг с другом. Как часто в ИИ делают - просто выделяют слоты вокруг игрока и боты занимают их по мере надобности и приоритета.

Управление таким проектом, а то и аутсорс некритичного кода (и плагинов) вполне становится возможным.

Код может разрастись до 100к и больше и при этом он будет цельным.


Но как и у любого подхода есть и своя ложка дёгтя.

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

Многие компиляторы оптимизируют как могут, но всё же в хорошо написанной системе со слабой связанностью и множеством (>50) объектов они часто не могут предсказать что потребуется. Поэтому поиск метода остаётся.

Платишь за удобство сопровождения и расширения скоростью. 

Приходится узкие места переписывать уже с inline кодом и вводя части дата-ориентированного подхода.

По этой причине в Unity не может быть больше 10к одинаковых персонажей. Скорость значительно падает. Да и движок однопоточный. До последнего времени было целой проблемой распараллелить вычисления.

 

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

Часто всё ещё усугубляется использование инверсией зависимостей (Dependency Injection) - отладка превращается в кошмар, и ещё один уровень абстракции и опосредованности приводит к большему усложнению понимания и так не простой системы.

И многие сталкиваются с такой проблемой системы типов - как ромбовидные и более сложные наследования: объект может быть кругом, что взрывается и прыгает. И это разносят в разные классы: Circle, BlowingCircle, JumpingBlowingCircle и т.д.

Комбинаторный взрыв вам обеспечен. И многие часы отладки, чтобы определить где вниз по ветке наследования неправильно переопределён метод.

И хорошо, если SOLID (в том числе Лисков - потомок эквивалентен предку в поведении) используется. Но если этого нет - становится ещё сложнее.


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

И отладка начинает требовать инструменты визуализации динамики взаимодействия сущностей (для игры, которая в реальном времени происходит), а также отображения деревьев наследования (как в IDA показывается граф выполнения) и графов прохождения событий.


И монср начинает тонуть в асфальтовой топи. Брукс машет вам ручкой.

А тут ещё отдел маркетинга новые типы врагов пообещал и другие механики.


---------------------------------------------------------------------------------------------------------------------

3. Компонентный подход

Пытаясь решить перечисленные проблемы многие студии приходят к компонентному подходу.

Возьмём пример Unity - как самого распространённого (хотя и Godot и Defold пытаются использовать ту же методику, но язык у них не полностью ООП, поэтому этот момент опустим до дальнейшего рассмотрения)

 

И идея с компонентами, которые суть - поведения вроде кажется панацеей и серебрянной пулей.

И да - объект часто можно собрать как из кубиков, да и редактор позволяет их настроить гибко прямо во время разработки уровня или префаба. 

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

Когда их переваливает за 10-15 в редакторе уже неудобно начинается их использовать и настраивать и основная идея компонентов начинает буксовать.

Скорость при этом тоже страдает - каждый из компонент имеет метод Update (они сами рекомендуют иметь один центральный компонент, что будет обновлять подложенные - но тут проблема "человека в середине" вылезает на первый план - и ценность компонент теряется)

И эти Update надо вызывать для каждого из них. И если объектов много - то скорость ещё падает.


Для молодых программистов, типа автора Yandere Sim может получиться ещё хуже. Игра со средним количеством персонажей идёт в 20 кадров даже на хорошей машине.

И с основной фишкой - префабами тоже проблема. 

Ждал целых пять лет, пока они сделают их вложенными и сделают наследование префабов.

И тут началась та самая проблема, что и в ООП с классами. Деревья начинают разрастаться и следить за ними становиться проблемным.

Благо инструмент позволяет создавать редакторы и смотреть связи сущностей. Да плагинов много.


То есть компонентная модель - решив одну проблему сделала круг и вернулась к тому, что было.


-----------------------------------------------------------------------------------------------------

Вывод:

Выбирать подход Вам нужно будет под конкретную игру. Когда вас мало и игра небольшая - лучше не тратить энергию на "трение" (по Джону Блоу) и просто писать. Возможно драйв вас вывезет.

Только не замахивайтесь на большую игру.

 

Для средних игр и  команд 2-5 разработчиков классического модульного или ООП подхода хватит.

А большие и сами прекрасно знают что им нужно выбрать.


И всегда надо помнить и понимать сильные и слабые стороны подхода, поскольку очень трудно переделать игру на другую парадигму. Переписывать придётся с нуля (практически) или применять метод заплаток и какую-то ограниченную ОО модель (когда применяются лишь объекты, но не их системы или инфраструктуры).


Удачного кодинга и с днём программиста.

Комментарии

Популярные сообщения