首页 > 解决方案 > 给定向量的指针作为 void * 调用向量的函数

问题描述

我正在使用 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() 函数是我从这个 stackexchange 问题中找到的一个技巧,它允许我将组件类型映射到我在 ComponentManager 向量中用作索引的整数。这对于组件的创建/访问非常有效,因为这些函数是模板并获取传入的组件类型(因此,我可以将 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函数不会通过模板函数将类型传递给它,并且只有 bitset 知道要销毁哪种类型的组件,我无法找到获取类型的方法,以便我可以将 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 中每个组件向量中的哪个组件,为了简洁起见,我省略了这些组件,这样就不会造成任何问题。

提前感谢您的帮助/您可以提供的任何提示!感谢您阅读这篇长篇文章,我愿意接受建议!

标签: c++game-engine

解决方案


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 每种类型一次;如果该开销太大,则可以使用对齐的存储和静态断言 F 不是太大。


推荐阅读