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

21

我正在制作一款2D游戏(针对Android),使用组件化系统,其中一个GameObject包含多个GameComponent对象。GameComponents可以是输入组件、渲染组件、子弹发射组件等等。目前,GameComponents引用了拥有它们的对象并可以修改它,但GameObject本身只有一个组件列表,只要这些组件在对象更新时可以被更新就行,不需要关心具体是哪些组件。

有时,组件具有一些GameObject需要知道的信息。例如,为了进行碰撞检测,GameObject向碰撞检测子系统注册自身,以便在与另一个对象碰撞时得到通知。碰撞检测子系统需要知道对象的边界框。我将x和y直接存储在对象中(因为它们被多个组件使用),但宽度和高度仅由持有对象位图的渲染组件所知。我想在GameObject中编写一个名为getBoundingBox或getWidth的方法,以获取该信息。或者说,我想从组件发送一些信息到对象。然而,在我的当前设计中,GameObject并不知道它的列表中有哪些具体的组件。

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

  1. 不是使用完全通用的组件列表,而是让GameObject为一些重要的组件具有特定的字段。例如,它可以有一个成员变量称为renderingComponent;每当我需要获取对象的宽度时,我只需使用renderingComponent.getWidth()。这种解决方案仍然允许使用通用的组件列表,但将某些组件视为不同的处理,并且我担心随着需要查询的组件越来越多,我会最终得出多个例外字段。有些对象甚至没有渲染组件。

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

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

  • 这个问题不涉及具体的问题。在我的设计中经常出现这种情况,我想知道处理它的最佳方式是什么。性能对于游戏来说有些重要,但每个对象的组件数量通常很小(最大为8)。

    简短版

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

    4个回答

    18
    我们在GameDev.net(游戏对象通常称为“实体”)上每周会收到三到四次关于这个问题的变体,到目前为止还没有就最佳方法达成共识。但是已经有多种不同的方法被证明是可行的,所以不必太担心。
    然而,通常问题涉及组件之间的通信。很少有人担心从组件获取信息到实体-如果一个实体知道它需要什么信息,那么它就应该知道需要访问哪种类型的组件以及在该组件上调用哪个属性或方法来获取数据。如果你需要是反应性而不是主动性,那么请注册回调或设置观察者模式与组件配合使用,让实体在组件发生改变时得到通知,并在此时读取值。
    完全通用的组件基本上是无用的:它们需要提供某种已知接口,否则它们存在的意义很小。否则,您可能可以只使用一个大型的未类型化值的键值对数组。
    在Java、Python、C#和其他比C++稍高级的语言中,您可以使用反射来为您提供一种更通用的使用特定子类的方式,而无需将类型和接口信息编码到组件本身中。
    至于通信方面:
    有些人假设一个实体将始终包含一组已知的组件类型(其中每个实例是几个可能子类中的一个),因此可以直接引用其他组件并通过其公共接口进行读写。
    有些人使用发布/订阅、信号/插槽等来创建组件之间的任意连接。这似乎更加灵活,但最终仍然需要具有这些隐式依赖关系知识的东西。(如果在编译时知道这一点,为什么不使用前面的方法呢?)

    或者,您可以将所有共享数据放在实体本身中,并将其用作共享通信区域(与AI中的黑板系统有点相关),每个组件都可以读写。这通常需要在期望属性不存在时具有一定的鲁棒性。它也不适合并行处理,虽然我认为在小型嵌入式系统上这并不是一个重大问题...?

    最后,有些人的系统根本没有实体存在。组件存在于其子系统中,唯一关于实体的概念只存在于特定组件中的ID值——如果渲染组件(在渲染系统内)和玩家组件(在玩家系统内)具有相同的ID,则可以假设前者处理后者的绘图。但是并没有任何单个对象聚合这些组件。


    我有点预料到这将是答案;即有许多方法,它们在不同的情境下都可行。你对于完全通用的系统是正确的,我过度设计并担心不存在的问题。 - Firas Assaad

    12

    像其他人说的,这里没有总是正确的答案。不同的游戏会倾向于不同的解决方案。如果您正在构建一个具有许多不同类型实体的大型复杂游戏,则更脱耦的通用架构和某种抽象消息传递可能值得付出努力以获得可维护性。对于类似实体的简单游戏,将所有状态推送到GameObject中可能是最合理的选择。

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

    1. 将其存储在碰撞组件本身中。
    2. 使碰撞检测代码直接使用组件。

    因此,不要让碰撞引擎遍历一组GameObject来解决交互,而是直接遍历一组CollisionComponents。发生碰撞后,由组件将其推送到其父GameObject。

    这给您带来了一些好处:

    1. 将特定于碰撞的状态留在GameObject之外。
    2. 免除您迭代没有碰撞组件的GameObject的麻烦。(如果您有很多非交互对象,如视觉效果和装饰品,这可以节省相当多的周期。)
    3. 免除了在对象和其组件之间走动的麻烦。如果您遍历对象然后在每个对象上执行getCollisionComponent(),那么指针跟踪可能会导致缓存未命中。每帧为每个对象执行此操作可能会消耗大量CPU。

    如果您感兴趣,我在这个模式这里有更多相关内容,但看起来您已经理解了该章节中的大部分内容。


    我非常喜欢这个想法,我将在特定情况下使用它。我熟悉你的书/网站,事实上我的实现是基于那一章的(这就是为什么我称其为GameObject而不是Entity)。我知道你在其中谈到了组件通信,但我想知道实体在与其组件通信时应该有多通用。感谢您提供如此精彩的资源(是时候再添加一章了!)。 - Firas Assaad
    是的,我知道!这本书不幸地暂停了一段时间。最近我离开了游戏行业并搬到了另一个州,所以我有其他事情要考虑。希望我能很快回来继续写它。 - munificent
    1
    我已经思考实体/组件框架几年了,有时候在有空的时候会回去尝试不同的项目。如果当时我读了那一章而不是更技术性的描述,我可能会好很多 :). 非常棒的阅读。看起来你的答案是正确的:将真正常见的东西推上去(例如位置),将行为上耦合的组件耦合在一起(例如物理和碰撞),并为简单的跨组件触发器发送一些消息。现在我很兴奋再试一次! - orlade

    4

    使用“事件总线”(请注意,您可能无法直接使用代码,但它应该能够给您基本的思路)。

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

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


    1
    听起来像是消息传递架构,通常被称为这个名字。 - snk_kid
    这可能是最通用的方法。对于像我的小游戏来说有点过头了,但作为一种通用解决方案值得考虑。 - Firas Assaad
    好的,由于这个设计非常通用且易于实现(Java中只需不到50行代码,Python中只需20行),因此它确实使设计变得非常简单。当您的游戏运行时,仍然可以优化一些部分,但这将让您快速启动。 - Aaron Digulla

    3

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

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

    在初始化期间,容器将连接组件之间的连接,在运行时将没有额外的成本。

    这接近于您的解决方案3),只是组件之间的连接仅连接一次,而不是在游戏循环的每次迭代中进行检查。

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


    这是一个好主意。这是一种非常通用的方法,初始化时在组件之间进行连线可以使其更加高效。对于小游戏来说,它似乎有点过于复杂,但如果我的设计变得太复杂或者是针对更大的项目,我会考虑使用它。谢谢。 - Firas Assaad

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接