Как стать автором
Обновить

Комментарии 5

Ох, пошла моя любимая тема. Жара, так сказать.

Заранее прошу прощения.

Итак приступим:

То что вы предложили, выглядит довольно интересно, своего рода идеологический esc без ecs - плоская структура данных, разделение данных и функций по их обработке, вторая структура индексации (ключи) поверх основной (ссылки в ОП).

Я часто критикую ecs, и во многом ее минусы переносятся и на вашу архитектуру, но, должен признать, у ECS есть несколько плюсов - явный Стейт (который легко сохранять и синхронизировать по сети, к примеру) и нацеленность на производительность за счёт эффективного использования Кеша процессора через организацию данных. Последнее и создаёт большую часть особенностей работы с ecs и боли.

Ваш подход призван решить эти проблемы удобства, но убивает основное преимущество - производительность. Статичные функции в burst не решат проблему, если каждое обращение к стейту это запрос в словарь за сложным объектом, его анбоксинг и приведение (а компилятор добавляет там проверки). Это буквально противоположная сути ecs действие: ecs пытается исправить то, что ООП объекты расположены в памяти случайно, увеличивая время доступа к свойствам. ECS организует структуры в ленты массивов, которые гарантированно попадут в кеш. Ваш подход добавляет ещё слой RAM запросов.

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

По моему мнению гораздо большая проблема - это организация.

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

Тут есть ощущение что все сложно там и все просто тут.

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

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

Так как последовательность операций конфигурируеиая, может даже не хватить просто кода.

Чтобы восстановить работу этой сущности нужно:

  • Найти Стейт

  • Найти все связанные поведения

  • Найти все связанные стейты, которые могут меняться помимо основного

  • Посмотреть варианты конфигураций порядка повелений

  • Создать у себя в голове ментальную модель того, как это работает, разбросанную по десятку файлов кода и конфигураций

  • Надеяться, что нет мало заметного сайд эффекта и никто не изменит порядок операций

На самом деле ООП даже с нарушенными паттернами может быть проще по организации, чем ваш подход. Можно использовать анемичные объекты (без поведения) и выносить поведение в некие системы или процессоры. Есть паттерн такой - DCI, для представления процесса как объекта (помогает в ответах на такие вопросы)

Второй момент - абстрактные переиспользуемые типы данных и плоская структура.

Опять же, выглядит неплохо, но есть и минусы. Дело в том, что DRY нужно применять с осторожностью, иногда объекты выглядят одинаково и имеют одинаковые поля, но они - разные. Это быстро выясняется, когда появляются новые неиспользуемые поля, специальные флаги, делающие функции не универсальными и т.д. Момент можно пропустить и потом получить печальные god-обьекты, которые являются сложными конфигурируемыми мутантами со всем поведением разом.

Аналогично с плоской структурой - это достоинство, когда ты хочешь через голову владельца армии посчитать патроны в карабине первого солдата. Но в остальном просто необходимо организовать армию так, что солдаты знают командира, карабин лежит в рюкзаке солдата и заряжен патронами. С плоской структурой нужно вручную следить за этими связями, нельзя использовать проверку на null для снаряжённого карабина. Вообще можно снарядить кабана в слот карабина и заметить фейл только на проде. Нет инструментов и паттернов для организации, как будто работаешь с void* ссылками в си и руками разыменуешь данные.

Третий момент - реактивные поля.

Идея звучит неплохо, но может порождать ад колбеков: так как порядок колбеков обычно не определен и объекты подписываются в случайном порядке, может получиться ситуация, когда флоу работает по разному а зависимости от порядка подписок. Это создаёт сложные баги и сложный для понимания код, потому что нельзя отделить те компоненты, которые управляют потоком, от тех, которые просто обновляют интерфейс.

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

Итак, резюмируя:

  • ECS с удобствами, но без преимуществ ecs

  • Плохая производительность

  • Бойлер Плейт код (частично решается кодогенерацией, но все равно, get entity.getComponent на каждый чих)

  • Боль с пониманием кода из за конфигурируемого порядка операций и супер декомпозиции

  • Ад зависимостей

  • Сложности с организацией кода

  • Есть и плюсы:

  • Явный плоский Стейт, удобно хранить и передавать (Этого можно добиться и другими инструментами)

  • Не так больно, как ECS

  • С тестированием проще, это правда

  • В общем готов обсудить детали, тема на самом деле интересная. Простите за форматирование, пишу с телефона, не мог молчать)

