Bootstrap

【Dison夏令营 Day 10】如何用 Python 中的 PyGame 制作闯关游戏

使用我们的 Python 代码生成器,为您的编码项目开个好头。非常适合您需要快速解决方案的时候使用。别等了,今天就试试吧!

编程涉及您有效处理代码的能力。这种能力能让你开发出有价值、发人深省的应用程序。

构建游戏项目是学习代码操作的最佳途径之一。在 Python 中,用于构建交互式游戏的流行库是 Pygame。

在本教程中,您将学习如何用 Python 构建一个 Breakout 游戏。这是一个经典游戏,其特点是有一个可移动的球拍、一个弹跳球和多个高架砖块。

在这里插入图片描述

游戏的目的是使用球来摧毁所有砖块,同时使用球拍来控制球。

要求

要学习本教程,您需要具备以下条件:

  1. Pygame:运行下面命令安装pygame

    pip install pygame

  2. Freesound 账户: 当游戏中发生某些事件时,我们需要添加声音。Freesound 有一个免费音轨库,您可以从中获取游戏音效。您需要创建一个账户才能下载声音。

配置游戏设置

第一步是在项目文件夹中创建 settings.py 文件。该文件将包含游戏的所有设置和变量。以下是 settings.py 文件的内容:

# settings.py
import pygame as pg

# Screen settings
WIDTH = 550
HEIGHT = 600
BG_COLOR = "purple"
# Text color
color = "white"
# Paddle settings
paddle_x = 250
paddle_y = 550
paddle_width = 100
paddle_height = 20
# Ball settings
ball_x = 250
ball_y = 540
ball_x_speed = 2
ball_y_speed = 2
ball_radius = 5
ball_color = "grey"
# Text settings
text_x = 300
# Bricks settings
brick_width = 40
brick_height = 20

如前所述,闯关游戏包含一个球拍、一个球、一组砖块和一个记分板。因此,上述设置将用于设置游戏中每个物体的大小、尺寸、位置(x、y)和其他特征。

创建游戏窗口

我们必须设置游戏屏幕,指定合适的尺寸、颜色等。新建一个名为 main.py 的文件。该文件将作为游戏的主脚本。在该文件中添加以下代码:

# main.py
import pygame as pg
from settings import *

# Initialize pygame
pg.init()

screen = pg.display.set_mode((WIDTH, HEIGHT))
pg.display.set_caption("Breakout Game")

clock = pg.time.Clock() 

running = True
while running:
   screen.fill(BG_COLOR)
   # Check for quit game
   for event in pg.event.get():
       if event.type == pg.QUIT:
           running = False
   pg.display.flip()
   # To adjust the game frame rate
   clock.tick(60)

上面的代码就像是在 pygame 中设置游戏窗口的模板。我们首先初始化了 pygame,并使用 settings.py 文件中的变量设置了窗口尺寸。

Clock() 类用于管理每秒帧频。这可以确保游戏运行速度不会太快,并且在所有系统上都保持一致。为了保持窗口打开,我们需要创建一个 while 循环,将游戏中的每个事件都添加到该循环中。

最后,我们建立一个循环,当用户关闭游戏窗口时退出游戏。pg.display.flip()方法可确保定期更新游戏中的每个对象和事件。

创建球拍

屏幕目前是空白的,因此我们需要添加对象。我们要添加的第一个对象是球拍。要添加球拍,需要新建一个名为 paddle.py 的 python 文件。我们将在该文件中创建 Paddle 类。

由于我们的项目需要添加球拍、球、砖块等对象。建议我们将它们各自分成独立的类。

在 paddle.py 文件中添加以下代码:

# paddle.py
import pygame as pg
from settings import paddle_height, paddle_width

class Paddle:
   def __init__(self, x, y):
       self.x = x
       self.y = y
       self.width = paddle_width
       self.height = paddle_height
       self.rect = pg.Rect(self.x, self.y, self.width, self.height)
       self.color = pg.Color("white")

   def appear(self, screen):
       pg.draw.rect(screen, self.color, self.rect)

   def move_right(self):
       if self.rect.x + self.width <= 550:
           self.rect.x += 2

   def move_left(self):
       if self.rect.x >= 0:
           self.rect.x -= 2

在上面的代码中,我们创建了一个 Paddle 类,并设置了它的属性,如屏幕上的 x 和 y 位置、高度、宽度和颜色。桨由 pygame 矩形对象创建,它由 self.rect 属性表示。

appear() 方法会将球拍显示在屏幕上。

