首页 > Python资料 博客日记
Python pygame(GUI编程)模块最完整教程(1)
2024-07-04 20:00:04Python资料围观118次
提示:下滑文章左侧可以查看目录!本教程分为多篇,总目录如下。
总目录:
README.md · Python-ZZY/Python-Pygame最完整教程 - Gitee.com
欢迎加入编程讨论群(QQ):
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模块,通常为了简便而命名为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()来刷新窗口,处理用户事件。
当用户在窗口上进行了点击鼠标、按下按键、拖拽窗口、关闭窗口等操作时,这些操作会以“事件”的形式被pygame获取。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 实现动画
在了解矩形对象之后,你应该知道如何移动矩形了,那么下一步将是实现动画。
这里先简要介绍一下实现动画的原理:其实就是用一张张静态的图片按顺序展示,如果顺序展示的速度比较快,人脑就会人物这是连续的。
在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 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_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 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不会显示中文输入候选框,如需要可以在pg.display.set_mode之前添加如下代码:
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上下浮动一点点(约为60±1)。
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)
实战 行走的人
本章是实战练习环节,将实现以下效果。
玩家可以用方向键操纵人物移动。
完整代码
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
下一篇文章
标签:
相关文章
最新发布
- 【Python】selenium安装+Microsoft Edge驱动器下载配置流程
- Python 中自动打开网页并点击[自动化脚本],Selenium
- Anaconda基础使用
- 【Python】成功解决 TypeError: ‘<‘ not supported between instances of ‘str’ and ‘int’
- manim边学边做--三维的点和线
- CPython是最常用的Python解释器之一,也是Python官方实现。它是用C语言编写的,旨在提供一个高效且易于使用的Python解释器。
- Anaconda安装配置Jupyter(2024最新版)
- Python中读取Excel最快的几种方法!
- Python某城市美食商家爬虫数据可视化分析和推荐查询系统毕业设计论文开题报告
- 如何使用 Python 批量检测和转换 JSONL 文件编码为 UTF-8
点击排行
- 版本匹配指南:Numpy版本和Python版本的对应关系
- 版本匹配指南:PyTorch版本、torchvision 版本和Python版本的对应关系
- Python 可视化 web 神器:streamlit、Gradio、dash、nicegui;低代码 Python Web 框架:PyWebIO
- 相关性分析——Pearson相关系数+热力图(附data和Python完整代码)
- Anaconda版本和Python版本对应关系(持续更新...)
- Python与PyTorch的版本对应
- Windows上安装 Python 环境并配置环境变量 (超详细教程)
- Python pyinstaller打包exe最完整教程