前置知识: Godot 引擎

无头脚本与命令行工具

10 min入门

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、甚至起一个最小场景树——所以工具脚本”生在引擎里”:能直接读项目的资源、用引擎的序列化与数学库。

text
编辑器里手动做:开编辑器 -> 点菜单 -> 人工检查结果
无头脚本做:godot --headless --script res://tools/xxx.gd -- --in=... --out=...
            -> 秒级完成 -> 退出码告诉流水线成败

选择判断:一次性手工操作(改一个场景属性)用编辑器;会重复两次以上的批量操作,写成无头工具。speed-rouge 的 tools/ 目录下 20+ 个 GDScript 工具(生成、审计、校验、迁移)全部同构,就是这条纪律的产物。

2. SceneTree 脚本的入口与生命周期

gdscript
# 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:
    # 进程退出前的收尾(清理临时文件等),可省略
    pass
bash
godot --headless --script res://tools/my_tool.gd --path . -- --seed=7 --out=user://gen.json

讲解:

  1. extends SceneTree 取代了游戏脚本的 extends Node——工具脚本自己就是一棵场景树的宿主,没有 _ready/_process 生命周期,入口是 _initialize():树初始化完成、资源系统就绪后调用一次。
  2. _finalize() 在退出前执行,适合清理(删临时目录、关文件);大多数工具用不到它,quit 之前做完所有事是更常见的形态。
  3. --headless 让引擎不开窗口不起渲染——工具脚本基本必带;--path . 指定项目根(资源路径的解析基准);--script 指向脚本。参数表的完整语义与更多开关(—import、—quit-after 等 CI 用法)在 044-130 §3.1,本篇不重复。
  4. -- 分隔符是关键细节:--(或 ++)之后的参数才是”用户参数”。之前的 --headless/--path 被 Godot 自己消费,之后的 --seed=7 由你的脚本处理——混在一起解析就会把引擎开关误当业务参数。
  5. 易错点:忘写 quit() 时脚本执行完 _initialize 后进程会一直挂着(SceneTree 还在跑主循环)——每个工具脚本的 _initialize 都以 quit 结尾是纪律。

3. 参数解析:OS.get_cmdline_user_args

gdscript
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")

讲解:

  1. 解析器只有十几行:带 = 的是键值对,不带的是布尔开关。类型转换在使用处做(int(…))——参数天生是字符串,解析层不做隐式转换,坏参数在使用时报错比在解析层猜类型可靠。
  2. speed-rouge 的 progen_level.gd(tools/progen_level.gd:1-14 范式,全仓 20+ 脚本同构)就是这个形态:--seed 控制确定性、--out 控制产物路径。确定性 RNG 是生成类工具的灵魂:rng.seed = seed_v 让同一参数永远产出同一结果——可复现才可测试、可回滚。
  3. 参数即契约:工具支持哪些参数要写进脚本头注释(或 —help 分支),批处理脚本按契约调用——参数是 CLI 工具的接口,和函数签名同等对待。

4. 输出约定与退出码

gdscript
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)

讲解:

  1. 退出码是 CLI 的通用语言:0 成功、非 0 失败。bash 的 &&、CI 的 job 判定、批处理的 errorlevel 都只认它。print 的 PASS/FAIL 前缀是人读的部分,退出码是机器读的部分——两层齐全,CI 日志既可自动判定又能人工复查。
  2. 约定分层:stdout 走”结果输出”(PASS/FAIL、产物清单),push_error 走”错误详情”(进 stderr,见 105 篇通道语义)。流水线可以只看退出码,排查时再翻 stderr。
  3. fail-fast 与完整报告的取舍:校验类工具优先收集全部问题再退非零(一次跑出完整清单),生成类工具 fail-fast(坏了就停,不留半成品文件——半成品用 .tmp 承载,见 115 篇原子写)。

5. CI 与批处理消费

bash
# 批处理脚本形态(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

讲解:

  1. set -e 让链条上任何一个非零退出码中止整条批处理——工具的 quit(1) 就是被这样消费的。GitHub Actions 里同一串命令放进 step,step 失败即 PR 红。
  2. 工具的输出可以再喂给下一个工具(重定向、读上一环的产物文件)——CLI 工具组合成流水线的能力来自”参数进、文件出、退出码判”这个最小契约。
  3. 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 篇参考实现出发,改三处——参数解析、输出前缀、退出码。

先自己操作,再对照参考实现:

任务一与任务三参考实现
gdscript
# 任务一: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 opts
bash
godot --headless --script res://tools/echo_tool.gd --path . -- --msg=hello
# 输出 hello,echo $? 为 0
godot --headless --script res://tools/echo_tool.gd --path .
# 输出用法,echo $? 为 1
gdscript
# 任务三的关键收尾(承接 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 消费退出码串成流水线。

参考链接