我目前正在了解有关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插槽,然后指向新创建的矢量,然后我可以有效地迭代它。
问题是我不知道你会怎么做,甚至不能做到最好。我想象在访问向量之前必须有模板,转换和知道方法,我需要什么类型但除此之外我没有线索。
答案 0 :(得分:1)
假设:
然后通过元组和迭代它的某种机制相对容易实现。
首先,我们希望元函数在给定组件类型列表的情况下生成正确的元组类型:
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 强>
(示例process()
中的N.b。仅为展示提供参数,而不是由于任何实现要求。)
现在您只需管理MAX_NUM
和ComponentTypes
,其他所有内容都将落实到位。
但是,如果您想为不同的组件类型允许不同的访问点(例如update()
AIComponent
和PhysicsComponent
render()
,RenderComponent
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)...};
}
,在问题中)那么我们显然还有一些工作要做。一种方法是为调用访问点添加一个间接级别,一种方法是干净地完成并且开销最小(语法和运行时)是使用一些实用程序来创建局部重载集,以便组件处理可以非常特殊 - 按类型分类。这是一个适用于所有仿函数(包括lambdas)的基本实现,但不适用于函数指针:
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_NUM
的 Online Demo 强>
现在您需要管理ComponentTypes
和process
,并且还可能向{{1}}添加新的重载(如果您忘记了,您将收到编译器错误)。
答案 1 :(得分:0)
你有一个你想要的对象定义良好的接口,所以我们可以简单地使用接口和多重继承,但你的组件列表不能是这里的拥有指针。
#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();
}
}