Pygame 小游戏——打砖块

Pygame 小游戏------打砖块


项目概述

Pygame 是 Python 中一个功能强大的 2D 游戏开发库,它提供了处理图形、声音、输入和事件循环的完整工具集。本文通过 Pygame 实现一个经典打砖块游戏(Breakout)。

在这个游戏中,玩家控制屏幕底部的挡板,弹射小球击碎上方所有砖块。每块砖拥有独立的生命值,颜色随血量变化而加深,击中即扣血,归零后砖块消失并触发粒子爆炸效果,目标是在不失去所有球命的情况下清空全部砖块。其中:

  • 移动挡板:左右方向键 / A、D 键水平移动挡板;挡板击球时,根据球撞击挡板的偏移位置改变出球角度,越靠近两端反弹角度越大。
  • 发球:按空格键发球,发球方向在竖直向上的基础上加入随机偏角(±0.5 弧度),每局可多次发球(失球不扣关卡)。
  • 砖块血量:不同行的砖块初始血量不同(最高行血量最高),每次被球击中扣 1 点血;血量 > 0 时砖块上显示剩余血量数字,血量归零后砖块消失。
  • 关卡递进:清空全部砖块后自动进入下一关,球速随关卡提升(上限 9.0);当前关卡、得分和最高分实时显示在 HUD 栏。
  • 键盘操作:SPACE:发球;R:重新开始;P:暂停/继续。
  • 胜利/失败条件:球掉落到屏幕底部扣一条命;命数归零 = 游戏失败;清空所有砖块 = 过关(自动进入下一关,无上限)。

游戏实现

初始化与基础设置

游戏启动时初始化 Pygame 并定义屏幕尺寸、HUD 高度、颜色常量和砖块布局参数。

python 复制代码
pygame.init()

WIDTH, HEIGHT = 480, 580
HUD_H = 56

screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Breakout: Brick Blaster")
clock = pygame.time.Clock()

颜色定义

python 复制代码
DARK_BG = (3,   8,  16)   # 全局深色背景
BLUE1   = (56, 138, 221)   # 挡板主色 / 球外圈
BLUE2   = (133,183, 235)   # 挡板高光 / 球内圈
WHITE   = (220, 225, 235)  # 文字 / 球高光
GRAY    = (100, 115, 130)  # 次要文字
RED     = (226,  75,  74)  # 生命指示圆 / 游戏结束文字
GOLD    = (250, 199, 117)  # 暂停文字
GREEN   = (93,  202, 165)  # 关卡文字

砖块颜色与布局

python 复制代码
BRICK_COLORS = [
    (83,  74, 183),   # 紫色  第 0 行(最高血量)
    (24,  95, 165),   # 蓝色  第 1 行
    (15, 110,  86),   # 青色  第 2 行
    (59, 109,  17),   # 绿色  第 3 行
    (186,117,  23),   # 琥珀  第 4 行
    (152,  60,  29),  # 珊瑚  第 5 行(最低血量)
]

PAD_W, PAD_H = 80, 12     # 挡板宽高
BALL_R = 8                 # 小球半径
COLS, ROWS_BRICKS = 8, 6   # 砖块列数、行数
B_GAP  = 6                 # 砖块间距
BW = (WIDTH - 40 - (COLS - 1) * B_GAP) // COLS  # 单块宽度
BH = 18                    # 单块高度
``

### 字体加载

