ООП ECS против чистого ECS

11

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

В прошлом месяце я много читал о Entity-Component-Systems, и теперь я вполне доволен этой концепцией. Тем не менее, есть один аспект, который, кажется, не хватает четкого «определения», и разные статьи предлагают радикально разные решения:

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

Обратите внимание, что я разрабатываю Framework / API / Engine. Таким образом, цель состоит в том, чтобы его можно было легко расширить тем, кто его использует. Это включает в себя такие вещи, как добавление нового типа компонента рендеринга или столкновения.

Проблемы с подходом ООП

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

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

Проблемы с чистым подходом

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

  • Связи между системами может быть трудно избежать. Например, для системы столкновений может потребоваться ограничительная рамка, которая рассчитывается на основе любого конкретного компонента визуализации. Это можно решить, позволив им общаться через данные. Однако это удаляет мгновенные обновления, так как система рендеринга будет обновлять компонент ограничительной рамки, а система коллизий будет использовать его. Это может привести к возникновению проблем, если не определен порядок вызова функций обновления системы. Существует система событий, которая позволяет системам вызывать события, на которые другие системы могут подписывать свои обработчики. Однако это работает только для того, чтобы сообщать системам, что делать, т.е.

  • Необходимы дополнительные флаги. Возьмите, например, компонент карты тайла. Он будет иметь размер, размер тайла и поле списка индексов. Система карт листов будет обрабатывать соответствующий массив вершин и назначать координаты текстуры на основе данных компонента. Однако пересчет всей карты тайлов в каждом кадре обходится дорого. Поэтому для отслеживания всех изменений, внесенных с целью их последующего обновления в системе, потребуется список. В способе ООП это может быть инкапсулировано компонентом карты тайлов. Например, метод SetTile () будет обновлять массив вершин при каждом его вызове.

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

Кроме того, я не настолько заинтересован в производительности, так что вся эта идея ориентированного на данные дизайна и промахов не имеет для меня никакого значения. Я просто хочу красивую архитектуру ^^

Тем не менее, большинство статей и обсуждений, которые я прочитал, предлагают второй подход. ПОЧЕМУ?

Анимация

Наконец, я хочу задать вопрос о том, как бы я справлялся с анимацией в чистом ECS. В настоящее время я определил анимацию как функтор, который управляет сущностью, основываясь на некотором прогрессе между 0 и 1. У компонента анимации есть список аниматоров, у которого есть список анимаций. В своей функции обновления он применяет все анимации, которые в данный момент активны к объекту.

Замечания:

Я только что прочитал этот пост. Ориентирован ли объект архитектуры Entity Component System на определение? что объясняет проблему немного лучше, чем я. Хотя, в основном, на одну и ту же тему, он по-прежнему не дает ответов на вопрос, почему подход с использованием чистых данных лучше.

Адриан Кох
источник
1
Возможно, простой, но серьезный вопрос: знаете ли вы преимущества / недостатки ECS? Это в основном объясняет «почему».
Карамириэль
Что ж, я понимаю преимущество использования компонентов, то есть композиции, а не наследования, чтобы избежать алмаза смерти в результате множественного наследования. Использование компонентов также позволяет управлять поведением во время выполнения. И они являются модульными. Что я не понимаю, так это то, почему нужно разделять данные и функции. Моя текущая реализация на github github.com/AdrianKoch3010/MarsBaseProject
Адриан Кох
Ну, у меня нет достаточного опыта с ECS, чтобы добавить полный ответ. Но состав не просто используется, чтобы избежать DoD; вы также можете создавать (уникальные) сущности во время выполнения, которые сложно (э) генерировать, используя подход ОО. Тем не менее, разделение данных / процедур позволяет легче рассуждать о данных. Вы можете легко реализовать сериализацию, сохранение состояния, отмену / повтор и тому подобное. Поскольку данные легко рассуждать, их также легче оптимизировать. Скорее всего, вы можете разделить сущности на пакеты (многопоточность) или даже разгрузить их на другое оборудование, чтобы полностью раскрыть его потенциал.
Карамириэль
«Может быть компонент рендеринга спрайта, который переопределяет виртуальный метод отрисовки компонента рендеринга». Я бы сказал, что это больше не ECS, если вы этого требуете.
Wondra

Ответы:

10

Это сложный вопрос. Я просто попытаюсь ответить на некоторые вопросы, основываясь на моем конкретном опыте (YMMV):

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

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

введите описание изображения здесь

... и это:

введите описание изображения здесь

... или это:

введите описание изображения здесь

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

Так? Аналогичный (или буквальный) эквивалент виртуальной и виртуальной таблиц может быть вызван через систему, а не через объект, скрывающий его основное состояние / данные. Полиморфизм все еще очень практичен и выполним с «чистой» реализацией ECS, когда аналогичный vtable или указатель (и) функции превращаются в своего рода «данные» для вызова системой.

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

Так? Я надеюсь, что это не сарказм (это не мое намерение, хотя меня часто в этом обвиняют, но я хотел бы лучше передавать эмоции с помощью текста), но полиморфное поведение "аутсорсинга" в этом случае не обязательно приводит к дополнительному стоимость для производительности.

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

Этот пример кажется мне особенно странным. Я не знаю, почему рендерер будет выводить данные обратно на сцену (я обычно рассматриваю рендереры только для чтения в этом контексте) или для рендерера выяснять AABB вместо какой-либо другой системы, чтобы сделать это как для рендерера, так и для столкновение / физика (возможно, я зациклился на названии «компонента рендеринга»). И все же я не хочу слишком зацикливаться на этом примере, поскольку понимаю, что это не то, что вы пытаетесь сделать. Тем не менее, обмен данными между системами (даже в косвенной форме чтения / записи в центральную базу данных ECS с системами, зависящими скорее непосредственно от преобразований, сделанных другими) не должен быть частым, если вообще необходим. Который'

