简介:面向Python及Pygame初学者的节日主题小游戏源码包,以“兔子接月饼”为玩法载体,完整展示了游戏开发中的初始化窗口、事件循环、精灵创建与碰撞检测等核心开发流程。资源压缩后共16个文件,主要包含Python源码、png/jpg静态图片与mp3背景音乐,整体大小3.12MB,结构清晰,目录层次清楚,便于直接运行和二次学习。其中游戏主体脚本统一控制游戏初始化、开始、结束与退出,精灵模块则定义了月饼、兔子、背景、得分、倒计时、菜单、鼠标共七个继承自pygame.sprite.Sprite的角色类,涵盖从元素渲染到状态切换的常见需求。随包附带的素材涵盖兔子、月饼、背景及音效,便于理解Pygame的资源加载方式与精灵组管理。目前已有168人学习下载,适合作为Python课程设计、兴趣开发或Pygame框架入门学习的参考实践项目。
1. 兔子接月饼:一个 Pygame 小游戏源码里的完整学习路径
带新人的时候,我一般会让第二周的项目从控制台程序切到图形界面,兔子接月饼这类题材就是从这时候开始高频出现的。一个python pygame 兔子接月饼小游戏源码.zip解压之后,通常只有两三百行代码加几张图片,却能同时覆盖 Python 基础里的条件判断、循环、随机数、类和键盘事件。这个标题真正值钱的地方不在“免费源码”,而在把“游戏循环、精灵对象、碰撞结算、状态切换”这四件事串起来的方式。这篇博客会顺着多数源码包的常见结构,把这些机制拆开讲,最后把调试好的工程整理成一个可分发的新源码包,方便你在本地改、在笔记里记、再发给别人跑。
2. 先把窗口转起来:Pygame 游戏循环与初始化步骤
2.1 环境准备与源码包目录识别
拿到 zip 之后不要急着双击运行。先确认 Python 环境干净,常见做法是建一个独立虚拟环境,避免 pygame 的版本和系统里其他包互相影响:
python -m venv rabbit_game # Windows 激活 rabbit_game\Scripts\activate # Linux / macOS 激活 source rabbit_game/bin/activate pip install pygame第一行用python -m venv创建虚拟环境,激活后 pip 安装的包都会装进这个目录而不是全局环境。pip install pygame会自动拉取当前平台对应的预编译包,Windows 和 macOS 一般不需要编译;如果安装过程中提示error: failed to build 'pygame' when getting requirements to build wheel,多半是 pip 版本太老或者网络源不稳定,可以先pip install --upgrade pip,再换国内镜像源重试。
解压后先看目录结构,一个结构正常的 pygame 源码包长这样:
| 文件 / 目录 | 作用 |
|---|---|
main.py或game.py | 入口文件,包含初始化与主循环 |
rabbit.png/mooncake.png | 兔子与月饼的图片资源 |
settings.py或config.py | 屏幕尺寸、速度、生成间隔等常量 |
notes.md或README.md | 运行说明与依赖清单 |
requirements.txt | 依赖列表,方便别人一条命令装环境 |
识别入口文件有个小技巧:源码包里文件名带main的通常是启动脚本。如果只有game.py,就搜索pygame.display.set_mode,这行代码所在文件就是入口。
2.2 游戏循环四段式:事件处理、更新、绘制、刷新
pygame 的图形界面不是 React 那样的声明式框架,它靠的是一个不结束的 while 循环,每秒钟把画面重新画几十遍。最小可运行的主循环是这样的:
import sys import pygame pygame.init() screen = pygame.display.set_mode((720, 960)) pygame.display.set_caption("兔子接月饼") clock = pygame.time.Clock() while True: for event in pygame.event.get(): if event.type == pygame.QUIT: pygame.quit() sys.exit() screen.fill((20, 24, 50)) pygame.display.flip() clock.tick(60)pygame.init()统一初始化所有模块,set_mode创建窗口并返回一个 Surface 对象,之后的绘制都画在这块画布上。事件循环里只处理了QUIT,也就是用户点关闭按钮时干净退出,sys.exit()用来结束整个进程。screen.fill用深蓝色填充背景,模拟夜空,display.flip()把后台缓冲一次性推到屏幕上,clock.tick(60)把循环限制在每秒 60 帧。
这段代码里最容易被忽略的是clock.tick(60)。没有它,程序会以 CPU 能跑多快就跑多快的方式刷新,不同机器上游戏速度完全不同。锁帧之后,后面所有物体的位移量才能按“每帧多少像素”来设计。
2.3 帧率与物体移动速度的关系
很多人改源码时会犯同一个错误:想让兔子跑得快一点,把speed从 8 改到 15,结果发现速度几乎没变,或者在不同电脑上体验不一样。原因在于没有理解“游戏速度 = 每帧位移 × 每秒帧数”。
clock.tick(60)保证约 16.7 毫秒一帧,此时每帧移动 8 像素,一秒大约移动 480 像素。如果取消帧率限制,高速显示器上每秒可能跑 300 帧以上,物体快到看不见。所以 pygame 项目里有两个位置不能动:帧率设置和位移代码里的固定增量。想做更严谨的帧率无关移动,可以把clock.tick(60) / 1000.0得到的秒数作为delta_time乘进位移公式里,但一个 60 帧锁定的简单小游戏不需要引入这个复杂度,保持固定增量反而更容易调参。
3. 兔子和月饼:精灵类、键盘控制与掉落逻辑
3.1 用精灵类封装游戏对象
游戏循环跑起来之后,下一步是让屏幕上有东西动。pygame 的sprite.Sprite基类提供了一套对象管理机制,所有可移动元素都继承它。看一下兔子对象的常见写法:
import random import pygame class Rabbit(pygame.sprite.Sprite): def __init__(self, image): super().__init__() self.image = image self.rect = self.image.get_rect(midbottom=(360, 920)) self.speed = 9 def update(self): keys = pygame.key.get_pressed() if keys[pygame.K_LEFT]: self.rect.x -= self.speed if keys[pygame.K_RIGHT]: self.rect.x += self.speed self.rect.x = max(8, min(self.rect.x, 720 - 8 - self.rect.width)) class Mooncake(pygame.sprite.Sprite): def __init__(self, image): super().__init__() self.image = image self.rect = self.image.get_rect(center=(random.randint(30, 690), -40)) self.speed = 5 def update(self): self.rect.y += self.speed if self.rect.top > 960: self.kill()Sprite子类必须设置self.image和self.rect,pygame 的碰撞检测和管理组都依赖这两个属性。get_rect(midbottom=(360, 920))用中点底部定位兔子,这样它站在画面底部而不是左上角。update方法里用pygame.key.get_pressed()一次性获取所有按键状态,比逐个判断KEYDOWN事件更适合连续移动。月饼对象从画布上方进入,y坐标每帧加 5,掉出底部之后调用kill()把自己从所有 Group 里移除,避免无限累积占内存。
这里能看出精灵封装的价值:每个对象自己管自己的位置和移动逻辑,主循环里只需要统一调用一次update()。
3.2 键盘控制与移动边界的参数设计
key.get_pressed()返回一个长度为几百的序列,按下对应键时对应位置为 True。K_LEFT和K_RIGHT是 pygame 预定义的键位常量,不需要额外引入键盘库。边界限制的表达式max(8, min(self.rect.x, 720 - 8 - self.rect.width))值得展开说明。
外层max(8, ...)保证 rect 的 x 坐标最小为 8,防止兔子贴到左边缘;内层min(...)保证右边界不超过720 - 8 - 宽度。这 8 像素是留出的缩进,视觉上兔子不会撞墙。加速度和手感相关的几个参数可以先按下面的基线起步:
| 参数 | 推荐值 | 调节效果 |
|---|---|---|
| 兔子速度 | 8 ~ 10 像素/帧 | 低于 6 手感迟钝,高于 13 容易穿过月饼 |
| 初始月饼速度 | 4 ~ 5 像素/帧 | 决定前 30 秒的容错率 |
| 月饼生成间隔 | 700 ~ 900 毫秒 | 间隔越短,同屏数量越多 |
3.3 月饼生成:定时器与随机坐标
月饼不能一次性全部生成,需要在游戏过程中每隔一段时间出现一个。pygame 提供了一套基于事件的定时器机制:
CREATE_MOONCAKE = pygame.USEREVENT + 1 pygame.time.set_timer(CREATE_MOONCAKE, 800) mooncakes = pygame.sprite.Group() # 主循环的事件处理里 for event in pygame.event.get(): if event.type == CREATE_MOONCAKE: mooncakes.add(Mooncake(images["mooncake"]))pygame.time.set_timer(event_id, milliseconds)会让 pygame 每隔指定毫秒自动向事件队列投递一个自定义事件。事件号不能直接写pygame.USEREVENT,因为同一个事件号可能被多处复用,常见做法是pygame.USEREVENT + N递增使用。Mooncake构造函数里用random.randint(30, 690)生成横向坐标,比固定一个位置掉落更有可玩性。
所有月饼实例放进一个Group之后,主循环里只需要调用mooncakes.update(),每个实例的update会被自动遍历执行。Group 的另一层作用是后面碰撞检测可以直接对整个组做结算,不需要手动遍历列表。别忘了在绘制阶段加上mooncakes.draw(screen),否则月饼只存在于逻辑中,屏幕上什么都看不到。
4. 碰撞检测、计分和难度曲线:兔子接月饼的手感从哪来
4.1 rect 碰撞、掩码碰撞与 groupcollide 的选择
月饼掉下来之后,判断“接没接到”是游戏的核心逻辑。pygame 提供了多级碰撞检测手段,最常用的是矩形交集判定。两个 Sprite 的rect只要相交,colliderect就返回 True。结合 Group 可以一行代码完成所有月饼的判定:
from pygame.sprite import spritecollide hits = spritecollide(rabbit, mooncakes, True) score += len(hits)spritecollide(sprite, group, dokill)的第三个参数True表示碰撞后从 Group 中移除该月饼,这正是接住月饼后它应该消失的行为。默认用的是rect碰撞,也就是两个矩形框相交就算命中。如果兔子和月饼的图片边缘大量留白,矩形框之间隔着透明区域也会判定碰撞,误判率会高到影响体验。图片素材干净的时候,可以换用更精确的掩码碰撞:
hits = spritecollide(rabbit, mooncakes, True, pygame.sprite.collide_mask)pygame.sprite.collide_mask基于图片的 alpha 通道做逐像素判定,只把不透明像素的相交视为碰撞。代价是每帧计算量更大,月饼数量超过 20 个时可能出现掉帧。
4.2 计分、生命值与游戏状态切换
有碰撞结果之后,后续逻辑要区分两种结果:接住加分,漏掉扣命。月饼掉出屏幕时已经在Mooncake.update里被kill()了,所以要在掉落过程中判断命数,而不是等它消失。常见做法是检查月饼的rect.top是否超过屏幕高度:
GAMEOVER = pygame.USEREVENT + 2 class State: MENU = 0 RUNNING = 1 OVER = 2 state = State.RUNNING score = 0 lives = 3状态机用三个常量区分菜单、游戏中和结束,主循环里根据state决定渲染内容。漏月饼的判断写在Mooncake.update里不太合适,因为精灵拿不到游戏全局变量。更常见的是在主循环里遍历 Group:
for mc in mooncakes: if mc.rect.top >= 960: mc.kill() lives -= 1 if lives <= 0: state = State.OVER计分和生命都不能在碰撞检测时直接改全局变量,而是先拿回结果再统一结算,这样调试时可以很容易加日志确认一轮碰撞的处理次数。pygame 并没有限制全局变量的用法,但代码整洁度会直接影响后面扩展难度曲线时的心情。
4.3 难度递增的三个参数与推荐取值
兔子接月饼的可玩性几乎全部来自难度曲线。难度由三个参数控制:月饼下落速度、生成间隔、同屏最大数量。调整规则一般是“速度线性增、间隔指数减、数量阶梯式加”:
| 等级 | 月饼速度(像素/帧) | 生成间隔(毫秒) | 同屏上限 |
|---|---|---|---|
| 1 | 4 | 900 | 3 |
| 2 | 6 | 700 | 5 |
| 3 | 8 | 500 | 7 |
| 4 | 11 | 350 | 9 |
| 5 | 14 | 240 | 12 |
调参时不要三个参数同时改,否则很难判断手感变化来自哪一个变量。我一般的做法是先固定生成间隔,只把速度从 4 往 14 推,找到玩家能反应过来的临界值;再把间隔从 900 毫秒往下压,记录败率明显上升的点;最后才设置同屏上限。同屏上限的实现可以放在生成计数器里,用一个变量记录当前 Group 长度,达到上限后本次定时器事件直接跳过,不创建新月饼。
等级提升的触发条件用分数阈值即可,例如每 100 分升一级。等级变化时用一个字典把等级映射到上面的四元组参数,代码里只做查表赋值,不写一堆 if:
LEVELS = { 1: {"speed": 4, "interval_ms": 900, "max_on_screen": 3}, 2: {"speed": 6, "interval_ms": 700, "max_on_screen": 5}, 3: {"speed": 8, "interval_ms": 500, "max_on_screen": 7}, 4: {"speed": 11, "interval_ms": 350, "max_on_screen": 9}, 5: {"speed": 14, "interval_ms": 240, "max_on_screen": 12}, } if level < 5 and score >= level * 100: level += 1 params = LEVELS[level] pygame.time.set_timer(CREATE_MOONCAKE, params["interval_ms"])set_timer可以被重复调用,新间隔会覆盖旧设置,无需重新初始化模块。这样升级过程中事件源始终是同一个,逻辑上不会出现多个生成器叠加。
5. 构建产物整理:把源码封装成可分发的新 zip
5.1 源码 zip 的标准目录与笔记
当项目调试完成,下一步是把它整理成别人也能直接用的新源码包。这个步骤和下载现成的 zip 正好形成闭环。规范的项目目录应该把配置、精灵和入口分开,而不是把几百行代码全塞进一个文件:
rabbit_catch_mooncake/ ├── main.py ├── settings.py ├── sprites.py ├── images/ │ ├── rabbit.png │ └── mooncake.png ├── notes.md ├── requirements.txt └── build.pyrequirements.txt只需写入pygame>=2.0,不要跟着本机装的版本号锁死,否则别人在另一台机器上安装时可能遇到版本不匹配。notes.md里记录运行步骤、按键操作和难度参数表,作为源码配套笔记。压缩时选中这个外层目录而不是里面的散文件,这样别人解压后直接得到一个文件夹,不会散落一地文件。
5.2 PyInstaller 打包与资源路径处理
源码 zip 适合学习和二次开发,但给别人演示时没人愿意先配 Python 环境。用 PyInstaller 把它打成单文件可执行程序是更成熟的交付方式:
pip install pyinstaller pyinstaller -F -w -n RabbitMooncake main.py-F表示生成单个可执行文件,-w表示 Windows 下不弹出控制台窗口,-n指定产物名称。执行后 exe 会出现在dist/目录下。需要注意图片资源不会自动打包进 exe,运行时相对路径可能失效。解决方法是写一个资源路径转换函数:
import sys from pathlib import Path def resource_path(relative: str) -> Path: if hasattr(sys, "_MEIPASS"): return Path(sys._MEIPASS) / relative return Path(__file__).parent / relative代码里hasattr(sys, "_MEIPASS")判断是否运行在 PyInstaller 解压出的临时目录里,是就优先使用该目录查找图片,否则使用脚本所在目录。重新打包后,把 exe 和 zip 一起发给测试者,再让他们重点验证三件事:首次打开是否超过 3 秒、连续运行 10 分钟内存是否有明显增长、关闭窗口后进程是否完全退出。
本文还有配套的精品资源,点击获取