Python pygame(GUI编程)模块最完整教程(1)

提示:下滑文章左侧可以查看目录!

1 初识pygame

1.1 简介

pygame是python中一个流行的GUI编程模块,是专门为了开发游戏而设计的。这是一个第三方模块,是SDL语言和Python的接口。

pygame的最新官网是:https://pyga.me/

pygame以前的官网是:https://www.pygame.org/

1.2 pygame的优势

pygame的核心功能使用的是C代码,大大提高了运行的速度。

pygame支持在大部分操作系统运行,可跨平台,在经过编译后可以在Andriod手机和网页上运行。

pygame十分简单而且易于掌握,自由性强。

pygame功能全面。其支持的功能包括:图片、文字、绘图、OpenGL 3D、音频、摄像头、游戏手柄等。

1.3 安装pygame

pygame是一个第三方模块,这意味着需要在安装后才能使用。

使用pip工具就可以安装,在cmd输入后按下回车,稍等一会儿:

pip install pygame-ce

注意:此处的pygame-ce是pygame官方的社区编辑版,推荐使用。普通版的pygame可用pip install pygame安装

安装完成后,尝试导入pygame,会打印出一段文字,提示pygame的版本:

 在本教程中,使用的是较新版的pygame2。

1.4 pygame子模块

pygame的许多功能都定义在不同的子模块里面。下面列举了常用的子模块。读者可以了解一下。

模块名描述
camera操作系统摄像头
cursors加载、编译光标图像
display配置pygame的显示表面
draw在表面上绘制形状
event管理用户事件(如键盘、鼠标)
font加载和绘制TrueType字体
gfxdraw绘制抗锯齿的形状
image加载、保存图片文件
joystick管理游戏手柄
key管理键盘输入
locals此模块储存所有的pygame常量
mixer播放声音
mouse管理光标位置和事件
scrap管理剪贴板
sndarray处理声音样本数据
sprite管理精灵
surfarray处理图像像素数据
time管理时间和帧率
transform变换表面(如缩放、旋转等)

注:::pygame还增加了许多功能,作者后期会进行补充

pygame中一些常用的模块会自动导入,所以大部分模块无需额外导入。

1.5 表面

pygame可以绘制一些图片、文本。为了在窗口上显示它们,pygame提供了一系列的方法来导入、渲染。但在这之前,所有的绘制内容都会转化成一个pygame.Surface对象(表面对象,可以理解成一张图片),这样才能进行绘制。表面由多个像素构成,是一个矩形。pygame也提供了一些方法处理表面。

整个pygame窗口除去标题栏的部分可以按照一个表面来操作。在操作表面的时候,比如要在某个地方绘制一段文字,必然会涉及到坐标的处理。在pygame中,最简单的一种坐标表示方式是使用一个形如(x, y)的可迭代对象,如(0, 0), (100, 50)等等。

pygame的坐标系中,原点(0, 0)在左上角,x轴正方向向右,y轴正方向向下。

2 第一个pygame示例

import pygame as pg #导入pygame模块,通常为了简便而命名为pg

pg.init() #初始化

screen = pg.display.set_mode((400, 400)) #建立一个400x400的窗口
pg.display.set_caption("Pygame窗口")

while True:
    for event in pg.event.get(): #获取用户事件
        if event.type == pg.QUIT: #如果事件为关闭窗口
            pg.quit() #退出pygame

接下来我将对这一段代码进行解释。

2.1 初始化pygame

pygame使用前,首先要进行初始化操作,也就是调用pygame.init()方法。如果不进行初始化操作,大部分功能将无法使用,会显示一段错误提示:

pygame.error: video system not initialized

除了使用pygame主模块,使用pygame的子模块有时候也需要初始化。pygame中有一些重要的子模块被提前导入到pygame主模块中,这样的模块的初始化操作会在调用pygame.init()的时候同时进行。

2.2 创建窗口

pygame.display.set_mode用于配置pygame窗口。pygame是单窗口的模式,这意味着在一个python解释器中默认只允许创建一个窗口。pg.display.set_mode创建一个窗口的Surface对象,原形如下:

set_mode(size=(0, 0), flags=0, depth=0, display=0, vsync=0) -> Surface

size参数指定了窗口尺寸,如(200, 300)是一个宽为200,高为300的窗口。关于这个函数的更多用法参见后文。

前面已经介绍过,set_mode返回窗口的Surface(表面)对象,可以和普通表面一样操作它。

如果在后面想要更改窗口的样式,也可以调用set_mode方法,不过那时就不会额外创建一个窗口了,而是直接在原先创建的窗口上更改。

2.3 更改标题

pg.display.set_caption方法用于设置窗口的标题。

set_caption(title) -> None

2.4 事件循环

接下来,代码进入了一个while True的无限循环,这个循环通常被称作事件循环。 在这个循环中不断调用pg.event.get()来刷新窗口,处理用户事件,如键盘按下、窗口拖拽、鼠标点击等。pg.event.get()将返回一个事件列表,通常遍历这个列表,与一些事件进行比对,来判断用户在窗口上进行了什么操作。列表中每一项都是一个pg.event.Event对象,存储了事件的信息。Event对象的type属性是这个事件类型的标识符(一个整数),如果和pg.QUIT事件类型的标识符匹配,那么就说明用户按下了关闭窗口的按钮。

2.5 退出pygame

和初始化pygame的init()方法相对,pygame.quit()方法用于退出pygame窗口。

3 基础表面操作

通过上面一个示例的学习,我们已经成功创建出一个纯黑色的窗口,但是这还远远不够,我们还要在窗口上绘制图片、图形、文字。在1.5节中,我们已经了解过pygame.Surface,知道pygame的图片、窗口表面都是Surface对象,接下来将进一步讲解操作的方式。

