引言

欢迎来到这份关于使用 Python 和 Pygame 构建桌面“钢琴块”游戏的教程!这款游戏旨在完全复刻经典的钢琴块玩法,让您在享受编程乐趣的同时,锻炼反应速度和集中注意力。无论您是 Python 游戏开发初学者,还是希望通过实际项目巩固 Pygame 技能,本项目都将为您提供一个实践的绝佳平台。

在本游戏中,黑色方块会从屏幕顶部不断落下,您的目标是快速点击这些黑色方块以得分,同时必须避免点击白色方块或错过任何一个黑色方块。一旦出现失误,游戏便会立即结束。这种引人入胜且富有挑战性的玩法,将是您练习和巩固基本编程概念、提升 Python 游戏开发技能的宝贵练习。

通过本教程,您不仅将学会构建一个激动人心的节奏游戏,更将深入理解游戏开发的核心概念,包括游戏循环、事件处理、图形渲染、碰撞检测、状态管理和计分系统。

学习目标 (What You’ll Learn)

完成本教程后,您将能够:

  • 掌握 Pygame 基础: 使用 Pygame 初始化窗口、加载资源、处理颜色、绘制图形和文本。
  • 理解游戏循环: 实现并管理游戏的主循环,包括事件处理、游戏状态更新和图形渲染。
  • 实现游戏机制: 学习方块的生成、下落、用户交互(点击)、得分和游戏结束逻辑。
  • 处理用户输入: 通过鼠标点击或键盘输入实现对游戏元素的响应。
  • 管理游戏状态: 在开始、游戏中和游戏结束等不同状态之间进行切换。
  • 实现计分系统: 跟踪玩家分数,并显示当前分数和最高分数。
  • 处理难度递增: 实现游戏速度随时间逐渐增加的逻辑。
  • 代码组织: 学习如何将游戏代码模块化,提高可维护性和可读性。
  • 调试与故障排除: 掌握游戏开发中常见的调试技巧,并学会分析和解决程序中遇到的问题。

前置知识 (Prerequisites)

为确保您能顺利完成本教程,建议您具备以下基础:

  • Python 基础语法: 对 Python 变量、数据类型、条件语句、循环、函数、列表、字典等有基本了解。
  • 面向对象编程 (OOP) 概念: 初步理解类、对象、方法和属性的概念。
  • 无需 Pygame 经验: 本教程将从 Pygame 的基本用法开始讲解。
  • 基本的编程思维: 具备解决问题、耐心阅读和实践代码的热情。

1. 项目概览与核心技术栈

1.1 关于钢琴块游戏

这是一个激动人心的节奏游戏,考验玩家的速度、准确性和反应能力。游戏中,黑色块会从屏幕顶部不断落下,玩家必须快速按下相应的按键才能击中它们,以免它们消失。错过块或按错键都会结束游戏,这无疑增加了游戏的挑战性。该项目使用 Python 的 Pygame 库开发,拥有流畅的动画、灵敏的操控和引人入胜的游戏循环,让玩家保持专注并获得乐趣。这是一个练习事件处理、碰撞检测和游戏循环等编程概念的好方法。

1.2 核心信息与技术栈

  • 语言 (Language): Python
  • 游戏库 (Game Library): Pygame (Python 的游戏开发库)
  • 开发环境 (Coding Tool): 内置 Python IDLE 或任何支持 Python 的 IDE (如 VS Code, PyCharm)
  • 应用类型 (Type): 桌面应用程序 / 游戏 (Desktop Application / Game)
  • 数据存储 (Database Used): 无 (本项目将分数等数据存储在内存中)
  • 目的 (Purpose): 仅供教育目的使用。

1.3 项目文件结构概览

对于这样一个相对简单的游戏,我们可以将所有代码放在一个文件中,以保持简洁。

piano_tiles_game/
├── piano_tiles.py      # 应用程序的入口点和所有游戏逻辑
└── README.md           # 项目说明 (可选)
└── SimHei.ttf          # 【可选】一个中文字体文件,如 SimHei.ttf 或其他,用于显示中文 (见安装指南)

1.4 Pygame 基础概念

