我正在使用ECS原理作为练习来实施游戏引擎。我当前的设计有一个ComponentManager类,用于存储与每种组件类型相对应的所有向量。该类的最小版本如下:
class ComponentManager{
private:
std::vector<void*> componentHolder;
public:
bool destroyEntity(int entityID, componentSignature toDestroy);
template <class T>
int registerComponent();
template <class T>
bool addComponent(int entity, T initialComp);
template <class T>
bool removeComponent(int entity);
};
componentHolder是void *的向量,其中每个条目都是包含不同组件类型的向量。我这样做的原因是因为我想将所有组件存储在连续内存中,但是每个组件都是不同的类型。如果我要使用指向某个基本组件类的指针向量,那将破坏高速缓存的一致性,这是我尝试使用此ECS引擎开发的面向数据流的好处。
此外,我的引擎的设计还使其他人可以通过简单地定义一个包含他们希望该组件存储的数据的结构来创建自定义组件,并在创建新游戏“世界”时注册该组件(如果您愿意,也可以创建实例) )。该注册是通过上面看到的registerComponent()函数完成的,为每种组件类型创建一个唯一的ID,并定义为:
template <class T>
int ComponentManager::registerComponent(){
componentHolder.push_back(new std::vector<T>);
return type_id<T>();
}
type_id()函数是我从this stackexchange question中发现的一个技巧,它使我能够将组件类型映射到用作ComponentManager向量中的索引的整数。这对于组件的创建/访问非常有效,因为这些函数是模板,并且可以获取传入的组件的类型(因此,我可以静态将位于组件索引处的void *强制转换为右边的void *。类型),这是一个示例:
template <class T>
bool ComponentManager::addComponent(int entityID){
int compID = type_id<T>();
std::vector<T>* allComponents = (std::vector<T>*)componentHolder[compID];
if (compEntityID.find(entityID) == compEntityID.end()){
(*allComponents).push_back(T());
return true;
}
return false;
}
但是,问题出在我想完全销毁一个实体时。我销毁实体的功能仅需要实体ID和存储在gameWorld对象中并传递的组件签名(组件签名(该位具有翻转为1的位,对应于该实体具有的组件))。但是,由于destroyEntity函数不会获取通过模板函数传递给它的类型,而只能让位集知道要销毁哪种类型的组件,我无法找到一种获取类型的方法,以便可以将void *转换为正确的向量。这是我希望destroyEntity函数执行的示例:
bool ComponentManager::destroyEntity(int entityID, componentSignature toDestroy){
for (int x = 0; x < MAX_COMPONENT; x++){
if (toDestroy[x]){
std::vector<??>* allComponents = (std::vector<??>*)componentHolder[x]; // Here is where the issue lies
(*allComponents).erase((*allComponents).begin() + entityIndex);
}
}
}
例如,在组件注册期间是否可以存储指向每个向量的擦除方法的函数指针,以后可以从destroyEntity()函数调用该函数?或者以某种方式存储从我在注册过程中创建的整数componentID到类型本身的映射,然后在以后进行转换?游戏中的组件类型将在运行时知道,所以我觉得这应该可行吗?需要注意的是,还有一些其他逻辑,我必须找出哪个实体拥有componentHolder中每个分量向量中的哪个分量,为简洁起见,我将其省略,这样不会引起任何问题。
非常感谢您的帮助/您可以提供的任何提示!感谢您阅读这篇长文章,并欢迎提出建议!
答案 0 :(得分:1)
template<class...Ts>
using operation = void(*)(void* t, void*state, Ts...);
template<class...Ts>
struct invoker{
operation<Ts...> f;
std::shared_ptr<void> state;
void operator()(void* t, Ts...ts)const{
f(t, state.get(), std::forward<Ts>(ts)...);
}
};
template<class T, class...Ts, class F>
invoker<Ts...> make_invoker(F&& f){
return {
[](void* pt, void* state, Ts...ts){
auto* pf=static_cast<std::decay_t<F>*>(state);
(*pf)( *static_cast<T*>(pt), std::forward<Ts>(ts)... );
},
std::make_shared<std::decay_t<F>>( std::forward<F>(f) )
};
}
那么这有什么帮助?好吧,您可以使用此存储如何按索引擦除的方法。
std::vector<??>* allComponents = (std::vector<??>*)componentHolder[x]; // Here is where the issue lies
(*allComponents).erase((*allComponents).begin() + entityIndex);
您想要的是执行上述操作的f(void*, int)
。
template<class T>
invoker<int> erase_at_index(){
return make_invoker<std::vector<T>,int>([]( auto&&vec, int index ){
vec.erase(vec.begin()+index);
};
}
仅存储std::vector<invoker<int>> erasers;
。添加新类型后,推入erase_at_index<T>
制造的新橡皮擦。
然后:
erasers[x](componentHolder[x],entityIndex);
完成。
每种类型共享一次ptr;如果开销太大,则可以使用对齐存储和static断言F不太大。