2.2    碰撞检测

我们不想让马里奥穿越水管,需要他在水管边缘停下来,也就是碰撞到水管的那一瞬间停下来。判断两个物体是否碰撞,在游戏开发中称为碰撞检测。

碰撞检测有多种方法,比如矩形碰撞、圆形碰撞、像素碰撞等等。矩形碰撞就是把两个物体都当做矩形,判断这两个矩形是否交叉的算法。圆形碰撞就是把两个物体都当做圆形。像素碰撞复杂一些,逐个像素判断两个物体是否重叠,适合边缘不整齐的形状。

我们这里明显需要矩形碰撞。不过,先不要着急,我们只是想让马里奥走路时别撞到水管就行,也就是X轴方向。先看一下碰撞的临界情况,如2‑2所示。

22 碰撞的临界情况

当马里奥的边界和水管的边界相交时,我们需要让马里奥停止移动。具体来说,当马里奥的右边界碰到水管的左边界时,马里奥不能向右继续移动,反之,当马里奥的左边界碰到水管的右边界时,马里奥不能向左继续移动。为了实现这一功能,我们可以在处理按键时添加相应的碰撞检测判断。

由于碰撞检测需要使用水管的位置和大小,所以用变量保存起来,修改后的代码如下所示:

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所示。

23 马里奥无法向左边离开

我们看一下向左移动的判断条件:

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所示的位置,距离水管还有一些距离。

24 马里奥位于水管外面

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

25 马里奥穿越水管

看起来,当马里奥的步伐较大时,是否允许移动的判断并不够准确。这是因为我们是在移动之前进行的碰撞判断,而由于步伐的不同,移动后可能会发生碰撞,也可能不会,只有在移动之后才能确切确定是否发生碰撞。但我们不能等马里奥移动后才告诉他“哎呀,你碰到水管了,退回去吧”。这样马里奥每迈出一步都要来回跳,显然是不切实际的。

为了解决这个问题,我们需要预测马里奥下一步的位置。我们已经知道了步伐,因此下一步的位置可以通过当前位置加上步伐来计算。如果在迈出下一步后发生碰撞,那么就不允许继续移动。让我们按照这个思路进行改进,修改后的代码如下所示:

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所示。马里奥离水管还很远,由于步伐较大,下一步就会进入水管,因此被判断为不可向右移动。这么大的中间空隙不是我们所希望的效果,马里奥应该能够走到水管旁边才对。

26 马里奥距离很远就无法移动

既然我们已经知道马里奥下一步会进入水管,那么为何不让他的步伐缩小一点呢?这样他恰好可以移动到水管边界。修改后的代码如下所示:

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()

运行程序,马里奥可以走到水管旁边了。