Pygame从零实现弹球小游戏:事件循环、碰撞检测与完整代码

发布时间:2026/9/30 3:02:13
Pygame从零实现弹球小游戏:事件循环、碰撞检测与完整代码 做游戏这件事听起来离普通人很远但你只要动过手就会明白一个能跑起来的小游戏本质上就是“循环、事件、绘制”这三件事的排列组合。Pygame 恰好把这三样东西都封装成了非常容易调用的接口所以它成了我身边很多朋友学习游戏开发的第一站。我自己用 Pygame 写的第一个作品是个弹球游戏从零到能玩前后就花了一个晚上。今天这篇文章就把整个过程摊开来讲清楚从安装、核心机制、完整代码到各种容易踩的坑一次性全给到。想练手的编程新手、要做课程作业的学生或者单纯想周末折腾点好玩东西的人都可以照着这篇文章走一遍做完你就有了一款真正能运行、能计分、能“输”的小游戏。先说清楚一点这篇文章不是把 Pygame 丢给你然后让你自己看文档而是按照我自己实际敲代码的顺序来写的。每个步骤里我会告诉你当时为什么这么做、那个函数到底是干嘛的、出错该往哪儿查。最后你会得到一份可以直接跑起来的游戏代码以及一堆我踩过之后才明白的细节。Pygame 本身不难真正劝退新手的是那些没有写进教程里的坑今天我把它们都补上。1. 为什么选 Pygame工具选型与准备工作1.1 Pygame 到底适合做什么很多人一上来就问做游戏为什么不用 Unity、Godot这个问题我当初也纠结过。后来想明白一件事选工具不是选最强大的而是选最短路径。Unity 和 Godot 学的是编辑器操作、资源和场景管理那一整套东西你打开界面先懵半小时Pygame 则是一个纯 Python 库安装完直接写代码窗口、绘制、声音、键盘鼠标这些能力全部封装好了你只需要关心游戏逻辑本身。Pygame 适合的场景我总结有三个第一学编程——用游戏练手比刷题有正反馈循环和事件这些概念在游戏里变得特别直观第二做 2D 小游戏和玩法原型——弹球、贪吃蛇、打砖块、Flappy Bird 这类体量的项目它完全扛得住第三课程设计、比赛演示、社团招新的小项目。它不适合做什么呢大型 3D 游戏、重度物理模拟、多人在线高并发场景这些老老实实去用 Unity、Godot 或者更专业的引擎。Pygame 的定位就是“轻量、快速、学习友好”把它放在正确的位置上用起来才顺手。对比维度PygameUnity/Godot学习曲线平缓纯代码陡峭编辑器脚本使用语言PythonC#/GDScript启动成本极低pip 即可需要安装大型编辑器适用项目2D 小游戏/原型中大型游戏我自己的体会是Pygame 特别适合拿来验证游戏创意。比如你想到了一个玩法机制用 Pygame 半小时就能做个原型试手感比直接上沉重引擎方便太多等要产品化了再迁移到成熟引擎也不迟。这种“先做原型再谈规模”的思路其实是游戏行业里非常常见的做法。1.2 安装 Pygame 的正确姿势安装 Pygame 的常规操作是 pip install pygame但这里有几个第一次装就容易碰到的细节。首先建议你为项目单独建立虚拟环境不要直接装到系统 Python 里。我见过太多人把包装到全局环境不同项目互相污染依赖然后到处是“莫名其妙报错”。创建虚拟环境的方式很简单python -m venv game_env # Windows 激活 game_env\Scripts\activate # macOS/Linux 激活 source game_env/bin/activate激活之后再安装pip install pygame装完验证一下版本python -c import pygame; print(pygame.version.ver)如果看到类似 2.x.x 的版本号就说明装好了。这里我再推荐一个 Pygame 自带的测试入口python -m pygame.examples.aliens它会直接弹出一个外星人小游戏的窗口能玩就代表你的 Pygame 环境和底层依赖都工作正常。第一次见到这个窗口的瞬间其实比后面写代码还开心。关于安装失败的问题最常见的原因有三个一是网络原因导致下载超时二是 Python 版本过老三是 pip 本身没升级。遇到下载慢可以给 Python 的包管理器换用国内 PyPI 镜像源这个操作很常规网上搜一下就有具体命令遇到老版本 Python建议直接把 Python 升级到 3.8 以上Pygame 2.x 系列对新版本支持更好遇到 pip 的问题先执行 python -m pip install --upgrade pip通常就解决了。1.3 开发前的环境配置代码编辑器我推荐 VS Code装一个 Python 插件就能直接跑对新手上手最友好如果你之前用过 PyCharm那更好它的调试工具对追踪游戏状态很有帮助。项目结构上不要上来就搞得很复杂我一般先建一个干净的文件夹比如 game_demo里面直接放一个 main.py 起步等游戏复杂了再拆模块。Pygame 小项目阶段单文件并不是坏习惯反而能让你把注意力集中在逻辑上。资源文件的路径问题是新手最不容易察觉的大坑。你的图片、音频、字体文件如果放在 assets 子目录直接写 assets/bg.png 有时候会报找不到文件因为代码的工作目录取决于你从哪里启动 Python。稳妥的做法是用绝对路径或者基于__file__来定位。后面我会在常见问题部分专门讲这个这里先记住一个原则你的 main.py 放在哪个目录启动命令就在哪个目录执行资源文件用相对路径时也要以那个目录为基准。2. 新手必须搞懂的三个核心机制2.1 游戏窗口与坐标系做 Pygame 游戏第一件事就是创建窗口。代码只有一行但背后牵扯到一个你必须先适应的坐标系。Pygame 窗口的左上角是原点 (0, 0)x 轴向右增大y 轴向下增大。这和数学课上的坐标系正好相反——y 是往下走的。第一次写代码十个新手里有九个在这里犯迷糊想让物体往上走y 要减小想让物体往下走y 要增大。记住这个“反直觉”设定后面做运动逻辑才不会写反。窗口创建相关的核心代码import pygame screen pygame.display.set_mode((800, 600)) pygame.display.set_caption(我的第一个Pygame游戏)pygame.display.set_mode 接收一个元组表示窗口的宽和高。它返回的 surface 对象就是你所有绘制操作的画布。set_caption 则设置窗口标题。这里有个很多人忽略的点窗口关闭并不是点了右上角的 X 就自动结束的操作系统会把关闭操作变成一条 QUIT 事件你的游戏循环必须接收并处理它否则点关闭只会让窗口卡死或闪退这个机制下面细讲。2.2 事件循环游戏的心脏Pygame 游戏本质上是一个死循环每一轮循环处理三件事接收用户输入、更新游戏状态、把画面画出来。这个循环就是游戏的心脏叫“主循环”或“事件循环”。很多新手第一次写代码会把它写成“向下顺序执行”的脚本画完一帧就结束了窗口闪一下消失然后很困惑为什么别人的游戏窗口能一直开着答案就在这个循环里。先看事件处理。pygame.event.get() 会从事件队列里取出所有发生的事件比如鼠标移动、按键按下、窗口关闭。最常见的写法是for event in pygame.event.get(): if event.type pygame.QUIT: running False这里的关键理解是事件不是“你读的时候才发生”而是 Pygame 在后台一直帮你收集你不取出来它们就累积在队列里。所以每帧都必须调用 event.get() 把队列清空。如果漏掉了操作系统会以为程序已无响应窗口就变成“转圈圈”的假死状态。这是非常经典的一个 bug我后面还会重点讲一遍。主循环的基本形态是固定的你以后会见到千千万万种变体但骨架就这一副running True while running: # 1. 处理事件 # 2. 更新游戏状态 # 3. 绘制画面 pygame.display.flip()2.3 surface 绘制与帧率控制Pygame 里所有的图形都画在 surface 上它本质就是一块内存中的像素画布。最常见的操作是先 screen.fill(color) 把整张画布填成背景色再用 pygame.draw.circle 画圆、pygame.draw.rect 画矩形把精灵图片放上去则用 screen.blit(image, position)。blit 是“块传输”的缩写意思是把一块像素数据“贴”到另一块 surface 上你把它理解成盖章就行。有一个关键点新手容易忽略你在 screen 上画完东西并不会立刻显示到屏幕上。必须调用 pygame.display.flip() 把整块画布推到显示器才会真正看到画面。这就是“双缓冲”机制——flip 把后台画好的内容一次性交换到前台避免绘制过程中画面半成品外露造成闪烁。配套的函数还有 pygame.display.update()它只更新部分区域性能更高但对新手来说直接用 flip 最稳。帧率控制同样重要。如果你的主循环没有任何限速机制它就会有多少算力跑多快CPU 直接吃满帧率上千游戏速度快到没法玩。正确做法是创建时钟对象clock pygame.time.Clock() clock.tick(60) # 每秒最多 60 帧clock.tick(60) 的意思是每帧之间至少留够 1/60 秒让循环稳定在 60 FPS。专业一点说这就是一个帧率上限同时它还能避免你的电脑风扇狂转。60 FPS 下每帧大约是 16.7 毫秒对这类小游戏来说既流畅又不吃力。你要是把 60 改成 30画面会明显感觉变“肉”这就是帧率对游戏手感的直接影响。3. 一步步动手做弹球小游戏理论讲完了接下来动手。我选的这个游戏叫“弹球小游戏”——屏幕右侧有一个挡板你用键盘上下方向键移动它一颗小球在屏幕里来回弹每次接到球得一分球从左侧飞出就游戏结束。这个项目麻雀虽小但游戏开发最核心的全套要素都包含了。3.1 初始化游戏框架先把最外层框架搭出来import pygame import sys pygame.init() WIDTH, HEIGHT 800, 600 FPS 60 WHITE (255, 255, 255) BLACK (0, 0, 0) GREEN (0, 200, 0) screen pygame.display.set_mode((WIDTH, HEIGHT)) pygame.display.set_caption(弹球小游戏) clock pygame.time.Clock() running True while running: for event in pygame.event.get(): if event.type pygame.QUIT: running False screen.fill(BLACK) pygame.display.flip() clock.tick(FPS) pygame.quit() sys.exit()这段代码跑起来你会看到黑色窗口稳稳地开着。有几个点说明一下pygame.init() 会把 Pygame 的各个模块一次性初始化写这一行就够不用逐个模块手动 initpygame.quit() 和 sys.exit() 放在循环结束后用来安全退出。颜色用 RGB 元组表示建议把常用颜色定义成常量后面写起来清爽不少。窗口宽高和帧率也放到顶部常量区以后调参数不用满屏找。3.2 让小球动起来要让小球动起来你只需要两样东西它的位置和它的速度。每帧都做一次“位置 位置 速度”的更新画面就会看到球连续移动。代码是这样写的ball_pos [WIDTH // 2, HEIGHT // 2] # 位置 ball_r 10 # 半径 ball_speed [5, 5] # 每帧移动距离 ball_pos[0] ball_speed[0] ball_pos[1] ball_speed[1] pygame.draw.circle(screen, WHITE, ball_pos, ball_r)注意这里用的是列表而不是元组因为列表可变方便每帧更新坐标。速度为什么是 5 不是 50因为这里的单位是“每帧移动的像素数”。60 FPS 下5 像素/帧意味着每秒移动 300 像素正好能在 800 宽的屏幕上来回几次。你把这个值调大球就会发疯一样快调小就会变得很佛系。这就是游戏参数调节最直观的体验。球撞到上下边界要反弹这是最基本的碰撞检测。检测条件非常直觉如果球的 y 坐标减去半径小于 0说明球顶越过了上边界如果 y 坐标加上半径大于屏幕高度说明球底越过了下边界。两种情况都触发反弹反弹就是让 y 方向速度取反if ball_pos[1] - ball_r 0 or ball_pos[1] ball_r HEIGHT: ball_speed[1] -ball_speed[1]3.3 挡板与键盘交互挡板是一个矩形初始位于右侧居中位置。玩家控制它上下移动这需要读取键盘状态。事件循环里的键盘事件适合处理“按一下”的场景比如按空格跳跃但“一直按住上下方向键持续移动”更适合用 pygame.key.get_pressed() 来读取。这个函数会返回所有按键当前状态的元组每帧你都能判断某个键是否被按住PADDLE_W, PADDLE_H 20, 100 paddle_x WIDTH - 40 paddle_y HEIGHT // 2 - PADDLE_H // 2 paddle_speed 7 keys pygame.key.get_pressed() if keys[pygame.K_UP]: paddle_y - paddle_speed if keys[pygame.K_DOWN]: paddle_y paddle_speed挡板不能划出屏幕外所以加一个边界限制。经典写法是用 max/min 把数值夹住paddle_y max(0, min(HEIGHT - PADDLE_H, paddle_y))这行代码等价于“如果 paddle_y 小于 0 就强制为 0如果大于 HEIGHT - PADDLE_H 就强制为那个值”简洁高效。画挡板只需要一行pygame.draw.rect(screen, WHITE, (paddle_x, paddle_y, PADDLE_W, PADDLE_H))这里传入的元组是“左上角 x、左上角 y、宽、高”注意不是“左上角和右下角”这是另一个容易搞错的地方。3.4 碰撞检测和计分球碰到挡板要反弹并加分。检测方法看几何关系球的右边缘碰到挡板的左边缘并且球的 y 坐标落在挡板的 y 范围里。写成条件就是if paddle_x ball_pos[0] ball_r paddle_x PADDLE_W and paddle_y ball_pos[1] paddle_y PADDLE_H: ball_speed[0] -ball_speed[0] score 1这个判断很直白但也是新手最容易出错的地方。初学者常犯的错是只判断“球水平方向碰到了挡板”就反弹结果球从上方或下方擦过也反弹游戏体验特别奇怪。加上 y 坐标范围的判断才算真正做到“球真的碰到了板”。计分用文字渲染。Pygame 显示文本需要三步先创建字体对象再把字符串渲染成 surface最后 blit 到屏幕上。字体我直接用默认字体不加载外部文件font pygame.font.Font(None, 36) text_surface font.render(fScore: {score}, True, GREEN) screen.blit(text_surface, (10, 10))游戏失败条件我设了两个球从左边飞出屏幕或者从右边飞出屏幕。左边飞出代表玩家接不住游戏结束右边飞出属于球越过挡板的极端情况也结束。判断代码if ball_pos[0] - ball_r 0 or ball_pos[0] ball_r WIDTH: running False3.5 完整代码汇总把上面的片段串起来就是一份可以直接运行的游戏import pygame import sys pygame.init() WIDTH, HEIGHT 800, 600 FPS 60 WHITE (255, 255, 255) BLACK (0, 0, 0) GREEN (0, 200, 0) screen pygame.display.set_mode((WIDTH, HEIGHT)) pygame.display.set_caption(弹球小游戏) clock pygame.time.Clock() BALL_SPEED 5 ball_pos [WIDTH // 2, HEIGHT // 2] ball_r 10 ball_speed [BALL_SPEED, BALL_SPEED] PADDLE_W, PADDLE_H 20, 100 paddle_x WIDTH - 40 paddle_y HEIGHT // 2 - PADDLE_H // 2 paddle_speed 7 score 0 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_UP]: paddle_y max(0, paddle_y - paddle_speed) if keys[pygame.K_DOWN]: paddle_y min(HEIGHT - PADDLE_H, paddle_y paddle_speed) ball_pos[0] ball_speed[0] ball_pos[1] ball_speed[1] if ball_pos[1] - ball_r 0 or ball_pos[1] ball_r HEIGHT: ball_speed[1] -ball_speed[1] if (paddle_x ball_pos[0] ball_r paddle_x PADDLE_W and paddle_y ball_pos[1] paddle_y PADDLE_H): ball_speed[0] -ball_speed[0] score 1 if ball_pos[0] - ball_r 0 or ball_pos[0] ball_r WIDTH: running False screen.fill(BLACK) pygame.draw.circle(screen, WHITE, ball_pos, ball_r) pygame.draw.rect(screen, WHITE, (paddle_x, paddle_y, PADDLE_W, PADDLE_H)) font pygame.font.Font(None, 36) text_surface font.render(fScore: {score}, True, GREEN) screen.blit(text_surface, (10, 10)) pygame.display.flip() clock.tick(FPS) pygame.quit() sys.exit()保存成 main.py然后 python main.py 就能玩。想调整难度的话改动三个地方就行BALL_SPEED 控制球速paddle_speed 控制挡板速度PADDLE_H 控制挡板长度。球速越快、挡板越短游戏越刺激。我实测里把 BALL_SPEED 升到 7、PADDLE_H 改到 60 之后反应时间明显吃紧。这种参数迭代本身就是做游戏最有趣的部分你可以慢慢找到自己最喜欢的手感。4. 常见问题速查与避坑经验这部分是全文最值钱的地方。以下每个问题我都真实遇到过有些还困扰了我大半天。先整理成一张速查表症状根本原因解决方法窗口一闪而过没有主循环或主循环没进入确保 while running 循环存在窗口假死转圈没有调用 event.get()每帧必须取走事件队列画面疯狂闪烁没清屏或双缓冲使用不对先 fill 再绘制最后 flip物体移动太快/太慢帧率没限制或速度值不合理用 clock.tick 固定 FPS调速度值报找不到图片/音频工作目录不对用基于文件位置的路径定位键位没反应事件机制和状态机制混用持续移动用 get_pressed单次触发用事件4.1 画面卡顿、闪屏怎么办画面卡顿最直接的原因通常是主循环体内做了太多耗时操作。Pygame 本质是纯 CPU 绘制如果你每帧都重新加载图片、重新渲染字体性能瞬间就崩。正确做法是图片和字体在循环外加载好循环内只做复制和粘贴字体对象只创建一次不要每帧都做 Font() 重建。我亲眼见过有人把 font.render 写进循环里每帧都重新渲染文本帧率直接掉到二十几移动一下就卡。闪屏问题则是绘制顺序的锅。如果你不先 screen.fill() 清空画面上一帧的图形残影会叠加出现看起来就像闪烁和拖影。正确的绘制流程永远是“清屏 - 画所有东西 - 一次性 flip”。如果这样还闪检查你是不是用了 pygame.display.update() 不带参数来替代 flip。在某些平台上 update() 的局部刷新策略会留下残影统一用 flip 就好。4.2 碰撞检测失灵的本质原因碰撞检测失灵尤其是“球从挡板中间穿过去了”新手都会怀疑是 Pygame 的 bug。其实不是这是离散检测的经典问题——隧道效应。每一帧你只判断一次“此刻球是否碰到了挡板”如果球速度太快上一帧球还在挡板左边下一帧已经跑到了挡板右边中间完全不满足碰撞条件自然就穿过去了。解决思路有三个。第一降低球速让每帧移动距离小于挡板宽度这是最省事的。第二把碰撞区间放宽检查“球的轨迹线段”与挡板的重叠而不是只检查当前点代码复杂一些但更严谨。第三用更细的物理步长把一帧拆成多个子步骤逐次判断。对这个小游戏来说球速 5、挡板宽 20每帧移动 5 像素不存在穿模问题如果你把球速改成 30 还保持挡板宽 20穿模就必然发生。理解了这一点以后做任何 2D 游戏碰撞你都能一眼定位问题方向。4.3 加载图片和音频的老大难Pygame 加载图片的坑几乎人人都会踩一遍。我见过最多的问题是代码里写了 pygame.image.load(assets/bg.png)文件明明存在运行时报错说找不到。原因前面提过——工作目录不是代码目录。比如你在项目根目录运行 python game/main.py那代码里的相对路径是相对于项目根目录的不是 game 目录。根治办法是用基于文件位置的路径import os BASE_DIR os.path.dirname(os.path.abspath(__file__)) image_path os.path.join(BASE_DIR, assets, bg.png) bg_image pygame.image.load(image_path)音频方面有一个顺序问题Pygame 的 mixer 模块需要在初始化阶段跟着准备好否则加载声音会报 pygame.error。建议把 pygame.mixer.pre_init(44100, -16, 2, 512) 写在 pygame.init() 之前再配合 pygame.mixer.Sound() 播放这样声音模块工作得最稳定。图片加载出来发现背景是黑块那是 PNG 的 alpha 通道没处理调用 convert_alpha() 就能解决。4.4 窗口假死与事件处理误区“窗口点了没反应系统提示程序无响应”是我带过的初学者里出现频率超高的一个问题。原因基本上都是同一个事件队列没被及时取空。pygame.event.get() 每帧必须执行如果你在某个循环里卡住不做事件处理或者事件处理耗时过长系统就判定程序失去响应。同理不要在事件处理里塞 sleep 或者其它阻塞操作比如 pygame.time.delay(100)它会阻塞整个程序连事件响应一起卡住。还有一个隐蔽的误区是事件和状态混用。比如你想检测“按键是否一直按住”却用了事件机制event.type pygame.KEYDOWN那你会发现按住不放的时候只有第一次触发有效因为系统只发一次 KEYDOWN 事件。持续按住的状态判断必须用 pygame.key.get_pressed() 读当前键盘快照。这两个机制傻傻分不清是很多交互 bug 的根源。我的判断标准很简单按下瞬间的“动作”用事件持续状态的“保持”用 get_pressed。5. 做完第一个游戏之后的几点体会第一次让小球在屏幕里弹起来的时候那种“我自己做出了一个会动的东西”的成就感是刷多少道算法题都换不来的。但做完这个小游戏我更大的收获其实是思维方式的转变游戏开发里所有东西都在和时间打交道你要学会站在“每帧发生什么”的角度思考问题。位置、速度、碰撞、计分全都要在一个循环里安排得明明白白。做完了弹球接下来你可以试着往上叠加功能给球增加随机的反弹角度、把挡板换成可控制的小人、加入背景音乐、做一个计分到 10 就胜利的过关逻辑、把最高分存进文件。每加一个功能你都会重新理解一遍 Pygame 的某个模块。我个人最推荐的两个方向是——做一个贪吃蛇学会用列表管理物体运动轨迹或者做一个简易的打砖块学会多物体的批量管理。这两个做完你的 Pygame 基础就算真正扎实了。最后分享一个我自己一直在用的调试技巧当游戏行为不符合预期时先别急着改代码在循环里加上一行 print 把关键变量打出来比如球的位置、挡板位置、得分放到终端里观察几秒绝大多数逻辑 bug 都能一眼看穿。这个方法听起来很土但在游戏开发里它比任何高级调试器都直接。希望这篇能把你的第一个 Pygame 小游戏顺利带出来做完之后你会发现做游戏并没有想象中那么远。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询