Skip to content

GDScript 基础

本页导读:这一页用最短的篇幅带你过完 GDScript 的核心语法:变量与类型、函数、条件循环,以及 Godot 最有特色的信号机制。全程配可运行的代码示例,学完后你能读懂并写出游戏里 80% 的日常脚本。

GDScript 是 Godot 自带的脚本语言,长得非常像 Python:同样用缩进组织代码块,同样追求“写得快、读得懂”。它只存在于 Godot 里,但正因如此,它和引擎深度整合——声明一个信号、访问一个节点都是一等公民操作。你有 Python 基础的话,本章你半小时就能学完;没有的话,也没关系,下面每一步都会解释。

一、变量与类型:var、const 与类型标注

声明变量用 var,声明常量用 const

gdscript
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 是动态类型语言),但我们强烈建议写上:

gdscript
var hp = 100        # 合法,但引擎要自己猜类型
var hp: int = 100   # 推荐,写错类型时编辑器直接标红提醒

类型标注不改变运行速度的量级,但能把大量“运行时才发现”的拼写错误提前到“编辑时就有红线”。新手把 hp 打成 hps,动态类型下程序静默出错,标注类型下编辑器当场骂你——被编辑器骂比被玩家骂好

两个特殊前缀提前认识一下,第四章实战会天天用:

gdscript
@export var jump_height: float = 200.0   # 暴露到编辑器检查器,可以在界面上直接改数值
@onready var sprite: Sprite2D = $Sprite2D  # 等"节点就绪"后再取值,避免读不到子节点

二、函数定义与调用

函数用 func 关键字定义(Python 是 def):

gdscript
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 几乎一样,注意缩进即可:

gdscript
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() 或直接遍历数组:

gdscript
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 / 23,想要 3.57.0 / 2.0);字符串格式化用 %“金币:%d” % score)或 str() 拼接,和 Python 用法接近。

四、信号机制:Godot 的通信核心

这是 GDScript 最重要的概念,请给这一节多两分钟。

问题:金币被玩家碰到时,界面上要加分。可是金币节点根本“不知道”界面节点在哪——它们是场景树里相隔十万八千里的两个节点。让金币直接伸手去改界面,耦合又乱又脆。

信号(Signal)就是 Godot 的答案,本质是“观察者模式”:某节点发生了值得注意的事,它就广播(emit)一个信号;任何感兴趣的节点都可以提前**订阅(connect)**这个信号,事件发生时自动收到通知。发出者不需要知道有谁在听,听者不需要盯着发出者问“完事了没”。

Godot 的每个节点都自带一堆内置信号:按钮有 pressedArea2Dbody_entered(有物体进入碰撞范围)。你也可以用 signal 关键字自定义。两个完整示例:

示例一:订阅内置信号——点击按钮计数

场景结构:Node2D 下挂一个 Button 和一个 Label,根节点挂下面的脚本:

gdscript
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 次" % count

button.pressed.connect(处理函数) 是 4.x 的标准写法,意思是“pressed 信号一响,就执行这个函数”。运行后每点一次按钮,文字就刷新一次。

示例二:自定义信号——玩家血量变化驱动血条

先在玩家脚本里声明并发出信号(player.gd,挂在 CharacterBody2D 上,并已 add_to_group(“player”)):

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

gdscript
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,全部值得背下来:

gdscript
# —— 找节点 ——
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 背景的读者重点看:

话题PythonGDScript
定义函数deffunc
声明变量直接赋值需要 var / const 关键字
继承class A(B):文件头 extends B,一个文件就是一个类
声明私有约定 _name真没有访问限制,约定同名
事件通知自己写回调/观察者内建 signal 机制
三引号多行字符串支持不支持,用 \n 或多行拼接
运行方式独立解释器只在 Godot 引擎内运行,依附于节点

最后是新手三大坑,照着检查能省很多 debug 时间:

  • 缩进混乱:GDScript 对 Tab 和空格混用零容忍。编辑器默认用 Tab,就全程 Tab;从网页复制代码后如果报奇怪的缩进错,剪切重打一遍缩进即可。
  • 大小写敏感Var 不是 varqueue_free() 写成 Queue_free() 直接报错;信号名 body_entered 写成 Body_Entered 连接不上且不报编译错,最隐蔽。
  • 节点路径写错$ScoreLable(拼错)在运行时才炸,报“Node not found”。对策就是前面说的 @onready 加缓存,并在场景树里对照确认真实节点名——路径必须和场景面板里的节点名一字不差

本页小结

  • 变量用 var、常量用 const,类型标注建议全写,让编辑器替你抓拼写错误。
  • _ready 做初始化、_process 管每帧逻辑、_physics_process 管移动与重力,自动调用的名字不能拼错。
  • 信号是 Godot 的核心通信方式:发出者 emit,订阅者 connect,两端解耦;内置信号(pressedbody_entered)和自定义信号写法一致。
  • 常用 API 四件套:$ 找节点、queue_free() 删自己、reload_current_scene() 重开、分组函数批量操作。
  • 与 Python 高度相似,上手成本低;三大坑是缩进、大小写、节点路径,全都靠“报错信息 + 对照场景树”解决。

语法备齐了,下一页进入真刀真枪的实战:从零做一个能跑能跳、有金币有敌人的平台跳跃游戏。

请看 实战:做一个平台跳跃游戏