另外两个方法–move_right() 和 move_left()–分别通过向右和向左移动两步来增加球拍的运动。这些方法还确保球拍只能在屏幕尺寸范围内移动。因此,如果您尝试将球拍移动到屏幕之外,它将不会移动。

我们需要在主脚本中添加桨叶对象。打开 main.py 文件并执行以下操作:

  1. 导入paddle类并创建一个paddle对象

    from paddle import Paddle
    
    ...
    # OBJECTS
    pad = Paddle(paddle_x, paddle_y)
    
    ...
    
  2. 在 while 循环中添加 appear() 方法,如下所示:

    while running:
    
       ...   
       pad.appear(screen)
       ...
    
    
  3. 检查按键是否将球向右或向左移动:

    while running:
    
       ...   
       
       # Check for key presses
       keys = pg.key.get_pressed()
       if keys[pg.K_RIGHT]:
           pad.move_right()
    
       if keys[pg.K_LEFT]:
           pad.move_left()   
       ...
    
    

现在,您的 main.py 文件应该如下所示:

# main.py

import pygame as pg
from paddle import Paddle
from settings import *

pg.init()

screen = pg.display.set_mode((WIDTH, HEIGHT))
pg.display.set_caption("Breakout Game")
clock = pg.time.Clock()
# OBJECTS
pad = Paddle(paddle_x, paddle_y)

running = True
while running:
   screen.fill(BG_COLOR)
   pad.appear(screen)
   # Check for quit game
   for event in pg.event.get():
       if event.type == pg.QUIT:
           running = False
   # Check for key presses
   keys = pg.key.get_pressed()
   if keys[pg.K_RIGHT]:
       pad.move_right()
   if keys[pg.K_LEFT]:
       pad.move_left()
   pg.display.flip()
   clock.tick(60)

运行程序。你应该会看到屏幕上出现一个类似下面这样的划面:

在这里插入图片描述
使用左右箭头键控制球拍。

创建弹跳球

像制作球拍一样,新建一个名为 ball.py 的文件,并创建一个球类。在该文件中添加以下代码:

# ball.py
import pygame as pg
from settings import ball_x_speed, ball_y_speed, ball_radius, ball_color

class Ball:
   def __init__(self, x, y, screen):
       self.x = x
       self.y = y
       self.screen = screen
       self.radius = ball_radius
       self.color = pg.Color(ball_color)
       self.x_speed = ball_x_speed
       self.y_speed = ball_y_speed

   def move(self):
       pg.draw.circle(self.screen, self.color, [self.x, self.y], self.radius)
       self.y -= self.y_speed
       self.x -= self.x_speed

在上面的代码中,我们为球创建了一个类,并设置了它的属性。move() 函数将球绘制在屏幕上指定的坐标处,并添加运动。它通过减小球的 x 和 y 值来实现这一目的。

请按照以下步骤将球添加到屏幕上:

  1. 打开 main.py 文件
  2. 导入 Ball 类并创建一个球对象:
...
from ball import Ball
...

# OBJECTS
ball = Ball(ball_x, ball_y, screen)
  1. 在 while 循环中添加 .move() 方法:
running = True
while running:
   ...
   ball.move()
   ...

这将确保球的位置在屏幕上定期更新。

添加小球弹跳逻辑

小球会不断滚出屏幕。最初,它应该在碰到屏幕或球拍边缘(屏幕底部除外)时弹起。我们需要在 Ball() 类中添加新方法来实现这一点。下面是这些方法:

...

def bounce_x(self):
   self.x_speed *= -1

def bounce_y(self):
   self.y_speed *= -1

def check_for_contact_on_x(self):
   if self.x - self.radius <= 0 or self.x + self.radius >= self.screen.get_width():
       self.bounce_x()

def check_for_contact_on_y(self):
   if self.y + self.radius <= 0:
       self.bounce_y()

bounce_x() 和 bounce_y() 方法分别实现了 x 轴和 y 轴的小球弹跳逻辑。

check_for_contact_on_y()和 check_for_contact_on_x() 方法使用球的半径以及当前的 x 和 y 位置比较球与屏幕边缘的距离。

这些方法将球的当前位置乘以-1,从而创建一个反向位置。例如,如果小球正在向上移动,然后撞到了屏幕的右侧,我们需要通过减小小球的 x 和 y 值让它弹回左侧。唯一的办法就是将其乘以-1,这样它就会变成负值并开始向左移动。

同样,如果小球向上移动并撞击到屏幕的左边,我们需要通过增加 x 和 y 的值来使它弹向右边。

由于 x 值已为负数,要使其为正数的唯一方法就是乘以-1,因此小球开始向右移动。

