第十一章:信号系统

第十一章:信号系统

"信号是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的信号系统:

  1. 信号基础:什么是信号、内置信号、连接方式
  2. 自定义信号:声明、发送、接收信号
  3. 连接选项:标志、绑定参数、断开连接
  4. 高级用法:await、信号转发、事件总线
  5. 最佳实践:命名约定、参数设计、使用场景
  6. 实际案例:玩家系统、UI系统、游戏事件

信号是Godot实现松耦合架构的关键工具。下一章我们将学习错误处理与调试,让你的代码更加健壮。


上一章:类与继承

下一章:错误处理与调试

← 返回目录