简介:这份资源是《Python游戏编程入门》一书的随书源代码,面向刚接触Python游戏开发、希望借助pygame动手实践的初学者与自学者。代码按书中示例整理,未经改动,可用于对照章节熟悉窗口绘制、精灵动画、音效播放与事件循环等基础环节,帮助读者把书本知识落到可运行的程序上。压缩包为rar格式,共117个文件,约2.9MB,其中43个py脚本承载各章示例逻辑,25个png与1个bmp提供图像素材,8个wav用于音效演示,另有txt、doc说明及若干临时与数据库文件,整体体量轻便,便于本地解压查阅。目前已有3036人学习下载,说明其在入门阶段具有一定参考价值。需要提醒的是,原书代码部分示例存在运行问题,读者可借此练习调试与排错,后续作者也计划上传修改后的版本,适合作为熟悉pygame的起步材料。
1. 从一份能跑起来的 Pygame 源码说起
很多人学 Python 游戏开发,卡在第一步不是语法,而是“照着教程敲完,窗口一闪就退,或者角色根本不动”。这份《Python 游戏编程入门 源代码》解决的正是这个断层:它把 pygame 里最容易翻车的窗口循环、事件处理、精灵碰撞、帧率控制拆成一个个能独立运行的小工程,你下载解压后直接python 文件名.py就能看到画面。它适合两类人:刚学完 Python 基础、想用 pygame 做点能玩的东西的新手,以及想快速摸清一个 2D 游戏骨架、准备二次开发小游戏的老手。下面我按“能干什么 → 怎么跑 → 怎么改”的顺序,把这份源码拆开讲。
2. 项目定位与技术栈:这套源码到底由什么组成
2.1 它不是一个完整商业游戏,而是一组递进式示例
拿到源码包先别急着找main.py,这类入门源码通常按知识点分目录,每个目录是一个可独立运行的最小闭环。常见组织方式是:01_window只开窗口和主循环,02_event加键盘鼠标事件,03_sprite引入精灵和图片加载,04_collision做矩形碰撞检测,05_sound加音效和背景音乐。这种“一个目录一个知识点”的结构对新手非常友好,因为你可以只跑其中一个,出问题范围也小。
我一般会先看目录里有没有requirements.txt和assets/文件夹。前者决定你装什么版本,后者决定图片、音效能不能被正确加载。很多入门源码跑不起来,八成是资源路径写死成作者本机的绝对路径,或者 pygame 版本和代码里的 API 对不上。所以第一步不是读代码,而是确认依赖和资源。
2.2 技术栈就是 pygame 加标准库,没有隐藏框架
这套源码的技术栈很干净:核心是pygame,负责窗口、渲染、事件、声音;辅助用sys、os、random、math这些标准库。没有引入 ECS 框架,也没有用第三方物理引擎,碰撞就是pygame.Rect的colliderect,动画就是按帧切换图片。这种“裸写”的方式对入门是好事,因为每一行你都能看懂,不会被框架的抽象层挡住。
常见做法是把常量集中放在文件顶部,比如屏幕宽高、帧率、颜色值、玩家速度。这样改参数时不用满文件找魔法数字。下面这段就是典型配置区,我把它单独拎出来讲,是因为二次开发时你 90% 的调整都从这里开始:
import pygame # 屏幕与帧率配置:改这里就能调整窗口大小和流畅度 SCREEN_WIDTH = 800 SCREEN_HEIGHT = 600 FPS = 60 # 颜色用 RGB 元组,背景色和文字色分开管理 BG_COLOR = (30, 30, 40) TEXT_COLOR = (240, 240, 240) # 玩家参数:速度单位是像素/帧,改大角色更跟手 PLAYER_SPEED = 5 PLAYER_SIZE = 48FPS = 60配合主循环里的clock.tick(FPS)决定游戏节奏;PLAYER_SPEED是每帧位移量,注意它不是“每秒像素”,因为帧率固定时两者等价,但一旦你改成可变帧率,就得乘delta_time。PLAYER_SIZE同时用于创建Rect和加载图片后的缩放,保持两者一致能避免碰撞框和视觉错位。
2.3 目录结构与资源加载方式
一个典型的入门源码目录大概长这样:根目录放各个示例的.py文件,assets/images/放角色和背景图,assets/sounds/放音效,assets/fonts/放字体。资源加载常见写法是用os.path.join拼相对路径,而不是直接写"assets/player.png",因为后者在从其他目录启动时会失效。
import os import pygame # 以当前脚本所在目录为基准,避免从别的路径启动时找不到资源 BASE_DIR = os.path.dirname(os.path.abspath(__file__)) ASSETS_DIR = os.path.join(BASE_DIR, "assets") def load_image(name, size=None): path = os.path.join(ASSETS_DIR, "images", name) image = pygame.image.load(path).convert_alpha() if size: image = pygame.transform.scale(image, size) return imageconvert_alpha()保留透明通道,适合带透明背景的 PNG 角色图;如果是背景大图且不需要透明,用convert()更快。size参数传None就保持原图尺寸,传元组就缩放。这里有个细节:pygame.transform.scale是直接拉伸,画质会糊,入门阶段够用,追求清晰可以换smoothscale。
3. 跑起来的关键:主循环、事件与精灵系统
3.1 主循环是 pygame 的心脏,顺序不能乱
几乎所有 pygame 项目跑不起来或行为诡异,问题都出在主循环的顺序上。正确顺序是:处理事件 → 更新状态 → 绘制画面 → 刷新显示 → 控制帧率。顺序错了会出现“按键没反应”“画面撕裂”“角色瞬移”这些玄学现象。下面是一个最小可运行骨架:
import pygame import sys pygame.init() screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) pygame.display.set_caption("入门示例") clock = pygame.time.Clock() running = True while running: # 1. 事件处理:必须先把事件队列抽干,否则窗口无响应 for event in pygame.event.get(): if event.type == pygame.QUIT: running = False elif event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE: running = False # 2. 更新游戏状态(位置、动画帧、碰撞等) # update_game() # 3. 绘制:先铺背景,再画精灵,最后画 UI screen.fill(BG_COLOR) # draw_game(screen) # 4. 刷新显示 pygame.display.flip() # 5. 帧率控制,防止 CPU 空转 clock.tick(FPS) pygame.quit() sys.exit()pygame.event.get()每次都要把队列取空,只处理QUIT不够,键盘事件也要在这里消费。screen.fill()必须在画精灵之前,否则上一帧的残影会留在屏幕上。pygame.display.flip()是双缓冲交换,少了它你什么都看不到。clock.tick(FPS)既限帧又返回毫秒数,后面做可变帧率时会用到返回值。
3.2 键盘持续移动要用 get_pressed,不是 KEYDOWN
新手最容易踩的坑:用KEYDOWN事件做移动,结果角色只能一格一格挪。KEYDOWN只在按下的那一瞬间触发一次,持续移动必须每帧查询按键状态。常见做法是在主循环的更新阶段调用pygame.key.get_pressed():
keys = pygame.key.get_pressed() dx, dy = 0, 0 if keys[pygame.K_LEFT] or keys[pygame.K_a]: dx -= PLAYER_SPEED if keys[pygame.K_RIGHT] or keys[pygame.K_d]: dx += PLAYER_SPEED if keys[pygame.K_UP] or keys[pygame.K_w]: dy -= PLAYER_SPEED if keys[pygame.K_DOWN] or keys[pygame.K_s]: dy += PLAYER_SPEED # 归一化对角线速度,避免斜着走比直着走快 if dx != 0 and dy != 0: dx *= 0.707 dy *= 0.707 player_rect.x += int(dx) player_rect.y += int(dy)get_pressed()返回一个布尔序列,用键常量索引。对角线同时按两个方向时,速度会变成sqrt(2)倍,乘0.707(约1/√2)做归一化是标准做法。int()转换是因为Rect只接受整数坐标,浮点会报错。这段代码放在更新阶段,不要放进事件循环里。
3.3 精灵与碰撞:Rect 是唯一可信的判定依据
pygame 的Sprite和Group不是必须的,但用了之后批量更新和绘制会省很多事。核心是每个精灵挂一个self.rect,图片只是视觉,碰撞永远用rect判定。下面是一个玩家精灵的典型写法:
class Player(pygame.sprite.Sprite): def __init__(self, x, y): super().__init__() self.image = load_image("player.png", (PLAYER_SIZE, PLAYER_SIZE)) # rect 决定位置和碰撞范围,图片缩放后要同步尺寸 self.rect = self.image.get_rect(topleft=(x, y)) self.speed = PLAYER_SPEED def update(self, keys): dx = (keys[pygame.K_RIGHT] - keys[pygame.K_LEFT]) * self.speed dy = (keys[pygame.K_DOWN] - keys[pygame.K_UP]) * self.speed self.rect.x += dx self.rect.y += dy # 限制在屏幕内,防止角色跑丢 self.rect.clamp_ip(pygame.Rect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT))keys[pygame.K_RIGHT] - keys[pygame.K_LEFT]是个小技巧,布尔值相减得到-1/0/1,省掉一堆if。clamp_ip原地把矩形限制在给定范围内,比手写边界判断干净。碰撞检测用pygame.sprite.spritecollide(player, enemy_group, False),第三个参数False表示撞到后不删除敌人,做子弹命中时传True更合适。
4. 避坑与排查:那些让新手卡一整晚的问题
4.1 窗口一闪就退
现象是双击运行后黑框一闪而过。原因通常是主循环没有正确保持,或者pygame.quit()之后又执行了绘制。排查顺序:先确认while running里的running初始为True,再确认QUIT事件里把它置False,最后看pygame.quit()是否在循环外。如果是在 IDE 里点运行,异常被吞掉也会表现为闪退,改成命令行python xxx.py跑一遍,报错就出来了。
4.2 图片加载报 FileNotFoundError
现象是pygame.error: Couldn't open assets/player.png。原因基本是相对路径的基准目录不对,从 IDE 启动时工作目录可能是项目根,也可能是脚本所在目录。解决办法就是 2.3 里那套os.path.dirname(os.path.abspath(__file__)),把资源路径锚定到脚本位置。另外注意文件名大小写,Linux 下Player.png和player.png是两个文件,Windows 不区分,跨平台时容易翻车。
4.3 角色移动一顿一顿的
现象是按住方向键角色走走停停。原因多半是把移动逻辑写进了KEYDOWN事件,或者clock.tick()的帧率设得太低。先检查移动是否用了get_pressed(),再把FPS调到 60 试试。还有一种情况是每帧都重新加载图片,pygame.image.load放在update里会导致严重卡顿,图片必须在初始化时加载一次并缓存。
4.4 碰撞判定和视觉对不上
现象是角色明明没碰到敌人却判定死亡,或者碰到了没反应。原因是rect尺寸和图片显示尺寸不一致,常见于用了transform.scale但没更新rect。解决方法是缩放后立刻self.rect = self.image.get_rect(...),或者用self.rect.size = self.image.get_size()同步。调试时可以把pygame.draw.rect(screen, (255,0,0), sprite.rect, 1)打开,肉眼确认碰撞框位置。
4.5 声音播放报 mixer 初始化失败
现象是pygame.error: mixer not initialized。原因是只调了pygame.init()但音频设备没起来,或者在没有声卡的环境里跑。稳妥做法是显式初始化pygame.mixer.init(),并放在pygame.init()之后。如果确实没有音频设备,用try/except包住,失败就跳过声音,别让整个游戏起不来。
5. 二次开发:把示例改成你自己的小游戏
5.1 先做状态机,别把所有逻辑堆进主循环
入门源码通常所有逻辑都在一个while里,加个开始菜单和游戏结束界面就会乱成一团。我的习惯是引入一个极简状态机,用字符串或枚举表示MENU、PLAYING、GAME_OVER,主循环里按状态分发更新和绘制。这样加暂停、加关卡切换都是加一个分支,不用改主循环骨架。
from enum import Enum class GameState(Enum): MENU = 1 PLAYING = 2 GAME_OVER = 3 state = GameState.MENU while running: for event in pygame.event.get(): if event.type == pygame.QUIT: running = False elif state == GameState.MENU and event.type == pygame.KEYDOWN: if event.key == pygame.K_RETURN: state = GameState.PLAYING if state == GameState.PLAYING: update_game() draw_game(screen) elif state == GameState.MENU: draw_menu(screen) elif state == GameState.GAME_OVER: draw_game_over(screen) pygame.display.flip() clock.tick(FPS)Enum比裸字符串好,拼错会直接报错而不是静默走错分支。状态切换只改state变量,各状态的绘制函数互相独立,改菜单不影响游戏逻辑。
5.2 用 delta_time 让速度不再依赖帧率
固定FPS时PLAYER_SPEED直接当每帧位移没问题,但一旦机器性能波动或你解锁帧率,角色速度就会变。改造方法是用clock.tick(FPS)的返回值算时间增量,位移乘上delta_time。这是从“示例”走向“能发布”的关键一步,也是我每次二次开发必做的第一件事。
dt = clock.tick(FPS) / 1000.0 # 转成秒 # 速度单位改成像素/秒,比如 300 player.rect.x += int(PLAYER_SPEED_PER_SEC * dt)注意dt要做上限保护,比如min(dt, 0.05),防止窗口拖动或断点调试后一帧时间过长导致角色瞬移穿墙。这个细节在示例里通常没有,但实际项目里不加迟早出问题。
5.3 改造验证清单
改完之后怎么确认没改坏?我一般按这个顺序过一遍:窗口能正常开关且不报错;方向键持续移动流畅、对角线速度正常;角色撞边界被挡住而不是跑出屏幕;敌人碰撞能正确触发且不重复扣血;音效在命中时只播一次;切到菜单再回游戏状态干净、没有残留精灵。每一项对应一个前面讲过的模块,哪项挂了就回到对应小节查。
| 改造点 | 验证方法 | 常见失败表现 |
|---|---|---|
| 状态机 | 菜单回车进游戏、结束回菜单 | 状态切换后画面不刷新 |
| delta_time | 拖动窗口后角色不瞬移 | 角色穿墙或速度突变 |
| 碰撞框 | 打开 rect 描边肉眼比对 | 判定偏移、误伤 |
| 资源路径 | 从其他目录启动脚本 | 图片音效加载失败 |
5.4 一个具体技巧:把常量抽成配置文件
示例里的常量散落在各文件顶部,改一个数值要翻好几个文件。二次开发时我会抽一个settings.py,把所有可调参数集中,其他模块from settings import *或按需导入。好处是调平衡、换皮肤、改分辨率只动一个地方,也方便以后接关卡数据。血泪经验是别在多个文件里重复定义同一个常量,改了一处忘了另一处,调试半天以为是逻辑 bug。
从那以后我每次接手这类入门源码,都先把资源路径、帧率、速度常量统一收口,再跑一遍 5.3 的清单,确认基线没问题才开始加功能。希望这份拆解能帮你少走几个晚上。
本文还有配套的精品资源,点击获取