给定向量的指针作为 void * 调用向量的函数

Call a vector's function given the vector's pointer as a void *

我正在致力于使用 ECS 原理实现游戏引擎作为练习。我当前的设计有一个 ComponentManager class 用于存储与每个组件类型对应的所有向量。 class 的最小版本如下所示:

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* 向量,其中每个条目都是包含不同组件类型的向量。我这样做的原因是因为我想将所有组件存储在连续的内存中,但是每个组件都是不同的类型。如果我要使用指向某个基本组件 class 的指针向量,那将破坏我试图通过此 ECS 引擎利用的缓存一致性、面向数据流的优势。

此外,我的引擎旨在让其他人可以通过简单地定义一个包含他们希望该组件存储的数据的结构来创建自定义组件,并在创建新游戏时注册该组件 "world"(或例如,如果你愿意的话)。这个注册是通过上面看到的 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 向量中的索引的整数。这对于组件的 creation/access 很有效,因为这些函数是模板并获取传入的组件的类型(因此我可以将 componentHolder 向量索引处的 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 中每个分量向量中的哪个分量,为简洁起见我省略了它,这样就不会引起任何问题。

提前感谢您提供的 help/any 提示!感谢您阅读这么长的内容 post,我愿意接受建议!

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) )
  };
}

那么这有什么帮助呢?那么你可以使用这个通过索引存储 howto 擦除。

    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); 

完成。

共享指针是每种类型一次;如果开销太大,可以使用对齐存储和静态断言 F 不是太大。