很多人学 Python,不是卡在语法上,而是卡在“语法都会了,项目不会做”上。变量、循环、函数、列表、字典,单独拿出来都能看懂,可真要打开编辑器写点东西,脑子又变成一片空白。网上教程收藏了一堆,跟着敲了一遍又一遍,合上视频依旧不会独立开发。我自己的转折点,来自一个很小的目标:用 Python 做一款能玩的小游戏。
做小游戏和刷题、看视频最大的区别,是它会强迫你把离散的语言点织成一张完整的程序网。只要一个窗口能响应键盘、能根据逻辑改变画面、能在失败后给出反馈,你就已经走完了一次完整的软件开发流程。这个流程比任何单个知识点都重要,它才是“会编程”和“学过语法”的真正分界线。
这篇文章不是 pygame 文档翻译,也不是游戏策划课,而是一个自学者把“Python 小游戏”从想法变成可运行程序的全过程复盘。我会用躲避方块这个具体例子,带你走一遍环境安装、游戏设计、核心代码、运行验证、错误排查和工程化改进。文章的最后,你不仅会得到一套可以直接运行的代码,还会理解 pygame 背后那些绕不开的基础机制。
先说明白:这篇内容适合刚学完 Python 基础语法、正在寻找第一个综合项目的读者。如果你已经能熟练写出几百行工具脚本,但一直没碰过图形程序,也完全适用。下面正式开始。
1. 为什么学 Python 一定要动手做一次小游戏
自学 Python 最常见的失败路径,不是资料不够,而是缺少一条把知识串起来的实践主线。跟着教程学,每节课都被设计成“刚好能懂”的难度,你很难意识到自己其实没有真正掌握。等到脱离教程,面对一个真实需求时,各种问题才一起暴露出来:不知道程序入口怎么设计,不知道数据在模块之间怎么流动,不知道出了 Bug 该从哪一行开始查。
刷题能解决问题,但刷题解决的是“算法题”,不是“程序题”。一道算法题再难,也不涉及窗口、事件、绘制、状态更新这些真实程序的核心骨架。你会写冒泡排序,不代表你能写一个带界面的工具或游戏。真正缺的,是对“一个完整程序是如何跑起来的”的整体认知。
小游戏恰好补上了这块空缺。它的代码量通常只有一两百行,却强制要求你处理输入、状态、逻辑、绘制、退出这五个环节。你不用先学 Web 框架,也不用理解复杂的架构设计,只需要 Python 基础语法,就能在短时间内获得一个看得见、玩得懂、跑得起来的成果。这种正反馈,是持续学习最重要的燃料。
如果你符合下面任一情况,用一篇小游戏项目作为破局点,效率会非常高:
- 刚学完基础语法,但还没有独立写过 200 行以上的程序。
- 准备转行或找实习,希望有一个能展示的实践项目。
- 想给孩子或朋友做一个小程序,但不打算深入游戏开发领域。
- 日常工作偏自动化脚本,想换个方式加深对 Python 的理解。
小游戏的价值不在于“游戏”两个字,而在于它是体积最小、反馈最直接的完整程序。这段经历沉淀下来的开发感,在你之后学习爬虫、数据处理、Web 开发时都会持续起作用。
2. 技术选型:为什么用 pygame 做 Python 小游戏
提到 Python 写小游戏,绕不开 pygame。它是 Python 生态里最经典、资料最丰富的 2D 游戏开发库。不过 pygames 并不是唯一选择,新手一上来很容易被各种库的名字搅乱。
| 技术方案 | 特点 | 适合场景 | 学习曲线 |
|---|---|---|---|
| pygame | 经典、资料多、概念贴近底层游戏循环 | 想理解游戏原理的自学者 | 中等 |
| Pygame Zero | 封装更简单,面向教育和少儿编程 | 快速做原型,不想接触太多细节 | 低 |
| Arcade | 现代、面向教育,文档清晰 | 教学和简单 2D 游戏 | 中等偏低 |
| Pyglet | 偏底层,面向 OpenGL | 需要窗口和多媒体控制的高级开发 | 偏高 |
| Tkinter | Python 自带 GUI 库 | 桌面工具、表格界面,不适合实时游戏 | 中等 |
对绝大多数初学者来说,pygame 是资料覆盖、社区体量和学习价值三者兼顾的选择。它没有帮你把细节全部隐藏起来,而是把事件循环、矩形碰撞、屏幕刷新这些概念直接放在代码里。这意味着你写完一个游戏,学到的不是“某个框架的快捷键”,而是图形程序通用知识。
有一点需要提前说清楚:pygame 是库,不是游戏引擎。游戏引擎往往集成了渲染、物理、资源管理、场景编辑等大量功能;pygame 只提供基础模块,窗口、碰撞规则、计分逻辑都需要你用代码自己拼。这个定位看似麻烦,其实是好事,因为你被迫理解了游戏最核心的骨架,以后即使切换到 Unity、Godot 或其他工具,也不会被虚浮的“可视化操作”掩盖底层原理。
3. Python 环境搭建与 pygame 安装
不管文章里的代码多简单,第一步永远是环境。很多初学者在安装环节就放弃了,再好的教程也救不回来。这里把环境操作拆成四步,照着做即可。
3.1 确认 Python 版本
打开终端(Windows 用 CMD 或 PowerShell,macOS/Linux 用自带终端),输入:
python --version如果看到类似Python 3.10.x的输出,说明 Python 已安装。如果提示找不到命令,在 Windows 上可以试试:
py --version原因很简单:Windows 某些安装方式下,系统命令名可能是py而不是python。如果两个命令都不存在,需要先到 Python 官方网站下载安装包,安装时务必勾选“Add Python to PATH”,否则后面命令行会一直报“不是内部或外部命令”。
3.2 创建虚拟环境
强烈建议每个 Python 项目使用独立的虚拟环境,避免不同项目依赖包冲突。在项目目录下执行:
python -m venv venv然后激活环境。
Windows:
venv\Scripts\activatemacOS / Linux:
source venv/bin/activate激活后,命令行前面会出现(venv)标识,表示当前环境已经切换到项目虚拟环境。这一步很重要,否则你后面安装的 pygame 很可能装到了全局 Python,换一台电脑或换个项目时留下一堆隐患。
3.3 安装 pygame 并验证
环境激活状态下,安装 pygame:
pip install pygame如果网速较慢,可以临时使用国内 PyPI 镜像源:
pip install pygame -i https://pypi.tuna.tsinghua.edu.cn/simple安装完成后验证是否成功:
python -c "import pygame; print(pygame.version.ver)"看到版本号输出,说明安装成功。如果报ModuleNotFoundError,先确认当前终端是否真的激活了 venv,再执行一次pip list查看包里有没有 pygame。很多人在 VSCode 里明明装好了 pygame,运行时仍然报找不到模块,就是因为 VSCode 选了解释器,但选成了全局 Python。
3.4 选择开发工具
自学者最常用的是 VSCode + Python 插件,或者 PyCharm Community Edition。VSCode 的配置要点是:安装 Python 扩展后,按Ctrl+Shift+P,输入Python: Select Interpreter,选择刚才创建的 venv 解释器。这一点不做,后面写代码时很容易出现“终端能运行、编辑器里不能运行”的诡异现象。
如果你不愿意折腾编辑器,直接用 Python 自带的 IDLE 也能完成本文所有练习。工具只是载体,真正重要的是把环境跑通。
4. 写代码前,先把游戏设计定下来
新手最常见的错误是打开编辑器就直接写。写了一会儿发现逻辑越来越乱,最后干脆删掉重来。正确顺序是先用自然语言把规则说清楚,再把它翻译成代码。程序本身就是规则的描述,规则越清晰,代码写起来越顺。
本文实现的游戏叫“躲避方块”。它的玩法设计如下:
- 玩家是一个蓝色方块,初始位置在窗口底部中央。
- 玩家通过键盘左右方向键移动,不能移出窗口边界。
- 红色障碍块从屏幕顶部随机位置生成,向下匀速移动。
- 红色方块碰到玩家,游戏结束,显示最终得分。
- 每个红色方块成功落出屏幕底部,玩家得到 1 分。
游戏只有两种状态:运行中和结束展示。正常运行期间持续循环:处理输入、更新玩家位置、生成和移动障碍、检测碰撞、绘制画面。一旦碰撞发生,退出主循环,进入结束画面,两秒后自动关闭窗口。
在代码里,这些参数会以常量形式集中定义。先看下表,理解每个值的作用:
| 参数 | 值 | 说明 |
|---|---|---|
| 窗口宽度 | 600 | 游戏区域宽度,单位像素 |
| 窗口高度 | 800 | 游戏区域高度,单位像素 |
| 帧率 | 60 | 每秒刷新次数 |
| 玩家尺寸 | 80x80 | 玩家方块宽和高 |
| 玩家速度 | 6 | 每帧移动像素数 |
| 障碍尺寸 | 60x60 | 障碍块宽和高 |
| 障碍速度 | 5 | 每帧下落像素数 |
| 生成间隔 | 40 帧 | 约每 0.67 秒生成一个新障碍 |
参数值可以随喜欢调整,但不要把魔法数字散落在代码里。把它们集中放在顶部,后续调难度时只需要改一个位置,这也是工程化意识的起点。
5. pygame 的四个核心概念,理解后再写代码
看到 pygame 里的event、blit、Rect、colliderect这些词,新手很容易发怵。它们其实对应着四个最基本的问题:怎么响应输入、怎么刷新画面、怎么表示物体、怎么判断碰撞。把这四个问题想清楚,代码就顺理成章了。
5.1 事件循环
窗口程序不能只运行一次就结束,它必须一直监听用户操作,并根据操作更新画面。这个“一直监听”的过程就是事件循环。pygame 的标准结构是:
while running: for event in pygame.event.get(): if event.type == pygame.QUIT: running = Falsepygame.event.get()会取出一批事件,比如窗口关闭、按键按下、鼠标移动。循环体内处理事件。没有事件循环的图形程序,最容易出现的现象是窗口闪退,因为代码从头到尾执行完,进程就结束了。
5.2 Surface 与屏幕刷新
Surface 可以理解成一张画布。screen = pygame.display.set_mode((600, 800))创建的是窗口主画布。所有绘制并不是画一个图形就立刻显示在屏幕上,而是先画到画布上,再统一提交。因此绘制顺序通常是:
- 清屏:
screen.fill(WHITE),把上一帧内容覆盖掉。 - 绘制:调用
pygame.draw.rect画玩家和障碍物。 - 提交:
pygame.display.flip(),把当前画布真正显示到窗口。
不管后面调用多少次pygame.draw.rect,漏掉flip()都等于白画。这是新手最容易忽略的一行代码。
5.3 Rect 与碰撞检测
pygame 中几乎每个图形物体都可以用一个矩形区域表示。Rect 对象包含x、y、width、height四个关键属性,对应位置和尺寸。玩家和障碍物直接用pygame.Rect管理,极大简化了碰撞判断:
player_rect = pygame.Rect(player_x, player_y, player_width, player_height) if player_rect.colliderect(block_rect): # 发生了碰撞这里的碰撞检测采用了最简单的矩形相交判断。后续做更复杂的游戏时,Rect 仍然是最常用的“碰撞箱”,已经足够应付绝大多数 2D 场景。
5.4 Sprite 与 Group
pygame 还提供了一套面向对象的封装:Sprite 是游戏对象基类,Group 用于统一管理多个 Sprite 的更新和绘制。在本文的最小版本里,我用最直接的方式写,先让逻辑清晰可读。把代码改造成 Sprite 结构,放在文章后面的工程化章节讲解。
换句话说,第一版不需要过度设计。先用最直白的代码把流程跑通,理解游戏循环,再谈架构优化。
6. 完整代码:一个可以直接运行的躲避小游戏
下面这段代码就是完整游戏。在项目目录新建一个文件,命名为dodge_game.py,把代码完整复制进去。
# 文件路径:dodge_game.py import pygame import random import sys pygame.init() # 基础颜色 RGB WHITE = (255, 255, 255) BLACK = (0, 0, 0) RED = (255, 80, 80) BLUE = (80, 150, 255) # 窗口与帧率 SCREEN_WIDTH = 600 SCREEN_HEIGHT = 800 FPS = 60 # 玩家相关参数 PLAYER_WIDTH = 80 PLAYER_HEIGHT = 80 PLAYER_SPEED = 6 # 障碍物相关参数 BLOCK_WIDTH = 60 BLOCK_HEIGHT = 60 BLOCK_SPEED = 5 SPAWN_INTERVAL = 40 # 每隔多少帧生成一个障碍物 screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) pygame.display.set_caption("躲避方块 - Python小游戏") clock = pygame.time.Clock() # 玩家初始位置:水平居中,底部留 30 像素 player_x = (SCREEN_WIDTH - PLAYER_WIDTH) // 2 player_y = SCREEN_HEIGHT - PLAYER_HEIGHT - 30 blocks = [] score = 0 spawn_timer = 0 font = pygame.font.Font(None, 48) running = True while running: clock.tick(FPS) # 处理关闭事件 for event in pygame.event.get(): if event.type == pygame.QUIT: running = False # 读取键盘状态并移动玩家 keys = pygame.key.get_pressed() if keys[pygame.K_LEFT]: player_x -= PLAYER_SPEED if keys[pygame.K_RIGHT]: player_x += PLAYER_SPEED # 限制玩家不出窗口边界 if player_x < 0: player_x = 0 if player_x > SCREEN_WIDTH - PLAYER_WIDTH: player_x = SCREEN_WIDTH - PLAYER_WIDTH # 按间隔生成障碍物 spawn_timer += 1 if spawn_timer >= SPAWN_INTERVAL: spawn_timer = 0 block_x = random.randint(0, SCREEN_WIDTH - BLOCK_WIDTH) block_y = -BLOCK_HEIGHT blocks.append(pygame.Rect(block_x, block_y, BLOCK_WIDTH, BLOCK_HEIGHT)) # 移动障碍物,落出屏幕后移除并加分 for block in blocks[:]: block.y += BLOCK_SPEED if block.y > SCREEN_HEIGHT: blocks.remove(block) score += 1 # 玩家矩形 player = pygame.Rect(player_x, player_y, PLAYER_WIDTH, PLAYER_HEIGHT) # 碰撞检测 for block in blocks: if player.colliderect(block): running = False # 绘制画面 screen.fill(WHITE) pygame.draw.rect(screen, BLUE, player) for block in blocks: pygame.draw.rect(screen, RED, block) score_text = font.render(f"Score: {score}", True, BLACK) screen.blit(score_text, (10, 10)) pygame.display.flip() # 游戏结束后显示分数 screen.fill(WHITE) game_over_text = font.render("Game Over", True, RED) final_score_text = font.render(f"Score: {score}", True, BLACK) screen.blit( game_over_text, ((SCREEN_WIDTH - game_over_text.get_width()) // 2, SCREEN_HEIGHT // 2 - 60), ) screen.blit( final_score_text, ((SCREEN_WIDTH - final_score_text.get_width()) // 2, SCREEN_HEIGHT // 2 + 10), ) pygame.display.flip() pygame.time.wait(2000) pygame.quit() sys.exit()这段代码不长,但已经把游戏开发的核心骨架全部包含进去了。逐段拆开看会更清楚。
第一部分是导入、初始化、常量定义。把窗口尺寸、玩家速度、障碍速度这些参数全部放在顶部,后面修改难度时不需要在逻辑代码里找数字。pygame.Rect用位置和尺寸创建矩形对象,玩家、障碍物都通过它来描述。
第二部分是键盘控制。keys = pygame.key.get_pressed()会返回当前所有按键的状态,适合左右移动这种需要“按住不放持续响应”的操作。相比之下,pygame.KEYDOWN更适合单次触发,比如按一次空格跳一下。这里区分清楚,以后写跳跃、射击逻辑就不会混。
第三部分是障碍物生命周期。spawn_timer是生成计时器,每帧加一,达到SPAWN_INTERVAL就生成一个新障碍并重置计时。生成位置用random.randint随机确定。障碍物向下移动后,如果落出屏幕底部,就从列表移除并加 1 分。for block in blocks[:]里的[:]是复制列表,目的是在循环过程中安全地删除元素,否则会出现“一边遍历一边删除”的坑。
第四部分是碰撞与绘制。玩家矩形和每个障碍矩形调用colliderect判断相交,一旦相交就退出主循环。绘制顺序是:先fill清屏,再画玩家和障碍,最后用font.render和screen.blit把分数文本贴到左上角,最后flip()刷新窗口。结束画面用了文本对象的get_width()计算居中位置,这种写法比硬编码坐标更稳妥。
7. 运行结果与验证方法
代码保存后,在激活了虚拟环境的终端里运行:
python dodge_game.py正常情况下会弹出一个标题为“躲避方块 - Python小游戏”的窗口。窗口宽 600、高 800,底部有一个蓝色方块,红色方块从顶部不断下落。按键左右方向键,蓝色方块会跟着移动。一旦碰撞,游戏会暂停并显示Game Over和最终得分,两秒后自动关闭。
判断主流程是否跑通,可以从几个方面确认:
- 窗口稳定存在,不会一闪而过。
- 按键响应跟手,画面以 60 FPS 刷新,没有明显卡顿。
- 红块下落速度稳定,不会瞬间消失或停在原地。
- 蓝块碰到红块后确实结束游戏。
- 每有一个红块落出底部,分数增加 1,显示正确。
如果某个环节不对,第一时间看终端输出的 Traceback。文本界面的错误提示比画面本身更关键。比如报ModuleNotFoundError: No module named 'pygame',这不是代码问题,是环境问题,回到第 3 节检查激活环境;报IndentationError,说明缩进不对,Python 用缩进表示代码块,循环和函数内部的代码层级必须一致。
调 BUG 阶段可以临时加打印语句,把关键变量的变化过程打出来:
print(f"player_x={player_x}, blocks={len(blocks)}, score={score}")把这段放在主循环里跑一遍,你会清楚看到数据在每个帧里如何变化。等逻辑确认没问题,再删掉这些打印语句。对新手来说,print调试比断点调试更容易理解,因为代码执行顺序是看得见的。
8. 常见问题与排查思路
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
启动报ModuleNotFoundError: No module named 'pygame' | pygame 未安装,或当前终端没有激活 venv | 运行pip list查看是否有 pygame | 激活 venv 后执行pip install pygame |
Windows 提示python不是内部或外部命令 | 安装时未勾选 Add Python to PATH | 运行py --version验证 | 使用py命令,或重装 Python 并勾选 PATH |
| 窗口一闪而过 | 主循环未正确执行或缩进错误 | 在 IDE 中运行查看 Traceback | 检查while running:是否包含完整逻辑,缩进必须一致 |
| 按键没反应 | 键盘事件代码写到了主循环外,或窗口未聚焦 | 确认代码层级,点击窗口后再操作 | 把pygame.key.get_pressed()放在主循环内部 |
| 画面全白且静止 | 忘了调用pygame.display.flip() | 检查主循环末尾 | 每次绘制完成后调用pygame.display.flip() |
| 游戏运行时间越长越卡 | 障碍物出界后没有移除,列表无限增长 | 打印len(blocks)观察 | 使用blocks[:]遍历并在落出底部时remove |
| 结束画面没有显示 | running被置为 False 后直接退出,没有绘制结束界面 | 检查主循环后面的代码是否存在 | 主循环结束后,先绘制结束画面,再调用pygame.quit() |
这些坑覆盖了环境、输入、绘制、内存四个方向。遇到问题时,最忌讳的是“全盘重写”。正确做法是:先读终端错误,确认是环境还是代码;如果是代码,用print定位变量异常点;最后只修改有问题的局部,保持其他部分不动。
9. 从“能跑”到“好维护”的工程实践
能运行只是一个起点。写过一次完整流程后,下一步要思考的是:代码怎么组织,才能让后续扩展更容易。这里分享几条对自学者最有实用价值的工程经验。
9.1 配置与常量管理
版本一里所有参数都是常量,这已经比散落魔法数字好很多。更进一步,可以把配置内容抽到独立 JSON 文件里,代码和配置分离。示例配置:
{ "screen_width": 600, "screen_height": 800, "fps": 60, "player_width": 80, "player_height": 80, "player_speed": 6, "block_width": 60, "block_height": 60, "block_speed": 5, "spawn_interval": 40 }这样改难度时不需要动代码,只改配置。甚至可以让玩家在菜单里自定义难度,再把配置写回 JSON。
9.2 模块拆分与类的封装
当代码超过 200 行时,单一文件会让定位问题越来越慢。可以把项目拆成多个文件,每个文件只负责一件事。
project/ config.py main.py player.py obstacle.py从面向对象角度封装Player类,是自然过度。下面展示一个最小化的Player类:
# 文件路径:player.py import pygame class Player: def __init__(self, x, y, width, height, speed): self.rect = pygame.Rect(x, y, width, height) self.speed = speed def update(self, keys, screen_width): if keys[pygame.K_LEFT] and self.rect.left > 0: self.rect.x -= self.speed if keys[pygame.K_RIGHT] and self.rect.right < screen_width: self.rect.x += self.speed def draw(self, surface, color): pygame.draw.rect(surface, color, self.rect)__init__负责初始化属性,update负责更新逻辑,draw负责绘制。主循环不需要关心具体移动规则,只需要调用这两个方法。这种将数据和操作绑定在一起的方式,就是面向对象编程的基本样子。障碍物、子弹、道具也都可以按同样模式写。
9.3 调试、异常处理与素材管理
给代码加显眼的调试开关,是成本最低的调试方式:
DEBUG = True if DEBUG: print(f"player={player}, block_count={len(blocks)}, score={score}")发布前把DEBUG改成False,调试代码保留在文件里也不会影响运行。涉及加载图片、音效等外部资源时,建议用try-except给出友好提示,而不是让用户看到一长串 Traceback。制作素材时注意版权,不要随意使用来源不明的图片和音乐。
9.4 版本控制与发布
从现在开始养成习惯:代码可用后,立刻执行git init把项目纳入版本管理,写一个简短的 README,记录项目说明和运行方法。这样后续改坏了代码,可以随时回退。准备把游戏分享给非技术朋友玩时,再学习pyinstaller打包成可执行文件。这些都是小游戏项目之外的工程能力,但它们才是“自学者”和“开发者”之间真正的距离。
10. 下一步扩展方向与学习建议
躲避方块已经跑通,接下来可以按熟悉的程度逐个加功能。比较推荐的方向有:
- 做计分机制:分数达到某个阈值后,提高障碍速度或缩短生成间隔,让难度动态上升。
- 加碰撞音效:用
pygame.mixer在碰撞时播放音效,在后台播放背景音乐。 - 做游戏菜单:加入开始界面、暂停功能、结束界面,用状态机管理不同界面。
- 做最高分记录:用 JSON 或 SQLite 保存历史最高分,下次启动时读取。
- 改玩法:把“躲避”改成“接苹果”,障碍改成需要主动收集的奖励。
每次只加一个功能,跑通后再加下一个。一次堆太多改动,往往会出现“哪里坏了都不知道”的尴尬情况。这种小步迭代的习惯,同样适用于真实项目。
后续学习可以往两个方向走。一个是把当前项目重构一遍,用类、模块、配置分离的方式重写,体会代码组织的差异;另一个是读一个足够小的开源 pygame 项目,看别人如何组织代码,遇到感兴趣的设计就动手改掉。想再往上走,可以了解游戏状态机、简单粒子效果、地图编辑工具,甚至试试 Unity 等更完整的引擎。
回到标题那句话:“自学 Python,我做出了这样一款小游戏”。游戏本身很简单,真正有价值的,是你完整走过了规则设计、代码实现、运行调试、重构优化的全过程。以后遇到爬虫、数据处理脚本或者更复杂的项目,你会有底气把目标拆小,先跑通,再迭代。这就是做小游戏能带给自学者最实在的东西。