3.1 fill()方法

Surface.fill方法用于将一个表面用纯色填充。在pygame中,颜色的表示方式通常有两种:(R, G, B)色彩元组或颜色名称(例如”red”, “yellow”)的字符串(也可以通过pg.color.Color对象,这里暂时不展开)。

import pygame as pg #导入pygame模块,习惯上命名为pg

pg.init() #初始化

screen = pg.display.set_mode((400, 400)) #建立一个400x400的窗口
pg.display.set_caption("Pygame窗口")

while True:
    screen.fill((255, 255, 255)) #将窗口用白色填充

    for event in pg.event.get(): #获取用户事件
        if event.type == pg.QUIT: #如果事件为关闭窗口
            pg.quit() #退出pygame
    
    pg.display.flip() #刷新窗口(很重要!!)

这个示例的大部分代码和我们的第一个示例一样,但是在while True循环中添加了一些代码,运行效果如下:

可以看到窗口变成了(255, 255, 255)的颜色(纯白)。

需要注意的是最后一行代码pg.display.flip(),这行代码用于更新窗口表面(窗口表面通常叫做屏幕)。在屏幕上进行的一系列操作如果不进行刷新,则无法正确在屏幕上显示。这是窗口表面的一个特殊之处。新手一定要注意:不要忘记刷新!除了flip方法,其实还有一个update()方法也可以用于刷新,但是flip速度会快一点。

那么,为什么fill要写在循环里面呢?其实写在循环外面,然后调用一次刷新也是可以的。但是我们的目标是用pygame最终实现动态的游戏界面。在pygame中,实现动态的方式是不断用新的表面操作去覆盖以前的表面操作,这些在后面会讲到。

3.2 载入图片

pg.image模块提供了一些图片导入的操作。pg.image.load方法从给定的电脑路径载入图片,并返回该图片的Surface对象。pygame支持导入的图片格式有:

  • BMP
  • GIF(无动画)
  • JPEG
  • LBM, PBM, PGM, PPM
  • PCX
  • PNG
  • PNM
  • SVG(仅Nano SVG)
  • TGA(无压缩)
  • TIFF
  • WEBP
  • XPM
pg.image.load(filename) -> Surface

示例:

image_surf = pg.image.load("xxx.png")

注意:pygame支持的路径包括绝对路径和相对路径。

学会载入图片之后,接下来需要将它绘制到屏幕上。

3.3 blit()方法

Surface.blit()方法可以在当前表面上的指定位置绘制另一个表面。

Surface.blit(source, dest, area=None, special_flags=0) -> Rect
# Rect参见下一节
import pygame as pg 

pg.init()

screen = pg.display.set_mode((400, 400))
image = pg.image.load("logo.png") #载入图片

while True:
    screen.fill((255, 0, 0)) # 用红色填充屏幕
    screen.blit(image, (50, 50)) #绘制图片,使图片左上角位于(50, 50)的位置

    for event in pg.event.get():
        if event.type == pg.QUIT:
            pg.quit()

    pg.display.flip()

由于图片过大,绘制并不完整。经过blit绘制后,图片的左上角位于(50, 50)位置。

3.4 Rect对象

上面示例中,我们将图片绘制在了(50, 50)处(左上角位置)。假如要让图片居中显示,坐标的计算将会比较麻烦,这时候我们可以获取图片的矩形对象,然后对图片的矩形对象进行处理。

图片的矩形对象记录了表面的宽、高。同时,矩形对象还可以表示图片的位置信息。下面是一个基础的创建矩形对象的过程。

rect = pg.Rect((50, 50, 400, 113))
rect = pg.Rect(50, 50, 400, 113)
rect = pg.Rect((50, 50), (400, 113))
# 注:上个示例中logo.png大小是400x113

pg.Rect支持四个数的一个元组,也可以是两个数两个数或者四个分开的数值。但是必须按照x坐标,y坐标,宽,高的顺序指定参数。上面建立的Rect对象,其实就表示了上一节示例中图片的大小和绘制方位。

Rect对象可以被传递给blit作为绘制位置的参数,如:

rect = pg.Rect((50, 50, 400, 113))
screen.blit(image, rect)

这时候,图片仍然会被绘制在(50, 50)的位置。

注意:调用blit方法的时候只关注rect的x, y坐标,而不会关注rect所指定的宽和高。所以无论是pg.Rect((50, 50, 400, 113))还是pg.Rect((50, 50, 1, 1))都不会影响图片绘制的结果。

如果仅仅支持以上的功能,那是不可能的。下面介绍pg.Rect最实用的功能:根据锚点调整和获取位置。

实例化一个Rect对象后,这个Rect有一些虚拟参数,可以用于调整和获取位置:

属性解释
x, y表示矩形左上角的x或y坐标(整数)
top, left, bottom, right表示矩形顶端的y坐标,左端的x坐标,底端的y坐标,右端的x坐标(整数)
topleft, bottomleft, topright, bottomright表示矩形左上角的坐标,左下角的坐标,右上角的坐标,右下角的坐标(元组)
midtop, midleft, midbottom, midright表示矩形顶端中点的坐标,左端中点的坐标,底端中点的坐标,右端中点的坐标(元组)
centerx, centery表示矩形中点的x坐标或y坐标(整数)
center表示矩形的中点坐标(元组),相当于(centerx, centery)
width, height, w, h表示矩形的宽或高(整数),width可以简写成w,height可以简写为h
size表示矩形的宽高(元组),相当于(width, height)

用示意图表示:

示例如下:

import pygame as pg 

pg.init()

screen = pg.display.set_mode((400, 400))
image = pg.image.load("logo.png") #载入图片
image_rect = pg.Rect((0, 0, 400, 113))
image_rect.center = (200, 200) #使image_rect的中点位于屏幕中心

