零基础入门Python游戏
上QQ阅读APP看书,第一时间看更新

2.12 事件处理

在前面的示例程序中,针对窗口事件,我们都只进行了简单的处理,即只响应了pygame.QUIT这一事件。但是通常情况下,大多数游戏程序都需要经常对键盘和鼠标事件进行响应和处理,那么在Pygame中该如何响应和处理这些事件呢?本节将详细介绍Pygame中的事件处理。

与事件处理相关的模块主要有三个:pygame.event、pygame.key和pygame.mouse。

1. Event队列和Event对象

我们知道,无论是键盘按键按下还是鼠标移动,系统都会产生一个对应的Event,而Pygame中的事件处理其实与大多数GUI系统的事件处理无异,都是产生一个个的Event,然后把它们放进Event Queue中(Windows系统称为消息和消息队列),接下来不断对该Event Queue进行读取和处理所读取到的Event。

Pygame中的Event Queue(事件队列)是有容量上限的。在SDL1.2中,它最多只能容纳128个Event,当Event Queue满了之后,新到的Event就会被丢弃。因此,为了保证所有Event都能得到及时的响应和正确的处理,就需要不间断地读取和清空事件队列。

Pygame Event Queue中所存储的Event其实是一个Event对象,类型为pygame.event.EventType,它有两类属性,分别为type和dict(或__dict__)。

type表示Event的类型ID,如QUIT,它就是一种Event类型,除此之外,Pygame还预定义了许多其他Event类型,如MOUSEMOTION、KEYDOWN等。type的取值在NOEVENT与NUMEVENTS之间,其中自定义Event的type应取值USEREVENT及其以上。

dict(或__dict__)是一个字典,它包含了Event对象除type以外的其他所有属性。也就是说,一个Event对象的属性并不止type和dict,可能还有一些其他属性。但是根据事件类型的不同,这些属性也都是不同的,dict包含了每个Event独有的那些属性。

如VIDEORESIZE事件:

执行

返回的是VIDEORESIZE。

执行

返回的值大概是:{'size':(640,480),'w':640,'h':480}。

字典中每个item的key即是该Event对象所专有的其他属性,value是其对应的属性值。可以看出,VIDEORESIZE这个Event对象,它的属性还有size、w和h,它们其实也都可以通过Event对象直接访问,如:

它的返回值为(640,480)。

注意:根据窗口尺寸的不同,上面VIDEORESIZE事件属性的值是不同的,这里给出的只是一个示例。另外,上面代码中的event代表了VIDEORESIZE Event对象。

再如MOUSEBUTTONDOWN事件:

执行

返回的是MOUSEBUTTONDOWN。

执行

返回的值是:{'pos':(295,316),'button':1,'window':None}。

说明该Event对象额外的属性还有pos、button和window,它们所对应的值分别为(295,316)、1和None。属性pos表示当前鼠标光标所处的位置坐标,属性button表示鼠标哪个按键被按下了,1为左键,2为中键,3为右键,最后的window是与系统相关的属性,对于不同的系统,该属性不尽相同,但前面的pos和button属性都是共有的。此处说明鼠标左键在(295,316)坐标处被按下。

我们自然也可以通过Event对象访问它的其他属性,如:

此时,如果还像前面一样访问event.size,那么程序将会出错,因为MOUSEBUTTONDOWN事件对象并没有size属性,size只是VIDEORESIZE事件对象专有的属性而已。

图2-13左半部分直观展示了Event Queue(事件队列)与Event Object(事件对象)的关系。

图2-13 Pygame中的事件队列与事件对象以及预定义事件列表

总而言之,事件队列是存储事件对象的容器,事件对象是组成事件队列的基本元素;每个事件对象都有两类属性,一是type,二是dict(或__dict__),前者代表类型ID,后者包含该类型专有的其他属性。

Pygame中预定义了许多Event对象,它们的type和dict如图2-13右半部分所示,这些事件其实也都是由系统所产生的事件。

在这些预定义的事件对象中,对于用户来说,最常用的事件对象有QUIT、KEYDOWN、KEYUP、MOUSEMOTION、MOUSEBUTTONDOWN、MOUSEBUTTONUP,通过它们便可以很方便地与键盘和鼠标进行交互,它们所附带的一些属性信息,如key、pos、button等,更是为响应键盘和鼠标事件提供了必要的帮助。

事件对象的type属性作为常量,它们都被直接放在了pygame模块的命名空间内,所以我们可以这样访问:

也可以如此访问:

通过from pygame.local simport*这种方式可以只引入Pygame中的所有常量。

前面讲过,USEREVENT也是一种Event类型,用作自定义事件。

