返回文章列表

进阶打砖块:关卡编辑器与道具系统

打砖块(Breakout)是经典街机游戏。在基础版本之上,本文将扩展实现关卡编辑器、道具系统、粒子爆炸效果和连击计分系统,打造一个功能丰富、趣味十足的进阶版打砖块游戏。

一、回顾基础打砖块

基础打砖块包含:球拍(挡板)、球、砖块三个核心元素。玩家移动球拍接住反弹的球,球撞击砖块时砖块消失。当所有砖块被消除即通关,球掉落则失败。

# 基础常量定义
SCREEN_WIDTH = 800
SCREEN_HEIGHT = 600
FPS = 60

# 球拍
PADDLE_WIDTH = 100
PADDLE_HEIGHT = 15
PADDLE_SPEED = 8
PADDLE_Y = SCREEN_HEIGHT - 40

# 球
BALL_SIZE = 10
BALL_SPEED = 6

# 砖块
BRICK_WIDTH = 60
BRICK_HEIGHT = 20
BRICK_ROWS = 6
BRICK_COLS = 12
BRICK_TOP_OFFSET = 50

# 颜色
COLOR_BG = (20, 20, 30)
COLOR_PADDLE = (100, 200, 255)
COLOR_BALL = (255, 255, 100)
COLOR_TEXT = (255, 255, 255)

# 砖块颜色(按耐久度)
BRICK_COLORS = {
    1: (100, 200, 100),   # 绿色 - 1击
    2: (255, 200, 100),   # 橙色 - 2击
    3: (255, 100, 100),   # 红色 - 3击
    4: (200, 100, 255),   # 紫色 - 4击
}

二、关卡数据格式设计

好的关卡数据格式应该易于设计、存储和加载。我们使用二维数组表示砖块布局,每个数字代表砖块的耐久度(0表示无砖块)。

# 关卡数据格式:二维数组,数字代表砖块耐久度
# 0 = 空,1-4 = 砖块耐久度,5 = 特殊砖块(掉落道具)

