简介:一套基于pygame的20款小游戏合集,面向Python初学者、游戏开发爱好者及编程教学场景。合集涵盖射击达人、贪吃蛇、2048、扫雷、飞机大战、外星人入侵、塔防、坦克大战等经典玩法,每个游戏均有完整可运行的Python源码。使用PyCharm等IDE配置好环境后即可直接运行,适合通过拆解源码学习Python语法、pygame图形与事件处理、游戏循环设计等知识。压缩包共2000个文件,以1558个py源码文件为核心,辅以pyc编译文件、png图片素材、wav音效和html辅助文档等,整体大小约165.68MB,目录结构清晰便于按项目检索。目前已有1298人下载学习。这套资源还包含地图、关卡配置及字体文件,每个小游戏独立成模块,既可作为入门练手项目,也能为课程设计或毕业设计提供现成参考,帮助读者快速掌握从零到一开发小游戏的完整流程。
1. pygame小游戏全集:不只有20个案例,更是一套能跑的入门教材
你是不是也下过那种“20个python小游戏”的源码包,解压一看,20个文件夹整整齐齐,心里想着这下能边玩边学了。结果双击运行,一半是黑屏闪退,一半是报ModuleNotFoundError,最后只能默默关掉。这类 pygame 小游戏全集的价值,恰恰不在于那20个游戏好不好玩,而在于它把pygame的核心机制——事件循环、精灵、碰撞检测、音效播放——分散到了一个个可以独立运行的例子里。你能把这20个全部跑通,pygame就入门了;能动手改其中几个,说明你已经有了写游戏的基本盘。这篇笔记就按我的调试习惯,带你把下载下来的全集从环境到运行整个捋一遍,顺便把最容易翻车的几个坑提前告诉你。
2. 从零跑通pygame小游戏:环境、目录与最小启动代码
2.1 为什么选pygame做小游戏全集:开发效率与上手曲线的取舍
pygame是基于SDL的Python 2D游戏开发库,脚本语言写逻辑,C语言跑底层,所以一个几百行的main.py就能撑起一个完整的小游戏。市面上的“python小游戏源码”绝大多数都用pygame,不是因为它是唯一选择,而是因为它满足两个条件:第一,安装简单,一条pip命令就能用,不会像某些引擎那样还要下载IDE和构建工具;第二,渲染方式足够直接——你往Surface上画图,翻转给屏幕,整个流程一眼到底。
相比Pygame Zero、pyglet这些替代品,pygame的资料量和案例量是碾压级的。你搜“python小游戏”,搜“pygame源码”,返回的结果里大部分都能直接跑。对入门者来说,遇到问题时能搜到答案比框架本身更先进重要得多。pygame的性能做2D小游戏也完全够用,60帧环境下CPU占用并不夸张,不需要去折腾OpenGL。更重要的是,这类合集通常不带图形化编辑器,所有逻辑都明明白白写在源码里,学的人能直接看到“按下按键到画面变化”的完整链路。这是Unity或Godot做不到的——它们会把逻辑埋在节点和信号槽里,新手根本不知道从哪看起。
当然pygame也有边界:不支持3D、没有物理引擎、音频功能相对基础,但小游戏全集的定位本来就是“学会游戏循环和基本交互”,这些边界不是问题。所以拿到一套pygame小游戏全集,本质上就是拿到了一套覆盖面很全的入门题目,贪吃蛇、飞机大战、打地鼠、记忆翻牌这些经典玩法,正好把游戏开发里的坐标映射、碰撞检测、状态管理、计时器全部过一遍。
2.2 搭建可复现的运行环境:Python版本与pygame安装的注意事项
很多合集下载后跑不起来,不是源码不行,而是环境不对。我见过的坑包括:系统里装的是Python 2、pygame还停留在1.9、或者压根没装pygame。要避免这些,我建议第一步就建一个干净的虚拟环境,把依赖隔离起来,也让以后删掉重来不费劲。
在项目根目录打开终端,按顺序执行下面这些命令:
# 创建虚拟环境,避免污染系统Python python -m venv pygame_env # 激活虚拟环境(Windows) pygame_env\Scripts\activate # 激活虚拟环境(macOS/Linux) source pygame_env/bin/activate # 安装pygame,建议指定版本以保持稳定 pip install pygame==2.5.2 # 验证安装 python -c "import pygame; print(pygame.__version__)"这里指定pygame==2.5.2是为了可复现。如果你直接pip install pygame,装到最新版通常也没问题,但老源码里如果用了pygame.sprite的内部写法,版本差异会引出一些奇怪的警告。另外,如果你的Python是3.8到3.11之间的版本,pygame 2.x都很稳定;3.12之后部分Linux发行版的wheel有兼容问题,建议还是用3.10左右的版本最省心。
如果下载速度慢,可以加一个镜像源参数,国内环境一般这样装:
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple pygame装完以后,我习惯先跑一个最小窗口,确认pygame本身能正常工作。这个最小代码也是以后所有调试的底子,请记住它:
import pygame pygame.init() screen = pygame.display.set_mode((640, 480)) pygame.display.set_caption("test") running = True while running: for event in pygame.event.get(): if event.type == pygame.QUIT: running = False pygame.display.flip() pygame.quit()这段代码的逻辑很简单:pygame.init()初始化所有模块,set_mode创建窗口,事件循环里等待QUIT事件,flip把后台缓冲送到屏幕。如果这个窗口能稳定打开,说明pygame环境没问题,剩下的事就是检查源码本身了。
2.3 读懂源码包的目录结构:main.py、素材与模块怎么分布
拿到合集先别急着运行,花两分钟看目录。常见的结构是这样:
pygame_games/ ├── launcher.py ├── requirements.txt ├── 01_snake/ │ ├── main.py │ ├── assets/ │ │ ├── images/ │ │ └── sounds/ │ └── data.py ├── 02_plane_war/ │ ├── main.py │ └── assets/ └── 03_memory/ └── main.py每个游戏目录里至少有一个main.py,图片和音效放在assets下,data.py用于集中管理颜色、速度、字体这类参数。有些合集还会把公共模块抽到根目录的common.py里,供多个游戏共用。你不需要读懂所有文件,但至少要知道每个游戏应该包含哪些东西——如果某个游戏目录里只有main.py,那它多半是纯绘制类游戏,不需要素材;如果素材文件夹存在但文件缺失,运行时会直接报错。
运行单个游戏最直接的方式是进入目录再执行:
cd 01_snake python main.py但合集如果没有提供launcher.py,每次都要手动进目录,20个游戏会把人逼疯。我一般会自己写一个启动器,自动扫描子目录里的main.py,做成菜单。这也是拿到新合集后的第一个实操:
import os import sys import subprocess def find_games(root='.'): result = [] for name in os.listdir(root): sub_dir = os.path.join(root, name) if os.path.isdir(sub_dir) and os.path.isfile(os.path.join(sub_dir, 'main.py')): result.append(name) return sorted(result) if __name__ == '__main__': games = find_games() if not games: print("没找到任何含 main.py 的子目录") sys.exit(1) for idx, game in enumerate(games, 1): print(f"{idx}. {game}") choice = input("请输入编号启动: ") selected = games[int(choice) - 1] subprocess.run([sys.executable, os.path.join(selected, 'main.py')])find_games用os.listdir遍历目录,同时判断子目录下是否存在main.py,这样能过滤掉放说明文档的文件夹。subprocess.run会阻塞直到游戏退出,所以游戏结束后能正常回到菜单。注意这里的int(choice)没有做异常处理,实际使用建议包一层try/except,否则输入非数字会直接报错退出。进阶一点的启动器还会在启动前检查依赖,这个我们放到第4章统一处理。
3. 拆解三个必玩的经典案例:循环、事件、碰撞是pygame的三大支柱
一个pygame游戏再花哨,底层也逃不开三件事:主循环让画面持续更新、事件让玩家能操作、碰撞让游戏元素发生交互。下面用三个最常见的合集必玩案例,把这三个支柱分别拆开看。
3.1 贪吃蛇:方块移动背后的坐标网格逻辑
贪吃蛇是理解坐标映射的最好例子。整个游戏建立在一张网格上,每个格子大小固定,蛇身就是一组网格坐标的列表。你不需要关心像素级的位置,只要维护一个蛇头坐标和方向向量,每次移动按方向计算新头坐标即可。
这是最核心的移动逻辑:
# 蛇身坐标列表,头部在前,每个元素是 (col, row) snake = [(10, 10), (9, 10), (8, 10)] direction = (1, 0) # (x方向增量, y方向增量),向右 def update(): head = snake[0] new_head = (head[0] + direction[0], head[1] + direction[1]) snake.insert(0, new_head) # 新头部插到列表最前 if new_head == food_position: # 吃到食物:不弹出尾部,蛇长度+1 return True else: snake.pop() # 没吃到食物:弹出尾部,长度保持不变 return False这里direction是向量,(1, 0)代表向右移动一格,(0, -1)代表向上。insert(0, new_head)把新头放最前面,pop()去掉尾部,正好模拟蛇的游动。如果新头和食物坐标重合,就不弹尾部,蛇变长。撞墙和撞自己都是基于这个坐标列表判断的:撞墙检查new_head是不是超出了网格边界,撞自己检查new_head是否已经在snake里。
画到屏幕上时,需要把网格坐标换算成像素坐标:
for segment in snake: col, row = segment rect = pygame.Rect(col * GRID_SIZE, row * GRID_SIZE, GRID_SIZE, GRID_SIZE) pygame.draw.rect(screen, (0, 255, 0), rect)这里的GRID_SIZE是一个格子占的像素数,比如20。col * GRID_SIZE就是左上角的x坐标,这是整个坐标映射的核心。新手最容易犯的错误是直接拿像素坐标存放在蛇身里,导致移动速度和碰撞判断都对不上。如果你想调整游戏难度,真正该改的参数是移动间隔时间而不是移动步长——固定步长下调整刷新速度,比在同一帧里移动多格更平滑。
3.2 飞机大战:精灵组、碰撞检测与分数系统
飞机大战几乎是每个合集必有的项目,它的价值在于引入pygame.sprite模块。子弹、敌机、玩家都继承自Sprite,统一放进Group里管理,批量更新、批量绘制,碰撞检测一条语句就能完成。
我一般会把子弹和敌机写成两个类:
import pygame import pygame.sprite as sprite class Bullet(sprite.Sprite): def __init__(self, x, y): super().__init__() self.image = pygame.Surface((8, 8)) self.image.fill((255, 255, 0)) self.rect = self.image.get_rect(center=(x, y)) def update(self): self.rect.y -= 10 # 子弹向上飞 if self.rect.bottom < 0: self.kill() # 飞出屏幕就销毁,防止列表无限膨胀 class Enemy(sprite.Sprite): def __init__(self, x, y): super().__init__() self.image = pygame.Surface((30, 20)) self.image.fill((255, 0, 0)) self.rect = self.image.get_rect(topleft=(x, y)) def update(self): self.rect.y += 2 # 敌机向下飞所有Sprite子类必须定义image和rect两个属性,Group才能正确绘制和碰撞。update方法会在调用group.update()时统一执行,所以每个精灵只写自己的行为,主循环里一行bullets.update(); enemies.update()就够了。
碰撞检测用groupcollide:
bullets = sprite.Group() enemies = sprite.Group() hits = sprite.groupcollide(bullets, enemies, True, True) for bullet in hits: # hits 的 key 是第一个组里的子弹 score += 10groupcollide前两个参数是要碰撞的两个组,后两个True表示碰撞后把子弹和敌机都删除。这个参数是游戏手感的关键:如果不想让子弹穿透敌人,两个都设为True;如果想保留敌方子弹而只删玩家,就根据实际需求调整。返回值是一个字典,键是第一个Group中的精灵,所以遍历hits就能拿到所有击中的子弹。把这个和pygame.sprite.spritecollide区分开:后者是单个精灵与一组精灵碰撞,适合检测玩家是否撞到敌机。
3.3 打地鼠/记忆翻牌:计时器与状态机设计
这类游戏更突出时间管理和状态切换。打地鼠的地鼠出现还是消失,不能靠time.sleep()控制,因为sleep会卡住整个事件循环,窗口会变成“未响应”。pygame真正的做法是自定义一个事件,然后用set_timer让它每隔一段时间自动触发一次。
import pygame pygame.init() screen = pygame.display.set_mode((600, 400)) pygame.display.set_caption("Timer Demo") MOLE_EVENT = pygame.USEREVENT + 1 pygame.time.set_timer(MOLE_EVENT, 800) # 每800毫秒触发一次 mole_visible = False running = True while running: for event in pygame.event.get(): if event.type == pygame.QUIT: running = False elif event.type == MOLE_EVENT: mole_visible = not mole_visible # 翻转地鼠可见状态 screen.fill((0, 0, 0)) if mole_visible: # 在地鼠位置绘制 pygame.draw.circle(screen, (150, 100, 50), (300, 200), 40) pygame.display.flip() pygame.quit()USEREVENT是pygame预留的用户事件起点,所有自定义事件从USEREVENT + 1开始递增,避免和内置事件撞车。set_timer的第二个参数单位是毫秒,800就是0.8秒。如果你需要一次性延时然后停止,用pygame.time.set_timer(MOLE_EVENT, 0)关闭定时器,再配合状态判断。
游戏状态可以用一个简单的枚举或字符串变量表示:PLAYING、PAUSED、GAMEOVER。把事件处理、更新和绘制都包在状态判断里,这是小游戏里最高频的架构。比写一堆散落的布尔变量强得多。
4. 让20个游戏都能正常跑起来:统一启动方式与帧率参数调优
有了第2章的启动器雏形,这一章把它升级得更健壮,顺便解决两个常见体验问题:为什么游戏在不同电脑上速度不一样,为什么窗口在高分屏上小得可怜。
4.1 统一入口:用launcher脚本选择游戏
一个能用的launcher不能只扫描目录,还要做环境和异常兜底。我会把启动器放在合集根目录,内容和下面的逻辑类似:
import os import sys import subprocess def check_pygame(): try: import pygame return True except ImportError: return False def get_game_list(): games = [] for entry in os.listdir('.'): if entry.startswith('_') or not os.path.isdir(entry): continue main_path = os.path.join(entry, 'main.py') if os.path.isfile(main_path): games.append(entry) return sorted(games, key=lambda x: x.lower()) if __name__ == '__main__': if not check_pygame(): sys.exit("缺少 pygame,请先执行 pip install pygame") games = get_game_list() print("可选小游戏:") for i, name in enumerate(games): print(f"{i+1:>2}. {name}") while True: raw = input("输入序号或 q 退出:") if raw.lower() == 'q': break try: idx = int(raw) - 1 game = games[idx] except (ValueError, IndexError): print("序号无效,重新输入") continue sys.exit(subprocess.call([sys.executable, os.path.join(game, 'main.py')]))check_pygame在启动前捕获异常,比进了游戏再报错友好得多。get_game_list跳过了以下划线开头的目录,这些目录通常是你的笔记或备份文件夹,不是游戏本体。subprocess.call和run的区别在于直接返回退出码,sys.exit会把游戏退出码透传给终端,方便后续在脚本里判断运行状态。这个启动器已经能应付绝大多数合集的结构,唯一的假设是每个游戏的入口都叫main.py,这个约定在pygame小游戏合集里几乎百分百成立。
4.2 帧率(FPS)与游戏速度:clock.tick的参数陷阱
很多新手把clock.tick(60)当成“游戏每秒跑60步”,这方向没错,但它不是精确的固定步长。tick方法返回上一次调用到现在经历的毫秒数,如果你完全忽略这个返回值,那游戏速度就会受屏幕刷新率和CPU性能影响。在144Hz的显示器上,主循环每秒钟跑的次数远高于60,物体每帧位移固定的话,整体就会快得多。
正确的做法是让物体的移动速度以秒为单位,乘以dt(delta time)来算每帧位移:
import pygame pygame.init() screen = pygame.display.set_mode((640, 480)) clock = pygame.time.Clock() x = 0 # 物体横坐标 speed = 300 # 像素/秒 running = True while running: dt = clock.tick(60) / 1000.0 # 毫秒转秒 for event in pygame.event.get(): if event.type == pygame.QUIT: running = False x += speed * dt # 使用 dt 计算位移 screen.fill((0, 0, 0)) pygame.draw.rect(screen, (255, 255, 255), (x, 100, 40, 40)) pygame.display.flip() pygame.quit()这段代码中speed = 300的意思是每秒钟物体在x轴上移动300像素。无论循环实际跑了多少帧,speed * dt一帧的位移都会自动调整,最终时间上的速度恒定。如果你在配置里看到speed = 5这种写法,多半是“每帧5像素”,这种代码在60帧下看起来正常,换到120帧就会飞起。这个参数是判断源码健壮性的一个快速指标。
clock.tick的另一个坑是它只应出现在主循环顶部,别在分支或子函数里再调。否则每帧会调用多次,内部计时就会被重置,导致dt极小或为零,物体几乎不动。
4.3 窗口大小与缩放:兼容不同分辨率屏幕
合集里的游戏窗口常常写死成640x480或800x600,在现在的高分屏上又小又别扭。直接改源码里的set_mode尺寸会引发连锁反应:坐标、字体、图片位置全都要跟着调,工作量巨大。更聪明的做法是双Surface方案:日志绘制在一个固定分辨率的canvas上,最后一帧缩放拉伸到窗口大小。
INTERNAL_W, INTERNAL_H = 480, 320 # 游戏逻辑分辨率 SCREEN_W, SCREEN_H = 960, 640 # 窗口实际分辨率 pygame.init() screen = pygame.display.set_mode((SCREEN_W, SCREEN_H)) canvas = pygame.Surface((INTERNAL_W, INTERNAL_H)) # 主循环里所有绘制都画到 canvas canvas.fill((0, 0, 0)) # ... 绘制角色、UI ... scaled = pygame.transform.scale(canvas, (SCREEN_W, SCREEN_H)) screen.blit(scaled, (0, 0)) pygame.display.flip()这样游戏内的所有坐标都基于480x320,放大时transform.scale会把整个画面等比例拉伸,不会出现元素错位。需要注意scale每次都会生成新Surface,如果窗口尺寸不变,建议缓存缩放结果,只在内容变化时才重新缩放。如果想在游戏内支持动态切换窗口大小,可以把SCREEN_W/SCREEN_H改成从事件里读取窗口尺寸,但要注意保持宽高比一致,否则画面会变形。
5. pygame小游戏源码常见问题避坑:从双缓冲到中文乱码的5个血泪经验
这部分是我拿到任何新合集之后排查问题的固定顺序。按频率从高到低,记录5条踩坑,每条都是现象、原因、解决一条龙。
5.1 游戏窗口一闪而过:事件循环被提前退出
现象:双击main.py,窗口刚露脸就消失,控制台没有报错,或者报错被Windows自动关掉。
原因:最常见的是源码里只写了初始化代码,没有进入主循环。还有的写了while True:,但分支里出现了错误的break,第一帧就跳出了循环。另外一些老代码把主循环写进了函数,但忘了调用函数,窗口建立完直接走到pygame.quit()。
解决:确认主循环被正确执行,且退出事件被处理:
running = True while running: for event in pygame.event.get(): if event.type == pygame.QUIT: running = False # 这里写更新和绘制逻辑 pygame.display.flip() pygame.quit()这个模式是pygame的“标准骨架”。如果窗口还是闪退,就在主循环前打印一行日志,通过控制台输出来判断是没进循环还是循环里崩了。用文本编辑器打开源码,搜索while和pygame.display.flip(),这两个位置占了这类问题的九成。
5.2 图像有残影/卡顿:缺少display.flip()或双缓冲没开
现象:画面上有拖影,物体移动时旧位置还残留,或者整个画面闪烁严重,像老式幻灯片。
原因:pygame的绘制默认发生在后台缓冲,必须调用pygame.display.flip()把后台内容一次性交换到屏幕。如果没调用,你的绘制永远在不可见的缓冲里,有的游戏为了“快速刷新”另用display.update(),但覆盖区域不完整就会留残影。
解决:每一帧先清屏,绘制所有物体,最后翻转缓冲。
screen.fill((0, 0, 0)) # 清屏 # ... 绘制所有物体 ... pygame.display.flip() # 翻转缓冲如果清屏颜色和窗口背景不一致,会造成肉眼可见的闪烁。另外pygame 2.x默认双缓冲,但万一你在set_mode里加了不兼容的flag,也可能导致闪屏。遇到这种情况,把set_mode((width, height))保持最简写法,不要画蛇添足加HWSURFACE这类旧参数。
5.3 中文路径或中文注释乱码:编码声明与资源路径
现象:源码在Windows下运行,注释和print里的中文变成乱码,或者加载图片时提示FileNotFoundError。
原因:Python 3默认UTF-8,但如果源码文件是GBK编码保存的,pygame读取时会因为解析问题报错。另一个高发问题是资源路径用的是相对路径,但你是从其他目录启动的,导致找不到images/hero.png。
解决:在源码文件第一行加编码声明,并用文件所在目录拼接资源路径:
# -*- coding: utf-8 -*- import os import pygame base_dir = os.path.dirname(__file__) img_path = os.path.join(base_dir, 'images', 'hero.png') hero = pygame.image.load(img_path)os.path.dirname(__file__)得到当前源码的绝对目录,再拼子路径,这样不管你在哪个目录执行python main.py,资源路径都是正确的。中文目录名本身在图像加载上一般没问题,但pygame.mixer音频模块在Windows上对中文路径支持较差,建议把带中文的素材目录改成英文,这是省钱省力的好习惯。
5.4 按键无响应:事件类型判断错误
现象:键盘怎么按都没反应,但鼠标点击正常,或者部分按键有反应部分没有。
原因:事件循环里把event.type == pygame.KEYDOWN写成了event.type == pygame.K_LEFT,后者根本不存在。还有的是在循环里多次调用了pygame.event.get(),第一次把事件消费掉了,第二次拿到的是空列表。
解决:先判断事件类型,再判断键值,并在整个循环里只调一次get。
for event in pygame.event.get(): if event.type == pygame.KEYDOWN: if event.key == pygame.K_LEFT: player.x -= step elif event.key == pygame.K_RIGHT: player.x += step如果你需要“按住方向键持续移动”,KEYDOWN单次触发是不够的,因为按一次只动一步。常见做法是在KEYDOWN里设置一个布尔标记,在KEYUP里取消,然后在主循环的每帧更新里根据标记移动。另一种替代是pygame.key.get_pressed(),它返回按键状态数组,适合持续移动。但注意不要把get_pressed和KEYDOWN放在同一个功能里混用,两种模型一旦冲突,按键灵敏度会变得不可控。这就是为什么总有人抱怨“为什么我按一下动两下”的原因。
5.5 音乐/音效播放失败:文件格式与初始化顺序
现象:游戏画面和逻辑正常,但没有任何声音,或者控制台报错pygame.error: Unable to open file。
原因:最常见的是没有调用pygame.mixer.init(),或者调用顺序不对。pygame.init()并不会自动初始化mixer的所有功能,需要显式准备。另外音频文件格式不兼容:部分MP3文件的编码变体在pygame里打不开,换成WAV或OGG立刻就好。
解决:先初始化mixer,再加载音频,全套代码如下:
import pygame pygame.init() pygame.mixer.init(frequency=44100, size=-16, channels=2) pygame.mixer.music.load('bgm.ogg') pygame.mixer.music.set_volume(0.6) pygame.mixer.music.play(-1) click_sound = pygame.mixer.Sound('click.wav') click_sound.play()mixer.init参数里frequency=44100是CD音质标准,size=-16表示16位有符号采样,channels=2是立体声。Music适合加载较大较长的背景音乐,Sound适合几十KB的短音效,两者是独立的接口。play(-1)里的-1是无限循环,如果你只想播一次,传0或不传即可。音频不响是最玄学的问题,有时候同一份文件在一台机器上正常另一台没声,可以先检查系统音量,再看文件格式,最后才怀疑源码——顺序反了会浪费很多时间。
6. 把20个案例变成自己的作品:三个进阶改造方向
当你把所有游戏都跑通之后,会意识到重复代码飞快膨胀:每个Game都是一个main.py,内部都有相似的主循环,事件处理逻辑几乎一样。这时候不改造,后面就没法学了。
6.1 用类重构重复代码:把每个游戏抽象成Scene基类
写一个基类,把主循环、事件分发、帧率控制都放在里面,子类只重写handle_event、update、draw:
import pygame class Scene: def __init__(self, screen): self.screen = screen self.running = True self.clock = pygame.time.Clock() def handle_event(self, event): pass def update(self, dt): pass def draw(self, screen): pass def run(self): while self.running: dt = self.clock.tick(60) / 1000.0 for event in pygame.event.get(): if event.type == pygame.QUIT: self.running = False else: self.handle_event(event) self.update(dt) self.draw(self.screen) pygame.display.flip() pygame.quit()之后每一个小游戏只需要继承Scene,把逻辑填进对应方法里,原本200行的main.py能瘦到80行。这个结构也方便你写一个全局的launcher,根据用户选择实例化不同的Scene子类,真正把20个游戏统一进一个程序里。
6.2 给游戏加一个暂停与存档功能
暂停是状态机改造的第一步。在update入口加一个paused判断,暂停时不更新逻辑但依然绘制游戏画面:
paused = False for event in pygame.event.get(): if event.type == pygame.KEYDOWN and event.key == pygame.K_p: paused = not paused if not paused: # 只有未暂停时才执行更新逻辑 update(dt) draw(screen)存档用json最省事,把最高分和当前关卡写进文件,下次启动读取:
import json, os SAVE_PATH = 'save.json' def save_score(score): with open(SAVE_PATH, 'w') as f: json.dump({'max_score': score}, f) def load_score(): if os.path.exists(SAVE_PATH): with open(SAVE_PATH) as f: return json.load(f).get('max_score', 0) return 0这比用普通文本文件好得多,pygame的字体渲染能直接读取到整数,不需要自己解析字符串。
6.3 用pygame.mixer自制音效与背景音乐
先用别的工具做音频,再用pygame.mixer整合。背景音乐用mixer.music承载,短音效用Sound。如果想在一个游戏里实现彩蛋音效,可以动态生成:
import array import pygame pygame.mixer.init() # 生成一段800Hz的方波,持续0.1秒 buf = array.array('h', [int(32000 * 0.5 if i < 2000 else -32000 * 0.5) for i in range(4000)]) tone = pygame.mixer.Sound(buffer=buf) tone.play()这种方式适合做临时调音,真正常用的还是加载现成的wav。我自己的教训是:拿到一个高质量合集,先别急着全部跑一遍,而是把5.1到5.5的排查顺序抄在便利贴上。因为至少有一半的“运行失败”不是源码问题,而是环境或路径问题。把这些固定的坑摸熟以后,你再打开任何一套pygame小游戏全集都能稳住。希望帮到你。
本文还有配套的精品资源,点击获取