Pygame 是一个 Python 模块,旨在帮助您编写视频游戏。它提供了一系列功能,使您能够轻松处理图形、声音、用户输入等。

  • 初始化 (pygame.init()): 启动 Pygame 模块。这是每个 Pygame 程序的第一步。
  • 窗口/屏幕设置 (pygame.display.set_mode()): 创建游戏的显示窗口,并设置其尺寸。
  • 颜色 (RGB 元组): 在 Pygame 中,颜色通常用 (红, 绿, 蓝) 的元组表示,每个值介于 0 到 255 之间。例如,(255, 0, 0) 是红色,(0, 0, 0) 是黑色,(255, 255, 255) 是白色。
  • 矩形 (pygame.Rect): Pygame 中的一个重要对象,用于表示屏幕上的矩形区域。它不仅可以用于绘制矩形,还可以用于检测碰撞和管理位置。
  • 事件处理 (pygame.event.get()): 获取并处理用户输入(如鼠标点击、键盘按键)和系统事件(如窗口关闭)。
  • 游戏循环 (while True): 游戏的核心,不断重复执行以下步骤:处理输入 -> 更新游戏状态 -> 渲染图形。
  • 绘图 (screen.fill(), pygame.draw.rect()): 在屏幕上填充背景颜色,或绘制矩形等基本图形。
  • 更新显示 (pygame.display.flip()pygame.display.update()): 将绘制的内容显示到屏幕上。
  • 时钟/帧率控制 (pygame.time.Clock): 用于控制游戏的帧率(FPS),确保游戏在不同机器上以一致的速度运行。
  • 字体 (pygame.font.Font): 用于在屏幕上渲染文本,例如显示分数或游戏提示。

1.5 游戏机制概述

该游戏的核心机制围绕着从屏幕顶部不断下落的黑色方块。

  • 方块下落: 屏幕被逻辑地划分为多列,方块会在这些列中随机生成并匀速下落。
  • 用户交互: 玩家通过鼠标点击或键盘按键来“击中”下落的黑色方块。
  • 计分: 成功点击一个黑色方块,玩家得分增加。
  • 游戏结束条件:
    • 错过一个黑色方块,让它下落到屏幕底部。
    • 点击了一个白色方块。
  • 难度递增: 随着游戏进行,方块下落的速度会逐渐加快,增加游戏的挑战性。

2. 核心功能实现详解

现在,我们将逐步深入到项目的核心功能实现。本节将结合代码逻辑,详细讲解如何构建用户界面、实现游戏机制、处理交互和计分。

2.1 用户界面 (UI) 构建

游戏的 UI 相对简洁,主要由游戏窗口、不断下落的方块、屏幕上方的计分板以及游戏结束时的提示信息构成。

  • 步骤 1: Pygame 初始化与窗口创建

    • 首先调用 pygame.init() 初始化所有 Pygame 模块。
    • 使用 pygame.display.set_mode() 创建一个固定大小的游戏窗口,并设置其尺寸。
    • 定义游戏背景颜色,通常是黑色或深色。
  • 步骤 2: 绘制方块

    • 游戏中的方块是基本的矩形,通过 pygame.draw.rect() 函数绘制。
    • 方块分为黑色和白色两种,通过不同的颜色值进行区分。
    • 它们会从屏幕顶部生成,并随着游戏循环逐渐向下移动。
  • 步骤 3: 计分板与文本显示

    • 使用 pygame.font.Font() 创建一个字体对象。
    • 利用字体对象渲染当前分数和最高分数的文本。
    • 这些文本会显示在游戏窗口的顶部,确保玩家能实时看到自己的得分。
  • 步骤 4: 游戏开始/结束界面

    • 游戏开始前会显示一个欢迎界面,包含游戏标题和开始游戏的提示。
    • 游戏结束后,会显示一个“游戏结束”界面,展示最终分数、最高分数以及重新开始游戏的选项。

    **【钢琴块游戏主界面示例】
    **

2.2 游戏机制与逻辑

