用Python Pygame制作动画

我们之前学过用 Python Pygame 函数绘制图形,本节将学习用 Python Pygame 制作动画

Python Pygame 加载图片

除了在屏幕上自己绘制各种形状,我们还可以在程序中加载图片。在 Pygame 中,操作图片最简单的方法就是调用 image() 函数。

我们来尝试在 Pygame 窗口中加载一张小狗的照片。首先,把要加载的照片复制到和保存 Python 程序相同的位置。这样,程序运行时 Python 就能很方便地找到这个文件,而不需要指定图片存储的路径。代码如下。
import pygame
pygame.init()
windowSurface = pygame.display.set_mode([800,600])
pic = pygame.image.load("dog1.png")
windowSurface.blit(pic, (100,200))
Running=True
while Running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            Running=False
    pygame.display.update()
pygame.quit()

这段代码中只有突出显示的那两行代码是新添加的,其他的代码我们在前面都见过。其中 pygame.image.load() 函数从硬盘上加载一个图像,图像文件的名称是“dog1.png”,并创建一个名为 pic 的 Surface。然后我们调用 blit() 函数,将像素从一个 Surface 复制到另一个 Surface 之上。

windowSurface.blit(pic, (100,100)) 是把 pic 对象复制到 screen 这个 Surface 上。在这个例子中,我们告诉 blit() 想要将pic绘制到位置(100,200),也就是屏幕左上角向右 100 像素且向下 200 像素的位置。

这里我们需要使用 blit() 函数,是因为 pygame.image.load() 函数与前面绘制函数的工作方式不同。所有的 pygame.draw 函数都接受一个 Surface 为参数,因此,通过将 screen 传递给 pygame.draw.line(),我们就能够让 pygame.draw.line() 绘制到显示窗口。

但是,pygame.image.load() 函数并不接受一个 Surface 作为参数,相反,它自动为图像创建一个新的 Surface。除非使用 blit(),否则,图像不会出现在最初的绘制屏幕上。

运行这个程序,得到的显示结果如图 1 所示。

Python Pygame 加载图片
图 1

Python Pygame 移动图片

除了可以加载小狗图片,我们还可以通过改变图片的位置,让小狗动起来。我们可以在游戏循环中,更新图片的坐标位置,然后每次执行循环的时候在新的位置绘制图片,这样一来,看上去就好像小狗在移动一样。代码如下。
import pygame
pygame.init()
windowSurface = pygame.display.set_mode([800,600])
pic = pygame.image.load("dog1.png")
picX = 0
picY = 200

Running=True
while Running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            Running=False

    picX += 1
    windowSurface.blit(pic, (picX,picY))
    pygame.display.update()
pygame.quit()

其中,有变化的代码还是突出显示的部分。在这里,添加两个变量 picX 和 picY,表示图像在屏幕上的 X 坐标和 Y 坐标。然后在 while 循环中,每次将 picX 变量加 1,这样图像就可以向右移动。

前面我们介绍过,+= 操作符就相当于左边变量加 1。我们把 blit() 函数也放到了 while 循环中,并且用 picX 和 picY 分别表示 X 坐标和 Y 坐标,这样每次都可以更新显示图片了。

运行代码来看一下效果,如图 2 所示。

Python Pygame 移动图片
图 2

我们可以看到,小狗会一直向右移动,直到离开窗口,并且还在窗口上留下像素的一个轨迹,这个轨迹是每一帧向右移动的图像之后留下的、与上一张图片偏离出来的一些像素。

如果想要修正这个问题,可以在每次复制图像之前,都调用一次 windowSurface.fill() 函数,来填充要绘制的窗口。例如,可以用黑色来进行填充。还是老样子,突出显示的代码表示新增代码。完整代码如下。
import pygame
pygame.init()
windowSurface = pygame.display.set_mode([800,600])
pic = pygame.image.load("dog1.png")
picX = 0
picY = 200
BLACK=(0,0,0)

Running=True
while Running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            Running=False

    windowSurface.fill(BLACK)
    picX += 1
    windowSurface.blit(pic, (picX,picY))   
    pygame.display.update()
pygame.quit()
我们定义了一个名为 BLACK 的常量,并且将元组 (0, 0, 0) 赋值给它,这个元组表示黑色。然后,在 while 循环中增加了一句 windowSurface.fill(BLACK),表示每次循环都要重新填充窗口。

再来看一下执行效果,如图 3 所示。

Python Pygame 移动图片
图 3
 
这一次在移动的图像之后没有留下一条像素的轨迹。通过用黑色的像素填充窗口,将窗口内每一帧旧的图像都擦除掉了之后,再在新的位置绘制新的图像。这就实现了平滑移动的效果,如图 4 所示。


图 4

碰撞检测

现在又有一个新的问题,就是小狗很快就会跑出屏幕,为了让程序更有趣味性,我们可以为屏幕设置一个边界,当检测到小狗碰到了这个边界,就改变它的移动方向,让它掉头往回跑。这里会用到碰撞检测这个概念。

碰撞检测(collision detection)负责检查并处理计算屏幕上的两个物体发生彼此接触(也就是发生碰撞)的情况。例如,如果玩家角色接触到了一个敌人,它可能会损失生命值。

对于我们这个例子来说,左边界很容易判断,当小狗的 X 坐标为 0 的时候,就是窗口的最左边,所以小狗的 X 坐标小于等于 0 就表示碰到了左边界。

右边界稍微复杂一点。我们的窗口宽度是 800,那么 800 是窗口的右边界。但是,除了要考虑小狗的 X 坐标,还要考虑小狗的宽度,也就是只有在 X 坐标加上小狗的宽度值大于等于 800 的时候,小狗就会跑出右边界。

当我们检测到小狗跑出了边界,就要把移动的像素修改为对应的负值,从而改变小狗移动的方向;并且为了看上去更生动形象,当小狗向右移动时,使用头朝右的图片,如图 5(a)所示;当小狗向左移动时,就要使用头朝左的图片,如图 5(b)所示。
移动像素为负
(a)

(b)
图 5

来看一下具体的代码。这次,还是将新增代码突出显示出来。代码如下。
import pygame
pygame.init()
windowSurface = pygame.display.set_mode([800,600])
pic = pygame.image.load("dog1.png")
picX = 0
picY = 200
BLACK=(0,0,0)
speed=1

Running=True
while Running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            Running=False
    windowSurface.fill(BLACK)
    picX += speed
    if picX +pic.get_width() >=800:
        speed = -speed
        pic = pygame.image.load("dog2.png")
    elif picX <= 0:  
        speed = -speed
        pic = pygame.image.load("dog1.png")
    windowSurface.blit(pic, (picX,picY))   
    pygame.display.update()
pygame.quit()

我们用变量 speed 来表示每次小狗移动的像素,而不再是将其设置为一个常量。然后,在游戏循环中,每一次执行循环的时候,使用变量 speed 来修改小狗移动的 X 坐标。

后面通过增加碰撞检测逻辑来检测是否碰到了屏幕的左边和右边的边界。

首先,通过判断 picX + pic.get_width() 之和是否大于等于屏幕的 800像 素的宽度,检测小狗是否碰到或超过了右边界。如果为True,设置 speed = -speed,让 speed 取反成为正值,从而改变小狗移动的方向;并且将变量 pic 设置为重新加载的图片“dog2.png”的对象。

否则,查看 picX 是否小于等于 0,检测是否已经碰到或超过左边屏幕。如果为 True,设置 speed = -speed,让 speed 取反成为负值,从而改变小狗移动的方向;并且将变量设置为加载的图片“dog1.png”的对象。

运行代码,现在看到的效果如图 6 所示。

Python Pygame碰撞检测
 
Python Pygame碰撞检测
图 6

设置帧速率

帧速率(frame rate)是指程序每秒钟绘制的图像的数目,用每秒多少帧(Frames Per Second,FPS)来度量。在前面的代码中,每次通过游戏循环的时候,我们将图像移动 1 个像素。

但是,如果提高移动速度,每次移动 5 个像素,那么对于性能好的计算机,由于每秒生成数百帧,这会导致图像移动得太快而看不清楚。这是因为平滑的动画需要保持每秒 30~ 60 帧的速率,因此,我们不需要每秒数百帧那么快。

pygame.time 模块中的 Clock 对象可以帮助避免程序运行得过快。Clock 对象有一个tick()方法,它接收的参数表示想要游戏运行速度是多少 FPS。FPS 越高,游戏运行得越快。通过调用 Clock 对象的 tick() 方法,可以让程序等待足够长的时间,以便无论计算机自身的速度有多快,程序都可以每秒钟迭代指定次数。这就确保了游戏的运行速度不会超过预期。

在每次游戏循环的最后,在调用了 pygame.display.update() 之后,还应该调用 Clock 对象的 tick() 方法。根据前一次调用 tick() 之后经过了多长时间,来计算需要暂停多长时间。我们来对前面的示例稍作修改,增加对帧速率的限制,新增的代码还是突出显示出来。完整代码如下。
import pygame
pygame.init()
windowSurface = pygame.display.set_mode([800,600])
pic = pygame.image.load("dog1.png")
picX = 0
picY = 200
BLACK=(0,0,0)
speed=5
timer = pygame.time.Clock()
Running=True
while Running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            Running=False
    windowSurface.fill(BLACK)
    picX += speed
    if picX +pic.get_width() >=800:
        speed = -speed
        pic = pygame.image.load("dog2.png")
    elif picX <= 0:  
        speed = -speed
        pic = pygame.image.load("dog1.png")
    windowSurface.blit(pic, (picX,picY))   
    pygame.display.update()
    timer.tick(60)
pygame.quit()

我们将 speed 变量的值从 1 改为 5,并且新创建了一个 Clock 对象,将其赋值给 timer 变量。然后我们在游戏循环调用 tick() 方法,它会告诉名为 timer 的时钟每秒钟只“滴答”60 次,从而使得帧速率保持在 60FPS,以防止程序运行得太快。可以看到,虽然移动速度比之前要快些,但是图像的动画还是很平滑的,如图 7 所示。


图 7