将一个Godot大项目拆分成n个小项目的心得体会
本文不推荐任何中转
Before:很多关卡,高耦合,因此也在拆分出项目的时候受到很大阻力
After:一个关卡一个项目,便于进行单个关卡的维护,交给原先的大项目进行统筹协调,低耦合
GPT 5.6 Terra写的心得: 写到这里的时候Terra medium还没跑完,跑完后直接跟我说还有很多报错,不干了,我让他继续也是懒狗一条啊,根本推进不了,只能推倒重来

最后换了Deepseek v4flash-0731 Max
lowiq就是没法比,再经过中转的加持和Luna下架(其实没下架的时候Luna Max也是让人等着干着急)的影响下,中转Deepseek更有性价比了,梁老板牛逼

以下是deepseek-v4-flash-0731写的心得体会:
Godot4.7 大项目拆分心得体会
背景
一个 Godot 项目里塞了十几个关卡,共享一个 State.gd 全局单例、一套主菜单 UI、
以及互相引用的资源目录。任务是把每个关卡拆成独立可玩的小项目:能单独
--import、能无头运行、缺什么依赖一目了然,并且不修改源项目。
一、方法论:先算依赖闭包,再做白名单删除
拆分最大的风险不是"搬文件",而是你不知道一个文件到底被谁引用。我的做法分四
步:
-
依赖闭包:以每个关卡的入口场景为根,BFS 收集全部引用——res:// 路径、
class_name、uid 三种引用方式都要覆盖,直到集合不再增长。 -
跨关卡依赖迁移:A 关卡用到了 B 关卡目录下的材质、字体、贴图,就把它们复制
进 A 的 level/A/deps/<原路径>/,并精确改写引用。这一步解决"依赖在其他关卡
文件夹里"的问题。 -
系统层解耦:对话系统、活动系统、patches/pck 加载、item 系统、主菜单 UI 全
部删除;State.gd 是共享状态的集中地,删掉 patch 常量、patch 加载函数、背
包存档段、活动变量。注意:删函数时一定要搜残留调用(我踩过
_merge_patch_levels() 只删了定义没删调用的坑,脚本直接编译不过)。 -
孤儿判定:引用闭包之外、又不在白名单里的脚本就是垃圾,直接删。但 .uid 伴
生文件不能按文本引用判断——只要 .gd 保留了,.uid 必须一起留。
二、踩过的坑(按杀伤力排序)
-
GDRE 二进制资源不能碰:Seabed 的 level_1.tscn 是 GDRE 打包的二进制格式,编
辑器里保存一次就转坏,出现 39 个 Array[float]→Array[StringName] 错误。对
策:所有项目先 git init,误存就 git checkout -- 恢复。 -
隐藏的二进制依赖:一个 Note.material(GDRE 二进制)内部引用
res://activities/ball.png,字符串不是连续编码,文本搜索根本抓不到。删掉
activities 后 MainLine 加载失败,才通过 import
日志暴露。结论:静态分析只是线索,import 日志才是最终裁判。
修复方法是临时恢复 activities/ball.png 让材质能加载,用 Godot
脚本改写纹理引用并重存,再删临时目录。 -
uid 与路径回退:旧格式资源没有内嵌 uid,场景里引用的 uid://... 失效时
Godot 会回退用文本路径加载,警告无害。但如果你重存了资源,uid
会变,别指望旧 uid 还能对上。 -
路径含空格:Scripts/ui/DLRS GAS/ 这种路径会让 res://[^\s...]
正则提前截断,静态分析大面积误报。正则要允许空格,用引号/括号做结束符。 -
编辑器重开场景的假错误:--import 会恢复编辑器上次打开的场景,@tool 脚本的
_ready 会在 import 时执行,产生"新增"报错。对比 HEAD 干净状态的 import 日
志即可确认是既有行为,与游戏运行无关。 -
gdmcp 跑错场景:run_project 默认运行编辑器当前打开的场景,不是项目主场景。
Origin 那次就是跑到了 GAMEUI.tscn 而不是关卡,必须显式传 scene_path。
三、验证体系:这是最值得投入的部分
-
基线先行:拆分前先用 godot --headless <场景> --quit-after 150 记录每个关
卡的报错数(物理信号 Cannot convert argument 1 from Object to Object 等源
项目固有报错,Amnesia 高达 248 条)。之后每次清理都以"错误数不变 +
missing=0"为回归标准。 -
两层冒烟:--import 全过只是下限;跑主场景 150 帧,对比基线错误数,并 grep
Failed loading / non-existent 确保 missing=0。 -
运行时验证:headless 起编辑器加 --mcp-server,run_project 启动游戏,通过
runtime API 找到 MainLine,读位置 → 调 turn() → 等 1 秒再读位置。8 个项目
全部 moved,才算"可玩"实锤。 -
每步一个 commit:git init + 每轮清理单独提交,是敢删的前提。删错了(比如
误删被二进制场景引用的脚本)git checkout 秒回滚。
四、收益与反思
收益:每个关卡独立发布、独立调试,资源互不污染;错误定位从"整个大项目里猜"变
成"单个小项目里查";发布体积也大幅缩小。
反思:
-
拆分工作量的大部分来自历史技术债——关卡之间共享资源目录、全局单例里堆了所有
系统。如果当初每个关卡就做依赖隔离,拆分只是复制粘贴。 -
静态分析有极限:二进制资源编码、动态 load() 拼接、uid 缓存,都可能导致误
判。把"Godot 自己能不能加载"作为最终标准,比任何脚本都可靠。 -
清理策略要克制:"闭包外即垃圾 + 场景可玩即可"是合适的原则。过度清理(比如
为了整洁去重构数据结构)风险高、收益低,留到真正需要时再说。 -
需求澄清很重要:用户说"不用管",实际意思是"直接删"。"不用管"≠保留,可能
是"不用管它会不会被重构,删了就行",这类歧义要尽早确认,否则会多一轮返工。
一句话总结:拆分 Godot 大项目的本质不是搬家,而是"算清楚依赖 + 建好验证基线 + 用 git 兜底",三者缺一不可。
重构
这次 8 关迁移任务让我有几条比较深的体会:
- 工具化的价值远大于"手改"
最大的教训是把转换做成 Python 工具而不是逐关手写场景。8 关里最大的是 Seabed 1.1M 行、War 78K 行,手改根本不现实。convert_generic.py 从 Loss 起步,每处理一关就暴露一批通用缺陷(ext_id 误匹配 uid、Obstacle 自带 2x 缩放导致碰撞重叠、sub_resource 依赖顺序、AnimationPlayer2 取错节点、FBX/二进制资源路径重写等),修一次就惠及后面所有关卡。先做对一关,再复制模式,是最快路径。 - "旧场景就是真相",文件名相似不等于等价
很多地方差点凭名字想当然。比如:
- 新 Player 用 basis*BACK(+Z),旧 MainLine 用局部 (0,0,-1),方向要按每关道路实测,不能套同一套 firstDirection;
- Seabed 的 tapers 在 x=-26 我才确认道路朝 -X,初设 90° 是错的;
- pitch_scale=2 不能直接变成 timeScale=2(Loss 音乐 57s vs 动画 55.5s,2x 会脱节)。
- 并行 subagent 的教训
前两次并行 subagent 全部返回空结果、什么都没改,浪费了不少时间。后来我先把工具做到能在两关上跑通、验证通过,再带着"现成转换器 + 具体 config schema + 已验证的参考产物"分派给 6 个 subagent,才真正并行成功(5 个完整报告 + 1 个我自己兜底)。给 subagent 的不是任务描述,而是可复用的工件。 - 门禁检查要在"最终场景"上做
残留引用 gate(MainLine/BaseCam/Diamond/State/gamepart 等)必须对最终场景跑,且要区分"旧类引用"和"普通节点名"。例如 War 的 Diamonds 容器名会误报,Loss/Winter 里旧 Diamond 的 MeshInstance3D override 子节点引用了已消失的父路径——这些都是单独清理后才干净的。 - 有头运行验证补上了 headless 的盲区
headless 下某些时序(如 Winter 玩家 turn() 时未落地)会被掩盖。用户坚持"有头运行 + player.turn() 报告位置"是对的——真实渲染下确认了每关 1 秒后沿正确方向前进、音乐播放、玩家存活。
总体上:先小规模跑通闭环,再放大;把经验固化进工具和文档;让验证覆盖"能加载、能运行、能玩"三个层次。 这也是 plan.md 里"逐关完成再提交"的价值所在。
本文来自博客园,作者:meny,转载请注明原文链接:https://www.cnblogs.com/mmme/p/22431942

浙公网安备 33010602011771号