Начнем с базового подхода систем-компонентов-сущностей .
Давайте создадим сборки (термин, полученный из этой статьи) просто из информации о типах компонентов . Это выполняется динамически во время выполнения, точно так же, как мы добавляем / удаляем компоненты к объекту один за другим, но давайте просто назовем его более точно, так как он касается только информации о типе.
Затем мы создаем объекты, определяющие сборку для каждого из них. Как только мы создаем сущность, ее сборка является неизменной, что означает, что мы не можем напрямую изменить ее на месте, но все же мы можем получить подпись существующей сущности в локальной копии (вместе с содержимым), внести в нее надлежащие изменения и создать новую сущность из этого
Теперь о ключевой концепции: всякий раз, когда сущность создается, она присваивается объекту, называемому корзиной сборки , что означает, что все сущности с одинаковой сигнатурой будут находиться в одном контейнере (например, в 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, динамический характер ? Конечно. Здесь я хотел бы подчеркнуть, что существует только один класс «сборка» и один класс «ведро». Контейнеры управляются данными и создаются во время выполнения в оптимальном количестве.
вам нужно будет пройти через все сегменты, которые могут содержать действительную цель. Без внешней структуры данных обнаружение столкновений может быть столь же трудным.
Вот почему у нас есть вышеупомянутые внешние структуры данных . Обходной путь так же прост, как введение итератора в системный класс, который определяет, когда перейти к следующему сегменту. Прыжки будут чисто прозрачными для логики.
источник
Ответы:
По сути, вы разработали статическую объектную систему с распределителем пулов и с динамическими классами.
Я написал объектную систему, которая работает почти идентично вашей системе «сборок» еще в школьные годы, хотя я всегда склонен называть «сборки» либо «чертежами», либо «архетипами» в моих собственных проектах. Архитектура была более болезненной, чем наивные объектные системы, и не имела ощутимых преимуществ в производительности по сравнению с некоторыми из более гибких конструкций, с которыми я ее сравнивал. Возможность динамически изменять объект без необходимости изменять его или перераспределять его чрезвычайно важна, когда вы работаете над редактором игры. Дизайнеры захотят перетащить компоненты в определения вашего объекта. Во время выполнения кода может даже потребоваться эффективная модификация компонентов, хотя лично мне это не нравится. В зависимости от того, как вы связываете ссылки на объекты в вашем редакторе,
Вы получите худшую когерентность кеша, чем вы думаете в большинстве нетривиальных случаев. Ваша система ИИ, например, не заботится о
Render
компонентах, но в конечном итоге застревает, повторяя их как часть каждой сущности. Итерируемые объекты имеют больший размер, и запросы кешлайна в конечном итоге вытягивают ненужные данные, и с каждым запросом возвращается меньше целых объектов). Это все равно будет лучше, чем наивный метод, а композиция объектов наивного метода используется даже в больших механизмах AAA, поэтому вам, вероятно, не нужно лучше, но, по крайней мере, не думайте, что вы не сможете улучшить его дальше.Ваш подход имеет смысл для некоторыхкомплектующие, но не все. Мне очень не нравится ECS, потому что он рекомендует всегда помещать каждый компонент в отдельный контейнер, что имеет смысл для физики, графики или чего-то еще, но не имеет никакого смысла, если вы разрешаете использовать несколько компонентов сценария или составной ИИ. Если вы разрешите использовать систему компонентов не только для встроенных объектов, но и как способ для разработчиков поведения и программистов игры составить поведение объекта, тогда имеет смысл сгруппировать все компоненты AI (которые будут часто взаимодействовать) или все сценарии. компоненты (так как вы хотите обновить их все в одной партии). Если вам нужна наиболее производительная система, вам понадобится сочетание схем размещения и хранения компонентов и время, чтобы окончательно определить, какой вариант лучше всего подходит для каждого конкретного типа компонента.
источник
То, что вы сделали, это переработанные объекты C ++. Причина, по которой это кажется очевидным, заключается в том, что если вы замените слово «сущность» на «класс» и «компонент» на «член», это стандартная конструкция ООП с использованием миксинов.
1) вы теряете динамическую природу подключаемых компонентов, которая была создана специально для того, чтобы уйти от создания статического класса.
2) согласованность памяти наиболее важна для типа данных, а не для объекта, объединяющего несколько типов данных в одном месте. Это одна из причин, по которой были созданы системы "компонент +", чтобы избежать фрагментации памяти класса + объекта.
3) этот дизайн также возвращается к стилю класса C ++, потому что вы думаете о сущности как о связном объекте, когда в дизайне компонента + системы сущность является просто тегом / идентификатором, чтобы сделать внутреннюю работу понятной для людей.
4) компоненту так же легко сериализовать себя, как сложному объекту, чтобы сериализовать несколько компонентов внутри себя, если на самом деле не легче отслеживать его как программиста.
5) следующий логический шаг по этому пути - удалить системы и поместить этот код непосредственно в объект, где у него есть все данные, необходимые для работы. Мы все можем видеть, что это значит =)
источник
Хранение как сущностей вместе не так важно, как вы думаете, поэтому трудно придумать причину, отличную от «потому что это единица». Но поскольку вы действительно делаете это для когерентности кэша, а не для логической когерентности, это может иметь смысл.
Одна сложность, с которой вы можете столкнуться - это взаимодействие между компонентами в разных сегментах. Не очень просто найти что-то, на что ваш ИИ может стрелять, например, вам нужно пройти все сегменты, которые могут содержать действительную цель. Без внешней структуры данных обнаружение столкновений может быть столь же трудным.
Чтобы продолжить объединение сущностей для логической согласованности, единственная причина, по которой мне нужно было объединять сущности, заключается в целях идентификации в моих миссиях. Мне нужно знать, если вы только что создали тип объекта A или тип B, и я обошел это путем ... как вы уже догадались, добавив новый компонент, который идентифицирует сборку, которая соединяет этот объект. Даже тогда я не собираю все компоненты вместе для большой задачи, мне просто нужно знать, что это такое. Поэтому я не думаю, что эта часть ужасно полезна.
источник