А в чем минусы ECS для разработки игр?

В комплексности разработки.
ECS предоставляет преимущества, типа высокой производительности и явного состояния, но они достаются не бесплатно.
ECS производительнее по умолчанию только при правильной ECS like организации:
- Мелкие компоненты для батчей
- Использование пулов сообщений вместо колбеков (это буквально одна из важнейших частей супер-производительности)
- Фрагментация логики - вместо условного файла игровой сущности у вас десяток систем, гораздо сложнее понять логику, как ваш объект будет себя вести.
- Для сложных вещей очень натуральна организация деревьев (отряд - солдат - оружие - пулька), в ООП можно организовать вложенность по ссылкам, но в плоской структуре нужна схема индексации, объекты должны собираться по индексам сущностей, это усложняет код.
- Использование индексов сущностей для пункта выше и подобных подобно использованием сырых указателей - нет проверки типов, можно запихнуть неправильную сущность в неправильный слот, постоянно нужно проверять, есть ли у сущности некоторый компонент, это приводит к сложным ошибкам в рантайме, усложняет код и дебаг.
- Так же требователен к архитектуре, но не имеет средств для ее решения. Людям кажется, что если структура плоская, то можно лепить все поверх и меньше рефакторинга. Я даже слышал шутку "Я использую ECS, теперь в моем приложении есть архитектура". Это не так, ты точно так же утопаешь в сложности решения и нужно иногда реорганизовывать свой код (а это сложнее, см пункты выше)


Это если кратко.
Иными словами, фраза "производительность по умолчанию за бесплатно" - это если не вранье, то со звездочкой.
Ты по чуть платишь буквально за каждый шаг и легко можешь лишиться преимуществ.

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

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

Зато код довольно критичный к организации, простоте работы дизайнеров, правильным форматам данных конфигураций и т.д. ООП имеет в этом неплохой задел. И стандартные компоненты юнити хороши именно этим - простотой концепции и организации (по факту это agent based OOP в чистом виде)

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

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

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

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

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

Так же подход имеет заведомо медленную и наивную реализацию для хранения данных. Я даже не буду говорить про скорость ECS и заточку под кеш, это не главный плюс ECS, но классический ООП вариант без индирекшена на Dictionary и тот будет быстрее, зачем продвигать использование неоптимальных решений для системы, которая является ядром всей игры, я не знаю.
С таким подходом фразы а-ля "статический метод можно сделать burst, поэтому использую quaternion" звучат как "дом без фундамента пошёл трещинами, будем использовать скотч".

Раз уж поиск компонентов сделан не по типу, а под каждый тип выделен int, то проще не использовать Dictionary, а например внутри каждой сущности сущности сделать массив object[MaxComponentId + 1], где null это отсутствие компонента (раз уж сделали ссылочную обёртку на примитивы, давайте использовать).
Вот так мы изобрели хранение компонентов, как это делает Entitas, массив быстрее создаётся, быстрее работает, а ещё мы либо не мучаем GC на ресайзе Dictionary, либо, если он имеет capacity с запасом, тупо экономим на памяти в несколько раз, и даже логику менять не пришлось, кроме кода отрисовки в редакторе.

Любой паттерн имеет плюсы и минусы. В зависимости от конкретного места и плюс может окащаться минусом и наоборот. К сожалению, в статье мы видим только плюсы. По моему опыту, наибольшую проблему данного подхода представляет, как раз, непредсказуемость поведения, а в некоторых случаях кумулятивный эффект. У нас в одном UI проекте разработчик решил все переписать на подобные рельсы и UI зажил своей жизнью. Какое-то время с этим мирились и исправляли, расширяли сущьности. Катастрофа случилась после подключения drag-n-drop, т.к. вообще никто не мог предсказать как какой элемент себя поведет, если провести курсором через все элемениы, хотя поодиночке работало, вроде, нормально.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории