21

对于我正在制作的 2D 游戏(对于 Android),我使用的是基于组件的系统,其中 GameObject 包含多个 GameComponent 对象。GameComponents 可以是诸如输入组件、渲染组件、子弹发射组件等。目前,GameComponents 拥有对拥有它们的对象的引用并可以对其进行修改,但 GameObject 本身只有一个组件列表,它并不关心组件是什么,只要在对象更新时可以更新它们即可。

有时一个组件有一些 GameObject 需要知道的信息。例如,对于碰撞检测,游戏对象向碰撞检测子系统注册自己,以便在它与另一个对象碰撞时得到通知。碰撞检测子系统需要知道对象的边界框。我将 x 和 y 直接存储在对象中(因为它被多个组件使用),但宽度和高度只有保存对象位图的渲染组件才知道。我想在 GameObject 中有一个方法 getBoundingBox 或 getWidth 来获取该信息。或者一般来说,我想将一些信息从组件发送到对象。但是,在我当前的设计中,GameObject 不知道它在列表中有哪些特定组件。

我可以想到几种方法来解决这个问题:

  1. 我可以让 GameObject 拥有一些重要组件的特定字段,而不是拥有一个完全通用的组件列表。例如,它可以有一个名为renderingComponent的成员变量;每当我需要获取我刚刚使用的对象的宽度时renderingComponent.getWidth()。该解决方案仍然允许通用组件列表,但它对其中一些组件的处理方式不同,而且我担心我最终会拥有几个异常字段,因为需要查询更多组件。有些对象甚至没有渲染组件。

  2. 将所需信息作为 GameObject 的成员,但允许组件对其进行更新。因此,对象的宽度和高度默认为 0 或 -1,但渲染组件可以在其更新循环中将它们设置为正确的值。这感觉像是一种 hack,为了方便起见,我最终可能会将许多东西推送到 GameObject 类,即使并非所有对象都需要它们。

  3. 让组件实现一个接口,该接口指示它们可以查询的信息类型。例如,渲染组件将实现 HasSize 接口,该接口包括 getWidth 和 getHeight 等方法。当 GameObject 需要宽度时,它会遍历其组件,检查它们是否实现了 HasSize 接口(instanceof在 Java 或isC# 中使用关键字)。这似乎是一种更通用的解决方案,一个缺点是搜索组件可能需要一些时间(但是,大多数对象只有 3 或 4 个组件)。

这个问题不是关于特定问题的。它经常出现在我的设计中,我想知道处理它的最佳方法是什么。因为这是一个游戏,所以性能有点重要,但每个对象的组件数量通常很少(最多为 8 个)。

短版

在基于组件的游戏系统中,在保持设计通用性的同时将信息从组件传递到对象的最佳方式是什么?

4

4 回答 4

18

我们每周在 GameDev.net(游戏对象通常被称为“实体”)上每周获得三到四次关于这个问题的变体,到目前为止,还没有就最佳方法达成共识。几种不同的方法已被证明是可行的,所以我不会太担心。

但是,通常问题与组件之间的通信有关。人们很少担心从组件向实体获取信息——如果一个实体知道它需要什么信息,那么它大概知道它需要访问什么类型的组件以及它需要在该组件上调用哪个属性或方法来获取数据。如果您需要反应而不是主动,则注册回调或使用组件设置观察者模式,以让实体知道组件中的某些内容何时发生更改,并读取该点的值。

完全通用的组件在很大程度上是无用的:它们需要提供某种已知的接口,否则它们存在的意义不大。否则,您也可以只拥有一个大的无类型值关联数组并完成它。在 Java、Python、C# 和其他比 C++ 稍高级的语言中,您可以使用反射为您提供一种更通用的方式来使用特定子类,而无需将类型和接口信息编码到组件本身中。

至于沟通:

有些人假设实体将始终包含一组已知的组件类型(其中每个实例都是几个可能的子类之一),因此可以直接获取对另一个组件的引用并通过其公共接口进行读/写。

有些人正在使用发布/订阅、信号/槽等,在组件之间创建任意连接。这似乎更灵活一些,但最终您仍然需要了解这些隐式依赖关系的知识。(如果这在编译时就知道了,为什么不直接使用以前的方法呢?)

或者,您可以将所有共享数据放在实体本身中,并将其用作每个组件都可以读取和写入的共享通信区域(与 AI中的黑板系统密切相关)。这通常需要一些鲁棒性,以应对某些您期望它们不存在的属性。它也不适合并行性,尽管我怀疑这对小型嵌入式系统来说是一个巨大的问题......?

最后,有些人的系统根本不存在实体。组件存在于它们的子系统中,实体的唯一概念是某些组件中的 ID 值 - 如果渲染组件(在渲染系统内)和播放器组件(在播放器系统内)具有相同的 ID,那么您可以假设前者处理后者的绘图。但是没有任何单个对象可以聚合这些组件中的任何一个。

于 2010-07-05T14:35:18.773 回答
12

正如其他人所说,这里没有永远正确的答案。不同的游戏将适用于不同的解决方案。如果您正在构建一个包含许多不同类型实体的大型复杂游戏,那么在组件之间具有某种抽象消息传递的更加解耦的通用架构可能值得您为获得的可维护性付出努力。对于具有相似实体的更简单的游戏,将所有状态推送到 GameObject 中可能是最有意义的。

对于您需要将边界框存储在某处并且只有碰撞组件关心它的特定场景,我会:

  1. 将其存储在碰撞组件本身中。
  2. 使碰撞检测代码直接与组件一起工作。

因此,与其让碰撞引擎遍历 GameObjects 的集合来解决交互,不如让它直接遍历 CollisionComponents 的集合。一旦发生碰撞,将由组件将其推送到其父游戏对象。

这给您带来了几个好处:

  1. 将特定于碰撞的状态排除在 GameObject 之外。
  2. 使您免于迭代没有碰撞组件的游戏对象。(如果你有很多非交互对象,比如视觉效果和装饰,这可以节省相当多的周期。)
  3. 使您免于在对象及其组件之间行走的燃烧循环。如果您遍历对象然后getCollisionComponent()对每个对象进行迭代,那么指针跟踪可能会导致缓存未命中。对每个对象的每一帧都这样做会消耗大量 CPU。

如果您有兴趣,我会在此处了解有关此模式的更多信息,尽管您似乎已经了解了该章中的大部分内容。

于 2010-07-05T21:12:39.610 回答
4

使用“事件总线”。(请注意,您可能无法按原样使用代码,但它应该为您提供基本概念)。

基本上,创建一个中央资源,每个对象都可以将自己注册为侦听器并说“如果 X 发生,我想知道”。当游戏中发生某些事情时,负责的对象可以简单地将事件 X 发送到事件总线,所有感兴趣的各方都会注意到。

[编辑] 有关更详细的讨论,请参阅消息传递(感谢snk_kid指出这一点)。

于 2010-07-05T14:04:50.727 回答
3

一种方法是初始化一个组件容器。每个组件都可以提供服务,也可能需要其他组件的服务。根据您的编程语言和环境,您必须想出一种方法来提供此信息。

在最简单的形式中,组件之间存在一对一的连接,但也需要一对多的连接。例如,CollectionDetector将有一个实现的组件列表IBoundingBox

在初始化期间,容器将在组件之间建立连接,并且在运行时不会有额外的成本。

这与您的解决方案 3) 很接近,预计组件之间的连接仅连接一次,并且不会在游戏循环的每次迭代中进行检查。

.NET的托管可扩展性框架是解决这个问题的好方法。我知道您打算在 Android 上进行开发,但您仍然可以从这个框架中获得一些灵感。

于 2010-07-05T14:22:16.457 回答