1.4    增加小方块

虽然游戏可以正常运行,但是窗口显得有些空旷。让我们来丰富一下游戏内容,让它变得更加生动有趣吧!

1.4.1  绘制小方块

现在,让我们尝试添加一个小方块到游戏窗口中,代码如下所示:

01\04.py

# 主循环

isRunning = True

while isRunning:

    for event in pygame.event.get():

        if event.type == pygame.QUIT:

            isRunning = False

 

    # 小方块

    pygame.draw.rect(screen, (255, 255, 255), ((5, 5), (30, 30)), 0)

 

    # 刷新显示

    pygame.display.flip()

 

    # 每秒60

    clock.tick(60)

# 退出 Pygame

pygame.quit()

为了节省篇幅,这里只展示了主循环的代码。运行程序,画面如1‑19所示。

119增加小方块

pygame.draw.rect()方法用于绘制矩形,其格式如下:

pygame.draw.rect(surface,color,Rect,width=0)

参数含义如下:

l  surface:在哪个表面(surface)上绘制矩形。本例中使用的是 screen 表面,因此矩形会直接绘制在屏幕上。也可以创建其他表面,先在表面上绘制,然后进行放大、缩放等处理,最后再显示在屏幕上。Surface是可以用来绘制的对象,可以理解为画布,屏幕、图片和文本等等都是Surface对象。

l  color:线条(或填充)的颜色,参数为元祖格式,其中的值是 RGB(红、绿、蓝) 格式,如 (255,255,255) 代表白色。

l  Rect:矩形的位置和大小,为Rect对象,格式可以是((x, y), (width, height))。第一个元组表示矩形的左上角坐标,在此例中是 (5, 5),第二个元组 (30, 30) 表示矩形的宽度和高度,此处矩形的宽度为 30,高度也为 30

l  Width:矩形轮廓线的宽度。默认值为0,表示填充整个矩形,不绘制轮廓线。

pygame.display.flip()方法用于将屏幕缓冲区的内容显示在实际屏幕上,实现画面更新。一般在主循环的最后,处理完所有的事件和绘制之后才调用该方法。实际上,矩形是绘制在缓冲区中,如果不调用 flip() 方法,则不会显示在屏幕上。

有关 SurfaceColorRect Display 等对象的详细说明,请参阅 Pygame 的官方文档,网址是https://www.pygame.org/docs/,如1‑20所示。

120 pygame官方文档

有些读者可能会对小方块的位置表示感到困惑,这是因为 Pygame 的坐标系与数学中常用的坐标系不同。在 Pygame 中,原点 (0,0) 位于左上角,X 轴向右增大,Y 轴向下增大,该坐标系示意图如1‑21所示。小方块的坐标是(5, 5),所以它显示在屏幕的左上角。

121 pygame的坐标系

1.4.2  控制小方块移动

1.4.2.1处理键盘事件

接下来让小方块动起来。我们将使用键盘的上、下、左、右方向键来控制小方块的移动,代码如下所示:

01\05.py

import pygame

 

# 初始化pygame

pygame.init()

 

# 设置窗口大小

WIDTH = 400

HEIGHT = 300

screen = pygame.display.set_mode((WIDTH, HEIGHT))

 

# 设置窗口标题

pygame.display.set_caption('super mario bros')

 

# 时钟

clock = pygame.time.Clock()

 

# 小方块位置

x = 10

y = 20

 

# 主循环

isRunning = True

while isRunning:

    for event in pygame.event.get():

        if event.type == pygame.QUIT:

            isRunning = False

        elif event.type == pygame.KEYDOWN:

            if event.key == pygame.K_LEFT:

                print("按了左方向键")

                x = x - 10

            elif event.key == pygame.K_RIGHT:

                print("按了右方向键")

                x = x + 10

            elif event.key == pygame.K_UP:

                print("按了上方向键")

                y = y - 10

            elif event.key == pygame.K_DOWN:

                print("按了下方向键")

                y = y + 10

 

    # 小方块

    pygame.draw.rect(screen, (255, 255, 255), ((x, y), (30, 30)), 0)

 

    # 刷新显示

    pygame.display.flip()

 

    # 每秒60

    clock.tick(60)

# 退出 Pygame

pygame.quit()

   运行程序,按下方向键,画面如1‑22所示。

122键盘控制小方块

   在代码中,通过响应 pygame.KEYDOWN 事件来捕获键盘按键操作。按下上下方向键时,小方块的 Y 坐标会发生变化,按下左右方向键时,X坐标会发生变化。

如果连续按下方向键,会发现小方块好像变成了贪吃蛇。这是因为虽然新位置绘制了小方块,但旧位置的小方块仍然存在,导致小方块叠加。要实现小方块的真正平滑移动,需要在移动后擦除旧位置的小方块。理想的做法是使用背景色重新绘制旧位置,这样就相当于擦除了旧的小方块。在这里我们采用了简单直接的方式,即全屏用背景色重新绘制,相当于擦除了屏幕上的所有内容,代码如下所示:

01\06.py

    # 全屏擦除

    screen.fill((0, 0, 0))

 

    # 小方块

    pygame.draw.rect(screen, (255, 255, 255), ((x, y), (30, 30)), 0)

每一帧绘制小方块之前,先使用 screen.fill((0,0,0)) 方法填充整个屏幕,其中的 (0,0,0) 表示黑色。运行程序,画面如1‑23所示。现在小方块可以在窗口中自由移动,到达任何位置都只会显示一个小方块,避免了拖尾现象。

123增加全屏擦除

1.4.2.2连续运动

在尝试操作游戏时,可能会感到不够畅快,因为每按一次方向键,小方块只移动一下。即使按住方向键不放,小方块也只会移动一次。这是因为在 Pygame 的默认设置下,按住某个按键只会触发一次按键事件,只在按下时触发一次 KEY_DOWN 事件,不会持续触发。如果希望实现按住键盘按键后持续响应的效果,可以使用 pygame.key.set_repeat 方法进行设置,格式如下:

pygame.key.set_repeat(delay, interval)

         参数含义如下:

l  delay:表示按下按键后,延迟多久开始重复响应,单位为毫秒。

l  interval:表示重复响应之间的间隔时间,单位为毫秒。

例如,使用 pygame.key.set_repeat(10, 15),会在按下按键时触发一次 KEY_DOWN 事件,然后延迟 10 毫秒后再触发一次,之后每隔 15 毫秒触发一次。将这个语句添加到set_caption()方法之后即可实现按住方向键后持续响应的效果,代码如下所示:

01\07.py

#设置窗口标题

pygame.display.set_caption('super mario bros')

 

#按键重复响应

pygame.key.set_repeat(10,15)

 

#时钟

clock = pygame.time.Clock()

运行程序,按住按键,发现小方块可以持续的移动了。

1.4.2.3限制运动边界

在程序支持连续运动后,小方块移动速度加快,很容易超出窗口范围。为了避免这种情况,需要限制小方块移动的边界。小方块位于窗口边界的情况如1‑24所示。

124 小方块位于边界的情况

从图可以看出:

l  X轴方向:最小值是0,最大值是窗口宽度减去小方块的宽度。

l  Y轴方向:最小值是0,最大值是窗口高度减去小方块的高度。

当小方块移动到边界时,只需要不响应对应方向的按键事件即可。例如,如果小方块一直向左移动且X坐标已经是0,则无需继续向左移动,其他方向也类似。修改后的代码如下所示:

01\08.py

import pygame

 

# 初始化pygame

pygame.init()

 

# 设置窗口大小

WIDTH = 400

HEIGHT = 300

screen = pygame.display.set_mode((WIDTH, HEIGHT))

 

# 设置窗口标题

pygame.display.set_caption('super mario bros')

 

# 按键重复响应

pygame.key.set_repeat(10, 15)

 

# 时钟

clock = pygame.time.Clock()

 

# 小方块位置和大小

x = 10

y = 20

blockWidth = 30

blockHeight = 30

 

# 主循环

isRunning = True

while isRunning:

    for event in pygame.event.get():

        if event.type == pygame.QUIT:

            isRunning = False

        elif event.type == pygame.KEYDOWN:

            if event.key == pygame.K_LEFT:

                print("按了左方向键")

                if x > 0:

                    x = x - 10

            elif event.key == pygame.K_RIGHT:

                print("按了右方向键")

                if x + blockWidth < WIDTH:

                    x = x + 10

            elif event.key == pygame.K_UP:

                print("按了上方向键")

                if y > 0:

                    y = y - 10

            elif event.key == pygame.K_DOWN:

                print("按了下方向键")

                if y + blockHeight < HEIGHT:

                    y = y + 10

 

    # 全屏擦除

    screen.fill((0, 0, 0))

 

    # 小方块

    pygame.draw.rect(screen, (255, 255, 255), ((x, y), (blockWidth, blockHeight)), 0)

 

    # 刷新显示

    pygame.display.flip()

 

    # 每秒60

    clock.tick(60)

# 退出 Pygame

pygame.quit()

代码中将小方块的宽度和高度存储在变量中,避免在程序中使用固定的数字,使代码更加清晰明了。运行程序,可以观察到小方块被成功限制在窗口范围内。

1.4.3  重力模拟

目前,游戏主角是一个小方块,它能在窗口内自由移动,感觉有点像飞行射击游戏,如1‑25所示。整个画面采用俯视图,玩家从上而下观察游戏世界,小方块有点像飞机,可以在空中自由飞行。

125 飞行射击游戏的画面

而马里奥的世界采用的是侧视图,玩家从侧面观察游戏世界,如1‑26所示。

126 马里奥的世界

马里奥会从空中掉落,最终落到地面上,仿佛受到了重力的影响,这就是重力模拟的效果。我们编写一个程序来模拟这个掉落的过程,基本思路是画一条横线代表地面,让小方块(也就是马里奥)从空中掉落,一旦碰到地面就停止下落,代码如下所示:

01\09.py

import pygame

 

# 初始化pygame

pygame.init()

 

# 设置窗口大小

WIDTH = 600

HEIGHT = 400

screen = pygame.display.set_mode((WIDTH, HEIGHT))

 

# 设置窗口标题

pygame.display.set_caption('super mario bros')

 

# 时钟

clock = pygame.time.Clock()

 

# 小方块位置和大小

x = 10

y = 20

blockWidth = 30

blockHeight = 30

 

# 主循环

isRunning = True

while isRunning:

    for event in pygame.event.get():

        if event.type == pygame.QUIT:

            isRunning = False

 

    # 全屏擦除

    screen.fill((0, 0, 0))

 

    # 地平线

    pygame.draw.line(screen, (255, 255, 255), (0, 300), (WIDTH, 300), 1)  # 画直线

 

    # 计算马里奥位置

    y = y + 1

    if y >= 300:

        y = 300

 

    # 显示马里奥

    pygame.draw.rect(screen, (255, 255, 255), ((x, y), (blockWidth, blockHeight)), 0)

 

    # 刷新显示

    pygame.display.flip()

 

    # 每秒60

    clock.tick(60)

# 退出 Pygame

pygame.quit()

马里奥的初始位置在顶部,然后每帧向下移动马里奥。运行程序,马里奥开始向下匀速运动,然后到达1‑27所示位置时停止。

127 马里奥停止掉落

这个停止位置不正确,马里奥掉到了地面之下。直线的Y坐标是300,考虑到马里奥的身高,正确的停止位置应该是300减去马里奥的身高。此外,马里奥掉落速度有些缓慢,每次掉落1像素,每秒钟60帧,因此每秒只移动了60像素。修改后的代码如下所示:

01\10.py

    # 计算马里奥位置

    y = y + 4

    if y >= 300 - blockHeight:

        y = 300 - blockHeight

 

    # 显示马里奥

    pygame.draw.rect(screen, (255, 255, 255), ((x, y), (blockWidth, blockHeight)), 0)

运行程序,马里奥稳稳地落到了地面上,画面如1‑28所示。

128 马里奥掉落在地面上

1.4.4  跳跃

1.4.4.1增加跳跃

让我们对马里奥的动作进行优化。上方向键不再响应,这样马里奥就不会随便向上移动了。我们还定义了一个跳跃键,这里使用字母G键。这样,右手控制方向,左手按G键跳跃,操作起来更加顺畅。同时,下方向键也不再响应,马里奥的掉落由重力来处理,修改后的代码如下所示:

01\11.py

import pygame

 

# 初始化pygame

pygame.init()

 

# 设置窗口大小

WIDTH = 600

HEIGHT = 400

screen = pygame.display.set_mode((WIDTH, HEIGHT))

 

# 设置窗口标题

pygame.display.set_caption('super mario bros')

 

# 按键重复响应

pygame.key.set_repeat(10, 15)

 

# 时钟

clock = pygame.time.Clock()

 

# 小方块位置和大小

x = 10

y = 20

blockWidth = 30

blockHeight = 30

 

# 主循环

isRunning = True

while isRunning:

    for event in pygame.event.get():

        if event.type == pygame.QUIT:

            isRunning = False

        elif event.type == pygame.KEYDOWN:

            if event.key == pygame.K_LEFT:

                print("按了左方向键")

                if x > 0:

                    x = x - 10

            elif event.key == pygame.K_RIGHT:

                print("按了右方向键")

                if x + blockWidth < WIDTH:

                    x = x + 10

            elif event.key == pygame.K_g:

                print("按了跳跃键")

                if y > 0:

                    y = y - 10

 

    # 全屏擦除

    # screen.fill((0,0,0))

 

    # 地平线

    pygame.draw.line(screen, (255, 255, 255), (0, 300), (WIDTH, 300), 1)  # 画直线

 

    # 计算马里奥位置

    y = y + 4

    if y >= 300 - blockHeight:

        y = 300 - blockHeight

 

    # 显示马里奥

    pygame.draw.rect(screen, (255, 255, 255), ((x, y), (blockWidth, blockHeight)), 0)

 

    # 刷新显示

    pygame.display.flip()

 

    # 每秒60

    clock.tick(60)

