news 2026/10/15 10:35:21

Pygame入门教程:从主循环到碰撞检测,手把手教你制作2D小游戏

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Pygame入门教程:从主循环到碰撞检测,手把手教你制作2D小游戏

简介:Pygame是Python开发2D游戏常用的第三方模块,基于SDL构建,免费开源且支持多平台,适合具备Python基础、希望进入游戏开发领域的读者系统入门。这套PDF教程采用“知识点+示例”相结合的方式编写,依次讲解了环境安装与第一个Pygame程序、游戏主循环、Surface对象、Event事件、Rect区域、Display显示模块、Transform图像变形、Time时间控制、Draw绘图函数、Font文本字体,以及精灵和碰撞检测等核心内容,覆盖面广且示例丰富;教程将复杂的开发过程拆分成可操作的小步骤,帮助读者实现“学一点、透一点”的效果。资源本身为1个PDF文件,压缩包大小仅1.32MB,携带方便,可随时查阅学习。目前已有1560人学习/下载,教程章节安排清晰,既适合搭建基础概念,也适合按需查找具体用法。通过完整学习,读者能够掌握Pygame常用的API和开发流程,并动手完成飞机大战、贪吃蛇等2D小游戏,为进一步开发更复杂的项目打下扎实基础。

1. 为什么 Ppygame 值得你花一个周末学一遍

我见过太多人学 Pygame 的方式:拿到库就想做一个俄罗斯方块,结果卡在事件循环上三天不动一步。Pygame 本身并不难,难的是你一开始就把它当做一个“游戏引擎”来用,而它实际上是一个写游戏循环的 SDL 封装库。你需要的不是背 API,而是趁早把「主循环 + 事件 + 帧率」这个骨架搭起来,之后所有功能都是往这个循环里填肉。

这篇教程按我做小游戏的习惯来写:先跑通最小窗口,再把事件、绘制、碰撞逐个加进去,最後给一个能直接改成任意 2D 小游戏的骨架。适合三类人:第一次接触 Pygame 的新手、想快速做课程设计的学生、以及想用 Python 写个可玩原型验证点子的开发者。看完你应该能独立写一个 30 分钟能玩完的小游戏,并且清楚翻车时该去哪找原因。

2. 安装与第一个窗口:PyPygame 的主循环到底是什么

很多人第一次运行 Pygame 程序会遇到“窗口闪一下就没”、或者“窗口卡死点不了关闭”的情况。追根溯源,是因为没理解 Pygame 程序的结构:它不是一个“从上到下执行完就退出”的脚本,而是一个“永远在 while 里转,直到收到退出事件”的循环程序。

2.1 用虚拟环境隔离你的 Pygame 项目

我一般会建议每个 Pygame 项目单独建一个虚拟环境,而不是直接 pip install 到系统 Python。原因很实际:Pygame 对 Python 版本有一些兼容要求,不同项目可能依赖不同版本,全局安装一旦升级就可能把别的项目搞挂。

mkdir my_pygame_demo cd my_pygame_demo python -m venv venv # Windows 激活 venv\Scripts\activate # macOS / Linux 激活 source venv/bin/activate pip install pygame python -c "import pygame; print(pygame.version.ver)"

这段命令做的事是:创建项目目录,建一个虚拟环境,然后激活它。最后一行是验证 Pygame 是否装好——如果能看到类似2.6.1的版本号,说明安装成功。

注意:如果你的 Python 是 3.7 或更早版本,安装 Pygame 可能会默认装到旧版,我建议直接用 Python 3.10 或更高版本,少踩很多兼容坑。

2.2 最小可运行窗口:初始化、画布与主循环

下面这 20 行是 Pygame 程序的地基。每次你新建一个 Pygame 项目,先把这段跑通,再做任何功能。