让我们在 while 循环中反映这些变化,如下所示:

running = True
while running:
   ...
   ball.move()
   ...
   # Check if ball hits the x-axis above 
   ball.check_for_contact_on_x()
   # Check if ball hits y-axis on the side
   ball.check_for_contact_on_y()
   # Check if ball hits paddle
   if (pad.rect.y < ball.y + ball.radius < pad.rect.y + pad.height
          and
       pad.rect.x < ball.x + ball.radius < pad.rect.x + pad.width):
       ball.bounce_y()
       ball.y = pad.y - ball.radius
   ...

当球碰到屏幕边缘或球拍时会弹起。

为游戏添加砖块

闯关游戏中的砖块总是在球拍上方。它们通常以不同的颜色出现。我们需要进行一些数学计算才能正确添加砖块。别着急,这又不是什么火箭科学。我会告诉你如何操作。

新建一个名为 bricks.py 的 Python 文件,并添加一个名为 Bricks 的类:

# bricks.py
import random
import pygame as pg

class Bricks:
   def __init__(self, screen, width, height):
       self.screen = screen
       self.width = width
       self.height = height
       self.random_colors = ['blue', 'yellow', 'red', 'green', 'orange']
       self.bricks = []
       self.brick_colors = []
       self.set_values()

   def set_values(self):
       y_values = [int(y) for y in range(100, 200, 25)]
       x_values = [int(x) for x in range(10, 550, 42)]
       y_index = 0
       self.loop(x_values, y_values, y_index)

   def loop(self, x_values, y_values, y_index):
       for n in x_values:
           # Check if it is the last position in the x_values list.
           if n == x_values[-1]:
               # Check if all the positions in the y_values has been occupied
               if y_index < len(y_values) - 1:
                   y_index += 1
                   # Run the method again if there are still vacant positions.
                   self.loop(x_values, y_values, y_index)
           # Create new bricks
           else:
               x = n
               y = y_values[y_index]
               brick = pg.Rect(x, y, self.width, self.height)
               self.bricks.append(brick)
               self.brick_colors.append(random.choice(self.random_colors))

   def show_bricks(self):
       for loop in range(len(self.bricks)):
           brick = self.bricks[loop]
           color = self.brick_colors[loop]
           pg.draw.rect(self.screen, color, brick)

在砖块类中,我们添加了砖块属性和其他属性,其中包括以下内容:

self.random_colors: 这是砖块可以选择的颜色列表
self.bricks: 这是一个包含为游戏生成的所有砖块的列表
self.brick_colors: 该列表将包含游戏中创建的每个砖块的已选颜色。
set_values() 方法将为 x 轴和 y 轴创建一个值列表。y_index 将用于访问列表中 Y 轴的每个值。但现在,它的值是 0。

loop() 方法需要三个位置参数。它们就是我们之前添加到 set_values() 方法中的变量。

现在,loop() 方法中的代码要做什么?

我们已经有了砖块的 x 和 y 位置列表。现在需要做的就是将它们放置在每个点上。一种方法是在 y 轴上选择一个位置,然后沿着这个位置排列砖块,每块砖块都有一个 x 值,该值来自 x 的生成位置列表。

当某个 y 轴被填满时,y_index 会增加 1。这样,它就会在下一个循环中移动到下一个 y 轴。如此循环下去,直到所有的 y 轴都被填满。然后,每块砖都会被添加到 self.bricks 列表中。

最后,show_bricks() 方法通过循环 self.bricks 列表在屏幕上显示砖块,并从 self.brick_colors 列表中为每个砖块赋予一种颜色。

现在,让我们把它添加到游戏循环中。请按照以下步骤操作:

  1. 导入 Bricks 类并创建一个砖块对象
...
from bricks import Bricks
...
# OBJECTS
bricks = Bricks(screen, brick_width, brick_height)

...

  1. 在while循环中添加show_bricks()方法:
running = True
while running:
   ...
   bricks.show_bricks()
   ...

运行程序,你会看到五颜六色的砖块排列在屏幕上,如下图所示:

在这里插入图片描述

检查球何时击中砖块

根据游戏规则,一旦球击中砖块,砖块就会碎裂并消失。我们可以通过检查小球是否与砖块相撞来实现这一功能。在 while 循环中添加一个新的 if 代码块来实现这一功能:

...
running = True
while running:
   ...
   # Check if ball hits brick
   for brick in bricks.bricks:
       if brick.collidepoint(ball.x, ball.y - ball.radius) or brick.collidepoint(ball.x, ball.y + ball.radius):
           bricks.bricks.remove(brick)
           ball.bounce_y()
   ...

