Pygame游戏开发入门:跑通主循环、精灵与碰撞检测的完整源码包

发布时间:2026/10/14 3:44:06
Pygame游戏开发入门:跑通主循环、精灵与碰撞检测的完整源码包
简介这是一份《Python游戏编程入门》的随书源码压缩包面向希望快速上手pygame的Python初学者以及想通过实例理解游戏开发流程的读者。资源内包含全部随书示例代码可用于对照学习或直接运行。已有3033人学习或下载压缩包共117个文件大小约2.9MB核心文件为43个Python脚本.py并配有图片.png/.bmp、音效.wav等游戏资源以及少量文本说明和数据库缓存文件整体结构清晰。这份源码未经改动保留原作风格虽然个别示例可能存在小瑕疵但足以帮助读者熟悉pygame的常用模块与游戏循环设计是入门阶段不错的参考材料。1. 把“我想做游戏”变成“我能跑通游戏”这份 pygame 源代码包值得先跑一遍很多刚学完 Python 基础语法的人卡住的不是“看不懂教程”而是“照着敲完代码窗口一闪就退图片加载不出来按键没反应”。问题不在你的理解力在于你缺一份可运行、可拆解、可改参数的完整源码做参照。这份 Python 游戏编程入门源代码包就是用 pygame 写好的可直接运行的游戏工程覆盖了游戏开发最基本的几条主线主循环、事件处理、精灵对象、碰撞检测、音效与计分。适合刚入门 Python、想往游戏开发方向走的人也适合做课程设计却总觉得“代码像黑匣子”的同学。拿到它你先别改功能第一步是把它跑起来再逐行拆。2. 运行环境与项目结构三步配好环境先分清主循环与事件队列2.1 环境配置Python 版本、pygame 安装方式与验证方法这份源码包基于 Python 3.8 以上版本编写pygame 使用 2.x 系列。如果你机器上还是 Python 3.7 或更老建议先升级否则 pygame 2.x 安装后可能报“DLL load failed”之类的错误。安装 pygame 本身不复杂最常见做法是用 pip 直接装pip install pygame如果你想精确控制版本可以指定版本号安装pip install pygame2.5.2装完后打开 Python 交互环境或写一个临时脚本验证安装是否成功import pygame pygame.init() print(pygame.version.ver)这段代码的作用是初始化 pygame 的所有模块并打印版本号。如果能看到类似“2.5.2”的输出说明安装没问题。注意pygame.init()必须放在任何窗口创建、图片加载之前因为它会一次性初始化显示、音频、字体、事件等全部子系统。跳过这一步后面调用加载图片或播放声音的接口时会直接报错。还有一个常见坑conda 环境和系统 Python 环境混用。如果你用了 conda建议在 conda 环境内单独安装conda activate mygame pip install pygame否则可能出现“python 里能 import但 IDE 里 import 失败”的玄学问题本质是解释器路径不一致。2.2 项目目录源码包里哪些文件是核心哪些是素材解压源码包后典型的目录结构大致如下game_project/ ├── main.py # 程序入口游戏从这里启动 ├── settings.py # 配置参数屏幕尺寸、帧率、颜色等 ├── player.py # 玩家精灵类 ├── enemy.py # 敌人精灵类 ├── bullet.py # 子弹精灵类 ├── game.py # 游戏主逻辑与场景管理 ├── resources/ │ ├── images/ # 图片素材 │ ├── sounds/ # 音效文件 │ └── fonts/ # 字体文件 └── score.txt # 分数持久化文件,用于记录最高分我先说明每个文件在游戏里扮演的角色。main.py是整个游戏的大门负责初始化 pygame、创建窗口、实例化游戏对象并启动主循环。settings.py集中存放所有可调整的参数——窗口宽高、背景色、玩家速度、子弹速度等这样你改参数时不用翻遍所有代码。player.py、enemy.py、bullet.py分别定义了玩家的飞船、敌人和子弹它们都是 pygame 中Sprite类的子类自带位置、移动、绘制和碰撞逻辑。game.py是核心调度模块管理精灵组的更新、碰撞检测、分数显示和游戏结束。resources目录里的图片和音效基本都是素材文件代码运行时需要加载它们。初学者最容易翻车的地方就在这里直接双击运行main.py时报错“Unable to load image”或者窗口能打开但画面是空白的。原因绝大多数是图片路径写成了绝对路径换一台机器就失效正确做法是用相对路径并且把入口脚本放在与资源目录同级的根目录下运行。2.3 第一次运行入口脚本与报错排查确认环境没问题后在终端进入项目根目录执行python main.py这时会弹出一个窗口里面应该有玩家角色和敌人按方向键或 WASD 控制角色移动空格键射击敌人碰到你或被打中都会触发对应逻辑。如果一切正常恭喜你这份源码包已经被你跑通了。但如果你看到了窗口一闪而过或者直接报错先不要急着怀疑源码包按下面顺序排查确认当前终端工作目录是项目根目录不是桌面或其他位置。用pwdWindows 用cd命令查看当前路径确认。确认resources目录与main.py的相对位置没有被改动过。图片加载代码通常写的是resources/images/xxx.png这是相对路径前提是入口目录正确。如果报错信息里含有pygame.error且提到了文件路径九成是资源文件缺失或路径拼写不一致。打开报错中的路径对照目录看文件是否存在注意大小写和扩展名是否完全一致。第一次运行的目标不是“看懂每一行”而是确认“整套代码能跑起来”。跑通之后再谈改逻辑。3. 主循环与事件队列读懂游戏的心跳改参数才有方向3.1 游戏主循环的四要素事件、更新、绘制、时钟pygame 游戏的骨架是主循环也就是每一帧都重复执行的一组操作。对新手来说最难理解的一点是游戏不是一个线性脚本而是一个永不退出的循环每一帧都在重复做“收集输入→更新状态→绘制画面”这三件事。下面是源码包中主循环的核心结构我把注释写在代码里# game.py 内部的核心循环结构 running True clock pygame.time.Clock() while running: # 1. 处理事件队列 for event in pygame.event.get(): if event.type pygame.QUIT: running False # 2. 更新游戏状态 all_sprites.update() # 碰撞检测、分数更新等逻辑也写在这一步 # 3. 绘制画面 screen.fill((0, 0, 0)) all_sprites.draw(screen) pygame.display.flip() # 4. 控制帧率 clock.tick(60) pygame.quit()这段代码的逻辑顺序很关键。第一步处理事件比如用户点了窗口右上角的关闭按钮、按下键盘或移动鼠标这些都会被放进事件队列第二步更新游戏状态比如玩家位置变化、子弹移动、碰撞判定第三步把更新后的画面绘制到屏幕上fill先清空上一帧画面draw再画所有精灵flip最后把画好的内容一次性显示出来第四步clock.tick(60)让循环每秒最多执行 60 次。如果你把pygame.event.get()这一步放到更新状态的后面就会出现一个典型问题按键响应延迟甚至完全失灵。原因是事件队列里的消息没有被及时取走积压到下一帧才处理表现就是手感“发黏”。3.2 事件队列键盘、鼠标、退出事件的常见处理模式pygame 的事件系统是一种队列模型pygame.event.get()会取走队列中当前所有事件并清空队列。也就是说如果你这一帧不去取下一帧再取事件一样在但会累积。键盘事件最常用的是KEYDOWN和KEYUP。源码包里的移动逻辑通常采用“按键状态持续生效”的方式处理模式如下keys pygame.key.get_pressed() if keys[pygame.K_LEFT] or keys[pygame.K_a]: player.rect.x - player.speed if keys[pygame.K_RIGHT] or keys[pygame.K_d]: player.rect.x player.speed这里用到的是pygame.key.get_pressed()它返回一个布尔数组表示当前每一帧所有按键的实时状态。这种方式适合做持续移动因为你不必记录“按键按下”和“按键抬起”两个事件直接在每一帧查询状态即可。而单次触发操作比如按一下空格发射一颗子弹一般用事件方式if event.type pygame.KEYDOWN and event.key pygame.K_SPACE: bullet Bullet(player.rect.centerx, player.rect.top) all_sprites.add(bullet) bullets.add(bullet)这里用KEYDOWN事件而不是get_pressed()是因为你只想让“按下”这个动作触发一次发射不想按住空格时每帧都连续发射。如果你用get_pressed()写发射逻辑按住空格就会像连发机枪一样停不下来这通常是初学改代码时最容易发现的差异点。鼠标事件也有对应处理比如点击某个区域开始游戏源码包里的做法一般是if event.type pygame.MOUSEBUTTONDOWN: if event.button 1: mouse_pos event.pos start_button.rect.collidepoint(mouse_pos)3.3 帧率控制clock.tick 与 delta time 的取舍帧率控制不只是为了让游戏不那么“快”更是为了让不同电脑上游戏速度一致。如果直接写位置增量而不控制帧率在 144Hz 显示器上游戏速度会是 60Hz 显示器上的两倍多。clock.tick(60)是最简单的方案它强制每帧不少于 1/60 秒。但这里有个微妙的问题如果某一帧逻辑执行时间过长tick只会保证“不早于”不会保证“不晚于”实际帧率会下降游戏移动速度也会相应地“变慢”。更严谨的做法是引入 delta time即每帧实际经过的时间dt clock.tick(60) / 1000.0 # 换算成秒 player.rect.x player.speed * dt这样移动速度的数值单位就变成了“像素/秒”而不是“像素/帧”。源码包里基础版本用的是clock.tick(60)加固定位移好处是简单直观适合入门缺点是如果你的电脑性能波动角色移动会肉眼可见地忽快忽慢。如果你打算改造成一个能长期玩的小游戏建议把位移逻辑改成乘dt的写法我一般会在拿到源码后第一步就做这个调整。4. 从模板到改游戏精灵、碰撞与音效的骨架拆解4.1 精灵类和精灵组为什么要用 group 而不是 listpygame 的Sprite类和Group类是一对搭档。Sprite代表一个游戏对象它内部有image和rect两个关键属性Group是精灵的容器自动帮你完成批量更新和批量绘制。一个典型的玩家精灵定义如下class Player(pygame.sprite.Sprite): def __init__(self, x, y): super().__init__() self.image pygame.Surface((50, 40)) self.image.fill((0, 255, 0)) self.rect self.image.get_rect() self.rect.x x self.rect.y y self.speed 5 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这里super().__init__()调用了父类构造函数这一步不能省否则Group内部维护的精灵列表不会包含这个对象。pygame.Surface创建了一个纯色矩形作为临时图像方便你在没有图片素材时先跑通逻辑后续换成真实图片只需要把Surface换成pygame.image.load()加载的图片对象。Group的最大价值是批量操作all_sprites pygame.sprite.Group() player Player(400, 500) all_sprites.add(player) # 每帧只需要调用一次 all_sprites.update() all_sprites.draw(screen)如果你用普通list保存游戏对象每次更新和绘制都要写for循环如果还要在循环中删除对象比如子弹击中敌人后子弹要消失list的删除操作还要小心翼翼处理索引变化很麻烦。Group内部封装了这一切你只需要调用remove()或在碰撞检测时使用kill()方法。4.2 碰撞检测colliderect、mask 与精度取舍碰撞检测是游戏逻辑里最核心的部分也是初学者最容易写出“差不多但不完全对”的代码的地方。pygame 提供了几种内置检测方式最常用的是矩形碰撞hits pygame.sprite.spritecollide(player, enemies, False) if hits: print(玩家被敌人碰到了)spritecollide的第三个参数是dokill传True时碰撞到的精灵会从组中自动删除适合子弹击中敌人的场景传False时不会删除适合玩家碰到敌人那种“扣血但不消失”的场景。矩形碰撞的问题在于精度。如果玩家的图片是一架飞机真实轮廓是不规则形状而rect是覆盖图片的矩形就会出现“明明没碰到飞机翅膀却判定为碰撞”的偏差。这时可以用mask像素级检测pygame.sprite.collide_mask(player, enemy)mask检测的原理是把两张图片的透明区域标记为“空”只在两个物体不透明像素重叠时才判定为碰撞。精度更高代价是性能开销更大。对入门项目来说精灵数量不超过几十个时完全用得起mask检测如果敌人数量上百还是老老实实用rect检测加合理的碰撞体积缩小。一个常见的中间方案是手动收缩矩形hitbox self.rect.inflate(-20, -10)inflate(-20, -10)让矩形宽度减少 20 像素、高度减少 10 像素这样就可以让碰撞判定区域比图像显示区域小一圈视觉上更接近真实飞机轮廓同时性能开销几乎没有增加。源码包里的敌人碰撞用的就是这种方案需要改精度时只需要调整这两个参数。4.3 音效与背景音乐初始化混音器的坑pygame 的音效系统单独拎出来说是因为它有一个和pygame.init()相关的坑。pygame.init()会初始化包括声音在内的所有模块但有时候在某些环境下混音器模块没有正确打开调用音效播放时会报错。先看标准的加载和播放方式pygame.mixer.init() pygame.init() shoot_sound pygame.mixer.Sound(resources/sounds/shoot.wav) shoot_sound.play() pygame.mixer.music.load(resources/sounds/bgm.mp3) pygame.mixer.music.play(-1)这里有两个模块需要区分pygame.mixer.Sound是短音效适合射击、爆炸这类一次性效果pygame.mixer.music是背景音乐流适合长时间播放的音乐支持播放列表和循环。play(-1)表示无限循环如果不传参数或传0音乐播放一次就结束。音效文件的格式推荐使用 WAV 或 OGGMP3 在部分平台的 pygame 版本上会出现无法解码的问题这属于发行版差异属于那种“代码没问题但就是不出声”的玄学雷区。我在本地复现这个源码包时把背景音乐从 MP3 转成 OGG 后就正常了你可以记下这个经验。另外混音器的初始化顺序在源码包里是约定好的先pygame.mixer.init()再pygame.init()避免后者重复初始化混音器导致冲突。如果你把顺序写反部分平台会报pygame.error: audio device already opened之类的错误。解决方法是二选一要么只调用pygame.init()要么先调用pygame.mixer.init()后调用pygame.init()但不要反过来。5. 避坑五个让初学者反复翻车的真实问题5.1 窗口一闪就退现象双击运行或运行python main.py后窗口出现瞬间就消失了没有报错或者报了NameError。原因最常见的是主循环被写进了if __name__ __main__:外面或者主循环的缩进错误导致while循环根本没执行到。还有一种是代码里写了pygame.quit()但没有写running False导致循环结束后直接退出。解决先确认main.py里是否包含完整的while running:循环并且确认循环内的代码都缩进了循环体内。其次在pygame.quit()旁边临时加一行print(game over)如果这行打印了但窗口没显示说明渲染阶段出了问题如果没有打印问题在事件循环没跑起来。我拿到任何一套新手源码包第一件事就是检查入口和循环缩进。5.2 图片加载报错或画面空白现象运行时报pygame.error: Unable to open file或者窗口能打开但所有角色都不显示。原因图片路径写的是绝对路径或者使用了错误的相对路径基准。你从桌面执行脚本时相对路径resources/images/player.png是相对于桌面去查找的当然找不到。解决代码里统一使用基于__file__的路径拼接这样无论从哪个目录启动都能找到资源import os BASE_DIR os.path.dirname(__file__) IMAGE_PATH os.path.join(BASE_DIR, resources, images, player.png) self.image pygame.image.load(IMAGE_PATH)os.path.dirname(__file__)拿到的是当前代码文件所在的目录os.path.join用它拼接出绝对路径。这样你在 IDE 里运行、双击运行、还是在终端里从项目根目录运行结果都一样。5.3 按键按下没反应现象游戏能启动但按方向键角色不动或者按空格没有子弹发射。原因事件类型和按键码写错比如用KEYUP写了移动逻辑或者用pygame.key.get_pressed()写了单次触发逻辑导致按下和抬起互相抵消。解决区分两种场景。持续移动用pygame.key.get_pressed()查询按键状态单次发射用KEYDOWN事件。另外确认你检测的按键码和实际按键一致比如方向键对应K_LEFT、K_RIGHTWASD 对应K_a、K_d大小写不影响但拼写不能错。调试时可以在事件处理里临时打印event.key看按下按键时输出什么键码一秒就能定位。5.4 碰撞检测总是“差一点”现象子弹看起来击中了敌人但没有触发击杀逻辑或者玩家离敌人还有一段距离就死了。原因rect是基于整个图片矩形的如果图片本身就是一张宽大的透明底图实际精灵的可视部分只占图片的一小块碰撞判定就会严重偏离视觉。解决加载图片后手动收缩rect或者用透明度较高的图片素材。具体做法是self.rect self.image.get_rect()之后调用self.rect.inflate(-30, -20)把碰撞区域缩小到接近机体轮廓。如果仍不满意改用pygame.mask.from_surface(self.image)做mask碰撞。源码包里默认使用收缩矩形我实测在 800x600 分辨率下收缩到图片宽度的 70%~80% 是比较合适的手感区间。5.5 帧率不稳速度忽快忽慢现象游戏在笔记本上跑得忽快忽慢或者同一段代码在不同电脑上角色移动速度明显不同。原因代码里没有用clock.tick固定帧率或者在逻辑里使用了time.sleep()打断主循环。time.sleep()会让事件队列积压帧率极不稳定。解决把主循环改成标准四步结构统一用clock.tick(60)。如果希望速度不受帧率影响把位移量改成乘dt的方式。另外检查是否在某个条件分支里加了time.sleep(0.1)这类调试代码这是“帧率玄学”最常见的来源删除后通常立即恢复稳定。6. 让它变成你自己的游戏三个立刻能落地的改造方向6.1 用配置文件代替硬编码数值源码包里的settings.py已经把屏幕尺寸、帧率、颜色、速度等参数集中管理了但你可能还想调整更多内容比如玩家发射子弹的间隔、敌人出现的频率、初始生命值。把这些都收进settings.py你的代码会清爽很多PLAYER_SPEED 5 BULLET_SPEED 10 BULLET_COOLDOWN 250 # 单位毫秒 ENEMY_SPAWN_INTERVAL 2000 INITIAL_LIVES 3改游戏手感时你只需要打开这一个文件调整数字不用在逻辑代码里来回找。如果你改参数后没生效检查一下你是不是在代码里又写了一遍硬编码数值比如init方法里写死了self.speed 5这样settings.py的设置会被覆盖。6.2 用状态机管理场景切换入门项目通常只有一个游戏场景但如果你想加一个“开始界面”和一个“游戏结束界面”最直接的办法是引入状态机。给游戏定义几个状态# 状态常量 STATE_MENU 0 STATE_PLAYING 1 STATE_GAMEOVER 2 current_state STATE_MENU主循环里根据current_state跳转到不同的逻辑分支。状态切换时需要注意一件事进入新状态时要重新初始化相关数据。比如从GAMEOVER回到PLAYING玩家位置、子弹列表、敌人数量、分数都要重置否则会保留上一局的残留状态。源码包里的“重新开始”功能就是用这个模式实现的我只改了状态常量和三个if分支就完成了。6.3 用文件持久化保存最高分pygame 项目里做排行榜一个简单可靠的办法是读写文本文件。每局游戏结束时读取现有最高分和本局得分比较如果更高就写回文件def load_high_score(): try: with open(score.txt, r) as f: return int(f.read().strip()) except (FileNotFoundError, ValueError): return 0 def save_high_score(score): with open(score.txt, w) as f: f.write(str(score))注意try里的两个异常文件不存在时FileNotFoundError会被触发文件内容不是数字时ValueError会被触发。这两个都要处理否则第一次运行或者文件损坏时游戏会崩溃。这个方案足够支撑课程设计和入门项目不需要引入数据库也不依赖外部配置。我拿到这套源码包后最先改的不是画面也不是音效而是把所有硬编码数值全部收进settings.py然后加了一个文本文件记录最高分。从那以后我在每次改参数前都强制走一遍“先备份再动手、改完跑一遍确认没破坏原有逻辑”的流程以这套源码包为基础的几次改造从来没有翻过车。希望这份经验对你的游戏开发入门之路有帮助。本文还有配套的精品资源点击获取