import pygame import sys # 1. 初始化 Pygame 所有模块 pygame.init() # 2. 创建窗口:宽 800,高 600 screen = pygame.display.set_mode((800, 600)) pygame.display.set_caption("第一个 Pygame 窗口") # 3. 创建帧率控制对象 clock = pygame.time.Clock() # 4. 主循环 running = True while running: # 5. 事件处理 for event in pygame.event.get(): if event.type == pygame.QUIT: running = False # 6. 清空屏幕 screen.fill((30, 30, 30)) # 7. 把画布内容显示到屏幕上 pygame.display.flip() # 8. 控制帧率为 60FPS clock.tick(60) # 9. 退出 Pygame pygame.quit() sys.exit()

这段代码逻辑并不复杂,但每一个编号步骤都有讲究:

  • pygame.init()会初始化所有 Pygame 模块,包括显示、事件、字体、声音等。如果你只用到显示模块,也可以用pygame.display.init(),但全量初始化更省心。
  • pygame.display.set_mode((800, 600))返回一个 Surface 对象,这是你要绘制的画布。注意传入的是一个元组。
  • pygame.event.get()取走所有等待处理的事件。pygame.QUIT是用户点击窗口关闭按钮时产生的事件。
  • screen.fill((30, 30, 30))用 RGB 颜色填充整个画布,这一步负责清屏。如果不做这步,上一帧的画面会残留,出现“拖影”。
  • pygame.display.flip()把画布内容一次性呈现到屏幕上。与它对应的是update(),后面细说。
  • clock.tick(60)让循环每秒最多跑 60 次,同时控制游戏速度。

提示:flip()是“整页翻转”,把所有绘制内容一次性显示;update()默认只更新有变化的区域。简单游戏用flip()就够了,做性能优化时才考虑update()。

2.3 主循环的三种典型“错觉”

很多初学者写到这里会踩一个坑:以为主循环只是“让窗口不关闭”的代码,然后为了做动画,自己又开了一个while True循环来更新坐标,结果两个循环互相打架。

循环是 Pygame 的 CPU 和 GPU 之间的交互枢纽,所有交互都必须在主循环里完成。一个常见做法是:把主循环看作一个“心跳”,每跳一次,程序做三件事——读事件、更新状态、绘制画面。任何想放进游戏里的功能,都得主动塞进这个心跳里,而不是另外开循环。

另一个常见错觉是:以为pygame.QUIT事件只会在点击 X 按钮时产生。其实程序被系统信号终止时,也可能触发退出事件,所以有些开发者会额外加一个pygame.event.get()兜底。我一般只在主循环处理QUIT事件,因为如果程序需要强制退出,直接在循环外退出就行。

还有一个误区是:不调用pygame.quit()。大多数情况下系统会自动回收资源,但如果你在程序里加载了声音文件、图片等资源,显式调用pygame.quit()释放内存是更安全的做法,这也是我每次写 demo 都保留它的原因。

3. 事件系统:为什么方向键有时灵有时不灵

事件处理是 Pygame 里最容易被误解的部分。你会发现一个现象:同样的代码,在for event in pygame.event.get():里判断KEYDOWN和在外面判断key.get_pressed(),行为完全不同。这一章我们把事件来回讲透。

3.1 事件队列与 pygame.event.get() 的“一次性”特性

pygame.event.get()会把当前所有待处理事件一次性取走,然后事件队列就清空了。所以如果你在处理完事件之后、循环末尾又调用一次get(),第二次拿到的往往是一个空列表。

for event in pygame.event.get(): if event.type == pygame.QUIT: running = False elif event.type == pygame.KEYDOWN: if event.key == pygame.K_SPACE: print("按下了空格键")

这里event.key只有在事件类型是KEYDOWN或KEYUP时才存在。如果你在MOUSEBUTTONDOWN事件里访问event.key,会直接报错说没有这个属性。所以用elif分支保护是必要的。

3.2 键盘按下检测:事件 vs 状态,两种完全不同的手感和用途

这是 Pygame 新手最容易混淆的地方:用事件监听键盘,和用pygame.key.get_pressed()轮询键盘状态,它们的用途截然不同。