游戏的核心在于不断生成和管理下落的方块,并响应玩家的点击。

  • 2.2.1 游戏状态管理

    • 为了在“开始界面”、“游戏进行中”和“游戏结束”之间切换,我们需要定义不同的游戏状态(例如,使用字符串常量 START, PLAYING, GAME_OVER)。
    • 游戏循环会根据当前状态执行不同的逻辑和渲染。
  • 2.2.2 方块生成与下落

    • 定义一个 Tile 类或字典结构来存储每个方块的信息:位置 (x, y)、颜色、宽度、高度。
    • 在游戏进行中,定时在屏幕顶部的随机一列生成新的方块(确保每一行只有一个黑色方块)。
    • 在每次游戏循环迭代中,更新所有方块的 y 坐标,使其向下移动。
  • 2.2.3 用户交互与事件处理

    • 在游戏循环中,通过 pygame.event.get() 监听鼠标点击事件 (pygame.MOUSEBUTTONDOWN)。
    • 当鼠标点击发生时,获取点击的 (x, y) 坐标。
    • 遍历当前屏幕上所有的方块,检查点击坐标是否与某个方块的 pygame.Rect 区域发生重叠(碰撞检测)。
  • 2.2.4 碰撞检测与得分

    • 如果玩家点击了一个方块:
      • 如果点击的是黑色方块: 增加玩家分数,并将该方块标记为已点击(或移除),以防止重复点击。
      • 如果点击的是白色方块: 游戏结束。
    • 如果一个黑色方块下落到屏幕底部而没有被点击,游戏结束。
  • 2.2.5 难度递增

    • 随着游戏分数的增加或时间的推移,逐步提高方块的下落速度。这可以通过调整方块每次更新 y 坐标的增量来实现。
    • 例如,每达到一定的分数阈值,就将 tile_speed 增加一个小的量。

2.3 计分与游戏结束

  • 计分逻辑:

    • 初始化 current_score 为 0。
    • 每次正确点击黑色方块时,current_score 加 1。
    • 同时维护一个 high_score,记录玩家的最高得分(在当前会话中)。
  • 游戏结束条件:

    • current_game_state 变为 GAME_OVER 时,显示游戏结束界面。
    • 如果 current_score 大于 high_score,则更新 high_score
  • 游戏结束界面:

    • 显示“游戏结束”标题、最终得分、最高得分。
    • 提供一个“重新开始”按钮或提示,让玩家可以再次进行游戏。

    **【钢琴块游戏结束界面示例】
    **

3. 代码风格与最佳实践

良好的代码习惯是编写高质量、可维护软件的关键。

3.1 PEP 8 规范

遵循 Python 官方的代码风格指南 (PEP 8) 是编写易读、一致性代码的基础。

  • 命名约定:
    • 变量名和函数名: 使用 snake_case (例如 tile_speed, handle_input)。
    • 类名: 使用 CamelCase (例如 PianoTilesGame)。
    • 常量名: 使用全大写和下划线 (例如 SCREEN_WIDTH, BLACK_COLOR)。
  • 代码布局: 每行代码长度不超过 79 字符;适当使用空行分隔代码块;使用 4 个空格作为缩进。
  • 导入: 将导入语句放在文件顶部,通常按照标准库、第三方库的顺序排列。

3.2 模块化与类设计

  • 虽然是单文件游戏,但可以将不同职责的代码封装到类和函数中。
  • 例如,创建一个 PianoTilesGame 类来封装游戏的所有逻辑(初始化、事件处理、更新、渲染等)。
  • 内部函数和变量使用 _ 前缀表示私有成员,增强代码的可读性和结构性。

3.3 使用常量

将屏幕尺寸、颜色、方块尺寸、初始速度、帧率等固定值定义为常量(全大写),而不是直接在代码中硬编码。这样做的好处是:

  • 提高可读性: 常量名比裸露的数字更具描述性。
  • 易于修改: 当需要调整这些值时,只需修改常量定义处即可,无需搜索整个代码。

3.4 错误处理与健壮性

  • Pygame 初始化错误: Pygame 初始化时可能失败(例如,缺乏必要的图形驱动)。使用 try-exceptpygame.quit() 确保程序能优雅地退出。
  • 防御性编程: 考虑用户可能进行的意外操作,例如快速连击或在不合时宜的时候点击,并确保游戏逻辑能够正确处理。

3.5 注释和 Docstrings

  • 为复杂的逻辑块、不明显的算法和重要的数据结构添加注释。
  • 为类和函数添加 Docstrings (文档字符串),解释它们的功能、参数、返回值。

完整项目代码

为了方便您自行保存、上传和分享,以下是本项目的完整 Python 代码文件。请在您的电脑上创建一个新文件夹(例如 piano_tiles_game),并将以下代码块的内容保存为 piano_tiles.py 文件。

piano_tiles.py (已修复中文字体乱码问题)

import pygame
import random
import sys
import os # 导入 os 模块以处理文件路径

# --- 常量 ---
SCREEN_WIDTH = 400
SCREEN_HEIGHT = 600
NUM_LANES = 4
TILE_HEIGHT = SCREEN_HEIGHT // 4  # 每个方块的初始高度,占屏幕高度的 1/4
TILE_WIDTH = SCREEN_WIDTH // NUM_LANES

