一个人做游戏,最容易死的不是不会写功能,而是被重复劳动耗死:每次调跳跃参数都要拿尺子量屏幕、每次加一关都要手动摆 40 个道具、每次导入新素材都要手点 30 次导入设置。这些活儿本身不创造乐趣,却吃掉你 70% 的开发时间。Godot 给了一把解决它的钥匙:让脚本在编辑器里跑。今天把
@tool、EditorScript、@export_tool_button这三件套讲透,它是「一个人干五个人的活」的关键工程手段。
今日速览
@tool让脚本在编辑器里执行:在脚本第一行加@tool,它就会在编辑器里也跑一遍。代码里用Engine.is_editor_hint()分流「编辑器行为」和「游戏行为」。@tool只适合做「可视化辅助」:官方推荐场景是画大炮的抛物线、画弹簧垫的最大跳跃高度、把用代码绘制的角色在编辑器里画出来——都是帮你做关卡设计的事。- 三个必须知道的坑:编辑器里的修改没有撤销/重做;
queue_free()节点可能直接把编辑器搞崩;@tool脚本引用到的其他 GDScript 也必须是@tool,否则实例化失败(static方法、常量、枚举可以例外)。 EditorScript是「一次性批处理工具」:@tool extends EditorScript+_run(),脚本编辑器里File > Run(Ctrl+Shift+X)就跑一次,适合批量改场景/资源。给它加class_name,还会自动出现在命令面板里。@export_tool_button把操作变成 inspector 按钮:@export_tool_button("重算", "Reload") var btn = rebuild,点一下就执行,图标名必须是编辑器内置图标名(区分大小写),不能用自己的图。
核心学习内容
一、@tool 的本质:同一份代码,两种运行环境
Godot 脚本默认只在游戏运行时执行;加 @tool 之后,同一份脚本在编辑器打开场景时也会执行。所以关键不是「怎么写」,而是分流:
@tool
extends Node2D
func _process(delta: float) -> void:
if Engine.is_editor_hint():
_draw_preview() # 只在编辑器里:画辅助线,不消耗玩家性能
else:
_update_gameplay(delta) # 只在游戏里:真正的逻辑
判断口诀:没包在任何 is_editor_hint() 判断里的代码,编辑器和游戏里都会跑。这句最容易被忽略,也最容易让「本该只在游戏里跑的逻辑」在编辑器里偷偷改你的场景数据。
官方明确说过:@tool 脚本能访问当前编辑场景的节点树,编辑器对它没有任何保护。所以第一条纪律是——用 @tool 做「只读的可视化」,不要用它自动改写场景内容;要改内容,用 EditorScript(下面讲),因为那是你主动按下去的一次性动作。
一个立刻能用的例子(官方文档同款思路,我换成了平台跳跃项目最需要的):
@tool
extends Node2D
## 把「跳跃能跳多高」画在场景里,调参数不用再开游戏试
@export var jump_velocity: float = -420.0
@export var gravity: float = 1200.0
@export var samples: int = 24
func _process(_delta: float) -> void:
if Engine.is_editor_hint():
queue_redraw()
func _draw() -> void:
if not Engine.is_editor_hint():
return
var t_total := absf(jump_velocity) / gravity * 2.0
var prev := Vector2.ZERO
for i in samples + 1:
var t := t_total * float(i) / float(samples)
var p := Vector2(120.0 * t, jump_velocity * t + 0.5 * gravity * t * t)
if i > 0:
draw_line(prev, p, Color(0.3, 0.9, 1.0, 0.8), 2.0)
prev = p
draw_line(Vector2.ZERO, Vector2(0, prev.y), Color(1, 0.4, 0.4, 0.5), 1.0)
改 jump_velocity,编辑器里那条弧线实时变形——这比「改数字 → 运行 → 跳 → 关掉 → 再改」快一个数量级。
二、@tool 的五个坑(踩过就记住了)
- 无撤销:工具脚本对场景的修改是永久的,
Ctrl+Z救不回来。动手前先git commit,或者干脆别让工具脚本写场景。 queue_free()会崩编辑器:在编辑器运行期间释放节点,可能直接崩。工具脚本里别删节点。- 依赖脚本也得是
@tool:@tool脚本preload或new()一个非 tool 脚本会失败——除非你只调用它的static方法/常量/枚举。这是个高频报错来源。 - 继承不会自动继承
@tool:子类也必须写@tool,否则父类的工具能力在子类上直接失效。 - 改了导出变量,面板不刷新:从脚本里改
@export变量后,要手动notify_property_list_changed(),否则 inspector 显示的还是旧值。
另外两个实用细节:@export 的 Array/Dictionary 在 inspector 里被改动时,setter 会被调用(运行时不会),正好拿来「改完自动重算」;以及工具脚本不能打断点调试,要么用 print,要么开一个新编辑器实例(Debug → Customize Run Instances,Main Run Args 填 --editor)来调试。
三、EditorScript:按一下就跑的批处理
@tool 是「常驻」,EditorScript 是「一次性」。它继承 RefCounted,实现 _run(),在脚本编辑器里 File > Run 或 Ctrl+Shift+X 执行:
@tool
extends EditorScript
## 把当前场景里所有名字含 "Enemy" 的节点按 X 坐标排序重命名
func _run() -> void:
var root := EditorInterface.get_edited_scene_root()
if root == null:
printerr("没有打开的场景"); return
var targets: Array[Node] = []
for n in root.find_children("*", "", true, false):
if n.name.begins_with("Enemy"):
targets.append(n)
targets.sort_custom(func(a, b): return a.global_position.x < b.global_position.x)
for i in targets.size():
print("%d -> %s" % [i, targets[i].name])
print("共 %d 个,检查无误后再执行重命名" % targets.size())
注意我故意只打印、不改——这是批处理工具的黄金法则:先 dry-run 打印,确认后再改成真正写入。想让它出现在命令面板(Ctrl+Shift+P)里,给脚本加个 class_name 即可。
四、@export_tool_button:把工具塞进属性面板
Godot 4.4+ 起,可以导出一个按钮,把「重算/重建/校验」这类操作直接放到 inspector 上,不用记快捷键:
@tool
extends Node
@export_tool_button("重算关卡统计", "Reload") var recalc = _recalc
func _recalc() -> void:
var count := get_tree().get_nodes_in_group("props").size()
print("场景内道具数:%d" % count)
图标名必须是 Godot 源码 editor/icons 里的文件名(如 Reload、Node2D、Callable),区分大小写,也只能用内置图标——这个限制值得记住,别浪费时间去查「怎么用自己的 png」。
🎯 DeepSeek 提示词
提示词 1 · 生成「关卡可视化」工具脚本
适用场景:想让某个数值/范围/轨迹在编辑器里直接看见(跳跃高度、炮台射程、巡逻路线、光照半径)。
你是 Godot 4.4+ 的 GDScript 工具脚本专家,熟悉 @tool 与 Engine.is_editor_hint() 的语义。
【任务】写一个 @tool 脚本,把下面这个游戏参数的「可视化辅助」画在编辑器里。
游戏参数:{例如:角色的跳跃初速度与重力;或炮台的射程与弹道;或敌人的视野扇形}
挂载节点类型:{Node2D / CollisionShape2D ...}
需要在编辑器里看到:{例如:跳跃最高点的水平线与抛物线轨迹;扇形视野边界;射程圆}
【要求】
1. 第一行必须是 @tool,并用 extends 指定挂载节点类型;
2. 所有绘制逻辑必须包在 if Engine.is_editor_hint(): 里,游戏运行时不得产生任何绘制开销;
3. 通过 @export 暴露可调参数(带类型和默认值),参数变化后能实时刷新(用 setter 里调 queue_redraw());
4. 只读:绝对不要修改场景里任何节点的属性,不要调用 queue_free()、不要 add_child();
5. 用 _draw() + draw_line/draw_circle/draw_arc 实现,颜色用半透明,避免挡住美术;
6. 中文注释说明每段在干什么,注释解释「为什么」而不是复述代码。
【输出格式】一个完整 .gd 文件内容 + 3 行使用说明(怎么挂、调哪个参数、注意什么)。
提示词 2 · 生成「批处理」EditorScript
适用场景:批量重命名、批量改导入设置、批量给场景加节点、批量生成 .tres 数据文件。
你是 Godot 4 的 EditorScript 专家。EditorScript 是继承 RefCounted 的一次性工具,
必须 @tool + extends EditorScript + 实现 _run(),通过 File > Run 执行。
【任务】写一个 EditorScript,完成:{例如:遍历 res://levels/ 下所有 .tscn,把其中名为 "Spawn" 的 Marker2D 的 position 四舍五入到 8 的整数倍}
【硬性要求】
1. 第一行 @tool,extends EditorScript,只实现 _run();
2. **必须先 dry-run**:第一版只统计并 print 出「将要改动哪些文件/节点、改动前后的值」,不写入;
用一个常量 const APPLY := false 控制,注释说明改成 true 才真正写入;
3. 遍历用 DirAccess + ResourceLoader,路径统一用 "res://" 开头;跳过 .godot/ 与 addons/ 目录;
4. 每个文件处理前检查 load() 是否返回 null,失败就 printerr 并 continue,不要中断整批;
5. 保存场景用 PackedScene.pack(root) + ResourceSaver.save(),保存前 print 完整路径;
6. 结束时打印汇总:扫描 N 个、命中 M 个、失败 K 个;
7. 不要动 EditorInterface 的撤销栈(EditorScript 不保证 undo),改为提示用户「先 git commit」。
【输出格式】完整 .gd 文件 + 「运行步骤」4 步 + 「风险提示」2 条。
提示词 3 · 让 DeepSeek 审查工具脚本的安全性
适用场景:DeepSeek 给了你一个 @tool 脚本,运行前先过一遍安检(工具脚本的 bug 代价是崩编辑器 + 丢场景)。
你是 Godot 4 工具脚本(@tool / EditorScript)的安全审查员。以下是待审查脚本:
```gdscript
{粘贴脚本}
请按下面清单逐项检查,每项给出「通过 / 风险 / 不适用」+ 行号 + 一句理由:
- 所有仅在编辑器中执行的逻辑,是否都包在 Engine.is_editor_hint() 里?有没有「本意只在游戏里跑」却被放进公共区的代码?
- 是否存在会直接崩编辑器的操作:queue_free()、free()、删除/重设正在被引用的节点?
- 是否存在对场景的隐式写入(改 position/rotation/属性)而没有用户明确触发?若有,建议改成只读可视化。
- 脚本引用到的其它自定义脚本是否也带 @tool?不带的,是否只用了 static 方法/常量/枚举?
- 子类是否重复声明了 @tool?(继承不自动传递)
- 从代码里修改 @export 变量后,是否调用 notify_property_list_changed()?
- 是否可能在编辑器里进入死循环或在 _process 里做重活(每帧 load()、find_children() 全树扫描)?
- 若脚本会保存资源,是否打印了目标路径并有 dry-run 开关?
最后输出:必须修(会导致崩溃/丢数据)/ 建议修(可维护性)/ 可以放着,每类最多 3 条,每条给出可直接替换的代码片段。
## 学习心得
今天这三件套改变的不是「代码能力」,而是**开发速度的斜率**。前 64 天我们一直在给游戏加「玩家看得见的东西」;今天加的是「只有我看得见的东西」——但正是它决定了你能不能在 3 个月里做出一个能上架 Steam 的体量。
对「用 DeepSeek 做游戏」这件事,今天还有个额外收获:**工具脚本是最适合交给 AI 的一类代码**。原因有三:它不进入最终构建(写错了不影响玩家)、它有明确的官方约束(`@tool` + `is_editor_hint` + 只读)可以写进提示词、它的正确性可以用「我在编辑器里点一下看到了什么」立刻验证。相比让 AI 写战斗逻辑(错了要玩十遍才发现),让 AI 写工具脚本是**投入产出比最高**的用法。
顺便一句实话:不要一上来就写 `EditorPlugin`。官方也说,先 `EditorScript` 解决一次性问题,真需要常驻面板/菜单了再升级成插件。**工具是用来省时间的,别把省下来的时间又花在造工具上。**
## 明日关注
- [ ] 把「跳跃弧线可视化」真的挂到自己项目的主角上,调 3 组参数对比手感(这是最便宜的关卡手感调优手段)。
- [ ] 写一个 `const APPLY := false` 的 dry-run EditorScript,先跑统计不改数据,养成批处理先看后改的习惯。
- [ ] 读一遍 `@export_tool_button` 与 `notify_property_list_changed()` 的类文档,把下一个工具直接做成 inspector 按钮。
*本文由自动化学习流水线每日生成,坚持即学即记。*
评论