# 退出 Pygame

pygame.quit()

现在,按住G键,会发现马里奥可以持续上升,而且按住时间越长,飞得越高。松开键后,马里奥会自动掉落到地面,这是因为每帧都会增加Y坐标,直到碰到地面,就如同受到持续的重力影响一样。

为了观察运动轨迹,我们暂时注释掉全屏擦除的代码,运行程序,画面如1‑29所示。

129 跳跃轨迹

1.4.4.2同时按键的处理

在操作游戏时,你可能会发现马里奥无法同时进行水平移动和跳跃,也就是无法进行弧线式的跳跃。要么走路,要么跳跃,二者无法同时进行。让我们打印出事件,看看究竟是怎么回事,代码如下所示:

01\12.py

# 主循环

isRunning = True

while isRunning:

    print("----------------")

    for event in pygame.event.get():

        print(event.type)

        if event.type == pygame.QUIT:

            isRunning = False

        elif event.type == pygame.KEYDOWN:

            if event.key == pygame.K_LEFT:

                print("按了左方向键")

                if x > 0:

                    x = x - 10

            elif event.key == pygame.K_RIGHT:

                print("按了右方向键")

                if x + blockWidth < WIDTH:

                    x = x + 10

            elif event.key == pygame.K_g:

                print("按了跳跃键")

                if y > 0:

                    y = y - 10

代码中只是增加了两行打印语句,每次循环用横线分隔。运行程序,一边走路,一边跳跃,结果如1‑30所示。

130 打印事件类型

从图可以看到,按下跳跃键之后,之前的方向键就没有触发了,说明pygame.key.set_repeat()方法不支持多个按键同时重复发送。看起来,是抛弃它的时候了。

我们可以使用pygame.key.get_pressed()方法,它返回一个包含所有按键状态的列表。例如,如果A键被按下,则返回True,如果B键没有被按下,则返回False,以此类推。该方法返回的是当前时刻的按键状态。例如,在第一帧按下A键,在第二帧按下B键,那么在第三帧中AB都为True。如果此时松开了A键,那么在第四帧中AFalseBTrue

让我们修改一下程序,代码如下所示:

01\13.py

import pygame

 

# 初始化pygame

pygame.init()

 

# 设置窗口大小

WIDTH = 600

HEIGHT = 400

screen = pygame.display.set_mode((WIDTH, HEIGHT))

 

# 设置窗口标题

pygame.display.set_caption('super mario bros')

 

# 避免输入法影响按键

pygame.key.stop_text_input()

 

# 时钟

clock = pygame.time.Clock()

 

# 小方块位置和大小

x = 10

y = 20

blockWidth = 30

blockHeight = 30

 

# 主循环

isRunning = True

while isRunning:

    for event in pygame.event.get():

        if event.type == pygame.QUIT:

            isRunning = False

 

    # 获取按键状态

    keys = pygame.key.get_pressed()

    if keys[pygame.K_LEFT]:

        print("按了左方向键")

        if x > 0:

            x = x - 10

    if keys[pygame.K_RIGHT]:

        print("按了右方向键")

        if x + blockWidth < WIDTH:

            x = x + 10

    if keys[pygame.K_g]:

        print("按了跳跃键")

        if y > 0:

            y = y - 10

 

    # 全屏擦除

    # screen.fill((0,0,0))

 

    # 地平线

    pygame.draw.line(screen, (255, 255, 255), (0, 300), (WIDTH, 300), 1)  # 画直线

 

    # 计算马里奥位置

    y = y + 4

    if y >= 300 - blockHeight:

        y = 300 - blockHeight

 

    # 显示马里奥

    pygame.draw.rect(screen, (255, 255, 255), ((x, y), (blockWidth, blockHeight)), 0)

 

    # 刷新显示

    pygame.display.flip()

 

    # 每秒60

    clock.tick(60)

# 退出 Pygame

pygame.quit()

请注意,已经移除了pygame.key.set_repeat()方法。但是,新增了一个pygame.key.stop_text_input()方法,用于停止接收pygame.TEXTEDITINGpygame.TEXTINPUT事件。在游戏运行时,如果中文输入法处于打开状态,可能会吞噬键盘输入,导致无法操作游戏,使用此方法可以避免这种影响。

另外,请注意,判断按键状态需要三个if语句,也就是,每帧都要分别判断三个按键是否被按下。运行程序,连续跳跃的画面如1‑31所示,可以看出,现在马里奥的运动还是比较灵活的。

131 连续小跳

让我们稍微总结一下:

l  pygame.key.get_pressed()方法适合处理多个按键,适合每帧都进行的动作,比如持续行走、持续射击等等。

l  KEYDOWN事件适合处理单个按键,适合执行单次动作,比如选择菜单,打开开关等等。