我们不想让马里奥穿越水管,需要他在水管边缘停下来,也就是碰撞到水管的那一瞬间停下来。判断两个物体是否碰撞,在游戏开发中称为碰撞检测。
碰撞检测有多种方法,比如矩形碰撞、圆形碰撞、像素碰撞等等。矩形碰撞就是把两个物体都当做矩形,判断这两个矩形是否交叉的算法。圆形碰撞就是把两个物体都当做圆形。像素碰撞复杂一些,逐个像素判断两个物体是否重叠,适合边缘不整齐的形状。
我们这里明显需要矩形碰撞。不过,先不要着急,我们只是想让马里奥走路时别撞到水管就行,也就是X轴方向。先看一下碰撞的临界情况,如图2‑2所示。

图2‑2 碰撞的临界情况
当马里奥的边界和水管的边界相交时,我们需要让马里奥停止移动。具体来说,当马里奥的右边界碰到水管的左边界时,马里奥不能向右继续移动,反之,当马里奥的左边界碰到水管的右边界时,马里奥不能向左继续移动。为了实现这一功能,我们可以在处理按键时添加相应的碰撞检测判断。
由于碰撞检测需要使用水管的位置和大小,所以用变量保存起来,修改后的代码如下所示:
02\02.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()
# 方块大小 blockSize = 30
# 马里奥位置和大小 marioX = 10 marioY = 20 marioWidth = blockSize marioHeight = blockSize
# 水管的位置和大小 pipeX = 400 pipeY = 300 - blockSize * 2 pipeWidth = blockSize pipeHeight = blockSize * 2
# 主循环 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("按了左方向键") isAllowLeft = True if marioX <= 0: isAllowLeft = False if marioX + marioWidth >= pipeX and pipeX + pipeWidth >= marioX: isAllowLeft = False if isAllowLeft: marioX = marioX - 2 if keys[pygame.K_RIGHT]: print("按了右方向键") isAllowRight = True if marioX + marioWidth >= WIDTH: isAllowRight = False if marioX + marioWidth >= pipeX and pipeX + pipeWidth >= marioX: isAllowRight = False if isAllowRight: marioX = marioX + 2 if keys[pygame.K_g]: print("按了跳跃键") if marioY > 0: marioY = marioY - 10
# 全屏擦除 screen.fill((0, 0, 0))
# 地平线 pygame.draw.line(screen, (255, 255, 255), (0, 300), (WIDTH, 300), 1) # 画直线
# 障碍物 pygame.draw.rect(screen, (255, 255, 255), ((pipeX, pipeY), (pipeWidth, pipeHeight)), 0)
# 计算马里奥位置 marioY = marioY + 4 if marioY >= 300 - marioHeight: marioY = 300 - marioHeight
# 显示马里奥 pygame.draw.rect(screen, (255, 255, 255), ((marioX, marioY), (marioWidth, marioHeight)), 0) pygame.draw.rect(screen, (0, 0, 0), ((marioX + 5, marioY + 5), (marioWidth - 10, marioHeight - 10)), 0)
# 刷新显示 pygame.display.flip()
# 每秒60帧 clock.tick(60) # 退出 Pygame pygame.quit() |
运行程序,让马里奥向右走,会发现马里奥到达临界位置时,被水管粘住了,无法向左离开,如图2‑3所示。