while True:
    screen.fill((255, 0, 0))
    screen.blit(image, image_rect)

    for event in pg.event.get():
        if event.type == pg.QUIT:
            pg.quit()

    pg.display.flip()

可以看到,图片成功居中显示(中点设置在了(200, 200)的位置)。

同理,如果想要让图片靠着顶端显示,可以这样写:

image_rect.midtop = (200, 0)

如果想要图片靠着左上角显示,可以这样写:

image_rect.topleft = (0, 0)
# 或者:
# image_rect.x = image_rect.y = 0
# 或者:
# image_rect.top = image_rect.left = 0

讲到Rect,不得不提到的是Surface对象的get_rect()方法。这个方法返回Surface对象的矩形,这让我们无需记住图片的宽高就能指定位置。如果用get_rect方法上面的代码应为:

import pygame as pg 

pg.init()

screen = pg.display.set_mode((400, 400))
image = pg.image.load("logo.png") #载入图片
image_rect = image.get_rect() # 获取图片的矩形,相当于image_rect = pg.Rect((0, 0, 400, 113))
image_rect.center = (200, 200) #使image_rect的中点位于屏幕中心

...

注意:get_rect()返回的Rect对象中,(x, y)默认为(0, 0)。因为Surface是不会记录你绘制的位置的。即使你将Surface绘制在了某个地方,也与新获取的Rect无关。返回的Rect对象中只会记录Surface的宽和高。

get_rect方法还支持一些参数,可以更快捷地修改矩形的位置,如:

image_rect = image.get_rect(center=(200, 200))

3.5 实现动画

在了解矩形对象之后,你应该知道如何移动矩形了,那么下一步将是实现动画。

如果要将一个矩形右移1个像素,那么也就是将矩形的x坐标增加,即:

image_rect.x += 1
# 也可以是image_rect.left += 1等等,但是常用x而不是left, right这样的数值
import pygame as pg 

pg.init()

screen = pg.display.set_mode((400, 400))
image = pg.image.load("logo.png") #载入图片
image_rect = image.get_rect() #默认位于左上角(topleft=(0, 0))

while True:
    screen.fill((0, 0, 0)) #填充为黑色
    screen.blit(image, image_rect)
    image_rect.x += 1
    image_rect.y += 1

    for event in pg.event.get():
        if event.type == pg.QUIT:
            pg.quit()

    pg.display.flip()

运行程序后,可以看到图片很快地移动,最后移出了黑色的屏幕。

这里需要注意的是screen.fill((0, 0, 0))这一行代码。为什么每次一定要进行填充呢?如果不填充会如何?在之前没有使用动画时,如果不进行填充,那么屏幕的颜色始终是默认的黑色,屏幕上绘制了一个不动的图片。但如果是动态的画面,绘制下一张图片时位置发生了改变,但是前面绘制的结果仍然保留在屏幕上,最后就会出现这样的画面:

可以看到,屏幕上出现了很多个logo.png的图片,这是由于没有清除掉之前绘制的痕迹导致的。想要快速清屏,最好的方法是使用fill方法,用纯色填满整个屏幕,然后再绘制新的内容。pygame实现绘制就是这么简单,不断地清除之前绘制的内容,再用新的内容覆盖。

注意:不用担心这个绘制过程很卡。电脑的计算速度是很快的,每次绘制时其实只是替换了一部分位置的像素,并不会记录之前绘制的内容。

接下来又遇到了一个问题:图片移动速度太快了,有办法解决吗?像素是只能为整数的,所以不能让图片只移动0.5个像素或让图片移动1.5像素。如果让Rect以浮点数个像素运动,Rect会先将你给定的浮点数取整,再进行计算。所以0.5像素相当于根本没有移动,1.5像素相当于只移动了1个像素。

关于这个问题的解决方案,将在第5章给出。

4 事件控制

参考资料:https://pyga.me/docs/ref/event.html

4.1 事件类型

事件是指用户在窗口上进行的一系列操作,它们大致可以分为:系统操作类、鼠标操作类、键盘操作类、游戏手柄操作类、窗口操作类、自定义事件等。这些事件都是一个pg.event.Event对象,每个事件都有一些不同的属性。Event对象的共同属性是type,可以用于区分事件的类型。

下面列举了一些常用的事件(关于更多事件参见上面的参考资料):

事件解释属性
QUIT退出
ACTIVEEVENT窗口获得或失去焦点gain(焦点状态), state(焦点事件类型)
KEYDOWN键盘按下key(按键码), mod(按键修饰符), unicode(按键的unicode值), scancode
KEYUP键盘松开key, mod, unicode, scancode
MOUSEMOTION鼠标在窗口上移动pos(鼠标位置), rel(相对于上次鼠标位置的坐标差), button(按键情况,是一个形如(0, 0, 0)的元组,分别表示是否按下左、中、右键), touch
MOUSEBUTTONDOWN鼠标按下pos(鼠标位置), button(按键情况), touch
MOUSEBUTTONUP鼠标松开pos, button, touch
MOUSEWHEEL鼠标滚动which, flipped, x, y, touch, precise_x, precise_y
JOYAXISMOTION游戏手柄的轴移动instance_id(游戏手柄标识符), axis, value
JOYBALLMOTION游戏手柄的球移动instance_id, ball, rel(相对移动距离(x, y))
JOYHATMOTION游戏手柄的帽子移动instance_id, hat, value
JOYBUTTONUP游戏手柄按钮松开instance_id, button
JOYBUTTONDOWN游戏手柄按钮按下instance_id, button
VIDEORESIZE窗口调整大小size, w, h
VIDEOEXPOSE窗口部分公开
USEREVENT触发用户事件
TEXTEDITING文本编辑(输入中文的时候会先显示拼音提示,即为文本编辑)text(文本), start(光标位置), length
TEXTINPUT实际文本输入内容text
DROPFILE拖拽文件进入窗口file
DROPTEXT拖拽文本进入窗口text