# 方式 A:事件监听,适合“按一下只触发一次”的行为(跳跃、开火、换弹) for event in pygame.event.get(): if event.type == pygame.KEYDOWN and event.key == pygame.K_SPACE: player_jump() # 方式 B:状态轮询,适合“按住持续生效”的行为(移动、冲刺) keys = pygame.key.get_pressed() if keys[pygame.K_LEFT]: player_move_left() if keys[pygame.K_RIGHT]: player_move_right()

两者的核心区别:事件是“离散的”——它只告诉你按下那一刻发生了什么;状态是“连续的”——它告诉你当前这一帧哪些键还按着。如果你用事件监听来做角色移动,会出现“按一下走一步,但连续按下时卡顿”的情况,因为事件频率远低于帧率。反过来,如果你用状态轮询来做跳跃,角色会跳得停不下来,因为只要键还按着,每帧都会触发跳跃。

我自己的习惯是:所有连续操作都用get_pressed(),所有单次操作都用KEYDOWN事件,这是 Pygame 游戏手感好的第一步。

3.3 鼠标事件:位置、按钮与坐标陷阱

鼠标事件在教程里经常被一笔带过,但在实际游戏里非常关键,比如菜单按钮、拖拽物体、射击瞄准。

for event in pygame.event.get(): if event.type == pygame.MOUSEBUTTONDOWN: mouse_pos = pygame.mouse.get_pos() print(f"鼠标按下,位置: {mouse_pos},按钮: {event.button}") if event.type == pygame.MOUSEMOTION: rel_x, rel_y = event.rel print(f"鼠标相对移动: {rel_x}, {rel_y}")

pygame.mouse.get_pos()返回的是全局鼠标坐标,单位是像素,原点在窗口左上角,x 向右增加,y 向下增加。这个原点方向让很多从数学坐标系转过来的人不适应,但这就是 Pygame 的规则,画图、碰撞检测全都基于它。

event.rel返回鼠标相对于上一帧的移动量,做第一人称视角类的视角旋转很有用。需要提醒的是:鼠标事件里没有event.pos的MOUSEBUTTONDOWN拿到的是窗口内坐标,和get_pos()返回一致;但MOUSEWHEEL事件的event.x/event.y是滚动增量,不是坐标,别搞混。

3.4 事件阻塞与窗口假死的真相

还有一个经典问题:窗口标题栏会显示“未响应”。原因往往不是 Pygame 自身卡了,而是你的主循环里有耗时的操作,比如加载很大的图片、播放声音的阻塞等待、或者一个time.sleep()。因为事件处理在主循环的前部,如果循环内部卡住,事件队列会越积越多,系统就会判定窗口无响应。

# 错误示范:在主循环里用 sleep 做延迟 while running: for event in pygame.event.get(): pass time.sleep(0.1) # 这会让事件处理延迟 100ms,窗口明显卡顿

正确做法是用时间戳或计数器来控制定时逻辑,而不是阻塞循环。Pygame 的pygame.time.get_ticks()返回程序启动以来的毫秒数,比较这个值可以做出“每 500ms 发射一颗子弹”的效果而不卡住窗口。

4. 绘制与动画:坐标、Surface 与图片加载的底层规则

Pygame 的“绘制”概念和 HTML Canvas 非常像:你在一张二维画布上用像素填色,然后把画布整块显示出来。理解 Surface、坐标和图片加载的底层规则,可以省去大量调试时间。

4.1 坐标系统:为什么 y 轴方向总搞反

Pygame 的坐标原点在窗口左上角,x 轴向右为正,y 轴向下为正。这与数学中常见的“右上为正”的坐标系是镜像关系,所以很多人在画抛物线、圆形运动轨迹时,感觉 y 方向反了。

# 在 (400, 300) 处画一个半径为 50 的圆,圆心位于窗口中心 pygame.draw.circle(screen, (255, 200, 0), (400, 300), 50)

如果想让物体“下落”,应该不断增加 y 坐标;想让物体“上升”,则减小 y 坐标。方向键控制物体移动时,按左减 x,按右加 x,按上加 y,按下加 y——这个直觉一旦建立,就再也不会把上下搞反。

