当前位置: 首页>>技术问答>>正文


通信在component-based游戏引擎

克拉苏 技术问答 , , 去评论

问题描述

对于我正在制作的2D游戏(对于Android),我使用的是一个component-based系统,其中GameObject拥有几个GameComponent对象。 GameComponent可以是输入组件,渲染组件,子弹发射组件等等。目前,GameComponents有一个对拥有它们并可以修改它的对象的引用,但是GameObject本身只有一个组件的列表,并且不管这些组件是什么,只要可以在对象被更新时被更新。

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

我可以想办法解决这个问题:

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

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

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

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

短版本

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

最佳解决办法

我们在GameDev.net上(每个玩家通常称之为’entity’),每周三或四次得到这个问题的变化,到目前为止,对最佳方法没有一致意见。已经证明了几种不同的方法是可行的,所以我不用担心太多了。

然而,通常问题是组件之间的通信。人们很少担心从组件到实体获取信息 – 如果一个实体知道需要哪些信息,那么大概知道它需要访问什么类型的组件,哪个属性或方法需要调用该组件来获取数据。如果需要被动而不是活动,则注册回调或者使用组件设置一个观察者模式,以使该实体知道组件中的某些内容发生变化,并读取该值。

完全通用的组件在很大程度上是无用的:他们需要提供某种已知的界面,否则现在就没有什么意义。否则,你可能只需要一个大型关联数组的无类型值,并完成它。在Java,Python,C#和其他slightly-higher-level语言中,您可以使用反射来为您提供使用特定子类的更通用的方式,而无需将类型和接口信息编码到组件本身。

至于沟通:

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

有些人正在使用发布/订阅,信号/插槽等来创建组件之间的任意连接。这似乎有点更灵活,但是最终你仍然需要知道这些隐含依赖关系的东西。 (如果在编译时已经知道了,为什么不用以前的方法呢?)

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

最后,有些人有实体根本不存在的系统。组件生活在其子系统中,实体的唯一概念是某些组件中的ID值 – 如果渲染组件(在渲染系统中)和播放器组件(在玩家系统内)具有相同的ID,则可以假定前者处理后者的绘图。但是没有任何单个对象聚合这些组件之一。

次佳解决办法

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

对于您需要在某处存储边框的具体场景,只有碰撞组件关心它,我会:

  1. 将其存储在碰撞组件本身中。

  2. 使碰撞检测码直接与组件一起使用。

因此,不是让碰撞引擎通过一组GameObjects来迭代来解决交互,而是通过CollisionComponents的集合直接迭代。一旦碰撞发生,它将由组件推送到它的父GameObject。

这给了你几个好处:

  1. 离开GameObject离开collision-specific状态。

  2. 使您无需对没有碰撞组件的GameObjects进行迭代。 (如果您有很多non-interactive对象,如视觉效果和装饰,则可以节省体面的周期数。)

  3. 避免燃烧循环在对象及其组件之间走动。如果您遍历对象,然后在每个对象上执行getCollisionComponent(),那么pointer-following可能导致高速缓存未命中。为每个对象的每个帧做这个都可以烧掉很多CPU。

如果你有兴趣,我会有更多的这种模式here,虽然看起来你已经了解了这一章的大部分内容。

第三种解决办法

使用“event bus”。 (注意,你可能不能使用代码,但它应该给你的基本想法)。

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

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

第四种办法

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

在最简单的形式中,您可以在组件之间连接one-to-one,但您也需要one-to-many连接。例如。 CollectionDetector将具有实现IBoundingBox的组件列表。

在初始化期间,容器将连接组件之间的连接,而在run-time期间,将不会有额外的成本。

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

Managed Extensibility Framework for .NET是一个很好的解决方案。我意识到您打算在Android上开发,但是您可能仍然会从此框架中获得灵感。

参考文献

注:本文内容整合自google/baidu/bing辅助翻译的英文资料结果。如果您对结果不满意,可以加入我们改善翻译效果:gxnotes#qq.com(#替换为@)。

本文由《共享笔记》整理, 博文地址: https://gxnotes.com/article/192141.html,未经允许,请勿转载。
Go