在pygame2中,又添加了以下WINDOW前缀的以下事件:

事件解释
WINDOWSHOWN窗口显示
WINDOWHIDDEN窗口隐藏
WINDOWMOVED窗口被移动
WINDOWSIZECHANGED窗口大小被修改
WINDOWMINIMIZED窗口最小化
WINDOWMAXMIZED窗口最大化
WINDOWRESTORED窗口还原(恢复)
WINDOWENTER鼠标进入窗口
WINDOWLEAVE鼠标离开窗口
WINDOWFOCUSGAINED窗口获取焦点
WINDOWFOCUSLOST窗口失去焦点
WINDOWCLOSE窗口被关闭

4.2 键盘事件

参考资料:详解Python中Pygame键盘事件_python_脚本之家

下面的示例演示了键盘事件的使用方法。

import pygame as pg 

pg.init()

screen = pg.display.set_mode((400, 400))
image = pg.image.load("logo.png")
image_rect = image.get_rect()

while True:
    screen.fill((0, 0, 0))
    screen.blit(image, image_rect)

    for event in pg.event.get():
        if event.type == pg.QUIT:
            pg.quit()
        elif event.type == pg.KEYDOWN: #按下按键
            if event.key == pg.K_LEFT: #按下方向键(左)
                image_rect.x -= 3
            elif event.key == pg.K_RIGHT:
                image_rect.x += 3

    pg.display.flip()

运行后,你可以用左右方向键控制图片的移动。 用户按下按键未松开时就会移动图片,如果想要用户松开按键时才进行移动,可以用KEYUP事件替换KEYDOWN。

注意:KEYDOWN检测的并不是持续按下按键,它只在用户按下按键的一瞬间触发。如果用户持续按下这个键没有松开,KEYDOWN事件不会触发多次,而是只触发一次。

除了这个示例中的K_LEFT, K_RIGHT,pygame还提供了一部分常量用于比较按键事件的key属性。

常量解释
K_BACKSPACE退格键(Backspace)
K_TAB制表键(Tab)
K_CLEAR清除键
K_RETURN回车键(Enter)
K_PAUSE暂停键 (Pause)
K_ESCAPE退出键(Escape)
K_SPACE空格键 (Space)
K_EXCLAIM感叹号
K_QUOTEDBL双引号
K_HASH井号
K_DOLLAR美元符号
K_AMPERSANDand 符号
K_QUOTE单引号
K_LEFTPAREN左小括号
K_RIGHTPAREN右小括号
K_ASTERISK星号
K_PLUS加号
K_COMMA逗号
K_MINUS减号
K_PERIOD句号
K_SLASH正斜杠
K_00
K_11
K_22
K_33
K_44
K_55
K_66
K_77
K_88
K_99
K_COLON冒号
K_SEMICOLON分号
K_LESS小于号
K_EQUALS等于号
K_GREATER大于号
K_QUESTION问号
K_AT@ 符号
K_LEFTBRACKET左中括号
K_BACKSLASH反斜杠
K_RIGHTBRACKET右中括号
K_CARET脱字符
K_UNDERSCORE下划线
K_BACKQUOTE重音符
K_aa
K_bb
K_cc
K_dd
K_ee
K_ff
K_gg
K_hh
K_ii
K_jj
K_kk
K_ll
K_mm
K_nn
K_oo
K_pp
K_qq
K_rr
K_ss
K_tt
K_uu
K_vv
K_ww
K_xx
K_yy
K_zz
K_DELETE删除键(delete)
K_KP00(小键盘)
K_KP11(小键盘)
K_KP22 (小键盘)
K_KP33(小键盘)
K_KP44(小键盘)
K_KP55 (小键盘)
K_KP66 (小键盘)
K_KP77 (小键盘)
K_KP88 (小键盘)
K_KP99 (小键盘)
K_KP_PERIOD句号(小键盘)
K_KP_DIVIDE除号(小键盘)
K_KP_MULTIPLY乘号(小键盘)
K_KP_MINUS减号(小键盘)
K_KP_PLUS加号(小键盘)
K_KP_ENTER回车键(小键盘)
K_KP_EQUALS等于号(小键盘)
K_UP向上箭头(up arrow)
K_DOWN向下箭头(down arrow)
K_RIGHT向右箭头(right arrow)
K_LEFT向左箭头(left arrow)
K_INSERT插入符(insert)
K_HOMEHome 键(home)
K_ENDEnd 键(end)
K_PAGEUP上一页(page up)
K_PAGEDOWN下一页(page down)
K_F1F1
K_F2F2
K_F3F3
K_F4F4
K_F5F5
K_F6F6
K_F7F7
K_F8F8
K_F9F9
K_F10F10
K_F11F11
K_F12F12
K_F13F13
K_F14F14
K_F15F15
K_NUMLOCK数字键盘锁定键
K_CAPSLOCK大写字母锁定键
K_SCROLLOCK滚动锁定键
K_RSHIFT右边的 shift 键
K_LSHIFT左边的 shift 键
K_RCTRL右边的 ctrl 键
K_LCTRL左边的 ctrl 键
K_RALT右边的 alt 键
K_LALT左边的 alt 键
K_RMETA右边的元键
K_LMETA左边的元键
K_LSUPER左边的 Window 键
K_RSUPER右边的 Window 键
K_MODE模式转换键
K_HELP帮助键
K_PRINT打印屏幕键
K_SYSREQ魔术键
K_BREAK中断键
K_MENU菜单键
K_POWER电源键
K_EURO欧元符号

