游戏引擎中事件驱动的通信:是或否?


62

我正在阅读《游戏编码完成》,作者建议游戏对象和模块之间进行事件驱动通信

基本上,所有在场的游戏参与者都应通过内部事件消息传递系统与关键模块(物理,AI,游戏逻辑,游戏视图等)进行通信。这意味着必须设计一个高效的事件管理器。设计不良的系统会占用CPU周期,特别是影响移动平台。

这是一种行之有效的推荐方法吗?我应该如何决定是否使用它?


詹姆斯,您好,谢谢您的回答。我喜欢它,尽管该书将“事件驱动”通信描述为真正的复杂系统,似乎消耗了大量CPU资源。
Bunkai.Satori 2011年

将其放入答案中,并添加到指向事件消息系统的高级概述的链接,该消息系统是我在堆栈溢出时给出的答案。请享用!只要记住,有些人认为复杂的不一定是:)
James

您也可以使用c ++ 11来检查此答案的
正确性

libuv最近已成为成功的事件库。(在C中。Node.js是其最著名的用例。)
Anko 2014年

Answers:


46

如建议的那样,这是我的评论扩展为完整的答案。

是的,简单明了。交流需要发生,并且在某些情况下“我们还在那里吗?” 类型轮询是必需的,检查事物是否应该执行其他操作通常会浪费时间。相反,您可以让他们对被告知要做的事情做出反应。此外,对象/系统/模块之间的明确定义的通信路径可显着提高并行设置。

我已经在Stack Overflow上对事件消息系统进行概述。从学校开始,我就将其用于专业游戏标题和现在的非游戏产品中,并且每次都适合该用例。

编辑:解决您如何知道该消息的对象的注释问题:应该Request将事件通知给对象本身。您的EventMessagingSystem(EMS)既需要Register(int iEventId, IEventMessagingSystem * pOjbect, (EMSCallback)fpMethodToUseForACallback)匹配,又需要匹配Unregister(为iEventId对象指针和回调之外的对象创建唯一的条目)。这样,当一个对象想知道一条消息时,它就可以Register()与系统通信。当不再需要了解事件时,它可以Unregister()。显然,您需要这些回调注册对象的池,以及从列表中添加/删除它们的有效方法。(我通常使用自排序数组;一种奇特的说法是,它们跟踪未使用对象的池栈和需要时将其大小移动到适当位置的数组之间的分配)。

编辑:对于具有更新循环和事件消息传递系统的完全正常工作的游戏,您可能想查看我的旧学校项目。上面链接的Stack Overflow帖子也引用了它。


詹姆斯,您好,我在考虑内部事件消息传递系统时,我认为它不需要为引擎增加更多成本。例如,如果屏幕上有50个对象,但是只有5个对象可以更改其行为;在传统系统下,所有50个对象都需要检查其所有可能的操作,以查看是否应做些事情。但是,使用事件消息传递后,只有5条消息将通过特定的操作更改发送给这5个对象。这看起来很节省时间。
Bunkai.Satori 2011年

我在上面的答案中链接到的系统起作用的方式是,对象仅注册即可听到他们想要的消息。.在视频游戏中,水平中可能有100-200个对象,但我们只能利用此优势,但是您只能“激活”玩家可以直接与之互动的内容,使收听的事物数量减少到大约10左右。总而言之,这种类型的系统应该比“我们还存在吗?”更“智能”。轮询系统,并应就通信而言至少减少引擎的开销。
詹姆斯

与事件驱动系统有关的一件事使我感到困惑:在传统系统下,每个对象都有定期调用的预定义方法集(OnUpdate(),OnMove(),OnAI(),OnCollisionCheck()...)。可用于检查和管理其状态。在事件驱动系统下,每个对象都必须具有主系统限制条件,然后将消息发送到检测到某个事件的对象。对我而言,这标准化了可能的对象状态,并限制了创建唯一对象行为的自由。真的吗?
Bunkai.Satori 2011年

观察者模式是轮询的典型替代方法。zh.wikipedia.org/wiki/Observer_pattern
cket

1
@ hamlin11很高兴这一信息仍然帮助人们:)在问候登记,如果发生这种情况相当多,记住你将要优化它的速度,将登记对象的池来,等画
詹姆斯

22

我的意见是,您应该开始制作游戏并实现自己喜欢的功能。当您这样做时,您会发现自己在某些地方使用了MVC,但在其他地方却没有。在某些地方发生事件,但在其他地方则没有;组成某些地方,但继承其他地方;清洁一些地方的设计,而另一些地方进行粗暴的设计。

没关系,因为完成后您实际上将拥有一个游戏,并且游戏比消息传递系统更酷。


2
嗨,乔,谢谢您的回答,我喜欢。您认为,无需人为地推销任何手段。我应该按照我认为可以使用的方式设计应用程序,如果以后无法使用某些内容,我将简单地重做。
Bunkai.Satori 2011年

