【问题标题】:How to store pointers of vectors, containing different types如何存储向量的指针,包含不同的类型
【发布时间】:2016-09-06 22:39:28
【问题描述】:

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

A reference I've been reading 建议将每个不同的组件类型放入不同的数组并循环遍历它,如下所示:

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 插槽,然后该插槽将指向新创建的向量,然后我可以有效地对其进行迭代。

问题是我不知道你会怎么做,甚至不知道怎么做。我想在访问向量之前必须有模板、强制转换和一种方法来知道我需要什么类型,但除此之外我不知道。

【问题讨论】:

  • 继续阅读您现在正在阅读的 C++ 书籍。在某些时候,您将学到足够的关于继承和子类化的知识,您可以自己解决这个问题。这是一个广泛的主题,无法在 stackoverflow.com 上的简短回答中完全涵盖。
  • 不确定你的意思,我不能在这里使用多态性,因为我必须有一个指针向量,这违背了缓存友好的目的。
  • 问题没有提到这种异国情调的环境,每一飞秒都值$$$。在这种情况下,您将需要构建一个完全自定义的容器来满足您的要求。你的 C++ 书仍然会涵盖这些内容,但会在更晚的高级章节中介绍;并且绝对不能在 stackoverflow.com 上被压缩成一个通用的答案。标准 C++ 库容器无法满足您的要求。
  • 好吧,就像我说的,这对我来说只是一个学习练习。我不介意考虑创建自己的容器,如果按照我描述的方式进行操作,我将不胜感激。
  • 第一个问题:我们到底在谈论多少种组件类型? 1 个声明和一个循环(可以重写为“句柄 X”辅助函数和 lambda)并没有太多样板。您真正想要多久动态更改一次组件类型的数量?这似乎是一个相对愚蠢的要求。而且动态输入有成本,如果您关心速度,无缘无故添加“哦,让它动态”是非常值得怀疑的。

标签: c++ templates vector c++14


【解决方案1】:

假设:

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

那么这可以通过一个元组和一些迭代它的机制来相对容易地实现。

首先,我们希望元函数在给定组件类型列表的情况下生成正确的元组类型:

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

Online Demo
(演示中的 Nb process() 仅提供了一个参数用于说明,而不是由于任何实现要求。)

现在您只需要管理MAX_NUMComponentTypes,其他一切都已到位。

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

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

Online Demo

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

【讨论】:

    【解决方案2】:

    你的对象有一个定义明确的接口,所以我们可以简单地使用接口和多重继承,但你的组件列表不能是这里的拥有指针。

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-08-28
      • 2011-11-22
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多