其实不止事件对象的type,Pygame中的众多常量都可以通过这两种方式进行访问。

2. 响应键盘与鼠标事件

了解了事件队列和事件对象,现在来看如何响应和处理事件,尤其是键盘和鼠标事件。

其实很简单,把之前示例程序中对事件消息的处理依葫芦画瓢地完善一下就可以了,如下代码所示。

在while循环中不间断地读取事件队列,然后把读取到的事件根据不同的类型分别做不同的处理,这就是Pygame处理事件的最通用的方法。

pygame.event模块中的get()函数用于从事件队列中读取事件对象,并把读取到的事件对象从事件队列中删除,该函数返回的是事件队列中的所有事件对象的列表,所以这里我们使用for循环遍历get()函数的返回结果。

从事件队列中读取事件对象也可以使用pygame.event.poll()或者pygame.event.wait()函数,与pygame.event.get()不同的是,它们一次只返回一个事件对象。但无论使用哪个函数,它们都会把读取到的事件对象从事件队列中删除。前面讲过,事件队列的容量是有限的,为了保证所有事件都能得到及时的响应和正确的处理,需要不间断地读取和清空事件队列,所以这些函数都恰如其分地保证了程序的正常运行。

除了以上提到的3个函数,pygame.event模块还提供了一些其他函数用来管理事件队列和事件对象,这里就不一一介绍了,后面会在使用到的地方顺带讲解。

获取了事件对象event,我们就可以利用它的众多属性(如type、dict、pos、button、key等)对程序做更进一步的处理了。所以使用以上方法,只要从事件队列中捕获到KEYDOWN、KEYUP、MOUSEMOTION、MOUSEBUTTONDOWN、MOUSEBUTTONUP事件,就可以对键盘和鼠标进行响应处理了。

除了上述方法,Pygame其实还提供了另外一种方法用来响应键盘和鼠标输入,该方法无须访问事件队列和事件对象,只需要检查判断输入设备(键盘和鼠标)的状态即可,pygame.key与pygame.mouse这两个模块提供了一些函数,可以用来查询它们的状态。

如下示例。

还是在while循环中不间断地通过pygame.mouse和pygame.key模块读取鼠标和键盘的状态,然后根据读取到的状态做不同的处理,这是Pygame处理鼠标键盘事件的另一种方法。

读取鼠标状态使用到的函数主要为pygame.mouse.get_pressed()和pygame.mouse.get_pos()。前者返回的是鼠标各个按键被按下情况的列表(元组),如(1,0,0)代表鼠标左键被按下,(0,1,0)代表鼠标中键被按下,(0,0,1)代表鼠标右键被按下;后者返回的是鼠标光标当前所在的位置坐标(x,y),参考原点为窗口左上角。

读取键盘状态使用到的函数主要是pygame.key.get_pressed(),它返回的也是一个序列表,代表键盘各个按键被按下的情况。如果某个按键被按下,那么该按键在序列表对应位置处的元素数值将为1,否则为0。使用按键ID作为index可以访问该按键的状态,如keys[K_a]为1表示键盘上的A键被按下。

注意:无论是pygame.mouse.get_pressed()还是pygame.key.get_pressed(),它们返回的都是当前函数被调用之时的按键状态,所以当按键过快或者检测不够及时时,有可能会有遗漏发生,从而使个别按键在被按下时检测不到,这一点就决定了这两种响应键盘鼠标事件方式的最主要的区别所在。

通过第一种事件队列模型响应键盘鼠标的方式能够精准捕获到所有键盘和鼠标事件,但是可能会不够实时;而通过第二种查询输入设备状态的方式可以做到迅速响应当前按键的情况,但是却不够精准,存在遗漏的状况。

除此之外,使用第二种方式还可以很方便地判断两个按键是否被同时按下,这在第一种方法中稍显困难。所以说,这两种响应键盘鼠标的方式各有利弊,需要酌情使用。对于要求精准性而对实时性要求不高的程序,应该使用第一种方式;对于要求实时性而对精准性要求不高的程序,应该使用第二种方式。不过,在程序中也可以把这两种方法混合起来使用,以充分利用它们各自的优势。

除了前面用到的几个函数,pygame.key和pygame.mouse这两个模块下各自也都提供了其他有用的函数,这里不一一介绍了,会在后面用到的地方顺带讲解。

再回过头看上面的代码,查询鼠标键盘状态之前,我们调用了pygame.event.pump()函数。此函数放在这里用来自动处理事件队列,保证了当前程序与操作系统的交互,从而不至于导致当前程序被系统锁定挂起。这里的pump()函数也可以用pygame.event模块中的其他函数替代,如get()、wait()、poll()、clear(),其实这就是要求程序每帧都要与事件队列进行一定的交互,从而保证程序得以正常运行。