这就是系统存在的原因。很多人都按照您所说的做,见证了噩梦,并说必须有更好的方法。您可以重复自己的错误,也可以向他们学习,甚至可以进一步提高自己的错误率。
user441521

16

一个更好的问题是,那里有哪些替代方案?在这样一个复杂的系统中,它具有适当划分的物理,人工智能等模块,您还可以如何编排这些系统?

消息传递似乎确实是此问题的“最佳”解决方案。我现在想不出其他选择。但是在实践中有大量的消息传递示例。实际上,操作系统将消息传递用于其多个功能。来自维基百科

消息也通常与进程间通信相同地使用。另一种常见的技术是流或管道,其中,数据是作为一系列基本数据项发送的(虚拟电路的高级版本)。

(进程间通信是操作系统环境中进程(即,正在运行的程序实例)之间的通信)

因此,如果它对操作系统足够好,那么对游戏来说可能就足够了,对吧?同样还有其他好处,但是我将让Wikipedia上有关消息传递的文章进行解释。

我还问了一个有关堆栈溢出的问题,“程序中传递消息的数据结构?” ,您可能需要阅读。


您好Ricket,谢谢您的回答。您询问可以使用什么其他方法使游戏对象彼此通信。我想到的是直接方法调用。它的核心是,它没有为您提供太多的灵活性,但是另一方面,它避免了事件消息的生成,传递,阅读等。我们仍然在谈论移动平台,而不是高端游戏。操作系统大量使用内部消息传递系统。但是,它不能实时运行,即使很小的延迟也会造成中断。
Bunkai.Satori 2011年

让我让这个问题开放一段时间。在结束之前,我想收集更多意见。
Bunkai.Satori 2011年

直接方法调用通常会导致相互调用的类耦合。对于将系统拆分为应该互换的组件而言,这不是很好。有一些模式可以以较少的内含性促进直接方法调用(例如,MVC的“控制器”部分基本上用于简化模型的视图查询,除非您将它们耦合在一起),但是总的来说,消息传递是唯一的方法一个系统,子系统之间的耦合为零(或者至少是我所知道的唯一方法)。
Ricket

嗯,所以现在我们开始讨论模式。我听说过这种编程方法。现在,我开始了解什么是模式。在应用程序设计上完全不同。您可以控制对象,同时使用相同的代码来检查每个对象。检查对象后,可以相应地设置其属性。
Bunkai.Satori 2011年

1
“数据结构...”问题有一个惊人的答案。我想说,您选择的答案和随附的Nebula3文章是我所见过的最大规模的游戏引擎架构的最佳描述。
deft_code 2011年

15

在以下情况下,消息通常运行良好:

  1. 发送消息的东西并不关心它是否被接收。
  2. 发送者不需要立即从接收者那里得到响应。
  3. 可能有多个接收者在侦听单个发送者。
  4. 消息将很少或无法预测地发送。(换句话说,如果每个对象每帧都需要获得“更新”消息,则消息并不能真正为您带来很多收益。)

您与该列表匹配的需求越多,适合的信息就会越好。从总体上来说,它们是非常好的。他们做得很好,不会浪费CPU周期,而不会像轮询或其他更具全局性的解决方案那样无所作为。他们非常擅长解耦代码库的各个部分,这总是很有帮助的。


4

到目前为止,除了James和Ricket的好答案之外,我只想补充一点警告。消息传递/事件驱动的通信无疑是开发人员手中的重要工具,但很容易被过度使用。当您拿着锤子时,一切看起来都像钉子,依此类推。

您绝对(100%)应该考虑标题周围的数据流,并充分了解信息是如何从一个子系统传递到另一个子系统的。在某些情况下,消息传递显然是最好的。在其他情况下,一个子系统在共享对象列表上进行操作可能更为合适,而其他子系统也可以在同一共享列表上进行操作;还有很多其他方法

在任何时候,您都应该知道哪些子系统需要访问哪些数据以及何时需要访问它们。这会影响您进行并行化和优化的能力,并帮助您避免在引擎的不同部分纠缠不清时出现的更棘手的问题。您需要考虑最小化不必要的数据复制,以及数据布局如何影响缓存使用率。所有这些都将指导您找到每种情况下的最佳解决方案。

话虽这么说,但我从事过的几乎每款游戏都具有可靠的异步消息传递/事件通知系统。即使面对复杂且快速变化的设计需求,它也允许您编写简洁,有效的可维护代码。


+1可获取良好的附加信息。嗨,Cranky先生,谢谢。根据您的说法,即使对于手机游戏,也非常值得考虑使用事件消息系统。但是,计划不应被忽略,应该在使用消息传递系统的地方进行充分考虑。
Bunkai.Satori 2011年

4

好吧,我知道这个帖子已经很老了,但是我无法抗拒。

