我目前正在制作一个Entity-Component-System程序作为学习C ++的一种方式(我知道这可能不是推荐的方式,但是我很开心)。到目前为止进展顺利,但我希望对其进行更多的改进。
每个系统当前都以std::function
的形式添加到ECS管理器中,并带有需要运行哪些组件的签名。我当前的策略是遍历每个系统和实体,如果组件签名匹配,则使用实体id作为参数调用系统功能。
// System function
static Game::runSystem(Manager & mngr, int id) {
ComponentA * a = mngr.getComponent<ComponentA>(id);
ComponentB * b = mngr.getComponent<ComponentB>(id);
// Do something with a and b
}
此功能通过以下方法附加到Manager:
// Definition
template<typename...Args>
void Manager::addSystem(std::function<void(XManager & mngr, int id)> f); // A bitmask signature is generated based on the template
// Use
manager.addSystem<ComponentA, ComponentB>(Game::runSystem);
ComponentA
和ComponentB
只是通用类型。如果需要,我甚至可以将组件设为float
或int
。使用getComponent<T>()
时,这些元素将转换为适当的指针。
这一切都很好,但是最终发生的是,我必须为每个系统包括Manager的头文件,并且我希望它们不要相互绑定(我想这是关注点分离)。
最好每个过程最终看起来都像这样:
static Game::runSystem(ComponentA * a, ComponentB * b) {
// Do something with a and b
}
系统唯一需要知道的是它正在处理的特定实体的组件指针。
我目前正在根据其类型的位掩码将组件存储在std::unordered_map
中。当需要组件时,模板提供了一种类型,我可以根据该类型制作位掩码,然后进行投射。我不确定是否可以存储要转换的组件的类型,因此我认为每个组件都必须作为空指针传递给函数。但是C ++不允许我在不知道类型的情况下从void*
隐式转换为另一个指针。有没有一种方法可以存储每个组件的类型,以便以后在需要时进行投射?
我在想我可以为系统所需的每个组件添加带有void*
的函数定义,但是从直觉上看,这似乎是个坏主意。它根本不是类型安全的,通过查看函数声明,我将无法知道它需要什么参数。
如何传递对可以采用任意数量的任何类型的指针的函数的引用?有没有办法通过模板执行此操作,或者我可以在不知道类型的情况下隐式转换一个void指针?我也对其他策略持开放态度。
答案 0 :(得分:0)
ECS的问题在于每个人对他们如何看待ECS都有自己的解释。
系统唯一需要知道的是它正在处理的特定实体的组件指针。
我不同意这一点,至少在它的有效编写方式上是不同的。
系统不应在实体的上下文中运行。实际上,这无关紧要。系统对组件元组感兴趣。在大多数情况下,这些元组是对实体的投影,但这仅仅是语义。
这一切都很好,但是最终发生的是,我必须为每个系统包括Manager的头文件,并且我希望它们不要相互绑定(我想这是关注点分离)< / p>
一个游戏系统实现不依赖于另一游戏系统实现是一件值得关注的事情。这很重要,因为ECS背后的想法是提供一种解决方案,即使实体不具有给定的组件或组件的元组或者给定的游戏系统被禁用,“代码也可以正常工作”。
在我的实现中,系统包括实体系统头。这主要是设计使然,因为我更喜欢实体数据和组件数据在中央位置进行管理,就像数据库一样。这些游戏系统的目的是仅基于游戏逻辑来操纵该中央数据库。
一个非常简单的系统,它会在死亡时发送事件
void DeathSystem::update(const FrameContext& context) {
const auto components = getEntitySystem().getComponents<AttributeComponent>();
for ( const AttributeComponent &component : components ) {
if ( component.getAttributeAsLong(HEALTH) == 0 ) {
sendEvent( EntityDiedEvent( component.getEntityId() ) );
}
}
}
每个系统都实现一个 interface ,该接口旨在为ECS框架提供尽可能多的信息,以了解系统彼此之间的依赖关系,如何明显调用各种生命周期回调等。担心在启动过程中,系统可以在运行时随时以任何顺序进行注册,添加或删除,并且“就可以正常工作”。
我想我可以为系统所需的每个组件使用void *定义函数,但是从直觉上看,这似乎是个坏主意。它根本不是类型安全的,通过查看函数声明,我将无法知道它需要什么参数。
我认为您应该在这里转变观念。这个称为这些游戏系统的高级课程,您是在假设系统需要什么,我不同意。您应该为游戏系统提供一种访问所需内容并驱动其运行的方法。
您可以轻松编写这样的系统。像这样的事情是理想的还是有用的还是有争议的,但是将系统的工作委托给自己只会使代码更容易恕我直言。
void LaggingSystem::update(const FrameContext& context) {
if ( context.getTimeSinceLastFrame() >= maxLagTolerance ) {
const auto &widget = getLagAlertWidget();
if ( !widget.isVisible() ) {
widget.show();
}
}
}