Pygame小游戏源码合集:从环境配置到改造打包
简介这份资源是一套基于Pygame实现的Python小游戏合集共收录20款可正常运行的项目涵盖射击达人、动物对决、迷宫、打地鼠、2048、贪吃蛇、扫雷、滑雪、飞翔的小鸟、塔防、坦克大战、飞机大战、外星人入侵、吃豆人、连连看等经典玩法。适合Python初学者在PyCharm中边玩边学快速理解游戏循环、事件响应、精灵碰撞等核心概念也可用于教学演示或作为课程设计的基础模板。压缩包整体约165.68MB内含2000个文件主体为1558个py源码另有pyc编译文件、png图片、html说明、音频及字体素材等目录结构清晰便于按项目检索与二次修改。目前已有1298人学习下载是社区内较受欢迎的Pygame练习合集。除可运行源码外资源还提供完整素材与运行配置读者既能直接体验游戏乐趣也能拆解代码学习按键控制、分数统计、关卡设计等技巧为后续独立开发小游戏打下扎实基础。1. 这套 Pygame 小游戏合集到底能带给你什么从跑通到改出自己的版本手头有一份“20个python小游戏 pygame源码”合集包最先要确认的通常是能不能直接跑跑完之后能不能改。合集的实用价值不在游戏多好玩而在于它把 Pygame 最常见的玩法——按键控制、碰撞计分、多关卡切换——压缩进可以逐行读的短源码里。对刚学 Python、想找一个能复现项目的人来说它是比纯看教程更快的上手路径对已有基础的人来说它又是一个能拆零件用的素材库。但把整个文件夹拖进 IDE 运行第一眼看到的往往不是游戏窗口而是红色报错。这多半不是源码本身的问题而是 Python 与 pygame 的环境没对齐。下面先从环境这一步把坑扫掉再讲源码怎么读、改哪里最能见效。2. 把 20 个 Pygame 小游戏源码在本地跑起来环境、命令与目录约定拿到 pygame 小游戏合集我先做的不是打开游戏代码而是先花五分钟把环境验证通过。很多合集标题写着“亲测源码可正常运行”实际拿到手跑不起来多半卡在环境不一致而不是代码问题。把 Python、pygame 和 IDE 解释器之间的关系捋顺后面 20 个源码才能无脑启动。2.1 安装 Python 与 pygame版本选择和 pip 命令先给结论不必追求最新版 Python装 3.8 到 3.11 之间的一个就够。pygame 用 pip 默认安装在多数电脑上能把依赖补齐如果你不小心用的是很新的 Python 版本先直接装 pygame 试试能 import 就说明没问题。真正麻烦的是系统里同时存在多个 Pythonpygame 装进了 A 环境你用 B 环境去执行源码报错后第一反应往往不是换环境而是怀疑源码坏了。用同一套命令把解释器和包管理的位置对齐python --version pip --version pip install pygame python -c import pygame; print(pygame.version.ver)这条链路的意思是第一行确认你将要用的解释器版本第二行确认 pip 属于同一个解释器第三行安装 pygame第四行再用同一个解释器验证安装结果。如果最后一行能输出版本号说明 pygame 核心已经就位。用 PyCharm 或 VS Code 的朋友还要去项目设置里核对一次解释器路径IDE 默认解释器和终端里的 python 经常不是同一个。另一个常被忽略的细节是pip 默认安装的是 pygame 主包社区分支包虽然沿用 pygame 的接口但维护节奏和编译依赖不同。普通小游戏源码都用标准 import 写法装主包就够不要为了“某篇教程说一定要装 XX”去额外加包扩展包装多了反而给排错添变量。这些年我接过几个本地跑不起来的小游戏根因十有八九不是 pygame 有问题而是多版本 Python 把包装岔了。2.2 用最小脚本验证环境先看到窗口再讨论游戏装完依赖用一个最朴素的方法验证创建一个小窗口持续刷新并响应关闭事件。这段脚本只有十几行却覆盖了 20 个源码全部要用到的三件事初始化、事件循环、画布刷新。如果这段跑不通先不要碰游戏源码问题几乎都在环境层。import pygame pygame.init() screen pygame.display.set_mode((640, 480)) 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((240, 240, 240)) pygame.display.flip() clock.tick(60) pygame.quit()几个要点说明set_mode((640, 480))的元组是窗口宽高数值越大占屏幕越多tick(60)把刷新率限制到每秒 60 帧数字越大刷新越快对 CPU 的占用也随之上升fill((240, 240, 240))后面的 RGB 三元组表示背景色数值范围 0 到 255。窗口标题故意写成中文是想顺手验证当前环境能不能正常显示中文如果这里出现乱码或报错后面游戏里中文字体的坑会更大。跑通这段以后你已经有了一份“最小可运行”的 pygame 项目。它和合集里 20 个游戏的差别只是素材、物品种类和逻辑分支的多少运行骨架没有本质区别。所以环境验证不过关时拿这个脚本去试不要先去翻源码这样能把问题范围缩到最小。2.3 合集目录的组织方式入口脚本与资源文件夹的约定打开一份 20 个游戏 pygame 源码合集你会看到常见的目录套路每个游戏一个子目录子目录里放入口脚本和资源文件夹资源文件夹通常叫 assets、res 或 img 加 sound。少数合集把所有 .py 文件平铺在根目录再配一个总的启动菜单脚本但那种做法对资源管理不友好改动单个游戏时容易互相影响。我把常见的目录层次整理成这样目录或文件作用运行方式游戏A/main.py入口脚本游戏从这里开始执行在游戏A目录下运行 main.py游戏A/assets/图片、音效、字体等素材代码用相对路径加载游戏A/README.txt运行说明和依赖清单先读它再动手根目录/启动菜单.py可选总入口同时加载多个游戏这种布局下最常见的问题是资源文件找不到。.py文件里如果写pygame.image.load(assets/bg.png)它用的是相对路径查找基准是“当前工作目录”而不是“脚本所在目录”。同一个 main.py在游戏 A 目录下运行就没问题在别的目录运行就报找不到文件。在任意游戏头部加两行可以把这个不确定性一次性消除import os os.chdir(os.path.dirname(os.path.abspath(__file__)))这两行把工作目录切到脚本自身所在位置资源路径的相对关系从此固定。这句原理我后面讲避坑还会提到因为资源路径问题是 pygame 运行中最常踩的一个坑。3. 拆开一个小游戏的源码事件循环、精灵对象与碰撞检测的通用骨架20 个 pygame 源码虽然游戏类型不同底层手法几乎都是同一套初始化窗口在while循环里重复处理事件、更新对象、重绘画布。把这条主循环吃透源码里的其余内容就像给同一副骨架填肉。这一章不按某个具体游戏的代码逐行讲而是抽成几乎每个源码都会出现的通用结构。3.1 初始化三步pygame.init()、set_mode 与 Clock 的分工主循环开始前几乎每个源码都有类似的初始化段。最省事的写法是直接pygame.init()它会把 display、mixer、font、joystick 等所有可用模块一次性初始化代价是无法中途观察哪个模块初始化失败。对 20 个源码这种量级的练习项目一次性写法已经够用没必要每个模块单独 try。关键参数集中在窗口尺寸和时钟上pygame.init() screen pygame.display.set_mode((1280, 720)) pygame.display.set_caption(小游戏示例) clock pygame.time.Clock()set_mode((1280, 720))的宽高直接决定游戏视野改成更大的值会让物体相对变小set_caption只影响窗口标题不影响逻辑Clock在循环里调用tick()决定每秒最多刷新多少帧。这里要提一下pygame.init()的内部行为比想象中“黑匣子”它不只初始化显示设备还会尝试打开音频设备。如果当前机器没有可用的音频输出某些环境会直接导致初始化失败。遇到这种情况常见做法是把pygame.init()拆成pygame.display.init()和pygame.font.init()逐个调用暂时跳过 mixer先让窗口能出来。这不是玄学是有故障现场时最直接的排查思路。3.2 事件循环pygame.event.get() 为什么必须放在 while 里每个小游戏的主循环结构都接近这个模式running True while running: for event in pygame.event.get(): if event.type pygame.QUIT: running False # 更新游戏逻辑 # 绘制画面 pygame.display.flip() clock.tick(60) pygame.quit()理解“事件队列”就能看懂这段pygame.event.get()会把队列里积攒的事件一次性取走不调用它窗口永远不会收到关闭事件操作系统会判定程序未响应。因此事件处理必须放在循环体内循环每秒跑几十上百遍事件随时进来随时被取走。事件对象里有type和key两个常用字段分别标识事件类型和按键码。键盘控制的游戏通常这样读取按键for event in pygame.event.get(): if event.type pygame.QUIT: running False elif event.type pygame.KEYDOWN: if event.key pygame.K_LEFT: player.move(-10, 0) elif event.key pygame.K_RIGHT: player.move(10, 0)这段说明两个参数K_LEFT、K_RIGHT是 pygame 预置的按键常量move方法接收的-10和10是像素步长步长越大每次按键移动越远。这里有个关键差异按住按键不放时KEYDOWN不会连续触发它只在按下瞬间触发一次。想实现“按住持续移动”要用pygame.key.get_pressed()函数它返回一个布尔数组表示当前哪些键处于按住状态适合放在普通逻辑判断而不是事件循环里用。3.3 精灵与 Rect 碰撞检测几何矩形替代逐像素判断小游戏里几乎所有可见对象都能拆成“图像 矩形”两件套图像负责长什么样矩形负责在哪里。pygame 的Sprite虽然封装了完整概念但不一定要用不少源码项目直接用Surface和Rect也能写得很清楚。定义一个敌方对象最常见的写法是这样class Enemy: def __init__(self, x, y): self.image pygame.Surface((60, 60)) self.image.fill((255, 0, 0)) self.rect self.image.get_rect() self.rect.topleft (x, y)这里self.image是程序里显示的那块方形fill((255,0,0))给它填上红色get_rect()返回一个矩形对象宽高等于 Surfacetopleft控制矩形在窗口里的位置。移动时直接改self.rect.x或self.rect.y画面刷新时按新位置把 image 画出来。碰撞判断用矩形相交实现这是源码里出镜率最高的逻辑if enemy.rect.colliderect(player.rect): game_state overcolliderect接收另一个 Rect 对象返回 True 表示两个矩形区域存在重叠。它的边界是矩形边缘对圆形或不规则物体显得“宽松”但对小游戏已经够用。想要更精确的碰撞可以用pygame.mask.Mask做像素级检测代价是计算成本高20 个合集项目大多不需要走到这一步。改难度、换对象的时候注意别把精灵对象的rect和实际绘制位置拆开一旦出现视觉没碰到却判击中多半是rect没跟着坐标同步。3.4 计分、生命值与游戏状态一个状态变量管理整场游戏源码里最容易写乱的地方是多个布尔量和多个计数器混杂。常见做法是引入一个状态变量状态同时只有一个取值分支条件就不会互相打架game_state menu # menu / playing / over if game_state menu: # 显示开始按钮按空格进入 playing pass elif game_state playing: # 更新物体位置检测碰撞 pass elif game_state over: # 显示分数按 R 重新开始 pass刚才碰撞代码里把game_state改成over就是这条链路的一部分。把状态跳转写进一个函数后面增加暂停、切换关卡会容易很多。计分部分在 20 个源码里多用score 1这类局部变量朴素但直观。重新开始时要记得把分数归零、对象复位、状态回到playing这步漏了会出现“看似新一局实际旧状态还在跑”的怪现象排查时很难凭感觉发现。判断逻辑按状态拆分以后每条分支短小清晰新手读源码也能很快找到当前正在执行的游戏环节。4. 动手改第一份游戏源码四个改完立刻见效的入口源码能跑通下一件大概率要做的事是“改出一个自己版本”。新手最容易踩的陷阱是钻进事件循环里随手改数字越改越乱。反过来先从配置区、难度曲线、素材路径和函数边界这四个入口下手改动小又能马上看到效果还不会把代码拆晕。4.1 先找全局配置区速度、血量、生成频率是最合适的改点好的 pygame 小游戏源码会在文件开头集中定义一批常量比如PLAYER_SPEED 5、ENEMY_GAP 30、LIVES 3。改这些值是最安全的开局。这块在源码里可能带注释也可能不带先搜索SPEED、GAP、LIVES这类词就能定位。改动前先按表格估算手感参数常用范围调大的后果调小的后果玩家移动步长4 到 10 像素反应变难容易冲出屏幕操作迟钝敌人生成间隔30 到 100 帧敌袭稀疏游戏偏闲满屏对象性能下降初始生命数3 到 5通关太容易失去反馈挫败感强以玩家移动为例源码里多是这样一行在更新位置player.rect.x PLAYER_SPEED if direction 1 else -PLAYER_SPEED改成手感最快的方式是动常量PLAYER_SPEED 6 # 原来是 4调大一点试试这样一个改动全游戏生效不会破坏方向判定。注意不要把这个常量改成负数或 0很多逻辑分支依赖它做方向判断改出乱子要花好几倍时间排查收益却很低。想验证改得合不合理跑一段时间观察玩家是否频繁撞到屏幕边缘再微调一两步。4.2 用帧数做难度阶梯每隔 N 秒提升一次难度静态参数改完下一个有感知的改动是“随时间变难”。初学者常会写time.sleep(1)来拉长间隔这是 pygame 里最容易卡死界面的做法——sleep 会阻塞整个主循环窗口直接无响应。正确做法是把它换算成帧数clock.tick(60)意味着每秒约 60 帧那么 30 秒就能用30 * 60 1800帧来表达。frame_count 1 if frame_count % (30 * 60) 0: # 每 30 秒触发一次 ENEMY_GAP max(20, ENEMY_GAP - 5) # 间隔最小缩到 20 帧 frame_count 0 # 重置周期避免后续误判参数说明frame_count每帧加一30 * 60是“每秒 60 帧乘以 30 秒”的帧数取模运算在计帧到整周期时写为 True执行难度升级max(20, ...)做下限保护防止间隔一直缩小导致一帧刷出整个屏幕。这种阶梯写法依赖帧率稳定所以前提是tick(60)设置在合理数字。如果游戏掉帧厉害帧数周期会被拉长难度上升比预期慢这是计时和逻辑帧绑定时的天然短板小游戏项目可以接受。4.3 替换图片和音效路径、格式与 convert_alpha 的用法换素材是满足感最强的小改动。加载图片的代码基本都是这两行background pygame.image.load(assets/background.png).convert() player_img pygame.image.load(assets/player.png).convert_alpha()参数区别很关键.convert()把图片转成与当前显示模式匹配的像素格式绘制时更快但会丢掉透明通道需要保留角色边缘透明的 PNG 贴图必须用.convert_alpha()。如果图片是透明背景用 convert 会把透明区域填充成黑色视觉上就像角色“背着黑底”。音效加载相对朴实sound pygame.mixer.Sound(assets/click.wav)wav 是默认兼容性最好的格式ogg 次之mp3 在部分平台依赖解码器会出现“代码不报错、但没声音”的怪状态。替换素材时留意格式别只盯着图片路径改。另外注意保持资源文件为纯英文命名中文文件名在某些版本里会触发编码问题后面避坑章会展开。4.4 把重复逻辑收进函数给后续加功能留余地合集中的单个源码往往只有几百行难免把同样的重置逻辑写在多个分支里。比如重新开始、进入下一关都要复位玩家位置和清空敌人列表这种代码抽成函数最省心def reset_round(): player.rect.topleft (START_X, START_Y) enemy_list.clear() score 0 game_state playing把重复代码收进reset_round后后面加“重新开始”或“下一关”只需调用一次复位逻辑只维护一处。这是从“能改”到“可维护”的转折点。20 个源码不必个个都重构自己打算长期改的那几款建议都先按这个思路捋一遍。函数边界清楚以后加暂停、加音效开关这类功能会轻松很多因为你不会再同时面对三个位置维护同一段逻辑的尴尬。5. 本地运行避坑Pygame 小游戏常见的 5 个翻车点与排查方法源码能跑起来的条件其实不复杂踩的坑却高度重复。下面 5 条是从常见运行问题里挑出来的高频项每一条按“现象、原因、解决”三层写方便你对号入座。5.1 ModuleNotFoundErrorpython 与 pygame 装进了两个环境现象终端执行python xx.py出现ModuleNotFoundError: No module named pygame但pip install pygame又显示已经安装。原因系统里存在多个 Python 环境pygame 被装进 A 环境的 site-packages运行脚本用的却是 B 环境。这个问题在多版本 Python 和 Anaconda 与系统 Python 混装的机器上特别普遍IDE 里的解释器路径和终端里的python经常不是同一个。解决不要再用裸pip install改用与解释器强绑定的python -m pip install pygame。python -m pip会把 pip 指向当前python对应的环境装完再执行python -c import pygame验证确认 CLI 和 IDE 用的是同一个环境。如果项目里用 virtualenv先激活虚拟环境再装再跑顺序不能反。5.2 素材读取失败路径里有中文或工作目录不对现象代码引用assets/bg.png运行时报pygame.error: Couldnt open assets/bg.png检查文件确实存在。原因第一是工作目录不是脚本所在目录相对路径起点错了第二是文件路径包含中文或特殊字符部分环境下 pygame 打开这类路径时编码处理会出问题。解决先在脚本开头用os.chdir(os.path.dirname(os.path.abspath(__file__)))固定工作目录再把素材文件命名统一改成纯英文和数字整个素材目录最好也放在纯英文路径下。写路径时统一用正斜杠/Windows 的反斜杠需要转义写起来容易乱。素材文件用英文命名还能顺便减少团队协作时互相覆盖的问题。5.3 窗口全黑或只有标题栏画面一动不动现象窗口能弹出来但内部一片空白或者标题栏能看但画面完全不更新游戏根本无法交互。原因事件循环里遗漏了pygame.display.flip()或pygame.display.update()绘制到后台缓冲的内容没有推到屏幕上也可能是screen.fill((背景色))不在每帧重绘的范围里画面残留让人以为冻住了。解决把事件循环末尾的刷新顺序固定成三步先screen.fill((背景色))再画所有游戏对象最后pygame.display.flip()。顺序颠倒会出现黑屏或残影。同时检查循环里有没有误用time.sleep()它一旦阻塞主循环画面刷新和事件处理全部停止表现就是窗口“死了”。5.4 游戏速度忽快忽慢参数怎么调都对不上现象同一份代码在某些机器很快在某些机器很慢改速度参数看不出规律性的变化。原因pygame 的tick(N)是帧率上限不是固定帧率。如果逻辑更新没有按帧计算高帧率机器上循环跑得更频繁游戏就更快。另一个因素是像高分屏或垂直同步这类显示设置把实际帧率拉到与显示器刷新率一致参数效果随之漂移。解决把逻辑更新绑定到“累计帧间隔”而不是“物理时间”。最简单的是先锁死clock.tick(60)再用帧计数控制事件周期也就是 4.2 节那种写法。进阶做法是读dt clock.tick(60) / 1000.0得到上一帧耗时所有位移乘以 dt这样修正好但改动量大小游戏合集优先把不变因素固定住不要一上来就引入时间步进。5.5 退出时报错 video system not initialized现象游戏窗口正常关闭但退出后终端里出现pygame.error: video system not initialized或者第二次运行时直接闪退。原因调用顺序出了问题。常见的是pygame.quit()之后还调用了pygame.display.set_mode()或pygame.display.flip()显示系统已经被关闭再操作就会抛错第二种常见情况是同一进程里多次 init 和 quit 交错资源状态没有清理干净。解决把pygame.quit()放在进程最结尾且只调用一次。pygame.quit()之后绝对不要再碰任何 display 相关函数。如果做“退出菜单返回桌面”这样的功能先切换状态再退出不要跳着调用。确保退出路径只有一条没有绕过quit()的分支能省掉不少奇怪闪退。6. 把改好的小游戏变成能发出去的作品打包与存档当 20 个源码都被你跑过并改出自己版本后接下来最常做的是把它发给别人玩。pygame 小游戏在目标机器上没有 Python 环境也能跑常见做法是用 PyInstaller 打包。命令不复杂但存档路径要单独处理否则一升级就丢最高分。6.1 用 PyInstaller 打包成单文件 exe在项目目录下执行pip install pyinstaller pyinstaller -F -w main.py-F表示把所有依赖打进单个文件-w表示不显示黑色控制台窗口。打包完的程序在dist目录下首次启动会比源码版慢一些因为要解压运行库。打包时注意资源文件不会被默认包含需要在 spec 文件里追加或者用固定的“脚本目录 相对路径”写法。我通常在打包前把os.chdir(os.path.dirname(os.path.abspath(__file__)))放进入口脚本把素材放到脚本目录下的assets文件夹里这样打包后的路径行为与源码运行一致减少“开发时正常、打包后找不到图”的落差。6.2 用 Json 保存最高分升级不丢记录游戏存档用 json 就够不必引数据库。把存档写到用户主目录而不是打包程序的目录常见做法是import json import os def save_best_score(score): path os.path.join(os.path.expanduser(~), pygame_best.json) with open(path, w) as f: json.dump({best: score}, f)os.path.expanduser(~)会给出当前用户的根目录普通用户对该目录有写权限。把存档写在打包程序旁边是个隐藏坑程序被升级覆盖后存档跟着没了或者在权限受限的目录里静默失败。读档时用os.path.exists(path)判断文件是否存在不存在就返回 0。每改完一个游戏我习惯把节奏参数和素材命名统一记在源码头部二十个小游戏轮着维护时不会出现“改了 A 忘了 B”。这个习惯帮我省了不少重复排查希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →