Я работаю над ориентированной на данные системой компонентом сущностей, где типы компонентов и системные подписи известны во время компиляции.
Объект - это совокупность компонентов. Компоненты могут быть добавлены/удалены из объектов во время выполнения.
Компонент - это небольшой класс без логических элементов.
A подпись - это список типов компонентов компиляции. Сообщается, что сущность соответствует сигнатуре, если она содержит все типы компонентов, необходимые для подписи.
Пример короткого кода покажет вам, как выглядит синтаксис пользователя и каково его предполагаемое использование:
// User-defined component types.
struct Comp0 : ecs::Component { /*...*/ };
struct Comp1 : ecs::Component { /*...*/ };
struct Comp2 : ecs::Component { /*...*/ };
struct Comp3 : ecs::Component { /*...*/ };
// User-defined system signatures.
using Sig0 = ecs::Requires<Comp0>;
using Sig1 = ecs::Requires<Comp1, Comp3>;
using Sig2 = ecs::Requires<Comp1, Comp2, Comp3>;
// Store all components in a compile-time type list.
using MyComps = ecs::ComponentList
<
Comp0, Comp1, Comp2, Comp3
>;
// Store all signatures in a compile-time type list.
using MySigs = ecs::SignatureList
<
Sig0, Sig1, Sig2
>;
// Final type of the entity manager.
using MyManager = ecs::Manager<MyComps, MySigs>;
void example()
{
MyManager m;
// Create an entity and add components to it at runtime.
auto e0 = m.createEntity();
m.add<Comp0>(e0);
m.add<Comp1>(e0);
m.add<Comp3>(e0);
// Matches.
assert(m.matches<Sig0>(e0));
// Matches.
assert(m.matches<Sig1>(e0));
// Doesn't match. (`Comp2` missing)
assert(!m.matches<Sig2>(e0));
// Do something with all entities matching `Sig0`.
m.forEntitiesMatching<Sig0>([](/*...*/){/*...*/});
}
В настоящее время я проверяю, соответствуют ли сущности подписям, используя операции std::bitset
. Однако производительность быстро ухудшается, как только увеличивается количество подписей и количество объектов.
псевдокод:
// m.forEntitiesMatching<Sig0>
// ...gets transformed into...
for(auto& e : entities)
if((e.bitset & getBitset<Sig0>()) == getBitset<Sig0>())
callUserFunction(e);
Это работает, но если пользователь вызывает forEntitiesMatching
с одной и той же сигнатурой несколько раз, все объекты должны быть снова сопоставлены.
Также может быть лучший способ для кэширования сущностей в контейнерах, совместимых с кешем.
Я попытался использовать какой-то кеш, который создает карту времени компиляции (реализованную как std::tuple<std::vector<EntityIndex>, std::vector<EntityIndex>, ...>
), где ключи являются типами подписи (каждый тип подписи имеет уникальный инкрементный индекс благодаря SignatureList
), и значения являются векторами индексов сущностей.
Я заполнил кеш-кортеж чем-то вроде:
// Compile-time list iterations a-la `boost::hana`.
forEveryType<SignatureList>([](auto t)
{
using Type = decltype(t)::Type;
for(auto entityIndex : entities)
if(matchesSignature<Type>(e))
std::get<idx<Type>()>(cache).emplace_back(e);
});
И очистил его после каждого цикла обновления менеджера.
К сожалению, он выполнялся медленнее, чем "сырой" цикл, показанный выше во всех моих тестах. Он также имел бы большую проблему: что, если вызов forEntitiesMatching
действительно удаляет или добавляет компонент к сущности? Кэш должен быть недействительным и пересчитан для последующих вызовов forEntitiesMatching
.
Есть ли более быстрый способ сопоставления сущностей с сигнатурами?
Во время компиляции известно много вещей (список типов компонентов, список типов подписи...) - существует ли какая-либо вспомогательная структура данных, которая может быть сгенерирована во время компиляции, что помогло бы с "bitet-like" соответствие?