
简介这份Pygame教程面向已掌握Python基础、希望入门2D游戏开发的初学者与自学者系统讲解游戏主循环、Surface对象、Event事件、Rect区域等核心概念帮助读者从零搭建可运行的小游戏。教程以知识点配合示例的方式组织覆盖Display显示模块、Transform图像变形、Time时间控制、Draw绘图函数、Font文本字体以及精灵与碰撞检测等模块并穿插飞机大战、贪吃蛇、扫雷等典型场景便于边学边练、逐步理解游戏逻辑。资源包内含1个PDF文件大小约1.32MB单文件结构适合按目录顺序通读或按模块检索查阅。目前已有1564人学习说明内容经过一定规模读者的检验。对于想以Pygame作为游戏开发敲门砖的读者可从中获得完整的基础知识框架、可参照的示例写法与常见模块的调用思路为后续独立开发2D小游戏打下基础。1. 游戏循环为什么一个while True就能撑起整个游戏很多人第一次读 Pygame 代码看到主循环里只有一个while True加一个事件遍历会觉得这玩意儿简陋得像玩具。但恰恰是这个看似朴素的死循环撑起了所有 2D 游戏的运行时骨架。Pygame 本身只是 SDL 的一层 Python 封装SDL 负责把图像、声音、输入输出这些底层能力暴露出来而真正让这些能力“活”起来的是开发者自己写的那段循环。换句话说Pygame 不替你跑游戏它只给你画布和画笔循环得你自己转。搞懂这个循环为什么必须存在、每一轮里到底发生了什么比记住任何 API 都重要——因为后面所有的 Surface、Event、Rect、FPS 控制全都是挂在这个循环上运转的零件。这份教程把主循环拆得很细正好拿来当切入点。2. Surface 与主窗口为什么屏幕本质上只是一张“纸”2.1set_mode返回的到底是什么几乎所有 Pygame 程序的第一句实质代码都是screen pygame.display.set_mode((400, 400))。新手容易把screen当成“窗口”这个抽象概念但在 Pygame 的模型里它就是一个Surface对象——一块有宽度、有高度、有像素格式的内存区域。窗口只是这块内存在操作系统层面被映射到屏幕上的一个视图。这个区分很关键。因为一旦你接受“屏幕就是一张纸”后面所有绘制操作就都顺了screen.fill()是给整张纸刷底色screen.blit(text, rect)是把另一张小纸片贴上去pygame.display.flip()是把这张纸的内容真正推到显示器上。Pygame 里没有“直接往屏幕上画”这回事一切都要先落到某个 Surface 上。常见的实现思路是set_mode内部会向 SDL 申请一个窗口和一个与之关联的绘图表面返回的 Python 对象持有这个表面的引用。你在它上面做的每一次blit改的都是这块内存只有调用flip或update时SDL 才把内存内容提交给显示后端。这就是为什么你可以在循环里放心地反复绘制而不会看到中间过程的闪烁——双缓冲机制在背后兜着。2.2blit的参数语义与area抠图blit是 Surface 之间搬运像素的核心方法签名大致是blit(source, dest, areaNone, special_flags0)。四个参数各有讲究source源 Surface也就是要被贴上去的那张“小纸片”。dest目标位置可以是(x, y)元组也可以是(x, y, width, height)还可以直接传一个Rect对象。area一个Rect指定只从源 Surface 上抠出这一块来贴。这相当于裁剪做精灵图集sprite sheet时必用。special_flags颜色混合模式比如BLEND_RGBA_ADD做加法混合不传则默认用源颜色直接覆盖目标。# 从一张精灵图集里抠出左上角 32x32 的一帧贴到屏幕 (100, 100) 处 sprite_sheet pygame.image.load(sheet.png).convert_alpha() frame_rect pygame.Rect(0, 0, 32, 32) screen.blit(sprite_sheet, (100, 100), areaframe_rect)这段代码里area的作用就是“只取源图的那一小块”。如果不传area整张图集都会被贴上去。参数special_flags在需要做发光、叠加、半透明特效时才会用到日常绘制保持默认即可。理解这四个参数等于理解了 Pygame 绘制模型的最小闭环源、目标、裁剪、混合。2.3convert()与convert_alpha()的性能账教程里反复出现pygame.image.load(xxx.png).convert()很多人照抄但不知道为什么。原因在于image.load加载出来的 Surface 像素格式是“原始”的可能和显示表面的格式不一致。每次blit时Pygame 都要做一次格式转换这个开销在每帧几十次绘制的场景下会累积成明显的卡顿。convert()把图像转换成与当前显示表面一致的像素格式convert_alpha()则在保留 alpha 通道的前提下做同样的转换。带透明通道的 PNG 必须用convert_alpha()否则透明区域会变成黑色或白色。这是一条血泪经验忘记convert不会报错只会让你的游戏在低配机器上莫名其妙地掉帧排查起来像玄学。3. 事件系统pygame.event.get()背后在做什么3.1 事件队列与get()的消费语义游戏循环里那句for event in pygame.event.get():看起来平平无奇但它承担的是整个输入系统的入口。SDL 在底层维护一个事件队列操作系统把键盘、鼠标、窗口消息塞进去Pygame 的event.get()一次性把队列里当前所有事件取出来返回一个列表。这里有个容易翻车的点get()是消费式的。取过一次队列就空了。如果你在循环里写了两个event.get()第二个大概率拿到空列表。常见的错误写法是在事件遍历里又调用一次get()去判断某个按键结果事件被吞掉表现为“按键偶尔没反应”。正确做法是在一轮循环里只取一次把需要的信息存到变量里后续逻辑读变量。keys_pressed None for event in pygame.event.get(): if event.type pygame.QUIT: pygame.quit() sys.exit() elif event.type pygame.KEYDOWN: keys_pressed event.key # 记录本帧按下的键供后续逻辑使用这段代码把“取事件”和“用事件”分开避免了重复消费队列的问题。event.key是键码常量比如pygame.K_SPACE、pygame.K_LEFT和pygame.locals里定义的那批常量对应。3.2 键盘事件的两种模型KEYDOWN与get_pressed()Pygame 提供了两套键盘输入模型用途完全不同。KEYDOWN/KEYUP是事件式的适合处理“按下的那一瞬间触发一次”的动作比如跳跃、开火、确认菜单。而pygame.key.get_pressed()返回的是一个布尔数组表示当前这一帧每个键是否处于按下状态适合处理“按住持续移动”的场景。# 事件式按一次触发一次 for event in pygame.event.get(): if event.type pygame.KEYDOWN and event.key pygame.K_SPACE: player.jump() # 状态式每帧检查按住就持续生效 keys pygame.key.get_pressed() if keys[pygame.K_LEFT]: player.x - speed if keys[pygame.K_RIGHT]: player.x speed把这两者搞混是新手最常见的翻车之一用KEYDOWN做持续移动结果角色走一步停一步用get_pressed()做跳跃结果按住空格角色疯狂起跳。设计动机上事件式对应“离散动作”状态式对应“连续输入”这是图形编程里通用的划分方式不是 Pygame 独有的怪癖。3.3 鼠标事件与坐标映射鼠标事件比键盘多一层坐标问题。MOUSEBUTTONDOWN事件对象上带event.pos是相对于窗口左上角的像素坐标。如果你的游戏做了缩放或者有摄像机偏移这个坐标不能直接当世界坐标用需要做一次逆变换。教程里对鼠标事件的示例比较基础只演示了取pos和button但在实际项目里坐标映射没处理好点击判定就会整体偏移表现为“按钮要点偏一点才生效”。4. 时间与帧率Clock.tick()为什么能稳住画面4.1 帧率失控的根源如果主循环里什么都不控制循环会以 CPU 能跑多快就跑多快的速度转一秒可能转几千次。这带来两个问题一是白白烧 CPU二是游戏逻辑里的“每帧移动多少像素”会随机器性能剧烈变化同一份代码在快机器上角色飞出去在慢机器上角色爬不动。pygame.time.Clock()就是来解决这个问题的。它的tick(fps)方法会在每帧结束时计算这一帧实际花了多少毫秒然后主动休眠补足到目标帧率对应的时间间隔。比如目标 60 FPS每帧预算约 16.6 毫秒如果逻辑只花了 5 毫秒tick就睡 11 毫秒左右。clock pygame.time.Clock() while True: for event in pygame.event.get(): if event.type pygame.QUIT: pygame.quit() sys.exit() # ... 更新逻辑与绘制 ... clock.tick(60) # 把本帧总耗时补齐到约 16.6mstick的返回值是距上一次调用经过的毫秒数可以拿来做基于时间的运动计算而不是基于帧的固定步长。这是让游戏在不同性能机器上表现一致的关键。4.2get_ticks()、wait()与set_timer()的分工pygame.time模块里还有几个容易混淆的方法。get_ticks()返回从pygame.init()到现在经过的毫秒数适合做计时器、冷却时间判断。wait(ms)是硬阻塞暂停整个程序卡住指定毫秒教程里用它演示“暂停 3 秒再加载图片”但在真实游戏里几乎不该用因为它会冻住事件响应。set_timer(event, ms)则是注册一个周期性事件每隔指定毫秒往事件队列里塞一个自定义事件适合做定时刷怪、倒计时提示这类逻辑。这三者的区别本质上是“读时钟”“睡死”“定闹钟”。用错场景的代价很直接用wait做技能冷却整个游戏在冷却期间无响应用get_ticks做周期事件得自己维护上次触发时间代码啰嗦还容易漏。4.3 时间单位为什么是毫秒Pygame 全系时间以毫秒为单位不是秒也不是微秒。这个选择是精度和整数运算的折中秒太粗做不了 60 FPS 级别的控制微秒精度过剩且在很多平台上时钟分辨率达不到。毫秒在 2D 游戏的时间尺度上刚好够用又避免了浮点误差累积。理解这一点看到tick(60)、wait(3000)、get_ticks()这些数字时就不会犯迷糊。5. 避坑与排查那些不报错但就是不对的瞬间5.1 画面一闪而过程序直接退出现象运行教程里的示例窗口刚出现就消失什么都没看到。原因主循环缺失或者循环体里没有持续的事件处理程序执行完绘制就走到末尾自然退出。Pygame 的窗口生命周期完全由你的循环维持循环结束窗口就关。解决确保有一个while True包住事件遍历和绘制退出条件只放在QUIT事件分支里。教程里那句“如果没有主循环代码运行结果会一闪而过”说的就是这个。5.2 图片加载了但屏幕全黑现象image.load没报错blit也调了屏幕上却什么都没有。原因最常见的是忘了调用pygame.display.flip()或update()。所有绘制都发生在后台 Surface 上不提交就永远不显示。其次是blit的坐标在窗口外或者绘制顺序被后面的fill覆盖了。解决检查循环末尾有没有刷新调用检查blit的目标坐标是否落在窗口尺寸范围内确认fill在blit之前执行。5.3 透明 PNG 出现黑边或白边现象带透明背景的精灵图贴上去透明区域变成黑色或白色方块。原因用了convert()而不是convert_alpha()alpha 通道在格式转换时被丢弃。解决所有带透明通道的图片统一用pygame.image.load(path).convert_alpha()。如果图片本身没有 alpha 通道但需要透明得先在图像工具里处理代码层面补不回来。5.4 按键响应时灵时不灵现象按住方向键移动角色走走停停或者某个按键偶尔完全没反应。原因多半是在同一帧里多次调用了event.get()事件被提前消费或者用KEYDOWN事件去做本该用get_pressed()的持续输入。解决每帧只取一次事件把结果存到变量区分离散动作和连续输入各用各的模型。这个坑我在早期项目里踩过不止一次排查时盯着代码看半天最后发现是多写了一行get()。5.5 帧率设置无效CPU 占用依然很高现象明明写了clock.tick(60)任务管理器里 CPU 还是跑满。原因tick只负责在帧末补睡眠如果单帧逻辑本身耗时超过预算比如每帧都image.load一张大图tick补不回来循环照样全速跑。另一种情况是tick写在了循环外面等于没生效。解决把资源加载移到循环外循环内只做blit和状态更新确认tick在while内部、每帧都调用。6. 从 Rect 到精灵碰撞坐标系统里的设计取舍Rect是 Pygame 里另一个被低估的对象。它不只是“一个矩形”而是把位置、尺寸、碰撞检测、边界运算打包在一起的工具。rect.colliderect(other)做矩形相交判断rect.collidepoint(pos)判断点是否落在矩形内rect.center、rect.left、rect.bottom这些属性让你可以用语义化的方式定位而不是手动算x width / 2。设计上Pygame 选择用轴对齐包围盒AABB做碰撞检测而不是精确的像素级或形状级检测。这是一个明确的取舍AABB 计算极快一次比较几个整数就出结果代价是对不规则形状会误判——两个精灵的包围盒相交了但实际图形可能没碰到。对于扫雷、贪吃蛇、飞机大战这类游戏AABB 完全够用需要更精确时才在 AABB 命中后再做进一步判断。精灵Sprite和精灵组Group则是在 Rect 之上再包一层。Sprite把图像和rect绑在一起Group批量管理更新和绘制groupcollide做组间碰撞。这套抽象的价值在于把“遍历所有对象两两检测”这种 O(n²) 的脏活收进框架让开发者专注在碰撞发生后的逻辑上。常见的实现思路是Group内部维护一个列表update和draw都是对列表的遍历碰撞检测则依赖每个精灵的rect属性——所以自定义精灵时忘了同步rect和图像位置碰撞就会整体错位这是另一个高频翻车点。回过头看这份教程把 Surface、Event、Rect、Time、Draw、Font、Sprite 这些模块按“先能跑起来再逐个拆开”的顺序铺开本质上是在带你走一遍 Pygame 的设计脉络一切皆 Surface循环驱动一切事件和状态分离时间用毫秒统一度量碰撞用 AABB 换速度。这些取舍不是 Pygame 独有的而是 2D 游戏开发里反复出现的通用权衡。我自己的习惯是每次开新项目先把主循环、时钟、事件分发这三件套写死再往上堆功能因为这三样一旦出问题后面所有调试都会变成黑匣子。希望这份拆解帮到你想动手的话把教程里的示例逐个跑一遍、改改参数看反应比读十遍都管用。本文还有配套的精品资源点击获取