最后说明一下类似K_q、K_a、K_b、K_c的常量,它们代表了键盘按键标识,如同MOUSEBUTTONDOWN、MOUSEMOTION、KEYDOWN一样,它们也都是在pygame模块的命名空间中定义的,可以通过pygame.K_q这种方式访问,也可以在from pygame.locals import*之后直接对常量名进行访问。很容易理解,K_q表示键盘上的Q键,K_a表示键盘上的A键,以此类推。Pygame为键盘上的每个按键都定义了这样的按键常量,它们的详细名称请参阅Pygame官方文档。

3. 自定义事件

无论是MOUSEMOTION、MOUSEBUTTONDOWN、MOUSEBUTTONUP、KEYDOWN、KEYUP还是QUIT,这些都是系统预定义的Event,它们由系统自动产生和发送,但是有些时候我们需要自定义事件,那么该如何做呢?

pygame.event模块提供了用来创建Event对象和发送Event对象的函数,代码如下。

首先定义自定义事件的类型,让它取值为USEREVENT或者以上,不过不能超过NUMEVENTS;然后使用Event()函数创建Event对象,该函数的第一个参数为事件类型type,后面所有的参数用来设置事件的dict属性,可以传递一个字典,也可以传递若干关键字参数,它返回的是一个pygame.event.EventType类型的Event对象;最后使用post()函数把该事件对象发送到事件队列中。

在以上代码中,我们自定义了一个MY_EVENT类型的事件对象event,并让该事件对象附带两个属性attri1和attri2,值分别为attribute1和attribute2。通过post()函数发送出去之后,我们就可以在事件队列中检测到该事件了。该事件对象有三个属性,分别为:event.type等于MY_EVENT、event.attri1等于attribute1、event.attri2等于attribute2。当然,自定义事件的dict属性是可以随意设置的。

上面的方法一次只能发送一次事件,如果想让事件能够定时产生发送,那么应该怎么实现呢?就像下面这样:

使用pygame.time模块的set_timer()函数即可实现事件的定时发送,该函数能够定时产生事件并把它塞到事件队列中,它的第一个参数代表事件类型type,第二个参数代表事件所产生的时间间隔,单位为毫秒(ms),如果为0,则表示取消定时发送。

注意:使用这种方法并不能指定事件对象的dict属性。

以上代码会每隔100ms产生发送一次MY_EVENT事件。

检测处理自定义事件与系统预定义事件的方法无异,也是通过pygame.event.get()/pygame.event.poll()/pygame.event.wait()函数查询事件队列。

关于pygame.time模块,在前面小节中,我们用到了它的clock()函数,这里又用到了set_timer()函数,这其实也是一个比较常用的模块,不过鉴于比较简单,就不独辟章节进行介绍了,会在用到的地方顺便讲解。

4. 具体示例

最后是两个完整的示例程序,分别用前面提到的两种不同方法响应键盘和鼠标。

(1)用事件队列模型的方法,代码如下。

执行效果如图2-14所示。

图2-14 通过事件队列模型响应处理事件示例程序

窗口上有3行文字:第1行文字显示的是Event对象的type信息,第2行文字显示的是Event对象的dict属性信息,第3行文字显示的是关于该Event对象其他的一些附加信息。

除了窗口中看到的这些,该程序中还有一些其他功能,如按下Q键时程序退出;按下Space键时发送自定义事件MY_EVENT1;按下E键时定时发送自定义事件MY_EVENT2;按下D键时停止发送自定义事件MY_EVENT2。此外,当键盘按键被按住不放时,出现在窗口第3行上的数字会不断增加,在图2-14中,这个数字为119,说明C键被按住了许久。

下面解释上面代码中新出现的一些函数。

默认情况下,当键盘上某个按键被按下时,无论按多久,都只产生一次KEYDOWN事件,而set_repeat()函数则可以在这种情况下重复产生KEYDOWN事件,也就是说,只要按住按键不放,就会有多个KEYDOWN事件产生,这在程序中有时是非常有用的。该函数的第一个参数表示第一个KEYDOWN事件到来之前的时间延迟,第二个参数表示每两个KEYDOWN事件之间的时间间隔,单位均为毫秒(ms)。

该函数可以把用整型表示的键盘按键常量转换成直观的字符串表示。

该函数可以把用整型表示的事件类型常量转换成直观的字符串表示。

(2)用读取输入设备状态的方法,代码如下。

执行效果如图2-15所示。

图2-15 通过读取输入设备状态响应处理事件示例程序

此段程序比较简单,不做过多介绍。