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中一些常用的模块会自动导入,所以大部分模块无需额外导入。

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_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不会显示中文输入候选框,如需要可以添加如下代码:

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

猜你喜欢

转载自blog.csdn.net/qq_48979387/article/details/126799308