提示:下滑文章左侧可以查看目录!本教程分为多篇,总目录如下。
总目录:
README.md · Python-ZZY/Python-Pygame最完整教程 - Gitee.com
1 初识pygame
1.1 简介
pygame是python中一个流行的GUI编程模块,是专门为了开发游戏而设计的。这是一个第三方模块,是SDL和Python的接口。
pygame的最新官网是:https://pyga.me/
pygame以前的官网是:https://www.pygame.org/
SDL的官网是:https://www.libsdl.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字体 |
freetype | font模块的扩展,提供更多字体操作 |
gfxdraw | 绘制抗锯齿的形状 |
image | 加载、保存图片文件 |
joystick | 管理游戏手柄 |
key | 管理键盘输入 |
locals | 此模块储存所有的pygame常量 |
mixer | 播放声音 |
mouse | 管理光标位置和事件 |
midi | MIDI输入与输出管理 |
scrap | 管理剪贴板 |
sndarray | 处理声音样本数据 |
sprite | 管理精灵 |
surfarray | 处理图像像素数据 |
system | 获取或管理当前系统上的一些信息 |
time | 管理时间和帧率 |
transform | 变换表面(如缩放、旋转等) |
_sdl2 | 一些实验性的东西 |
注:pygame还增加了许多功能,作者后期会进行补充
pygame中一些常用的模块会自动导入,所以大部分模块无需额外导入。
1.5 关于本教程
众所周知,教程和文档是两个不同的东西,文档最为全面但不易学,教程易学但不可能涵盖所有内容。
本教程为了达到完整性,会先在每一节介绍模块中较常用的使用方法,最后以“模块索引”的形式列举所有方法。
标题标注为红色的是重要内容,制作游戏中必然会用到。黑色的标题读者可自行了解,灰色的标题表示不重要、不常用。
1.6 表面
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模块,通常为了简便而命名为pgpg.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模块,习惯上命名为pgpg.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 实现动画
在了解矩形对象之后,你应该知道如何移动矩形了,那么下一步将是实现动画。
这里先简要介绍一下实现动画的原理:其实就是用一张张静态的图片按顺序展示,如果顺序展示的速度比较快,人脑就会人物这是连续的。
在pygame中,如果要实现慢慢向右移动一张图片,需要在循环中每次增加一点图片的x坐标,然后一遍遍地覆盖上一次的绘制结果。
将一个矩形右移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个像素。
关于这个问题的解决方案有两种,第一种方案是使用支持浮点数的FRect(绘制时候是取整的,但是计算时允许你使用浮点数,将在后文介绍),第二种方式是控制刷新速率(将在第五章介绍)。
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_AMPERSAND | and 符号 |
K_QUOTE | 单引号 |
K_LEFTPAREN | 左小括号 |
K_RIGHTPAREN | 右小括号 |
K_ASTERISK | 星号 |
K_PLUS | 加号 |
K_COMMA | 逗号 |
K_MINUS | 减号 |
K_PERIOD | 句号 |
K_SLASH | 正斜杠 |
K_0 | 0 |
K_1 | 1 |
K_2 | 2 |
K_3 | 3 |
K_4 | 4 |
K_5 | 5 |
K_6 | 6 |
K_7 | 7 |
K_8 | 8 |
K_9 | 9 |
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_a | a |
K_b | b |
K_c | c |
K_d | d |
K_e | e |
K_f | f |
K_g | g |
K_h | h |
K_i | i |
K_j | j |
K_k | k |
K_l | l |
K_m | m |
K_n | n |
K_o | o |
K_p | p |
K_q | q |
K_r | r |
K_s | s |
K_t | t |
K_u | u |
K_v | v |
K_w | w |
K_x | x |
K_y | y |
K_z | z |
K_DELETE | 删除键(delete) |
K_KP0 | 0(小键盘) |
K_KP1 | 1(小键盘) |
K_KP2 | 2 (小键盘) |
K_KP3 | 3(小键盘) |
K_KP4 | 4(小键盘) |
K_KP5 | 5 (小键盘) |
K_KP6 | 6 (小键盘) |
K_KP7 | 7 (小键盘) |
K_KP8 | 8 (小键盘) |
K_KP9 | 9 (小键盘) |
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_HOME | Home 键(home) |
K_END | End 键(end) |
K_PAGEUP | 上一页(page up) |
K_PAGEDOWN | 下一页(page down) |
K_F1 | F1 |
K_F2 | F2 |
K_F3 | F3 |
K_F4 | F4 |
K_F5 | F5 |
K_F6 | F6 |
K_F7 | F7 |
K_F8 | F8 |
K_F9 | F9 |
K_F10 | F10 |
K_F11 | F11 |
K_F12 | F12 |
K_F13 | F13 |
K_F14 | F14 |
K_F15 | F15 |
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 pgpg.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_X1 | X1键 |
BUTTON_X2 | X2键 |
除此之外,还有一个属性event.pos,表示鼠标相对于窗口屏幕的位置的元组(不是相对于电脑显示器)。
4.4 窗口焦点事件
如果想要在游戏进行发生焦点变化时做出反应,如暂停和恢复游戏,主要使用ACTIVEEVENT事件。如果需要区分焦点变化的类型,可以通过ACTIVEEVENT的gain和state属性。
以下是ACTIVEEVENT的gain和state属性在不同情况下的对应值(作者),总结而言,state是激活事件的状态,gain是一个1或0的值,表示窗口是否存在激活的情况。
用户操作 | 对应事件 | gain | state |
鼠标进入屏幕 | WINDOWENTER | 1 | 1 |
鼠标离开屏幕 | WINDOWLEAVE | 0 | 1 |
窗口获取焦点 | WINDOWFOCUSGAINED | 1 | 2 |
窗口失去焦点 | WINDOWFOCUSLOST | 0 | 2 |
窗口从最小化还原 | WINDOWRESTORED | 1 | 4 |
窗口最小化 | WINDOWMINIMIZED | 0 | 4 |
注:不存在state=3的情况。如果需要区分事件类型,更加推荐使用pygame2提供的几个窗口控制事件,而不是ACTIVESTATE的gain和state属性。
下面的示例中,只有当用户处于操作窗口的状态时,才会显示logo.png。
import pygame as pgpg.init()screen = pg.display.set_mode((300, 200))image = pg.image.load("logo.png")image_rect = image.get_rect()showing = Falsewhile 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 pgpg.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 pgpg.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不会显示中文输入候选框,如需要可以在pg.display.set_mode之前添加如下代码:
import osos.environ["SDL_IME_SHOW_UI"] = "1" #显示输入候选框UI
关于更多内容将在后面提及。
4.7 发送自定义事件
pygame还支持用户自己定义事件,首先需要了解pg.event.Event对象。
Event(type, dict) -> EventEvent(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 pgpg.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) -> Nonepg.event.set_blocked(typelist) -> Nonepg.event.set_blocked(None) -> None
set_blocked方法可以传入一个事件类型标识符或者一个事件类型表示符的列表。如果传入None,则禁用所有事件。
如果想要取消事件阻塞可以使用set_allowed方法,用法与set_blocked相同,但作用相反。
pg.event.set_allowed(type) -> Nonepg.event.set_allowed(typelist) -> Nonepg.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对象可以控制FPSwhile 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上下浮动一点点(约为60±1)。
Clock.get_fps方法返回实际的FPS值。下面的示例用time.sleep模拟了一个比较卡的事件循环,可以看到实际FPS大幅下降,只有10左右。
import pygame as pgimport timepg.init()screen = pg.display.set_mode((400, 400))clock = pg.time.Clock() #Clock对象可以控制FPSwhile 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 pgpg.init()screen = pg.display.set_mode((400, 400))image = pg.image.load("logo.png")image_rect = image.get_rect()clock = pg.time.Clock() #Clock对象可以控制FPSlast_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 pgpg.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 = 0time_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) -> Nonepg.event.set_timer(event, millis, loops=0) -> None
第一个参数event表示事件类型,支持Event对象或事件type标识符。millis是发送事件的延迟,单位为ms,如设置为1000表示每隔1秒发送一次事件。loops是发送的次数,默认为0表示无限发送。
import pygame as pgpg.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)
实战 行走的人
本章是实战练习环节,将实现以下效果。
玩家可以用方向键操纵人物移动。
完整代码
import pygame as pgfrom pygame.locals import * #导入所有常量import osWIDTH = 500HEIGHT = 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 pgfrom pygame.locals import *import osWIDTH = 500HEIGHT = 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 #设置玩家左侧坐标为0elif self.rect.right > WIDTH: #当玩家右侧坐标大于窗口宽度 self.rect.right = WIDTH #设置玩家右侧位于窗口右侧 if self.rect.top < 0: self.rect.top = 0elif self.rect.bottom > HEIGHT: self.rect.bottom = HEIGHT
下一篇文章
https://blog.csdn.net/qq_48979387/article/details/128784116
来源地址:https://blog.csdn.net/qq_48979387/article/details/126799308