图2‑3 马里奥无法向左边离开
我们看一下向左移动的判断条件:
if marioX+marioWidth>=pipeX and pipeX+pipeWidth>=marioX:
当马里奥处于图中位置时,马里奥右边界和水管左边界重合了,marioX+marioWidth恰好等于pipeX,所以判断条件成立,认为不可向左移动。我们现在想让马里奥离开,那么把这个等号去掉即可。向右移动的判断条件也类似修改,修改后的代码如下所示:
02\03.py
|
# 获取按键状态 keys = pygame.key.get_pressed() if keys[pygame.K_LEFT]: print("按了左方向键") isAllowLeft = True if marioX <= 0: isAllowLeft = False if marioX + marioWidth > pipeX and pipeX + pipeWidth >= marioX: isAllowLeft = False if isAllowLeft: marioX = marioX - 2 if keys[pygame.K_RIGHT]: print("按了右方向键") isAllowRight = True if marioX + marioWidth >= WIDTH: isAllowRight = False if marioX + marioWidth >= pipeX and pipeX + pipeWidth > marioX: isAllowRight = False if isAllowRight: marioX = marioX + 2 |
运行程序,马里奥现在可以离开临界点了。不过,当我们尝试增大马里奥的步伐,即将marioX=marioX+2这个数值2增大,比如改成18时,还是发现了一些问题。为了显示马里奥的边界位置,我们给他增加一个边框,代码如下所示:
02\04.py
|
# 显示马里奥 pygame.draw.rect(screen, (0, 0, 0), ((marioX, marioY), (marioWidth, marioHeight)), 1) # 边框 pygame.draw.rect(screen, (255, 255, 255), ((marioX + 1, marioY + 1), (marioWidth - 2, marioHeight - 2)), 0) # 边框内部白色填充 pygame.draw.rect(screen, (0, 0, 0), ((marioX + 5, marioY + 5), (marioWidth - 10, marioHeight - 10)), 0) # 中心黑色填充 |
运行程序,让马里奥来到水管左边,如图2‑4所示的位置,距离水管还有一些距离。

图2‑4 马里奥位于水管外面
然后让马里奥向右移动,会发现他还是进入了水管,如图2‑5所示。

图2‑5 马里奥穿越水管
看起来,当马里奥的步伐较大时,是否允许移动的判断并不够准确。这是因为我们是在移动之前进行的碰撞判断,而由于步伐的不同,移动后可能会发生碰撞,也可能不会,只有在移动之后才能确切确定是否发生碰撞。但我们不能等马里奥移动后才告诉他“哎呀,你碰到水管了,退回去吧”。这样马里奥每迈出一步都要来回跳,显然是不切实际的。
为了解决这个问题,我们需要预测马里奥下一步的位置。我们已经知道了步伐,因此下一步的位置可以通过当前位置加上步伐来计算。如果在迈出下一步后发生碰撞,那么就不允许继续移动。让我们按照这个思路进行改进,修改后的代码如下所示:
02\05.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()
# 方块大小 blockSize = 30
# 马里奥位置和大小 marioX = 10 marioY = 20 marioWidth = blockSize marioHeight = blockSize
# 马里奥的步伐 marioWalkStep = 18
# 水管的位置和大小 pipeX = 400 pipeY = 300 - blockSize * 2 pipeWidth = blockSize pipeHeight = blockSize * 2
# 主循环 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("按了左方向键") isAllowLeft = True if marioX - marioWalkStep <= 0: isAllowLeft = False if marioX + marioWidth - marioWalkStep > pipeX and pipeX + pipeWidth >= marioX - marioWalkStep: isAllowLeft = False if isAllowLeft: marioX = marioX - marioWalkStep if keys[pygame.K_RIGHT]: print("按了右方向键") isAllowRight = True if marioX + marioWidth + marioWalkStep >= WIDTH: isAllowRight = False if marioX + marioWidth + marioWalkStep >= pipeX and pipeX + pipeWidth > marioX + marioWalkStep: isAllowRight = False if isAllowRight: marioX = marioX + marioWalkStep if keys[pygame.K_g]: print("按了跳跃键") if marioY > 0: marioY = marioY - 10
# 全屏擦除 screen.fill((0, 0, 0))
# 地平线 pygame.draw.line(screen, (255, 255, 255), (0, 300), (WIDTH, 300), 1) # 画直线
# 障碍物 pygame.draw.rect(screen, (255, 255, 255), ((pipeX, pipeY), (pipeWidth, pipeHeight)), 0)
# 计算马里奥位置 marioY = marioY + 4 if marioY >= 300 - marioHeight: marioY = 300 - marioHeight
# 显示马里奥 pygame.draw.rect(screen, (0, 0, 0), ((marioX, marioY), (marioWidth, marioHeight)), 1) # 边框 pygame.draw.rect(screen, (255, 255, 255), ((marioX + 1, marioY + 1), (marioWidth - 2, marioHeight - 2)), 0) # 边框内部白色填充 pygame.draw.rect(screen, (0, 0, 0), ((marioX + 5, marioY + 5), (marioWidth - 10, marioHeight - 10)), 0) # 中心黑色填充
# 刷新显示 pygame.display.flip()
# 每秒60帧 clock.tick(60) # 退出 Pygame pygame.quit() |
运行程序,马里奥已经不会进入水管了。然而,新的问题出现了,如图2‑6所示。马里奥离水管还很远,由于步伐较大,下一步就会进入水管,因此被判断为不可向右移动。这么大的中间空隙不是我们所希望的效果,马里奥应该能够走到水管旁边才对。

