Pygame入门:从主循环到碰撞检测,手写一个可打包的接球游戏

发布时间:2026/10/7 16:45:47
Pygame入门:从主循环到碰撞检测,手写一个可打包的接球游戏 我到现在还记得第一次运行 Pygame 窗口时的感受一个黑色窗口弹出来什么都没画但光标变成加载状态程序好像卡住了。其实它不是卡住了是进入了游戏主循环——在这个循环里画面每秒刷新60次才是常态。很多刚写完print(Hello World)的初学者想试试做游戏挑来挑去还是从 Pygame 入手这是非常正确的选择。Pygame 是 SDL2 在 Python 上的封装库它不替你管理场景、不帮你搞物理引擎但提供了窗口、绘图、事件、音频等一整套基础模块你用代码拼出属于自己的小游戏。这篇文章从环境搭建开始逐步带你写一个能玩、能计分、能重开、能打包分发的接球小游戏整个过程代码量不大但覆盖了游戏开发最常见的核心概念主循环、事件、碰撞检测、游戏状态机和打包分发。无论你是刚学完 Python 基础语法还是想看看游戏开发到底是怎么回事这篇文章都值得顺着走一遍。1. 为什么选 Pygame 做第一个游戏而不是 Unity、Godot 或命令行贪吃蛇1.1 Pygame 的真实定位它不是一个游戏引擎而是一套积木很多人听到做游戏第一反应就是 Unity、Unreal 这种大块头。但这类引擎的问题是你为了玩一个积木拼装游戏先花了一周学怎么用电锯。Pygame 则相反它就是个乐高积木盒窗口、画布、矩形、碰撞、音效、字体这些都是积木你怎么拼是你的事。它的底层是 SDL2Simple DirectMedia Layer这是一个跨平台的底层多媒体库Pygame 用 Python 让你直接操作它。这个定位决定了它的核心价值让写代码的人用纯 Python 就能感受到游戏开发的核心循环不需要引入重量级的编辑器、脚本语言和复杂的资源管线。它和游戏引擎的区别很像手工拼装机器人和买现成机器人的区别前者麻烦但每一步都有掌控感后者功能全但调试问题的时候你连机器人的内部结构长什么样都不知道。1.2 和常见替代方案做一次老实对比我见过不少人和我当年一样在 Pygame 和其他方案之间来回纠结这里直接做一个实在的对比。方案优势劣势适合谁Pygame纯 Python、上手快、文档成熟、社区案例多不适合 3D、性能上限低、没有可视化编辑器零基础学游戏逻辑、快速做小游戏原型tkinterPython 自带、能画基本图形没有面向游戏设计的事件帧循环游戏做起来很别扭只想写个图形化小工具Unity功能全面、跨平台、生态庞大学习曲线陡、需要 C#、项目体积大想走专业游戏开发路线的学习者Godot引擎本身做得很精致、节点设计合理但使用的是 GDScript实际场景仍要看这个脚本生态愿意离开纯 Python 环境做游戏的人浏览器 Canvas可以在网页里分享、不需要装环境要走 JavaScript和 Python 语法差异大已经熟悉前端的人一句话总结我的建议如果你想用 Python 亲眼看着自己的代码变成一个有交互的东西Pygame 是最近的一条路。它不是终点但它是很好的起点。1.3 什么人适合这篇教程这篇文章适合三类人Python 基础语法刚学完想找东西练手的人想验证游戏开发是不是真的有意思的观望者以及已经在 Pygame 里画过几个图形但从来没完整写完一个游戏的半入门选手。如果你完全没写过 Python建议先花一点时间掌握变量、循环、函数和 import这篇教程不会花篇幅教你语法。不过也不用太担心整个代码量摊开看很直观跟着敲一遍也能跑起来。2. 跑起来的最小骨架窗口、主循环和那三个必会的对象2.1 pygame.init() 和 display.set_mode先理解你打开的不是窗户而是画布任何 Pygame 项目第一行实操大概率都是import pygame pygame.init()pygame.init()会把 Pygame 内部所有模块都初始化一遍包括显示、事件、字体、音频等。你可以只初始化某一个模块比如pygame.font.init()但绝大多数情况下直接全量初始化是最省事的它的速度开销可以忽略不计。接着最关键的一步是创建窗口screen pygame.display.set_mode((800, 600))这一步发生的事情远比弹出一个窗口复杂。它做的是向操作系统申请一块屏幕区域同时在内存里建立一块内存画布这个画布在 Pygame 里叫Surface你后续所有的绘图操作其实都是在这块内存画布上进行的。这里有一个新手理解上的分水岭你用pygame.draw.rect()画的矩形先落在内存画布上而不是直接出现在屏幕上。只有当你调用pygame.display.flip()的那一刻内存画布才会被整体翻转到屏幕上。这个机制叫双缓冲目的是防止画面闪烁——如果像素画一个显示一个肉眼就会看到闪烁和撕裂。2.2 坐标系Pygame 的 y 轴是向下增长的这个细节值得单独拎出来说因为它是所有游戏绘图坐标的出发点。Pygame 的坐标系原点在窗口左上角x 轴向右增大y 轴向下增大。# 左上角是 (0, 0) # 右下角是 (800, 600) pygame.draw.circle(screen, (255, 0, 0), (400, 50), 30)上面这行代码会在窗口横坐标 400、纵坐标 50 的位置画一个半径 30 的红色圆。注意(400, 50)是圆心坐标而在pygame.Rect里用的是左上角坐标 宽 高比如pygame.Rect(0, 0, 100, 100)是一个从窗口左上角开始、边长 100 的正方形。如果你之前做过数学竞赛那种坐标系需要一个短期记忆在这个世界里游戏物体掉出屏幕底部的判断条件是y HEIGHT而不是y 0。我一开始写小球下落时条件写反了结果小球永远停在空中这个小细节你在代码里会遇到。2.3 永不退出的主循环游戏和普通程序的分水岭普通 Python 程序从上到下执行一遍就完了但游戏程序不一样它需要一直运行一边接收你的键盘鼠标输入一边更新画面。这个一直运行靠的就是主循环。running True while running: # 1. 处理事件按键、鼠标、窗口关闭 # 2. 更新游戏逻辑移动、碰撞、计分 # 3. 绘制画面 # 4. pygame.display.flip() 将画面展示出去 # 5. clock.tick(60) 控制循环每秒只跑 60 次在这个循环里你写的代码每 1/60 秒执行一遍。这意味着如果你让挡板每次循环往右移动 7 像素挡板的实际移动速度是 7 × 60 420 像素/秒。clock.tick(60)这个调用很多人会漏掉或者不知道它是干嘛的。它的作用是让循环的节奏维持在每秒 60 帧避免游戏速度受到 CPU 性能影响。如果不加这一行程序会以每秒几万帧的速度疯转CPU 直接满载风扇狂转画面快到根本看不见。我在实际操作中发现一个很实用的做法把clock.tick(60)放在循环的最开头而不是结尾。这样即使某帧逻辑运行时间超过预期tick 也会把这帧的时间起点拉准游戏节奏整体更稳定。2.4 事件系统为什么关闭窗口也要你自己写代码在 GUI 程序里你点了窗口右上角的叉这个动作不会自动结束程序。操作系统只是把一个窗口关闭请求事件挂到了事件队列里你得主动去读取并处理它。for event in pygame.event.get(): if event.type pygame.QUIT: running Falsepygame.event.get()会把这一帧里面所有发生的事件取回来循环遍历处理。如果不处理QUIT事件或者不调用event.get()你会看到窗口点叉后程序还赖着不退出卡死在那里——这是新手最常遇到的假死现象之一。事件系统本质上是个队列操作系统不断往里塞事件你每帧取走一批。如果你的主循环里有一段耗时很长的代码比如time.sleep(5)事件就会在队列里积压等你 sleep 完程序会突然一次性处理 5 秒内的几百个事件出现鼠标延迟、按键跳格的诡异现象。所以记住一个原则主循环里不要放任何长时间阻塞的操作。3. 完整实战一个能玩的接球游戏从头写到底3.1 游戏规则设计做游戏先设计规则再写代码写代码之前我习惯先把规则定清楚。这次做的接球小游戏规则很简单屏幕上方不断落下小球玩家在底部控制挡板左右移动来接球。每接住一个球得 1 分球没接住就掉出屏幕底部损失 1 条生命。初始 3 条生命生命为 0 时游戏结束。游戏结束按空格重新开始按 Q 退出。这样设计有几个好处逻辑足够简单碰撞检测、计分、物品重置、游戏状态这几个游戏开发核心概念全部覆盖代码量控制在约 150 行不会吓退新手。3.2 用 pygame.Rect 管理挡板和球而不是自己算坐标我不推荐新手直接用 x、y 变量管理物体位置因为碰撞检测要自己写一堆判断。Pygame 提供了pygame.Rect它把一个矩形的左上角坐标、宽度、高度打包在一起并且自带一堆非常方便的碰撞方法。paddle pygame.Rect(WIDTH // 2 - 60, HEIGHT - 40, 120, 16)这个挡板是从窗口底部往上 40 像素处、一个宽 120 高 16 的水平矩形。矩形自带的.x、.y、.left、.right、.center等属性都可以直接读写非常顺手。小球我同样用pygame.Rect管理但绘制的时候用pygame.draw.circle()按圆心画圆。这里有一个值得你了解的细节碰撞检测用的是小球矩形外壳不是圆形们本身的轮廓。也就是说当小球边缘碰到挡板边缘还差一点时矩形碰撞可能已经触发了。这个小游戏的场景里这点误差完全无感但你要在心里知道矩形碰撞是所有游戏碰撞里最廉价、最快的一种如果以后做需要精确碰撞的游戏再用距离判断或更复杂的碰撞算法。3.3 连续的键盘检测用 get_pressed 而不是 KEYDOWN让挡板移动有两种写法区别很大。第一种是对每个按键按下事件做一次位移if event.type pygame.KEYDOWN: if event.key pygame.K_LEFT: paddle.x - 7这种写法的问题是你必须按一次键等它触发一次事件想要持续移动得靠操作系统自带的按键重复功能而它的延迟和节奏不受你控制手感会非常僵硬。第二种是每帧检查一次键盘状态keys pygame.key.get_pressed() if keys[pygame.K_LEFT]: paddle.x - 7pygame.key.get_pressed()返回的是所有按键当前状态的元组按住的键对应位置是 True。因为主循环每 1/60 秒跑一次所以你只要按住左键挡板就会每帧稳定向左移动 7 像素连续移动完全由你自己控制。这是游戏开发里做玩家控制的常规做法手感干净利落。3.4 球的运动和碰撞速度翻转是游戏里最简单的物理小球的移动逻辑一句话就能说清每帧往 x、y 方向各加一个速度值碰到左右边缘把 x 速度取反碰到上边缘把 y 速度取反碰到挡板也把 y 速度取反并加分。写出来是这样的ball.x speed_x ball.y speed_y if ball.left 0 or ball.right WIDTH: speed_x -speed_x if ball.top 0: speed_y -speed_y if ball.colliderect(paddle) and speed_y 0: speed_y -speed_y score 1这里speed_y 0的判断是有讲究的。因为球向下运动时speed_y为正值只有当球正在下落时撞到挡板才应该反弹如果球已经在向上飞经过挡板的时候触发了碰撞代码那就会把向上的速度又翻成向下出现球粘在挡板上来回抖的 bug。这个细节是我当年调试了十几分钟才发现的写在这里提醒你。每次接球之后我还会让速度稍微加一点score 1 if score % 5 0: speed_y 1每接住 5 个球球下落速度加快 1让游戏难度递增。这种动态难度的写法很简单效果却很好玩家会明显感觉到紧张感上升。3.5 完整代码现在就能直接运行的接球游戏下面是整个游戏的完整代码建议你新建一个catch_game.py文件复制进去直接运行。import pygame import random import sys pygame.init() WIDTH, HEIGHT 800, 600 screen pygame.display.set_mode((WIDTH, HEIGHT)) pygame.display.set_caption(接球小游戏 - Pygame 入门) clock pygame.time.Clock() BLACK (10, 10, 15) WHITE (240, 240, 240) RED (220, 60, 60) BLUE (70, 130, 220) GREEN (80, 200, 120) paddle pygame.Rect(WIDTH // 2 - 60, HEIGHT - 40, 120, 16) ball pygame.Rect(0, 0, 30, 30) ball.center (random.randint(30, WIDTH - 30), 50) speed_x random.choice([-3, 3]) speed_y 5 score 0 lives 3 state start # start / playing / gameover font_large pygame.font.SysFont(SimHei, 64) font_mid pygame.font.SysFont(SimHei, 36) font_small pygame.font.SysFont(SimHei, 24) def reset_ball(): ball.center (random.randint(30, WIDTH - 30), 50) return random.choice([-3, 3]), 5 def draw_text(text, font, color, center): surf font.render(text, True, color) rect surf.get_rect(centercenter) screen.blit(surf, rect) while True: clock.tick(60) for event in pygame.event.get(): if event.type pygame.QUIT: pygame.quit() sys.exit() if state ! playing and event.type pygame.KEYDOWN and event.key pygame.K_SPACE: score 0 lives 3 speed_x, speed_y reset_ball() state playing if state gameover and event.type pygame.KEYDOWN and event.key pygame.K_q: pygame.quit() sys.exit() if state playing: keys pygame.key.get_pressed() if keys[pygame.K_LEFT] and paddle.left 0: paddle.x - 7 if keys[pygame.K_RIGHT] and paddle.right WIDTH: paddle.x 7 ball.x speed_x ball.y speed_y if ball.left 0 or ball.right WIDTH: speed_x -speed_x if ball.top 0: speed_y -speed_y if ball.colliderect(paddle) and speed_y 0: speed_y -speed_y score 1 if score % 5 0: speed_y 1 if ball.top HEIGHT: lives - 1 if lives 0: state gameover else: speed_x, speed_y reset_ball() screen.fill(BLACK) if state start: draw_text(接球小游戏, font_large, WHITE, (WIDTH // 2, 220)) draw_text(按空格开始, font_mid, GREEN, (WIDTH // 2, 320)) draw_text(左右方向键移动挡板接住小球, font_small, WHITE, (WIDTH // 2, 380)) elif state playing: pygame.draw.rect(screen, BLUE, paddle) pygame.draw.circle(screen, RED, ball.center, ball.width // 2) draw_text(f得分: {score}, font_small, WHITE, (60, 20)) draw_text(f生命: {lives}, font_small, WHITE, (WIDTH - 60, 20)) else: draw_text(游戏结束, font_large, WHITE, (WIDTH // 2, 200)) draw_text(f最终得分: {score}, font_mid, RED, (WIDTH // 2, 300)) draw_text(按空格重新开始按Q退出, font_small, WHITE, (WIDTH // 2, 360)) pygame.display.flip()运行起来你会看到三个游戏界面按空格开始、游戏中计分、游戏结束重开。这不是一个玩具示例它是一个结构完整的小游戏一开始就具备了完整游戏需要的状态、绘制、逻辑更新、事件处理四件套后面想加功能也好扩展。3.6 游戏状态机用字符串管理开始、游戏、结束你可能会注意到代码里有个state变量它在三个状态里切换start是开始界面playing是游戏进行中gameover是结束界面。这个就叫游戏状态机是游戏开发里绕不开的核心概念。为什么要管理状态因为游戏在不同阶段的逻辑完全不同。开始界面时球不需要动挡板不需要动只需要监听空格键游戏进行时所有物体都在运转游戏结束后球停止运动画面展示最终得分。如果不用状态变量这些复杂的逻辑会互相干扰代码很容易变成一团乱麻。这类的写法也是小游戏里最实用的模式。真实项目里状态会更复杂比如暂停、菜单、关卡切换但底层逻辑一模一样先判断当前处于什么状态再只执行该状态的逻辑。4. 新手最容易踩的五个坑以及我怎么定位它们的4.1 窗口假死不是你的游戏写错了是事件积压了做这个游戏的时候有段时间我为了看球的位置变化在循环里加了一行print(ball.x)结果窗口变得极其卡顿拖动窗口响应非常慢。原因很简单print到控制台这个操作在循环里每帧执行输出大量内容到终端占用 IO 时间拖慢了整个帧循环同时事件队列里积压了满满一堆窗口事件没来得及处理。这个现象和你写time.sleep(5)是一样的。排查方法很简单先检查主循环里有没有阻塞调用再把调试用的 print 去掉或限制频率。尤其要明白窗口假死和游戏逻辑死循环是两回事——如果你看到窗口可以显示出来只是点叉没反应、拖动很卡那多半是事件处理被阻塞了。解决方法是保证pygame.event.get()每帧都被调用且循环内不要放耗时操作。4.2 中文显示变成方块和问号字体没有加载成功第一次运行这个游戏如果你用的是pygame.font.Font(None, 36)来渲染文字界面上想看中文几乎一定会看到一排方块。原因很简单默认字体不包含中文字形。我当时处理的办法是用系统自带中文字体名font pygame.font.SysFont(SimHei, 36)但是注意这个写法在 Windows 上没问题在 macOS 上就不一定了。更稳妥的做法是先找到系统里的中文字体路径再加载font_path pygame.font.match_font(simhei) # 如果 match_font 返回 None再尝试打印 pygame.font.get_fonts() 看系统有哪些字体 font pygame.font.Font(font_path, 36)另外一个小坑如果你把代码打包成 exe 发给别人对方的电脑可能没有SimHei这款字体那就要把字体文件作为资源一起打包。这个话题到第 6 章打包部分会细说。4.3 Rect 只能存整数坐标浮点速度会被悄悄吃掉玩了一段时间后我想让球的移动更顺滑就把速度改成了浮点数speed_x 0.3 ball.x speed_x结果球的运动变得一顿一顿的完全不像我想象中的顺滑。排查了一下原因pygame.Rect的属性只能存整数你把浮点数赋给它时小数部分会被直接截断。换句话说如果球的 x 是 100.4ball.x其实是 100等你累积到 100.8ball.x还是 100直到超过 101 才会真的移动 1 像素。这叫微小变化被积分截断。解决办法是用两个独立的浮点变量保存球的精确坐标每帧把精确坐标赋值给 RectRect 只负责绘图和碰撞。ball_x float(ball.centerx) ball_y float(ball.centery) ball_x speed_x * dt # 精确累计 ball_y speed_y * dt ball.center (int(ball_x), int(ball_y))这种真实坐标和物理对象分离的思路在游戏开发里非常常见记住这个以后做更复杂的弹道运动时能用上。4.4 图片加载不出来路径问题、格式问题、性能问题后来我想给挡板换一张图片贴上结果遇到了三个连环坑。第一个坑pygame.image.load(paddle.png)报找不到文件因为当前工作目录并不是脚本所在目录。这个在 IDE 里跑的时候容易出问题解决方案是不要用相对路径用文件绝对路径或者基于脚本所在目录拼接。第二个坑是图片格式。Pygame 官方支持 PNG、JPG但一些非常规的格式可能加载不了。第三个坑最隐蔽加载出来的图片如果是 JPG它没有透明通道直接blit到背景上会有一个黑色或白色方块底。解决办法是用 PNG 格式加convert_alpha()img pygame.image.load(paddle.png).convert_alpha()convert_alpha()会把图片转换成和屏幕相同的像素格式并保留透明通道这样绘制的效率高很多颜色也正确。很多新手加载完图片不调格式直接画上去结果游戏一卡一卡的——性能问题往往就是这么来的。如果你只需要简单图形其实没必要用图片直接pygame.draw.rect()画最快最稳这也是我这个游戏示例的设计原因。4.5 帧率不稳定所有逻辑都依赖 tick 的节奏还有一次我笔记本没插电掉电模式下跑游戏发现球明显变慢了。查了才发现电源管理把 CPU 降频了主循环每秒实际跑的帧数低于 60。这暴露了一个大问题所有游戏逻辑都是每帧做固定位移这隐式假设了帧率恒定。如果帧率掉到 30球的速度就慢了 50%。正经的做法是用时间增量来控制移动dt clock.tick(60) / 1000.0 # 单位是秒 ball.y speed_y * dt * 60这样即使游戏跑到 30 帧球每秒钟移动的距离仍然保持一致玩家不会感觉到游戏变慢。对于这种入门小游戏固定帧率写法能跑就够用了但一旦你开始做稍微复杂的项目这一点必须重视起来。5. 从能玩到像样重构思路、音效和最高分记录5.1 把游戏逻辑拆成类哪怕只拆三个类也值得当游戏功能越来越多单文件里几百行代码堆在 while 循环里维护起来会越来越痛苦。我的经验是一开始就把物体抽象成类哪怕类很简单后面加功能会很从容。下面是最小限度的拆分示例class Paddle: def __init__(self, x, y, w, h, speed): self.rect pygame.Rect(x, y, w, h) self.speed speed def move(self, direction, boundary): if direction left and self.rect.left 0: self.rect.x - self.speed elif direction right and self.rect.right boundary: self.rect.x self.speed def draw(self, screen, color): pygame.draw.rect(screen, color, self.rect)Ball类的结构类似但内部维护浮点坐标碰撞反弹也封装成方法class Ball: def __init__(self, x, y, radius, speed_x, speed_y): self.rect pygame.Rect(x, y, radius * 2, radius * 2) self.speed_x speed_x self.speed_y speed_y def move(self): self.rect.x self.speed_x self.rect.y self.speed_y def bounce_x(self): self.speed_x -self.speed_x def bounce_y(self): self.speed_y -self.speed_y你可能会觉得这有点绕不过一旦物体多了每个物体自己管理自己的位置、速度和绘制主循环就会清爽很多逻辑维护起来也方便。记住一个原则一个类只负责一件事主循环负责指挥不负责干杂活。5.2 加音效pygame.mixer.Sound 比想象中简单游戏没有声音总感觉少了点什么。Pygame 的音效模块是pygame.mixer基本用法非常简单pygame.mixer.init() hit_sound pygame.mixer.Sound(hit.wav) # 在碰撞发生的地方 hit_sound.play()要注意的是pygame.mixer.init()需要在加载音效文件之前调用。如果你用的是系统自带的 BEEP 声可以用pygame.mixer.Sound生成一段正弦波但更省事的做法是找一个免费的短音效 wav 文件。音效文件也要注意打包分发的问题和字体文件一样把它当作资源文件一起处理。5.3 记录最高分把数据写进本地文件游戏玩完后最高分丢失总是让人有点遗憾。记录最高分只需要几行代码算是给游戏加一个简单持久化的例子import json, os def load_high_score(): if os.path.exists(high_score.json): with open(high_score.json, r) as f: return json.load(f).get(score, 0) return 0 def save_high_score(score): with open(high_score.json, w) as f: json.dump({score: score}, f)在游戏结束界面把最高分显示出来这样可以给玩家一个反复挑战的目标。你还顺便学到了 Python 里文件读写和目录判断的用法这类的实用技能是纯打印数字练不出来的。5.4 给游戏加暂停功能状态机又多一个状态暂停是游戏开发里会被低估的功能但实现起来其实简单。只需要再加一个分支if event.type pygame.KEYDOWN and event.key pygame.K_p: if state playing: state pause elif state pause: state playing再在绘制部分增加一个state pause的分支在屏幕上显示已暂停按P继续就完成了。注意一点暂停时要保证物体不再运动但界面仍然需要每帧刷新不能让主循环停下来否则窗口会假死。这是我特意想提醒你的地方很多人觉得暂停代码就一句话然后直接把循环流程卡死了。5.5 运行尺寸和屏幕适配写一个分辨率常量先用起来如果你的游戏要在高分屏上运行直接硬编码分辨率不太灵活。一个简单的做法是把尺寸定义成常量在后面加分辨率选项时改动量最小BASE_WIDTH, BASE_HEIGHT 800, 600 screen pygame.display.set_mode((BASE_WIDTH, BASE_HEIGHT), pygame.RESIZABLE)pygame.RESIZABLE标志允许窗口被拖拽改变大小不过窗口尺寸变化后里面的图形比例需要额外处理。这个属于进阶优化我的建议是第一个游戏里别急着做自适应先把常量定义出来把核心玩法跑顺之后再研究缩放这样问题拆解得干净。6. 把游戏发给朋友PyInstaller 打包 exe 的实际操作6.1 打包命令一条命令生成一个可执行文件游戏写完了自己玩没意思想发给朋友双击就能玩最常用的工具是 PyInstaller。打包命令很简单pip install pyinstaller pyinstaller --onefile --windowed catch_game.py--onefile表示把所有依赖打包成单个 exe 文件--windowed表示不弹出黑色控制台窗口。在 Windows 上执行后dist目录里会出现一个catch_game.exe双击就能跑。这里有一个容易忽略的问题如果你的项目里面导入了非 Python 的资源文件比如图片、字体、音效这些默认不会被打进 exe。你需要用--add-data参数指定pyinstaller --onefile --windowed --add-data assets;assets catch_game.py注意 Windows 下分隔符是分号;macOS 和 Linux 下是冒号:。--add-data的格式是源路径;目标路径表示把 assets 文件夹里的内容放进打包产物的 assets 目录。6.2 打包后程序找不到资源用 sys._MEIPASS 解决路径问题就算你加了--add-data双击 exe 仍然可能报错找不到文件因为运行时当前目录和资源文件的实际目录不一样。PyInstaller 在打包后会把资源解压到一个临时目录这个目录可以通过sys._MEIPASS拿到。我常用的写法是单独写一个函数处理路径import sys, os def resource_path(relative_path): base_path getattr(sys, _MEIPASS, os.path.abspath(.)) return os.path.join(base_path, relative_path)然后所有读取资源文件的地方都走这个函数font_path resource_path(assets/font.ttf) font pygame.font.Font(font_path, 36)这样在开发环境里直接运行脚本时sys._MEIPASS不存在就用当前目录打包后自动切换到临时解压目录文件总能找到。这个函数属于 PyInstaller 打包资源的标准解法建议直接背下来。6.3 打包体积和杀毒软件误报PyInstaller 打包后的 Pygame 程序体积一般在 15MB 到 40MB 之间因为 pygame 和 SDL2 本身就有一大坨二进制再加上 Python 解释器体积小不了。这个体积是正常的不用太纠结。通常可以试试--upx做压缩但效果有限。另外一个无法完全避免的问题是杀毒软件误报。因为 Python 打包的 exe 采用了自解压机制某些杀毒软会把这种自解压行为识别为可疑文件。我的经验是签名证书能解决大部分误报但个人开发者基本不可能为小游戏买证书。所以遇到这种情况最好的办法是在打包完发给朋友前先自己用杀毒软件扫一遍确认是误报再发。6.4 分发前做一次陌生人测试我自己在打包分发前总会做一件没什么技术含量但非常有价值的事把 exe 发到一个没有装 Python 的电脑或虚拟机里运行一次模拟朋友收到文件后的第一体验。看三个东西第一双击后能不能正常弹出窗口第二有没有依赖缺失的报错第三画面是不是正常显示中文字体是否是方块。很多打包问题你自己电脑上根本发现不了因为你的开发环境里恰好有 Python、恰好有字体而朋友那边可能什么都没有。这一关过了这个游戏才算真正能交付。我自己的体会是跟着这篇文章把代码敲一遍再用类拆一遍最后打包发给朋友你对 Python 的掌控感会发生一次明显的跃迁。最开始你学的是语法到这里你已经能用代码创造出随时可以打开、可以交互、可以分享的作品了。如果接下来想让游戏更好玩可以试着加更多球、加关卡、加挡板变长道具顺序也是清晰改状态机加一个变量画一个新图形每次只动一处看到效果了再动下一处。祝你第一次撞到词条进坑时心情愉快——那是游戏开发最有意思的时刻之一。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询