Godot 3 GDScript 教程 / 函数与信号
函数与信号
函数基础
函数是 GDScript 中组织代码的基本单元。每个函数都属于某个脚本(类),可以有参数和返回值。
函数定义
# 无参数,无返回值
func say_hello() -> void:
print("Hello!")
# 有参数,无返回值
func greet(name: String) -> void:
print("Hello, %s!" % name)
# 有参数,有返回值
func add(a: int, b: int) -> int:
return a + b
# 多个参数
func create_enemy(type: String, pos: Vector2, level: int) -> void:
print("创建 %s 敌人,位于 %s,等级 %d" % [type, pos, level])
# 返回复杂类型
func get_nearest_enemy(pos: Vector2) -> Node2D:
var nearest: Node2D = null
var min_dist: float = INF
for enemy in get_tree().get_nodes_in_group("enemies"):
var dist = pos.distance_to(enemy.position)
if dist < min_dist:
min_dist = dist
nearest = enemy
return nearest
void 返回值
# void 表示不返回任何值
func do_nothing() -> void:
pass # pass 是空语句
# 也可以省略 void(不推荐)
func implicit_void():
print("没有类型注解")
⚠️ 注意:建议始终使用类型注解,有助于编辑器自动补全和错误检测。
参数详解
默认参数
# 默认参数必须在非默认参数之后
func spawn_bullet(pos: Vector2, speed: float = 500.0, damage: int = 10) -> void:
print("子弹位置: %s, 速度: %f, 伤害: %d" % [pos, speed, damage])
# 调用时可以省略有默认值的参数
spawn_bullet(Vector2(100, 200)) # 使用全部默认值
spawn_bullet(Vector2(100, 200), 800.0) # 自定义速度
spawn_bullet(Vector2(100, 200), 800.0, 25) # 全部自定义
# 默认值可以是表达式
func create_effect(pos: Vector2, color: Color = Color.white, lifetime: float = 1.0) -> void:
pass
参数传递方式
# 基本类型(int, float, bool, String)是值传递
func modify_value(x: int) -> void:
x = 100 # 不影响调用者
var a = 10
modify_value(a)
print(a) # 10(未改变)
# 对象类型(Node, Array, Dictionary)是引用传递
func modify_array(arr: Array) -> void:
arr.append(4) # 会影响调用者
var my_array = [1, 2, 3]
modify_array(my_array)
print(my_array) # [1, 2, 3, 4](已改变)
⚠️ 注意:Array 和 Dictionary 是引用传递!如果不想修改原数据,需先 .duplicate()。
可变参数模拟
GDScript 不直接支持可变参数,但可以用数组实现:
func calculate_sum(numbers: Array) -> float:
var total: float = 0.0
for n in numbers:
total += n
return total
# 调用
var result = calculate_sum([1, 2, 3, 4, 5]) # 15.0
参数展开(unpack)
# 使用 call() 动态调用函数
func attack(target: String, damage: int, element: String) -> void:
print("%s 对 %s 造成 %d 点 %s 伤害" % ["Hero", target, damage, element])
var args = ["Slime", 25, "Fire"]
attack(args[0], args[1], args[2]) # 手动展开
# 使用 callv() 自动展开
attack.callv(args) # 自动传入数组参数
静态函数(static)
extends Node
# 静态函数不依赖实例,不能访问成员变量
static func add(a: int, b: int) -> int:
return a + b
static func clamp_value(value: float, min_val: float, max_val: float) -> float:
if value < min_val:
return min_val
if value > max_val:
return max_val
return value
# 不能在静态函数中使用 self 或成员变量
static func broken() -> void:
# print(self) # ❌ 错误!静态函数中不能使用 self
pass
工具类示例
# math_utils.gd
class_name MathUtils
static func lerp_angle(a: float, b: float, t: float) -> float:
return a + (b - a) * t
static func vector_from_angle(angle: float) -> Vector2:
return Vector2(cos(angle), sin(angle))
static func random_in_circle(radius: float) -> Vector2:
var angle = randf() * TAU
var r = sqrt(randf()) * radius
return Vector2(cos(angle), sin(angle)) * r
使用:
var pos = MathUtils.random_in_circle(100.0)
信号(Signal)
信号是 Godot 的观察者模式实现,用于节点间松耦合通信。
定义信号
extends Node
# 基本信号定义
signal died
signal health_changed
# 带参数的信号
signal took_damage(amount: int)
signal healed(amount: int)
signal level_up(new_level: int)
signal position_changed(new_pos: Vector2)
发射信号
extends KinematicBody2D
signal died()
signal health_changed(current: int, maximum: int)
var health: int = 100
var max_health: int = 100
func take_damage(amount: int) -> void:
health -= amount
health = max(0, health)
# 发射信号(emit_signal 或 emit)
emit_signal("health_changed", health, max_health)
if health <= 0:
emit_signal("died")
queue_free()
# Godot 3.5+ 简写
func heal(amount: int) -> void:
health = min(health + amount, max_health)
emit_signal("health_changed", health, max_health)
连接信号
方式一:代码连接(推荐)
extends Node
func _ready() -> void:
# 连接信号到函数
$Player.connect("health_changed", self, "_on_player_health_changed")
$Player.connect("died", self, "_on_player_died")
func _on_player_health_changed(current: int, maximum: int) -> void:
$HealthBar.value = current
$HealthLabel.text = "%d / %d" % [current, maximum]
func _on_player_died() -> void:
$GameOverScreen.show()
get_tree().paused = true
方式二:编辑器连接
- 选中发射信号的节点
- 在 Inspector → Node 面板中选择信号
- 点击 “Connect” → 选择目标节点 → 选择方法
- 编辑器自动生成回调函数
方式三:Lambda(Godot 3.x 不支持)
⚠️ 注意:Godot 3.x 不支持 Lambda 连接信号,需在 Godot 4.x 中使用。
connect() 语法详解
# 完整语法
emitter.connect("signal_name", target_object, "method_name", [], flags)
# 示例
$Enemy.connect("died", self, "_on_enemy_died", [], CONNECT_ONESHOT)
# CONNECT_ONESHOT - 触发一次后自动断开
# CONNECT_DEFERRED - 延迟到空闲帧处理
# CONNECT_PERSIST - 持久化连接(编辑器用)
# 断开信号
$Enemy.disconnect("died", self, "_on_enemy_died")
# 检查信号是否已连接
if $Enemy.is_connected("died", self, "_on_enemy_died"):
print("已连接")
# 检查对象是否有某信号
if $Enemy.has_signal("died"):
print("有 died 信号")
信号参数传递
# 信号定义时声明参数类型
signal item_picked(item_name: String, quantity: int)
signal enemy_spawned(enemy: Node2D, pos: Vector2)
# 发射时传入参数
emit_signal("item_picked", "Health Potion", 3)
emit_signal("enemy_spawned", enemy_instance, Vector2(100, 200))
# 接收时参数要匹配
func _on_item_picked(item_name: String, quantity: int) -> void:
print("拾取了 %d 个 %s" % [quantity, item_name])
# 连接
$Item.connect("item_picked", self, "_on_item_picked")
⚠️ 注意:接收回调的参数数量必须和信号定义的参数数量一致,否则报错。
协程与 yield
Godot 3 中使用 yield 实现协程(异步等待)。
yield 基础
# 等待信号
func attack_sequence() -> void:
print("蓄力中...")
yield(get_tree().create_timer(1.0), "timeout") # 等待1秒
print("攻击!")
yield(get_tree().create_timer(0.5), "timeout")
print("后摇结束")
# 等待信号的一般形式
func wait_for_signal(emitter: Object, signal_name: String):
return yield(emitter, signal_name)
yield 常用场景
# 等待时间
func flash_red() -> void:
$Sprite.modulate = Color.red
yield(get_tree().create_timer(0.2), "timeout")
$Sprite.modulate = Color.white
# 等待动画播放完毕
func play_attack_anim() -> void:
$AnimationPlayer.play("attack")
yield($AnimationPlayer, "animation_finished")
print("攻击动画完成")
# 等待输入
func wait_for_input() -> void:
yield(self, "input_received") # 自定义信号
# 等待帧结束
func _ready() -> void:
yield(get_tree(), "idle_frame") # 等待一帧(所有 _ready 完成后)
print("所有节点已就绪")
yield 的注意事项
# ⚠️ yield 返回一个 GDScriptFunctionState 对象
# 如果持有该对象,可以稍后恢复协程
var state = some_coroutine()
# state.resume() # 手动恢复
# 如果协程中的对象被销毁,协程会自动取消
# 可以用 is_valid() 检查
var state2 = some_coroutine()
if state2 is GDScriptFunctionState and state2.is_valid():
state2.resume()
⚠️ 注意:
yield后的代码在恢复前不会执行- 如果节点被
queue_free(),挂起的协程会自动取消 - 不要在
_process或_physics_process中使用yield
自定义信号事件系统
事件总线模式(EventBus)
创建一个全局 Autoload 作为事件总线:
# res://scripts/EventBus.gd
extends Node
# 定义所有全局信号
signal player_damaged(amount: int, source: Node2D)
signal player_healed(amount: int)
signal player_died()
signal enemy_killed(enemy: Node2D, points: int)
signal item_collected(item_name: String)
signal level_changed(new_level: int)
signal boss_defeated(boss_name: String)
signal game_paused()
signal game_resumed()
signal score_changed(new_score: int)
在 Project Settings → Autoload 中添加 EventBus。
使用方式:
# 玩家脚本
extends KinematicBody2D
func take_damage(amount: int, source: Node2D) -> void:
health -= amount
EventBus.emit_signal("player_damaged", amount, source)
if health <= 0:
EventBus.emit_signal("player_died")
# 敌人脚本
extends KinematicBody2D
func die() -> void:
EventBus.emit_signal("enemy_killed", self, score_value)
queue_free()
# UI 脚本
extends Control
func _ready() -> void:
EventBus.connect("score_changed", self, "_on_score_changed")
EventBus.connect("player_damaged", self, "_on_player_damaged")
func _on_score_changed(new_score: int) -> void:
$ScoreLabel.text = "Score: %d" % new_score
func _on_player_damaged(amount: int, source: Node2D) -> void:
$DamageFlash.play("flash")
信号与观察者模式
# 可观察属性(Observable Property)
extends Node
signal property_changed(property_name: String, old_value, new_value)
var _health: int = 100 setget set_health, get_health
func set_health(value: int) -> void:
var old = _health
_health = value
emit_signal("property_changed", "health", old, _health)
func get_health() -> int:
return _health
# 外部使用
$Player.health = 80 # 自动触发信号
高级信号技巧
一次性信号
# 只触发一次后自动断开连接
$Timer.connect("timeout", self, "_on_timer_timeout", [], CONNECT_ONESHOT)
func _on_timer_timeout() -> void:
print("只执行一次")
延迟信号
# CONNECT_DEFERRED - 回调延迟到空闲帧执行
# 适合在物理处理中安全地修改场景树
$Area.connect("body_entered", self, "_on_body_entered", [], CONNECT_DEFERRED)
信号链
# 将信号转发给其他对象
func _ready() -> void:
$Child.connect("some_signal", self, "_relay_signal")
signal relay(data)
func _relay_signal(data) -> void:
emit_signal("relay", data) # 转发信号
游戏开发场景
场景:Boss 战阶段系统
Boss 战中,Boss 在不同血量阶段切换行为模式,需要解耦各系统的响应。
# Boss.gd
extends KinematicBody2D
signal phase_changed(new_phase: int)
signal health_changed(current: int, maximum: int)
signal boss_defeated()
enum Phase { PHASE_1, PHASE_2, PHASE_3 }
var health: int = 500
var max_health: int = 500
var current_phase: int = Phase.PHASE_1
func take_damage(amount: int) -> void:
health = max(0, health - amount)
emit_signal("health_changed", health, max_health)
# 阶段切换检查
var health_percent = float(health) / max_health
if health_percent <= 0.3 and current_phase != Phase.PHASE_3:
set_phase(Phase.PHASE_3)
elif health_percent <= 0.6 and current_phase == Phase.PHASE_1:
set_phase(Phase.PHASE_2)
if health <= 0:
emit_signal("boss_defeated")
queue_free()
func set_phase(new_phase: int) -> void:
current_phase = new_phase
emit_signal("phase_changed", new_phase)
match new_phase:
Phase.PHASE_1:
$AnimationPlayer.play("idle")
Phase.PHASE_2:
$AnimationPlayer.play("enraged")
Phase.PHASE_3:
$AnimationPlayer.play("desperate")
# BossUI.gd
extends Control
func _ready() -> void:
var boss = get_node("/root/Game/Boss")
boss.connect("health_changed", self, "_on_health_changed")
boss.connect("phase_changed", self, "_on_phase_changed")
boss.connect("boss_defeated", self, "_on_boss_defeated")
func _on_health_changed(current: int, maximum: int) -> void:
$HealthBar.value = float(current) / maximum * 100
func _on_phase_changed(new_phase: int) -> void:
var names = ["Phase 1", "Phase 2", "Phase 3"]
$PhaseLabel.text = names[new_phase]
func _on_boss_defeated() -> void:
$VictoryScreen.show()