在上面的代码中,砖块被击中后会从列表中移除,而球会反弹回来。

更新分数

下一步是记录每次成功击球的得分,然后在球拍未击中球时减少试验次数/生命值。

新建一个名为 scores.py 的文件。在该文件中,创建一个 Scoreboard 类来处理分数。下面是 ScoreBoard 类:

# scores.py

import pygame as pg

class ScoreBoard:
   def __init__(self, x, color, screen):
       self.screen = screen
       self.color = color
       self.x = x
       self.score = 0
       self.high_score = 0
       self.trials = 2
       self.font = pg.font.SysFont("calibri", 20)

   def show_scores(self):
       score_text = self.font.render(f"Score: {self.score}", True, self.color)
       high_score_text = self.font.render(f"High Score: {self.high_score}", True, self.color)
       trials_text = self.font.render(f"Trials: X{self.trials}", True, self.color)

       score_text_rect = score_text.get_rect(topleft=(self.x, 10))
       high_score_text_rect = high_score_text.get_rect(topleft=(self.x, 26))
       trials_text_rect = trials_text.get_rect(topleft=(self.x, 42))

       self.screen.blit(score_text, (self.x, 10))
       self.screen.blit(high_score_text, (self.x, 26))
       self.screen.blit(trials_text, (self.x, 42))

   def is_game_over(self):
       if self.trials == 0:
           return True
       return False

   def game_over(self):
       game_over_color = 'red'
       game_over_font = pg.font.SysFont("calibri", 30)
       game_over_text = game_over_font.render(f"Game Over! Click '0' to restart.", True, game_over_color)
       game_over_rect = game_over_text.get_rect(topright=(50, 300))
       self.screen.blit(game_over_text, (50, 300))
       self.record_high_score()

   def success(self):
       game_success_color = 'green'
       game_success_font = pg.font.SysFont("calibri", 30)
       game_success_text = game_success_font.render(f"You won! Click '0' to restart.", True, game_success_color)
       game_success_rect = game_success_text.get_rect(topleft=(50, 300))
       self.screen.blit(game_success_text, (50, 300))
       self.record_high_score()

   def set_high_score(self):
       try:
           with open("records.txt", mode="r") as file:
               lines = file.readlines()
       except FileNotFoundError:
           with open("records.txt", mode="w") as data:
               data.write("0")
               score = 0
       else:
           score = lines[0]

       self.high_score = int(score)

   def record_high_score(self):
       if self.score > self.high_score:
           with open("records.txt", mode="w") as file:
               file.write(f"{self.score}")

在这段代码中,我们设置了记分板属性,如字体、高度等。show_scores() 方法会在屏幕上显示分数、高分和试验。

与之前的示例一样,导入 Scores 类并创建一个新的分数对象。在 while 循环中添加以下代码:

...
running = True
while running:
   ...
   # Check if ball hits brick
   for brick in bricks.bricks:
       if brick.collidepoint(ball.x, ball.y - ball.radius) or brick.collidepoint(ball.x, ball.y + ball.radius):
           bricks.bricks.remove(brick)
           ball.bounce_y()
           # Increase scores by 1
          score.score += 1
    # Check if ball falls off
    if ball.y + ball.radius >= 580:
          ball.y = pad.y - ball.radius
          pg.time.delay(2000)
          score.trials -= 1
          ball.bounce_y()
...

这样,如果球击中砖块,您的得分就会增加 1 分。此外,如果球掉到球拍之外,您的得分也会减少。下图显示了记分板:

在这里插入图片描述

检查游戏结束条件

当没有试验次数或所有砖块都被摧毁时,游戏就结束了。我们必须创建一个条件,当试验次数或砖块数量等于 0 时结束游戏。

在 ScoreBoard 类中,有三个方法,即 game_over()、is_game_over() 和 success()。

is_game_over() 和 game_over() 方法会在用户没有试玩次数(即输了)时调用,而 success() 方法则会在用户打碎所有砖块时调用。每个方法都会显示描述每个事件的文本。

最后两个方法 set_high_score() 和 record_high_score() 增加了游戏的连续性。目前,您只能玩一会儿,您的分数不会被记录下来。这两个方法将在 .txt 文档中记录您的最高分,从而改变这一状况。这就是为什么要将它们添加到 success() 和 game_over() 方法中,以便在游戏结束时更新高分。

因此,当您重新开始游戏时,您的最高分就会出现在记分板上。让我们更新主脚本,以包含新的更新。将下面的代码添加到 main.py 文件中:

...
# OBJECTS
...
score = ScoreBoard(text_x, color, screen)
score.set_high_score