图2‑6 马里奥距离很远就无法移动
既然我们已经知道马里奥下一步会进入水管,那么为何不让他的步伐缩小一点呢?这样他恰好可以移动到水管边界。修改后的代码如下所示:
02\06.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()
# 方块大小 blockSize = 30
# 马里奥位置和大小 marioX = 10 marioY = 20 marioWidth = blockSize marioHeight = blockSize
# 水管的位置和大小 pipeX = 400 pipeY = 300 - blockSize * 2 pipeWidth = blockSize pipeHeight = blockSize * 2
# 主循环 isRunning = True while isRunning: for event in pygame.event.get(): if event.type == pygame.QUIT: isRunning = False
# 马里奥的步伐 marioWalkStep = 18
# 获取按键状态 keys = pygame.key.get_pressed() if keys[pygame.K_LEFT]: print("按了左方向键") if marioX - marioWalkStep <= 0: marioWalkStep = marioX if marioX - marioWalkStep <= pipeX + pipeWidth and pipeX + pipeWidth <= marioX: marioWalkStep = marioX - (pipeX + pipeWidth) marioX = marioX - marioWalkStep if keys[pygame.K_RIGHT]: print("按了右方向键") if marioX + marioWidth + marioWalkStep >= WIDTH: marioWalkStep = WIDTH - (marioX + marioWidth) print("marioWalkStep1=" + str(marioWalkStep)) if marioX + marioWidth + marioWalkStep >= pipeX and marioX + marioWidth <= pipeX: marioWalkStep = pipeX - (marioX + marioWidth) print("marioWalkStep2=" + str(marioWalkStep)) marioX = marioX + marioWalkStep if keys[pygame.K_g]: print("按了跳跃键") if marioY > 0: marioY = marioY - 10
# 全屏擦除 screen.fill((0, 0, 0))
# 地平线 pygame.draw.line(screen, (255, 255, 255), (0, 300), (WIDTH, 300), 1) # 画直线
# 障碍物 pygame.draw.rect(screen, (255, 255, 255), ((pipeX, pipeY), (pipeWidth, pipeHeight)), 0)
# 计算马里奥位置 marioY = marioY + 4 if marioY >= 300 - marioHeight: marioY = 300 - marioHeight
# 显示马里奥 pygame.draw.rect(screen, (0, 0, 0), ((marioX, marioY), (marioWidth, marioHeight)), 1) # 边框 pygame.draw.rect(screen, (255, 255, 255), ((marioX + 1, marioY + 1), (marioWidth - 2, marioHeight - 2)), 0) # 边框内部白色填充 pygame.draw.rect(screen, (0, 0, 0), ((marioX + 5, marioY + 5), (marioWidth - 10, marioHeight - 10)), 0) # 中心黑色填充
# 刷新显示 pygame.display.flip()
# 每秒60帧 clock.tick(60) # 退出 Pygame pygame.quit() |
运行程序,马里奥可以走到水管旁边了。