我正在开发一个以数据为导向的实体组件系统,其中组件类型和系统签名在编译时已知。
一个实体是由多个组件聚合而成的。组件可以在运行时添加/删除。
一个组件是一个小型无逻辑类。
签名是一个在编译时确定的组件类型列表。如果实体包含签名所需的所有组件类型,则称实体与签名匹配。
以下简短的代码示例将展示用户语法以及预期使用方式:
这个方法可行,但如果用户多次使用相同的签名调用
另外,可能有更好的方法将实体预先缓存在友好的容器中。
我尝试使用某种缓存机制创建一个编译时映射(实现为
我填充了类似以下的缓存元组:
一个实体是由多个组件聚合而成的。组件可以在运行时添加/删除。
一个组件是一个小型无逻辑类。
签名是一个在编译时确定的组件类型列表。如果实体包含签名所需的所有组件类型,则称实体与签名匹配。
以下简短的代码示例将展示用户语法以及预期使用方式:
// 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
调用。
有没有更快的匹配实体和签名的方法?
在编译时已知许多东西 (组件类型列表,签名类型列表等等) - 是否可以生成任何辅助数据结构来帮助“位集合”匹配?
std::tuple<std::vector<EntityIndex>, std::vector<EntityIndex>, ...>
。向量类型在元组内重复出现的次数等于签名类型的计数。 - Vittorio RomeoManager::refresh()
才会真正被创建(由系统考虑)和销毁(被系统忽略)。通过循环,我指的是所有用户代码对实体和系统的操作以及refresh()
调用。我不确定如何高效地动态更新缓存。当组件被移除时,我必须遍历所有向量以查找实体ID。此外,用户可能在同一周期内多次调用addComponent<T>
或delComponent<T>
。 - Vittorio Romeorefresh()
调用时运行一次。这可能会有问题,因为组件的添加/删除直到下一个“循环”才会被注册(这本身没问题,但与无缓存实现不一致)。 - Vittorio RomeoforEntityMatching<Signature>
内部被调用一次。用户在“循环”期间多次调用forEntityMatching
- 通常每次调用都针对不同的签名。最常见的用法是每个周期为每个签名调用一次forEntityMatching
。 - Vittorio Romeo