
如果你问我Python 学了点基础之后最想做的是什么我猜十个人里有八个会说做个游戏。原因很简单平时写脚本、抓数据、跑爬虫结果都是冷冰冰的文字而游戏不一样窗口一开画面会动键盘一按方块会跑这种“我能控制屏幕上的东西”的即时反馈是其他练习给不了的。Pygame 就是帮你把这种冲动最快变现的库。它不是什么高不可攀的 3D 引擎而是一套用 Python 写 2D 游戏的工具集建窗口、画图形、放图片、播声音、读键盘鼠标全都有现成函数。你不需要先啃五万字文档装好库二十行代码就能把第一个窗口跑起来。今天我就带你从头走一遍用 Pygame 做一款真正能玩的“接苹果”小游戏顺便把窗口、循环、事件、碰撞这些概念一次性说透。这篇文章适合刚学完 Python 基础、对面向对象一知半解也没关系的人看也适合那些一直想写游戏但被众多引擎吓退的朋友。我尽量不说废话全程用代码和经验说话。1. 为什么用 Pygame 做第一个游戏1.1 为什么挑 Pygame而不是 Unity 或 Godot我经常被人问做游戏为什么不直接学 Unity这个问题要分情况。Unity、Godot 这类引擎确实强大但它们解决的问题比“入门”要多得多场景、资源管理、组件系统、API 变化、编辑器操作全都叠在一起对刚学会 if 和 for 的人来说根本分不清哪些是编程、哪些是工具操作。Pygame 的思路完全不同。它只做一件事把 SDL 这套底层图形音频库包成 Python 函数。你写的就是纯 Python 代码游戏循环手动控制画布手动绘制碰撞手动判断。听起来“原始”但这恰恰是它的价值——你被迫理解游戏到底是怎么跑起来的而不是把一个角色拖进场景里就完了。还有一个现实原因生态稳定。Pygame 从 2000 年发布到现在接口一直很稳定网上资料多官方文档清晰遇到问题搜一下基本都有答案。相比之下很多新框架看着热闹实际踩坑时连样例都找不到。我并不是说另外几个库不行只是对“第一个游戏”这个目标来说Pygame 是最不容易让你卡死在半路的。另外要提一句Pygame 只是一个库不需要安装什么“制作工具”或“编辑器”你手上只要有 Python 和一个文本编辑器就够了。开发环境越简单你真正花在学习上的时间就越多。1.2 第一个游戏选题为什么是“接苹果”初学者做项目最容易犯的毛病是想法太大。今天想做个《我的世界》复刻版明天想做网游结果写了一周连角色都还没动起来热情先耗光了。我的建议非常明确第一个游戏规则绝对不能超过两句话。“接苹果”这种玩法就是典型的好选择。它有几大优势玩法直观屏幕顶上掉苹果你用挡板接住接到加分漏掉结束。涉及面全它有游戏循环、键盘输入、物体移动、碰撞检测、分数记录、难度递增麻雀虽小五脏俱全。素材简单不需要找美术资源矩形和圆形就能画出来。扩展空间大做完之后往上加生命值、加音效、加多种掉落物都是水到渠成的事。很多教程上来就教你写“俄罗斯方块”或“贪吃蛇”我也写过。但接苹果比贪吃蛇更简单的地方在于它只有一个主动运动的物体挡板另一个物体只是沿固定方向下落你不需要维护一长串蛇身的数据结构。对于“跑通第一个循环”这个目标来说少一个概念就少一分劝退的风险。2. 环境搭建5 分钟把 Pygame 跑起来2.1 安装 Pygame 的几种方式先说结论大多数人直接用 pip 安装就行。打开命令行Windows 用 cmd 或 PowerShellmacOS/Linux 用终端敲一行pip install pygame这一步会从 PyPI 下载最新的 pygame 版本并自动装好。装完后验证一下python -m pygame.examples.aliens这条命令会直接启动一个内置的外星人小游戏示例。如果你能看到窗口正常弹出并且能用键盘操控飞船说明环境没问题可以放心往下走。如果你在官网看到“pygame 官方下载”之类的入口想从官网拿安装包也可以直接访问 pygame.org 的下载页面挑一个和你 Python 版本对应的 wheel 文件下载然后用 pip 安装本地文件pip install pygame-2.6.1-cp312-cp312-win_amd64.whl不过说句实在话对绝大多数人来说官网下载比 pip 多好几步没太大必要。除非你的网络环境下载 PyPI 特别慢否则认准 pip 就好。至于具体装哪个版本默认装最新的就行不需要刻意选版本。还有一个我强烈建议的习惯新建一个项目目录并在里面创建虚拟环境再安装。这样不同项目的依赖不会互相打架。Windows 下操作是这样mkdir catch_apple cd catch_apple python -m venv venv venv\Scripts\activate pip install pygame虚拟环境不是必选项但如果你后面还想玩 Django、写爬虫、跑数据分析时间长了就知道这步有多省心了。至少我当年没有这个习惯后来被一堆“明明昨天还能跑今天就爆错”的问题搞到头大。2.2 初始化窗口让第一块屏幕亮起来安装完成之后写代码就很直白了。先建立一张几乎每个 Pygame 程序都会出现的“床位代码”import pygame pygame.init() screen pygame.display.set_mode((800, 600)) pygame.display.set_caption(我的第一个游戏)这几行里最关键的是pygame.init()。很多人搞不清它到底做了什么我打个比方Pygame 内部有显示、声音、字体、手柄等多个功能模块pygame.init()就是挨个把能初始化的一次性全部初始化好省得你手动调用每个模块的 init。实战中你极少需要逐个初始化所以这一行直接写上别省。接着是pygame.display.set_mode((800, 600))。你传进去的元组就是窗口的宽高单位是像素它返回给我们的screen相当于一块画布之后所有绘制操作都是在这块画布上进行的。对于“第一个小游戏”窗口大小建议固定成 800×600简单清晰。但注意跑到这里只执行一遍的话窗口闪一下就没了。原因是程序执行完最后一行就退出了窗口自然关闭。你需要在set_mode后面加一个“等窗口关闭”的循环这也正是下一节的重点。3. 游戏循环与事件系统游戏的心脏3.1 主循环的三段式读输入、更新逻辑、重新绘制所有游戏不管多复杂内核都是一个不断重复的主循环。这个循环每转一圈就相当于“刷了一帧”。你可以把它理解成动画片一帧一帧的画面快速切换在人眼看来就是连续运动。Pygame 里最朴素的主循环长这样running True while running: # 1. 处理事件键盘、鼠标、关窗口 # 2. 更新游戏状态计算位置、判断碰撞 # 3. 绘制画面把状态画到 screen 上 # 4. 控制帧率这个顺序是有讲究的先读用户输入再根据输入更新状态最后把状态画出来。如果顺序反了画面会慢一帧玩起来会发现“我明明按了键角色顿了一下才动”。别小看这一帧的延迟手感就是这么一点点被败坏的。在循环末尾你需要加两行代码很多新手漏了它们导致各种诡异问题pygame.display.flip() clock.tick(60)pygame.display.flip()的作用是“把画布内容真正显示到屏幕上”。因为 Pygame 默认是先画在后台缓冲里画完再整体刷出来这样避免屏幕闪烁。如果漏了这行你画了东西也看不见。同理还有一个pygame.display.update()它在不传参数时和 flip 效果类似但支持只刷新某块区域做第一个游戏记住用 flip 就够了。clock.tick(60)则是锁帧率。参数 60 表示让循环每秒最多执行 60 次。如果没有这行循环会以 CPU 愿意跑多快就跑多快游戏速度会失控而且 CPU 占用率会直冲 100%。加上这行之后你的游戏移动速度才有一个稳定的时间基准。这个细节比你想的重要得多。3.2 事件队列程序怎么知道你想关窗口在循环里第一步是读事件。Py求game 会把所有发生的事按键、鼠标移动、窗口点击关闭等放进一个事件队列你用pygame.event.get()把它们取出来。比如判断用户是否点击了窗口右上角的“X”running True while running: for event in pygame.event.get(): if event.type pygame.QUIT: running False这段代码是每个 Pygame 程序的“底线”。只要你不想让程序卡死到只能强制结束就必须在主循环里不断调用pygame.event.get()并且处理退出事件。否则窗口会像没响应一样点关闭都没反应——不是程序坏了而是你根本没给它机会处理“关闭”这个请求。事件处理还分两种需求我在这里多讲一句因为后面做移动时会用到如果你要响应“按一下”这种瞬间动作比如按空格跳跃一次用事件类型pygame.KEYDOWN。如果你要响应“一直按住”这种持续动作比如按住左箭头持续向左走用pygame.key.get_pressed()它返回一个包含所有按键状态的序列每帧读取都是当前最新的。接苹果游戏里挡板移动就是典型的持续动作所以我用key.get_pressed()而不是监听按键事件。这两种用法的区分是很多教程没说透的也是新手写游戏时最容易“手感不对”的原因。4. 画面上的一切坐标、颜色与碰撞4.1 Pygame 坐标系统y 轴是朝下的说到绘制Pygame 的坐标系统和数学课本上不一样。它的原点(0, 0)在窗口左上角x 轴向右增大y 轴向下增大。也就是说越往下y 值越大。这个反直觉的设定至少会坑到一半新手。比如你想把一个物体放在“窗口底部”它的 y 值就应该接近窗口高度600想让它往下掉就要让它的 y 值不断增加。我早期写下落效果时总是不自觉把 y 递减结果物体反而往上飞当时我以为是自己逻辑错了后来才发现是坐标系习惯问题。绘制基本图形也很简单。矩形pygame.draw.rect(screen, (0, 0, 0), (player_x, player_y, 100, 20))第一个参数是画布第二个参数是颜色RGB 三元组第三个参数是矩形的位置和大小。圆形pygame.draw.circle(screen, (200, 30, 30), (apple_x, apple_y), 10)与矩形不同圆形的定位是基于圆心的。后面的10是半径所以它的“底部边缘”在 y 坐标apple_y 10处。记住这一点等你做碰撞检测的时候就明白了——你其实是在拿圆心位置加半径做判断而不是直接用矩形左上角坐标。4.2 挡板移动与碰撞检测动手环节接苹果游戏里挡板由玩家控制苹果自动下落。挡板移动的写法如下keys pygame.key.get_pressed() if keys[pygame.K_LEFT]: player_x - player_speed if keys[pygame.K_RIGHT]: player_x player_speed不过直接这么写会有一个小毛病挡板可以一路挪出窗口边缘再也回不来了。所以要加边界限制if keys[pygame.K_LEFT] and player_x 0: player_x - player_speed if keys[pygame.K_RIGHT] and player_x 800 - 100: player_x player_speed800 - 100是窗口宽度减去挡板自身宽度意思是挡板右边缘最多贴到窗口右边不会越界。这里把 100 写死并不优雅但第一个游戏能跑明白比优雅重要。后面我会讲怎么用pygame.Rect把它整理干净。苹果下落就一行apple_y apple_speed每次循环执行一次苹果的 y 坐标越来越大看起来就是往下掉。掉到某个位置时我们要判断它有没有被挡板接住if apple_y apple_size player_y and player_x apple_x player_x player_width: score 1 apple_x random.randint(0, 800 - apple_size) apple_y 0这个条件拆开看很简单apple_y apple_size是苹果底部的 y 坐标它要撞到挡板的顶边player_y同时苹果圆心的 x 坐标必须在挡板左边界和右边界之间。两个条件同时满足就认为接住了。这是最朴素的碰撞判断属于“够用就好”的级别。游戏实战里碰撞检测通常更复杂但接苹果这种圆形和矩形的碰撞用这种办法完全没问题。如果你想把碰撞判断写得干净一点可以用 Pygame 内置的 Rect 对象它自带碰撞检测方法player_rect pygame.Rect(player_x, player_y, 100, 20) apple_rect pygame.Rect(apple_x - 10, apple_y - 10, 20, 20) if apple_rect.colliderect(player_rect): score 1Rect会帮你管理左上角坐标和宽高colliderect专门判断两个矩形是否相交。它的好处是逻辑明确、不容易写错边界匹配。不过我建议你先亲手写一遍纯坐标版本理解原理之后再换成 Rect这样即使哪天不用 Pygame底层思路也跑不掉。5. 完整实战用 Pygame 做一款“接苹果”小游戏5.1 游戏规则与完整代码到这一步我直接把整个游戏代码贴出来。你可以新建一个catch_apple.py把下面代码复制进去直接运行。规则很简单方向键左右控制挡板接住苹果加 1 分苹果落底游戏结束。import pygame import random # 初始化 pygame.init() screen pygame.display.set_mode((800, 600)) pygame.display.set_caption(接苹果小游戏) clock pygame.time.Clock() # 颜色常量 WHITE (255, 255, 255) BLACK (0, 0, 0) RED (200, 30, 30) # 挡板参数 player_width 100 player_height 20 player_x (800 - player_width) // 2 player_y 560 player_speed 8 # 苹果参数 apple_size 10 apple_x random.randint(0, 800 - apple_size) apple_y 0 apple_speed 6 # 分数与字体 score 0 font pygame.font.SysFont(simhei, 24) running True while running: for event in pygame.event.get(): if event.type pygame.QUIT: running False keys pygame.key.get_pressed() if keys[pygame.K_LEFT] and player_x 0: player_x - player_speed if keys[pygame.K_RIGHT] and player_x 800 - player_width: player_x player_speed # 苹果下落 apple_y apple_speed # 碰撞检测苹果底部碰到挡板顶部且圆心横坐标在挡板范围内 if apple_y apple_size player_y and player_x apple_x player_x player_width: score 1 apple_x random.randint(0, 800 - apple_size) apple_y 0 apple_speed 0.2 # 苹果落到底部游戏结束 if apple_y 600: print(游戏结束得分, score) running False # 绘制 screen.fill(WHITE) pygame.draw.rect(screen, BLACK, (player_x, player_y, player_width, player_height)) pygame.draw.circle(screen, RED, (apple_x, apple_y), apple_size) screen.blit(font.render(f得分{score}, True, BLACK), (10, 10)) pygame.display.flip() clock.tick(60) pygame.quit()这个版本只有六十来行但已经完整跑通了“初始化、循环、输入、物理下落、碰撞、计分、退出”的全流程。对一个从零开始的人来说能亲手跑通这个流程价值比看十篇理论文章都大。5.2 逐段拆解每个变量为什么这么设我重点解释几个容易被一带而过的“玄机”。第一个是挡板的初始坐标。player_x (800 - player_width) // 2意思是把 100 宽的挡板放在 800 宽窗口的正中间。为什么不直接写350因为一旦你调整窗口宽度这里就要跟着改。用公式算出来以后改窗口尺寸这行代码不用动。同理苹果重新出现的random.randint(0, 800 - apple_size)保证苹果圆心不会超出窗口右边界。你可能会问半径只有 10为什么不用800 - 10因为这里random.randint生成的是圆心坐标苹果最右端是apple_x 10所以上限给到800 - apple_size才能保证整个圆形不会画出屏幕。这种“把对象边缘而非中心作为边界”的思维是做游戏时很多人犯迷糊的地方。第二个是apple_speed 0.2。这一行很不起眼但它让游戏有了“难度递增”的感觉。每一分苹果下落的速率都加快一点玩家会从轻松变成紧张。如果没有这行玩久了就是机械操作没有压迫感。0.2 这个数值是我试出来的加太快会让人觉得苹果是崩出去的加太慢又感觉不到变化。你可以在自己的版本里改成 0.1 或 0.3 体验一下体会数字对游戏手感的影响。第三个是绘制顺序。大家注意到我是先screen.fill(WHITE)清屏再画挡板、画苹果、画文字最后flip()。顺序颠倒不会崩溃但会出现遮挡问题。Pygame 没有自动处理图层谁后画谁就盖在谁上面。所以fill必须永远是第一步它相当于把上一帧的残留全部擦掉重新铺一张白纸。漏掉它你会看到物体拖出长长的残影。5.3 让游戏更像样生命值、多个苹果、图片素材当前这个版本是最小可玩版我称之为 1.0。运行完之后我建议你按下面几个方向去迭代每个改动都能学到新东西。第一个是加生命值。苹果落底本来应该扣除 1 条命而不是直接游戏结束。改成生命值后问题就变成了“如何控制游戏结束的条件”。你需要一个lives变量每次苹果落底减 1减到 0 才退出循环。这看起来是小事但它逼迫你区分“当前这一局的状态”和“整个游戏的元状态”对理解游戏状态机的概念很有帮助。第二个是生成多个苹果。现在屏幕上只有一个苹果玩起来略显单调。多苹果版本维护的是“苹果列表”每次下落结束后随机生成新的苹果对象存储在一个 list 里每一帧遍历这个 list逐个位移、逐一检测碰撞。这会自然引导你用类来封装苹果的属性和行为class Apple: def __init__(self): self.x random.randint(0, 800 - apple_size) self.y 0 self.speed random.randint(3, 8) def update(self): self.y self.speed def draw(self, screen): pygame.draw.circle(screen, RED, (self.x, self.y), apple_size)等到你开始用类组织游戏对象时恭喜你你已经半只脚踏进了“游戏开发”的门槛。这也再次印证我开头那句话Pygame 的技术难度不高但它逼着你养成游戏开发的正确思维。第三个是替换图片素材。你会发现不管几何图形画得多好它终究是几何图形。想让画面好看可以用pygame.image.load(apple.png)加载真实图片然后通过get_rect()获取它的矩形区域。加载图片后有一个小坑直接加载的图片颜色可能偏黑这是因为图片有三通道数据没有预处理解决办法是在 load 之后调用convert_alpha()或convert()让图片格式和屏幕格式对齐这样显示才正常绘制速度也会快一些。6. 常见问题与排查技巧实录6.1 新手最容易踩的五个坑我把这些年见过的高频问题整理成下面这个表基本覆盖了“第一个 Pygame 游戏”会遇到的绝大多数困境。现象原因解决办法提示ModuleNotFoundError: No module named pygamepygame 没有安装或装进了别的 Python 环境在项目对应的虚拟环境里执行pip install pygame用python -m pip list确认窗口一闪而过程序立刻退出代码里没有主循环或者循环条件一开始就是 False检查是否有while running循环且running初始为 True窗口点关闭没反应像卡死循环内没有处理pygame.QUIT事件或没有调用pygame.event.get()在事件循环里加上if event.type pygame.QUIT: running False画了图形但屏幕一片黑/白忘写pygame.display.flip()画的内容停留在后台缓冲在绘制完所有元素后调用pygame.display.flip()中文字体显示成方框当前系统中的默认字体不支持中文渲染使用pygame.font.SysFont(simhei, 24)或改用英文字符串物体移动速度没有规律有时飞快没有clock.tick(60)限制帧率循环速度取决于 CPU在循环末尾调用clock.tick(60)物体移动到窗口边缘还能继续位移缺少边界判断坐标可以无限涨给坐标加上if player_x 0等上下限限制这里我想单独展开讲一下字体问题。pygame.font.SysFont(simhei, 24)里的simhei是黑体在 Windows 上通常能用macOS 上你改成pingfangLinux 上可能是wqy-zenhei。跨平台最稳妥的办法是把自己选好的字体文件放进项目目录用pygame.font.Font(myfont.ttf, 24)加载。如果没有中文字体文件直接显示英文也可行比如Score: 10。还有一个容易被忽略的点某些系统安装 pygame 时要用pygame-ce而不是pygame。pygame-ce是原项目的社区维护分支兼容老 API更新更活跃如果你在安装 pygame 后遇到一些版本相关的问题可以直接pip install pygame-ce替换试试。对写第一个游戏来说两者用法几乎一致不用纠结。6.2 性能优化与调试小贴士主循环每秒执行 60 次看起来不多但如果你在循环里反复执行耗时操作照样会卡顿。最常见的是“每帧都加载一次图片”和“每帧都创建一个字体对象”。这些资源应该在循环外创建一次循环里只是使用。我自己的调试习惯也很简单。当游戏行为不符合预期时加打印是最快的定位手段print(fapple_x: {apple_x}, player_x: {player_x}, score: {score})把关键坐标和状态值打出来运行一遍看数值变化就能立刻发现问题。别小看这种笨办法很多看似玄学的 bug最后都是靠打印定位的。另外提醒一句Pygame 的窗口在 macOS 上有时候会出现“白屏但不退出”的情况这通常是刷新频率没对上的问题。先确认clock.tick有没有写再确认flip有没有写90% 的情况这两行能解决一多半显示类 bug。最后送你们一个调试利器在循环里显示实时帧率。fps_text font.render(fFPS: {clock.get_fps():.1f}, True, BLACK) screen.blit(fps_text, (10, 40))clock.get_fps()会返回上一秒平均的帧率。如果这个数字离 60 差得远说明你的循环里有东西拖慢速度。帧率掉得离谱第一个要怀疑的就是有没有在循环里重复创建对象其次是事件队列有没有越攒越多。结尾再分享一点我的经验接苹果游戏做完之后你可以沿着两条路继续走。一条是把玩法做厚加音效、加动画、加排行榜另一条是回头去学一点面向对象把挡板、苹果都改造成类。我个人更推荐先走第二条路因为代码组织能力的提升比多塞几个功能对你后续的帮助更大。我以前带过几个新手观察到一个很普遍的现象大家喜欢一次性写很长的代码然后从头到尾查 bug查得头皮发麻。我希望你反过来每写五到十行就跑一下确认没崩再继续写。这听起来慢实际上是最快的开发方式。游戏代码不像写作文你可以边写边跑让每一步的输出都成为下一步的依据。接苹果 1.0 版本跑通的那一刻哪怕它再简陋也是你的作品。记住那种感觉然后去改它。改出 2.0、3.0比重新开一百个新项目都值。