0

我目前正在制作一个实体-组件-系统程序作为学习 C++ 的一种方式(我知道这可能不是推荐的方式,但我很开心)。到目前为止进展顺利,但我希望对其进行更多改进。

当前,每个系统都添加到 ECS 管理器中,std::function并附有它需要运行的组件的签名。我当前的策略是遍历每个系统和实体,如果组件签名匹配,则使用实体 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>(),这些被转换为适当的指针。

这一切都完美无缺,但最终发生的是我必须为每个系统包含管理器的头文件,我希望它们不要相互绑定(我想这将是关注点分离)。

最好每个过程最终看起来都类似于:

static Game::runSystem(ComponentA * a, ComponentB * b) {
    // Do something with a and b
}

系统唯一需要知道的是它正在处理的特定实体的组件指针。

我目前std::unordered_map根据其类型的位掩码将组件存储在一个中。当需要一个组件时,模板提供了我能够从中制作位掩码然后转换它的类型。我不确定是否可以存储要转换的组件的类型,所以我认为每个组件都必须作为 void 指针传递给函数。但是 C++ 不允许我在void*不知道类型的情况下从 a 隐式转换为另一个指针。有没有办法可以存储每个组件的类型以便以后需要时进行转换?

我想我可能只void*为系统所需的每个组件定义函数,但直观地说这似乎是个坏主意。它根本不是类型安全的,我无法通过查看函数声明来知道它需要什么参数。

如何将引用传递给可以采用任意数量的任何类型的指针的函数?有没有办法通过模板来做到这一点,或者我可以在不知道类型的情况下隐式转换一个 void 指针?我也对其他策略持开放态度。

4

1 回答 1

1

ECS 的问题在于每个人对他们如何看待它的工作方式都有自己的解释。

系统唯一需要知道的是它正在处理的特定实体的组件指针。

我不同意这一点,至少不同意它的有效写作方式。

系统不应在实体的上下文中运行。事实上,这无关紧要。系统对组件元组感兴趣。在大多数情况下,这些元组是实体上的投影,但这只是语义。

这一切都完美无缺,但最终发生的是我必须为每个系统包含管理器的头文件,我希望它们不要相互绑定(我想这将是关注点分离)

一个游戏系统的实现不依赖于另一个是值得关注的。这一点很重要,因为 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() ) );
    }
  }
}

每个系统都实现了一个接口,该接口旨在为 ECS 框架提供尽可能多的信息,以了解系统之间的依赖关系、如何明显调用各种生命周期回调等。这消除了在启动期间可以注册系统的担忧以任何顺序,在运行时随时添加或删除,并且“它只是工作”。

我想我可以只为系统需要的每个组件使用 void* 的函数定义,但直观地说这似乎是个坏主意。它根本不是类型安全的,我无法通过查看函数声明来知道它需要什么参数。

我认为你应该在这里转变你的心态。这个调用这些游戏系统的更高级别的类,你正在对系统需要什么做出假设,我不同意。您应该为游戏系统提供一种访问它需要的任何东西并让它驱动它所做的事情的方法。

您可以轻松编写这样的系统。这样的事情是理想的还是有用的还有待商榷,但是将系统的工作委托给自己只会让代码更容易恕我直言。

void LaggingSystem::update(const FrameContext& context) {
  if ( context.getTimeSinceLastFrame() >= maxLagTolerance ) {
    const auto &widget = getLagAlertWidget();
    if ( !widget.isVisible() ) {
      widget.show();
    }
  }
}
于 2019-10-11T15:16:18.883 回答