组(Group)
ECS 中的经典 “Hello World” 就是所谓的 “移动系统”。移动系统是一个将所有具有位置和速度组件的实体提取出来的系统,并在此实体上交换位置组件,有效地将其移向速度向量。AHA 的时刻来临时,我们意识到无论该实体上还有什么其他类型的组件,都无关紧要。它可以是一个人、狗、汽车、直升机或房屋。如果它有位置组件和速度组件,它就必须被移动。
然而,我们如何获取这些实体呢?
如上下文章节所述,上下文管理着所有的实体,因此我们可以向上下文询问所有的实体,然后遍历它们,收集那些具有位置和速度组件的实体。这将是一个非常简单的实现。在 Entitas 中,我们为此情况提供了所谓的 组。
context.GetGroup(GameMatcher.AllOf(GameMatcher.Position, GameMatcher.Velocity));
在上面的语句中,我们要求上下文为我们提供一个持有具有 Position
和 Velocity
组件的实体的组。组是一个始终保持最新状态的实体集合。这意味着,如果您从实体中移除一个位置组件,它将立即离开该组。如果您向实体添加位置和速度组件,它将直接进入该组。
您可以毫不犹豫地要求获取组,因为它们在内部会被重用。上下文保留了您要求的所有组的内部列表,因此如果您再次使用相同的匹配器请求一个组,它将只是给您一个对已经存在的组的引用。说到匹配器…
匹配器(Matcher)
匹配器是我们描述我们感兴趣的实体类型的方式。如果您愿意,这是我们的小查询语言。GameMatcher
表示我们有一个 Game
上下文(请参阅上下文章节中的多个上下文类型部分),并且我们可以访问与该上下文关联的所有组件类型。如果我们写 context.GetGroup(GameMatcher.Position);
,我们将得到一个具有 Position
组件的实体组。为了定义更复杂的组,我们可以使用 AllOf
、AnyOf
和 NoneOf
方法。AllOf
表示实体上必须存在所有列出的组件,以便该实体成为组的一部分。AnyOf
表示列出的组件中必须存在一个。而在 NoneOf
的情况下,我们不希望列出的组件存在。NoneOf
不是独立的描述,这意味着您将无法编写 context.GetGroup(GameMatcher.NoneOf(GameMatcher.Position));
。这是被禁止的,因为它会创建一个非常大的集合。NoneOf
只能与 AllOf
或 AnyOf
结合使用。
context.GetGroup(GameMatcher.AllOf(GameMatcher.Position, GameMatcher.Velocity).NoneOf(GameMatcher.NotMovable));
通过这种方式,我们可以说我们需要一个具有 Position
和 Velocity
组件但不具有 NotMovable
组件的实体组。
AllOf
和 AnyOf
也可以组合使用:context.GetGroup(Matcher.AllOf(Matcher.A, Matcher.B).AnyOf(Matcher.C, Matcher.D).NoneOf(Matcher.E))
匹配器的定义也可以以 AnyOf
开头:context.GetGroup(Matcher.AnyOf(Matcher.C, Matcher.D).NoneOf(Matcher.E))
组观察
如前所述,组始终保持最新状态,因此如果我们可以观察组并在实体被添加或从中删除时得到通知,那么它将提供巨大的好处。更重要的是要理解,当我们在实体上替换一个组件时,旧的组件将被删除,新的组件将被添加。这意味着实体将离开一个组,然后以新值重新进入该组。这为我们提供了反应式编程的基础。
在 Entitas-CSharp 中,在内部我们实际上并没有删除和添加组件。生成的代码会询问用户获取新值,像是我们会删除具有旧值的组件一样触发事件,然后在组件中设置新值,再像是添加了新组件一样触发事件。通过这种方式,我们避免了内存分配,并模拟了使用不可变组件的感觉。
组有以下事件,您可以订阅它们:
- OnEntityAdded
- OnEntityRemoved
- OnEntityUpdated
其他类似 Collector、Index 和 Reactive 系统的组件也使用了相同的事件。因此,对于日常工作,您可能可以使用这些事件。但如果您想构建一些自定义内容,您可能需要查看实现细节。