第十一章:信号系统
"信号是Godot实现松耦合通信的秘密武器,让对象之间不再紧密依赖。"
信号(Signal)是Godot引擎的核心特性之一,实现了观察者模式,让节点之间能够以松耦合的方式通信。本章将全面介绍GDScript的信号系统。
11.1 信号基础
11.1.1 什么是信号
信号是一种发布-订阅机制:
- 发送者:发出信号,通知某事发生
- 接收者:连接信号,响应事件
# 发送者不需要知道谁在监听
# 接收者不需要知道信号从哪里来
# 这就是"松耦合"
11.1.2 内置信号
Godot节点自带许多信号:
# Button的信号
button.pressed # 按钮被按下
button.button_down # 按钮按下(按住时)
button.button_up # 按钮释放
# Timer的信号
timer.timeout # 计时器超时
# Area2D的信号
area.body_entered # 物体进入区域
area.body_exited # 物体离开区域
# AnimationPlayer的信号
anim.animation_finished # 动画播放完成
# HTTPRequest的信号
http.request_completed # HTTP请求完成
11.1.3 连接信号
func _ready():
# 方法1:代码连接(推荐)
$Button.pressed.connect(_on_button_pressed)
# 方法2:使用Callable
$Button.pressed.connect(func(): print("点击!"))
# 方法3:编辑器连接
# 在编辑器的节点面板 → 信号标签 → 双击信号
func _on_button_pressed():
print("按钮被点击")
11.2 自定义信号
11.2.1 声明信号
extends Node
# 无参数信号
signal game_started
signal game_over
# 带参数信号
signal health_changed(new_health: int)
signal player_died(player_name: String, score: int)
signal item_collected(item: Item, position: Vector2)
# 信号可以有默认值(但通常不推荐)
signal damage_dealt(amount: int, critical: bool)
11.2.2 发送信号
extends Node
signal health_changed(new_health: int)
signal died
var health: int = 100
func take_damage(amount: int) -> void:
health -= amount
# 发送信号
health_changed.emit(health)
if health <= 0:
died.emit()
func heal(amount: int) -> void:
health = min(100, health + amount)
health_changed.emit(health)
11.2.3 接收信号
# game_ui.gd
extends Control
@onready var health_label: Label = $HealthLabel
@onready var player: Player = $"../Player"
func _ready():
# 连接玩家的信号
player.health_changed.connect(_on_health_changed)
player.died.connect(_on_player_died)
func _on_health_changed(new_health: int) -> void:
health_label.text = "HP: %d" % new_health
func _on_player_died() -> void:
$GameOverScreen.show()
11.3 信号连接选项
11.3.1 connect()参数
# connect(callable, flags)
# 基本连接
signal_name.connect(callback_function)
# 带标志的连接
signal_name.connect(callback_function, CONNECT_DEFERRED)
连接标志:
| 标志 | 说明 |
|---|---|
CONNECT_DEFERRED | 延迟到帧结束时调用 |
CONNECT_PERSIST | 保存场景时保留连接 |
CONNECTONESHOT | 只触发一次后自动断开 |
CONNECTREFERENCECOUNTED | 引用计数连接 |
# 一次性连接
$Button.pressed.connect(_on_first_click, CONNECT_ONE_SHOT)
# 延迟调用(避免在物理回调中修改场景树)
body_entered.connect(_on_body_entered, CONNECT_DEFERRED)
11.3.2 绑定参数
# 在连接时绑定额外参数
signal clicked
func _ready():
for i in range(3):
var button = Button.new()
button.text = "按钮 %d" % i
# 绑定按钮索引
button.pressed.connect(_on_button_pressed.bind(i))
add_child(button)
func _on_button_pressed(index: int) -> void:
print("点击了按钮 ", index)
11.3.3 断开信号
func _ready():
$Button.pressed.connect(_on_pressed)
func _on_pressed():
print("只执行一次")
# 手动断开
$Button.pressed.disconnect(_on_pressed)
# 检查是否已连接
if signal_name.is_connected(callback):
signal_name.disconnect(callback)
11.4 高级信号用法
11.4.1 信号与await
# 等待信号
func wait_for_button_click():
print("等待点击...")
await $Button.pressed
print("按钮被点击了!")
# 等待自定义信号
signal operation_complete(result)
func async_operation():
await get_tree().create_timer(2.0).timeout
operation_complete.emit("成功")
func start_operation():
async_operation()
var result = await operation_complete
print("操作结果:", result)
# 带超时的等待
func wait_with_timeout():
var timer = get_tree().create_timer(5.0)
# 等待任意一个信号
var result = await Signal($Button.pressed) or Signal(timer.timeout)
# 注意:这种写法在GDScript中需要用不同方式实现
11.4.2 信号转发
# 子节点信号转发到父节点
class_name HealthBar
extends Control
signal value_changed(value: int)
@onready var progress: ProgressBar = $ProgressBar
func set_value(value: int) -> void:
progress.value = value
value_changed.emit(value)
# 父节点可以监听HealthBar的信号
# 而不需要直接访问ProgressBar
11.4.3 信号总线(Event Bus)
# event_bus.gd - 作为Autoload
extends Node
# 全局信号
signal game_paused
signal game_resumed
signal player_died(player_id: int)
signal enemy_killed(enemy_type: String, position: Vector2)
signal item_collected(item_id: String)
signal achievement_unlocked(achievement_name: String)
# 便捷方法
func pause_game():
get_tree().paused = true
game_paused.emit()
func resume_game():
get_tree().paused = false
game_resumed.emit()
# 使用(任何脚本)
func _ready():
EventBus.player_died.connect(_on_player_died)
EventBus.enemy_killed.connect(_on_enemy_killed)
func kill_enemy(enemy: Enemy):
EventBus.enemy_killed.emit(enemy.type, enemy.position)
11.4.4 动态信号
# 动态获取和使用信号
func connect_dynamic_signal(obj: Object, signal_name: String, callback: Callable):
if obj.has_signal(signal_name):
obj.connect(signal_name, callback)
# 获取对象的所有信号
func list_signals(obj: Object):
for signal_info in obj.get_signal_list():
print("信号:", signal_info.name)
print("参数:", signal_info.args)
11.5 信号最佳实践
11.5.1 命名约定
# 使用过去时或进行时
signal health_changed # 生命值已改变
signal player_died # 玩家已死亡
signal item_collecting # 正在收集物品
signal game_starting # 游戏正在开始
# 动作相关用"动词_ed"
signal button_pressed
signal level_completed
signal enemy_spawned
# 状态相关用"状态_changed"
signal visibility_changed
signal state_changed
11.5.2 参数设计
# 提供足够的信息
signal damage_dealt(amount: int, source: Node, target: Node, is_critical: bool)
# 使用字典传递复杂数据
signal event_occurred(data: Dictionary)
# data = {"type": "damage", "amount": 10, "source": attacker}
# 避免过多参数,考虑使用资源类
class_name DamageEvent
extends RefCounted
var amount: int
var source: Node
var target: Node
var damage_type: String
var is_critical: bool
signal damage_dealt(event: DamageEvent)
11.5.3 何时使用信号
# 使用信号的场景:
# 1. 通知多个监听者
# 2. 发送者不需要知道接收者
# 3. 跨越场景边界的通信
# 4. UI更新
# 5. 事件驱动的系统
# 不适合使用信号的场景:
# 1. 需要立即返回值
# 2. 一对一的直接调用
# 3. 性能关键代码(信号有开销)
11.5.4 避免信号滥用
# 不好的做法:信号意大利面
# A发信号给B,B发信号给C,C发信号给A...
# 好的做法:清晰的层次结构
# - 子节点发信号给父节点
# - 使用信号总线处理全局事件
# - 保持信号流向简单可追踪
11.6 实际案例
11.6.1 玩家系统
# player.gd
class_name Player
extends CharacterBody2D
signal health_changed(current: int, maximum: int)
signal mana_changed(current: int, maximum: int)
signal died
signal respawned
signal leveled_up(new_level: int)
signal experience_gained(amount: int)
@export var max_health: int = 100
@export var max_mana: int = 50
var health: int:
set(value):
health = clampi(value, 0, max_health)
health_changed.emit(health, max_health)
if health == 0:
died.emit()
var mana: int:
set(value):
mana = clampi(value, 0, max_mana)
mana_changed.emit(mana, max_mana)
var level: int = 1
var experience: int = 0
func _ready():
health = max_health
mana = max_mana
func gain_experience(amount: int):
experience += amount
experience_gained.emit(amount)
while experience >= get_exp_for_next_level():
experience -= get_exp_for_next_level()
level += 1
leveled_up.emit(level)
func get_exp_for_next_level() -> int:
return level * 100
11.6.2 UI系统
# player_hud.gd
class_name PlayerHUD
extends CanvasLayer
@onready var health_bar: ProgressBar = $HealthBar
@onready var mana_bar: ProgressBar = $ManaBar
@onready var level_label: Label = $LevelLabel
@onready var exp_bar: ProgressBar = $ExpBar
var player: Player
func setup(p: Player):
player = p
# 连接所有信号
player.health_changed.connect(_on_health_changed)
player.mana_changed.connect(_on_mana_changed)
player.leveled_up.connect(_on_leveled_up)
player.experience_gained.connect(_on_exp_gained)
# 初始化显示
_on_health_changed(player.health, player.max_health)
_on_mana_changed(player.mana, player.max_mana)
_update_level_display()
func _on_health_changed(current: int, maximum: int):
health_bar.max_value = maximum
health_bar.value = current
func _on_mana_changed(current: int, maximum: int):
mana_bar.max_value = maximum
mana_bar.value = current
func _on_leveled_up(new_level: int):
_update_level_display()
play_levelup_effect()
func _on_exp_gained(amount: int):
exp_bar.value = player.experience
exp_bar.max_value = player.get_exp_for_next_level()
func _update_level_display():
level_label.text = "Lv. %d" % player.level
func play_levelup_effect():
# 播放升级特效
pass
11.6.3 游戏事件系统
# game_events.gd (Autoload)
extends Node
# 游戏流程
signal game_started
signal game_paused
signal game_resumed
signal game_over(score: int)
# 玩家事件
signal player_spawned(player: Player)
signal player_died(player: Player)
# 战斗事件
signal enemy_spawned(enemy: Enemy)
signal enemy_killed(enemy: Enemy, killer: Node)
signal boss_appeared(boss: Boss)
signal boss_defeated(boss: Boss)
# 进度事件
signal checkpoint_reached(checkpoint_id: String)
signal level_completed(level_id: String, time: float, score: int)
# 物品事件
signal item_picked_up(item: Item, picker: Node)
signal item_used(item: Item, user: Node)
# 成就事件
signal achievement_progress(id: String, current: int, target: int)
signal achievement_unlocked(id: String)
本章小结
本章我们全面学习了Godot的信号系统:
- 信号基础:什么是信号、内置信号、连接方式
- 自定义信号:声明、发送、接收信号
- 连接选项:标志、绑定参数、断开连接
- 高级用法:await、信号转发、事件总线
- 最佳实践:命名约定、参数设计、使用场景
- 实际案例:玩家系统、UI系统、游戏事件
信号是Godot实现松耦合架构的关键工具。下一章我们将学习错误处理与调试,让你的代码更加健壮。
上一章:类与继承
下一章:错误处理与调试