组合键是指同时按下的多个按键。event.mod属性用于判断用户按下的组合键。下面是有关的组合键常量, 判断组合键时,将event.mod属性与多个组合键进行按位与”&”操作。如果没有组合键,则用event.mod属性和KMODE_NONE进行判断。

常量

解释

KMOD_NONE

未同时按下组合键

KMOD_LSHIFT

同时按下左边的 shift 键

KMOD_RSHIFT

同时按下右边的 shift 键

KMOD_SHIFT

同时按下 shift 键

KMOD_CAPS

同时按下大写字母锁定键

KMOD_LCTRL

同时按下左边的 ctrl 键

KMOD_RCTRL

同时按下右边的 ctrl 键

KMOD_CTRL

同时按下 ctrl 键

KMOD_LALT

同时按下左边的 alt 键

KMOD_RALT

同时按下右边的 alt 键

KMOD_ALT

同时按下 alt 键

KMOD_LMETA

同时按下左边的元键

KMOD_RMETA

同时按下右边的元键

KMOD_META

同时按下元键

KMOD_NUM

同时按下数字键盘锁定键

KMOD_MODE

同时按下模式转换键

示例如下:

import pygame as pg

pg.init()
screen = pg.display.set_mode((300, 200))

while True:
    for event in pg.event.get():
        if event.type == pg.QUIT:
            pg.quit()
        elif event.type == pg.KEYDOWN:
            if event.mod == pg.KMOD_NONE:
                print("无组合键按下")
            elif event.mod & pg.KMOD_CTRL:
                print("按下了Ctrl和标识符为", event.key, "的按键")

当按下一些输入类键,比如a, b, c, 1, 2, 3以及各种符号时,Event对象有一个unicode属性,可以用于检测基本的unicode字符输入。 当按下Ctrl, Shift等功能键时,unicode属性为空字符串。

4.3 鼠标事件

下面的示例演示了鼠标事件。

import pygame as pg 

pg.init()

screen = pg.display.set_mode((400, 400))

while True:
    for event in pg.event.get():
        if event.type == pg.QUIT:
            pg.quit()
        elif event.type == pg.MOUSEBUTTONDOWN: #按下鼠标
            if event.button == 1:
                print("鼠标左键点击位置", event.pos)

运行后,当用户在屏幕上点击左键时,会提示鼠标点击的位置。  

event.button是鼠标键的类型,左键、中键、右键、滚轮上滑、滚轮下滑、X1键(前进)、X2键(后退)分别对应1, 2, 3, 4, 5, 6, 7。pygame还提供了一些常量表示这些按键。

常量解释
BUTTON_LEFT左键
BUTTON_MIDDLE中键
BUTTON_RIGHT右键
BUTTON_WHEELUP滚轮上滑
BUTTON_WHEELDOWN滚轮下滑
BUTTON_X1X1键
BUTTON_X2X2键

除此之外,还有一个属性event.pos,表示鼠标相对于窗口屏幕的位置的元组(不是相对于电脑显示器)。

4.4 窗口焦点事件

如果想要在游戏进行发生焦点变化时做出反应,如暂停和恢复游戏,主要使用ACTIVEEVENT事件。如果需要区分焦点变化的类型,可以通过ACTIVEEVENT的gain和state属性。

以下是ACTIVEEVENT的gain和state属性在不同情况下的对应值(作者),总结而言,state是激活事件的状态,gain是一个1或0的值,表示窗口是否存在激活的情况。

用户操作对应事件gainstate
鼠标进入屏幕WINDOWENTER11
鼠标离开屏幕WINDOWLEAVE01
窗口获取焦点WINDOWFOCUSGAINED12
窗口失去焦点WINDOWFOCUSLOST02
窗口从最小化还原WINDOWRESTORED14
窗口最小化WINDOWMINIMIZED04

注:不存在state=3的情况。如果需要区分事件类型,更加推荐使用pygame2提供的几个窗口控制事件,而不是ACTIVESTATE的gain和state属性。

下面的示例中,只有当用户处于操作窗口的状态时,才会显示logo.png。

import pygame as pg

pg.init()

screen = pg.display.set_mode((300, 200))
image = pg.image.load("logo.png")
image_rect = image.get_rect()
showing = False

while True:
    screen.fill((0, 0, 0))

    if showing:
        screen.blit(image, image_rect)

    for event in pg.event.get():
        if event.type == pg.QUIT:
            pg.quit()
        elif event.type == pg.ACTIVEEVENT:
            showing = event.gain

    pg.display.flip()

4.5 拖拽文件或文本事件

当用户将一个文件或一段文字拖拽进入窗口时,会触发文件拖拽或文本拖拽事件,即DROPFILE和DROPTEXT。

import pygame as pg

pg.init()

screen = pg.display.set_mode((300, 200))

while True:
    for event in pg.event.get():
        if event.type == pg.QUIT:
            pg.quit()
        elif event.type == pg.DROPFILE:
            print("文件路径", event.file)

4.6 文本输入事件

pygame在用户输入文本时会触发TEXTINPUT事件,如果输入的内容是编辑状态的(比如中文输入中,需要输入拼音后再从输入候选框中选择字词,此时未输入完整的拼音就是编辑状态的内容),还会触发TEXTEDITING事件。

import pygame as pg

pg.init()
screen = pg.display.set_mode((300, 200))

while True:
    for event in pg.event.get():
        if event.type == pg.QUIT:
            pg.quit()
        elif event.type == pg.TEXTINPUT:
            print("INPUT", event.text)
        elif event.type == pg.TEXTEDITING:
            print("EDIT", event.text)

 

注意:默认情况下,pygame不会显示中文输入候选框,如需要可以添加如下代码:

import os
os.environ["SDL_IME_SHOW_UI"] = "1" #显示输入候选框UI

