Группировка объектов одного и того же компонента в линейную память

12

Начнем с базового подхода систем-компонентов-сущностей .

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

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

Теперь о ключевой концепции: всякий раз, когда сущность создается, она присваивается объекту, называемому корзиной сборки , что означает, что все сущности с одинаковой сигнатурой будут находиться в одном контейнере (например, в std :: vector).

Теперь системы просто перебирают все интересующие их группы и выполняют свою работу.

Этот подход имеет ряд преимуществ:

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

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

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

Существует также проблема с недействительностью указателя после перераспределения векторов - это можно решить, введя такую ​​структуру:

struct assemblage_bucket {
    struct entity_watcher {
        assemblage_bucket* owner;
        entity_id real_index_in_vector;
    };

    std::unordered_map<entity_id, std::vector<entity_watcher*>> subscribers;

    //...
};

Поэтому всякий раз, когда по какой-то причине в нашей игровой логике мы хотим отслеживать вновь созданную сущность, внутри корзины мы регистрируем entity_watcher , и как только сущность должна быть std :: move'd во время удаления, мы ищем ее наблюдатели и обновляем их real_index_in_vectorк новым ценностям. В большинстве случаев это требует только одного поиска в словаре для каждого удаления объекта.

Есть ли еще недостатки этого подхода?

Почему решение нигде не упоминается, несмотря на то, что оно довольно очевидно?

РЕДАКТИРОВАТЬ : я редактирую вопрос, чтобы «ответить на ответы», так как комментариев недостаточно.

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

Я не. Возможно я не объяснил это достаточно ясно

auto signature = world.get_signature(entity_id); // this would just return entity_id.bucket_owner->bucket_signature or so
signature.add(foo_component);
signature.remove(bar_component);
world.delete_entity(entity_id); // entity_id would hold information about its bucket owner
world.create_entity(signature); // automatically assigns new entity to an existing or a new bucket

Это так же просто, как просто взять подпись существующей сущности, изменить ее и снова загрузить как новую сущность. Pluggable, динамический характер ? Конечно. Здесь я хотел бы подчеркнуть, что существует только один класс «сборка» и один класс «ведро». Контейнеры управляются данными и создаются во время выполнения в оптимальном количестве.

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

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

Патрик Чачурски
источник
Я также прочитал статью Рэнди Галла о хранении всех компонентов в векторах и о том, как их системы просто обрабатывают их. Я вижу две большие проблемы: что, если я хочу обновить только подмножество сущностей (например, подумайте об отбраковке). Из-за этого компоненты будут снова связаны с сущностями. Для каждого шага итерации компонента мне нужно будет проверить, выбран ли объект, к которому он принадлежит, для обновления. Другая проблема заключается в том, что некоторым системам необходимо обрабатывать несколько различных типов компонентов, что лишает возможности когерентности кэша снова. Есть идеи, как бороться с этими проблемами?
Тигучи

Ответы:

7

По сути, вы разработали статическую объектную систему с распределителем пулов и с динамическими классами.

Я написал объектную систему, которая работает почти идентично вашей системе «сборок» еще в школьные годы, хотя я всегда склонен называть «сборки» либо «чертежами», либо «архетипами» в моих собственных проектах. Архитектура была более болезненной, чем наивные объектные системы, и не имела ощутимых преимуществ в производительности по сравнению с некоторыми из более гибких конструкций, с которыми я ее сравнивал. Возможность динамически изменять объект без необходимости изменять его или перераспределять его чрезвычайно важна, когда вы работаете над редактором игры. Дизайнеры захотят перетащить компоненты в определения вашего объекта. Во время выполнения кода может даже потребоваться эффективная модификация компонентов, хотя лично мне это не нравится. В зависимости от того, как вы связываете ссылки на объекты в вашем редакторе,

Вы получите худшую когерентность кеша, чем вы думаете в большинстве нетривиальных случаев. Ваша система ИИ, например, не заботится о Renderкомпонентах, но в конечном итоге застревает, повторяя их как часть каждой сущности. Итерируемые объекты имеют больший размер, и запросы кешлайна в конечном итоге вытягивают ненужные данные, и с каждым запросом возвращается меньше целых объектов). Это все равно будет лучше, чем наивный метод, а композиция объектов наивного метода используется даже в больших механизмах AAA, поэтому вам, вероятно, не нужно лучше, но, по крайней мере, не думайте, что вы не сможете улучшить его дальше.

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

