强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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 - 节点与场景树