ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从经典小游戏源码拆解 pygame 游戏开发核心套路

从经典小游戏源码拆解 pygame 游戏开发核心套路 简介13个基于pygame的Python小游戏源码集合面向想入门Python游戏开发的学习者适合用来理解pygame常用模块与游戏循环、事件响应、碰撞检测、音效播放等核心概念。压缩包共73个文件以13个.py游戏主程序为核心配合45张png素材图、7个wav和4个ogg音效、2个mid背景音乐以及1个ttf字体和1个txt关卡配置文件整体大小仅1.18MB。资源覆盖四子棋、推箱子、拼图、记忆翻牌、俄罗斯方块、贪吃蛇等经典玩法每个游戏都有独立目录结构便于读者逐个运行和拆解学习。作者douluo998当前已有903人学习下载适合作为课程设计、自学练手或教学演示的参考资料。1. pygame-games 的 13 个游戏源码真正的读法是从跑通到拆开拿到一套「Python 游戏源码 pygame-games13个」时多数人先找画面最华丽的那个跑两分钟就关掉。这份源码的价值不在玩而在拆贪吃蛇、打砖块、弹球这些经典小游戏背后是同一套 pygame 主循环、碰撞检测与状态切换逻辑读通一个其余十二个的骨架基本就掌握了。适合刚学完 Python 语法、想用项目验证自己的人也适合写过业务代码、想看看游戏循环怎么组织状态的工程师。接下来按环境搭建、循环骨架、模块拆解、改动验证四步走每一步都有可直接照做的命令与代码。2. 跑 pygame-games 前先把环境装对venv、pygame 安装与 failed to build 排查2.1 用 venv 隔离环境装 pygame 时避开系统 Python 的坑很多 Linux 发行版自带 Python 3.9 或 3.11macOS 和 Windows 上又常同时存在多个解释器直接执行pip install pygame装到的包很可能不属于你写代码时用的那个 Python。这也是「明明装了import 却报 ModuleNotFoundError」最普遍的原因。常见做法是每个项目建一个虚拟环境python3 -m venv .venv source .venv/bin/activate python -m pip install --upgrade pip python -m pip install pygame第一行创建项目内的 .venv 目录第二行激活环境Windows 下换成.venv\Scripts\activate之后所有 pip 安装都落在 .venv 内与系统级包互不干扰。用python -m pip而不是裸pip能保证 pip 和当前解释器绑定升级 pip 也不是例行公事旧版本在选择 pygame 发行包时可能放弃预编译 wheel 转而编译源码直接触发下文那个高频报错。在 VS Code 里打开项目后用命令面板切换解释器指向 .venv 下的 python终端和调试器才会共享同一套依赖。激活后执行python -c import pygame; print(pygame.version.ver)能打印版本号就说明安装完成。2.2 pygame 安装报 failed to build pygame when getting requirements to build wheel 的三条排查路径这条报错是搜索记录里的常客出现时 pip 已经从预编译 wheel 路线退回到源码包安装于是进入构建准备阶段。多数情况下不是 pygame 本身的问题而是下面三类原因。可能原因报错特征处理方式pip 版本过旧日志停留在 building wheel长时间无进展python -m pip install --upgrade pip后重装Python 版本过新3.13 等新版本在部分平台没有现成 wheel改用 3.10 或 3.11 重建 venv 再装Linux 缺 SDL 开发头文件编译期提示找不到 SDL.h、SDL2.h安装 -dev 包后重试Debian/Ubuntu 系缺依赖时执行sudo apt install libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev libsdl2-ttf-dev装完再执行 pip install pygame。pygame 2.x 对 Windows、macOS 和主流 Linux 都已提供预编译包优先检查上面第一、二行镜像源是最后才考虑的因素。注意报错信息里出现 wheel 字样时不要直接去源码目录里手动编译。先确认 pip 和 Python 版本再补系统依赖按顺序排查能省下大量时间。2.3 最小验证脚本查看版本、渲染驱动和无窗口运行环境装完后先跑一个最小脚本确认渲染链路通畅避免把「代码问题」误判成「环境问题」import pygame import sys pygame.init() print(pygame version:, pygame.version.ver) screen pygame.display.set_mode((800, 600)) print(display driver:, pygame.display.get_driver()) pygame.quit() sys.exit(0)pygame.init()初始化所有已导入的 pygame 模块set_mode((800, 600))创建窗口并返回 Surface后续所有绘图都画在这块画布上pygame.display.get_driver()返回当前渲染后端Linux 桌面常见 x11 或 waylandWindows 上为 windows。如果服务器或容器里没有显示环境set_mode会抛出pygame.error: video system not initialized。这时可以加一行环境变量再跑SDL_VIDEODRIVERdummy python check_pygame.pydummy 驱动不依赖窗口系统能完整执行主循环适合放进 CI 做逻辑层面的自动化验证但 dummy 驱动下某些依赖硬件加速的行为会和真实窗口有差异本地开发还是要在真实窗口里做最终确认。这套环境验证做完再打开 pygame-games 的任意一个游戏文件跑不起来的概率就很小了。3. 13 个 pygame 游戏源码通用的主循环事件、更新与绘制3.1 五段式主循环骨架从最小的可运行程序看起无论贪吃蛇、打砖块还是像素射击pygame-games 里 13 个游戏的主循环都遵循同一套结构初始化、取事件、更新逻辑、绘制、翻页。最小骨架如下import pygame def run(): pygame.init() screen pygame.display.set_mode((800, 600)) pygame.display.set_caption(skeleton) clock pygame.time.Clock() running True while running: for event in pygame.event.get(): if event.type pygame.QUIT: running False # 更新阶段移动、碰撞、计分都放在这里 # 绘制阶段先清屏再逐个画对象 screen.fill((0, 0, 0)) pygame.display.flip() # 把绘制结果提交到窗口 clock.tick(60) # 锁帧率每秒最多 60 帧 pygame.quit() if __name__ __main__: run()pygame.event.get()一次性取出队列里的所有事件返回列表QUIT是窗口关闭按钮触发的系统事件。screen.fill((0, 0, 0))用纯黑清空画布不清屏的话上一帧的画面会残留。display.flip()做双缓冲翻页把离屏内容整体呈现避免逐像素绘制时闪烁clock.tick(60)让循环按 1/60 秒的节奏推进不打满 CPU。新手改动源码时最常见的性能退化是在绘制阶段反复创建新 Surface比如每次screen.blit(pygame.Surface(...), ...)正确做法是初始化时建好、循环里只改内容。3.2 帧率锁定之外还要算 dt时间步长让移动速度与帧率解耦固定tick(60)在简单游戏里够用但显示器刷新率、窗口拖动、后台卡顿都会让实际帧数波动。按「每帧移动 5 像素」写的代码在 30 帧和 60 帧下速度差一倍。更通用的写法是用两帧间隔 dt 乘速度clock pygame.time.Clock() prev pygame.time.get_ticks() while running: now pygame.time.get_ticks() dt (now - prev) / 1000.0 # 转成秒 prev now player_x 300 * dt # 每秒移动 300 像素 clock.tick(60)pygame.time.get_ticks()返回从pygame.init()起累计的毫秒数dt是上一帧到这个间隔的秒数60 帧时约 0.016730 帧时约 0.0333。300 * dt让单位时间内的位移恒定帧率只影响采样密度不影响总路程。13 个源码里偏入门向的游戏多用固定步长修改时如果发现高速物体穿墙或速度不对优先把固定步长改成 dt 模式而不是去调 tick 的数值。3.3 事件查询与按键轮询两类输入写法的分工pygame 的输入分两个层面离散的事件和持续的键状态。读源码时会同时看到两种写法它们各有适用场景。输入方式API适用场景典型动作离散事件event.type pygame.KEYDOWN按下瞬间触发一次跳跃、开火、菜单确认持续轮询pygame.key.get_pressed()按住期间持续生效方向键移动、加速get()返回的 KEYDOWN 事件是系统按一定频率重复派发的如果用它持续移动移动节奏会受系统键盘重复延迟影响get_pressed()返回一个布尔数组下标对应键位常量比如keys[pygame.K_SPACE]每一帧的当前状态都准确。射击和跳跃动作要想「只触发一次」还需要记录上一帧的按键状态检测从 0 变 1 的上升沿keys pygame.key.get_pressed() if keys[pygame.K_SPACE] and not space_held: shoot() space_held keys[pygame.K_SPACE]这份 13 个游戏的源码里方向控制几乎都用get_pressed()菜单确认和开火用事件或上升沿检测。按这个分工去读代码意图就清楚得多。4. 从 pygame-games 拆可复用模块贪吃蛇坐标、打砖块碰撞与状态机13 个游戏虽然是独立小项目但实现层面高度雷同基本可以浓缩成三类重复出现的模块模式。读源码时先认模式再抠细节效率会高很多。模块模式代表游戏可复用到的地方网格坐标建模贪吃蛇、扫雷逻辑坐标与像素坐标分离矩形最小重叠轴碰撞打砖块、弹球碰撞方向判断与反弹显式状态机菜单/暂停/结算输入与绘制的状态分支下面按这三个模式拆开讲每个模式都能直接从 13 个游戏源码里摘出来移植到自己的项目。4.1 贪吃蛇源码里的网格坐标与方向队列贪吃蛇是 13 个游戏里数据模型最直观的一个蛇身是坐标列表移动是头插尾删。典型实现CELL 20 GRID_W, GRID_H 40, 30 snake [(10, 10), (9, 10), (8, 10)] direction (1, 0) def move(): head snake[0] new_head (head[0] direction[0], head[1] direction[1]) snake.insert(0, new_head) snake.pop() # 没吃到食物时尾部同步移除 def grow(): head snake[0] new_head (head[0] direction[0], head[1] direction[1]) snake.insert(0, new_head) # 吃到食物时不 pop长度加一坐标系以格子为单位绘制时才乘 CELL 换算成像素逻辑层与渲染层分离direction 用单位向量表示省去角度换算。两个细节读源码时值得留意。第一连续两次方向输入可能导致蛇头反向直接顶进身体解决办法是给方向更新加锁一帧内只接受一次变更changed False def turn(new_dir): global direction, changed if changed: return direction new_dir changed True每帧开头把changed重置为 False这样玩家即使一帧内按了两个方向键也只有一个生效。第二自碰撞检测只需要比较蛇头与新蛇身if new_head in snake[1:]: running Falsein对列表是 O(n) 扫描但蛇身通常只有几十格每帧一次完全可接受不必提前优化成 set。边界判断要在 insert 之前完成否则越界的蛇头先入队绘制时会出现索引和坐标对不上的脏状态。4.2 打砖块里的矩形碰撞最小重叠轴决定反弹方向pygame 的 Rect 自带colliderect()只能告诉你两个矩形相交不能告诉你是从哪个方向撞上的。打砖块要求球拍、砖块、墙壁各有不同的反弹行为常见做法是比较两轴方向的重叠量重叠小的那侧就是碰撞法线方向def reflect(ball: pygame.Rect, brick: pygame.Rect): left ball.right - brick.left right brick.right - ball.left top ball.bottom - brick.top bottom brick.bottom - ball.top min_x min(left, right) min_y min(top, bottom) if min_x min_y: return (-1 if left right else 1, 0) return (0, -1 if top bottom else 1)四个值分别表示球从左、右、上、下侵入砖块的距离。取 x、y 两轴的最小值得到侵入最浅的方向球应该在这个轴上反弹。把返回的(dx, dy)乘到球的当前速度向量上速度方向就翻到正确一侧。这个函数在打砖块、乒乓、弹球类源码里有各种变体但核心都是最小重叠轴思想后续做三消、消除类游戏里的方块判定也用得上。注意球速超过砖块边长时一帧内球会整个穿过砖块重叠量为 0反弹丢失。通用解法是把一帧的位移拆成若干小段分段检测碰撞这是「快速物体穿墙」的标准处理方式。4.3 用状态机管菜单、暂停与结束替换散落的 if 分支13 个游戏里只要稍带菜单或暂停功能主循环里就会出现成串的 if 判断。更干净的做法是显式的游戏状态机from enum import Enum, auto class State(Enum): MENU auto() PLAYING auto() PAUSED auto() GAME_OVER auto() state State.MENU def on_keydown(key): global state if state State.MENU and key pygame.K_SPACE: state State.PLAYING elif state State.PLAYING and key pygame.K_p: state State.PAUSED elif state State.PAUSED and key pygame.K_p: state State.PLAYING elif state State.GAME_OVER and key pygame.K_r: reset() state State.MENU状态枚举让每个分支只处理当前状态关心的按键菜单里按 P 不会误触发暂停结束后按方向键不会移动蛇。更新与绘制函数同样按状态分支PAUSED 时跳过 update 但保留画面GAME_OVER 时只画结算文字。这个结构能直接迁移到任何 pygame 项目也是从读源码过渡到写自己游戏时最值得先抄走的骨架。5. 验证与进阶耗时采样、高频异常定位和一份最小改造清单5.1 用时间戳差值采样每帧耗时定位性能瓶颈改完代码先别急着加功能用 pygame 自带的计时能力做一次采样import pygame pygame.init() clock pygame.time.Clock() samples [] prev pygame.time.get_ticks() for _ in range(120): clock.tick(60) # 仍然锁帧模拟真实运行 now pygame.time.get_ticks() samples.append(now - prev) prev now avg sum(samples) / len(samples) p99 sorted(samples)[int(len(samples) * 0.99)] print(favg {avg:.1f} ms, p99 {p99} ms)平均值看整体负载p99 看卡顿尖峰。p99 明显高于平均值时说明存在偶发重计算优先查绘制阶段是否反复创建 Surface以及碰撞检测是否从 O(n) 退化成了 O(n²)。5.2 三个高频异常与对应排查口异常触发场景排查方向pygame.error: Unable to open file图片或音效加载失败用os.path.join拼资源路径别依赖工作目录ValueError: subsurface rect outsideblit 源矩形越界打印 Rect 的宽高与目标 Surface 尺寸对比事件队列积压导致 CPU 高主循环里没有及时取事件每帧开头必须event.get()取空队列5.3 最小改造把贪吃蛇改成双人模式对照源码做一次最小改造比新写一个游戏更能验证理解。复制一份蛇状态为 snake_a 和 snake_b分别绑定 WASD 与方向键每帧先判断两条蛇的蛇头坐标是否重合再判断各自新头是否撞进对方蛇身食物改为同时刷新两份各自计分。改完用 5.1 的方式采样 120 帧对比改动前后的平均耗时确认双蛇更新没有把 O(n) 操作改成 O(n²)。这套「认模式 → 拆模块 → 改参数 → 采样验证」的流程跑通后再去动源码里其他游戏就有了套路想用 pygame 做三消时把网格坐标那套逻辑扩展成 9×9 棋盘索引换算、相邻交换、消除判定都能从这 13 个源码里直接改出来。本文还有配套的精品资源点击获取
返回列表