注意:Pygame 的坐标系在旋转、缩放操作时会围绕物体左上角或中心点进行,具体取决于你调用哪个方法。这个下面单独讲。

4.2 绘制图形:Rect、圆与线条的基本套路

Pygame 提供了一套方便的画图接口,但它们的参数并不统一,很多人第一次用时容易对不上:

# 矩形:需要指定 Rect 对象 pygame.draw.rect(screen, (0, 128, 255), pygame.Rect(100, 100, 120, 60)) # 圆形:圆心 + 半径 pygame.draw.circle(screen, (255, 0, 0), (200, 200), 40) # 线条:起点 + 终点 [+ 宽度] pygame.draw.line(screen, (255, 255, 255), (50, 50), (150, 200), 3) # 多边形:顶点列表 pygame.draw.polygon(screen, (0, 255, 0), [(300, 50), (350, 150), (250, 150)])

pygame.draw.rect第一个版本要求一个Rect对象,这是 Pygame 里最核心的数据结构。Rect说得直白一点,就是 {x, y, width, height} 四个值的封装,但它提供了非常方便的碰撞检测和位置操作,是往下学必须吃透的东西。

4.3 图片加载:convert() 与 convert_alpha() 的区别

pygame.image.load()加载图片后,返回的 Surface 还不能直接绘制,很多初学者把图片画上去发现“黑块”或“性能极差”,问题就出在这里。

player_img = pygame.image.load("player.png") # 直接 blit 绘制 screen.blit(player_img, (100, 100)) # 正确做法:用 convert_alpha() 转换后再绘制 player_img = pygame.image.load("player.png").convert_alpha() screen.blit(player_img, (100, 100))

convert()把图片转换成与屏幕相同像素格式的 Surface,绘制速度快很多;convert_alpha()额外保留了透明度通道,适合 PNG 透明背景图片。如果你不调用转换,Pygame 每帧绘制时都要做一次像素格式转换,虽然小图片感觉不出来,但图片一多、画面一大,帧数会直线下降。

提示:如果加载的图片不是 PNG,而是 JPG,convert_alpha()会丢掉透明度,属于正常行为;JPG 没有 alpha 通道。

4.4 blit 与 Rect:从绘制到碰撞

blit是 Pygame 里最常用的绘制方法,它的作用是把一个 Surface 渲染到另一个 Surface 上。第二个参数可以是坐标元组,也可以是 Rect 对象。

player_rect = player_img.get_rect() player_rect.topleft = (100, 100) screen.blit(player_img, player_rect) # 获取玩家矩形区域 enemy_rect = enemy_img.get_rect() enemy_rect.center = (400, 300) # 检测碰撞 if player_rect.colliderect(enemy_rect): print("碰撞发生!")

get_rect()会根据图片尺寸生成一个以图片左上角为基准的Rect。Rect提供了colliderect()、collidepoint()等碰撞检测方法,这是 Pygame 做游戏碰撞最简单靠谱的方式,比手写坐标范围判断要高效得多。

Rect对象有 8 个常见属性可以设置:x,y,top,bottom,left,right,centerx,centery。设置player_rect.center = (x, y)会自动更新它的位置,省去手动计算左上角坐标的功夫。

5. Pygame 避坑清单:5 个最常见的翻车现场

这一章我把做 Pygame 项目时最常遇到的 5 个问题整理成清单,按照「现象 → 原因 → 解决」的格式写清楚。这些问题不只是初学者会遇到,我做原型时也经常会因为忘记某条规则翻车。

5.1 窗口一运行就闪退,根本来不及看画面

  • 现象:双击运行或从 IDE 运行程序,窗口一闪而过,没有任何报错。
  • 原因:绝大多数情况是代码最后没有进入主循环。比如只写了pygame.init()和set_mode(),然后pygame.quit(),程序从第一行执行到最后一行,窗口当然会立即关闭。或者主循环条件running一开始就被设成了False,导致循环一次都没执行。
  • 解决:确保主循环正确包裹游戏逻辑;如果用的是while running:,确认running初始值为True。还有一种情况是事件循环里误把QUIT事件判断写错,比如写成if event.type == pygame.QUIT: running = True,这样当然会秒退。

5.2 画面上出现残影或拖影

  • 现象:圆或方块移动后,原来的位置留下痕迹,画面重叠。
  • 原因:主循环里没有调用screen.fill()清屏。Pygame 不会自动清除上一帧的画面内容,需要手动清理。
  • 解决:在每次绘制之前先填充背景色。注意顺序:fill必须在所有blit之前。另外有一种特殊情况:如果你用了pygame.display.update()而不是flip(),并且只更新了部分区域,旧画面残留也可能是因为更新区域没覆盖到移动后留下的空白位置。

5.3 游戏速度时快时慢,完全吃帧率

  • 现象:高配电脑上游戏飞快,低配电脑上像幻灯片。同一个游戏在不同机器上速度差异巨大。
  • 原因:因为你没有控制帧率,或者移动逻辑每帧都加固定数值,而不考虑每帧实际消耗的时间。Pygame 的clock.tick(60)会限制最大帧率,但如果机器跑不到 60 帧,实际帧率可能只有 30,这时物体每秒移动的距离就少了一半。
  • 解决:使用clock.tick(60)锁定帧率;对于需要跨平台一致的速度,使用基于时间的位移。Pygame 提供clock.tick(60)返回上一帧到这一帧的毫秒数,把它换算成秒,乘以移动速度,就能保证无论机器多快或多慢,物体每秒移动的距离一样。

5.4 图片加载后是黑块或背景不是透明

  • 现象:加载 PNG 图片,绘制后透明区域变成黑色或难看的颜色。
  • 原因:第一个原因是没用convert_alpha(),图片格式与屏幕格式不一致导致 alpha 通道丢失;第二个原因是图片本身并非真正的透明 PNG,可能是白色背景或带 JPEG 压缩痕迹。
  • 解决:用convert_alpha()转换,或者换一张背景是透明通道的 PNG 图片。

5.5 中文文字显示为方框或乱码

  • 现象:使用pygame.font.Font(None, 36)渲染中文字符串,运行时屏幕上全是方框。
  • 原因:Pygame 默认字体不支持中文,必须指定一个支持中文的系统字体文件,比如 Windows 下的simhei.ttf或 macOS 下的PingFang.ttc。
  • 解决:用pygame.font.SysFont()或直接指定字体文件路径:
font = pygame.font.SysFont("simhei", 36) text_surface = font.render("你好,Pygame", True, (255, 255, 255)) screen.blit(text_surface, (200, 200))

6. 一个可以直接拿走的 Pygame 骨架:用 60 行代码实现“方块躲避”

最后我用一个可运行的完整小游戏,把上面讲的知识点串起来。你改改坐标和素材,就能扩展成吃金币、打飞机、跑酷这类 2D 游戏。代码不长,但包含了事件处理、帧率控制、绘制、碰撞检测、游戏结束判断这 5 个核心闭环。

import pygame import sys import random pygame.init() screen = pygame.display.set_mode((600, 800)) pygame.display.set_caption("方块躲避") clock = pygame.time.Clock() player_rect = pygame.Rect(270, 700, 60, 40) obstacle_rect = pygame.Rect(random.randint(0, 540), 0, 60, 30) speed = 5 score = 0 font = pygame.font.SysFont("simhei", 30) 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_rect.left > 0: player_rect.x -= speed if keys[pygame.K_RIGHT] and player_rect.right < 600: player_rect.x += speed obstacle_rect.y += speed if obstacle_rect.top > 800: obstacle_rect.y = 0 obstacle_rect.x = random.randint(0, 540) score += 1 speed += 0.5 # 每躲过一个障碍,移速加快 if player_rect.colliderect(obstacle_rect): running = False screen.fill((20, 20, 20)) pygame.draw.rect(screen, (0, 200, 100), player_rect) pygame.draw.rect(screen, (200, 50, 50), obstacle_rect) score_text = font.render(f"得分: {score}", True, (255, 255, 255)) screen.blit(score_text, (10, 10)) pygame.display.flip() clock.tick(60) pygame.quit() print(f"游戏结束,最终得分: {score}") sys.exit()

