Nomad游戏引擎:第5部分-系统

最后一些游戏功能

这篇文章是系列文章的一部分,我在其中记录我从头开始构建ECS游戏引擎的经验。 请查看 该项目 主页,以 获取更多帖子,信息和源代码。

在进行了一系列理论讨论之后,我们终于可以接触到实际游戏代码所在的引擎部分了。 如果本文中的任何术语令人困惑,请查看以下两篇文章,以全面了解引擎架构:

Nomad游戏引擎:第2部分-ECS
继承下来! medium.com Nomad游戏引擎:第3部分-大图
游戏引擎架构 medium.com

我们这篇文章的目标是建立一个非常简单的系统:运动系统。 该系统依赖两个组件:

在我们进一步实施之前,我想提出几点:

  1. 这个特定的系统实际上可能不是以这种方式设置的-在具有场景图之类的更复杂的3d游戏中,“运动系统”可能根本不是一个系统。 我将在以后的文章中介绍游戏引擎中的“自定义功能”的概念,但现在请您理解,这可能不是处理游戏中动作的“最佳”方法(尽管对于简单的游戏来说效果很好)游戏)
  2. 实际上,我们可以做一些非常酷的优化来加速这样的系统,但是由于我们现在将其用作演示,因此我将在以后的文章中介绍这些优化。

酷,现在让我们跳进去。 这是System类:

如前几篇文章所述,系统必须指定它关心的组件。 在这种情况下,我们的MovementSystem希望注意同时具有Transform组件和Movement组件的任何实体。 为了跟踪系统关心的组件,我们有一个std::bitset systemSignature 。 此签名代表对我们的系统重要的所有组件。

我看到了两种不同的方法来处理实体注册,在这里我将尽力详细介绍这两种方法。

方法1

第一种方法是让每个系统在运行时指定它关心的组件。 这是EntityX使用的解决方案。 本质上,您打了个电话给世界,并要求所有符合给定条件的组件。 在这个例子中,看起来像这样:

这样做的好处是,默认情况下,系统不需要保持太多状态-每个系统都可以随时调用组件的任意组合并获得正确的实体。 此外,添加或删除组件没有额外的开销–您的信息直接来自组件管理器。 最大的缺点是每次都需要进行此查找以检查更改。 通过缓存可以大大加快这一过程,但是最终仍然会调用一个方法,并且每个系统,每个更新都会进行查找。

方法2

第二种方法(也是我更喜欢的一种方法)是让World注意何时从实体中添加和删除组件,并从必要的系统中“注册”或“注销”实体。 这是一个简单的示例:

在黑框周围,您可以看到每个系统中已注册和注销的实体。

这种方法的优点是,每次系统更新都像遍历列表和获取所需组件一样容易。 我们已经知道我们关心的实体,因此无需查找。 主要缺点是,每当添加或删除组件时,都会增加开销。 如果您一次添加或删除大量组件,这确实会降低性能,因为每个实体都需要在所有系统中进行检查。 幸运的是,由于我们使用的是位集,因此这只是一个比较,但是如果在同一更新上进行大量删除/添加操作,仍然可能会很困难。

这两种解决方案在大多数情况下的行为都非常相似,但是我偏爱第二种方案的原因是,我认为大多数实体往往会在大多数时间保留大部分组件–也就是说,组件相对不常见。添加或删除,并且更常见的是保持不变。 要使用第二种解决方案,我们的系统上有以下两种方法:

  void registerEntity(Entity *实体); 
void deRegisterEntity(Entity *实体);

假设只要我们的系统需要更改其registeredEntities列表,世界就会调用这些方法。

执行逻辑

要了解的第二个重要功能是构造函数init()update()render()交互。

构造函数

应该为每个唯一的系统专门定义。 系统可能需要的所有依赖关系都应放在此处。 例如,假设我们有一个需要数据库连接的系统–我们可以在构造函数中传递它:

除了满足任何依赖关系之外,系统还应该通过修改systemSignature来设置它们关心的组件,如果这令人困惑,那么在我们开始实现时就更有意义了。 此处的假设是,发生这种情况之后,一切都将自动化,并且系统可以独立运行。

在里面()

初始化方法应该在运行update()或render()之前被调用,但是在进行基本游戏初始化之后(在所有系统都已添加,所有组件都已添加等之后)被称为。 此init方法有两种用法,但理想情况下,必须执行的大多数繁重工作都会在此init方法中发生。

update()和render()

这两个都在init()运行之后开始被调用。 如果您对更新和渲染之间的差异感到困惑,我强烈建议您查看这篇文章。