Godot 4 GDScript 教程 / 信号系统(await/信号连接)
信号系统(await/信号连接)
信号是 Godot 引擎实现节点间通信的核心机制,遵循观察者模式。Godot 4 对信号系统进行了重大改进,引入了类型化信号、Callable 连接语法和 await 协程集成。掌握信号系统是编写解耦、可维护代码的关键。
1. 信号定义与发射
1.1 定义信号
extends CharacterBody2D
# 基本信号定义
signal health_changed
signal died
# 带参数的信号(类型化信号)
signal damaged(amount: int, source: Node)
signal healed(amount: int)
signal level_up(new_level: int, bonus: int)
signal position_changed(old_pos: Vector2, new_pos: Vector2)
# 信号可以有多达 8 个参数
signal complex_signal(a: int, b: float, c: String, d: bool, e: Vector2)
1.2 发射信号
extends CharacterBody2D
signal health_changed(old_value: int, new_value: int)
signal died
signal score_gained(points: int)
var health: int = 100
var max_health: int = 100
func take_damage(amount: int) -> void:
var old_health := health
health = clampi(health - amount, 0, max_health)
# 发射信号
health_changed.emit(old_health, health)
if health <= 0:
died.emit()
func heal(amount: int) -> void:
var old_health := health
health = clampi(health + amount, 0, max_health)
health_changed.emit(old_health, health)
func add_score(points: int) -> void:
score_gained.emit(points)
# 使用 emit 与旧语法对比
func old_syntax() -> void:
# Godot 3 旧语法(已弃用)
# emit_signal("health_changed", old_health, health)
# Godot 4 新语法(推荐)
health_changed.emit(health, health)
2. 信号连接
2.1 基本连接(新 Callable 语法)
extends Node
signal data_loaded(data: Dictionary)
signal error_occurred(message: String)
@onready var button: Button = $Button
@onready var timer: Timer = $Timer
@onready var area: Area2D = $Area2D
func _ready() -> void:
# 新语法:signal.connect(callable)
button.pressed.connect(_on_button_pressed)
timer.timeout.connect(_on_timer_timeout)
area.body_entered.connect(_on_body_entered)
# 连接到自身信号
data_loaded.connect(_on_data_loaded)
error_occurred.connect(_on_error)
# Lambda 连接
button.pressed.connect(func():
print("按钮被点击(Lambda)")
)
func _on_button_pressed() -> void:
print("按钮被点击")
func _on_timer_timeout() -> void:
print("定时器超时")
func _on_body_entered(body: Node2D) -> void:
print(f"物体进入: {body.name}")
func _on_data_loaded(data: Dictionary) -> void:
print(f"数据加载完成: {data}")
func _on_error(message: String) -> void:
print(f"错误: {message}")
2.2 连接选项
extends Node
signal important_event
signal one_time_event
func _ready() -> void:
# CONNECT_ONE_SHOT: 只连接一次,触发后自动断开
one_time_event.connect(_on_one_time, CONNECT_ONE_SHOT)
# CONNECT_DEFERRED: 延迟到帧末处理(线程安全)
important_event.connect(_on_important, CONNECT_DEFERRED)
# CONNECT_PERSIST: 持久化连接(保存到场景文件)
# 在编辑器中连接信号时使用
# 组合标志
# important_event.connect(_on_important, CONNECT_ONE_SHOT | CONNECT_DEFERRED)
func _on_one_time() -> void:
print("这只执行一次")
func _on_important() -> void:
print("延迟处理的事件")
2.3 断开连接
extends Node
func _ready() -> void:
# 连接信号
some_signal.connect(_on_signal)
# 断开连接
some_signal.disconnect(_on_signal)
# 检查是否已连接
if some_signal.is_connected(_on_signal):
some_signal.disconnect(_on_signal)
# 获取所有连接
var connections := some_signal.get_connections()
for conn in connections:
print(f"连接: {conn['callable']}")
# 动态连接管理
func enable_shooting() -> void:
if not input_action.is_connected(_shoot):
input_action.connect(_shoot)
func disable_shooting() -> void:
if input_action.is_connected(_shoot):
input_action.disconnect(_shoot)
3. 信号参数与绑定
3.1 绑定额外参数
extends Node
@onready var enemy_container: Node2D = $Enemies
func _ready() -> void:
# 为每个敌人连接信号并绑定标识
for i in range(enemy_container.get_child_count()):
var enemy: Node2D = enemy_container.get_child(i)
if enemy.has_signal("died"):
# bind() 添加额外参数
enemy.died.connect(_on_enemy_died.bind(i, enemy.name))
func _on_enemy_died(enemy_index: int, enemy_name: String) -> void:
print(f"敌人 {enemy_name} (索引 {enemy_index}) 已死亡")
# 绑定多个参数
signal damage_dealt(amount: int, damage_type: String)
func _ready() -> void:
damage_dealt.connect(
_on_damage.bind("火焰", true)
)
func _on_damage(
amount: int,
damage_type: String,
is_critical: bool
) -> void:
print(f"造成 {amount} 点{damage_type}伤害" + (" 暴击!" if is_critical else ""))
3.2 信号参数类型
# 信号参数支持所有 Variant 类型
signal entity_spawned(entity: Node2D, spawn_data: Dictionary)
signal path_found(path: PackedVector2Array, cost: float)
signal inventory_updated(items: Array[Dictionary], change_type: StringName)
signal texture_loaded(texture: Texture2D, resource_path: String)
signal animation_event(event_name: StringName, frame: int)
# 使用类型化信号
func _ready() -> void:
entity_spawned.connect(_on_entity_spawned)
func _on_entity_spawned(entity: Node2D, spawn_data: Dictionary) -> void:
entity.position = spawn_data.get("position", Vector2.ZERO)
entity.rotation = spawn_data.get("rotation", 0.0)
4. await 与信号
4.1 等待信号
extends Node
signal loading_complete
signal user_input(text: String)
# 等待自定义信号
async func load_game_data() -> Dictionary:
print("开始加载...")
# 模拟异步加载
await get_tree().create_timer(2.0).timeout
loading_complete.emit()
return {"player": "Hero", "level": 1}
async func wait_for_user() -> String:
print("等待用户输入...")
var text: String = await user_input
return text
# 等待内置信号
async func fade_out(duration: float = 1.0) -> void:
var tween := create_tween()
tween.tween_property(self, "modulate:a", 0.0, duration)
await tween.finished
async func play_and_wait(anim_name: String) -> void:
var anim: AnimationPlayer = $AnimationPlayer
anim.play(anim_name)
await anim.animation_finished
async func wait_for_click() -> void:
await get_tree().create_timer(0.1).timeout # 避免立即触发
await get_viewport().gui_input_event # 等待输入事件
4.2 并行等待
# 等待多个信号中的任意一个
async func wait_for_any(signals_array: Array[Signal]) -> void:
var semaphore := Semaphore.new()
var callback = func(): semaphore.post()
for sig in signals_array:
sig.connect(callback, CONNECT_ONE_SHOT)
await semaphore.wait_completed # 注意:这在 GDScript 中的实现方式
# 更实用的模式:使用竞速
async func race_with_timeout(
work_callable: Callable,
timeout: float
) -> Variant:
var result: Variant = null
var completed := false
# 设置超时
get_tree().create_timer(timeout).timeout.connect(func():
if not completed:
completed = true
result = null # 超时返回 null
)
# 执行工作
result = await work_callable.call()
completed = true
return result
# 使用示例
async func load_with_timeout() -> Dictionary:
var data := await race_with_timeout(_load_data, 5.0)
if data == null:
print("加载超时!")
return {}
return data
5. 信号链
5.1 链式信号处理
extends Node
# 信号链:一个信号触发另一个信号
signal raw_input(action: String)
signal validated_input(action: String)
signal processed_action(action: String, result: Dictionary)
func _ready() -> void:
# 建立信号链
raw_input.connect(_validate_input)
validated_input.connect(_process_action)
processed_action.connect(_apply_result)
func _validate_input(action: String) -> void:
# 验证输入合法性
if action.is_empty():
return
validated_input.emit(action)
func _process_action(action: String) -> void:
# 处理动作
var result := {"action": action, "success": true}
processed_action.emit(action, result)
func _apply_result(action: String, result: Dictionary) -> void:
# 应用结果
if result["success"]:
print(f"动作 {action} 执行成功")
# 触发链
func _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed("attack"):
raw_input.emit("attack")
5.2 事件总线模式
# autoload/game_events.gd
# 通过 项目 > 项目设置 > 自动加载 添加为 GameEvents
extends Node
# ── 游戏事件 ──────────────────────────
signal game_started
signal game_paused
signal game_resumed
signal game_over(score: int, reason: String)
# ── 玩家事件 ──────────────────────────
signal player_spawned(player: CharacterBody2D)
signal player_died(player: CharacterBody2D)
signal player_leveled_up(new_level: int)
signal player_hit(damage: int, source: Node)
signal player_healed(amount: int)
# ── 敌人事件 ──────────────────────────
signal enemy_spawned(enemy: Node2D, enemy_type: String)
signal enemy_died(enemy: Node2D, reward: int)
signal boss_defeated(boss_name: String)
# ── UI 事件 ──────────────────────────
signal ui_notification(message: String, type: String)
signal ui_dialogue_started(npc_name: String)
signal ui_dialogue_ended
signal ui_menu_opened(menu_name: String)
signal ui_menu_closed
# ── 系统事件 ──────────────────────────
signal save_requested
signal load_requested
signal settings_changed(category: String)
# 使用全局事件总线的示例
extends CharacterBody2D
var health: int = 100
var level: int = 1
var experience: int = 0
func _ready() -> void:
# 连接全局事件
GameEvents.player_spawned.emit(self)
# 监听事件
GameEvents.game_paused.connect(_on_game_paused)
GameEvents.game_resumed.connect(_on_game_resumed)
func take_damage(amount: int, source: Node) -> void:
health -= amount
GameEvents.player_hit.emit(amount, source)
if health <= 0:
GameEvents.player_died.emit(self)
GameEvents.game_over.emit(_calculate_score(), "生命值耗尽")
func gain_experience(amount: int) -> void:
experience += amount
if experience >= _exp_to_next_level():
level += 1
GameEvents.player_leveled_up.emit(level)
func _calculate_score() -> int:
return level * 1000 + experience
func _exp_to_next_level() -> int:
return level * level * 100
func _on_game_paused() -> void:
set_process(false)
set_physics_process(false)
func _on_game_resumed() -> void:
set_process(true)
set_physics_process(true)
💡 提示: 全局事件总线可以有效解耦各个系统,让玩家、敌人、UI 等模块独立运行。
6. 信号与解耦设计
6.1 组件化设计
# health_component.gd - 独立的生命值组件
class_name HealthComponent
extends Node
signal health_changed(old_value: int, new_value: int)
signal health_depleted
signal healed(amount: int)
signal damage_taken(amount: int)
@export var max_health: int = 100
@export var invincible: bool = false
@export var invincibility_time: float = 0.5
var current_health: int
var _invincibility_timer: float = 0.0
func _ready() -> void:
current_health = max_health
func _process(delta: float) -> void:
if _invincibility_timer > 0:
_invincibility_timer -= delta
func take_damage(amount: int) -> bool:
"""返回是否实际受到伤害"""
if invincible or _invincibility_timer > 0:
return false
if amount <= 0:
return false
var old_health := current_health
current_health = clampi(current_health - amount, 0, max_health)
damage_taken.emit(amount)
health_changed.emit(old_health, current_health)
_invincibility_timer = invincibility_time
if current_health <= 0:
health_depleted.emit()
return true
func heal(amount: int) -> void:
var old_health := current_health
current_health = clampi(current_health + amount, 0, max_health)
healed.emit(amount)
health_changed.emit(old_health, current_health)
func get_health_percent() -> float:
return float(current_health) / float(max_health) if max_health > 0 else 0.0
# player.gd - 使用 HealthComponent
extends CharacterBody2D
@onready var health_comp: HealthComponent = $HealthComponent
@onready var sprite: Sprite2D = $Sprite2D
@onready var anim_player: AnimationPlayer = $AnimationPlayer
func _ready() -> void:
# 通过信号实现解耦
health_comp.health_changed.connect(_on_health_changed)
health_comp.health_depleted.connect(_on_health_depleted)
health_comp.damage_taken.connect(_on_damage_taken)
func _on_health_changed(old_value: int, new_value: int) -> void:
GameEvents.player_hit.emit(new_value - old_value, self)
func _on_health_depleted() -> void:
GameEvents.player_died.emit(self)
anim_player.play("death")
func _on_damage_taken(amount: int) -> void:
# 闪白效果
sprite.modulate = Color.WHITE
var tween := create_tween()
tween.tween_property(sprite, "modulate", Color.RED, 0.1)
tween.tween_property(sprite, "modulate", Color.WHITE, 0.1)
7. 类型化信号
# 类型化信号 - Godot 4 支持信号参数类型
extends Node
# 完全类型化的信号
signal position_updated(new_position: Vector2)
signal inventory_changed(items: Array[Dictionary], operation: StringName)
signal stats_modified(stat_name: StringName, old_value: float, new_value: float)
signal target_acquired(target: Node2D, threat_level: int)
# 信号本身也可以作为类型
var on_damage_callback: Signal
func _ready() -> void:
# 信号可以赋值给变量
on_damage_callback = stats_modified
# 连接类型化信号
position_updated.connect(_on_position_updated)
inventory_changed.connect(_on_inventory_changed)
func _on_position_updated(new_position: Vector2) -> void:
# 参数类型自动匹配
global_position = new_position
func _on_inventory_changed(items: Array[Dictionary], operation: StringName) -> void:
match operation:
"add":
print(f"添加物品,当前 {items.size()} 件")
"remove":
print(f"移除物品,当前 {items.size()} 件")
8. 信号调试
8.1 信号调试工具
# 信号调试辅助函数
class_name SignalDebugger
static func print_signal_connections(node: Node) -> void:
"""打印节点所有信号的连接信息"""
var signal_list := node.get_signal_list()
for sig_info in signal_list:
var connections := node.get_signal_connection_list(sig_info["name"])
if connections.size() > 0:
print(f"\n信号 '{sig_info['name']}':")
for conn in connections:
print(f" -> {conn['callable']} (flags: {conn['flags']})")
static func get_signal_count(node: Node) -> int:
"""获取节点信号总数"""
return node.get_signal_list().size()
static func is_signal_connected_to(
emitter: Node,
signal_name: String,
target: Object,
method: String
) -> bool:
"""检查信号是否连接到特定方法"""
var connections := emitter.get_signal_connection_list(signal_name)
for conn in connections:
var callable: Callable = conn["callable"]
if callable.get_object() == target and callable.get_method() == method:
return true
return false
# 在游戏中使用
func _ready() -> void:
if OS.is_debug_build():
SignalDebugger.print_signal_connections(self)
8.2 常见信号问题排查
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 信号未触发 | 未正确连接 | 检查 connect() 是否在 _ready() 中调用 |
| 信号参数不匹配 | 参数数量/类型不一致 | 确保 emit() 参数与定义匹配 |
| 对象已释放 | 节点在信号发出前被删除 | 使用 is_instance_valid() 检查 |
| 连接多次 | _ready() 被多次调用 | 使用 CONNECT_ONE_SHOT 或检查已连接 |
| 延迟问题 | 使用了 CONNECT_DEFERRED | 理解延迟处理机制 |
9. 游戏开发场景
场景:成就系统
# autoload/achievement_system.gd
extends Node
signal achievement_unlocked(achievement_id: String, title: String, description: String)
signal achievement_progress(achievement_id: String, current: int, required: int)
## 成就定义
var achievements: Dictionary = {
"first_blood": {
"title": "初次击杀",
"description": "击败第一个敌人",
"icon": "res://assets/icons/first_blood.png",
"unlocked": false,
"progress": 0,
"required": 1,
"hidden": false
},
"combo_master": {
"title": "连击大师",
"description": "达成50连击",
"icon": "res://assets/icons/combo.png",
"unlocked": false,
"progress": 0,
"required": 50,
"hidden": false
},
"speed_runner": {
"title": "速通达人",
"description": "在10分钟内通关",
"icon": "res://assets/icons/speed.png",
"unlocked": false,
"hidden": true
}
}
func _ready() -> void:
# 连接游戏事件
GameEvents.enemy_died.connect(_on_enemy_died)
GameEvents.boss_defeated.connect(_on_boss_defeated)
GameEvents.player_leveled_up.connect(_on_player_leveled_up)
GameEvents.game_over.connect(_on_game_over)
func report_progress(achievement_id: String, amount: int = 1) -> void:
if not achievements.has(achievement_id):
return
var ach: Dictionary = achievements[achievement_id]
if ach["unlocked"]:
return
ach["progress"] = mini(ach["progress"] + amount, ach["required"])
achievement_progress.emit(achievement_id, ach["progress"], ach["required"])
if ach["progress"] >= ach["required"]:
_unlock(achievement_id)
func _unlock(achievement_id: String) -> void:
var ach: Dictionary = achievements[achievement_id]
ach["unlocked"] = true
achievement_unlocked.emit(achievement_id, ach["title"], ach["description"])
GameEvents.ui_notification.emit(
f"🏆 成就解锁: {ach['title']}!",
"achievement"
)
func _on_enemy_died(_enemy: Node2D, _reward: int) -> void:
report_progress("first_blood")
func _on_boss_defeated(boss_name: String) -> void:
report_progress("boss_slayer")
func _on_player_leveled_up(new_level: int) -> void:
report_progress("level_" + str(new_level))
func _on_game_over(score: int, reason: String) -> void:
if score >= 10000:
report_progress("high_scorer")
10. 扩展阅读
上一章: 06 - 函数与 Lambda 下一章: 08 - 节点与场景树