ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

一个 Python 新手,靠 AI 帮忙把贪吃蛇做成了“蛇娘“游戏(5/5):给 exe 装个“黑匣子“,抓到一个只差 1 像素的崩溃

一个 Python 新手,靠 AI 帮忙把贪吃蛇做成了“蛇娘“游戏(5/5):给 exe 装个“黑匣子“,抓到一个只差 1 像素的崩溃 系列《一个 Python 新手靠 AI 帮忙把贪吃蛇做成了蛇娘游戏》共 5 篇这是最后一篇。讲打包、一次真实的崩溃排查以及我这个新手的收官感想。前四篇讲完了架构、画面、战斗和内容。这最后一篇我想讲一个真实发生、把我折腾得够呛的 bug——它也是我整个开发过程里学到最多的一次。事情是这样的游戏做得差不多了我用 PyInstaller 打包成一个.exe希望可以自己体验一下。结果开始游戏中点到了无尽模式游戏直接报错崩溃了。我当时就懵了。这一篇就是我怎么从这个毫无头绪的困境里一步步抓到真凶的全过程。如果你也遇到过开发时好好的、一打包就崩、还啥都不报的诡异问题希望这篇能帮到你。一、噩梦开始exe 崩了却一个字都不肯说我这边选无尽模式就闪退、没有任何提示。作为一个新手我的第一反应是慌——没有报错信息我连从哪查起都不知道。我把情况描述给 AI它问了我一个关键问题你打包的时候是不是设置了consoleFalse 我说是因为游戏窗口带个黑乎乎的命令行窗口太丑了。它一针见血这就是问题所在。consoleFalse的 exe程序崩溃时不会像命令行那样把错误打印出来而是直接静默退出。你现在等于蒙着眼睛找 bug。我恍然大悟。原来在 PyCharm 里跑报错会打印在控制台可打包成没有控制台的 exe 后异常无处可去程序就直接消失了。不是没报错是报错被我关掉了。二、第一步不是修 bug而是让崩溃说话我以为 AI 会直接帮我猜哪里错了结果它给了一个我当时没想到的建议先别急着找 bug。你现在最该做的是让程序崩溃的时候能把错误信息留下来。它让我写一个崩溃兜底模块——我把它理解成给游戏装了个黑匣子就像飞机那样出事了能回溯。这个模块干两件事把完整的错误堆栈写进一个日志文件再弹一个系统消息框告诉玩家出错了而不是让窗口默默消失。下面是这个crash.py的核心我删了一些边角保留了主干# 教学简化版·非完整可运行·by 【外收内放】 import os, sys, time, traceback, ctypes def report_crash(excNone, context): 崩溃时把完整 traceback 写进 crash.log再弹一个系统消息框。 tb .join(traceback.format_exception(type(exc), exc, exc.__traceback__)) # ① 追加写到 exe 同目录的 crash.logBASE_DIR exe 所在目录历次崩溃都留痕 with open(os.path.join(BASE_DIR, crash.log), a, encodingutf-8) as f: f.write(f崩溃时间: {time.strftime(%Y-%m-%d %H:%M:%S)}\n) f.write(f上下文: {context}\n{tb}\n) # ② 弹一个 Windows 原生错误框0x10错误图标, 0x1000置顶取 traceback 最后一行当摘要 ctypes.windll.user32.MessageBoxW( 0, f出错了\n{tb.strip().splitlines()[-1]}, 贪吃蛇娘化版, 0x10 | 0x1000) def install_excepthook(): 兜住任何漏网的异常连主循环之外的都不放过。 sys.excepthook lambda etype, value, tb: report_crash(value, sys.excepthook)然后我把它接到游戏主循环里——把每一帧的处理输入 → 更新 → 绘制整个包进try一旦哪个界面崩了就记下是哪个界面崩的# 教学简化版·非完整可运行·by 【外收内放】 try: scene.handle_events(events) scene.update(dt) scene.draw() except Exception as e: # 绝不让你打包的 exe 静默闪退 report_crash(e, contextfscene{type(scene).__name__}) self.running False说句实话这段代码里的ctypes.windll、traceback.format_exception、sys.excepthook我一个都不熟基本都是 AI 写的。但我看得懂它在干嘛——把错误写下来 弹出来这就够了。新手用 AI 的一个正确姿势我觉得就是这样不一定要会写每一行但要能看懂它替你做了什么。三、黑匣子立大功拿到真凶了我重新打包了一个带黑匣子的 exe 再试玩一遍再点一次无尽模式。这次游戏崩溃时弹出了一个错误框exe 旁边还多了一个crash.log。我查看了日志发给AI内容大概是这样 崩溃时间 : 2026-10-02 00:48:37 上下文 : sceneSceneSelectScene Python : 3.14.7 打包运行 : True Traceback (most recent call last): File scenes\scene_select.py, line 164, in _draw_card img img.subsurface(pygame.Rect(0, top, preview.w, preview.h)).copy() ValueError: subsurface rectangle outside surface area看到没上下文 sceneSceneSelectScene直接告诉我是选场景这个界面崩的最后一行ValueError: subsurface rectangle outside surface area子表面矩形超出了表面范围就是错误本身。我之前完全看不懂这个错。AI 解释subsurface是从一张大图上裁一块小的这个报错的意思是——我要裁的范围比图片本身还大裁到外面去了。当时崩溃就发生在进入这里之前的一个界面正常点击无尽是可以进入当前界面的然而却崩溃了。四、根因一个只差 1 像素的取整误差顺着这条线我找到了崩溃的地方选场景界面要给每张地图卡片画一个预览小图做法是先把背景图缩放到卡片宽度再从中间裁一块。问题出在缩放这一步。我的缩放函数get_scaled里是这么算目标宽度的# 教学简化版·非完整可运行·by 【外收内放】 ratio width / w # 目标宽 / 原图宽 # ❌ 我最初的写法用 int() 截断 surf smoothscale(base, (int(w * ratio), int(h * ratio)))AI 让我盯着int(w * ratio)这行看。它说你请求把图缩放到width宽但int()是直接砍掉小数。由于浮点数计算有微小误差w * ratio有时会算出类似360.9999999这样的值int()一砍就变成360——比你请求的 361 少了整整 1 像素。而下游裁剪时却仍然按卡片宽度 361去subsurface。图片实际只有 360 宽你要裁 361就多裁了这 1 像素 → 越界 → 崩溃。真凶就是这看不见的 1 个像素。五、解开谜团这才是这个 bug 最阴险的地方也是我最想分享的教训。我后来才想明白卡片预览图的宽度preview.w是根据窗口分辨率算出来的。而取整会不会恰好差 1 像素完全取决于这个宽度的具体数值我的自动化测试固定跑在一个分辨率下算出的宽度恰好不触发那个 0.9999 的误差我模拟在电脑正常玩的真实分辨率算出的宽度恰好落在会差 1 像素的档位上。所以同一个 bug在本地电脑那儿必崩在程序中测试却是不会显现的。这种依赖特定数值才会触发的 bug靠我这儿跑着没问题是永远发现不了的。AI 教我的办法是与其瞎猜不如写个探针把所有可能的情况扫一遍。我写了个小脚本模拟从 0.5 倍到 2.2 倍的各种窗口缩放等于覆盖各种分辨率逐个检查旧写法会不会越界。结果触目惊心——旧写法在 179 个缩放档位下都会差 1 像素、并复现出和在本地电脑一模一样的ValueError而改成新写法后0 个越界。到这一刻我才 100% 确认自己找对了根因而不是碰运气。六、修复改一个round再加一道保险修复分两步这个两步走的思路也是 AI 教的第一步根治——把int()截断换成round()四舍五入360.9999就会正确地变成 361和图片宽度对上# 教学简化版·非完整可运行·by 【外收内放】 # ✅ 改用 round360.9999… → 361和下游要裁的宽度对齐 surf smoothscale(base, (round(w * ratio), round(h * ratio)))第二步加保险——AI 说根因要修但最好再补一道防御万一将来别的地方又出现类似的取整差也不至于直接崩。 于是我在裁剪前把要裁的宽高死死钳制在图片实际尺寸之内# 教学简化版·非完整可运行·by 【外收内放】 iw, ih img.get_size() cw, ch min(preview.w, iw), min(preview.h, ih) # 裁多大都不超过图片本身 img img.subsurface(pygame.Rect((iw - cw) // 2, (ih - ch) // 2, cw, ch)).copy()修根因 加防御双管齐下这个折磨了我好几天的 bug才算真正了结。这件事让我记住一句话改 bug 不能只求这次不崩了要问同类问题会不会在别处再冒出来。七、顺带聊聊打包PyInstaller 的几个新手坑既然讲到 exe就把打包的经验也一起记下来。我的打包配置.spec文件关键就几行# 教学简化版·非完整可运行·by 【外收内放】 a Analysis([main.py], datas[(assets, assets), (data, data)]) # 把素材和 JSON 配置打进包 exe EXE(..., name贪吃蛇娘化版, runtime_tmpdirNone, # 打成单个 exe 文件onefile upxTrue, # 压缩体积 consoleFalse) # ⚠️ 无控制台——正是它导致崩溃时静默闪退新手打包最容易栽的两个坑忘了打包素材datas里一定要把assets图片和dataJSON带上否则 exe 一跑就找不到文件路径问题打包后程序运行时的当前目录和你开发时不一样了。读取素材的路径要特殊处理打包后资源在一个临时解压目录里而存档、日志这类要写在 exe 旁边。这块我一开始也搞混过是 AI 帮我理清哪些路径跟着 exe 走、哪些跟着资源包走。另外为了不让低级错误浪费一次几分钟的打包我写了个selfcheck.py小工具每次打包前先静态扫一遍# 教学简化版·非完整可运行·by 【外收内放】 # 打包前先跑一遍自检把低级错误挡在打包之前 check_syntax() # ① 每个 .py 能不能通过语法解析 check_settings_imports() # ② from settings import X 的 Xsettings 里真的存在吗 check_json() # ③ data/ 下的 JSON 格式对不对 check_assets() # ④ 代码和 JSON 里引用的图片文件在不在 check_undefined_names() # ⑤ 有没有用了却忘了 import 的名字这五关任意一关报错我就先修再打包省下了无数次打包五分钟、一跑就崩、发现是个拼写错误的冤枉时间。对新手来说把检查也自动化是很值的一笔投入。八、系列收官一个三无新手做下来最大的收获五篇写到这里就收官了。回头看这个项目——一个不会画画、不懂音乐、Python 才学几个月的人居然真的做出了一个有 6 个角色、4 张地图、连招、肉鸽卡池、还能打包成 exe 的游戏。放在一年前我是不敢想的目前游戏大致框架已经做出来了后续可能就是优化了。如果说最大的收获是什么不是某个具体技术而是一套和 AI 一起解决问题的方法遇到不懂的先让 AI 讲原理别急着抄代码——抄了也不踏实讲懂了才敢改出了 bug先想办法让它说话装日志、加打印、写探针而不是盯着代码干瞪眼把重复的事交给代码切图脚本、自检工具、自动试玩哪怕一开始写工具比手动做还慢每次踩坑都记下来——这个系列本身就是我踩坑记录的一部分。我依然是个新手这五篇里一定有很多讲得不严谨、甚至讲错的地方非常欢迎评论区的大佬指正我会认真看、认真改。如果你也是刚起步、想做点东西又怕自己什么都不会的新手希望我这五篇啰嗦的踩坑记录能让你觉得原来他也是一路错过来的那我也可以试试。那就够了。谢谢你看到这里。最后分享一下游戏的抽卡界面。
返回列表