简介:这是一份《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 pygame==2.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 控制角色移动,空格键射击,敌人碰到你或被打中都会触发对应逻辑。如果一切正常,恭喜你,这份源码包已经被你跑通了。
但如果你看到了窗口一闪而过,或者直接报错,先不要急着怀疑源码包,按下面顺序排查:
- 确认当前终端工作目录是项目根目录,不是桌面或其他位置。用
pwd(Windows 用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 而不是 list
pygame 的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 或 OGG,MP3 在部分平台的 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_RIGHT,WASD 对应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,然后加了一个文本文件记录最高分。从那以后,我在每次改参数前都强制走一遍“先备份再动手、改完跑一遍确认没破坏原有逻辑”的流程,以这套源码包为基础的几次改造从来没有翻过车。希望这份经验对你的游戏开发入门之路有帮助。
本文还有配套的精品资源,点击获取