> GDScript 长得像 Python,但它不是 Python——它是为「节点树上的游戏逻辑」量身定制的领域语言。本篇从类型系统写到 4.5 新特性,顺路扫清新人最常见的十个坑。
一、语言定性:解释、动态、但鼓励静态
GDScript 是 Godot 自研的解释语言,运行于引擎内置 VM,与引擎零胶水——它访问的一切节点、属性、信号都是引擎原生对象。4.x 的关键取向:语法保持动态灵活,但工具链全力推静态类型。写上类型标注,编辑器就有补全、有重构、有编译期告警,性能也更好。本篇所有示例一律静态类型。
基础语法一屏放得下:
extends CharacterBody2D
class_name Player # 全局类名,任何脚本都可直接引用 Player 类型
const MAX_HP := 100 # 常量,:= 由右侧推断类型
var hp: int = MAX_HP
var speed: float = 300.0
@export var jump_force: float = -400.0 # 导出到 Inspector,设计师可调
@onready var sprite: Sprite2D = $%Sprite # 就绪时求值
func take_damage(amount: int) -> void:
hp = maxi(hp - amount, 0) # maxi/minf 是 4.x 的全局工具函数
if hp == 0:
died.emit()
两个语义细节必须第一时间讲清:
@onready var x = $...:$取节点是「当下」求值,写在普通 var 初始化里会因子节点未就绪而报 null。@onready把求值推迟到_ready(),配合上一级讲的「自底向上」节律,正好安全。:=与=::=声明并推断类型(编译期定死),=赋值给已声明变量。静态类型项目的纪律是「声明用:=,赋值用=」。
二、类型系统:鸭子打字的护栏
GDScript 的类型体系要点:
- 内建值类型:
int、float、bool、String、Vector2/2i/3/3i/4、Color、Rect2、Transform2D/3D……注意 Vector2 与 Vector2i 是不同类型,像素用i、米用浮点,混用会隐式转换但会告警。 - 容器带类型参数:
var scores: Array[int] = []、var dict: Dictionary[String, Node] = {}。 typed Array 是 4.x 强推的,混进异型会运行时报错——这是护栏不是枷锁。 Variant是「任何东西」,静态类型的逃生门;能不用就不用,用了要as转换或is检查。- 枚举与字典都是一等公民:
enum State {IDLE, RUN, JUMP},甚至enum {A, B}匿名枚举直接映射常量。 match语句替代 switch,支持数组/字典/类型模式匹配,比 if-else 链可读性高一截。
三、信号与异步:await 是游戏脚本的灵魂
上一级讲过信号连接,这里补全语言的另一半——协程:
func play_intro() -> void:
$AnimationPlayer.play("intro")
await $AnimationPlayer.animation_finished # 挂起,动画完再回来
await get_tree().create_timer(1.5).timeout # 一行延时
$Music.play()
var result := await dialog.ask_choice() # 甚至能接收信号返回值
await 之后,函数从这里「暂停」,控制权归还引擎,事件到了再续。把时序流程写成线性文字,这是 GDScript 对游戏脚本最实在的贡献。对比一下回调地狱版的同一个逻辑,高下立判。
注意:调用一个含 await 的函数时,它也会返回一个「可 await 的协程」;如果不 await,函数会跑到第一个 await 点就返回——新人常在这里埋下「为什么后半段没执行」的悬案。
四、4.5 新特性:变参函数与抽象类
变参函数——函数可以接收任意多个参数:
func sum(first: float, ...rest: Array) -> float:
var total := first
for n in rest:
total += n
return total
sum(1.0) # 1.0
sum(1.0, 2.0, 3.0) # 6.0
抽象类与抽象方法——基类只定契约、禁止实例化:
# animal.gd
@abstract class_name Animal
extends Node
@abstract func cry() -> void
# cat.gd
class_name Cat
extends Animal
func cry() -> void:
print("Meow!")
子类不实现 cry() 直接报错。这对「武器系统」「技能系统」这类「一堆兄弟类共享同一接口」的结构是量身定做——第三级这里正好呼应第二级的「组合优于继承」:抽象基类定接口,具体场景做实现。
五、十坑清单(新人血泪浓缩版)
- 类名与文件名:
class_name Player后,脚本全局叫 Player,但$Player取的是节点名,两码事,别混。 self与省略:GDScript 里方法调用默认就近解析,但静态类型下跨脚本调用优先用player.take_damage(5)显式对象,可读性优先。super:覆写_ready()时如果父类有逻辑要保留,必须写super._ready()——不写就是整个替换,不会自动链。- 静态变量:
static var是 4.1 加入的,挂在类上而非实例上,做全局计数器、注册表好用,但要自己管理生命周期。 - getter/setter:
var hp: int = 100: set(v): hp = clampi(v, 0, 100)——属性拦截写在冒号后,不是 Java 式的setHp()。 - 字符串格式化:
"%s 的血量 %d" % [name, hp]或 4.x 更推荐的"血量 {hp}".format({"hp": hp});%在格式串里的坑与 Python 同源。 is_instance_valid(obj):延迟回调、信号触发时对象可能已销毁,调用前先验尸,否则「Instance is invalid」是输出面板常客。- 整数除法:
1/2得 0,1.0/2.0得 0.5。像素坐标混算时最常见的诡异 bug 来源。 move_and_slide()无参:4.x 里它不接参数,速度全在velocity成员变量里,3.x 教程的旧写法会编译错误。_process里别做重活:每帧都跑的代码是性能放大器,缓存引用(@onready)、避免每帧get_node、字符串拼接进_process是三大典型反模式。
六、工具链:LSP、格式化与调试
- LSP:编辑器 → 编辑器设置 → 网络 → 语言服务器,开启后 VS Code 装 godot-tools 插件即可获得全量跳转与补全。
- 断点调试:脚本行号左侧点击设断点,F5 运行后底部调试器面板可看调用栈、变量、监视;4.6 新增 Step Out 按钮,跳出当前函数一步到位。
- 远程检查器:运行中游戏里的节点树可以实时选中改属性——4.5 还支持了多选与批量改,调手感神器。
- 对象快照(4.6):调试器可对运行中游戏做 ObjectDB 快照并对比两次快照,谁创建了、谁没销毁一目了然——查内存泄漏从此不用猜。
*本篇配图:代码与信号流。*