关于更多内容将在后面提及。

4.7 发送自定义事件

pygame还支持用户自己定义事件,首先需要了解pg.event.Event对象。

Event(type, dict) -> Event
Event(type, **attributes) -> Event

第一个type参数是指事件的标识符数值,一般选择pg.USEREVENT作为标识。第二个参数可以以一个字典或关键字参数的形式传入,表示这个事件附带的一些参数。下面的代码创建了一个事件:

mouse_down = pg.event.Event(pg.USEREVENT, tip="鼠标按下")

但是创建一个事件是不够的,要捕获这个事件,首先需要把这个事件发送出去。此时可以使用pg.event.post方法。post方法放一个布尔值,表示是否成功发送(如果事件是一个阻塞事件,那么无法发送,详见下一节)

pg.event.post(Event) -> bool

 示例如下:

import pygame as pg

pg.init()

screen = pg.display.set_mode((300, 200))

mouse_down = pg.event.Event(pg.USEREVENT, tip="鼠标按下")
key_down = pg.event.Event(pg.USEREVENT, tip="键盘按下")

while True:
    for event in pg.event.get():
        if event.type == pg.QUIT:
            pg.quit()
            
        elif event.type == pg.MOUSEBUTTONDOWN:
            print("MOUSEBUTTONDOWN 鼠标按下")
            pg.event.post(mouse_down)

        elif event.type == pg.KEYDOWN:
            print("KEYDOWN 键盘按下")
            pg.event.post(key_down)
            
        elif event.type == pg.USEREVENT:
            print("触发了我的事件", event.tip)

上面的这个示例中,如果按下鼠标会发送一个mouse_down事件,按下键盘会发生一个key_down事件。由于这两个事件的type和pg.USEREVENT是一样的,所以只需要与pg.USEREVENT比较,就能判断出事件。运行效果如图:

注意:event.type是一个标识符,是一个整数。

4.8 阻塞事件

被阻塞的事件无法被发送。pg.event.set_blocked方法用于阻塞事件。

pg.event.set_blocked(type) -> None
pg.event.set_blocked(typelist) -> None
pg.event.set_blocked(None) -> None

set_blocked方法可以传入一个事件类型标识符或者一个事件类型表示符的列表。如果传入None,则禁用所有事件。

如果想要取消事件阻塞可以使用set_allowed方法,用法与set_blocked相同,但作用相反。

pg.event.set_allowed(type) -> None
pg.event.set_allowed(typelist) -> None
pg.event.set_allowed(None) -> None

4.9 event模块索引-事件处理

get(eventtype=None, pump=True, exclude=None) -> Eventlist
从消息队列中获取事件。

poll() -> Event instance
从消息队列中获取一个单个的事件。

wait() -> Event instance
wait(timeout) -> Event instance

一直等待直到收到某个事件,并将该事件从事件队列删除。如果指定timeout参数,而在指定时间内未收到任何事件,则返回pygame.NOEVENT。

peek(eventtype=None, pump=True) -> bool
判断某个类型的事件是否在事件队列中。

clear(eventtype=None, pump=True) -> None
从事件队列中移除事件。

event_name(type) -> string
通过一个事件类型获取事件名称的字符串。例如:pg.event.event_name(pg.KEYDOWN) -> “KEYDOWN”。

set_blocked(type) -> None
set_blocked(typelist) -> None
set_blocked(None) -> None

阻塞事件。

set_allowed(type) -> None
set_allowed(typelist) -> None
set_allowed(None) -> None

取消阻塞事件。

get_blocked(type) -> bool
get_blocked(typelist) -> bool

判断事件是否阻塞。

set_grab(bool) -> None
控制与其他应用程序共享输入设备。如果设置为True,则用户无法将鼠标移出pygame窗口,不能在其他地方操作。

get_grab() -> bool
判断是否控制与其他应用程序共享输入设备。

post(Event) -> bool
发送事件到事件队列。

custom_type() -> int
新建一个用户事件。如果创建的事件过多将引发pygame.error。

Event(type, dict) -> Event
Event(type, **attributes) -> Event

基本的事件对象。

5 时间控制

参考资料:https://www.pyga.me/docs/ref/time.html

5.1 控制FPS

帧每秒,简称FPS,是游戏中常见的名词。FPS限制了动画的播放速度,比如FPS=30,也就是30帧每秒,屏幕上每1秒显示30张静态图片。人眼的视觉暂留机制使静态画面连接起来,就形成了动画。

游戏的FPS一般在30-60之间。FPS设置过高,会导致显卡的负载过重;FPS设置过低,用户则无法看到连贯的画面。

pygame.time模块提供了一些有关时间的操作,其中包含控制最高FPS的功能。首先要创建一个pygame.time.Clock对象,然后在每个循环的末尾通过Clock.tick方法控制FPS。如下所示:

import pygame as pg 

pg.init()

screen = pg.display.set_mode((400, 400))
image = pg.image.load("logo.png")
image_rect = image.get_rect()

clock = pg.time.Clock() #Clock对象可以控制FPS

while True:
    screen.fill((0, 0, 0))
    screen.blit(image, image_rect)
    image_rect.x += 1
    image_rect.y += 1

    for event in pg.event.get():
        if event.type == pg.QUIT:
            pg.quit()

    clock.tick(60) #设置最高FPS为60
    pg.display.flip() 

运行后,发现图片的移动速度要比之前慢了,但是仍然连贯。

需要注意的是,tick方法控制的只是最高帧率。如果循环中处理的内容太多,实际帧率仍然会有所下降。但在多数情况下,实际的FPS是正常的,一般在设置的FPS上下浮动(59-61)。

Clock.get_fps方法返回实际的FPS值。下面的示例用time.sleep模拟了一个处理数据比较多的事件循环,可以看到实际FPS大幅下降,只有10左右。

