技术博客

GDScript 2.0 破坏性变更清单:3.x 迁移时踩过的坑按类归档

第 94 / 149 篇 · 2025-05-13 · godot · GDScript迁移实战笔记

把 3.x 工程迁到 4.x,官方项目升级器(Project Converter)能自动改掉大部分 API 重命名,但有一批变更不是"换个名字"而是"语义变了",自动转换改不了,编译过了运行还是错。这篇把破坏性变更按坑的深度分四类,每类给迁移时的检查方法。

第一类:机械重命名,升级器全包

占变更量的八成,纯体力活,升级器 --convert-3to4 直接改完:

3.x 4.x 改名理由
Spatial Node3D 3D 节点命名统一 Node3D 前缀族
KinematicBody / KinematicBody2D CharacterBody3D / CharacterBody2D 名字误导(它从来不是 kinematic body 语义)
Position2D / Position3D Marker2D / Marker3D 它只是个变换标记
Area Area3D 与 Area2D 对齐
OS.window_fullscreen DisplayServer.window_set_mode OS god 类拆分
funcref(o, "m") Callable(o, "m") Callable 成为一等公民
yield(get_tree(), "idle_frame") await get_tree().process_frame 协程机制重写
onready var @onready var 注解语法统一

这一类迁移完编译直接过,不用人工复核。真正的坑在后面三类。

第二类:语法收紧,编译期报错但语义要重想

信号声明和 emit 改了语法,行为也有变。3.x 的 signal my_signal(x) 加 emit_signal("my_signal", x) 在 4.x 是 signal my_signal(x: int) 加 my_signal.emit(x)。字符串形式的 emit_signal 和 connect 还能用,但新风格是强类型信号对象,类型不匹配在连接时就报错而不是触发时才炸。迁移时顺手把字符串 connect 全换成方法引用形式——btn.pressed.connect(_on_pressed)——一次到位,别再留 "pressed" 字符串,重构改名时字符串不跟着变,是纯地雷。

setget 被属性 setter/getter 替代,且语义不同。3.x 的 setget set_x, get_x 触发时机宽松,4.x 的属性写法:

var health := 100:
    set(value):
        health = clamp(value, 0, max_health)

set 块里直接写属性名赋值是递归赋值陷阱的修复点——4.x 里 health = ... 在 setter 内部指向底层存储,不会再次触发 setter。但前提是你这么写;迁移时如果机械地把 3.x 的 _set_health 函数体搬进来,里面调 set_health(v) 的旧写法会死循环。检查方法:全文搜 setter 函数名,确认没有自调用。

export 变 @export,同时类型从"导出语句自带类型"变成"类型标注说了算"。3.x 的 export(int) var x 迁移后变成 @export var x: int——顺序反了,类型声明是本体。混着写 @export var x 不给类型,导出的是 Variant,检查器里什么控件都不出现。迁移后扫一遍检查器,凡是显示为空白控件的导出属性都是漏标类型。

第三类:语义变了,编译不报错运行才错

这一类最危险,数量少但个个咬人。

yield → await 不是改名是换机制。 3.x 的 yield 恢复执行靠信号,函数返回值是个 GDScriptFunctionState 对象,你得接住它。4.x 的 await 是协程挂起点,返回值就是协程的最终值。行为差异:3.x 里 yield() 不接返回值时协程照样跑到信号来;4.x 里写了 await 的地方,调用方拿到的是 Callable 状态而不是立刻的结果——如果外层代码把返回当普通值用,拿到的是意外的类型。排查法:搜所有调用"内部有 yield/await 的函数"的点,确认调用方自己也在 await 链上。

Pool*Array 全部改名 Packed*Array 且共享语义变了。3.x 的 PoolArray 赋值是共享底层内存,4.x 的 PackedArray 是写时复制。3.x 代码里如果有"拷贝一个 PoolArray 当快照、原数组改了快照跟着变"的依赖,迁移后行为反转:快照变成真快照。这是少数语义变化方向"修复了 bug 外观"的例子——3.x 的共享行为更像 bug,4.x 的 COW 才对——但如果你的逻辑恰好依赖共享,就是静默故障。

整除和类型转换收紧。 3.x 里 5 / 2 在某些上下文是 2(取决于操作数类型推断),4.x 中 / 对 int 操作数是截断除法返回 int、float 除法必须显式。升级器会给可疑点插警告注释,迁移时把每个警告当 error 处理,别批量忽略。

第四类:API 消失,要找替代路径

3.x 的 Spatial.get_global_transform().origin 直接可用,4.x 推荐 global_position——老路径还在但已标 deprecated。真正消失的是一批 god 类功能拆分:OS 拆出 DisplayServer、Time、ResourceLoader 成为静态类、Engine.get_main_loop() 类型收窄。迁移工作流上,我的做法是升级器跑完后留着 .godot 备份,逐文件打开看脚本编辑器的黄色警告条,4.x 的 deprecation 警告写得比 3.x 用心,基本就是迁移指南本人。

最后一句话:前三类加起来决定了迁移工作量的大头不在重命名而在测试覆盖——语义类变更只在运行时显形,没有自动化测试的 3.x 工程,迁移预算里测试补齐的时间应当和改代码的时间一样多。

四类破坏性变更与迁移策略 ① 机械重命名(约八成) Spatial→Node3D 等;升级器自动改完 复核成本:零,编译即过 ② 语法收紧,编译报错 setget→setter/getter、export→@export 复核成本:低,按报错逐条改 ③ 语义变了,运行才错 yield→await、PoolArray 共享→COW 复核成本:高,靠测试覆盖逼出来 ④ API 消失或拆分 OS 拆 DisplayServer/Time,god 类瘦身 复核成本:中,靠 deprecation 警告指引 迁移工作流 1. git 干净提交 → 升级器 --convert-3to4 跑一遍(处理①) 2. 编译错误清零(②),扫 deprecation 警告替代旧路径(④) 3. 补自动化测试逼出语义类故障(③)——没有测试的工程这步和改代码同价