目录
事件队列也叫消息队列,是更广为人知的观察者模式的异步实现。
一、意图
解耦发出消息或事件的时间和处理它的时间。
二、动机
一些常见的场景:
1.GUI事件循环
当用户与程序交互时,操作系统就会生成一个事件。程序需要去获取这些事件,然后做出相应的反应。而操作系统生成的事件,并不会去主动调用你的程序的代码。而是存在一个地方,这个地方就是事件队列。
2.中心事件总线
在大多数游戏中使用事件循环来支撑中枢系统是常见的。
比如:想做个新手教程,当玩家在打到第一个敌人时xxxxx。就可以使用中心事件队列。任何游戏系统都可以发送事件给队列。任何游戏系统也都能从队列中接收事件。教程系统只需要在队列中注册自己,并表明需要收到“敌人死亡”事件。就可以把消息从战斗系统传到教程引擎。
3.播放声音的事件
如果在播放声音的按钮下,直接调用播放声音的函数。而声音函数需要加载对应音源,就会出现以下问题。
1.API在音源引擎完成对请求的处理前阻塞了调用者。
有时屏幕会被冻住几帧。
2.请求无法合并处理。
当需要在打到怪物时就播放一个怪物哀嚎的音效,在同帧打到多个怪物时,就会播放多个相同的音效。而这时两个相同的波形的相加,就是一个声音的两倍响,就会很刺耳。
3.请求在错误的线程上执行
音频引擎不一定在收到这个请求时,就能立即去处理。但是这种简单的做法没有给它选择。
事件队列就可以很好的处理这些问题。
三、事件队列
1.事件队列的描述
事件队列在队列中按先入先出的顺序存储一系列的时间。发送的系统,将请求放入队列,立即返回。处理请求的系统之后从队列中获取请求并处理。
就解耦了发送者和接收者,既静态又及时。
2.合适使用的场景:
观察者模式和命令模式都可以解耦接受者和发送者。
但是在解耦某些需要及时处理的东西时使用队列。因为这里比其他模式多设置了缓存。这样可以在处理方方便时将请求拉入。而队列可以及时返回发送方,不堵塞发送方的流程。也可以在事件队列中处理合并。
注意这里发送者的操作只是发送了事件,不会收到回复。所以在发送者需要收到回复时,队列不是好的选择。
3.注意事项
i.中心事件队列
中心事件队列是一个全局变量。程序每个部分都可以解除到其中的一小个状态。有全局变量引发的全部危险。
ii.事件的处理可能并不及时
这个事件处理时可能已经是几帧后了,如果需要结合当时现场的一些信息才能处理的事件。就不能让处理方自己再去观察场景,因为可能已经改变了。就需要在发送的时候,将一些细节一起发送出去,方便使用。
iii.避免陷于反馈系统环路中
A发送事件,B接收然后发送事件作为回应,A又关注了事件并回应。然后B又。。。。
同步的消息系统会让栈溢出游戏崩溃。使用队列,就会异步地使用栈,这些无用的事件不会阻碍游戏继续运行。难以找到问题。
解决:避免在处理事件的代码中发送事件。
4.实例代码
事件队列可以使用:Fibonacci heap或者skip list或者最起码链表。 但是在实践中,存储一堆同类事物最好的办法是使用一个平凡无奇的经典数组:
i.没有动态分配。
ii.没有为记录信息造成的额外的开销或者多余的指针。
iii.对缓存友好的连续存储空间。
示意为声音系统的一个消息队列:使用数组在存储消息。将数组大小设置为最糟情况下的大小。
简单的处理方:这里的处理时立即返回的,然后在Updata函数中播放声音
5.环状缓存
环状缓存保留了数组的所有优点,同时能不断从队列的前方移除事物。
这里可以使用链表,也可以使用数组。相比之前的 numPending。取代他的是head_和tail_
,一个指向正需要被处理的事件位置,一个指向事件队列空闲的下一个。并且这里的环状体现在当尾部指针指向了数组的尾部时,能从尾部再回到头部。那些已被处理的空闲事件位置。
折回的实现:
这里的断言保护了,不会队列满了,新消息覆盖未处理的旧消息的情况。
6.处理合并
需要在请求入队时合并。
7.分离线程
队列应该与程序其他部分隔离。避免其他程序的调用阻塞了事件队列的正常运行。