Подходы к кодингу игр: дата-ориентированный, ООП чистый и компонентный
Когда приступаешь к разработке игр, то первым делом сталкиваешься с тем, какую парадигму выбрать.
Можно просто взять и писать игру как она есть. Это классический и самый простой подход.
И зачастую самый успешный (для небольших проектов, пока не разрастутся достаточно, чтобы мешать связями-спагетти и сложностью сопровождения) когда ты маленький или только начинаешь.
Посмотрим примеры 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 разработчиков классического модульного или ООП подхода хватит.
А большие и сами прекрасно знают что им нужно выбрать.
И всегда надо помнить и понимать сильные и слабые стороны подхода, поскольку очень трудно переделать игру на другую парадигму. Переписывать придётся с нуля (практически) или применять метод заплаток и какую-то ограниченную ОО модель (когда применяются лишь объекты, но не их системы или инфраструктуры).
Удачного кодинга и с днём программиста.
Комментарии
Отправить комментарий