游戏编程模式之事件队列模式
对消息或事件的发送与受理进行时间上的解耦。
(摘自《游戏编程模式》)
事件队列模式维护着一个事件队列。元命令入队意味着我们主观想要立即想要执行该命令的相关操作;而出队意味着响应执行该命令相关的操作。受限于各种硬件和软件的情况,这些命令执行并不是立即响应的,但顺序是固定的、也不会出现命令遗漏的情况。我们使用事件队列模式,很重要的目的就是序列唯一且不会遗漏。这一模式适合管理对实时要求不算高的命令响应。
和这以模式类似的有:观察者模式和命令模式。但是他们之间的思考方向和解决的问题是不一样的。观察者模式、命令模式是对发送事件方和接受(执行)事件方的解耦;事件队列模式是某一个问题上对时间(执行顺序严格但不要求严格实时)进行解耦。
既然都画出了自己理解的三个模式的图示,那么还是有必要介绍一下我对它们的理解。
- 命令模式。在命令模式中,一个命令绑定的这一个操作。在执行命令时需要传入执行对象,针对不同的执行对象执行相同的操作。
- 观察者模式。在观察者模式中,一个命令绑定着或多个操作(也可以按照观察者模式的说法:一个或多个操作订阅某一个命令),观察者发出执行某命令的指令时,多个绑定的操作则会一起执行。
- 事件队列模式。命令(即事件)包含了执行对象和操作的信息,当程序要执行某一个命令,则将该命令入队,并等待出队后的执行。
示例
我们实现一个[音频播放事件队列]作为事件队列模式的示例。下面是一些细节信息:
-
播放音效需要三个步骤
-
根据soundId获得资源
-
判断当前是否可用的音频信道(硬件是否符合播放的条件)
-
获得可用音频信道后调用播放函数
以下为相关代码
-
-
事件队列的逻辑结构为——环状缓冲区队列,相较于线性表,使用环状缓冲区的好处为出队操作队内元素不用移动。
class AudioPlayCommand
{
SoundId id;
float volume;
}
class AudioSystem
{
public:
static init()
{
head=0;
tail=0;
}
//此方法没有立即执行播放,而是使用队列模式——先入队,让事件队列处理
void PlaySound(SoundId id,float volume)
{
//断言事件队列是否已经满了
assert((tail+1)%MAX_SIZE!=HEAD);
commandQueue[tail].id=id;
commandQueue[tail].volume=volume;
tail=(tail+1)%MAX_SIZE;
}
//此函数将在游戏循环中(直接或间接)调用
void Update()
{
//队列中无命令添加
if(head==tail) return;
Resource music=Sound.FindById(commandQueue[head].id);
int chanel=AudioSystem.FindOpenChannel();
//播放音频的条件还没有达到
if(channel==-1)
return;
//条件达到,播放音乐
StartSound(music,chanel,commandQueue[head].volume);
head=(head+1)%MAX_SIZE;
}
//找打开的信道
int FindOpenChannel()
{
//To Do...
}
private:
//环状缓冲区的头索引和尾索引
static int head;
static int tail;
const static int MAX_SIZE=20;
static AudioPlayCommand commandQueue[MAX_SIZE]; //命令队列
}
注意:
事件队列在不同线程中请求入列操作时,要注意同步的问题。