Это может привести к возникновению проблем, если не определен порядок вызова функций обновления системы.

Это абсолютно должно быть определено. ECS не является конечным решением для изменения порядка оценки обработки системы для каждой возможной системы в кодовой базе и получения конечных пользователей одинаковых результатов, имеющих дело с кадрами и FPS. Это одна из вещей, при разработке ECS, которую я, по крайней мере, настоятельно рекомендую, следует предвосхитить несколько заранее (хотя с большим количеством прощающей передышки, чтобы потом передумать, при условии, что это не изменяет наиболее важные аспекты упорядочения системный вызов / оценка).

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

Я не совсем понял это, за исключением того, что это проблема, ориентированная на данные. И нет никаких ошибок в представлении и хранении данных в ECS, включая запоминание, чтобы избежать таких ошибок производительности (самые большие из них с ECS, как правило, связаны с такими вещами, как системы, запрашивающие доступные экземпляры определенных типов компонентов, что является одним из наиболее сложные аспекты оптимизации обобщенного ECS). Тот факт, что логика и данные разделены в «чистом» ECS, не означает, что вам вдруг придется пересчитывать вещи, которые вы могли бы иначе кэшировать / запоминать в представлении ООП. Это спорный / нерелевантный вопрос, если я не приукрашиваю что-то очень важное.

С «чистым» ECS вы все еще можете хранить эти данные в компоненте карты тайлов. Единственное существенное отличие состоит в том, что логика для обновления этого массива вершин куда-то переместится в систему.

Вы даже можете опираться на ECS, чтобы упростить аннулирование и удаление этого кэша из сущности, если вы создадите отдельный компонент, например TileMapCache. В тот момент, когда кеш желателен, но недоступен в объекте с TileMapкомпонентом, вы можете его вычислить и добавить. Когда он признан недействительным или больше не нужен, вы можете удалить его через ECS без необходимости писать больше кода специально для такого аннулирования и удаления.

Зависимости между компонентами все еще существуют, хотя и скрыты в системах

В «чистом» представлении нет зависимости между компонентами (я не думаю, что было бы правильно сказать, что зависимости скрыты здесь системами). Данные не зависят от данных, так сказать. Логика зависит от логики. И «чистый» ECS имеет тенденцию продвигать логику, которая должна быть написана таким образом, чтобы она зависела от абсолютного минимального подмножества данных и логики (часто ни одной), необходимой для работы системы, что в отличие от многих альтернатив, которые часто поощряются в зависимости от гораздо больше функциональности, чем требуется для реальной задачи. Если вы используете чистое право ECS, первое, что вы должны оценить - это преимущества разделения, одновременно задавая вопросы обо всем, что вы когда-либо учили ценить в ООП, об инкапсуляции и, в частности, о сокрытии информации.

Под развязкой я подразумеваю, как мало информации требуется вашим системам для работы. Ваша система движения даже не должна знать о чем-то гораздо более сложном, таком как Particleили Character(разработчик системы не обязательно должен даже знать, что такие сущностные идеи даже существуют в системе). Просто нужно знать о минимальных данных, таких как компонент позиции, который может быть таким же простым, как несколько операций с плавающей точкой в ​​структуре. Это даже меньше информации и меньше внешних зависимостей, чем то, что имеет обыкновение иметь чистый интерфейс IMotion. Это в первую очередь из-за этого минимального знания, что каждая система требует для работы, что делает ECS часто столь прощающим, чтобы справляться с непредвиденными изменениями дизайна задним числом, не сталкиваясь с каскадными поломками интерфейса повсюду.

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

Зависимости от данных

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

Разработчики продолжали находить причины для добавления, удаления или изменения функций в этом центральном интерфейсе, и каждое изменение было ужасным и дорогостоящим, потому что оно, как правило, ломало каждый отдельный класс, который внедрялся IMotionвместе с каждым последующим местом в системе, которая использовалась IMotion. Между тем, все время с таким количеством болезненных и каскадных изменений IMotionвсе реализованные объекты просто хранили матрицу с плавающей точкой 4х4, а весь интерфейс был просто озабочен тем, как преобразовать и получить доступ к этим числам; представление данных было стабильным с самого начала, и можно было бы избежать большой боли, если бы этот централизованный интерфейс, столь подверженный изменениям с непредвиденными потребностями проектирования, вообще не существовал.

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

введите описание изображения здесь

Наконец, я хочу задать вопрос о том, как бы я справлялся с анимацией в чистом ECS. В настоящее время я определил анимацию как функтор, который управляет сущностью, основываясь на некотором прогрессе между 0 и 1. У компонента анимации есть список аниматоров, у которого есть список анимаций. В своей функции обновления он применяет все анимации, которые в данный момент активны к объекту.

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

С ECS это не серебряная пуля, и у меня все еще есть склонность входить и добавлять новые системы, удалять некоторые, добавлять новые компоненты, изменять существующую систему, чтобы подобрать этот новый тип компонентов, и т. Д. Я не понимаю все правильно с первого раза все еще. Но разница в моем случае заключается в том, что я ничего не меняю, если не могу заранее предвидеть определенные потребности в дизайне. Я не получаю волнообразного эффекта от каскадных поломок, которые требуют, чтобы я разбирался повсюду и изменял так много кода, чтобы справиться с какой-то новой потребностью, которая возникает, и это довольно экономит время. Мне также кажется, что мне легче это делать, потому что когда я работаю с конкретной системой, мне не нужно знать / запоминать так много о чем-то еще, кроме соответствующих компонентов (которые являются просто данными), чтобы работать над ней.

Энергия Дракона
источник