这个游戏的核心机制:玩家用左右键控制绿色方块,红色障碍从顶部随机位置下落,每躲避一个障碍得一分,同时速度增加,直到碰撞后结束。你可以看到我把所有逻辑都塞在一个主循环里:读事件、获取键盘状态、更新位置、检测碰撞、绘制。

速度增量让游戏越到后面越刺激,也是一种最简单的难度曲线。如果你想调整手感,重点改两个参数:speed初始值控制基础难度,speed += 0.5控制难度增长速度。至于障碍物宽度,修改obstacle_rect定义中的60和30即可。

这 60 行代码就是我认为的 Pygame 最小可用骨架。我自己每次从零开始做小游戏,都会先把这段跑通,再往里加图片、音效、关卡——主循环不动,游戏世界随便换。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 10:35:09

算法时间复杂度与八大排序选型实战指南

1. 这不是背公式&#xff0c;而是理解“算法成本”的真实账本 刚接触数据结构时&#xff0c;我见过太多人把时间复杂度记成一句顺口溜&#xff1a;“O(1)是常数&#xff0c;O(n)是线性&#xff0c;O(n)是平方……”然后在笔试题里对着冒泡排序写个O(n)&#xff0c;心里还觉得挺…

作者头像 李华
网站建设 2026/10/15 10:34:59

模式匹配:从原理到实战的编程利器

1. 模式匹配的本质与应用场景 模式匹配是计算机科学中一种强大的抽象工具&#xff0c;它允许我们通过声明式的规则来描述和处理数据的结构。想象一下你在整理衣柜时&#xff0c;会自然地将衬衫、裤子和外套分类挂放——模式匹配就是程序世界的分类法则&#xff0c;只不过它能处…

作者头像 李华
网站建设 2026/10/15 10:34:57

多帧融合与超分辨率:hyperframes图像增强实战解析

如果你在图像处理、计算摄影或者机器视觉领域折腾过一阵子&#xff0c;对“多帧融合”“超分辨率”这类词应该不陌生。hyperframes不是某家公司的商业软件&#xff0c;也不是某个大厂的闭源框架&#xff0c;而是一种比较硬核的处理思路&#xff1a;把连续拍摄的多帧图像当成一个…

作者头像 李华
网站建设 2026/10/15 10:34:38

矩形微带天线尺寸计算公式与MATLAB实现指南

简介&#xff1a;矩形微带天线尺寸计算的专用文档&#xff0c;面向射频天线初学者与工程技术人员&#xff0c;解决由中心频率、介质基板参数推算贴片宽度、长度及馈电点位置的问题&#xff0c;是微带天线初步设计的常用参考。包内仅有1个doc文档&#xff0c;压缩包约52KB&#…

作者头像 李华
网站建设 2026/10/15 10:34:37

市场能力不是话术,而是可验证的客户价值操作系统

1. 这不是“营销话术速成班”&#xff0c;而是一套能立刻上手、持续生效的市场能力操作系统“marketingskills”这个词最近在招聘平台、自由职业接单社区和创业者私域群里高频出现&#xff0c;但它绝不是指PPT里写的“用户增长”“品牌心智”“私域转化”这类空泛概念。我带过3…

作者头像 李华
网站建设 2026/10/15 10:34:28

grep context-mode实战:用-A -B -C参数快速定位日志与代码问题

排查线上问题时&#xff0c;我经常在服务器上对着日志发呆。搜到一个报错码&#xff0c;只看到一行孤零零的ERROR&#xff0c;前后发生了什么完全不知道&#xff0c;只好重新翻日志、加过滤器、再跑一遍命令&#xff0c;往往来回折腾好几分钟。后来我把grep的context-mode用熟了…

作者头像 李华