LEVELS = [
    # 关卡1:经典布局
    {
        "name": "经典开局",
        "layout": [
            [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
            [2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2],
            [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3],
            [2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2],
            [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
        ],
        "ball_speed": 6,
    },
    # 关卡2:金字塔
    {
        "name": "金字塔",
        "layout": [
            [0, 0, 0, 0, 0, 4, 4, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 3, 3, 3, 3, 0, 0, 0, 0],
            [0, 0, 0, 2, 2, 5, 5, 2, 2, 0, 0, 0],
            [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0],
            [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
        ],
        "ball_speed": 7,
    },
    # 关卡3:棋盘
    {
        "name": "棋盘格",
        "layout": [
            [3, 0, 3, 0, 3, 0, 3, 0, 3, 0, 3, 0],
            [0, 2, 0, 2, 0, 5, 0, 5, 0, 2, 0, 2],
            [3, 0, 3, 0, 3, 0, 3, 0, 3, 0, 3, 0],
            [0, 2, 0, 2, 0, 5, 0, 5, 0, 2, 0, 2],
            [3, 0, 3, 0, 3, 0, 3, 0, 3, 0, 3, 0],
        ],
        "ball_speed": 8,
    },
]

import json

class LevelManager:
    """关卡管理器"""

    def __init__(self):
        self.levels = LEVELS
        self.current_level = 0
        self.total_score = 0

    def get_current_level(self):
        if 0 <= self.current_level < len(self.levels):
            return self.levels[self.current_level]
        return None

    def next_level(self):
        self.current_level += 1
        return self.current_level < len(self.levels)

    def reset(self):
        self.current_level = 0
        self.total_score = 0

    def save_levels(self, filename="breakout_levels.json"):
        """保存自定义关卡"""
        with open(filename, 'w', encoding='utf-8') as f:
            json.dump(self.levels, f, ensure_ascii=False, indent=2)

    def load_levels(self, filename="breakout_levels.json"):
        """加载关卡文件"""
        import os
        if os.path.exists(filename):
            with open(filename, 'r', encoding='utf-8') as f:
                self.levels = json.load(f)
            return True
        return False

三、关卡编辑器实现(鼠标编辑)

关卡编辑器允许玩家用鼠标设计自己的关卡。点击或拖动来放置/移除砖块,支持选择砖块类型。

import pygame

class LevelEditor:
    """关卡编辑器"""

    def __init__(self):
        self.rows = 8
        self.cols = 12
        self.grid = [[0] * self.cols for _ in range(self.rows)]
        self.current_brick = 1  # 当前选中的砖块类型
        self.brush_size = 1     # 笔刷大小

        pygame.init()
        self.screen = pygame.display.set_mode(
            (SCREEN_WIDTH, SCREEN_HEIGHT)
        )
        pygame.display.set_caption("打砖块 - 关卡编辑器")
        self.clock = pygame.time.Clock()
        self.font = pygame.font.SysFont("simhei", 16)

        # 编辑器区域
        self.edit_area = pygame.Rect(
            20, 50,
            self.cols * BRICK_WIDTH,
            self.rows * BRICK_HEIGHT
        )

        # 砖块类型选择栏
        self.brick_types = [0, 1, 2, 3, 4, 5]  # 0=橡皮擦, 1-5=砖块
        self.palette_rects = []

    def run(self):
        """编辑器主循环"""
        running = True
        mouse_pressed = False

        while running:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    running = False
                elif event.type == pygame.KEYDOWN:
                    if event.key == pygame.K_ESCAPE:
                        running = False
                    elif event.key == pygame.K_s:
                        self.save_level()
                    elif event.key == pygame.K_c:
                        self.clear_grid()
                    # 数字键选择砖块类型
                    elif pygame.K_0 <= event.key <= pygame.K_5:
                        self.current_brick = event.key - pygame.K_0
                elif event.type == pygame.MOUSEBUTTONDOWN:
                    if event.button == 1:  # 左键
                        mouse_pressed = True
                        self.handle_mouse(event.pos)
                elif event.type == pygame.MOUSEBUTTONUP:
                    if event.button == 1:
                        mouse_pressed = False
                elif event.type == pygame.MOUSEMOTION:
                    if mouse_pressed:
                        self.handle_mouse(event.pos)

            self.draw()
            pygame.display.flip()
            self.clock.tick(60)

        pygame.quit()

    def handle_mouse(self, pos):
        """处理鼠标点击"""
        x, y = pos

        # 检查是否点击了调色板
        for i, rect in enumerate(self.palette_rects):
            if rect.collidepoint(pos):
                self.current_brick = self.brick_types[i]
                return

        # 编辑网格
        if self.edit_area.collidepoint(pos):
            col = (x - self.edit_area.x) // BRICK_WIDTH
            row = (y - self.edit_area.y) // BRICK_HEIGHT
            if 0 <= row < self.rows and 0 <= col < self.cols:
                # 笔刷大小处理
                for dr in range(self.brush_size):
                    for dc in range(self.brush_size):
                        r, c = row + dr, col + dc
                        if 0 <= r < self.rows and 0 <= c < self.cols:
                            self.grid[r][c] = self.current_brick

    def draw(self):
        """绘制编辑器界面"""
        self.screen.fill(COLOR_BG)

        # 绘制标题
        title = self.font.render(
            "关卡编辑器 [0-5选择砖块] [S保存] [C清空] [ESC退出]",
            True, COLOR_TEXT
        )
        self.screen.blit(title, (10, 10))

        # 绘制网格背景
        pygame.draw.rect(self.screen, (40, 40, 50), self.edit_area)

        # 绘制砖块
        for row in range(self.rows):
            for col in range(self.cols):
                val = self.grid[row][col]
                if val > 0:
                    x = self.edit_area.x + col * BRICK_WIDTH
                    y = self.edit_area.y + row * BRICK_HEIGHT
                    color = BRICK_COLORS.get(val, (150, 150, 150))
                    if val == 5:
                        color = (255, 255, 0)  # 特殊砖块用金色
                    rect = pygame.Rect(x + 1, y + 1,
                                     BRICK_WIDTH - 2, BRICK_HEIGHT - 2)
                    pygame.draw.rect(self.screen, color, rect)
                    pygame.draw.rect(self.screen, (255, 255, 255), rect, 1)

        # 绘制网格线
        for col in range(self.cols + 1):
            x = self.edit_area.x + col * BRICK_WIDTH
            pygame.draw.line(self.screen, (60, 60, 70),
                           (x, self.edit_area.y),
                           (x, self.edit_area.bottom), 1)
        for row in range(self.rows + 1):
            y = self.edit_area.y + row * BRICK_HEIGHT
            pygame.draw.line(self.screen, (60, 60, 70),
                           (self.edit_area.x, y),
                           (self.edit_area.right, y), 1)

        # 绘制调色板
        self.draw_palette()

        # 显示当前选择
        info = self.font.render(
            f"当前砖块: {self.current_brick} (0=橡皮擦, 5=道具砖)",
            True, COLOR_TEXT
        )
        self.screen.blit(info, (10, SCREEN_HEIGHT - 30))

    def draw_palette(self):
        """绘制砖块选择调色板"""
        palette_y = self.edit_area.bottom + 20
        self.palette_rects = []

        for i, brick_type in enumerate(self.brick_types):
            x = 20 + i * (BRICK_WIDTH + 10)
            rect = pygame.Rect(x, palette_y, BRICK_WIDTH, BRICK_HEIGHT)

            # 砖块颜色
            if brick_type == 0:
                color = (80, 80, 80)  # 橡皮擦
            elif brick_type == 5:
                color = (255, 255, 0)  # 道具砖
            else:
                color = BRICK_COLORS[brick_type]

            pygame.draw.rect(self.screen, color, rect)
            pygame.draw.rect(self.screen, COLOR_TEXT, rect, 2)

            # 高亮当前选择
            if brick_type == self.current_brick:
                pygame.draw.rect(self.screen, (255, 255, 0), rect.inflate(4, 4), 2)

            # 标签
            label = self.font.render(str(brick_type), True, COLOR_TEXT)
            self.screen.blit(label, (x + BRICK_WIDTH // 2 - 5, palette_y + 2))

            self.palette_rects.append(rect)

    def clear_grid(self):
        """清空网格"""
        self.grid = [[0] * self.cols for _ in range(self.rows)]

    def save_level(self):
        """保存关卡"""
        level = {
            "name": f"自定义关卡",
            "layout": [row[:] for row in self.grid],
            "ball_speed": 6,
        }
        LEVELS.append(level)
        print(f"关卡已保存!当前共 {len(LEVELS)} 个关卡")

四、道具系统设计

道具系统为游戏增加策略性和趣味性。特殊砖块(类型5)被击碎时会掉落道具,玩家用球拍接住道具即可获得相应效果。

import pygame
import random

# 道具类型
POWERUP_MULTI_BALL = 'multi_ball'    # 多球
POWERUP_WIDE_PADDLE = 'wide_paddle'  # 加宽球拍
POWERUP_LASER = 'laser'              # 激光
POWERUP_SLOW = 'slow'                # 减速
POWERUP_EXTRA_LIFE = 'extra_life'    # 加命
POWERUP_SHRINK = 'shrink'            # 缩小球拍(负面)

# 道具配置
POWERUP_CONFIG = {
    POWERUP_MULTI_BALL: {
        'color': (100, 255, 100),
        'symbol': 'M',
        'name': '多球',
        'duration': 0,  # 永久(直到球丢失)
    },
    POWERUP_WIDE_PADDLE: {
        'color': (100, 200, 255),
        'symbol': 'W',
        'name': '加宽',
        'duration': 600,  # 10秒(60FPS)
    },
    POWERUP_LASER: {
        'color': (255, 100, 100),
        'symbol': 'L',
        'name': '激光',
        'duration': 600,
    },
    POWERUP_SLOW: {
        'color': (200, 200, 255),
        'symbol': 'S',
        'name': '减速',
        'duration': 480,
    },
    POWERUP_EXTRA_LIFE: {
        'color': (255, 200, 100),
        'symbol': '+',
        'name': '加命',
        'duration': 0,
    },
    POWERUP_SHRINK: {
        'color': (150, 150, 150),
        'symbol': 'X',
        'name': '缩小',
        'duration': 480,
    },
}

# 道具掉落概率权重
POWERUP_WEIGHTS = [
    (POWERUP_MULTI_BALL, 15),
    (POWERUP_WIDE_PADDLE, 20),
    (POWERUP_LASER, 15),
    (POWERUP_SLOW, 15),
    (POWERUP_EXTRA_LIFE, 5),
    (POWERUP_SHRINK, 10),  # 负面道具
]


class PowerUp:
    """道具类"""

    FALL_SPEED = 3
    SIZE = 20

    def __init__(self, x, y, power_type):
        self.x = x
        self.y = y
        self.type = power_type
        self.config = POWERUP_CONFIG[power_type]
        self.rect = pygame.Rect(x, y, self.SIZE, self.SIZE)
        self.active = True

    def update(self):
        """更新道具位置"""
        self.y += self.FALL_SPEED
        self.rect.y = self.y
        if self.y > SCREEN_HEIGHT:
            self.active = False

    def draw(self, screen, font):
        """绘制道具"""
        color = self.config['color']
        # 旋转效果(简单的闪烁)
        pygame.draw.rect(screen, color, self.rect)
        pygame.draw.rect(screen, (255, 255, 255), self.rect, 2)
        # 符号
        symbol = font.render(self.config['symbol'], True, (0, 0, 0))
        screen.blit(symbol, (self.x + 6, self.y + 2))

    @staticmethod
    def random_powerup():
        """随机选择一个道具类型"""
        types = [t for t, _ in POWERUP_WEIGHTS]
        weights = [w for _, w in POWERUP_WEIGHTS]
        return random.choices(types, weights=weights, k=1)[0]


class PowerUpManager:
    """道具效果管理器"""

    def __init__(self, game):
        self.game = game
        self.active_effects = {}  # {type: remaining_frames}

    def activate(self, power_type):
        """激活道具效果"""
        config = POWERUP_CONFIG[power_type]

        if power_type == POWERUP_MULTI_BALL:
            self._activate_multi_ball()
        elif power_type == POWERUP_WIDE_PADDLE:
            self.game.paddle.width = PADDLE_WIDTH * 1.5
            self.active_effects[power_type] = config['duration']
        elif power_type == POWERUP_LASER:
            self.game.paddle.has_laser = True
            self.active_effects[power_type] = config['duration']
        elif power_type == POWERUP_SLOW:
            for ball in self.game.balls:
                ball.speed_multiplier = 0.6
            self.active_effects[power_type] = config['duration']
        elif power_type == POWERUP_EXTRA_LIFE:
            self.game.lives += 1
        elif power_type == POWERUP_SHRINK:
            self.game.paddle.width = PADDLE_WIDTH * 0.7
            self.active_effects[power_type] = config['duration']

        print(f"道具激活: {config['name']}")

    def _activate_multi_ball(self):
        """多球效果:每个球分裂成2个"""
        new_balls = []
        for ball in self.game.balls:
            new_ball = Ball(ball.x, ball.y)
            new_ball.dx = -ball.dx
            new_ball.dy = ball.dy
            new_balls.append(new_ball)
        self.game.balls.extend(new_balls)

    def update(self):
        """更新所有活跃效果"""
        expired = []
        for power_type, frames in self.active_effects.items():
            frames -= 1
            if frames <= 0:
                expired.append(power_type)
            else:
                self.active_effects[power_type] = frames

        for power_type in expired:
            self.deactivate(power_type)

    def deactivate(self, power_type):
        """取消道具效果"""
        if power_type == POWERUP_WIDE_PADDLE:
            self.game.paddle.width = PADDLE_WIDTH
        elif power_type == POWERUP_LASER:
            self.game.paddle.has_laser = False
        elif power_type == POWERUP_SLOW:
            for ball in self.game.balls:
                ball.speed_multiplier = 1.0
        elif power_type == POWERUP_SHRINK:
            self.game.paddle.width = PADDLE_WIDTH
        del self.active_effects[power_type]

    def get_active_names(self):
        """获取当前活跃的道具名称列表"""
        return [POWERUP_CONFIG[t]['name'] for t in self.active_effects]

五、核心游戏对象实现

import pygame
import math

class Paddle:
    """球拍"""

    def __init__(self):
        self.width = PADDLE_WIDTH
        self.height = PADDLE_HEIGHT
        self.x = (SCREEN_WIDTH - self.width) // 2
        self.y = PADDLE_Y
        self.has_laser = False
        self.laser_cooldown = 0

    @property
    def rect(self):
        return pygame.Rect(self.x, self.y, int(self.width), self.height)

    def move(self, dx):
        self.x += dx
        self.x = max(0, min(self.x, SCREEN_WIDTH - self.width))

    def draw(self, screen):
        color = (255, 100, 100) if self.has_laser else COLOR_PADDLE
        pygame.draw.rect(screen, color, self.rect, border_radius=5)
        # 激光发射器
        if self.has_laser:
            for i in range(3):
                lx = self.x + (i + 1) * self.width / 4
                pygame.draw.circle(screen, (255, 200, 0),
                                 (int(lx), self.y - 3), 3)


class Ball:
    """球"""

    def __init__(self, x, y):
        self.x = x
        self.y = y
        self.size = BALL_SIZE
        self.speed = BALL_SPEED
        self.speed_multiplier = 1.0
        angle = random.uniform(-math.pi / 3, math.pi / 3)
        self.dx = self.speed * math.sin(angle)
        self.dy = -self.speed * math.cos(angle)
        self.trail = []  # 拖尾效果

    @property
    def rect(self):
        return pygame.Rect(self.x - self.size, self.y - self.size,
                          self.size * 2, self.size * 2)

    def update(self):
        self.trail.append((self.x, self.y))
        if len(self.trail) > 8:
            self.trail.pop(0)

        speed = self.speed * self.speed_multiplier
        self.x += self.dx * self.speed_multiplier
        self.y += self.dy * self.speed_multiplier

        # 墙壁碰撞
        if self.x - self.size < 0:
            self.x = self.size
            self.dx = abs(self.dx)
        if self.x + self.size > SCREEN_WIDTH:
            self.x = SCREEN_WIDTH - self.size
            self.dx = -abs(self.dx)
        if self.y - self.size < 0:
            self.y = self.size
            self.dy = abs(self.dy)

    def draw(self, screen):
        # 拖尾
        for i, (tx, ty) in enumerate(self.trail):
            alpha = (i + 1) / len(self.trail)
            radius = int(self.size * alpha)
            color_val = int(100 + 155 * alpha)
            pygame.draw.circle(screen, (color_val, color_val, 50),
                             (int(tx), int(ty)), radius)
        # 球
        pygame.draw.circle(screen, COLOR_BALL,
                         (int(self.x), int(self.y)), self.size)
        pygame.draw.circle(screen, (255, 255, 200),
                         (int(self.x), int(self.y)), self.size, 1)


class Brick:
    """砖块"""

    def __init__(self, x, y, durability, has_powerup=False):
        self.x = x
        self.y = y
        self.width = BRICK_WIDTH
        self.height = BRICK_HEIGHT
        self.durability = durability
        self.max_durability = durability
        self.has_powerup = has_powerup
        self.alive = True
        self.hit_flash = 0  # 被击中闪烁

    @property
    def rect(self):
        return pygame.Rect(self.x, self.y, self.width, self.height)

    def hit(self):
        """被击中,返回是否被破坏"""
        self.durability -= 1
        self.hit_flash = 5
        if self.durability <= 0:
            self.alive = False
            return True
        return False

    def draw(self, screen):
        if not self.alive:
            return

        # 颜色根据耐久度
        ratio = self.durability / self.max_durability
        base_color = BRICK_COLORS.get(self.max_durability, (150, 150, 150))

        # 闪烁效果
        if self.hit_flash > 0:
            color = tuple(min(255, c + 100) for c in base_color)
            self.hit_flash -= 1
        else:
            color = base_color

        pygame.draw.rect(screen, color, self.rect)
        pygame.draw.rect(screen, (255, 255, 255), self.rect, 1)

        # 道具砖块标记
        if self.has_powerup:
            cx = self.x + self.width // 2
            cy = self.y + self.height // 2
            pygame.draw.circle(screen, (255, 255, 0),
                             (cx, cy), 5, 2)


class Laser:
    """激光(激光道具的弹丸)"""

    def __init__(self, x, y):
        self.x = x
        self.y = y
        self.speed = 10
        self.active = True

    @property
    def rect(self):
        return pygame.Rect(self.x - 2, self.y, 4, 15)

    def update(self):
        self.y -= self.speed
        if self.y < 0:
            self.active = False

    def draw(self, screen):
        pygame.draw.rect(screen, (255, 100, 100),
                        (self.x - 2, self.y, 4, 15))

六、粒子爆炸效果

粒子效果让砖块被击碎时产生碎片飞溅,增加视觉冲击力。

import random
import math

class Particle:
    """单个粒子"""

    def __init__(self, x, y, color):
        self.x = x
        self.y = y
        angle = random.uniform(0, math.pi * 2)
        speed = random.uniform(2, 6)
        self.dx = math.cos(angle) * speed
        self.dy = math.sin(angle) * speed
        self.color = color
        self.size = random.randint(2, 5)
        self.life = random.randint(20, 40)
        self.max_life = self.life
        self.gravity = 0.15

    def update(self):
        self.x += self.dx
        self.y += self.dy
        self.dy += self.gravity  # 重力
        self.dx *= 0.98  # 空气阻力
        self.life -= 1

    def draw(self, screen):
        if self.life > 0:
            alpha = self.life / self.max_life
            size = max(1, int(self.size * alpha))
            color = tuple(int(c * alpha) for c in self.color)
            pygame.draw.circle(screen, color,
                             (int(self.x), int(self.y)), size)


class ParticleSystem:
    """粒子系统"""

    def __init__(self):
        self.particles = []

    def emit(self, x, y, color, count=15):
        """在指定位置发射粒子"""
        for _ in range(count):
            self.particles.append(Particle(x, y, color))

    def emit_explosion(self, x, y, color):
        """爆炸效果(更多粒子)"""
        self.emit(x, y, color, count=25)
        # 额外的亮色粒子
        bright_color = tuple(min(255, c + 80) for c in color)
        self.emit(x, y, bright_color, count=10)

    def update(self):
        """更新所有粒子"""
        for p in self.particles:
            p.update()
        self.particles = [p for p in self.particles if p.life > 0]

    def draw(self, screen):
        for p in self.particles:
            p.draw(screen)

    def clear(self):
        self.particles.clear()

七、计分与连击系统

class ScoreSystem:
    """计分与连击系统"""

    # 砖块分数
    BRICK_SCORES = {
        1: 10,
        2: 20,
        3: 30,
        4: 50,
        5: 40,  # 道具砖
    }

    def __init__(self):
        self.score = 0
        self.combo = 0
        self.combo_timer = 0
        self.max_combo = 0
        self.combo_threshold = 120  # 2秒内连续击中算连击

    def add_brick_score(self, brick_durability):
        """添加砖块分数"""
        base_score = self.BRICK_SCORES.get(brick_durability, 10)
        combo_multiplier = 1 + self.combo * 0.1  # 连击加成
        earned = int(base_score * combo_multiplier)
        self.score += earned
        self.combo += 1
        self.combo_timer = self.combo_threshold
        self.max_combo = max(self.max_combo, self.combo)
        return earned

    def update(self):
        """更新连击计时器"""
        if self.combo_timer > 0:
            self.combo_timer -= 1
            if self.combo_timer <= 0:
                self.combo = 0  # 连击中断

    def get_combo_text(self):
        """获取连击显示文本"""
        if self.combo >= 3:
            return f"{self.combo} 连击!"
        return ""

    def reset(self):
        self.score = 0
        self.combo = 0
        self.combo_timer = 0
        self.max_combo = 0

八、完整游戏主类

将所有组件整合到游戏主类中:

import pygame
import sys
import random

class BreakoutGame:
    """进阶打砖块游戏主类"""

    def __init__(self):
        pygame.init()
        self.screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
        pygame.display.set_caption("进阶打砖块 - 关卡编辑器与道具系统")
        self.clock = pygame.time.Clock()
        self.font = pygame.font.SysFont("simhei", 20)
        self.big_font = pygame.font.SysFont("simhei", 48)

        self.level_manager = LevelManager()
        self.score_system = ScoreSystem()
        self.particle_system = ParticleSystem()
        self.powerup_manager = None

        self.state = "menu"  # menu, playing, paused, level_complete, game_over
        self.lives = 3
        self.powerups = []  # 掉落中的道具
        self.lasers = []    # 激光弹丸
        self.bricks = []
        self.balls = []
        self.paddle = None

        self.load_level()

    def load_level(self):
        """加载当前关卡"""
        level = self.level_manager.get_current_level()
        if not level:
            self.state = "game_over"
            return

        self.bricks = []
        layout = level["layout"]
        for row, row_data in enumerate(layout):
            for col, val in enumerate(row_data):
                if val > 0:
                    x = 20 + col * BRICK_WIDTH
                    y = BRICK_TOP_OFFSET + row * BRICK_HEIGHT
                    has_powerup = (val == 5)
                    durability = 1 if val == 5 else val
                    self.bricks.append(Brick(x, y, durability, has_powerup))

        self.paddle = Paddle()
        self.balls = [Ball(SCREEN_WIDTH // 2, SCREEN_HEIGHT - 60)]
        self.powerup_manager = PowerUpManager(self)
        self.particle_system.clear()
        self.powerups.clear()
        self.lasers.clear()

    def run(self):
        """主游戏循环"""
        running = True
        while running:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    running = False
                elif event.type == pygame.KEYDOWN:
                    if event.key == pygame.K_ESCAPE:
                        if self.state == "playing":
                            self.state = "paused"
                        elif self.state == "paused":
                            self.state = "playing"
                        else:
                            running = False
                    elif event.key == pygame.K_SPACE:
                        if self.state == "menu":
                            self.state = "playing"
                        elif self.state == "level_complete":
                            if self.level_manager.next_level():
                                self.load_level()
                                self.state = "playing"
                            else:
                                self.state = "game_over"
                        elif self.state == "game_over":
                            self.restart()
                    elif event.key == pygame.K_r:
                        self.restart()
                    elif event.key == pygame.K_f:
                        # 发射激光
                        if self.paddle and self.paddle.has_laser:
                            self.fire_laser()

            if self.state == "playing":
                self.update()

            self.draw()
            pygame.display.flip()
            self.clock.tick(FPS)

        pygame.quit()
        sys.exit()

    def restart(self):
        """重新开始游戏"""
        self.level_manager.reset()
        self.score_system.reset()
        self.lives = 3
        self.load_level()
        self.state = "playing"

    def fire_laser(self):
        """发射激光"""
        if self.paddle.laser_cooldown <= 0:
            self.lasers.append(Laser(self.paddle.x + 20, self.paddle.y))
            self.lasers.append(Laser(self.paddle.x + self.paddle.width - 20,
                                     self.paddle.y))
            self.paddle.laser_cooldown = 15

    def update(self):
        """更新游戏状态"""
        keys = pygame.key.get_pressed()
        if keys[pygame.K_LEFT] or keys[pygame.K_a]:
            self.paddle.move(-PADDLE_SPEED)
        if keys[pygame.K_RIGHT] or keys[pygame.K_d]:
            self.paddle.move(PADDLE_SPEED)

        self.paddle.laser_cooldown = max(0, self.paddle.laser_cooldown - 1)

        # 更新球
        for ball in self.balls[:]:
            ball.update()
            self.check_ball_collisions(ball)
            if ball.y - ball.size > SCREEN_HEIGHT:
                self.balls.remove(ball)

        # 球丢失
        if not self.balls:
            self.lives -= 1
            if self.lives <= 0:
                self.state = "game_over"
            else:
                self.balls = [Ball(SCREEN_WIDTH // 2, SCREEN_HEIGHT - 60)]

        # 更新道具
        for powerup in self.powerups[:]:
            powerup.update()
            if powerup.rect.colliderect(self.paddle.rect):
                self.powerup_manager.activate(powerup.type)
                self.powerups.remove(powerup)
            elif not powerup.active:
                self.powerups.remove(powerup)

        # 更新激光
        for laser in self.lasers[:]:
            laser.update()
            for brick in self.bricks:
                if brick.alive and laser.rect.colliderect(brick.rect):
                    brick.hit()
                    laser.active = False
                    break
            if not laser.active:
                self.lasers.remove(laser)

        # 更新粒子
        self.particle_system.update()

        # 更新道具效果
        self.powerup_manager.update()

        # 更新连击
        self.score_system.update()

        # 检查关卡完成
        if not any(b.alive for b in self.bricks):
            self.state = "level_complete"
            self.level_manager.total_score = self.score_system.score

    def check_ball_collisions(self, ball):
        """检查球的碰撞"""
        # 球拍碰撞
        if ball.rect.colliderect(self.paddle.rect) and ball.dy > 0:
            ball.y = self.paddle.y - ball.size
            # 根据击中位置改变角度
            relative_x = (ball.x - self.paddle.x) / self.paddle.width
            angle = (relative_x - 0.5) * math.pi * 0.7
            speed = math.hypot(ball.dx, ball.dy)
            ball.dx = speed * math.sin(angle)
            ball.dy = -abs(speed * math.cos(angle))

        # 砖块碰撞
        for brick in self.bricks[:]:
            if not brick.alive:
                continue
            if ball.rect.colliderect(brick.rect):
                # 判断碰撞方向
                dx = ball.x - (brick.x + brick.width / 2)
                dy = ball.y - (brick.y + brick.height / 2)
                if abs(dx / brick.width) > abs(dy / brick.height):
                    ball.dx = -ball.dx
                else:
                    ball.dy = -ball.dy

                destroyed = brick.hit()
                if destroyed:
                    self.score_system.add_brick_score(brick.max_durability)
                    # 粒子效果
                    self.particle_system.emit_explosion(
                        brick.x + brick.width // 2,
                        brick.y + brick.height // 2,
                        BRICK_COLORS.get(brick.max_durability, (150, 150, 150))
                    )
                    # 道具掉落
                    if brick.has_powerup and random.random() < 0.8:
                        power_type = PowerUp.random_powerup()
                        self.powerups.append(PowerUp(
                            brick.x + brick.width // 2 - 10,
                            brick.y,
                            power_type
                        ))
                break

    def draw(self):
        """绘制游戏画面"""
        self.screen.fill(COLOR_BG)

        if self.state == "menu":
            self.draw_menu()
        elif self.state in ("playing", "paused", "level_complete", "game_over"):
            self.draw_game()
            if self.state == "paused":
                self.draw_overlay("暂停", "按 ESC 继续")
            elif self.state == "level_complete":
                self.draw_overlay("关卡完成!", "按 空格 进入下一关")
            elif self.state == "game_over":
                self.draw_overlay("游戏结束", f"总分: {self.score_system.score}")

    def draw_game(self):
        """绘制游戏画面"""
        # 砖块
        for brick in self.bricks:
            brick.draw(self.screen)

        # 道具
        for powerup in self.powerups:
            powerup.draw(self.screen, self.font)

        # 激光
        for laser in self.lasers:
            laser.draw(self.screen)

        # 粒子
        self.particle_system.draw(self.screen)

        # 球拍
        self.paddle.draw(self.screen)

        # 球
        for ball in self.balls:
            ball.draw(self.screen)

        # HUD
        self.draw_hud()

    def draw_hud(self):
        """绘制信息栏"""
        info = (f"分数: {self.score_system.score}  "
                f"生命: {self.lives}  "
                f"关卡: {self.level_manager.current_level + 1}")
        text = self.font.render(info, True, COLOR_TEXT)
        self.screen.blit(text, (10, 10))

        # 连击显示
        combo_text = self.score_system.get_combo_text()
        if combo_text:
            combo = self.big_font.render(combo_text, True, (255, 200, 0))
            self.screen.blit(combo, (SCREEN_WIDTH // 2 - 80, 15))

        # 活跃道具
        active = self.powerup_manager.get_active_names()
        if active:
            power_text = self.font.render(
                f"道具: {', '.join(active)}", True, (150, 255, 150)
            )
            self.screen.blit(power_text, (SCREEN_WIDTH - 200, 10))

    def draw_menu(self):
        """绘制主菜单"""
        title = self.big_font.render("进阶打砖块", True, COLOR_TEXT)
        self.screen.blit(title, (SCREEN_WIDTH // 2 - 120, 200))

        subtitle = self.font.render(
            "按 空格 开始游戏  |  ESC 退出", True, COLOR_TEXT
        )
        self.screen.blit(subtitle, (SCREEN_WIDTH // 2 - 120, 280))

        controls = [
            "A/D 或 ←/键 移动球拍",
            "F 发射激光(需激光道具)",
            "ESC 暂停/继续",
            "R 重新开始",
        ]
        for i, line in enumerate(controls):
            text = self.font.render(line, True, (180, 180, 180))
            self.screen.blit(text, (SCREEN_WIDTH // 2 - 100, 340 + i * 30))

    def draw_overlay(self, title, subtitle):
        """绘制覆盖层"""
        overlay = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT), pygame.SRCALPHA)
        overlay.fill((0, 0, 0, 150))
        self.screen.blit(overlay, (0, 0))

        title_surf = self.big_font.render(title, True, COLOR_TEXT)
        self.screen.blit(title_surf,
                        (SCREEN_WIDTH // 2 - title_surf.get_width() // 2, 250))

        sub_surf = self.font.render(subtitle, True, COLOR_TEXT)
        self.screen.blit(sub_surf,
                        (SCREEN_WIDTH // 2 - sub_surf.get_width() // 2, 320))

if __name__ == "__main__":
    game = BreakoutGame()
    game.run()

九、扩展建议

通过本教程,你学会了在基础打砖块上扩展实现关卡编辑器、道具系统、粒子效果和连击计分。这些技术不仅适用于打砖块,也可以应用到其他2D游戏中。关卡编辑器让游戏内容可扩展,道具系统增加策略深度,粒子效果提升视觉体验,连击系统激励玩家追求高分。继续扩展和完善,你可以打造出一款真正有趣的游戏作品。

动手挑战

学到这里,不妨动手试一试以下练习,巩固你的理解:

  1. 基础练习:回顾本文核心概念,用自己的话总结关键知识点。
  2. 进阶实践:将文中的示例代码运行一遍,尝试修改参数观察变化。
  3. 拓展思考:想一想这个技术/方法还能应用在哪些场景中?

小贴士:遇到问题时,先独立思考,再查阅资料,最后请教他人——这是成长最快的学习方式。

赞赏支持

本文更新于 2026-08-22,环境 Python 3.12