running = True
while running:
   ...
   # Check if there are more trials
   if score.is_game_over():
       score.game_over()
   # Check if all bricks are broken
   elif len(bricks.bricks) == 0:
       score.success()
   else:
       ball.move()
   ...

在上面的代码中,我们更新了 while 循环,这样如果剩余试验次数等于 0 或用户打破了所有砖块,小球就会停止移动。这就是 ball.move() 方法被移到 if 代码块中的原因。因此,当满足任何一个条件时,小球都会停止移动。

下图显示了用户输掉比赛时的情况:

在这里插入图片描述
下图显示了用户获胜时的图像:
在这里插入图片描述

为游戏添加声音

下一步是为游戏添加声音。我们将为游戏中的某些事件添加声音

正如本教程开头所述,Freesound.org 有一个庞大的背景音效库可供使用。我选择了本教程将使用的音频文件。不过,你也可以随意选择。下面列出了游戏中将使用的音频文件以及使用它们的位置:

最适合 pygame 的音频文件格式是 .ogg。它能将音频文件打包成较小的尺寸,从而确保传输速度。你可以使用免费的在线音频转换器将音频文件转换为 .ogg。

转换完这些文件后,就可以通过以下步骤将其用于游戏:

  1. 在项目中创建一个名为 audio 的单独文件夹。
  2. 将音频文件添加到该文件夹中。
  3. 打开 settings.py 文件并在 Pygame 中初始化声音
# settings.py
...
# Initialize Sound
pg.mixer.init()
# Audio files
pad_hit = pg.mixer.Sound('audio/pad_hit.ogg')
brick_breaking = pg.mixer.Sound("audio/brick_breaking.ogg")
game_end = pg.mixer.Sound("audio/game_end_.ogg")
dropping_ball = pg.mixer.Sound("audio/dropping_ball.ogg")
win_game = pg.mixer.Sound("audio/win_game.ogg")
...

上面的代码初始化了项目的音频。打开 main.py 文件,为特定事件添加声音,如下所示:

# main.py
...
sound_played = False
running = True
while running:
    ...
    # Check if there are more trials
    if score.is_game_over():
       if not sound_played:
          # Sound added
          pg.mixer.Sound.play(game_end)
          sound_played = True
          score.game_over()
    # Check if all bricks are broken
    elif len(bricks.bricks) <= 0:
       if not sound_played:
          # Sound added
          pg.mixer.Sound.play(win_game)
          sound_played = True
          score.success()
    else:
         ball.move()
    ...
    # Check if ball falls off
    if ball.y + ball.radius >= 580:
       # Sound added
       pg.mixer.Sound.play(dropping_ball)
       ball.y = pad.y - ball.radius
       pg.time.delay(2000)
       score.trials -= 1
       ball.bounce_y()
    # Check if ball hits paddle
    if (pad.rect.y < ball.y + ball.radius < pad.rect.y + pad.height
           and
        pad.rect.x < ball.x + ball.radius < pad.rect.x + pad.width):
       # Sound added  
       pg.mixer.Sound.play(pad_hit)
       ball.bounce_y()
       ball.y = pad.y - ball.radius
    # Check if ball hits brick
    for brick in bricks.bricks:
       if brick.collidepoint(ball.x, ball.y - ball.radius) or        brick.collidepoint(ball.x, ball.y + ball.radius):
          # Sound added
          pg.mixer.Sound.play(brick_breaking)
          bricks.bricks.remove(brick)
          ball.bounce_y()
          score.score += 1
    ...

添加重启键

这个游戏的最后一步是允许用户在输或赢时重启游戏。我们需要添加一个按键监听器事件,以便在用户点击 "0 "时重启游戏。下面是代码:

...
sound_played = False
running = True
while running:
   ...
   # Check for key presses
   ...
   # Restart game
   if keys[pg.K_0]:
       if score.is_game_over():
           score.score = 0
           score.trials = 5
           score.sound_played = False
           bricks.bricks.clear()
           bricks.set_values()
   ...

这段代码会将所有游戏变量重置为默认设置。

结论

这个项目是开始用 Python 构建游戏的绝佳方式。你已经学会了如何使用 Pygame 库构建一个完整的桌面游戏。我们还添加了声音,使游戏更具娱乐性。

现在,你可以自信地自己制作类似的项目了。请在评论区告诉我你对这个游戏的看法,以及你可能有的其他问题。

感谢大家花时间阅读我的文章,你们的支持是我不断前进的动力。期望未来能为大家带来更多有价值的内容,请多多关注我的动态!

;