import pygame as pg
import time

pg.init()

screen = pg.display.set_mode((400, 400))

clock = pg.time.Clock() #Clock对象可以控制FPS

while True:
    time.sleep(0.1)
    pg.display.set_caption(str(clock.get_fps())) #设置标题为实际FPS

    for event in pg.event.get():
        if event.type == pg.QUIT:
            pg.quit()

    clock.tick(60) #设置最高FPS为60
    pg.display.flip()

5.2 获取游戏运行时间

pg.time.get_ticks方法用于获取游戏运行的时间(从pg.init()调用起开始计算),单位为毫秒(简称ms,等于1/1000秒)。如果要让图片每隔1秒移动一次,可以用get_ticks,如下所示。

import pygame as pg

pg.init()

screen = pg.display.set_mode((400, 400))
image = pg.image.load("logo.png")
image_rect = image.get_rect()

clock = pg.time.Clock() #Clock对象可以控制FPS

last_move = 0 #上一次移动的时间

while True:
    screen.fill((0, 0, 0))
    screen.blit(image, image_rect)

    now = pg.time.get_ticks()
    if now - last_move > 1000: #时间差大于1000ms=1s
        last_move = now #将上一次移动时间设置为当前时间
        image_rect.x += 50 #右移50像素

    for event in pg.event.get():
        if event.type == pg.QUIT:
            pg.quit()

    clock.tick(60) #设置最高FPS为60
    pg.display.flip()

注意:pg.time.get_ticks()具有一定隐患,在制作游戏中一定要注意!当鼠标长按在窗口标题栏上时,可以发现所有的刷新暂停了,此时代码阻塞在pg.event.get()的位置,但是get_ticks()不会在阻塞的时候等待,这就会导致一个时间上的问题。如果通过get_ticks()做一个技能冷却的功能,玩家可以在用完一次技能后将鼠标按在窗口上暂停刷新,然后等待技能冷却完成后松开,这样就可以一直重复使用技能了。

解决方案:在pg.event.get()前记录一次时间,在pg.event.get()后的时间与这个记录的时间相减,即为期间暂停的时间。再用pg.time.get_ticks()减去期间暂停时间的总和,即可得到实际游戏运行时间。

import pygame as pg

pg.init()

screen = pg.display.set_mode((400, 400))
image = pg.image.load("logo.png")
image_rect = image.get_rect()

clock = pg.time.Clock()

last_move = 0

time_offset = 0 #时间偏移量
while True:
    screen.fill((0, 0, 0))
    screen.blit(image, image_rect)

    now = pg.time.get_ticks() - time_offset
    if now - last_move > 1000:
        last_move = now
        image_rect.x += 50

    t = pg.time.get_ticks()
    for event in pg.event.get():
        if event.type == pg.QUIT:
            pg.quit()
    time_offset += pg.time.get_ticks() - t

    clock.tick(60)
    pg.display.flip()

5.3 定期发送事件

pg.event.set_timer方法可以定期发送事件。

pg.event.set_timer(event, millis) -> None
pg.event.set_timer(event, millis, loops=0) -> None

第一个参数event表示事件类型,支持Event对象或事件type标识符。millis是发送事件的延迟,单位为ms,如设置为1000表示每隔1秒发送一次事件。loops是发送的次数,默认为0表示无限发送。

import pygame as pg

pg.init()

screen = pg.display.set_mode((300, 200))

pg.time.set_timer(pg.USEREVENT, 1000)

while True:
    for event in pg.event.get():
        if event.type == pg.QUIT:
            pg.quit()
            
        elif event.type == pg.USEREVENT:
            print("USEREVENT")

运行后,每隔1秒打印一次”USEREVENT”。

对于相同标识符的事件,set_timer只能有一个,如果调用set_timer的事件正在进行中,那么将会丢弃创建时间较早的事件计时器。

如果要停止set_timer,可以将millis参数设为0。如上面的示例想要暂停set_timer,可以调用:

pg.time.set_timer(pg.USEREVENT, 0)

5.4 time模块索引-时间控制

get_ticks() -> milliseconds
获取从pg.init()调用起经过的时间。

wait(milliseconds) -> time
暂停pygame窗口一段时间。

delay(milliseconds) -> time
暂停pygame窗口一段时间(比wait方法精确)。

set_timer(event, millis) -> None
set_timer(event, millis, loops=0) -> None

重复生成事件,millis为间隔时间,loops是循环次数。

Clock() -> Clock
时间对象,用于追踪时间。
Clock.tick(framerate=0) -> milliseconds
每帧调用一次,返回自上次调用以来经过的时间(通常不准确,但占用CPU较少)。如果给定framerate,将限制游戏的FPS。
Clock.tick_busy_loop(framerate=0) -> milliseconds
和tick方法作用一样,但是时间测量更准确(CPU占用较多)。
Clock.get_time() -> milliseconds
返回两次tick方法调用的间隔时间。
Clock.get_rawtime() -> milliseconds
和get_time方法类似,但不会把tick方法延迟用于限制FPS的时间计算进来。
Clock.get_fps() -> float
返回十次调用tick方法后得到的平均值(即游戏的FPS)

实战1 行走的人

本章是实战练习环节,将实现以下效果。

玩家可以用方向键操纵人物移动。

完整代码

import pygame as pg
from pygame.locals import * #导入所有常量
import os

WIDTH = 500
HEIGHT = 320 #屏幕的宽、高

def load_animations(name):
    '''加载帧序列图片'''
    images = []
    i = 0
    while True:
        i += 1

        filename = name.format(i)
        if os.path.exists(filename): #如果文件存在
            images.append(pg.image.load(filename))
        else:
            break

    return images
            