```python
CHINESE_FONT_PATH = r"C:/Windows/Fonts/simsun.ttc"

font_sm  = pygame.font.Font(CHINESE_FONT_PATH, 20)
font_md  = pygame.font.Font(CHINESE_FONT_PATH, 28)
font_big = pygame.font.Font(CHINESE_FONT_PATH, 52)

直接加载系统中文字体文件,保证中文 HUD 文字正常显示。跨平台时可替换为 pygame.font.SysFont(None, size)


核心类设计

1. 挡板类(Paddle)

Paddle 类封装挡板的位置、移动逻辑和绘制方法。

构造函数 __init__

python 复制代码
def __init__(self):
    self.x = WIDTH // 2   # 水平中心位置(以挡板中心为基准)
    self.y = HEIGHT - 36  # 固定在屏幕底部附近
    self.speed = 7        # 每帧移动像素数

移动方法 move

python 复制代码
def move(self, dx):
    self.x = max(PAD_W // 2, min(WIDTH - PAD_W // 2, self.x + dx))

通过 max/min 将挡板限制在屏幕边界内,防止超出屏幕。

矩形区域 rect

python 复制代码
def rect(self):
    return pygame.Rect(self.x - PAD_W // 2, self.y, PAD_W, PAD_H)

self.x 为中心计算左边界,便于后续碰撞检测复用。

绘制方法 draw

python 复制代码
def draw(self, surf):
    r = self.rect()
    pygame.draw.rect(surf, BLUE1, r, border_radius=6)               # 底层主色
    pygame.draw.rect(surf, BLUE2, r.inflate(-20, -4), border_radius=4)  # 高光条

用两层矩形叠加实现简洁的高光立体感:外层为深蓝主色,内层收缩后绘制浅蓝高光条。


2. 小球类(Ball)

Ball 类管理小球的物理运动、边界反弹和出屏检测。

构造函数 __init__

python 复制代码
def __init__(self, px, py):
    self.x = float(px)
    self.y = float(py - BALL_R - 2)   # 初始贴在挡板上方
    self.vx = 0.0
    self.vy = 0.0
    self.launched = False              # 是否已发球
    self.radius = BALL_R

未发球时小球跟随挡板水平移动,按空格后才获得速度。

发球方法 launch

python 复制代码
def launch(self, speed):
    ang = -math.pi / 2 + random.uniform(-0.5, 0.5)   # 在竖直向上基础上随机偏角
    self.vx = speed * math.cos(ang)
    self.vy = speed * math.sin(ang)
    self.launched = True

使用极坐标分解速度,random.uniform(-0.5, 0.5) 约 ±28.6°的随机偏角让每局发球方向各不相同,增加趣味性。

更新方法 update

python 复制代码
def update(self, pad_x):
    if not self.launched:
        self.x = float(pad_x)   # 未发球时跟随挡板
        return
    self.x += self.vx
    self.y += self.vy
    # 左右边界反弹
    if self.x - self.radius < 0:
        self.x = float(self.radius); self.vx = abs(self.vx)
    if self.x + self.radius > WIDTH:
        self.x = float(WIDTH - self.radius); self.vx = -abs(self.vx)
    # 顶部边界反弹(HUD 下沿)
    if self.y - self.radius < HUD_H:
        self.y = float(HUD_H + self.radius); self.vy = abs(self.vy)

三面墙反弹均使用 abs() 强制方向,避免因浮点误差导致球"穿墙"后速度方向错误的情况。

出屏检测 off_screen

python 复制代码
def off_screen(self):
    return self.y - self.radius > HEIGHT

绘制方法 draw

python 复制代码
def draw(self, surf):
    ix, iy = int(self.x), int(self.y)
    pygame.draw.circle(surf, BLUE1, (ix, iy), self.radius + 2)  # 外发光圈
    pygame.draw.circle(surf, BLUE2, (ix, iy), self.radius)       # 主球体
    pygame.draw.circle(surf, WHITE, (ix - 2, iy - 2), 3)         # 高光点

三层圆形叠绘:外层比主球半径大 2px 的深蓝光晕,中层浅蓝主体,右上角白色高光点,使球体具有立体感。


3. 砖块类(Brick)

Brick 类封装单块砖的状态、血量和带渐变的绘制逻辑。

构造函数 __init__

python 复制代码
def __init__(self, col, row, hp):
    self.col    = col
    self.row    = row
    self.hp     = hp        # 当前血量
    self.max_hp = hp        # 初始最大血量(用于计算颜色渐变比例)
    self.alive  = True
    self.x = 20 + col * (BW + B_GAP)          # 像素坐标(左上角)
    self.y = HUD_H + 20 + row * (BH + B_GAP)

绘制方法 draw

python 复制代码
def draw(self, surf):
    if not self.alive:
        return
    frac  = self.hp / self.max_hp                       # 血量比例 0.0~1.0
    base  = BRICK_COLORS[self.row % len(BRICK_COLORS)]
    color = tuple(int(c * (0.4 + 0.6 * frac)) for c in base)  # 血量越少颜色越暗
    r = self.rect()
    pygame.draw.rect(surf, color, r, border_radius=4)
    border_a = int(80 * frac)                           # 边框透明度随血量变化
    pygame.draw.rect(surf, (*WHITE[:3], border_a), r, width=1, border_radius=4)
    if self.max_hp > 1:
        label = font_sm.render(str(self.hp), True,
                               tuple(min(255, c + 120) for c in color))
        surf.blit(label, (r.centerx - label.get_width()  // 2,
                          r.centery - label.get_height() // 2))

该方法实现了三层视觉反馈:

  1. 颜色变暗0.4 + 0.6 * frac 将血量映射到亮度,满血时最亮,濒死时保留 40% 亮度(不完全变黑)。
  2. 边框渐隐:白色边框透明度随血量同步减弱,强化"受损"的视觉感。
  3. 血量数字:血量 > 1 的砖块在中心绘制剩余血量,文字色在砖块底色基础上提亮 120,保证可读性。

4. 粒子类(Particle)

Particle 类实现击砖时的粒子爆炸效果,模拟带重力的碎片运动。

构造函数 __init__

python 复制代码
def __init__(self, x, y, color):
    self.x    = float(x)
    self.y    = float(y)
    self.vx   = random.uniform(-3, 3)    # 水平随机速度
    self.vy   = random.uniform(-4, 0.5)  # 初始向上(含少量向下)
    self.life = 28                        # 生命帧数
    self.color = color

更新方法 update

python 复制代码
def update(self):
    self.x  += self.vx
    self.y  += self.vy
    self.vy += 0.2     # 模拟重力加速度
    self.life -= 1

每帧对 vy 施加 0.2 的向下加速度,模拟重力抛物线轨迹。

绘制方法 draw

python 复制代码
def draw(self, surf):
    if self.life <= 0:
        return
    a = min(255, self.life * 9)          # 透明度随生命衰减
    s = pygame.Surface((5, 5), pygame.SRCALPHA)
    s.fill((*self.color[:3], a))
    surf.blit(s, (int(self.x), int(self.y)))

使用 SRCALPHA 透明 Surface 绘制带 Alpha 通道的小方块,life * 9 使粒子在约 28 帧内从不透明线性淡出至消失。


5. 游戏主类(Game)

Game 类统筹所有子系统,负责关卡管理、碰撞检测、状态更新和画面渲染。

构造函数 __init__

python 复制代码
def __init__(self):
    self.best = 0
    self.hp_values = [13, 11, 7, 5, 3, 2]   # 各行砖块初始血量(由上至下递减)
    self.reset()

hp_values 列表决定每行砖块的初始生命值,第 0 行(最上方)最厚实,第 5 行(最下方)最薄,鼓励玩家优先击打下方砖块。

关卡初始化 _init_level

python 复制代码
def _init_level(self):
    self.paddle = Paddle()
    self.ball   = Ball(self.paddle.x, self.paddle.y)
    self.bricks = [
        Brick(c, r, self.hp_values[r])
        for r in range(ROWS_BRICKS)
        for c in range(COLS)
    ]

使用列表推导式快速生成 6×8 共 48 块砖,每行血量由 hp_values[r] 统一控制。

事件处理 handle_events

python 复制代码
def handle_events(self):
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit(); sys.exit()
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_r:
                self.reset()
            if event.key == pygame.K_p:
                self.paused = not self.paused
            if event.key == pygame.K_SPACE and not self.ball.launched:
                self.ball.launch(self.ball_speed)

核心更新 update

update 方法集中处理三段碰撞检测逻辑:

python 复制代码
def update(self):
    if self.paused or self.game_over or self.win:
        return

    # ① 挡板移动
    keys = pygame.key.get_pressed()
    if keys[pygame.K_LEFT]  or keys[pygame.K_a]: self.paddle.move(-self.paddle.speed)
    if keys[pygame.K_RIGHT] or keys[pygame.K_d]: self.paddle.move(self.paddle.speed)

    self.ball.update(self.paddle.x)

    # ② 球出屏 → 扣命
    if self.ball.off_screen():
        self.lives -= 1
        if self.lives <= 0:
            self.game_over = True
            if self.score > self.best: self.best = self.score
        else:
            self.ball = Ball(self.paddle.x, self.paddle.y)
        return

    # ③ 球 vs 挡板
    pr = self.paddle.rect()
    b  = self.ball
    if (b.vy > 0 and
            pr.left - b.radius <= b.x <= pr.right + b.radius and
            pr.top  - b.radius <= b.y <= pr.bottom):
        b.vy = -abs(b.vy)
        offset = (b.x - self.paddle.x) / (PAD_W / 2)   # -1.0 ~ +1.0
        b.vx   = offset * abs(b.vy) * 1.4               # 偏移越大,水平速度越强
        spd    = math.hypot(b.vx, b.vy)
        cap    = self.ball_speed * 1.5
        if spd > cap:
            b.vx *= cap / spd; b.vy *= cap / spd        # 限速,防止球速失控
        b.y = float(pr.top - b.radius - 1)              # 防止球嵌入挡板

    # ④ 球 vs 砖块
    for brick in self.bricks:
        if not brick.alive: continue
        br = brick.rect()
        if not (b.x + b.radius > br.left  and b.x - b.radius < br.right and
                b.y + b.radius > br.top   and b.y - b.radius < br.bottom):
            continue
        # 判断从哪个方向击中(水平穿透量 vs 垂直穿透量)
        ol   = abs(b.x + b.radius - br.left)
        or_  = abs(br.right  - (b.x - b.radius))
        ot   = abs(b.y + b.radius - br.top)
        ob   = abs(br.bottom - (b.y - b.radius))
        minH = min(ol, or_)
        minV = min(ot, ob)
        if minH < minV: b.vx = -b.vx    # 从侧面击中,反转水平速度
        else:           b.vy = -b.vy    # 从上/下击中,反转垂直速度
        brick.hp -= 1
        self.score += brick.max_hp * 10 * self.level
        if self.score > self.best: self.best = self.score
        for _ in range(12):              # 生成粒子
            self.particles.append(Particle(br.centerx, br.centery,
                                           BRICK_COLORS[brick.row % len(BRICK_COLORS)]))
        if brick.hp <= 0: brick.alive = False
        break                            # 每帧只处理一块砖,防止多重碰撞

    # ⑤ 粒子更新
    for p in self.particles[:]:
        p.update()
        if p.life <= 0: self.particles.remove(p)

    # ⑥ 过关检测
    if all(not br.alive for br in self.bricks):
        self.level     += 1
        self.ball_speed = min(4.5 + self.level * 0.4, 9.0)
        self._init_level()

碰撞检测的核心思路:通过比较球在四个方向上的穿透深度(overlap),取最小值判断实际接触面:水平穿透浅 → 从侧面撞;垂直穿透浅 → 从上/下撞。这种方法比复杂的几何求交更简洁,适合 AABB 碰撞场景。

绘制方法 draw

python 复制代码
def draw(self):
    screen.fill(DARK_BG)

    # 星空背景(固定点阵,无需每帧随机)
    for i in range(80):
        sx = (i * 137) % WIDTH
        sy = (i * 251) % HEIGHT
        b  = 30 + (i % 4) * 15
        pygame.draw.circle(screen, (b, b, b + 20), (sx, sy), 1)

    # HUD 信息栏
    pygame.draw.rect(screen, (8, 14, 26), (0, 0, WIDTH, HUD_H))
    pygame.draw.line(screen, BLUE1, (0, HUD_H), (WIDTH, HUD_H), 1)
    screen.blit(font_sm.render(f"分数 {self.score}", True, WHITE), (12, 10))
    screen.blit(font_sm.render(f"最高 {self.best}",  True, GRAY),  (12, 32))
    screen.blit(font_sm.render(f"LV {self.level}",  True, GREEN), (WIDTH - 100, 10))
    for li in range(self.lives):                           # 生命圆点
        pygame.draw.circle(screen, RED, (WIDTH - 22 - li * 20, 34), 6)

    # 游戏对象
    for br in self.bricks:   br.draw(screen)
    for p  in self.particles: p.draw(screen)
    self.paddle.draw(screen)
    self.ball.draw(screen)

    # 提示 / 暂停 / 游戏结束遮罩
    if not self.ball.launched and not self.game_over and not self.win:
        hint = font_sm.render("按 SPACE 发球", True, GRAY)
        screen.blit(hint, (WIDTH // 2 - hint.get_width() // 2, HEIGHT - 70))

    if self.paused:
        overlay = pygame.Surface((WIDTH, HEIGHT - HUD_H), pygame.SRCALPHA)
        overlay.fill((3, 8, 16, 160))
        screen.blit(overlay, (0, HUD_H))
        t = font_big.render("PAUSED", True, GOLD)
        screen.blit(t, (WIDTH // 2 - t.get_width() // 2, HEIGHT // 2 - 30))

    if self.game_over:
        overlay = pygame.Surface((WIDTH, HEIGHT - HUD_H), pygame.SRCALPHA)
        overlay.fill((3, 8, 16, 190))
        screen.blit(overlay, (0, HUD_H))
        t1 = font_big.render("GAME OVER", True, RED)
        t2 = font_sm.render(f"得分 {self.score}  最高 {self.best}", True, WHITE)
        t3 = font_sm.render("按 R 重新开始", True, GRAY)
        screen.blit(t1, (WIDTH // 2 - t1.get_width() // 2, HEIGHT // 2 - 60))
        screen.blit(t2, (WIDTH // 2 - t2.get_width() // 2, HEIGHT // 2 + 10))
        screen.blit(t3, (WIDTH // 2 - t3.get_width() // 2, HEIGHT // 2 + 44))

    pygame.display.flip()

绘制顺序为:背景 → 星空 → HUD → 砖块 → 粒子 → 挡板 → 球 → 叠加遮罩,严格保证层次正确。

主循环 run

python 复制代码
def run(self):
    while True:
        self.handle_events()
        self.update()
        self.draw()
        clock.tick(60)

固定 60 FPS,保证物理运动和动画在不同性能机器上表现一致。


全部代码

python 复制代码
import pygame
import sys
import math
import random

pygame.init()

WIDTH, HEIGHT = 480, 580
HUD_H = 56

screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Breakout: Brick Blaster")
clock = pygame.time.Clock()

DARK_BG  = (3, 8, 16)
BLUE1    = (56, 138, 221)
BLUE2    = (133, 183, 235)
WHITE    = (220, 225, 235)
GRAY     = (100, 115, 130)
RED      = (226, 75, 74)
GOLD     = (250, 199, 117)
GREEN    = (93, 202, 165)

BRICK_COLORS = [
    (83,  74, 183),   # purple   row 0
    (24,  95, 165),   # blue     row 1
    (15, 110,  86),   # teal     row 2
    (59, 109,  17),   # green    row 3
    (186,117,  23),   # amber    row 4
    (152,  60,  29),  # coral    row 5
]

CHINESE_FONT_PATH = r"C:/Windows/Fonts/simsun.ttc"

font_sm  = pygame.font.Font(CHINESE_FONT_PATH, 20)
font_md  = pygame.font.Font(CHINESE_FONT_PATH, 28)
font_big = pygame.font.Font(CHINESE_FONT_PATH, 52)

PAD_W, PAD_H = 80, 12
BALL_R = 8
COLS, ROWS_BRICKS = 8, 6
B_GAP  = 6
BW = (WIDTH - 40 - (COLS - 1) * B_GAP) // COLS
BH = 18


class Paddle:
    def __init__(self):
        self.x = WIDTH // 2
        self.y = HEIGHT - 36
        self.speed = 7

    def move(self, dx):
        self.x = max(PAD_W // 2, min(WIDTH - PAD_W // 2, self.x + dx))

    def rect(self):
        return pygame.Rect(self.x - PAD_W // 2, self.y, PAD_W, PAD_H)

    def draw(self, surf):
        r = self.rect()
        pygame.draw.rect(surf, BLUE1, r, border_radius=6)
        pygame.draw.rect(surf, BLUE2, r.inflate(-20, -4).move(0, 0), border_radius=4)


class Ball:
    def __init__(self, px, py):
        self.x = float(px)
        self.y = float(py - BALL_R - 2)
        self.vx = 0.0
        self.vy = 0.0
        self.launched = False
        self.radius = BALL_R

    def launch(self, speed):
        ang = -math.pi / 2 + random.uniform(-0.5, 0.5)
        self.vx = speed * math.cos(ang)
        self.vy = speed * math.sin(ang)
        self.launched = True

    def update(self, pad_x):
        if not self.launched:
            self.x = float(pad_x)
            return
        self.x += self.vx
        self.y += self.vy
        if self.x - self.radius < 0:
            self.x = float(self.radius); self.vx = abs(self.vx)
        if self.x + self.radius > WIDTH:
            self.x = float(WIDTH - self.radius); self.vx = -abs(self.vx)
        if self.y - self.radius < HUD_H:
            self.y = float(HUD_H + self.radius); self.vy = abs(self.vy)

    def off_screen(self):
        return self.y - self.radius > HEIGHT

    def draw(self, surf):
        ix, iy = int(self.x), int(self.y)
        pygame.draw.circle(surf, BLUE1, (ix, iy), self.radius + 2)
        pygame.draw.circle(surf, BLUE2, (ix, iy), self.radius)
        pygame.draw.circle(surf, WHITE, (ix - 2, iy - 2), 3)


class Brick:
    def __init__(self, col, row, hp):
        self.col = col
        self.row = row
        self.hp  = hp
        self.max_hp = hp
        self.alive = True
        self.x = 20 + col * (BW + B_GAP)
        self.y = HUD_H + 20 + row * (BH + B_GAP)

    def rect(self):
        return pygame.Rect(self.x, self.y, BW, BH)

    def draw(self, surf):
        if not self.alive:
            return
        frac  = self.hp / self.max_hp
        base  = BRICK_COLORS[self.row % len(BRICK_COLORS)]
        color = tuple(int(c * (0.4 + 0.6 * frac)) for c in base)
        r = self.rect()
        pygame.draw.rect(surf, color, r, border_radius=4)
        border_a = int(80 * frac)
        pygame.draw.rect(surf, (*WHITE[:3], border_a), r, width=1, border_radius=4)
        if self.max_hp > 1:
            label = font_sm.render(str(self.hp), True,
                                   tuple(min(255, c + 120) for c in color))
            surf.blit(label, (r.centerx - label.get_width() // 2,
                              r.centery - label.get_height() // 2))


class Particle:
    def __init__(self, x, y, color):
        self.x  = float(x)
        self.y  = float(y)
        self.vx = random.uniform(-3, 3)
        self.vy = random.uniform(-4, 0.5)
        self.life = 28
        self.color = color

    def update(self):
        self.x  += self.vx
        self.y  += self.vy
        self.vy += 0.2
        self.life -= 1

    def draw(self, surf):
        if self.life <= 0:
            return
        a = min(255, self.life * 9)
        s = pygame.Surface((5, 5), pygame.SRCALPHA)
        s.fill((*self.color[:3], a))
        surf.blit(s, (int(self.x), int(self.y)))


class Game:
    def __init__(self):
        self.best = 0
        self.hp_values = [13, 11, 7, 5, 3, 2]
        self.reset()


    def reset(self):
        self.score      = 0
        self.lives      = 3
        self.level      = 1
        self.ball_speed = 4.5
        self.paused     = False
        self.game_over  = False
        self.win        = False
        self.particles  = []
        self._init_level()

    def _init_level(self):
        self.paddle = Paddle()
        self.ball   = Ball(self.paddle.x, self.paddle.y)
        self.bricks = [
            Brick(c, r, self.hp_values[r])
            # Brick(c, r, random.randint(1, 10))
            for r in range(ROWS_BRICKS)
            for c in range(COLS)
        ]

    def handle_events(self):
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit(); sys.exit()
            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_r:
                    self.reset()
                if event.key == pygame.K_p:
                    self.paused = not self.paused
                if event.key == pygame.K_SPACE and not self.ball.launched:
                    self.ball.launch(self.ball_speed)

    def update(self):
        if self.paused or self.game_over or self.win:
            return
        keys = pygame.key.get_pressed()
        if keys[pygame.K_LEFT]  or keys[pygame.K_a]:
            self.paddle.move(-self.paddle.speed)
        if keys[pygame.K_RIGHT] or keys[pygame.K_d]:
            self.paddle.move(self.paddle.speed)

        self.ball.update(self.paddle.x)

        # Ball lost
        if self.ball.off_screen():
            self.lives -= 1
            if self.lives <= 0:
                self.game_over = True
                if self.score > self.best:
                    self.best = self.score
            else:
                self.ball = Ball(self.paddle.x, self.paddle.y)
            return

        # Ball vs paddle
        pr = self.paddle.rect()
        b  = self.ball
        if (b.vy > 0 and
                pr.left - b.radius <= b.x <= pr.right + b.radius and
                pr.top  - b.radius <= b.y <= pr.bottom):
            b.vy = -abs(b.vy)
            offset = (b.x - self.paddle.x) / (PAD_W / 2)
            b.vx   = offset * abs(b.vy) * 1.4
            spd    = math.hypot(b.vx, b.vy)
            cap    = self.ball_speed * 1.5
            if spd > cap:
                b.vx *= cap / spd; b.vy *= cap / spd
            b.y = float(pr.top - b.radius - 1)

        # Ball vs bricks
        for brick in self.bricks:
            if not brick.alive:
                continue
            br = brick.rect()
            if not (b.x + b.radius > br.left  and b.x - b.radius < br.right and
                    b.y + b.radius > br.top    and b.y - b.radius < br.bottom):
                continue
            ol = abs(b.x + b.radius - br.left)
            or_ = abs(br.right  - (b.x - b.radius))
            ot = abs(b.y + b.radius - br.top)
            ob = abs(br.bottom - (b.y - b.radius))
            minH = min(ol, or_)
            minV = min(ot, ob)
            if minH < minV:
                b.vx = -b.vx
            else:
                b.vy = -b.vy
            brick.hp -= 1
            pts = brick.max_hp * 10 * self.level
            self.score += pts
            if self.score > self.best:
                self.best = self.score
            color = BRICK_COLORS[brick.row % len(BRICK_COLORS)]
            for _ in range(12):
                self.particles.append(Particle(br.centerx, br.centery, color))
            if brick.hp <= 0:
                brick.alive = False
            break

        # Update particles
        for p in self.particles[:]:
            p.update()
            if p.life <= 0:
                self.particles.remove(p)

        # Level clear?
        if all(not br.alive for br in self.bricks):
            self.level      += 1
            self.ball_speed  = min(4.5 + self.level * 0.4, 9.0)
            self._init_level()

    def draw(self):
        screen.fill(DARK_BG)

        # subtle star dots
        for i in range(80):
            sx = (i * 137) % WIDTH
            sy = (i * 251) % HEIGHT
            b  = 30 + (i % 4) * 15
            pygame.draw.circle(screen, (b, b, b + 20), (sx, sy), 1)

        # HUD
        pygame.draw.rect(screen, (8, 14, 26), (0, 0, WIDTH, HUD_H))
        pygame.draw.line(screen, BLUE1, (0, HUD_H), (WIDTH, HUD_H), 1)
        screen.blit(font_sm.render(f"分数 {self.score}", True, WHITE),  (12, 10))
        screen.blit(font_sm.render(f"最高 {self.best}",  True, GRAY),   (12, 32))
        screen.blit(font_sm.render(f"LV {self.level}",  True, GREEN),  (WIDTH - 100, 10))
        # lives
        for li in range(self.lives):
            pygame.draw.circle(screen, RED, (WIDTH - 22 - li * 20, 34), 6)

        for br in self.bricks:
            br.draw(screen)
        for p in self.particles:
            p.draw(screen)
        self.paddle.draw(screen)
        self.ball.draw(screen)

        if not self.ball.launched and not self.game_over and not self.win:
            hint = font_sm.render("按 SPACE 发球", True, GRAY)
            screen.blit(hint, (WIDTH // 2 - hint.get_width() // 2, HEIGHT - 70))

        if self.paused:
            overlay = pygame.Surface((WIDTH, HEIGHT - HUD_H), pygame.SRCALPHA)
            overlay.fill((3, 8, 16, 160))
            screen.blit(overlay, (0, HUD_H))
            t = font_big.render("PAUSED", True, GOLD)
            screen.blit(t, (WIDTH // 2 - t.get_width() // 2, HEIGHT // 2 - 30))

        if self.game_over:
            overlay = pygame.Surface((WIDTH, HEIGHT - HUD_H), pygame.SRCALPHA)
            overlay.fill((3, 8, 16, 190))
            screen.blit(overlay, (0, HUD_H))
            t1 = font_big.render("GAME OVER", True, RED)
            t2 = font_sm.render(f"得分 {self.score}  最高 {self.best}", True, WHITE)
            t3 = font_sm.render("按 R 重新开始", True, GRAY)
            screen.blit(t1, (WIDTH // 2 - t1.get_width() // 2, HEIGHT // 2 - 60))
            screen.blit(t2, (WIDTH // 2 - t2.get_width() // 2, HEIGHT // 2 + 10))
            screen.blit(t3, (WIDTH // 2 - t3.get_width() // 2, HEIGHT // 2 + 44))

        pygame.display.flip()

    def run(self):
        while True:
            self.handle_events()
            self.update()
            self.draw()
            clock.tick(60)


if __name__ == "__main__":
    Game().run()

附:文章说明

本文仅为个人理解,若有不当之处,欢迎指正~

相关推荐
AI科技星16 小时前
全域数学公理:32维超球体投影、微观曲率与霍奇猜想的几何化证明
c语言·开发语言·网络·量子计算·agi
薛定谔的猫喵喵16 小时前
【从 HTTP 到 HTTPS】Flask 多项目迁移到 Nginx 子路径完整实战
python·nginx·http·https·flask·ssl
幸运小圣16 小时前
前端三种输入数据来源生成 worksheet(工作表)新手适用详细篇【SheetJS】
开发语言·前端·javascript
lunzi_082616 小时前
【学习笔记】《Python编程 从入门到实践》第1章:Python环境搭建与Hello World(完整版)
笔记·python·学习
花月C16 小时前
LangGraph 状态机与 ReAct Agent
python·agent·react·langgragh
ch.ju16 小时前
Java Programming Chapter 4——Construction method
java·开发语言
烤代码的吐司君16 小时前
面向对象编程(OOP)在 Python 中的实现——类、继承与特殊方法
开发语言·python
小龙报16 小时前
【优选算法】双指针专项:1.移动零 2. 复写零 3.快乐数
java·c语言·数据结构·c++·python·算法·面试