我最近建立了一个游戏引擎。它使用3d方库进行渲染和物理处理,但是我写了核心部分,定义和处理实体和游戏逻辑。

发动机肯定遵循传统方法。有一个主更新循环,为所有实体调用更新功能。冲突由实体上的回调直接报告。实体之间的通信是使用实体之间交换的智能指针进行的。

有一个原始消息系统,该系统仅处理一小部分实体以生成消息。这些消息最好在游戏交互结束时进行处理(例如创建或销毁实体),因为它们可能会干扰更新列表。因此,在每个游戏循环结束时,都会消耗一小部分消息。

尽管有原始的消息系统,但我想说该系统很大程度上是“基于更新循环”的。

好。使用该系统后,我认为它非常简单,快速且组织良好。游戏逻辑是可见的,并且自身包含在实体内部,而不像消息队列那样动态。我真的不会让它成为事件驱动的,因为我认为事件系统会给游戏逻辑带来不必要的复杂性,并使游戏代码难以理解和调试。

但是,我也认为像我这样的纯“基于更新循环”系统也存在一些问题。

例如,在某些时刻,一个实体可能处于“什么都不做的状态”,可能正在等待玩家靠近或发生其他事情。在大多数情况下,实体一无所有地消耗处理器时间,最好关闭实体,并在发生特定事件时将其打开。

因此,在我的下一个游戏引擎中,我将采用另一种方法。实体将为引擎操作注册自己,例如更新,绘制,碰撞检测等。这些事件中的每一个将具有用于实际实体的实体接口的单独列表。


您会发现实体成为怪物代码库,而游戏中的任何复杂性都使其难以管理。您最终将它们耦合在一起,将很难添加或更改功能。通过将功能分解为小的组件,可以更轻松地维护和添加功能。然后问题变成这些组件如何通信?答案是来自一个组件的事件,这些组件在传递原始数据到参数中的同时提高了另一个组件的功能。
user441521

3

是。这是游戏系统相互通信的一种非常有效的方式。事件可以帮助您使许多系统脱钩,甚至可以在不知道彼此存在的情况下分别编译事物。这意味着您的类可以更容易地原型化,编译时间也更快。更重要的是,您最终得到的是平面代码设计,而不是依赖混乱。

事件的另一个重要好处是可以轻松地通过网络或其他文本通道流式传输事件。您可以录制它们以供以后播放。可能性是无止境。

另一个好处:您可以让多个子系统侦听同一事件。例如,您的所有远程视图(播放器)都可以自动订阅实体创建事件,并在每个客户端上繁琐的工作就可以生成实体。想象一下,如果不使用事件,将需要做多少工作:您将不得不在Update()某个地方放置调用,或者可能是view->CreateEntity从Game逻辑中调用(其中不包含有关视图的知识以及所需的信息)。没有事件就很难解决这个问题。

有了事件,您将获得一个优雅的,解耦的解决方案,该解决方案支持无限数量的无限种类的对象,这些对象都可以简单地订阅事件并在游戏中发生某些事情时就可以做自己的事情。这就是为什么活动很棒。

更多细节和实现在这里


好点,尽管是单方面的。我总是对通用性表示怀疑:是否有针对事件的用例?
2014年

1
反使用要点:当您绝对必须有直接的反应时。当您要对某个事件执行任何操作时,总是直接在同一线程中执行。当绝对没有外部延迟时,可能会延迟呼叫完成。当您只有线性依赖性时。当您很少同时执行多项操作时。如果您查看node.js,则所有io调用均基于事件。Node.js是事件已100%正确实现的地方。
user2826084 2014年

事件系统不需要是异步的。您可以使用协程来模拟异步,同时在需要时进行同步。
user441521

2

从我所看到的来看,完全基于消息传递的引擎似乎并不常见。当然,有些子系统非常适合此类消息传递系统,例如网络,GUI以及其他。总的来说,我会想到一些问题:

  • 游戏引擎处理大量数据。
  • 游戏必须快(至少20-30 FPS)。
  • 通常,必须知道是否已完成某件事或何时完成某件事。

因此,使用“必须尽可能高效”的通用游戏开发人员方法,这种消息传递系统并不那么普遍。

但是,我同意您应该继续尝试。这样的系统肯定有很多优点,并且今天可以廉价地获得计算能力。


我不知道我是否同意这一点。另一种选择是轮询,这很浪费。仅响应事件是最有效的。是的,仍然会进行一些轮询并引发事件,但是也有很多面向事件的事情。
user441521 '17

我也不同意这一点。由于事件消息传递是为子系统-子系统通信而设计的,因此处理的数据游戏引擎的数量基本上无关紧要。游戏对象不应直接与其他游戏对象通信(尽管对象中的自定义事件处理程序可能是一个例外)。由于子系统的数量相对较少,而且它们的“关注范围”很大,因此在多线程引擎中设计良好的消息传递主干的性能成本可以忽略不计。
伊恩·杨
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.