如何存储指针的矢量,包含不同的类型

How to store pointers of vectors, containing different types

本文关键字:包含不 类型 指针 何存储 存储      更新时间:2023-10-16

我目前正在学习更多关于ECS模式的知识,并一直在尝试创建我自己的实现作为实践。我决定在遍历组件时,通过将所有不同的组件打包成向量,而不是使用指针向量,使其对缓存更友好。

我一直在阅读的参考建议将每个不同的组件类型放入不同的数组并循环它,像这样:

AIComponent aiComponents[MAX_NUM];
PhysicsComponent physicsComponents[MAX_NUM];
RenderComponent renderComponents[MAX_NUM];
while (!gameOver)
{
  // Process AI.
  for (int i = 0; i < numEntities; i++)
  {
    aiComponents[i].update();
  }
  // Update physics.
  for (int i = 0; i < numEntities; i++)
  {
    physicsComponents[i].update();
  }
  // Draw to screen.
  for (int i = 0; i < numEntities; i++)
  {
    renderComponents[i].render();
  }
  // Other game loop machinery for timing...
}

我发现这是非常有限的,因为它将要求每次我创建一个新的组件,我将不得不手工创建数组和数组循环。

我更喜欢像这样的单个字段:

// A vector of pointers to other vectors of different types.
// For example, componentPool[0] could be RenderComponent and then
// componentPool[1] could be PhysicsComponent
vector< vector<AnyConcreteComponentType>* > componentPool;
for (int i = 0; i < componentPool.size(); i++)
{
    for (auto& component : componentPool[i]) {
        Update(component);
    }
}

这将允许我在我的init()中动态地添加新系统,像这样:

AddComponent(entityId, RenderComponent());

它将自动扩展componentPool以添加一个新的RenderComponent插槽,然后它将指向新创建的矢量,然后我可以有效地迭代。

问题是我不知道你会怎么做,甚至不知道怎么做是最优的。我想,在访问vector之前,必须有模板、类型转换和一种方法来知道我需要什么类型,但除此之外,我没有任何线索。

假设:

  • 你的问题是一个XY问题,你只认为存储指针到不同类型的向量可能是一个解决方案而不是解决方案,和
  • 你知道编译时的组件类型列表(而不是允许运行时注册),

,这是相对容易实现的方式,通过元组和一些机制来迭代它。

首先,在给定组件类型列表的情况下,我们需要一个元函数来生成正确的元组类型:

namespace detail {
    template<std::size_t N, typename... Components>
    std::tuple<std::array<Components, N>...>
    makeComponentPool(std::tuple<Components...>) noexcept;
} // namespace detail
template<std::size_t N, typename ComponentTup>
using ComponentPool = decltype(detail::makeComponentPool<N>(std::declval<ComponentTup>()));
// example:
static_assert(std::is_same<
    ComponentPool<10, std::tuple<AIComponent, PhysicsComponent, RenderComponent>>,
    std::tuple<
        std::array<AIComponent, 10>,
        std::array<PhysicsComponent, 10>,
        std::array<RenderComponent, 10>
    >
>::value);

则需要某种遍历元组的方法;boost::fusion::for_each在这里工作得很好,或者我们可以滚动我们自己的:

namespace detail {
    template<typename TupT, typename FunT, std::size_t... Is>
    void for_each(TupT&& tup, FunT&& f, std::index_sequence<Is...>) {
        using expand = int[];
        (void)expand{0, (f(std::get<Is>(std::forward<TupT>(tup))), void(), 0)...};
    }
} // namespace detail
template<
    typename TupT, typename FunT,
    std::size_t TupSize = std::tuple_size<std::decay_t<TupT>>::value
>
void for_each(TupT&& tup, FunT&& f) {
    detail::for_each(
        std::forward<TupT>(tup), std::forward<FunT>(f),
        std::make_index_sequence<TupSize>{}
    );
}

现在我们需要做一个决定:每个组件类型应该有相同的公共访问点吗?问题中既有update()也有render();但是,如果我们可以给这些都赋予相同的名称(例如process()),那么事情就非常简单了:

struct AIComponent      { void process() { } };
struct PhysicsComponent { void process() { } };
struct RenderComponent  { void process() { } };
class Game {
    using ComponentTypes = std::tuple<AIComponent, PhysicsComponent, RenderComponent>;
    static constexpr std::size_t MAX_NUM = 3;
    ComponentPool<MAX_NUM, ComponentTypes> componentPool;
    std::atomic_bool gameOver{false};
public:
    void runGame() {
        while (!gameOver) {
            for_each(componentPool, [](auto& components) {
                for (auto& component : components) {
                    component.process();
                }
            });
        }
    }
    void endGame() { gameOver = true; }
};

在线演示
<子>(注意。在演示中,给process()一个参数只是为了说明,而不是因为任何实现需求。)

现在你只需要管理MAX_NUMComponentTypes,其他的一切都到位了。

然而,如果你想允许不同组件类型的不同接入点(例如update()用于AIComponentPhysicsComponent,但render()用于RenderComponent,如问题所示),那么我们显然还有一些工作要做。一种方法是为调用访问点添加一个间接级别,而以最小的开销(语法和运行时)干净地完成该任务的一种方法是使用一些实用程序来创建本地重载集,以便组件处理可以按类型进行简单的特殊处理。下面是一个基本的实现,适用于所有的函函数(包括lambdas),但不适用于函数指针:

template<typename FunT, typename... FunTs>
struct overloaded : private FunT, private overloaded<FunTs...> {
    overloaded() = default;
    template<typename FunU, typename... FunUs>
    overloaded(FunU&& f, FunUs&&... fs)
      : FunT(std::forward<FunU>(f)),
        overloaded<FunTs...>(std::forward<FunUs>(fs)...)
    { }
    using FunT::operator();
    using overloaded<FunTs...>::operator();
};
template<typename FunT>
struct overloaded<FunT> : private FunT {
    overloaded() = default;
    template<typename FunU>
    overloaded(FunU&& f) : FunT(std::forward<FunU>(f)) { }
    using FunT::operator();
};
template<typename... FunTs>
overloaded<std::decay_t<FunTs>...> overload(FunTs&&... fs) {
    return {std::forward<FunTs>(fs)...};
}

如果需要,可以在网上找到更健壮的overload实现,但即使有这个简单的实现,我们现在也可以做以下事情:

struct AIComponent      { void update() { } };
struct PhysicsComponent { void update() { } };
struct RenderComponent  { void render() { } };
class Game {
    using ComponentTypes = std::tuple<AIComponent, PhysicsComponent, RenderComponent>;
    static constexpr std::size_t MAX_NUM = 3;
    ComponentPool<MAX_NUM, ComponentTypes> componentPool;
    std::atomic_bool gameOver{false};
public:
    void runGame() {
        // `auto` overload is the least specialized so `update()` is the default
        static auto process = overload(
            [](           auto& comp) { comp.update(); },
            [](RenderComponent& comp) { comp.render(); }
        );
        while (!gameOver) {
            for_each(componentPool, [](auto& components) {
                std::for_each(begin(components), end(components), process);
                // alternatively, equivalently:
                //for (auto& component : components) {
                //    process(component);
                //}
            });
        }
    }
    void endGame() { gameOver = true; }
};

在线演示

现在您需要管理MAX_NUMComponentTypes,并且还可能为process添加一个新的重载(尽管如果您忘记了,您将得到一个编译器错误)。

你想从你的对象中得到一个定义良好的接口,所以我们可以简单地使用接口和多重继承,但是你的组件列表不能是这里的归属指针。

#include <vector>
#include <iostream>

struct ComponentInterface {
    virtual void update();
};
struct AIComponent : public ComponentInterface {
    virtual void update() override {
        std::cout << "AIComponentn";
    }
};
struct GraphicsStuff {};
public RenderComponent : public GraphicsStuff, ComponentInterface {
    virtual void update() override {
        std::cout << "Rendern";
    }
};
int main() {
    std::vector<ComponentInterface*> components;
    AIComponent ai;
    RenderComponent render;
    components.push_back(&ai);
    components.push_back(&render);
    for (auto&& comp: components) {
        comp->update();
    }
}