拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

Pygame小游戏源码合集:从环境配置到改造打包

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 就说明没问题。真正麻烦的是系统里同时存在多个 Python,pygame 装进了 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()返回一个矩形对象,宽高等于 Surface,topleft控制矩形在窗口里的位置。移动时直接改self.rect.x或self.rect.y,画面刷新时按新位置把 image 画出来。

碰撞判断用矩形相交实现,这是源码里出镜率最高的逻辑:

if enemy.rect.colliderect(player.rect): game_state = "over"

colliderect接收另一个 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 ModuleNotFoundError:python 与 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: Couldn't 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”。这个习惯帮我省了不少重复排查,希望帮到你。

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

返回列表