基于组件的游戏引擎中的通信

时间:2010-07-05 14:02:35

标签: oop entity-system component-based

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

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

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

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

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

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

  4. 这个问题与特定问题无关。它常常出现在我的设计中,我想知道处理它的最佳方法是什么。性能有点重要,因为这是一个游戏,但每个对象的组件数量通常很小(最大值为8)。

    简短版

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

4 个答案:

答案 0 :(得分:16)

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

然而,通常问题是组件之间的通信。人们很少担心从组件获取信息到实体 - 如果实体知道它需要什么信息,那么可能它确切地知道它需要访问哪种类型的组件以及它需要在该组件上调用哪个属性或方法来获取数据。如果你需要被动而不是主动,那么注册回调或者用组件设置观察者模式,让实体知道组件中的某些东西何时发生了变化,并在那时读取值。

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

至于沟通:

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

有些人正在使用发布/订阅,信号/插槽等来创建组件之间的任意连接。这看起来有点灵活,但最终你还需要了解这些隐式依赖的知识。 (如果在编译时知道这一点,为什么不使用以前的方法呢?)

或者,您可以将所有共享数据放在实体本身中,并将其用作每个组件可以读取和写入的共享通信区域(与AI中的blackboard system有些相关)。这通常需要面对某些属性时的强大性,而这些属性在您预期时不存在。它也不适用于并行性,尽管我怀疑这是一个小型嵌入式系统的大问题......?

最后,有些人拥有实体根本不存在的系统。组件存在于其子系统中,实体的唯一概念是某些组件中的ID值 - 如果Rendering组件(在Rendering系统中)和Player组件(在Players系统中)具有相同的ID,那么您可以假设前者处理后者的绘制。但是没有任何单个对象聚合这两个组件。

答案 1 :(得分:10)

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

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

  1. 将其存储在碰撞组件中。
  2. 使碰撞检测代码直接与组件一起使用。
  3. 因此,不要让碰撞引擎迭代游戏对象的集合来解决交互,而是让它直接迭代通过CollisionComponents的集合。一旦发生碰撞,将由组件将其推送到其父GameObject。

    这给你带来了几个好处:

    1. 将特定于碰撞的状态保留在GameObject之外。
    2. 让您免于迭代没有碰撞组件的GameObjects。 (如果您有很多非交互式对象,如视觉效果和装饰,这可以节省相当多的周期。)
    3. 让您免受在对象及其组件之间行走的燃烧循环的影响。如果遍历对象然后对每个对象执行getCollisionComponent(),则指针跟随可能导致高速缓存未命中。为每个对象的每个帧执行此操作可能会耗费大量CPU。
    4. 如果你感兴趣我对这个模式here有更多了解,虽然看起来你已经理解了该章的大部分内容。

答案 2 :(得分:4)

使用“event bus”。 (请注意,您可能无法按原样使用代码,但它应该为您提供基本的想法。)

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

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

答案 3 :(得分:3)

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

最简单的形式是组件之间有一对一的连接,但您还需要一对多的连接。例如。 CollectionDetector将包含实施IBoundingBox的组件列表。

在初始化期间,容器将连接组件之间的连接,并且在运行期间不会产生额外费用。

这很接近你的解决方案3),期望组件之间的连接只连接一次,并且在游戏循环的每次迭代都不会被检查。

Managed Extensibility Framework for .NET是解决此问题的好方法。我意识到你打算在Android上开发,但你仍然可以从这个框架中获得灵感。