这次我们来看一个特别适合 Python 初学者的项目:用 pygame 从零写一款可玩的小游戏。不是把网上的完整源码拉下来跑一遍,而是把场景、角色、掉落物、碰撞判定、计分规则、生命值、难度递增这些模块拆开重做,全部用 Python 自带能力加 pygame 完成。整个项目不需要 GPU、不需要外部 API、不需要模型服务,装好 Python 和 pygame 就能跑。如果你正在学 Python,想做点能交互、有图形界面的东西,从命令行脚本过渡到桌面程序,这篇文章可以给你一条完整的练习路线。
先给结论:这个项目核心是一个 2D 简易小游戏,玩家控制底部的挡板接金币、躲炸弹。技术栈是 Python 3 + pygame 2.x,代码量控制在 300 行以内,素材直接用几何图形代替,不需要准备美术资源。游戏支持得分、生命值、难度递增、重新开始、自动测试模式,整体非常适合用来练习面向对象、事件循环和碰撞检测。
文章会按“项目能力 -> 适用边界 -> 环境准备 -> 代码设计 -> 运行测试 -> 性能观察 -> 问题排查 -> 最佳实践”的顺序展开。你可以直接把代码片段组合成一个可运行项目,也可以只读思路,再用自己的方式重写。
1. 核心能力速览
| 能力项 | 说明 |
|---|---|
| 项目类型 | Python 2D 小游戏 |
| 主要玩法 | 方向键控制挡板接金币、避开炸弹 |
| 技术栈 | Python 3.8+,pygame 2.x |
| 核心功能 | 精灵管理、碰撞检测、计分、生命值、难度递增、自动测试模式 |
| 硬件要求 | 普通 CPU 即可,无 GPU 要求,无显存占用 |
| 支持系统 | Windows / macOS / Linux |
| 启动方式 | 命令行运行python main.py |
| API 接口 | 无网络 API,提供命令行参数和 JSON 配置 |
| 批量任务 | 支持多组配置自动回归测试 |
| 适合场景 | Python 入门练手、面向对象练习、小游戏原型开发 |
这个项目最大的价值不是“做了一个游戏”,而是用最小成本覆盖了 Python 编程里的关键概念:类与继承、集合与碰撞检测、事件监听、时间循环、配置分离。游戏本身很简单,但代码组织方式是工程化的,后续想扩展成什么玩法,都有清晰的切入点。
2. 适用场景与使用边界
这个项目最适合这几类人。
第一类是刚学完 Python 基础语法、但不知道做什么练习的人。很多人的第一个 Python 程序只会在终端里跑print,遇到图形界面就心虚。pygame 可以让你第一次看到“输入 -> 逻辑 -> 画面反馈 -> 分数变化”的完整回路,学起来比单独背知识点更有直观反馈。
第二类是正在学面向对象编程的人。游戏里的玩家、金币、炸弹天然是“对象”,每个对象有属性、方法和自己的绘制方式。通过给类加方法、调整属性,能真正理解继承和封装的作用,而不是死记语法。
第三类是想快速验证小游戏原型的人。比如你在设计一个独立游戏关卡,想先试一下掉落物密度、速度、碰撞范围,不一定要用 Unity 这类重型引擎。Python + pygame 足够快速验证核心玩法。
使用边界也要说清楚。pygame 适合做 2D 原型和简单小游戏,不适合做大型商业项目、3D 场景、复杂网络同步或者高性能物理模拟。如果需要大量美术素材,也不建议硬用几何图形撑起整个产品。另一个重要边界是素材合规问题:替换成真实图片、音效或背景音乐时,必须使用自己制作的内容,或者使用明确允许商用的素材,不要从网上随手下载未授权资源。
3. 环境准备与前置条件
3.1 Python 安装检查
先从 Python 开始。Windows 用户可以直接去 Python 官网下载安装包,安装时需要勾选“Add Python to PATH”,否则命令行里敲python会提示找不到命令。macOS 和 Linux 用户一般可以通过系统包管理器安装,也可以使用官网安装包。
安装完之后,打开终端或命令提示符,验证环境:
python --version pip --version如果pip提示不存在,可以尝试:
python -m pip --version这条命令在 macOS 和 Linux 下更容易成功,因为很多系统自带的 Python 没有把pip加入 PATH。
3.2 安装 pygame
确认 Python 可用之后,直接安装 pygame:
pip install pygame建议安装 2.x 版本。pygame 2.x 在 macO 和 Windows 下的兼容性都比 1.9 好很多,字体渲染、窗口缩放、帧率控制也更稳定。
安装完成后,运行一个测试命令:
python -m pygame.examples.aliens如果看到 pygame 自带的“外星人”示例窗口能正常打开、关闭,说明环境已经可用。
3.3 项目目录结构
建议在本地建一个独立文件夹存放游戏代码,我用的结构如下:
catch_game/ ├── main.py ├── settings.json └── test_batch.py不需要额外的虚拟环境,但如果你同时在维护其他 Python 项目,用虚拟环境更稳妥:
python -m venv venv # Windows venv\Scripts\activate # macOS / Linux source venv/bin/activate这样可以避免不同项目之间的依赖冲突。
3.4 系统依赖
pygame 在 Windows 上基本是开箱即用。macOS 如果遇到音频相关报错,通常是因为权限或驱动问题,可以后面再处理;Linux 可能需要安装一些系统库,比如 SDL 相关依赖,不过大多数发行版都能通过系统包管理器自动解决。这个游戏不使用外部图片和音频,所以这部分依赖可以降到最低。
4. 游戏设计与代码结构
4.1 玩法拆解
要做游戏,先把玩法拆成可实现的模块:
- 玩家:在屏幕底部左右移动,用键盘方向键控制。
- 掉落物:从屏幕顶部随机生成并向下移动,分为金币和炸弹两类。
- 碰撞:玩家接到金币加分,碰到炸弹扣命。
- 计分:累计得分,到达一定阈值后提高难度。
- 游戏状态:运行时、结束、退出。
- 自动测试:支持命令行参数控制,方便后续做批量验证。
这样做的好处是每个功能都能单独测试,不会等到整段代码写完之后发现问题却无从下手。
4.2 状态与帧率
pygame 游戏的核心是主循环。主循环每秒刷新很多帧,每一帧完成“读取输入 -> 更新状态 -> 绘制画面”这个顺序。游戏中的移动、掉落、碰撞都需要依赖一个稳定的帧率控制,否则在不同电脑上运行速度不一样。
配置文件中我会把窗口宽度、高度、帧率、玩家速度、金币分值、初始生命值、掉落物生成间隔、难度升级分数都集中管理,后续调参数不用改代码。
4.3 核心文件职责
settings.json放所有数值参数;main.py放游戏逻辑;test_batch.py放批量自动测试逻辑。主文件内部按职责分成配置加载、精灵类、游戏主循环、命令行入口四个部分。这个分层思路可以复用到几乎所有 Python 桌面小项目上。
5. 核心模块实现
5.1 配置文件
先写settings.json,把所有可调参数集中起来:
{ "screen_width": 800, "screen_height": 600, "fps": 60, "player_speed": 8, "coin_value": 10, "start_lives": 3, "spawn_interval_min": 20, "difficulty_step_score": 100, "coin_ratio": 0.8 }解释一下每个字段的含义:screen_width和screen_height是游戏窗口大小;fps是目标帧率;player_speed是玩家移动速度;coin_value是每个金币得分;start_lives是初始生命值;spawn_interval_min是最小生成间隔,数字越小生成越快;difficulty_step_score是每积累多少分提升一次难度;coin_ratio是生成金币的概率,0.8 表示八成概率生成金币、两成概率生成炸弹。
把参数独立到配置文件之后,调游戏难度就不需要改动游戏逻辑代码,很大的后期维护成本会被省掉。
5.2 配置加载与全局常量
在main.py开头加载配置,并设置颜色常量:
import json import random import sys import time import argparse import pygame def load_settings(path="settings.json"): with open(path, "r", encoding="utf-8") as f: return json.load(f) cfg = load_settings() # 颜色定义 BG_COLOR = (245, 245, 245) PLAYER_COLOR = (70, 130, 180) GOLD_COLOR = (255, 215, 0) BOMB_COLOR = (200, 50, 50) BLACK = (20, 20, 20) RED = (200, 50, 50)颜色定义虽然不是配置项,但集中写在文件顶部,后面渲染分数、绘制角色时直接用,代码会干净很多。
5.3 玩家类
玩家是游戏里唯一的可控对象,只需要实现移动功能:
class Player(pygame.sprite.Sprite): def __init__(self): super().__init__() self.width = 80 self.height = 20 self.image = pygame.Surface((self.width, self.height)) self.image.fill(PLAYER_COLOR) self.rect = self.image.get_rect() self.rect.midbottom = (cfg["screen_width"] // 2, cfg["screen_height"] - 20) self.speed = cfg["player_speed"] def update(self, keys): left_pressed = keys[pygame.K_LEFT] or keys[pygame.K_a] right_pressed = keys[pygame.K_RIGHT] or keys[pygame.K_d] if left_pressed and self.rect.left > 0: self.rect.x -= self.speed if right_pressed and self.rect.right < cfg["screen_width"]: self.rect.x += self.speed这里继承了pygame.sprite.Sprite,所以玩家可以直接放进精灵组,由精灵组统一负责更新和绘制。移动时加了边界判断,防止角色滑出屏幕。除了方向键,也支持 A 和 D 键,照顾不同操作习惯。
5.4 掉落物类
接下来是金币和炸弹。两种对象结构很相似,差异化在于颜色和碰撞后触发的效果:
class FallingItem(pygame.sprite.Sprite): def __init__(self, kind): super().__init__() self.kind = kind size = 24 self.image = pygame.Surface((size, size), pygame.SRCALPHA) center = size // 2 if kind == "coin": pygame.draw.circle(self.image, GOLD_COLOR, (center, center), center) else: pygame.draw.circle(self.image, BOMB_COLOR, (center, center), center) self.rect = self.image.get_rect() self.rect.x = random.randint(0, cfg["screen_width"] - size) self.rect.y = -size self.speed = random.randint(3, 6) def update(self): self.rect.y += self.speed if self.rect.top > cfg["screen_height"]: self.kill()掉落物在创建时随机出现在窗口顶部的水平位置,然后每帧向下移动。移出屏幕底部时调用kill(),把自身从精灵组里移除,避免对象无限累积。这里用random.randint控制速度,让掉落节奏产生变化,比所有物体匀速下落更有可玩性。
5.5 主循环与碰撞检测
游戏主体是run_game函数。每一帧按照“事件 -> 玩家移动 -> 生成掉落物 -> 更新掉落物 -> 碰撞判定 -> 绘制画面”的顺序执行:
def run_game(max_seconds=0): pygame.init() screen = pygame.display.set_mode((cfg["screen_width"], cfg["screen_height"])) pygame.display.set_caption("CatchGame - 接金币小游戏,躲开炸弹") clock = pygame.time.Clock() start_time = time.time() player = Player() all_sprites = pygame.sprite.Group(player) items = pygame.sprite.Group() score = 0 lives = cfg["start_lives"] difficulty = 1 spawn_timer = 0 running = True frame_count = 0 while running: clock.tick(cfg["fps"]) frame_count += 1 for event in pygame.event.get(): if event.type == pygame.QUIT: running = False if event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE: running = False keys = pygame.key.get_pressed() player.update(keys) if max_seconds > 0 and time.time() - start_time > max_seconds: running = False if spawn_timer <= 0: kind = "coin" if random.random() < cfg["coin_ratio"] else "bomb" item = FallingItem(kind) items.add(item) all_sprites.add(item) spawn_timer = max(cfg["spawn_interval_min"], 40 - difficulty * 2) else: spawn_timer -= 1 items.update() for hit in pygame.sprite.spritecollide(player, items, True): if hit.kind == "coin": score += cfg["coin_value"] else: lives -= 1 if lives <= 0: running = False if score // cfg["difficulty_step_score"] >= difficulty: difficulty += 1 screen.fill(BG_COLOR) all_sprites.draw(screen) font = pygame.font.SysFont("simhei", 24) score_surface = font.render(f"得分: {score}", True, BLACK) lives_surface = font.render(f"生命: {lives}", True, RED) screen.blit(score_surface, (16, 16)) screen.blit(lives_surface, (cfg["screen_width"] - 140, 16)) pygame.display.flip() elapsed = time.time() - start_time avg_fps = frame_count / elapsed if elapsed > 0 else 0 pygame.quit() return score, difficulty, avg_fps这个主循环已经把游戏完整串起来了。spritecollide是 pygame 提供的碰撞检测方法,它检查玩家和掉落物精灵组的碰撞,命中后自动从组里移除碰撞到的对象,返回值就是撞到的精灵列表。这里对每个碰撞对象判断类型:金币加分,炸弹减命。难度提升逻辑也简单,累计得分除以难度阈值,就能知道已经升到了第几级。
5.6 命令行入口与自动测试模式
为了让游戏支持自动化验证,我加了--auto-test参数。传入秒数后,游戏会自动运行指定时间然后退出,不用手动操作:
def main(): parser = argparse.ArgumentParser(description="CatchGame 自动测试模式") parser.add_argument("--auto-test", type=int, default=0, help="自动运行秒数,0 表示手动模式") args = parser.parse_args() score, difficulty, avg_fps = run_game(max_seconds=args.auto_test) print(f"score={score}, difficulty={difficulty}, avg_fps={avg_fps:.1f}") if __name__ == "__main__": main()手动运行时,直接执行python main.py;自动验证时,执行python main.py --auto-test 10,10 秒后程序退出并打印得分、难度和平均帧率。这个机制对后续批量测试很有用,改配置、跑脚本、看结果,全程不需要人工操作。
6. 启动运行与功能测试
6.1 首次启动
确保当前目录有main.py和settings.json,然后运行:
python main.py启动后应该出现一个 800x600 的窗口,背景为浅色,底部有一个蓝色挡板,顶部会不断掉落金色圆形金币和红色炸弹。按左右方向键或 A/D 键控制挡板移动。接到金币,左上角分数增加 10 分;碰到炸弹,右上角生命值减少 1,生命归零时窗口自动关闭。
6.2 功能测试清单
按下面的表格做一轮快速验证:
| 测试项 | 操作 | 预期结果 | 判断标准 |
|---|---|---|---|
| 窗口启动 | 运行python main.py | 窗口正常打开且不掉帧 | 窗口出现,标题正确 |
| 玩家移动 | 按住方向键 | 挡板左右移动且不超出边界 | 移动顺畅,有边界限制 |
| 金币碰撞 | 控制挡板接触金币 | 左上角得分增加 | 每次增加coin_value |
| 炸弹碰撞 | 控制挡板接触炸弹 | 右上角生命减少 | 每次减少 1 点生命 |
| 游戏结束 | 让生命降到 0 | 窗口关闭,程序退出 | 退出无报错 |
| 自动测试 | 运行python main.py --auto-test 5 | 5 秒后自动退出并打印数据 | 打印 score、difficulty、avg_fps |
6.3 自动测试模式验证
执行下面的命令:
python main.py --auto-test 5正常输出格式类似:
score=60, difficulty=1, avg_fps=60.0如果输出正常,说明游戏逻辑和主循环都能稳定跑通。这个模式也可以用来验证不同配置下游戏是否会出现异常,比如把coin_ratio改成 0,持续生成炸弹,运行时是否卡死、是否能正常自动退出。
6.4 批量任务示例
项目本身是桌面游戏,没有网络 API。需要做“批量任务”时,可以通过test_batch.py使用多组配置反复跑自动测试:
import json import subprocess configs = [ {"player_speed": 6, "spawn_interval_min": 25}, {"player_speed": 8, "spawn_interval_min": 20}, {"player_speed": 10, "spawn_interval_min": 15}, ] results = [] for cfg_item in configs: backup = json.load(open("settings.json", "r", encoding="utf-8")) backup.update(cfg_item) with open("settings.json", "w", encoding="utf-8") as f: json.dump(backup, f, ensure_ascii=False, indent=2) ret = subprocess.run( ["python", "main.py", "--auto-test", "5"], capture_output=True, text=True ) results.append({ "config": cfg_item, "returncode": ret.returncode, "stdout": ret.stdout.strip() }) print(results)批量测试时要注意:脚本会直接改写settings.json,所以最好先备份一份默认配置,测试完再恢复。如果一组配置下游戏运行异常,returncode会非 0,脚本能快速暴露问题。这虽然不是传统意义上的“批量任务”,但对调难度、调刷新率这类重复工作非常有用。
7. 资源占用与性能观察
这个游戏不需要 GPU,也没有模型推理任务,资源占用的重点在 CPU、内存和帧率稳定性。
运行时可以在系统的任务管理器或资源监视器里观察进程。通常 pygame 小游戏的内存占用不会高到哪去,但如果你批量生成大量掉落物且没有及时清理,对象会一直堆积,表现为内存缓慢上升。代码里已经对移出屏幕的掉落物调用kill(),正常情况下不会出现持续泄漏。
帧率是一个重要指标。代码用clock.tick(cfg["fps"])把目标帧率锁在 60,超过这个数值的多余计算会被延后,这样可以避免在 144Hz 高刷屏上跑得过于快导致游戏速度失控。自动测试模式里返回了avg_fps,如果在自己电脑上明显低于 60,说明性能存在问题,可以优先检查是否开了太多后台程序、窗口大小是否过大,或者老电脑上 CPU 占用过高。
如果想进一步降低资源占用,有几个方向。一是减小窗口尺寸,从 800x600 降到 640x480;二是减少掉落物生成频率;三是在绘制图片时使用小尺寸图形,避免不必要的缩放。实际数字以本机运行结果为准,不用追求极端优化。
8. 常见问题与排查方法
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| pip 安装 pygame 失败 | 网络问题或 pip 版本过旧 | 查看报错信息,尝试国内镜像 | pip install pygame -i https://pypi.tuna.tsinghua.edu.cn/simple |
import pygame报错 | pygame 未安装到当前 Python 环境 | 运行python -m pip list | 重新安装 pygame,或激活正确的虚拟环境 |
| 窗口一闪而过 | 主循环结束后立即退出 | 检查while running是否执行 | 按本文结构写主循环,避免pygame.quit()在意外位置被调用 |
| 方向键无响应 | 游戏窗口没有获得键盘焦点 | 点击游戏窗口后再按键 | 确认窗口不是后台状态 |
| 中文文字显示方块 | 系统缺少中文字体 | 打印pygame.font.get_fonts() | 换用系统可用字体名,或用英文字体替代 |
| 游戏速度过快 | 未限制帧率 | 检查主循环是否有clock.tick | 加上clock.tick(cfg["fps"]) |
| 自动测试无输出 | --auto-test参数被拼写错误 | 运行python main.py --help | 确认参数名正确 |
| 修改配置后不生效 | 配置文件路径不对或格式错误 | 用python -c "import json; json.load(open('settings.json'))"检查 | 修复 JSON 格式,保证配置文件与main.py同目录 |
这里重点说一个常见问题:如果你把settings.json里的screen_width改成比窗口还小的值,掉落物的生成范围也会自动变小,因为生成随机数使用的是配置里的宽度值。这种改动虽然不会报错,但玩法会明显变化。调整参数时最好一次只改一个字段,跑一次自动测试确认效果。
9. 最佳实践与扩展方向
9.1 工程化建议
第一,配置和代码分离。所有数值参数都放settings.json,不写死在代码里,后续调难度、调玩法都很方便。第二,分类管理文件。建议把音效资源、图片资源、字体文件都放在独立目录下,比如assets/images/、assets/sounds/、assets/fonts/,不要和脚本文件混在一起。第三,第一次调整邻域先做小参数测试。比如把coin_ratio改成 0.5,先用自动测试跑 10 秒观察游戏节奏,再决定是否保留新配置。
9.2 可以继续扩展的方向
这个基础版还有很多可以玩的空间:
- 添加开始界面和结束界面,用键盘或鼠标操作重新开始。
- 加入不同分值的道具,比如金色大金币、加分星星、减速道具。
- 给掉落物增加左右漂移效果,让移动轨迹更有变化。
- 加入本地最高分记录,把分数保存到 JSON 文件。
- 使用
pyinstaller打包成独立可执行文件,方便分发给朋友测试。
打包时需要注意,pygame 应用需要把依赖一起打入,建议先安装pyinstaller并用pyinstaller --onefile main.py打包。第一次打包时注意生成的dist目录下是否有独立可执行文件,运行时是否能找到同目录的settings.json和相关资源。
9.3 合规与分享提醒
替换游戏素材时,不要直接使用来源不明的图片和音频。图标、背景、音效尽量自己绘制或录制,也可以使用明确标注“可商用”的素材库。如果未来在公开平台发布游戏或者参加比赛,特别注意字体授权、音效授权和素材版权。
10. 总结与下一步
这个项目最值得尝试的地方,是它能用很短时间让你看到 Python 程序的完整可视化结果。你不再只是在终端里看到文字,而是屏幕上有一个会移动、会碰撞、会计分的游戏世界。代码拆得足够细,每加一个新功能都有清晰的落点,适合反复重构。
先验证的功能是主循环是否能稳定跑起来。只要窗口打开、挡板能移动、金币和炸弹能正常掉落,整个项目的核心就已经跑通了。最容易踩的坑有三个:配置文件写错导致游戏启动异常、中文显示乱码、不同电脑上帧率不一致导致游戏速度不同。前两个靠检查配置和字体解决,第三个靠clock.tick限制帧率解决。
后续扩展建议从“开始界面”和“最高分记录”入手,这两个功能与现有代码耦合度低,能迅速看到成果。如果想往自动化方向走,可以完善test_batch.py,把更多配置组合加入批量测试,让每次改动都有数据反馈。建议把这篇流程收藏,等你把基础版跑通之后,再回头对照扩展方向做升级。