class Player:
    def __init__(self):
        self.images = load_animations("assets/player{}.png") #玩家图片列表
        self.image_idx = 0
        self.image = self.images[0]
        self.rect = self.image.get_rect(bottomleft=(0, HEIGHT)) #把玩家位置放在左下角
        self.move = [0, 0] #通过初始化一个move列表来让玩家移动

    def draw(self, screen):
        '''在屏幕上绘制玩家'''
        screen.blit(self.image, self.rect)

        self.rect.x += self.move[0] #移动玩家
        self.rect.y += self.move[1]

        if self.rect.left < 0:
            self.rect.left = 0
        elif self.rect.right > WIDTH:
            self.rect.right = WIDTH
            
        if self.rect.top < 0:
            self.rect.top = 0
        elif self.rect.bottom > HEIGHT:
            self.rect.bottom = HEIGHT #使玩家无法移出屏幕

    def update_index(self):
        '''更新玩家图片,使玩家有移动的效果'''
        self.image_idx += 1
        self.image = self.images[self.image_idx % len(self.images)]

def main():
    pg.init() #初始化pyame
    screen = pg.display.set_mode((WIDTH, HEIGHT)) #设置窗口大小
    pg.display.set_caption("行走的人") #设置标题
    clock = pg.time.Clock() #时钟:控制FPS

    bg = pg.image.load("assets/bg.png") #背景图片
    player = Player() #玩家
    speed = 2 #初始化玩家速度为2px

    moving = pg.event.Event(USEREVENT) 
    pg.time.set_timer(moving, 100) #每隔0.1s生成一个moving事件,控制帧序列图刷新 
    
    while True: #游戏循环
        screen.blit(bg, (0, 0))
        player.draw(screen)
        
        for event in pg.event.get():
            if event.type == QUIT:
                pg.quit() #退出程序
                
            elif event.type == KEYDOWN: #按下按键
                if event.key == K_LEFT:
                    player.move[0] = -speed
                elif event.key == K_RIGHT:
                    player.move[0] = speed
                    
                if event.key == K_UP:
                    player.move[1] = -speed
                elif event.key == K_DOWN:
                    player.move[1] = speed #按下方向键移动玩家

            elif event.type == KEYUP: #松开按键
                if event.key == K_LEFT:
                    player.move[0] = 0
                elif event.key == K_RIGHT:
                    player.move[0] = 0
                    
                if event.key == K_UP:
                    player.move[1] = 0
                elif event.key == K_DOWN:
                    player.move[1] = 0 #松开方向键停止玩家
                    
            elif event.type == moving.type: #如果检测到moving事件
                player.update_index() #更新玩家图片
                
        clock.tick(60) #设置FPS为60
        pg.display.flip() #刷新绘制内容

if __name__ == "__main__":
    main()

准备素材

在游戏制作开始前,首先需要对游戏进行构思,并准备基本所需的素材。一般在开发应用时,首先会建立一个应用专属的文件夹,其中包含一个类似于main.py的主程序文件,新建一个文件夹将图片、音效等素材放在里面。这样很方便地就能在程序中调用它们。程序中最忌直接使用绝对路径(如”C:/abc/abc.png”这种),而应该将图片等素材放在应用的文件夹里,使用相对路径来标识。这样在后期打包处理的时候,不会出现找不到素材而出现错误的情况。

本游戏使用了5张图片素材,包括一张背景图片和玩家运动的四帧图片,都存放在一个叫做assets的文件夹中。

pygame.locals

pygame.locals是Pygame的常量库,里面包括了KEYDOWN, KEYUP, K_LEFT等一系列常量。如果需要使用的常量较多,可以在开头将所有常量导入进来。

导入模块、定义常量

在程序的开头,首先需要导入使用的模块,并定义一些常量。

import pygame as pg
from pygame.locals import *
import os

WIDTH = 500
HEIGHT = 320 #屏幕的宽、高

os模块在本示例中的作用是为了加载4张玩家图片。

这里需要注意,游戏中为什么要在开头定义常量呢?常量一般会在整个程序中都有用,可以在程序中避免重复提到一个数字,方便后期更改。就比如屏幕的宽和高,如果后面想要让屏幕变得更大一点,就只需要在上方定义的常量中修改,而无需在整个程序文件中翻找所有涉及到屏幕宽高数字的代码。同时,常量还可以使代码更加容易理解。编程者看到500很难明白这是什么意思,但看到WIDTH很容易就能明白这是宽的意思。

加载帧序列

帧序列是指多个静态图片,能够组成动画的效果。在游戏中,帧序列图被频繁使用。

def load_animations(name):
    '''加载帧序列图片'''
    images = []
    i = 0
    while True:
        i += 1

        filename = name.format(i)
        if os.path.exists(filename): #如果文件存在
            images.append(pg.image.load(filename))
        else:
            break

    return images

通过这段代码,只需要调用load_animations(“assets/player{}.png”)就可以得到一个列表,储存了player帧序列的表面对象。

保证玩家在屏幕内移动

通过下面一个简单的算法,可以使玩家在移动时不移出屏幕。

if self.rect.left < 0:        #当玩家左侧坐标<0
    self.rect.left = 0        #设置玩家左侧坐标为0
elif self.rect.right > WIDTH: #当玩家右侧坐标大于窗口宽度
    self.rect.right = WIDTH   #设置玩家右侧位于窗口右侧
    
if self.rect.top < 0:
    self.rect.top = 0
elif self.rect.bottom > HEIGHT:
    self.rect.bottom = HEIGHT

下一篇文章

https://blog.csdn.net/qq_48979387/article/details/128784116

文章出处登录后可见!

已经登录?立即刷新

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
青葱年少的头像青葱年少普通用户
上一篇 2023年4月22日
下一篇 2023年4月23日

相关推荐