Шон Миддледич
источник
Я сказал: мы не можем изменить подпись объекта, и я имел в виду, что мы не можем напрямую изменить его на месте, но все же мы можем просто получить существующую сборку в локальную копию, внести в нее изменения и снова загрузить ее как новый объект - и эти Операции довольно дешевые, как я показал в вопросе. Еще раз - есть только ОДИН "ведро" класс. «Assemblages» / «Signatures» / «давайте назовем его так, как мы хотим», могут создаваться динамически во время выполнения, как при стандартном подходе, я бы даже пошел так далеко, что думал о сущности как о «сигнатуре».
Патрик Чачурски
И я сказал, что вы не обязательно хотите иметь дело с овеществлением. «Создание нового объекта» потенциально может означать нарушение всех существующих дескрипторов объекта в зависимости от того, как работает ваша система дескрипторов. Ваш звонок, если они достаточно дешевые или нет. Я нашел, что это просто боль в заднице, с которой приходится иметь дело.
Шон Мидлдич
Хорошо, теперь я понял вашу точку зрения по этому поводу. В любом случае, я думаю, что даже если добавление / удаление было бы немного дороже, это происходит настолько случайно, что все же стоит значительно упростить процесс доступа к компонентам, который происходит в режиме реального времени. Таким образом, накладные расходы на «изменение» незначительны. Что касается вашего примера ИИ, разве это не стоит тех немногих систем, которым в любом случае нужны данные из нескольких компонентов?
Патрик Чачурски
Моя точка зрения заключалась в том, что ИИ был местом, где ваш подход лучше, но для других компонентов это не обязательно так.
Шон Миддледич
4

То, что вы сделали, это переработанные объекты C ++. Причина, по которой это кажется очевидным, заключается в том, что если вы замените слово «сущность» на «класс» и «компонент» на «член», это стандартная конструкция ООП с использованием миксинов.

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

2) согласованность памяти наиболее важна для типа данных, а не для объекта, объединяющего несколько типов данных в одном месте. Это одна из причин, по которой были созданы системы "компонент +", чтобы избежать фрагментации памяти класса + объекта.

3) этот дизайн также возвращается к стилю класса C ++, потому что вы думаете о сущности как о связном объекте, когда в дизайне компонента + системы сущность является просто тегом / идентификатором, чтобы сделать внутреннюю работу понятной для людей.

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

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

Патрик Хьюз
источник
2) возможно, я не полностью понимаю кеширование, но допустим, что есть система, которая работает, скажем, с 10 компонентами. В стандартном подходе обработка каждого объекта означает доступ к ОЗУ 10 раз, потому что компоненты разбросаны в произвольных местах памяти, даже если используются пулы, - потому что разные компоненты принадлежат разным пулам. Разве не было бы «важно» одновременно кэшировать всю сущность и обрабатывать все компоненты без единой ошибки кэширования, даже не выполняя поиск по словарю? Кроме того, я сделал правку, чтобы охватить 1) пункт
Патрик Чачурски
В своем ответе @Sean Middleditch хорошо описывает эту проблему с кэшированием.
Патрик Хьюз
3) Они никак не связаны между собой. О том, что компонент А находится рядом с компонентом В в памяти, это просто «когерентность памяти», а не «логическая когерентность», как указал Джон. Ковши, при их создании, могли даже перетасовать компоненты в подписи в любом желаемом порядке, и принципы все равно сохранялись. 4) может быть одинаково легко «отслеживать», если у нас достаточно абстракции - мы говорим только о схеме хранения, снабженной итераторами, и, возможно, карта смещения байтов может сделать обработку такой же простой, как при стандартном подходе.
Патрик Чачурски
5) И я не думаю, что что-то в этой идее указывает на это направление. Дело не в том, что я не хочу с вами соглашаться, мне просто любопытно, к чему может привести это обсуждение, хотя в любом случае оно, вероятно, приведет к своего рода «измерению» или известной «преждевременной оптимизации». :)
Патрик Чачурски
@PatrykCzachurski, но ваши системы не работают с 10 компонентами.
user253751
3

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

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

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

Джон Макдональд
источник
Должен признать, я не совсем понимаю ваш ответ. Что вы подразумеваете под «логической связностью»? По поводу сложностей во взаимодействиях я внес правку.
Патрик Чачурски
«Логическая согласованность», как в: «Имеет« логический смысл »держать все компоненты, составляющие сущность дерева, близко друг к другу.
Джон Макдональд,