无头脚本与命令行工具
extends SceneTree 的入口与生命周期、命令行参数解析、print 约定与退出码、CI 与批处理消费
知识点地图
- 知识类别:GDScript 的无头(headless)脚本与命令行工具——SceneTree 脚本的编写与生命周期、用户参数解析、输出与退出码约定、被 CI 与批处理消费的方式。
- 解决什么问题:批量生成/校验/修复数据这类工作不想靠手点编辑器;把 Godot 当”带引擎能力的脚本运行时”用(读资源、跑物理、写文件),且产出能被自动化流水线判定成败。
- 什么时候用到:程序化生成关卡、批量校验资源完整性、数据迁移与夜间修复、把”编辑器里手动做”变成”流水线上自动跑”。
- 与 044-godot 的分工声明(跨批次重叠切割):GDScript 语言层——SceneTree 脚本怎么写、_initialize 生命周期、OS.get_cmdline_user_args 的解析规则、print 约定与 quit 退出码——归本篇(045-118);引擎工具链工作流——
--headless/--import/--quit-after参数表、导入门禁、无头冒烟与 CI 门禁编排——在 044-130《导出项目》3.1 节(已落地),计划的 044-127《调试与性能分析》承接 Debugger/Profiler。执行顺序协调约定:045 侧先落地;044-127 后落地时按本篇与 044-130 的实际内容调整边界,—headless 参数表不重复写,本篇只引用。与 105 篇的分工:那篇讲编辑器内调试(print 谱系、assert、断点),本篇讲脱离编辑器的运行形态。
前置知识
- 《文件 IO 与数据持久化》(115 篇)——工具脚本的主要产出物是文件;
- 《调试与断言》(105 篇)——print 谱系与 push_error 的通道语义;
- 场景树基本概念(044-godot 的节点与场景篇)。
学习目标
- 会写 extends SceneTree 的无头脚本,理解 _initialize 与 _finalize 的调用时机;
- 会用 OS.get_cmdline_user_args 解析 —key=value 参数;
- 会按”print 约定输出 + quit(0/1) 退出码”写出可被 CI 判定的工具;
- 能把工具接进批处理脚本与 CI 流水线。
1. 心智模型:把 Godot 当脚本运行时
无头脚本的本质:不打开编辑器、不开窗口,用 Godot 二进制执行一个脚本。Godot 引擎在此形态下仍提供完整能力——加载 .tres/.tscn 资源、跑 JSON/文件 IO、甚至起一个最小场景树——所以工具脚本”生在引擎里”:能直接读项目的资源、用引擎的序列化与数学库。
编辑器里手动做:开编辑器 -> 点菜单 -> 人工检查结果
无头脚本做:godot --headless --script res://tools/xxx.gd -- --in=... --out=...
-> 秒级完成 -> 退出码告诉流水线成败选择判断:一次性手工操作(改一个场景属性)用编辑器;会重复两次以上的批量操作,写成无头工具。speed-rouge 的 tools/ 目录下 20+ 个 GDScript 工具(生成、审计、校验、迁移)全部同构,就是这条纪律的产物。
2. SceneTree 脚本的入口与生命周期
# res://tools/my_tool.gd
extends SceneTree
func _initialize() -> void:
# 脚本入口:SceneTree 就绪,资源系统可用
var args := OS.get_cmdline_user_args()
print("收到参数:", args)
# ...工具主体...
quit(0) # 显式退出并带退出码
func _finalize() -> void:
# 进程退出前的收尾(清理临时文件等),可省略
passgodot --headless --script res://tools/my_tool.gd --path . -- --seed=7 --out=user://gen.json讲解:
extends SceneTree取代了游戏脚本的extends Node——工具脚本自己就是一棵场景树的宿主,没有 _ready/_process 生命周期,入口是_initialize():树初始化完成、资源系统就绪后调用一次。_finalize()在退出前执行,适合清理(删临时目录、关文件);大多数工具用不到它,quit 之前做完所有事是更常见的形态。--headless让引擎不开窗口不起渲染——工具脚本基本必带;--path .指定项目根(资源路径的解析基准);--script指向脚本。参数表的完整语义与更多开关(—import、—quit-after 等 CI 用法)在 044-130 §3.1,本篇不重复。--分隔符是关键细节:--(或++)之后的参数才是”用户参数”。之前的--headless/--path被 Godot 自己消费,之后的--seed=7由你的脚本处理——混在一起解析就会把引擎开关误当业务参数。- 易错点:忘写 quit() 时脚本执行完 _initialize 后进程会一直挂着(SceneTree 还在跑主循环)——每个工具脚本的 _initialize 都以 quit 结尾是纪律。
3. 参数解析:OS.get_cmdline_user_args
func _parse_args() -> Dictionary:
var opts := {}
for raw in OS.get_cmdline_user_args():
var arg := raw.lstrip("--")
var kv := arg.split("=", true, 1)
if kv.size() == 2:
opts[kv[0]] = kv[1] # --seed=7 -> {"seed": "7"}
else:
opts[kv[0]] = true # --dry-run -> {"dry-run": true}
return opts
func _initialize() -> void:
var opts := _parse_args()
var seed_v := int(opts.get("seed", "0"))
var dry_run: bool = opts.has("dry-run")讲解:
- 解析器只有十几行:带
=的是键值对,不带的是布尔开关。类型转换在使用处做(int(…))——参数天生是字符串,解析层不做隐式转换,坏参数在使用时报错比在解析层猜类型可靠。 - speed-rouge 的 progen_level.gd(tools/progen_level.gd:1-14 范式,全仓 20+ 脚本同构)就是这个形态:
--seed控制确定性、--out控制产物路径。确定性 RNG 是生成类工具的灵魂:rng.seed = seed_v让同一参数永远产出同一结果——可复现才可测试、可回滚。 - 参数即契约:工具支持哪些参数要写进脚本头注释(或 —help 分支),批处理脚本按契约调用——参数是 CLI 工具的接口,和函数签名同等对待。
4. 输出约定与退出码
func _initialize() -> void:
var ok := _generate_and_validate()
if ok:
print("PROGEN PASS: level_07.json")
quit(0)
else:
print("PROGEN FAIL: gap too wide at col %d" % fail_col)
quit(1)讲解:
- 退出码是 CLI 的通用语言:0 成功、非 0 失败。bash 的
&&、CI 的 job 判定、批处理的errorlevel都只认它。print 的 PASS/FAIL 前缀是人读的部分,退出码是机器读的部分——两层齐全,CI 日志既可自动判定又能人工复查。 - 约定分层:stdout 走”结果输出”(PASS/FAIL、产物清单),push_error 走”错误详情”(进 stderr,见 105 篇通道语义)。流水线可以只看退出码,排查时再翻 stderr。
- fail-fast 与完整报告的取舍:校验类工具优先收集全部问题再退非零(一次跑出完整清单),生成类工具 fail-fast(坏了就停,不留半成品文件——半成品用 .tmp 承载,见 115 篇原子写)。
5. CI 与批处理消费
# 批处理脚本形态(bash)
set -e # 任一工具非零退出即中止
godot --headless --script res://tools/progen_level.gd --path . -- --seed=7 --out=res://levels/lv7.json
godot --headless --script res://tools/check_levels.gd --path .
godot --headless --script res://tools/dump_tile_usage.gd --path . > usage.txt讲解:
set -e让链条上任何一个非零退出码中止整条批处理——工具的 quit(1) 就是被这样消费的。GitHub Actions 里同一串命令放进 step,step 失败即 PR 红。- 工具的输出可以再喂给下一个工具(重定向、读上一环的产物文件)——CLI 工具组合成流水线的能力来自”参数进、文件出、退出码判”这个最小契约。
- speed-rouge 的门禁全景(import 0 错、17 个 check 脚本、截图回归)在 028-software-testing 模块有完整案例;本篇的分工是”怎么写出一个合格的工具”,门禁怎么编排去那边看。
6. 三个真实场景
场景一:程序化关卡生成器(真实工程 progen_level.gd)。 段长、断口宽度、难度曲线全由参数控制,--seed=7 保证可复现;生成后跑四条准入自检(断口可跳、出生点有地、平台不嵌地、桥面无撞),全过才落盘并打 PROGEN PASS。 CHANGELOG 里”progen 全 PASS”是发布门禁的一项——生成器从”作者的玩具”变成”团队的基建”,靠的正是退出码契约。它与 020-algorithm 模块的”作关语法”是同一件事的两面:那边讲生成规则怎么设计,这边讲工具怎么包装成可自动化的形态。
场景二:批量资源完整性校验。 美术提交了一批新图集后,跑 check_resources.gd:遍历资源目录,校验每个 .png 的尺寸是瓦片网格的整数倍、每个 .tres 引用的贴图存在、命名符合约定;输出违规清单并 quit(1)。它在”美术提交”与”程序合入”之间立了一道自动闸门——问题在提交当天暴露,而不是在打包或运行时炸出来。
场景三:夜间数据修复脚本。 线上存档格式升级后,玩家的本地 JSON 有一批缺新键。写 migrate_user_data.gd:--dir 指向存档目录,逐文件读入、补缺省键、校验结构、原子写回;dry-run 模式(--dry-run 开关)只报告不落盘。夜跑到生产环境前先 dry-run 出报告,人工过目再真跑——CLI 工具的参数开关在这里充当”危险操作的保险栓”。
7. 动手实践
任务一:最小工具骨架。 写一个 extends SceneTree 的 echo 工具:解析 --msg=... 参数原样打印并 quit(0);无参数时打印用法说明并 quit(1)。在命令行分别以带参与不带参运行,用 echo $?(bash)验证退出码。提示:Windows 下 Git Bash 的 echo $? 或 PowerShell 的 $LASTEXITCODE 都能读退出码;-- 分隔符别忘了。
任务二:确定性生成验证。 给任务一加一个 --seed 参数,用它初始化 RandomNumberGenerator 生成 10 个随机数并打印;同一 seed 跑两次对比输出,不同 seed 再跑。提示:确定性来自”seed 决定 RNG 状态”,工具的任何随机行为都应经过这个 RNG 而不是直接调随机全局函数。
任务三:目录校验工具。 结合 115 篇任务三的 JSON 扫描脚本,把它改造成完整 CLI 工具:--dir 参数指定目录、输出 SCAN PASS/FAIL、退出码承接、损坏清单写报告文件。提示:从 115 篇参考实现出发,改三处——参数解析、输出前缀、退出码。
先自己操作,再对照参考实现:
任务一与任务三参考实现
# 任务一:res://tools/echo_tool.gd
extends SceneTree
func _initialize() -> void:
var opts := _parse_args()
if not opts.has("msg"):
print("用法:godot --headless --script res://tools/echo_tool.gd -- --msg=文本")
quit(1)
return
print(opts["msg"])
quit(0)
func _parse_args() -> Dictionary:
var opts := {}
for raw in OS.get_cmdline_user_args():
var kv := raw.lstrip("--").split("=", true, 1)
opts[kv[0]] = kv[1] if kv.size() == 2 else true
return optsgodot --headless --script res://tools/echo_tool.gd --path . -- --msg=hello
# 输出 hello,echo $? 为 0
godot --headless --script res://tools/echo_tool.gd --path .
# 输出用法,echo $? 为 1# 任务三的关键收尾(承接 115 篇扫描逻辑,此处只列成败收口段)
if broken.is_empty():
print("SCAN PASS: %d files ok" % checked)
quit(0)
else:
print("SCAN FAIL: %d broken, see user://report.txt" % broken.size())
quit(1)要点:a) 无参数时的”用法说明 + 非零退出”是 CLI 礼仪——让调用方(无论人还是脚本)在第一秒知道怎么正确使用;b) 解析器与 118 篇正文同构,可抽成公共基类或 preload 的工具脚本供全仓工具复用(speed-rouge 的 20+ 工具正是同构复用);c) PASS/FAIL 前缀让人扫日志,退出码让脚本判成败,报告文件承载细节——三层各司其职。
8. 小结
- extends SceneTree 的无头脚本以 _initialize 为入口、quit(码) 收尾,忘写 quit 进程会挂着;—headless 与参数表语义见 044-130,本篇不重复。
--/++之后的才是用户参数,OS.get_cmdline_user_args 取出后自行解析键值对与开关;参数是工具的接口契约。- print 约定输出(PASS/FAIL 前缀人读)+ quit(0/1) 退出码(机器判)+ 报告文件(细节存)三层分离;校验类收集全量问题再退非零,生成类 fail-fast。
- 生成类工具用 seed 做确定性 RNG,可复现是可测试与可回滚的前提;批处理用 set -e 消费退出码串成流水线。
参考链接
- Godot 官方文档:命令行教程(CC BY 3.0)
- Godot 官方文档:OS 单例(get_cmdline_user_args,CC BY 3.0)
- speed-rouge 项目素材:tools/progen_level.gd(1-14 行范式)及 tools/ 下 20+ 同构工具
- 门禁编排全景见 028-software-testing 模块与 044-godot《导出项目》