一、说明
当我在上个世纪末开始学习计算机编程时,我的愿望是编写计算机游戏。我试图弄清楚如何在我学到的每种语言和每个平台上编写游戏,包括 Python。这就是我发现pygame并学习如何使用它来编写游戏和其他图形程序的方式。当时,我真的很想要一本入门书pygame
。
读完本文后,您将能够:
- 在屏幕上绘制项目
- 播放音效和音乐
- 处理用户输入
- 实施事件循环
- 描述游戏编程与标准过程式 Python 编程有何不同
目录
- 背景和设置
- 基本 PyGame 程序
- PyGame 概念
- 初始化和模块
- 显示器和表面
- 图像和矩形
- 基础游戏设计
- 导入并初始化 PyGame
- 设置显示
- 设置游戏循环
- 处理事件
- 在屏幕上绘图
- 使用 .blit() 和 .flip()
- 精灵
- 玩家
- 用户输入
- 敌人
- 精灵组
- 自定义事件
- 碰撞检测
- 精灵图像
- 改变对象构造函数
- 添加背景图片
- 游戏速度
- 声音特效
- 关于来源的说明
- 结论
您可以获得本文中的所有代码以进行后续操作:
- 示例代码: 单击此处下载本教程中使用的 PyGame 示例项目的源代码。
二、背景和设置
pygame
是SDL 库的 Python 包装器,SDL 库代表Simple DirectMedia Layer。SDL 提供对系统底层多媒体硬件组件(例如声音、视频、鼠标、键盘和操纵杆)的跨平台访问。作为停滞的PySDL 项目pygame
的替代品而开始。SDL 的跨平台特性意味着您可以为每个支持它们的平台编写游戏和丰富的多媒体 Python 程序!pygame
要pygame
在您的平台上安装,请使用适当的pip命令:
$ pip install pygame
您可以通过加载该库附带的示例之一来验证安装:
$ python3 -m pygame.examples.aliens
如果出现游戏窗口,则pygame
说明安装正确!如果您遇到问题,入门指南概述了所有平台的一些已知问题和注意事项。
三、基本 PyGame 程序
在讨论具体细节之前,让我们先看一个基本pygame
程序。该程序创建一个窗口,用白色填充背景,并在中间绘制一个蓝色圆圈:
1# Simple pygame program
2
3# Import and initialize the pygame library
4import pygame
5pygame.init()
6
7# Set up the drawing window
8screen = pygame.display.set_mode([500, 500])
9
10# Run until the user asks to quit
11running = True
12while running:
13
14 # Did the user click the window close button?
15 for event in pygame.event.get():
16 if event.type == pygame.QUIT:
17 running = False
18
19 # Fill the background with white
20 screen.fill((255, 255, 255))
21
22 # Draw a solid blue circle in the center
23 pygame.draw.circle(screen, (0, 0, 255), (250, 250), 75)
24
25 # Flip the display
26 pygame.display.flip()
27
28# Done! Time to quit.
29pygame.quit()
当您运行该程序时,您将看到一个如下所示的窗口:
让我们逐节分解这段代码:
-
第 4 行和第 5 行导入并初始化
pygame
库。没有这些线,就没有pygame
. -
第 8 行设置程序的显示窗口。您提供一个列表或元组来指定要创建的窗口的宽度和高度。该程序使用列表创建一个每边 500 像素的方形窗口。
-
第 11 行和第 12 行设置了一个游戏循环来控制程序何时结束。您将在本教程后面介绍游戏循环。
-
第 15 至 17 行扫描并处理游戏循环内的事件。您也会稍后参加活动。在本例中,唯一处理的事件是
pygame.QUIT
,该事件在用户单击窗口关闭按钮时发生。 -
第 20 行用纯色填充窗口。
screen.fill()
接受指定颜色 RGB 值的列表或元组。自从(255, 255, 255)
提供以来,窗口就充满了白色。 -
第 23 行使用以下参数在窗口中绘制一个圆:
screen
:要在其上绘图的窗口(0, 0, 255)
:包含 RGB 颜色值的元组(250, 250)
:指定圆心坐标的元组75
:要绘制的圆的半径(以像素为单位)
-
第 26行将显示内容更新到屏幕上。如果没有这个调用,窗口中就不会出现任何内容!
-
29号线出口
pygame
。这仅在循环结束后发生。
这就是pygame
“你好,世界”的版本。现在让我们更深入地了解这段代码背后的概念。
四、PyGame 概念
由于pygame
SDL 库可跨不同平台和设备移植,因此它们都需要定义和使用各种硬件现实的抽象。理解这些概念和抽象将帮助您设计和开发自己的游戏。
4.1 初始化和模块
该pygame
库由许多 Python 结构组成,其中包括几个不同的模块。这些模块提供对系统上特定硬件的抽象访问,以及使用该硬件的统一方法。例如,display
允许统一访问您的视频显示,同时joystick
允许对操纵杆进行抽象控制。
导入上面示例中的库后,您所做pygame
的第一件事是使用. 该函数调用所有包含模块的单独函数。由于这些模块是特定硬件的抽象,因此需要执行此初始化步骤,以便您可以在 Linux、Windows 和 Mac 上使用相同的代码。pygame.init()
init()pygame
4.2 显示器和表面
除了模块之外,pygame
还包括几个Python类,它们封装了非硬件相关的概念。其中之一是Surface,从最基本的角度来说,它定义了一个可以在其上绘图的矩形区域。Surface
对象在 中的许多上下文中使用pygame
。稍后您将了解如何将图像加载到 a 中Surface
并将其显示在屏幕上。
在 中pygame
,所有内容都在单个用户创建的 上查看display,该用户可以是一个窗口或全屏。显示是使用 创建的.set_mode(),它返回Surface
代表窗口可见部分的 。您可以Surface
将其传递给 、 等绘图函数pygame.draw.circle(),并且在您调用时将其内容Surface
推送到显示器上pygame.display.flip()。
4.4 图像和矩形
您的基本pygame
程序直接在显示器上绘制形状Surface
,但您也可以处理磁盘上的图像。该image模块允许您加载和保存各种流行格式的图像。图像被加载到Surface
对象中,然后可以通过多种方式对其进行操作和显示。
如上所述,Surface
对象由矩形表示,就像 中的许多其他对象一样pygame
,例如图像和窗口。矩形的使用如此频繁,以至于有一个特殊的Rect类来处理它们。您将Rect
在游戏中使用对象和图像来绘制玩家和敌人,并管理他们之间的碰撞。
好吧,理论已经足够了。让我们设计并编写一个游戏!
五、基础游戏设计
在开始编写任何代码之前,最好先进行一些设计。由于这是一款教程游戏,我们也为其设计一些基本的游戏玩法:
- 游戏的目标是避开传入的障碍物:
- 播放器从屏幕左侧开始。
- 障碍物从右侧随机进入并沿直线向左移动。
- 玩家可以向左、向右、向上或向下移动以避开障碍物。
- 玩家无法移出屏幕。
- 当玩家被障碍物击中或用户关闭窗口时游戏结束。
我的一位前同事在描述软件项目时常常说:“除非你知道自己不做什么,否则你不知道自己在做什么。” 考虑到这一点,以下是本教程中不会介绍的一些内容:
- 没有多重生命
- 没有记分
- 没有玩家攻击能力
- 没有进步的水平
- 没有boss角色
您可以随意尝试将这些功能和其他功能添加到您自己的程序中。我们开始吧!
5.1 导入并初始化 PyGame
导入后pygame
,您还需要对其进行初始化。这允许pygame
将其抽象连接到您的特定硬件:
1# Import the pygame module
2import pygame
3
4# Import pygame.locals for easier access to key coordinates
5# Updated to conform to flake8 and black standards
6from pygame.locals import (
7 K_UP,
8 K_DOWN,
9 K_LEFT,
10 K_RIGHT,
11 K_ESCAPE,
12 KEYDOWN,
13 QUIT,
14)
15
16# Initialize pygame
17pygame.init()
pygame
除了模块和类之外,该库还定义了许多东西。它还为击键、鼠标移动和显示属性等定义了一些局部常量。您可以使用语法引用这些常量pygame.<CONSTANT>
。通过从 导入特定常量pygame.locals
,您可以改用语法<CONSTANT>
。这将为您节省一些击键次数并提高整体可读性。
5.2 设置显示
现在你需要一些可以借鉴的东西!创建一个屏幕作为整体画布:
1# Import the pygame module
2import pygame
3
4# Import pygame.locals for easier access to key coordinates
5# Updated to conform to flake8 and black standards
6from pygame.locals import (
7 K_UP,
8 K_DOWN,
9 K_LEFT,
10 K_RIGHT,
11 K_ESCAPE,
12 KEYDOWN,
13 QUIT,
14)
15
16# Initialize pygame
17pygame.init()
18
19# Define constants for the screen width and height 20SCREEN_WIDTH = 800 21SCREEN_HEIGHT = 600 22
23# Create the screen object 24# The size is determined by the constant SCREEN_WIDTH and SCREEN_HEIGHT 25screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_mode()
您可以通过调用并传递具有所需宽度和高度的元组或列表来创建要使用的屏幕。SCREEN_WIDTH
在本例中,窗口大小为 800x600,由第 20 行和SCREEN_HEIGHT
第 21 行上的常量定义。这将返回Surface
表示窗口内部尺寸的 a。这是您可以控制的窗口部分,而操作系统控制窗口边框和标题栏。
如果您现在运行该程序,您将看到一个窗口短暂弹出,然后随着程序退出而立即消失。不要眨眼,否则你可能会错过它!在下一节中,您将重点关注主游戏循环,以确保程序仅在给出正确的输入时退出。
5.3 设置游戏循环
从 Pong 到 Fortnite,每个游戏都使用游戏循环来控制游戏玩法。游戏循环做了四件非常重要的事情:
- 处理用户输入
- 更新所有游戏对象的状态
- 更新显示和音频输出
- 保持游戏的速度
游戏循环的每个周期称为一个帧,每个周期做事的速度越快,游戏运行的速度就越快。帧会继续出现,直到满足退出游戏的某些条件。在您的设计中,有两个条件可以结束游戏循环:
- 玩家与障碍物发生碰撞。(稍后您将介绍碰撞检测。)
- 玩家关闭窗口。
游戏循环所做的第一件事是处理用户输入以允许玩家在屏幕上移动。因此,您需要某种方法来捕获和处理各种输入。您可以使用pygame
事件系统来执行此操作。
5.4 处理事件
按键、鼠标移动、甚至操纵杆移动都是用户提供输入的一些方式。所有用户输入都会生成一个事件。事件可以随时发生,并且通常(但并非总是)源自程序之外。中的所有事件pygame
都放置在事件队列中,然后可以访问和操作事件队列。处理事件称为处理事件,执行此操作的代码称为事件处理程序。
中的每个事件pygame
都有一个与其关联的事件类型。对于您的游戏,您将关注的事件类型是按键和窗口关闭。按键事件具有事件类型KEYDOWN
,窗口关闭事件具有类型QUIT
。不同的事件类型还可能具有与其关联的其他数据。例如,KEYDOWN
事件类型还有一个变量调用key
来指示按下了哪个键。
您可以通过调用 来访问队列中所有活动事件的列表pygame.event.get()
。然后,您循环遍历此列表,检查每个事件类型,并做出相应响应:
27# Variable to keep the main loop running
28running = True
29
30# Main loop
31while running:
32 # Look at every event in the queue
33 for event in pygame.event.get():
34 # Did the user hit a key?
35 if event.type == KEYDOWN:
36 # Was it the Escape key? If so, stop the loop.
37 if event.key == K_ESCAPE:
38 running = False
39
40 # Did the user click the window close button? If so, stop the loop.
41 elif event.type == QUIT:
42 running = False
让我们仔细看看这个游戏循环:
-
第 28行为游戏循环设置一个控制变量。要退出循环和游戏,您可以设置
running = False
。游戏循环从第 29 行开始。 -
第 31行启动事件处理程序,遍历事件队列中当前的每个事件。如果没有事件,则列表为空,并且处理程序不会执行任何操作。
-
第 35 至 38 行检查当前是否
event.type
为KEYDOWN
事件。如果是,则程序通过查看该event.key
属性来检查按下的是哪个键。如果该键是Esc键,由 表示K_ESCAPE
,则通过设置 退出游戏循环running = False
。 -
第 41 行和第 42 行对名为 的事件类型执行类似的检查
QUIT
。仅当用户单击窗口关闭按钮时才会发生此事件。用户还可以使用任何其他操作系统操作来关闭窗口。
当您将这些行添加到前面的代码并运行它时,您将看到一个带有空白或黑色屏幕的窗口:
Esc在您按下该键或QUIT
通过关闭窗口触发事件之前,该窗口不会消失。
5.5 在屏幕上绘图
在示例程序中,您使用两个命令在屏幕上绘图:
screen.fill()
来填充背景pygame.draw.circle()
画一个圆
现在您将了解在屏幕上绘图的第三种方法:使用Surface
.
回想一下,aSurface是一个可以在其上绘图的矩形对象,就像一张白纸一样。该screen
对象是,您可以独立于显示屏幕Surface
创建自己的对象。Surface
让我们看看它是如何工作的:
44# Fill the screen with white
45screen.fill((255, 255, 255))
46
47# Create a surface and pass in a tuple containing its length and width
48surf = pygame.Surface((50, 50))
49
50# Give the surface a color to separate it from the background
51surf.fill((0, 0, 0))
52rect = surf.get_rect()
在第 45 行用白色填充屏幕后,Surface
在第 48 行创建一个新的。它Surface
宽 50 像素,高 50 像素,并分配给surf
。此时,您就像对待screen
. 所以在线上,51你用黑色填充它。您还可以Rect
使用访问其底层.get_rect()
。这被存储以rect
供以后使用。
5.6 使用.blit()
和.flip()
仅创建一个新内容Surface
并不足以在屏幕上看到它。为此,您需要将blit转移Surface
到另一个Surface
. 该术语blit
代表“块传输”,.blit()
是指将一个数据块的内容复制Surface
到另一个数据块的方式。您只能.blit()
从一个屏幕Surface
转到另一个屏幕,但由于屏幕只是另一个屏幕Surface
,所以这不是问题。surf
以下是在屏幕上绘图的方法:
54# This line says "Draw surf onto the screen at the center"
55screen.blit(surf, (SCREEN_WIDTH/2, SCREEN_HEIGHT/2))
56pygame.display.flip()
第 55 行的调用.blit()
采用两个参数:
- 要
Surface
画的 - 在源上绘制它的位置
Surface
坐标(SCREEN_WIDTH/2, SCREEN_HEIGHT/2)
告诉你的程序放置surf
在屏幕的正中心,但它看起来并不完全是这样:
图像看起来偏离中心的原因是.blit()
把左上角放在surf
给定的位置。如果你想surf
居中,那么你必须做一些数学运算才能将其向上和向左移动。您可以通过surf
从屏幕的宽度和高度中减去 的宽度和高度,然后除以 2 来定位中心,然后将这些数字作为参数传递给screen.blit()
:
54# Put the center of surf at the center of the display
55surf_center = (
56 (SCREEN_WIDTH-surf.get_width())/2,
57 (SCREEN_HEIGHT-surf.get_height())/2
58)
59
60# Draw surf at the new coordinates
61screen.blit(surf, surf_center)
62pygame.display.flip()
请注意调用 topygame.display.flip()之后的调用blit()
。这将使用自上次翻转以来绘制的所有内容更新整个屏幕。如果不调用.flip()
,则不会显示任何内容。
六、精灵
在你的游戏设计中,玩家从左侧开始,障碍物从右侧进入。您可以用对象来表示所有障碍物,Surface
以便更轻松地绘制所有内容,但是您如何知道在哪里绘制它们呢?你如何知道障碍物是否与玩家发生碰撞?当障碍物飞出屏幕时会发生什么?如果你想绘制也会移动的背景图像怎么办?如果您希望图像动画化怎么办?您可以使用精灵来处理所有这些情况以及更多情况。
用编程术语来说,精灵是屏幕上某物的 2D 表示。本质上,它是一张图片。pygame
提供一个Sprite类,该类旨在保存要在屏幕上显示的任何游戏对象的一个或多个图形表示。要使用它,您需要创建一个扩展的新类Sprite
。这允许您使用其内置方法。
6.1 玩家
以下是如何在当前游戏中使用Sprite
对象来定义玩家。在第 18 行后插入此代码:
20# Define a Player object by extending pygame.sprite.Sprite
21# The surface drawn on the screen is now an attribute of 'player'
22class Player(pygame.sprite.Sprite):
23 def __init__(self):
24 super(Player, self).__init__()
25 self.surf = pygame.Surface((75, 25))
26 self.surf.fill((255, 255, 255))
27 self.rect = self.surf.get_rect()
您首先Player
通过在第 22 行扩展来定义pygame.sprite.Sprite
。然后.__init__()
使用.super()
来调用.__init__()
的方法Sprite
。有关为什么需要这样做的更多信息,您可以阅读Supercharge Your Classes With Python super()。
接下来,您定义并初始化.surf
以保存要显示的图像,该图像当前是一个白框。您还可以定义并初始化.rect
,稍后将使用它来绘制玩家。要使用这个新类,您需要创建一个新对象并更改绘图代码。展开下面的代码块以查看所有内容:
展开查看完整代码显示隐藏
运行这段代码。您会在屏幕中间大致看到一个白色矩形:
如果将第 59 行更改为 ,您认为会发生什么screen.blit(player.surf, player.rect)
?尝试一下,看看:
55# Fill the screen with black
56screen.fill((0, 0, 0))
57
58# Draw the player on the screen
59screen.blit(player.surf, player.rect) 60
61# Update the display
62pygame.display.flip()
当您将 a 传递Rect
给时.blit()
,它会使用左上角的坐标来绘制曲面。稍后您将使用它来让您的玩家移动!
6.2 用户输入
pygame
到目前为止,您已经学习了如何在屏幕上设置和绘制对象。现在,真正的乐趣开始了!您将使玩家可以使用键盘进行控制。
之前,您看到它pygame.event.get()
返回事件队列中的事件列表,您可以在其中扫描KEYDOWN
事件类型。嗯,这不是读取按键的唯一方法。pygame
还提供pygame.event.get_pressed()
,它返回一个包含队列中所有当前事件的字典。KEYDOWN
将其放在事件处理循环之后的游戏循环中。这将返回一个字典,其中包含在每帧开始时按下的键:
54# Get the set of keys pressed and check for user input
55pressed_keys = pygame.key.get_pressed()
接下来,您编写一个方法来Player
接受该字典。这将根据按下的键定义精灵的行为。看起来可能是这样的:
29# Move the sprite based on user keypresses
30def update(self, pressed_keys):
31 if pressed_keys[K_UP]:
32 self.rect.move_ip(0, -5)
33 if pressed_keys[K_DOWN]:
34 self.rect.move_ip(0, 5)
35 if pressed_keys[K_LEFT]:
36 self.rect.move_ip(-5, 0)
37 if pressed_keys[K_RIGHT]:
38 self.rect.move_ip(5, 0)
K_UP
、K_DOWN
、 、K_LEFT
、K_RIGHT
分别对应键盘上的方向键。如果该键的字典条目是True
,则该键按下,您可以将玩家移动.rect
到正确的方向。这里你使用.move_ip()
,它代表移动到位,来移动当前Rect
。
然后,您可以调用.update()
每一帧来移动玩家精灵以响应按键。在调用后立即添加此调用.get_pressed()
:
52# Main loop
53while running:
54 # for loop through the event queue
55 for event in pygame.event.get():
56 # Check for KEYDOWN event
57 if event.type == KEYDOWN:
58 # If the Esc key is pressed, then exit the main loop
59 if event.key == K_ESCAPE:
60 running = False
61 # Check for QUIT event. If QUIT, then set running to false.
62 elif event.type == QUIT:
63 running = False
64
65 # Get all the keys currently pressed
66 pressed_keys = pygame.key.get_pressed()
67
68 # Update the player sprite based on user keypresses 69 player.update(pressed_keys) 70
71 # Fill the screen with black
72 screen.fill((0, 0, 0))
现在您可以使用箭头键在屏幕上移动播放器矩形:
您可能会注意到两个小问题:
- 如果按住某个键,玩家矩形可以快速移动。你稍后会处理这个问题。
- 玩家矩形可以移出屏幕。现在让我们解决这个问题。
为了让玩家保持在屏幕上,您需要添加一些逻辑来检测玩家是否rect
会移出屏幕。为此,您需要检查rect
坐标是否已超出屏幕边界。如果是这样,那么您指示程序将其移回边缘:
25# Move the sprite based on user keypresses
26def update(self, pressed_keys):
27 if pressed_keys[K_UP]:
28 self.rect.move_ip(0, -5)
29 if pressed_keys[K_DOWN]:
30 self.rect.move_ip(0, 5)
31 if pressed_keys[K_LEFT]:
32 self.rect.move_ip(-5, 0)
33 if pressed_keys[K_RIGHT]:
34 self.rect.move_ip(5, 0)
35
36 # Keep player on the screen 37 if self.rect.left < 0: 38 self.rect.left = 0 39 if self.rect.right > SCREEN_WIDTH: 40 self.rect.right = SCREEN_WIDTH 41 if self.rect.top <= 0: 42 self.rect.top = 0 43 if self.rect.bottom >= SCREEN_HEIGHT: 44 self.rect.bottom = SCREEN_HEIGHT
这里,不使用,而是直接更改、、、 或.move()
对应的坐标。测试一下,你会发现玩家矩形不能再移出屏幕。.top
.bottom
.left
.right
现在让我们添加一些敌人!
6.3 敌人
没有敌人的游戏算什么?您将使用已经学到的技术来创建基本的敌人类别,然后创建大量敌人以供玩家躲避。首先,导入random
库:
4# Import random for random numbers
5import random
然后创建一个名为 的新精灵类Enemy
,遵循您使用的相同模式Player
:
55# Define the enemy object by extending pygame.sprite.Sprite
56# The surface you draw on the screen is now an attribute of 'enemy'
57class Enemy(pygame.sprite.Sprite):
58 def __init__(self):
59 super(Enemy, self).__init__()
60 self.surf = pygame.Surface((20, 10))
61 self.surf.fill((255, 255, 255))
62 self.rect = self.surf.get_rect(
63 center=(
64 random.randint(SCREEN_WIDTH + 20, SCREEN_WIDTH + 100),
65 random.randint(0, SCREEN_HEIGHT),
66 )
67 )
68 self.speed = random.randint(5, 20)
69
70 # Move the sprite based on speed
71 # Remove the sprite when it passes the left edge of the screen
72 def update(self):
73 self.rect.move_ip(-self.speed, 0)
74 if self.rect.right < 0:
75 self.kill()
Enemy
和之间有四个显着区别Player
:
-
在第 62 至 67 行,您更新
rect
为沿屏幕右边缘的随机位置。矩形的中心就在屏幕之外。它位于距离右边缘 20 到 100 像素之间的某个位置,并且位于顶部边缘和底部边缘之间的某个位置。 -
在第 68 行,您定义
.speed
为 5 到 20 之间的随机数。这指定了敌人向玩家移动的速度。 -
在第 73 至 76 行,您定义了
.update()
。由于敌人会自动移动,所以不需要任何参数。相反,将敌人按照创建时定义的位置.update()
移向屏幕左侧。.speed
-
在第 74 行,您检查敌人是否已移出屏幕。为了确保
Enemy
完全离开屏幕并且不会在仍然可见时消失,您需要检查 的右侧是否.rect
已经超出了屏幕的左侧。一旦敌人离开屏幕,您就可以调用.kill()
以防止其被进一步处理。
那么,它有什么.kill()
作用呢?要弄清楚这一点,您必须了解Sprite Groups。
七、精灵组
提供的另一个超级有用的类pygame
是Sprite Group. 这是一个包含一组Sprite
对象的对象。那么为什么要使用它呢?您不能只跟踪Sprite
列表中的对象吗?嗯,可以,但是使用 a 的优点Group
在于它公开的方法。这些方法有助于检测是否有任何Enemy
与 发生冲突Player
,这使得更新更加容易。
让我们看看如何创建精灵组。您将创建两个不同的Group
对象:
- 第一个
Group
将容纳Sprite
游戏中的所有内容。 - 第二个
Group
将只容纳Enemy
物体。
代码如下:
82# Create the 'player'
83player = Player()
84
85# Create groups to hold enemy sprites and all sprites 86# - enemies is used for collision detection and position updates 87# - all_sprites is used for rendering 88enemies = pygame.sprite.Group() 89all_sprites = pygame.sprite.Group() 90all_sprites.add(player) 91
92# Variable to keep the main loop running
93running = True
当您调用 时.kill()
,将从它所属的Sprite
每个对象中删除。Group
这也会删除对 的引用Sprite
,从而允许Python 的垃圾收集器根据需要回收内存。
现在您已经有了一个all_sprites
组,您可以更改对象的绘制方式。您可以迭代 中的所有内容,而不是.blit()
仅仅调用:Player
all_sprites
117# Fill the screen with black
118screen.fill((0, 0, 0))
119
120# Draw all sprites 121for entity in all_sprites: 122 screen.blit(entity.surf, entity.rect) 123
124# Flip everything to the display
125pygame.display.flip()
现在,放入的任何内容都all_sprites
将在每一帧中绘制,无论是敌人还是玩家。
只有一个问题……你没有任何敌人!你可以在游戏开始时创建一堆敌人,但当他们在几秒钟后全部离开屏幕时,游戏很快就会变得无聊。相反,让我们探索如何随着游戏的进展保持稳定的敌人供应。
八、自定义事件
该设计要求敌人定期出现。这意味着在设定的时间间隔内,您需要做两件事:
- 创建一个新的
Enemy
. - 将其添加到
all_sprites
和enemies
。
您已经拥有处理随机事件的代码。事件循环旨在查找每帧发生的随机事件并适当地处理它们。幸运的是,它pygame
并不限制您只能使用它定义的事件类型。您可以定义自己的事件来根据您的需要进行处理。
让我们看看如何创建每隔几秒生成一次的自定义事件。您可以通过命名来创建自定义事件:
78# Create the screen object
79# The size is determined by the constant SCREEN_WIDTH and SCREEN_HEIGHT
80screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
81
82# Create a custom event for adding a new enemy 83ADDENEMY = pygame.USEREVENT + 1 84pygame.time.set_timer(ADDENEMY, 250) 85
86# Instantiate player. Right now, this is just a rectangle.
87player = Player()
pygame
在内部将事件定义为整数,因此您需要使用唯一的整数定义一个新事件。最后一个事件pygame
保留名为USEREVENT
,因此ADDENEMY = pygame.USEREVENT + 1
在第 83 行定义可确保它是唯一的。
接下来,您需要在整个游戏过程中定期将这个新事件插入到事件队列中。这就是该time模块的用武之地。第 84 行每 250 毫秒触发一次新ADDENEMY
事件,即每秒四次。.set_timer()
您可以在游戏循环之外调用,因为您只需要一个计时器,但它会在整个游戏过程中触发。
添加代码来处理您的新事件:
100# Main loop
101while running:
102 # Look at every event in the queue
103 for event in pygame.event.get():
104 # Did the user hit a key?
105 if event.type == KEYDOWN:
106 # Was it the Escape key? If so, stop the loop.
107 if event.key == K_ESCAPE:
108 running = False
109
110 # Did the user click the window close button? If so, stop the loop.
111 elif event.type == QUIT:
112 running = False
113
114 # Add a new enemy? 115 elif event.type == ADDENEMY: 116 # Create the new enemy and add it to sprite groups 117 new_enemy = Enemy() 118 enemies.add(new_enemy) 119 all_sprites.add(new_enemy) 120
121 # Get the set of keys pressed and check for user input
122 pressed_keys = pygame.key.get_pressed()
123 player.update(pressed_keys)
124
125 # Update enemy position 126 enemies.update()
每当事件处理程序在第 115 行看到新ADDENEMY
事件时,它就会创建 anEnemy
并将其添加到enemies
和all_sprites
。由于 Enemy
是all_sprites
,所以每一帧都会绘制它。您还需要调用enemies.update()
第 126 行,该行更新 中的所有内容enemies
,以确保它们正确移动:
然而,这并不是有一个团体的唯一原因enemies
。
九、碰撞检测
您的游戏设计要求每当敌人与玩家发生碰撞时游戏就结束。检查碰撞是游戏编程的一项基本技术,通常需要一些重要的数学来确定两个精灵是否会相互重叠。
这就是像这样的框架pygame
派上用场的地方!编写碰撞检测代码很乏味,但是pygame
有很多碰撞检测方法可供您使用。
在本教程中,您将使用一种名为 的方法.spritecollideany(),其读作“sprite collide any”。该方法接受 aSprite
和 aGroup
作为参数。它查看 中的每个对象Group
并检查其是否.rect
与.rect
的相交Sprite
。如果是,则返回True
。否则,它返回False
。这对于这个游戏来说是完美的,因为你需要检查单个是否player
与 a 之一Group
碰撞enemies
。
代码如下:
130# Draw all sprites
131for entity in all_sprites:
132 screen.blit(entity.surf, entity.rect)
133
134# Check if any enemies have collided with the player 135if pygame.sprite.spritecollideany(player, enemies): 136 # If so, then remove the player and stop the loop 137 player.kill() 138 running = False
第 135 行测试是否player
与 中的任何对象发生碰撞enemies
。如果是,则player.kill()
调用 then 将其从其所属的每个组中删除。由于唯一被渲染的对象位于 中all_sprites
,因此player
将不再渲染。一旦玩家被杀死,您也需要退出游戏,因此您running = False
在第 138 行设置了跳出游戏循环。
至此,您已经掌握了游戏的基本元素:
现在,让我们稍微打扮一下它,使其更具可玩性,并添加一些高级功能以帮助它脱颖而出。
十、精灵图像
好吧,你有一个游戏,但说实话……有点难看。玩家和敌人只是黑色背景上的白色块。当Pong刚出现时,这已经是最先进的了,但现在已经不再适用了。让我们用一些更酷的图像替换所有那些无聊的白色矩形,这将使游戏感觉像一个真正的游戏。
Surface
之前,您了解到磁盘上的图像可以在模块的帮助下加载到 a 中image
。在本教程中,我们为玩家制作了一架小型喷气机,为敌人制作了一些导弹。欢迎您使用此艺术作品,绘制自己的艺术作品,或下载一些免费的游戏艺术资源来使用。您可以单击下面的链接下载本教程中使用的艺术作品:
示例代码: 单击此处下载本教程中使用的 PyGame 示例项目的源代码。
10.1 改变对象构造函数
在使用图像来表示玩家和敌人精灵之前,您需要对其构造函数进行一些更改。下面的代码替换了之前使用的代码:
7# Import pygame.locals for easier access to key coordinates
8# Updated to conform to flake8 and black standards
9# from pygame.locals import *
10from pygame.locals import (
11 RLEACCEL, 12 K_UP,
13 K_DOWN,
14 K_LEFT,
15 K_RIGHT,
16 K_ESCAPE,
17 KEYDOWN,
18 QUIT,
19)
20
21# Define constants for the screen width and height
22SCREEN_WIDTH = 800
23SCREEN_HEIGHT = 600
24
25
26# Define the Player object by extending pygame.sprite.Sprite
27# Instead of a surface, use an image for a better-looking sprite
28class Player(pygame.sprite.Sprite):
29 def __init__(self):
30 super(Player, self).__init__()
31 self.surf = pygame.image.load("jet.png").convert() 32 self.surf.set_colorkey((255, 255, 255), RLEACCEL) 33 self.rect = self.surf.get_rect()
让我们稍微解压一下第 31 行。pygame.image.load()
从磁盘加载图像。您将文件的路径传递给它。它返回 a Surface
,并且.convert()
调用优化Surface
,使将来的.blit()
调用更快。
第 32 行用于.set_colorkey()
指示颜色pygame
将呈现为透明。在本例中,您选择白色,因为这是喷射图像的背景颜色。RLEACCEL常量是一个可选参数,有助于pygame
在非加速显示器上更快地渲染。这被添加到pygame.locals
第 11 行的导入语句中。
其他什么都不需要改变。该图像仍然是一个Surface
,只不过现在上面画了一张图片。你仍然以同样的方式使用它。
以下是类似的外观变化Enemy
:
59# Define the enemy object by extending pygame.sprite.Sprite
60# Instead of a surface, use an image for a better-looking sprite
61class Enemy(pygame.sprite.Sprite):
62 def __init__(self):
63 super(Enemy, self).__init__()
64 self.surf = pygame.image.load("missile.png").convert() 65 self.surf.set_colorkey((255, 255, 255), RLEACCEL) 66 # The starting position is randomly generated, as is the speed
67 self.rect = self.surf.get_rect(
68 center=(
69 random.randint(SCREEN_WIDTH + 20, SCREEN_WIDTH + 100),
70 random.randint(0, SCREEN_HEIGHT),
71 )
72 )
73 self.speed = random.randint(5, 20)
现在运行该程序应该会显示这与您之前玩的游戏相同,只是现在您添加了一些带有图像的漂亮图形皮肤。但为什么只停留在让玩家和敌人精灵看起来好看呢?让我们添加几朵飘过的云,给人一种喷气式飞机在天空中飞翔的感觉。
10.2 添加背景图片
对于背景云,您使用与Player
和相同的原则Enemy
:
- 创建
Cloud
班级。 - 添加云的图像。
- 创建一个将向屏幕左侧
.update()
移动的方法。cloud
- 创建自定义事件和处理程序以
cloud
按设定的时间间隔创建新对象。 - 将新创建的对象添加到名为 的
cloud
新对象中。Group
clouds
clouds
在游戏循环中更新并绘制。
看起来像这样Cloud
:
83# Define the cloud object by extending pygame.sprite.Sprite
84# Use an image for a better-looking sprite
85class Cloud(pygame.sprite.Sprite):
86 def __init__(self):
87 super(Cloud, self).__init__()
88 self.surf = pygame.image.load("cloud.png").convert()
89 self.surf.set_colorkey((0, 0, 0), RLEACCEL)
90 # The starting position is randomly generated
91 self.rect = self.surf.get_rect(
92 center=(
93 random.randint(SCREEN_WIDTH + 20, SCREEN_WIDTH + 100),
94 random.randint(0, SCREEN_HEIGHT),
95 )
96 )
97
98 # Move the cloud based on a constant speed
99 # Remove the cloud when it passes the left edge of the screen
100 def update(self):
101 self.rect.move_ip(-5, 0)
102 if self.rect.right < 0:
103 self.kill()
这看起来应该很熟悉。和 几乎一样Enemy
。
要让云以一定的间隔出现,您将使用类似于创建新敌人的事件创建代码。将其放在敌人创建事件的正下方:
116# Create custom events for adding a new enemy and a cloud
117ADDENEMY = pygame.USEREVENT + 1
118pygame.time.set_timer(ADDENEMY, 250)
119ADDCLOUD = pygame.USEREVENT + 2 120pygame.time.set_timer(ADDCLOUD, 1000)
这表示在创建下一个 .txt 文件之前等待 1000 毫秒或一秒cloud
。
接下来,创建一个新的Group
来保存每个新创建的cloud
:
125# Create groups to hold enemy sprites, cloud sprites, and all sprites
126# - enemies is used for collision detection and position updates
127# - clouds is used for position updates
128# - all_sprites is used for rendering
129enemies = pygame.sprite.Group()
130clouds = pygame.sprite.Group() 131all_sprites = pygame.sprite.Group()
132all_sprites.add(player)
ADDCLOUD
接下来,在事件处理程序中添加新事件的处理程序:
137# Main loop
138while running:
139 # Look at every event in the queue
140 for event in pygame.event.get():
141 # Did the user hit a key?
142 if event.type == KEYDOWN:
143 # Was it the Escape key? If so, then stop the loop.
144 if event.key == K_ESCAPE:
145 running = False
146
147 # Did the user click the window close button? If so, stop the loop.
148 elif event.type == QUIT:
149 running = False
150
151 # Add a new enemy?
152 elif event.type == ADDENEMY:
153 # Create the new enemy and add it to sprite groups
154 new_enemy = Enemy()
155 enemies.add(new_enemy)
156 all_sprites.add(new_enemy)
157
158 # Add a new cloud? 159 elif event.type == ADDCLOUD: 160 # Create the new cloud and add it to sprite groups 161 new_cloud = Cloud() 162 clouds.add(new_cloud) 163 all_sprites.add(new_cloud)
最后,确保clouds
每帧都更新:
167# Update the position of enemies and clouds
168enemies.update()
169clouds.update() 170
171# Fill the screen with sky blue
172screen.fill((135, 206, 250))
第 172 行更新了原来的内容screen.fill()
,使屏幕充满宜人的天蓝色。您可以将此颜色更改为其他颜色。也许你想要一个紫色天空的外星世界,霓虹绿的有毒荒原,或者红色的火星表面!
请注意,每个新的Cloud
和 都Enemy
被添加到andall_sprites
和 中。这样做是因为每个组都有不同的用途:clouds
enemies
- 渲染是使用
all_sprites
. - 位置更新是使用
clouds
和完成的enemies
。 - 碰撞检测是使用 完成的
enemies
。
您可以创建多个组,以便可以更改精灵移动或行为的方式,而不会影响其他精灵的移动或行为。
十一、游戏速度
在测试游戏时,您可能已经注意到敌人移动得有点快。如果没有,那也没关系,因为此时不同的机器会看到不同的结果。
原因是游戏循环处理帧的速度与处理器和环境允许的速度一样快。由于所有精灵每帧移动一次,因此它们每秒可以移动数百次。每秒处理的帧数称为帧速率,正确掌握这一点是可玩游戏和容易被遗忘的游戏之间的区别。
通常,您需要尽可能高的帧速率,但对于此游戏,您需要稍微放慢帧速率才能玩游戏。幸运的是,该模块time
包含一个Clock专门为此目的而设计的模块。
用于Clock
建立可播放的帧速率只需要两行代码。第一个Clock
在游戏循环开始之前创建一个新的:
106# Setup the clock for a decent framerate
107clock = pygame.time.Clock()
第二个调用.tick()
通知pygame
程序已到达帧末尾:
188# Flip everything to the display
189pygame.display.flip()
190
191# Ensure program maintains a rate of 30 frames per second 192clock.tick(30)
传递的参数.tick()
确定所需的帧速率。为此,.tick()
根据所需的帧速率计算每帧应花费的毫秒数。.tick()
然后,它将该数字与自上次调用以来经过的毫秒数进行比较。如果没有经过足够的时间,则.tick()
延迟处理以确保它永远不会超过指定的帧速率。
传递较小的帧速率将导致每帧有更多的时间进行计算,而较大的帧速率则提供更流畅(并且可能更快)的游戏玩法:
尝试一下这个数字,看看什么最适合您!
11.1 声音特效
到目前为止,您已经专注于游戏的游戏玩法和视觉效果。现在让我们探索如何为您的游戏添加一些听觉风味。pygame
提供mixer处理所有与声音相关的活动。您将使用此模块的类和方法为各种操作提供背景音乐和声音效果。
该名称mixer
指的是该模块将各种声音混合成一个有凝聚力的整体。使用music子模块,您可以流式传输各种格式的单个声音文件,例如MP3、Ogg和Mod。您还可以使用Ogg 或未压缩的 WAVSound
格式保存要播放的单个音效。所有播放都在后台进行,因此当您播放 a 时,该方法会在声音播放时立即返回。Sound
注意:文档指出 MP3 支持有限,不支持的格式可能会导致系统崩溃pygame。本文中引用的声音已经过测试,我们建议在发布游戏之前彻底测试所有声音。
与大多数事情一样pygame
,使用mixer
从初始化步骤开始。幸运的是,这已经由 处理了pygame.init()
。pygame.mixer.init()
如果您想更改默认值,只需调用:
106# Setup for sounds. Defaults are good. 107pygame.mixer.init() 108
109# Initialize pygame
110pygame.init()
111
112# Set up the clock for a decent framerate
113clock = pygame.time.Clock()
pygame.mixer.init()
接受多个参数,但默认值在大多数情况下都可以正常工作。请注意,如果要更改默认值,需要pygame.mixer.init()
在调用 之前调用pygame.init()
. 否则,无论您如何更改,默认值都将生效。
系统初始化后,您可以进行声音和背景音乐设置:
135# Load and play background music
136# Sound source: http://ccmixter.org/files/Apoxode/59262
137# License: https://creativecommons.org/licenses/by/3.0/
138pygame.mixer.music.load("Apoxode_-_Electric_1.mp3")
139pygame.mixer.music.play(loops=-1)
140
141# Load all sound files
142# Sound sources: Jon Fincher
143move_up_sound = pygame.mixer.Sound("Rising_putter.ogg")
144move_down_sound = pygame.mixer.Sound("Falling_putter.ogg")
145collision_sound = pygame.mixer.Sound("Collision.ogg")
第 138 和 139 行加载背景声音剪辑并开始播放。您可以通过设置命名参数来让声音剪辑循环播放并且永不结束loops=-1
。
第 143 至 145 行加载了三种声音,您将用于各种声音效果。前两个是上升和下降的声音,当玩家向上或向下移动时播放。最后一个是发生碰撞时使用的声音。您还可以添加其他声音,例如创建时的声音Enemy
,或游戏结束时的最终声音。
那么,如何使用音效呢?您希望在特定事件发生时播放每种声音。例如,当船向上移动时,你想玩move_up_sound
。.play()
因此,只要处理该事件,您就添加一个调用 。在设计中,这意味着将以下调用添加到.update()
for Player
:
26# Define the Player object by extending pygame.sprite.Sprite
27# Instead of a surface, use an image for a better-looking sprite
28class Player(pygame.sprite.Sprite):
29 def __init__(self):
30 super(Player, self).__init__()
31 self.surf = pygame.image.load("jet.png").convert()
32 self.surf.set_colorkey((255, 255, 255), RLEACCEL)
33 self.rect = self.surf.get_rect()
34
35 # Move the sprite based on keypresses
36 def update(self, pressed_keys):
37 if pressed_keys[K_UP]:
38 self.rect.move_ip(0, -5)
39 move_up_sound.play() 40 if pressed_keys[K_DOWN]:
41 self.rect.move_ip(0, 5)
42 move_down_sound.play()
对于玩家和敌人之间的碰撞,您可以在检测到碰撞时播放声音:
201# Check if any enemies have collided with the player
202if pygame.sprite.spritecollideany(player, enemies):
203 # If so, then remove the player
204 player.kill()
205
206 # Stop any moving sounds and play the collision sound
207 move_up_sound.stop() 208 move_down_sound.stop() 209 collision_sound.play() 210
211 # Stop the loop
212 running = False
在这里,您首先停止任何其他声音效果,因为在碰撞中玩家不再移动。然后播放碰撞声音并从那里继续执行。
最后,当游戏结束时,所有声音都应该停止。无论游戏因碰撞而结束还是用户手动退出,都是如此。为此,请在循环后的程序末尾添加以下行:
220# All done! Stop and quit the mixer.
221pygame.mixer.music.stop()
222pygame.mixer.quit()
从技术上讲,最后几行不是必需的,因为程序在此之后立即结束。但是,如果您稍后决定向游戏添加介绍屏幕或退出屏幕,则游戏结束后可能会运行更多代码。
就是这样!再次测试一下,您应该看到类似这样的内容:
11.2 关于来源的说明
您可能已经注意到加载背景音乐时第 136-137 行的注释,其中列出了音乐的来源以及知识共享许可证的链接。这样做是因为该声音的创造者需要它。许可证要求规定,为了使用声音,必须提供正确的归属和许可证的链接。
以下是一些音乐、声音和艺术来源,您可以搜索有用的内容:
- OpenGameArt.org:声音、音效、精灵和其他艺术作品
- Kenney.nl:声音、音效、精灵和其他艺术作品
- 2D 玩家艺术:精灵和其他艺术作品
- CC Mixter:声音和音效
- Freesound:声音和音效
当您制作游戏并使用从其他来源下载的内容(例如艺术、音乐或代码)时,请确保您遵守这些来源的许可条款。
十二、结论
通过本教程,您已经了解了游戏编程与pygame
标准过程编程的不同之处。您还学习了如何:
- 实施事件循环
- 在屏幕上绘制项目
- 播放音效和音乐
- 处理用户输入
为此,您使用了pygame
模块的子集,包括display
、mixer
、music
、 time
、image
、event
和key
模块。您还使用了多个pygame
类,包括Rect
、Surface
、Sound
和Sprite
。pygame
但这些只是我们能做的事情的皮毛!查看官方pygame文档以获取可用模块和类的完整列表。
您可以通过单击下面的链接找到本文的所有代码、图形和声音文件:
示例代码: 单击此处下载本教程中使用的 PyGame 示例项目的源代码。
也请随意在下面发表评论。快乐Python!
参考文章:PyGame: A Primer on Game Programming in Python – Real Python