# 颜色
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
GRAY = (100, 100, 100)
LIGHT_GRAY = (200, 200, 200)
RED = (255, 0, 0)
GREEN = (0, 255, 0)

# 游戏状态
GAME_STATE_START = 0
GAME_STATE_PLAYING = 1
GAME_STATE_GAME_OVER = 2

# 初始游戏速度 (每帧移动像素)
INITIAL_TILE_SPEED = 3
SPEED_INCREASE_INTERVAL = 10 # 每增加 10 分提高一次速度
SPEED_INCREASE_AMOUNT = 0.5 # 每次速度增加量

# --- 方块类 ---
class Tile(pygame.sprite.Sprite):
    def __init__(self, x, y, width, height, color):
        super().__init__()
        self.image = pygame.Surface([width, height])
        self.image.fill(color)
        self.rect = self.image.get_rect()
        self.rect.x = x
        self.rect.y = y
        self.color = color
        self.is_black = (color == BLACK)
        self.clicked = False # 用于防止对同一个方块重复点击

    def update(self, speed):
        self.rect.y += speed

# --- 钢琴块游戏类 ---
class PianoTilesGame:
    def __init__(self):
        pygame.init()
        self.screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
        pygame.display.set_caption("Python 钢琴块") # 设置窗口标题为中文
        self.clock = pygame.time.Clock()
        self.running = True

        # --- 字体加载改进,支持中文字符 ---
        font_path = None
        
        # 尝试查找系统中的常见中文字体 (主要针对 Windows 系统)
        if sys.platform == "win32":
            potential_system_fonts = ["SimHei", "Microsoft YaHei", "Arial Unicode MS"]
            for f_name in potential_system_fonts:
                try:
                    matched_font_path = pygame.font.match_font(f_name, bold=False, italic=False)
                    if matched_font_path:
                        font_path = matched_font_path
                        # print(f"已加载系统字体: {f_name} from {matched_font_path}") # 调试信息
                        break
                except Exception:
                    continue 
        
        # 如果系统字体未找到,尝试加载程序目录下的中文字体文件
        bundled_font_name = "SimHei.ttf" # 假设中文字体文件名为 SimHei.ttf
        bundled_font_path = os.path.join(os.path.dirname(__file__), bundled_font_name)
        if font_path is None and os.path.exists(bundled_font_path):
            font_path = bundled_font_path
            # print(f"已加载捆绑字体: {bundled_font_path}") # 调试信息

        if font_path is None:
            print("警告: 未找到中文支持字体。请确保系统安装有中文字体,或将 'SimHei.ttf' 等中文字体文件放入游戏目录。将使用默认字体,可能显示乱码。")
            # 如果都没有找到,fallback 到 Pygame 默认字体 (可能会是乱码)
        
        # 使用加载到的字体路径,或者 None (表示使用默认字体)
        self.font_large = pygame.font.Font(font_path, 74)
        self.font_medium = pygame.font.Font(font_path, 50)
        self.font_small = pygame.font.Font(font_path, 36)
        # --- 字体加载改进结束 ---

        self.all_tiles = pygame.sprite.Group()
        self.game_state = GAME_STATE_START
        self.score = 0
        self.high_score = 0 # 当前会话的最高分
        self.tile_speed = INITIAL_TILE_SPEED

        self._reset_game()

    def _reset_game(self):
        """重置游戏变量以开始新一轮游戏。"""
        self.score = 0
        self.tile_speed = INITIAL_TILE_SPEED
        self.all_tiles.empty()
        self._generate_initial_tiles()
        self.missed_black_tile = False
        self.clicked_wrong_tile = False

    def _generate_new_row(self):
        """生成一行新的方块,其中包含一个随机的黑色方块。"""
        black_lane = random.randint(0, NUM_LANES - 1)
        for i in range(NUM_LANES):
            x = i * TILE_WIDTH
            y = -TILE_HEIGHT # 新方块在屏幕顶部上方生成,以便向下坠落
            color = BLACK if i == black_lane else WHITE
            new_tile = Tile(x, y, TILE_WIDTH, TILE_HEIGHT, color)
            self.all_tiles.add(new_tile)

    def _generate_initial_tiles(self):
        """
        修复后的初始方块生成逻辑。
        生成足够数量的方块以填满屏幕,并确保它们在屏幕上方或刚好在屏幕内,不会立即触发游戏结束。
        """
        self.all_tiles.empty() # 清除所有现有方块

        # 需要生成多少行方块来覆盖屏幕,并预留一些行在屏幕上方以便平滑下落
        num_visible_rows = SCREEN_HEIGHT // TILE_HEIGHT # 屏幕内可见的行数 (例如 600/150 = 4)
        num_buffer_rows_above = 2 # 额外在屏幕上方预留的行数,用于方块下落缓冲

        total_rows_to_generate = num_visible_rows + num_buffer_rows_above # 总共需要生成的行数 (例如 4+2=6)

        for row_idx in range(total_rows_to_generate):
            black_lane = random.randint(0, NUM_LANES - 1)
            for i in range(NUM_LANES):
                x = i * TILE_WIDTH
                # 计算方块的 Y 坐标:
                # 最顶部的行 (row_idx=0) 会在 -TILE_HEIGHT * num_buffer_rows_above 的位置 (例如 -150*2 = -300)
                # 最底部的行 (row_idx=total_rows_to_generate-1) 会在 SCREEN_HEIGHT - TILE_HEIGHT 的位置 (例如 600-150 = 450)
                # 这样确保初始状态下所有方块都在屏幕上方或屏幕内,且不会立即触发游戏结束条件
                y = -TILE_HEIGHT * num_buffer_rows_above + (row_idx * TILE_HEIGHT)
                
                color = BLACK if i == black_lane else WHITE
                new_tile = Tile(x, y, TILE_WIDTH, TILE_HEIGHT, color)
                self.all_tiles.add(new_tile)

    def _handle_input(self):
        """处理所有 Pygame 事件 (鼠标点击, 退出)。"""
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                self.running = False
            
            if self.game_state == GAME_STATE_START:
                if event.type == pygame.MOUSEBUTTONDOWN:
                    self.game_state = GAME_STATE_PLAYING
                    self._reset_game() # 在开始游戏前重置游戏状态和方块
            
            elif self.game_state == GAME_STATE_PLAYING:
                if event.type == pygame.MOUSEBUTTONDOWN:
                    clicked_pos = event.pos # 鼠标点击的 (x, y) 坐标
                    self._check_tile_click(clicked_pos)
            
            elif self.game_state == GAME_STATE_GAME_OVER:
                if event.type == pygame.MOUSEBUTTONDOWN:
                    # 检查是否点击了重新开始按钮
                    mouse_x, mouse_y = event.pos
                    button_width = 150
                    button_height = 50
                    button_x = (SCREEN_WIDTH - button_width) // 2
                    button_y = (SCREEN_HEIGHT - button_height) // 2 + 100 # 按钮位于分数下方
                    if button_x <= mouse_x <= button_x + button_width and \
                       button_y <= mouse_y <= button_y + button_height:
                        self.game_state = GAME_STATE_PLAYING
                        self._reset_game() # 重新开始游戏

    def _check_tile_click(self, clicked_pos):
        """
        修复后的点击检测逻辑。
        检查是否点击了方块,并更新分数或游戏状态。
        移除了之前限制点击区域的 `if tile.rect.y > SCREEN_HEIGHT // 2:` 条件。
        """
        # 找出所有与点击位置碰撞的可见方块
        candidate_tiles = []
        for tile in self.all_tiles:
            # 只考虑仍在屏幕上方的方块 (避免点击已落出屏幕的方块)
            if tile.rect.collidepoint(clicked_pos) and tile.rect.y < SCREEN_HEIGHT:
                candidate_tiles.append(tile)

        if not candidate_tiles: # 如果点击了空白区域,不触发任何操作
            return

        clicked_black_tile = None
        clicked_white_tile = None
        
        # 遍历所有被点击的方块,找出最底部(最“活跃”)的未点击黑色方块,或任意白色方块
        for tile in candidate_tiles:
            if tile.is_black and not tile.clicked:
                # 找到最底部的未点击黑色方块 (通常是玩家应该点击的目标)
                if clicked_black_tile is None or tile.rect.y > clicked_black_tile.rect.y:
                    clicked_black_tile = tile
            elif not tile.is_black:
                # 如果点击了白色方块,则这是一个错误的点击
                clicked_white_tile = tile
        
        if clicked_black_tile: # 如果点击了一个有效的黑色方块
            self.score += 1
            clicked_black_tile.clicked = True
            clicked_black_tile.image.fill(GRAY) # 改变颜色表示已被点击
            self.all_tiles.remove(clicked_black_tile) # 移除已被点击的方块
            
            # 增加速度
            if self.score > 0 and self.score % SPEED_INCREASE_INTERVAL == 0:
                self.tile_speed += SPEED_INCREASE_AMOUNT
        elif clicked_white_tile: # 如果点击了白色方块,游戏结束
            self.clicked_wrong_tile = True
            self.game_state = GAME_STATE_GAME_OVER
        else: # 如果点击了某个方块,但它不是未点击的黑色方块 (例如点击了已变灰的黑色方块),也视为错误点击
            self.clicked_wrong_tile = True
            self.game_state = GAME_STATE_GAME_OVER


    def _check_and_generate_rows(self):
        """
        移除屏幕外的方块,并在需要时生成新的方块行。
        """
        tiles_to_remove = []
        
        for tile in self.all_tiles:
            # 检查是否有黑色方块越过屏幕底部
            if tile.rect.y >= SCREEN_HEIGHT:
                if tile.is_black and not tile.clicked: # 黑色方块未点击就到底部,游戏结束
                    self.missed_black_tile = True
                    self.game_state = GAME_STATE_GAME_OVER
                tiles_to_remove.append(tile) # 将越过屏幕的方块标记为待移除
        
        # 移除已标记的方块
        for tile in tiles_to_remove:
            self.all_tiles.remove(tile)
        
        # 检查是否需要生成新的行 (当最上方的方块行移动到一定位置时)
        # 获取当前所有方块中最高的 Y 坐标
        top_most_y = min([t.rect.y for t in self.all_tiles]) if self.all_tiles else 0

        # 如果最高的方块 Y 坐标已经大于等于 0 (即最高方块已经进入屏幕),
        # 则在屏幕上方生成一行新方块,以保持连续性
        if top_most_y >= 0:
            self._generate_new_row()

    def _update_game_state(self):
        """根据当前游戏状态更新游戏逻辑。"""
        if self.game_state == GAME_STATE_PLAYING:
            self.all_tiles.update(self.tile_speed) # 更新所有方块的位置
            self._check_and_generate_rows() # 检查并生成新的方块行

            # 检查游戏是否结束
            if self.missed_black_tile or self.clicked_wrong_tile:
                self.game_state = GAME_STATE_GAME_OVER
                if self.score > self.high_score:
                    self.high_score = self.score # 更新最高分

    def _draw_elements(self):
        """在屏幕上绘制所有游戏元素。"""
        self.screen.fill(LIGHT_GRAY) # 填充背景色

        # 绘制分界线
        for i in range(1, NUM_LANES):
            pygame.draw.line(self.screen, GRAY, (i * TILE_WIDTH, 0), (i * TILE_WIDTH, SCREEN_HEIGHT), 2)

        # 绘制所有方块
        self.all_tiles.draw(self.screen)

        # 绘制分数
        score_text = self.font_small.render(f"分数: {self.score}", True, BLACK)
        self.screen.blit(score_text, (10, 10))

        high_score_text = self.font_small.render(f"最高分: {self.high_score}", True, BLACK)
        self.screen.blit(high_score_text, (SCREEN_WIDTH - high_score_text.get_width() - 10, 10))

    def _draw_start_screen(self):
        """绘制开始界面。"""
        self.screen.fill(WHITE)
        title_text = self.font_large.render("钢琴块", True, BLACK)
        self.screen.blit(title_text, (SCREEN_WIDTH // 2 - title_text.get_width() // 2, SCREEN_HEIGHT // 4))

        instructions_text = self.font_small.render("点击黑色方块得分,避免白色方块", True, BLACK)
        self.screen.blit(instructions_text, (SCREEN_WIDTH // 2 - instructions_text.get_width() // 2, SCREEN_HEIGHT // 2))

        start_text = self.font_medium.render("点击任意位置开始", True, RED)
        self.screen.blit(start_text, (SCREEN_WIDTH // 2 - start_text.get_width() // 2, SCREEN_HEIGHT * 3 // 4))

    def _draw_game_over_screen(self):
        """绘制游戏结束界面。"""
        self.screen.fill(WHITE)
        game_over_text = self.font_large.render("游戏结束", True, RED)
        self.screen.blit(game_over_text, (SCREEN_WIDTH // 2 - game_over_text.get_width() // 2, SCREEN_HEIGHT // 4))

        final_score_text = self.font_medium.render(f"最终分数: {self.score}", True, BLACK)
        self.screen.blit(final_score_text, (SCREEN_WIDTH // 2 - final_score_text.get_width() // 2, SCREEN_HEIGHT // 2 - 50))
        
        high_score_text = self.font_small.render(f"最高分: {self.high_score}", True, BLACK)
        self.screen.blit(high_score_text, (SCREEN_WIDTH // 2 - high_score_text.get_width() // 2, SCREEN_HEIGHT // 2))

        # 重新开始按钮
        button_width = 150
        button_height = 50
        button_x = (SCREEN_WIDTH - button_width) // 2
        button_y = (SCREEN_HEIGHT - button_height) // 2 + 100
        
        pygame.draw.rect(self.screen, GREEN, (button_x, button_y, button_width, button_height))
        restart_text = self.font_small.render("重新开始", True, WHITE)
        self.screen.blit(restart_text, (button_x + (button_width - restart_text.get_width()) // 2,
                                        button_y + (button_height - restart_text.get_height()) // 2))

    def run(self):
        """主游戏循环。"""
        while self.running:
            self._handle_input()

            if self.game_state == GAME_STATE_PLAYING:
                self._update_game_state()

            if self.game_state == GAME_STATE_START:
                self._draw_start_screen()
            elif self.game_state == GAME_STATE_PLAYING:
                self._draw_elements()
            elif self.game_state == GAME_STATE_GAME_OVER:
                self._draw_game_over_screen()

            pygame.display.flip() # 更新整个显示界面到屏幕
            self.clock.tick(60) # 限制游戏帧率为 60 FPS

        pygame.quit()
        sys.exit()

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



5. 安装与运行指南

5.1 环境搭建

  • Python 安装:

    • 确保您的计算机上已安装 Python 3.6 或更高版本。
    • Windows 用户: 访问 Python 官方网站 (https://www.python.org/downloads/) 下载并安装最新版本。安装时请勾选“Add Python to PATH”选项。
    • macOS / Linux 用户: 通常预装了 Python,但建议通过 Homebrew (brew install python3) 或包管理器 (sudo apt install python3) 安装最新版本。
    • 验证安装: 打开命令行或终端,输入 python --versionpython3 --version 来检查 Python 版本。
  • Pygame 安装:

    • Pygame 是一个第三方库,需要通过 pip 进行安装。打开命令行或终端,输入以下命令:
      pip install pygame
      
    • 如果您的系统同时存在 Python 2 和 Python 3,请确保为 Python 3 安装 Pygame:
      pip3 install pygame
      
    • 验证 Pygame: 在 Python 解释器中输入 import pygame。如果没有报错,则表示 Pygame 已正确安装。

5.2 获取项目代码

请参照上一节完整项目代码部分,将提供的代码复制并保存到您新建的 piano_tiles_game 文件夹中。

  • 步骤:
    1. 创建一个新的项目文件夹,例如 piano_tiles_game
    2. 在该文件夹内,创建一个空白的 Python 文件,命名为 piano_tiles.py
    3. 将上述提供的完整代码,完整地复制并粘贴piano_tiles.py 文件中。
    • 重要提示: 请确保所有Python文件都以 UTF-8 编码 保存,且代码的缩进是正确的(Python使用缩进来定义代码块,通常是4个空格)。

5.3 解决中文字体乱码问题(关键步骤)

由于 Pygame 默认字体通常不支持中文字符,您需要确保程序能够找到一个合适的中文字体。代码中已经加入了自动检测逻辑:

  1. 自动检测系统字体: 程序会首先尝试在 Windows 系统中查找一些常见的中文字体(如“SimHei”、“Microsoft YaHei”)。如果找到并成功加载,则无需额外操作。
  2. 加载程序目录下的字体文件(推荐方式): 如果自动检测系统字体失败,程序会尝试在 piano_tiles.py 文件所在的目录中寻找一个名为 SimHei.ttf 的字体文件。
    • 操作: 您可以从互联网上下载一个免费的中文字体文件(例如 思源黑体 (Source Han Sans) 或 Windows 系统自带的 SimHei.ttfMicrosoft YaHei.ttf)。
    • 将下载的字体文件(例如 SimHei.ttf)复制到 piano_tiles.py 所在的 piano_tiles_game 文件夹中。
    • 如果您下载的字体文件不是 SimHei.ttf 这个名字,请将其重命名为 SimHei.ttf,或者修改 piano_tiles.pybundled_font_name = "SimHei.ttf" 这一行,将其改为您实际使用的字体文件名。

完成上述字体设置后,程序应该就能正常显示中文了。

5.4 运行应用程序

  1. 导航到项目根目录: 打开命令行或终端,使用 cd 命令进入您的项目文件夹(例如 cd piano_tiles_game)。
  2. 运行 piano_tiles.py 文件:
    python piano_tiles.py
    
    或者,如果您的系统上同时存在 Python 2 和 Python 3,请明确指定:
    python3 piano_tiles.py
    
  3. 游戏窗口应该会立即弹出,并显示开始界面。点击任意位置即可开始游戏。

5.5 常见问题与调试技巧

  • Pygame 模块未找到 (ModuleNotFoundError: No module named 'pygame'):
    • 原因: Pygame 库没有正确安装。
    • 解决方案: 确保您已按照 5.1 节的说明使用 pip install pygame (或 pip3 install pygame) 成功安装了 Pygame。
  • 中文字体仍显示乱码:
    • 原因: 程序未能找到或加载到合适的中文字体。
    • 解决方案: 仔细检查 5.3 节的字体设置步骤。确保 SimHei.ttf 文件已放置在 piano_tiles.py 同目录下,或您已将代码中的字体文件名修改为正确的文件名。在控制台输出中,可以查看是否有“警告: 未找到中文支持字体”的信息。
  • 黑屏或窗口不响应:
    • 原因: 可能是游戏循环没有正确更新显示,或者事件处理堵塞。
    • 解决方案: 检查 pygame.display.flip()self.clock.tick(60) 是否在游戏循环的末尾正确调用。
  • 游戏逻辑错误:
    • print() 语句: 在代码的关键位置插入 print() 语句,输出变量的值(例如方块的 y 坐标、玩家分数),函数的执行路径。这是 Pygame 调试最直接的方法。
    • IDE 调试器: 如果您使用 VS Code 或 PyCharm 等 IDE,它们通常内置了强大的调试器。您可以在代码中设置断点,然后逐步执行代码,检查变量状态。
    • 错误信息分析: 当程序抛出异常时,仔细阅读控制台输出的错误信息 (Traceback)。它会指明错误类型、错误发生的文件名和行号,是定位问题的关键线索。

6. 总结与展望

恭喜您!您已经完成了这个 Python Pygame 钢琴块游戏的构建。

通过这个项目,您不仅成功搭建了一个功能齐全的桌面游戏,更重要的是,您深入掌握了 Pygame 游戏开发的核心技能,理解了游戏循环、事件处理、图形渲染、状态管理和计分系统等重要的游戏编程概念。

6.1 知识点回顾

  • Pygame 基础: 初始化、窗口创建、颜色、矩形、字体(包括中文字体加载)。
  • 游戏循环: 事件处理、状态更新、图形渲染的循环模式。
  • 游戏状态管理: 如何在开始、进行中、结束等不同游戏状态之间切换。
  • 对象导向编程: 使用 Tile 类管理游戏中的方块。
  • 用户输入处理: 响应鼠标点击事件。
  • 游戏机制: 方块生成与下落、碰撞检测、得分逻辑、难度递增。
  • 图形渲染: 在屏幕上绘制方块、文本。
  • 代码质量: 接触了 PEP 8 规范、常量使用、模块化思想。

6.2 进一步的优化与扩展 (Next Steps)

本教程为您奠定了坚实的基础,但一个完整的游戏还有很多可以改进和扩展的地方。您可以尝试以下挑战,继续提升您的技能:

  • 添加音效和背景音乐:
    • 为方块点击添加音效,为游戏添加背景音乐,增强沉浸感。
    • 使用 pygame.mixer 模块实现。
  • 更复杂的方块模式:
    • 不仅仅是单行单块,可以尝试多块、特殊方块(例如长方块)。
    • 引入不同颜色的方块,每个颜色有不同的效果。
  • 计分系统增强:
    • 实现连击(Combo)机制,连续正确点击可获得额外分数。
    • 持久化最高分:将最高分保存到文件(例如 JSON 文件),以便下次启动游戏时加载。
  • 更精美的UI和动画:
    • 替换矩形方块为图片精灵,使游戏看起来更专业。
    • 为方块点击或游戏结束添加粒子效果或过渡动画。
  • 增加游戏模式:
    • 时间挑战模式:在规定时间内尽可能得分。
    • 无限模式:没有明确的目标,只看你能坚持多久。
  • 优化性能: 对于更复杂的场景,学习如何优化 Pygame 绘图和事件处理,以保持流畅的帧率。
  • 键盘控制: 允许玩家使用键盘按键(例如 A, S, D, F 对应四列)来点击方块。

希望这份详尽的教程能够为您在 Python 游戏开发领域打开一扇新的大门。持续学习,不断实践,您将成为一名优秀的开发者!

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