GDScript 基础
本页导读:这一页用最短的篇幅带你过完 GDScript 的核心语法:变量与类型、函数、条件循环,以及 Godot 最有特色的信号机制。全程配可运行的代码示例,学完后你能读懂并写出游戏里 80% 的日常脚本。
GDScript 是 Godot 自带的脚本语言,长得非常像 Python:同样用缩进组织代码块,同样追求“写得快、读得懂”。它只存在于 Godot 里,但正因如此,它和引擎深度整合——声明一个信号、访问一个节点都是一等公民操作。你有 Python 基础的话,本章你半小时就能学完;没有的话,也没关系,下面每一步都会解释。
一、变量与类型:var、const 与类型标注
声明变量用 var,声明常量用 const:
var player_name: String = "白雪" # 变量,可以改
var speed: float = 300.0 # 小数用 float
var level: int = 1 # 整数用 int
var is_alive: bool = true # 布尔,只有 true / false
const MAX_HEALTH: int = 100 # 常量,全大写命名,不可修改冒号后面的部分叫类型标注,可以省略(GDScript 是动态类型语言),但我们强烈建议写上:
var hp = 100 # 合法,但引擎要自己猜类型
var hp: int = 100 # 推荐,写错类型时编辑器直接标红提醒类型标注不改变运行速度的量级,但能把大量“运行时才发现”的拼写错误提前到“编辑时就有红线”。新手把 hp 打成 hps,动态类型下程序静默出错,标注类型下编辑器当场骂你——被编辑器骂比被玩家骂好。
两个特殊前缀提前认识一下,第四章实战会天天用:
@export var jump_height: float = 200.0 # 暴露到编辑器检查器,可以在界面上直接改数值
@onready var sprite: Sprite2D = $Sprite2D # 等"节点就绪"后再取值,避免读不到子节点二、函数定义与调用
函数用 func 关键字定义(Python 是 def):
func add_coin(amount: int) -> int:
coin_count += amount
return coin_count
func _ready() -> void:
add_coin(10)
add_coin(5)
print(coin_count) # 输出 15-> int 表示返回值类型,-> void 表示不返回任何东西。参数同样可以标注类型。
有几类函数是 Godot 自动调用的,名字不能拼错:
| 函数名 | 调用时机 | 典型用途 |
|---|---|---|
_ready() | 节点进入场景树时,只调一次 | 初始化:读节点、连信号、设初值 |
_process(delta) | 每帧调用,delta 为帧间隔秒数 | 与帧率相关的逻辑:动画、计时、输入 |
_physics_process(delta) | 每物理帧(默认每秒 60 次)调用 | 移动、重力等需要稳定步长的物理逻辑 |
记住分工:动的东西放 _physics_process,其他每帧逻辑放 _process,初始化放 _ready。第三章平台跳跃的角色移动全部写在 _physics_process 里。
三、条件与循环:if / for / while
语法和 Python 几乎一样,注意缩进即可:
func check(hp: int, coins: int) -> void:
if hp <= 0:
print("游戏结束")
elif hp < 30:
print("快死了,小心!")
else:
print("状态良好")
if coins >= 10 and hp > 0: # 逻辑与用 and,或用 or,非用 not
print("可以进商店了")for 遍历用 range() 或直接遍历数组:
for i in range(3): # i 依次是 0, 1, 2
print("第 %d 次循环" % i)
for enemy in get_tree().get_nodes_in_group("enemies"):
enemy.queue_free() # 清空场上所有敌人while 与 Python 相同,唯一提醒:循环体里一定要有能改变条件的语句,死循环会让编辑器整个卡死(Godot 4 检测到卡死会弹窗报错,保存一下工作再说)。
补充两个新手容易忽略的小语法:整数相除会截断(7 / 2 得 3,想要 3.5 写 7.0 / 2.0);字符串格式化用 %(“金币:%d” % score)或 str() 拼接,和 Python 用法接近。
四、信号机制:Godot 的通信核心
这是 GDScript 最重要的概念,请给这一节多两分钟。
问题:金币被玩家碰到时,界面上要加分。可是金币节点根本“不知道”界面节点在哪——它们是场景树里相隔十万八千里的两个节点。让金币直接伸手去改界面,耦合又乱又脆。
信号(Signal)就是 Godot 的答案,本质是“观察者模式”:某节点发生了值得注意的事,它就广播(emit)一个信号;任何感兴趣的节点都可以提前**订阅(connect)**这个信号,事件发生时自动收到通知。发出者不需要知道有谁在听,听者不需要盯着发出者问“完事了没”。
Godot 的每个节点都自带一堆内置信号:按钮有 pressed,Area2D 有 body_entered(有物体进入碰撞范围)。你也可以用 signal 关键字自定义。两个完整示例:
示例一:订阅内置信号——点击按钮计数
场景结构:Node2D 下挂一个 Button 和一个 Label,根节点挂下面的脚本:
extends Node2D
var count: int = 0
@onready var button: Button = $Button
@onready var label: Label = $Label
func _ready() -> void:
# 把按钮的 pressed 信号,连接到本脚本的处理函数
button.pressed.connect(_on_button_pressed)
_update_label()
func _on_button_pressed() -> void:
count += 1
_update_label()
func _update_label() -> void:
label.text = "已被点击 %d 次" % countbutton.pressed.connect(处理函数) 是 4.x 的标准写法,意思是“pressed 信号一响,就执行这个函数”。运行后每点一次按钮,文字就刷新一次。
示例二:自定义信号——玩家血量变化驱动血条
先在玩家脚本里声明并发出信号(player.gd,挂在 CharacterBody2D 上,并已 add_to_group(“player”)):
extends CharacterBody2D
# 声明一个自定义信号,参数是最新血量和最大血量
signal health_changed(new_health: int, max_health: int)
var max_health: int = 100
var health: int = 100
func take_damage(amount: int) -> void:
health = maxi(health - amount, 0) # maxi 取较大值,血量不低于 0
health_changed.emit(health, max_health) # 广播:我血量变了!
if health == 0:
queue_free()再在 UI 脚本里订阅(hud.gd,挂在 CanvasLayer 上,子节点有 ProgressBar):
extends CanvasLayer
@onready var bar: ProgressBar = $ProgressBar
func _ready() -> void:
var player := get_tree().get_first_node_in_group("player")
if player != null:
player.health_changed.connect(_on_health_changed)
bar.max_value = player.max_health
bar.value = player.health
func _on_health_changed(new_health: int, _max_health: int) -> void:
bar.value = new_health玩家发信号时完全不知道血条的存在;血条订阅后自动刷新。以后你再加“血量低于 30% 屏幕变红”的功能,只需再写一个订阅者,玩家脚本一个字都不用改——这就是信号架构的威力。另外一个等价写法也认识一下:health_changed.emit(a, b) 与 emit_signal(“health_changed”, a, b) 效果相同,4.x 推荐 .emit() 点语法。
五、常用节点 API 与 $ 语法
脚本和节点的日常互动就这几个 API,全部值得背下来:
# —— 找节点 ——
get_node("Sprite2D") # 按路径获取子节点,路径写错运行时报错
$Sprite2D # 上面那行的糖衣语法,效果完全相同
$"HUD/ScoreLabel" # 多层路径用斜杠隔开,$后面可以不写引号但含特殊字符时要写
@onready var label := $HUD/ScoreLabel # 惯用组合:onready + 变量缓存,别在每帧里反复找
# —— 生命周期 ——
queue_free() # 安全删除本节点(本帧结束时清掉),删敌人、删金币都用它
add_child(node) # 把一个节点动态加为子节点(刷怪时用)
# —— 场景树 ——
get_tree().reload_current_scene() # 重新加载当前场景(死亡重来)
get_tree().get_nodes_in_group("enemies") # 按分组拿一批节点
get_tree().paused = true # 暂停整个游戏$ 语法的路径是相对于脚本所挂的节点的;想从根开始就写绝对路径 get_node(“/root/Main/Player”)。新手期建议用相对路径加 @onready 缓存,场景挪动时最不容易坏。
is_instance_valid(node) 顺便记一笔:判断一个节点是否还活着。先 queue_free() 又立刻访问它,是新手期高频崩溃来源。
六、与 Python 的相似与不同,以及新手常见坑
相似点让你上手快:缩进分块、动态类型、and / or / not、数组字典字面量 [1, 2, 3] 与 {“hp”: 10}、for i in range(n)、elif——全和 Python 一个模子。
不同点列一张表,有 Python 背景的读者重点看:
| 话题 | Python | GDScript |
|---|---|---|
| 定义函数 | def | func |
| 声明变量 | 直接赋值 | 需要 var / const 关键字 |
| 继承 | class A(B): | 文件头 extends B,一个文件就是一个类 |
| 声明私有 | 约定 _name | 真没有访问限制,约定同名 |
| 事件通知 | 自己写回调/观察者 | 内建 signal 机制 |
| 三引号多行字符串 | 支持 | 不支持,用 \n 或多行拼接 |
| 运行方式 | 独立解释器 | 只在 Godot 引擎内运行,依附于节点 |
最后是新手三大坑,照着检查能省很多 debug 时间:
- 缩进混乱:GDScript 对 Tab 和空格混用零容忍。编辑器默认用 Tab,就全程 Tab;从网页复制代码后如果报奇怪的缩进错,剪切重打一遍缩进即可。
- 大小写敏感:
Var不是var,queue_free()写成Queue_free()直接报错;信号名body_entered写成Body_Entered连接不上且不报编译错,最隐蔽。 - 节点路径写错:
$ScoreLable(拼错)在运行时才炸,报“Node not found”。对策就是前面说的@onready加缓存,并在场景树里对照确认真实节点名——路径必须和场景面板里的节点名一字不差。
本页小结
- 变量用
var、常量用const,类型标注建议全写,让编辑器替你抓拼写错误。 _ready做初始化、_process管每帧逻辑、_physics_process管移动与重力,自动调用的名字不能拼错。- 信号是 Godot 的核心通信方式:发出者
emit,订阅者connect,两端解耦;内置信号(pressed、body_entered)和自定义信号写法一致。 - 常用 API 四件套:
$找节点、queue_free()删自己、reload_current_scene()重开、分组函数批量操作。 - 与 Python 高度相似,上手成本低;三大坑是缩进、大小写、节点路径,全都靠“报错信息 + 对照场景树”解决。
语法备齐了,下一页进入真刀真枪的实战:从零做一个能跑能跳、有金币有敌人的平台跳跃游戏。
请看 实战:做一个平台跳跃游戏。