第一部分 08:Signal 信号系统
这一章讲 Godot 里非常核心的通信机制:Signal。
先用一句话理解:
Signal = Godot 的事件通知机制
一个节点发生了某件事,它可以发出信号;其他节点可以连接这个信号,然后在信号发生时执行自己的函数。Godot 官方文档也这样描述:节点在特定事情发生时会发出信号,比如按钮被按下;其他节点可以连接这些信号并响应事件。(Godot Engine documentation)
1. Signal 是什么
Signal 翻译成中文一般叫:
信号
它的作用是:
一个对象告诉外界:我这里发生了一件事。
比如:
Button 被点击了
Area2D 检测到玩家进来了
玩家血量变化了
宝箱被打开了
怪物死亡了
天气改变了
任务完成了
这些都很适合用 signal。
2. 前端类比
前端里你肯定熟悉事件:
button.addEventListener("click", handleClick)
Vue 里:
<button @click="handleClick">点击</button>
React 里:
<button onClick={handleClick}>点击</button>
Godot 里类似:
button.pressed.connect(_on_button_pressed)
func _on_button_pressed() -> void:
print("按钮被点击了")
所以可以这样类比:
前端 click 事件 ≈ Godot pressed 信号
前端 addEventListener ≈ Godot connect
前端 dispatchEvent / emit ≈ Godot signal.emit()
3. 为什么需要 Signal
假设玩家血量变化后,UI 要更新血条。
一种很糟糕的写法是:
player.hud.health_label.text = str(player.health)
这个问题很大。
因为 Player 直接依赖 HUD。
以后你的结构一变,比如 HUD 路径换了、UI 拆成独立场景了、血条换成 ProgressBar 了,Player 代码就要跟着改。
更好的方式是:
Player 只负责发出 health_changed 信号
HUD 自己监听这个信号并更新 UI
代码大概是:
signal health_changed(value: int)
func take_damage(amount: int) -> void:
health -= amount
health_changed.emit(health)
HUD 监听:
player.health_changed.connect(_on_player_health_changed)
func _on_player_health_changed(value: int) -> void:
health_label.text = "生命:" + str(value)
这样 Player 不需要知道 HUD 的存在。
这就是 Signal 最重要的价值:
减少对象之间的直接依赖
Godot 的 Signal 类文档也说明,信号允许连接的 Callable 监听并响应事件,而不需要对象之间直接引用,从而让代码更灵活、更容易管理。(Godot Engine documentation)
4. Signal 的核心流程
Signal 的流程通常是三步:
1. 声明信号
2. 连接信号
3. 发出信号
对应代码:
signal health_changed(value: int)
func _ready() -> void:
health_changed.connect(_on_health_changed)
func take_damage(amount: int) -> void:
health -= amount
health_changed.emit(health)
func _on_health_changed(value: int) -> void:
print("血量变了:", value)
不过更常见的是:
一个节点发信号
另一个节点连接并响应
比如:
Player 发出 health_changed
HUD 监听 health_changed
5. 内置信号
Godot 很多节点自带信号。
常见的有:
Button.pressed
Area2D.body_entered
Area2D.body_exited
Area2D.area_entered
Area2D.area_exited
Timer.timeout
AnimationPlayer.animation_finished
这些是节点本来就有的,不需要你自己声明。
6. Button.pressed:按钮点击信号
它是什么
pressed 是 Button 的常用信号。
当按钮被按下时触发。
Godot Button 文档里也有示例:创建 Button 后,使用 button.pressed.connect(_button_pressed) 连接按钮点击函数。(Godot Engine documentation)
代码连接方式
场景结构:
Main Control
└── StartButton Button
脚本:
extends Control
@onready var start_button: Button = $StartButton
func _ready() -> void:
start_button.pressed.connect(_on_start_button_pressed)
func _on_start_button_pressed() -> void:
print("开始游戏")
编辑器连接方式
也可以在编辑器里连接:
1. 选中 Button
2. 右侧切到 Node 面板
3. 找到 Signals
4. 双击 pressed
5. 选择要连接到哪个节点
6. Godot 自动生成函数
生成出来通常像这样:
func _on_start_button_pressed() -> void:
pass
7. Area2D.body_entered:身体进入区域
它是什么
body_entered 是 Area2D 很常用的信号。
当一个物理 Body 进入这个区域时触发。
Area2D 官方文档说明,Area2D 是 2D 空间区域,可以检测其他 CollisionObject2D 进入或离开;区域形状由 CollisionShape2D 或 CollisionPolygon2D 子节点定义。(Godot Engine documentation)
常见用途
玩家靠近 NPC
玩家进入宝箱交互范围
玩家进入门口
怪物进入攻击范围
角色踩到陷阱
进入天气区域
进入室内区域
基础例子
场景结构:
TalkArea Area2D
└── CollisionShape2D
脚本:
extends Area2D
func _ready() -> void:
body_entered.connect(_on_body_entered)
body_exited.connect(_on_body_exited)
func _on_body_entered(body: Node2D) -> void:
print(body.name, "进入区域")
func _on_body_exited(body: Node2D) -> void:
print(body.name, "离开区域")
8. body_entered 和 area_entered 的区别
Area2D 有几类常见信号:
body_entered
body_exited
area_entered
area_exited
它们区别是:
body_entered:
检测 PhysicsBody2D 进入,比如 CharacterBody2D、StaticBody2D、RigidBody2D。
area_entered:
检测另一个 Area2D 进入。
简单记:
检测玩家身体进来:body_entered
检测另一个检测区域进来:area_entered
比如玩家是:
Player CharacterBody2D
那就用:
body_entered.connect(_on_body_entered)
如果你检测的是玩家身上的攻击范围:
AttackArea Area2D
那可能用:
area_entered.connect(_on_area_entered)
9. Timer.timeout:计时结束信号
它是什么
Timer 到时间后会发出 timeout 信号。
比如:
每 1 秒更新时间
每 3 秒生成怪物
技能冷却结束
提示框 2 秒后隐藏
天气每隔一段时间变化
示例:每秒打印一次
场景结构:
Main Node
└── Timer
Timer 设置:
Wait Time: 1
One Shot: false
Autostart: true
脚本:
extends Node
@onready var timer: Timer = $Timer
func _ready() -> void:
timer.timeout.connect(_on_timer_timeout)
func _on_timer_timeout() -> void:
print("过了 1 秒")
10. AnimationPlayer.animation_finished:动画结束信号
它是什么
AnimationPlayer 播放完动画后会发出:
animation_finished
常见用途:
宝箱打开动画结束后发奖励
攻击动画结束后恢复状态
死亡动画结束后删除敌人
淡出动画结束后切场景
UI 弹出动画结束后允许操作
示例:动画结束后删除节点
extends Node2D
@onready var animation_player: AnimationPlayer = $AnimationPlayer
func _ready() -> void:
animation_player.animation_finished.connect(_on_animation_finished)
func die() -> void:
animation_player.play("die")
func _on_animation_finished(anim_name: StringName) -> void:
if anim_name == "die":
queue_free()
这里的逻辑是:
播放死亡动画
动画结束
删除节点
这比直接 queue_free() 更自然。
11. 自定义信号 signal
内置信号不够用时,可以自己声明 signal。
官方文档也有 Custom signals 部分,说明可以在脚本中定义自己的信号,比如玩家血量归零时发出 died 或 health_depleted。(Godot Engine documentation)
基础写法
signal health_changed(value: int)
这声明了一个叫 health_changed 的信号。
它会携带一个参数:
value: int
发出信号
health_changed.emit(health)
完整:
extends Node
signal health_changed(value: int)
var health: int = 100
func take_damage(amount: int) -> void:
health -= amount
health_changed.emit(health)
12. emit:发出信号
emit 就是“发出这个信号”。
health_changed.emit(health)
意思是:
通知所有连接了 health_changed 的对象:
血量变化了,新血量是 health。
如果信号没有参数:
signal died
func die() -> void:
died.emit()
如果信号有多个参数:
signal item_collected(item_id: String, amount: int)
func collect_item() -> void:
item_collected.emit("potion", 1)
13. connect:连接信号
connect 的作用是:
当这个信号发出时,调用某个函数。
基础写法:
player.health_changed.connect(_on_player_health_changed)
完整:
func _ready() -> void:
player.health_changed.connect(_on_player_health_changed)
func _on_player_health_changed(value: int) -> void:
print("玩家血量变化:", value)
注意这里不是:
player.health_changed.connect(_on_player_health_changed())
不要加括号。
因为你不是现在调用函数,而是把函数本身交给信号系统。
正确:
connect(_on_button_pressed)
错误:
connect(_on_button_pressed())
这个坑特别常见。
14. 信号参数怎么传
假设信号这样声明:
signal health_changed(value: int)
发出时:
health_changed.emit(80)
接收函数要能接住这个参数:
func _on_health_changed(value: int) -> void:
print(value)
如果信号有两个参数:
signal item_collected(item_id: String, amount: int)
发出:
item_collected.emit("potion", 2)
接收:
func _on_item_collected(item_id: String, amount: int) -> void:
print(item_id, amount)
参数数量最好对应。
15. 例子:玩家血量变化更新 UI
这是 Signal 最经典的用法。
Player.gd
class_name Player
extends CharacterBody2D
signal health_changed(value: int)
signal died
@export var max_health: int = 100
var health: int
func _ready() -> void:
health = max_health
health_changed.emit(health)
func take_damage(amount: int) -> void:
if health <= 0:
return
health -= amount
health = max(health, 0)
health_changed.emit(health)
if health <= 0:
died.emit()
HUD.gd
class_name HUD
extends Control
@onready var health_label: Label = $HealthLabel
func bind_player(player: Player) -> void:
player.health_changed.connect(_on_player_health_changed)
player.died.connect(_on_player_died)
func _on_player_health_changed(value: int) -> void:
health_label.text = "生命:" + str(value)
func _on_player_died() -> void:
health_label.text = "生命:0"
print("显示死亡界面")
Main.gd
extends Node2D
@onready var player: Player = $Player
@onready var hud: HUD = $UI/HUD
func _ready() -> void:
hud.bind_player(player)
这里的关系很清楚:
Player 负责血量逻辑
HUD 负责显示
Main 负责把它们连接起来
Player 不知道 HUD 长什么样。
HUD 不负责扣血。
这就干净很多。
16. 例子:宝箱打开后通知主场景加金币
Chest.gd
class_name Chest
extends StaticBody2D
signal opened(reward_gold: int)
@export var reward_gold: int = 100
@onready var animation_player: AnimationPlayer = $AnimationPlayer
var is_opened: bool = false
func interact() -> void:
if is_opened:
return
is_opened = true
animation_player.play("open")
opened.emit(reward_gold)
Main.gd
extends Node2D
var gold: int = 0
@onready var chest: Chest = $World/Objects/Chest
@onready var hud: HUD = $UI/HUD
func _ready() -> void:
chest.opened.connect(_on_chest_opened)
func _on_chest_opened(reward_gold: int) -> void:
gold += reward_gold
hud.update_gold(gold)
这里宝箱只负责:
我打开了
奖励金币是 reward_gold
它不应该直接操作 HUD。
17. 例子:怪物死亡后掉落物品
Enemy.gd
class_name Enemy
extends CharacterBody2D
signal died(enemy: Enemy)
@export var max_health: int = 30
var health: int
func _ready() -> void:
health = max_health
func take_damage(amount: int) -> void:
health -= amount
if health <= 0:
die()
func die() -> void:
died.emit(self)
queue_free()
EnemySpawner.gd
extends Node2D
@export var drop_scene: PackedScene
func spawn_enemy(enemy_scene: PackedScene, pos: Vector2) -> void:
var enemy: Enemy = enemy_scene.instantiate()
add_child(enemy)
enemy.global_position = pos
enemy.died.connect(_on_enemy_died)
func _on_enemy_died(enemy: Enemy) -> void:
var drop = drop_scene.instantiate()
add_child(drop)
drop.global_position = enemy.global_position
这里注意:
died.emit(self)
把死亡的敌人自己传出去。
这样外部可以知道:
是谁死了
死在哪里
掉落物该生成在哪里
18. 例子:NPC 对话开始和结束
NPC.gd
class_name NPC
extends CharacterBody2D
signal dialog_requested(npc_name: String, dialog_text: String)
@export var npc_name: String = "村民"
@export var dialog_text: String = "你好,旅行者。"
func interact() -> void:
dialog_requested.emit(npc_name, dialog_text)
DialogBox.gd
class_name DialogBox
extends Control
signal dialog_finished
@onready var name_label: Label = $NameLabel
@onready var text_label: Label = $TextLabel
func show_dialog(npc_name: String, dialog_text: String) -> void:
visible = true
name_label.text = npc_name
text_label.text = dialog_text
func close_dialog() -> void:
visible = false
dialog_finished.emit()
Main.gd
extends Node2D
@onready var npc: NPC = $World/NPCs/NPC
@onready var dialog_box: DialogBox = $UI/DialogBox
@onready var player: Player = $Player
func _ready() -> void:
npc.dialog_requested.connect(_on_dialog_requested)
dialog_box.dialog_finished.connect(_on_dialog_finished)
func _on_dialog_requested(npc_name: String, dialog_text: String) -> void:
player.input_locked = true
dialog_box.show_dialog(npc_name, dialog_text)
func _on_dialog_finished() -> void:
player.input_locked = false
职责分工:
NPC:我请求显示对话
DialogBox:我负责显示对话
Main:我协调玩家输入锁和 UI
Player:我只管能不能移动
19. 例子:天气变化通知世界和 UI
WeatherManager.gd
class_name WeatherManager
extends Node
signal weather_changed(weather: String)
var current_weather: String = "sunny"
func set_weather(weather: String) -> void:
if current_weather == weather:
return
current_weather = weather
weather_changed.emit(current_weather)
World.gd
extends Node2D
func bind_weather_manager(weather_manager: WeatherManager) -> void:
weather_manager.weather_changed.connect(_on_weather_changed)
func _on_weather_changed(weather: String) -> void:
match weather:
"sunny":
modulate = Color(1, 1, 1)
"rainy":
modulate = Color(0.75, 0.8, 0.9)
"foggy":
modulate = Color(0.8, 0.8, 0.8)
HUD.gd
extends Control
@onready var weather_label: Label = $WeatherLabel
func bind_weather_manager(weather_manager: WeatherManager) -> void:
weather_manager.weather_changed.connect(_on_weather_changed)
func _on_weather_changed(weather: String) -> void:
weather_label.text = "天气:" + weather
一个天气变化,同时通知:
世界画面变化
UI 文本变化
音效变化
NPC 行为变化
这就是 Signal 很适合系统解耦的地方。
20. 连接信号的两种方式
Signal 可以通过两种方式连接:
编辑器连接
代码连接
编辑器连接
适合初学者,直观。
比如 Button:
选中 Button
Node 面板
Signals
pressed
Connect
选择目标节点
生成函数
优点:
可视化
不容易写错信号名
适合简单按钮和 Area2D
缺点:
项目大了不容易追踪
动态生成的节点不方便
多人协作时容易漏看连接关系
代码连接
button.pressed.connect(_on_button_pressed)
优点:
清晰写在代码里
动态生成节点时方便
方便集中管理
适合复杂项目
缺点:
需要自己写对路径和函数名
我的建议:
入门阶段:编辑器连接可以用
正式项目:核心逻辑尽量用代码连接
21. 动态生成节点时连接信号
比如你生成多个按钮。
extends Control
@onready var button_container: VBoxContainer = $ButtonContainer
func create_option_button(text: String) -> void:
var button := Button.new()
button.text = text
button_container.add_child(button)
button.pressed.connect(_on_option_button_pressed.bind(text))
func _on_option_button_pressed(option_text: String) -> void:
print("选择了:", option_text)
这里用了:
bind(text)
它可以把额外参数绑定给回调函数。
22. bind:给信号回调额外传参
假设你有多个按钮:
攻击
防御
道具
逃跑
每个按钮都连接同一个函数,但需要知道点的是哪个。
func _ready() -> void:
$AttackButton.pressed.connect(_on_action_pressed.bind("attack"))
$DefendButton.pressed.connect(_on_action_pressed.bind("defend"))
$ItemButton.pressed.connect(_on_action_pressed.bind("item"))
$RunButton.pressed.connect(_on_action_pressed.bind("run"))
func _on_action_pressed(action: String) -> void:
print("选择动作:", action)
这样就不用写四个函数:
_on_attack_pressed
_on_defend_pressed
_on_item_pressed
_on_run_pressed
23. 断开信号 disconnect
有时候你需要断开信号。
player.health_changed.disconnect(_on_player_health_changed)
但要注意,如果没有连接却去 disconnect,可能会报错。
可以先判断:
if player.health_changed.is_connected(_on_player_health_changed):
player.health_changed.disconnect(_on_player_health_changed)
常见用途:
切换玩家角色
关闭临时 UI
移除监听
对象销毁前清理
避免重复连接
24. 避免重复连接
重复连接可能导致一个信号触发多次。
比如你不小心多次执行:
button.pressed.connect(_on_button_pressed)
结果点一次按钮,函数执行多次。
更稳的写法:
if not button.pressed.is_connected(_on_button_pressed):
button.pressed.connect(_on_button_pressed)
不过正常情况下,只要你在 _ready() 里连接一次,通常没问题。
25. await 等待信号
Signal 不只能 connect,还可以 await。
比如等待 1 秒:
await get_tree().create_timer(1.0).timeout
print("1 秒后执行")
等待动画结束:
animation_player.play("open")
await animation_player.animation_finished
print("打开动画结束")
宝箱例子:
func interact() -> void:
if is_opened:
return
is_opened = true
animation_player.play("open")
await animation_player.animation_finished
print("发放奖励")
这种写法很像前端里的:
await sleep(1000)
但 Godot 等待的是信号。
26. connect 和 await 的区别
connect
适合长期监听。
player.health_changed.connect(_on_player_health_changed)
意思是:
以后每次玩家血量变化,我都要响应。
适合:
血量变化
金币变化
天气变化
时间变化
按钮点击
Area2D 进入离开
await
适合临时等待一次。
await animation_player.animation_finished
意思是:
我现在等这一次动画结束,然后继续往下执行。
适合:
等待动画结束
等待计时器结束
等待淡入淡出结束
等待对话结束
27. 信号命名建议
信号名建议用过去式或事件式。
推荐:
signal health_changed(value: int)
signal died
signal opened
signal item_collected(item_id: String, amount: int)
signal dialog_started
signal dialog_finished
signal weather_changed(weather: String)
signal day_changed(day: int)
signal quest_completed(quest_id: String)
不太推荐:
signal change_health
signal open
signal do_dialog
signal weather
因为 Signal 表示“发生了什么”,不是“命令别人做什么”。
也就是说:
health_changed = 血量已经变化了
opened = 已经打开了
quest_completed = 任务完成了
而不是:
change_health = 去改变血量
open = 去打开
这个语义差别很重要。
28. Signal 适合表达事件,不适合表达命令
推荐:
signal opened
意思是:
我打开了,关心的人自己处理。
不推荐把 Signal 当命令总线乱用:
signal make_player_move
signal force_ui_update
signal create_enemy_now
当然项目大了可以做 EventBus,但入门阶段先不要把所有东西都丢进全局信号中心。那东西用好了是事件总线,用烂了是电子垃圾桶。
29. 谁应该发信号
一个简单规则:
谁拥有这个状态,谁发出这个状态变化的信号。
比如:
Player 拥有 health,所以 Player 发 health_changed
Chest 拥有 opened 状态,所以 Chest 发 opened
WeatherManager 拥有 current_weather,所以 WeatherManager 发 weather_changed
TimeManager 拥有 current_day/current_hour,所以 TimeManager 发 time_changed/day_changed
Inventory 拥有 items,所以 Inventory 发 item_added/item_removed
QuestManager 拥有任务状态,所以 QuestManager 发 quest_completed
不要让 HUD 发 health_changed。
HUD 只是显示血量,不拥有血量。
30. 谁应该连接信号
谁关心这个事件,谁连接。
比如:
HUD 关心 Player.health_changed
Main 关心 Chest.opened
AudioManager 关心 WeatherManager.weather_changed
QuestManager 关心 NPC.dialog_finished
Door 关心 Player 是否有钥匙
有时也可以由一个上层节点统一连接。
比如 Main:
func _ready() -> void:
player.health_changed.connect(hud.update_health)
weather_manager.weather_changed.connect(world.apply_weather)
time_manager.day_changed.connect(hud.update_day)
这种写法很清楚:
Main 负责组装关系
各个模块负责自己的事
31. 常见结构:Main 负责连接各模块
推荐结构:
Main Node2D
├── World Node2D
├── Player
├── Systems Node
│ ├── WeatherManager
│ └── TimeManager
└── UI CanvasLayer
└── HUD
Main.gd:
extends Node2D
@onready var player: Player = $Player
@onready var weather_manager: WeatherManager = $Systems/WeatherManager
@onready var time_manager: TimeManager = $Systems/TimeManager
@onready var hud: HUD = $UI/HUD
@onready var world: Node2D = $World
func _ready() -> void:
player.health_changed.connect(hud.update_health)
weather_manager.weather_changed.connect(hud.update_weather)
weather_manager.weather_changed.connect(world.apply_weather)
time_manager.day_changed.connect(hud.update_day)
这里很优雅:
Player 不知道 HUD
WeatherManager 不知道 World 和 HUD
HUD 不知道天气怎么计算
Main 负责把模块插起来
很像前端里的根组件组合子组件和 store。
32. 直接调用和信号怎么选
这是重点。
直接调用适合什么
当 A 明确需要让 B 立刻做某件事,并且 A 合理地知道 B 存在时,可以直接调用。
比如:
animation_player.play("open")
宝箱内部调用自己的 AnimationPlayer,很合理。
current_interactable.interact()
玩家调用当前交互物的 interact(),也合理。
信号适合什么
当 A 只是宣布“我发生了某件事”,但不应该知道谁会响应时,用 signal。
比如:
health_changed.emit(health)
opened.emit(reward_gold)
weather_changed.emit(current_weather)
简单判断:
我是在命令某个具体对象做事?
用直接调用。
我是在通知外界某件事发生了?
用信号。
33. 不要所有东西都用信号
Signal 很好,但不要滥用。
不推荐:
signal move_requested(direction)
signal animation_requested(anim_name)
signal position_change_requested(pos)
如果这些都是同一个对象内部逻辑,直接调用函数就好。
比如 Player 内部:
func _physics_process(delta: float) -> void:
var direction := get_move_direction()
update_animation(direction)
没必要写成:
movement_changed.emit(direction)
Signal 主要用来跨对象通信。
34. 例子:TimeManager 时间变化
TimeManager.gd
class_name TimeManager
extends Node
signal hour_changed(hour: int)
signal day_changed(day: int)
var day: int = 1
var hour: int = 6
func advance_hour() -> void:
hour += 1
if hour >= 24:
hour = 0
day += 1
day_changed.emit(day)
hour_changed.emit(hour)
HUD.gd
class_name HUD
extends Control
@onready var time_label: Label = $TimeLabel
@onready var day_label: Label = $DayLabel
func bind_time_manager(time_manager: TimeManager) -> void:
time_manager.hour_changed.connect(_on_hour_changed)
time_manager.day_changed.connect(_on_day_changed)
func _on_hour_changed(hour: int) -> void:
time_label.text = "时间:" + str(hour) + ":00"
func _on_day_changed(day: int) -> void:
day_label.text = "第 " + str(day) + " 天"
这样以后:
NPC 作息系统
商店开关门
昼夜光照
天气变化概率
农作物成长
都可以监听 TimeManager 的信号。
35. 例子:Inventory 背包变化
Inventory.gd
class_name Inventory
extends Node
signal item_added(item_id: String, amount: int)
signal item_removed(item_id: String, amount: int)
signal inventory_changed
var items: Dictionary = {}
func add_item(item_id: String, amount: int = 1) -> void:
if not items.has(item_id):
items[item_id] = 0
items[item_id] += amount
item_added.emit(item_id, amount)
inventory_changed.emit()
func remove_item(item_id: String, amount: int = 1) -> void:
if not items.has(item_id):
return
items[item_id] -= amount
if items[item_id] <= 0:
items.erase(item_id)
item_removed.emit(item_id, amount)
inventory_changed.emit()
InventoryPanel.gd
class_name InventoryPanel
extends Control
var inventory: Inventory
func bind_inventory(value: Inventory) -> void:
inventory = value
inventory.inventory_changed.connect(refresh)
func refresh() -> void:
print("刷新背包 UI")
这里 Inventory 不知道 UI 怎么显示。
它只通知:
背包变了
UI 自己刷新。
36. 例子:任务完成
QuestManager.gd
class_name QuestManager
extends Node
signal quest_completed(quest_id: String)
var completed_quests: Array[String] = []
func complete_quest(quest_id: String) -> void:
if completed_quests.has(quest_id):
return
completed_quests.append(quest_id)
quest_completed.emit(quest_id)
HUD.gd
func bind_quest_manager(quest_manager: QuestManager) -> void:
quest_manager.quest_completed.connect(_on_quest_completed)
func _on_quest_completed(quest_id: String) -> void:
print("任务完成:", quest_id)
AudioManager.gd
func bind_quest_manager(quest_manager: QuestManager) -> void:
quest_manager.quest_completed.connect(_on_quest_completed)
func _on_quest_completed(quest_id: String) -> void:
print("播放任务完成音效")
一个信号可以被多个对象监听:
HUD 显示提示
AudioManager 播放音效
SaveManager 保存进度
这就是信号的威力。
37. 一个信号可以连接多个函数
比如:
player.died.connect(hud.show_game_over)
player.died.connect(audio_manager.play_game_over_music)
player.died.connect(game_manager.stop_game)
当玩家死亡:
died.emit()
这三个函数都会响应。
这和前端事件很像:
eventEmitter.on("playerDied", showGameOver)
eventEmitter.on("playerDied", playMusic)
eventEmitter.on("playerDied", stopGame)
38. 一个函数也可以响应多个信号
比如多个按钮都调用同一个函数:
$StartButton.pressed.connect(_on_menu_button_pressed.bind("start"))
$LoadButton.pressed.connect(_on_menu_button_pressed.bind("load"))
$QuitButton.pressed.connect(_on_menu_button_pressed.bind("quit"))
func _on_menu_button_pressed(action: String) -> void:
match action:
"start":
start_game()
"load":
load_game()
"quit":
quit_game()
这样代码更集中。
39. 信号和 Group 的区别
后面会单独讲 Group,但这里先简单区分。
Signal:
某个对象发生事件,通知监听者。
Group:
把一批节点归类,然后统一调用它们。
比如:
Signal:
Player.health_changed → HUD 更新血量
Group:
所有 enemies 组里的敌人 → 全部暂停
对应:
get_tree().call_group("enemies", "freeze")
简单记:
Signal 是事件通知
Group 是批量管理
40. 信号和 Autoload/EventBus
项目大了以后,可能会用一个全局事件总线。
比如:
# Events.gd
extends Node
signal weather_changed(weather: String)
signal quest_completed(quest_id: String)
signal item_collected(item_id: String, amount: int)
然后任何地方都能:
Events.quest_completed.emit("first_quest")
或者:
Events.quest_completed.connect(_on_quest_completed)
这个方式有用,但不建议一开始就滥用。
因为它的问题是:
谁都能发
谁都能听
关系容易变隐形
调试起来比较麻烦
入门阶段推荐:
局部对象之间,用直接信号连接
全局跨系统事件,再考虑 EventBus
比如:
Player.health_changed 不一定要放 EventBus
QuestManager.quest_completed 可以考虑全局
WeatherManager.weather_changed 可以看项目规模决定
41. 信号常见错误
错误 1:connect 时加了括号
错误:
button.pressed.connect(_on_button_pressed())
正确:
button.pressed.connect(_on_button_pressed)
原因:
不加括号 = 把函数引用传进去
加括号 = 现在就调用这个函数
错误 2:信号参数和函数参数对不上
信号:
signal health_changed(value: int)
发出:
health_changed.emit(health)
回调却写:
func _on_health_changed() -> void:
pass
不推荐。
应该:
func _on_health_changed(value: int) -> void:
pass
错误 3:重复连接导致执行多次
如果你多次执行:
button.pressed.connect(_on_button_pressed)
可能会导致点一次按钮,回调执行多次。
可以防一下:
if not button.pressed.is_connected(_on_button_pressed):
button.pressed.connect(_on_button_pressed)
错误 4:节点路径错了,导致连接失败
比如结构是:
UI
└── HUD
└── StartButton
你写:
@onready var start_button: Button = $StartButton
就错了。
应该:
@onready var start_button: Button = $UI/HUD/StartButton
或者脚本挂在 HUD 上:
@onready var start_button: Button = $StartButton
错误 5:发信号的对象已经被删除
比如敌人死亡:
died.emit()
queue_free()
这个顺序通常可以。
但如果你先:
queue_free()
died.emit()
就不合理。
应该先通知,再删除。
错误 6:用信号替代所有函数调用
不推荐为了“解耦”把内部逻辑全做成信号。
比如 Player 内部播放动画,直接调用:
animation_player.play("walk_down")
没必要发:
animation_requested.emit("walk_down")
42. Signal 命名和函数命名习惯
信号名:
signal health_changed(value: int)
回调函数名通常写:
func _on_player_health_changed(value: int) -> void:
pass
格式:
_on_谁_什么信号
比如:
func _on_start_button_pressed() -> void:
pass
func _on_interact_area_body_entered(body: Node2D) -> void:
pass
func _on_animation_player_animation_finished(anim_name: StringName) -> void:
pass
func _on_weather_manager_weather_changed(weather: String) -> void:
pass
这个命名会稍微长一点,但可读性好。
43. 推荐你的 RPG 项目常用信号
Player:
signal health_changed(value: int)
signal mana_changed(value: int)
signal died
signal interacted(target: Node)
signal gold_changed(value: int)
Chest:
signal opened(reward_gold: int)
NPC:
signal dialog_requested(npc: NPC)
Inventory:
signal item_added(item_id: String, amount: int)
signal item_removed(item_id: String, amount: int)
signal inventory_changed
TimeManager:
signal hour_changed(hour: int)
signal day_changed(day: int)
signal season_changed(season: String)
WeatherManager:
signal weather_changed(weather: String)
QuestManager:
signal quest_started(quest_id: String)
signal quest_completed(quest_id: String)
signal objective_updated(quest_id: String)
DialogBox:
signal dialog_started
signal dialog_finished
signal line_changed(index: int)
44. 一个完整小型信号架构例子
场景结构:
Main Node2D
├── Player
├── World Node2D
│ └── Chest
├── Systems Node
│ ├── TimeManager
│ └── WeatherManager
└── UI CanvasLayer
└── HUD
Player.gd
class_name Player
extends CharacterBody2D
signal health_changed(value: int)
signal died
@export var max_health: int = 100
var health: int
func _ready() -> void:
health = max_health
health_changed.emit(health)
func take_damage(amount: int) -> void:
health -= amount
health = max(health, 0)
health_changed.emit(health)
if health == 0:
died.emit()
Chest.gd
class_name Chest
extends StaticBody2D
signal opened(reward_gold: int)
@export var reward_gold: int = 100
var is_opened: bool = false
func interact() -> void:
if is_opened:
return
is_opened = true
opened.emit(reward_gold)
TimeManager.gd
class_name TimeManager
extends Node
signal hour_changed(hour: int)
var hour: int = 6
func advance_hour() -> void:
hour += 1
if hour >= 24:
hour = 0
hour_changed.emit(hour)
WeatherManager.gd
class_name WeatherManager
extends Node
signal weather_changed(weather: String)
var current_weather: String = "sunny"
func set_weather(weather: String) -> void:
current_weather = weather
weather_changed.emit(current_weather)
HUD.gd
class_name HUD
extends Control
@onready var health_label: Label = $HealthLabel
@onready var gold_label: Label = $GoldLabel
@onready var time_label: Label = $TimeLabel
@onready var weather_label: Label = $WeatherLabel
func update_health(value: int) -> void:
health_label.text = "生命:" + str(value)
func update_gold(value: int) -> void:
gold_label.text = "金币:" + str(value)
func update_hour(hour: int) -> void:
time_label.text = "时间:" + str(hour) + ":00"
func update_weather(weather: String) -> void:
weather_label.text = "天气:" + weather
Main.gd
extends Node2D
@onready var player: Player = $Player
@onready var chest: Chest = $World/Chest
@onready var time_manager: TimeManager = $Systems/TimeManager
@onready var weather_manager: WeatherManager = $Systems/WeatherManager
@onready var hud: HUD = $UI/HUD
var gold: int = 0
func _ready() -> void:
player.health_changed.connect(hud.update_health)
player.died.connect(_on_player_died)
chest.opened.connect(_on_chest_opened)
time_manager.hour_changed.connect(hud.update_hour)
weather_manager.weather_changed.connect(hud.update_weather)
func _on_chest_opened(reward_gold: int) -> void:
gold += reward_gold
hud.update_gold(gold)
func _on_player_died() -> void:
print("玩家死亡,显示 Game Over")
这个结构很适合你现在的 RPG 项目初期。
45. 这套结构的好处
Player 不直接操作 HUD
Chest 不直接操作金币 UI
TimeManager 不直接操作 Label
WeatherManager 不直接操作世界节点
Main 负责连接模块关系
HUD 只负责显示
这比所有节点互相 $../../UI/HUD/xxx 到处乱找要舒服很多。
前端类比就是:
子组件发 emit
父组件接住
父组件更新状态或传给其他组件
Godot 里 Main 就像一个组合层。
46. Signal 使用速查
声明信号:
signal health_changed(value: int)
发出信号:
health_changed.emit(health)
连接信号:
player.health_changed.connect(_on_player_health_changed)
断开信号:
player.health_changed.disconnect(_on_player_health_changed)
判断是否已连接:
player.health_changed.is_connected(_on_player_health_changed)
等待信号:
await animation_player.animation_finished
按钮点击:
button.pressed.connect(_on_button_pressed)
Area2D 进入:
area.body_entered.connect(_on_body_entered)
Timer 到点:
timer.timeout.connect(_on_timer_timeout)
47. 这一章你先记住这些
Signal 是事件通知机制。
节点发生事情时,可以 emit 信号。
其他节点可以 connect 信号并响应。
内置信号包括 pressed、body_entered、timeout、animation_finished。
自定义信号用 signal 声明。
connect 时不要给函数加括号。
Signal 适合跨对象通信,不适合同一个对象内部乱用。
谁拥有状态,谁发状态变化信号。
谁关心事件,谁连接信号。
Main 可以作为模块连接中心。
48. 最重要的一句话
Signal 的核心不是“调用别人”,而是“通知别人:我发生了某件事”。
再压缩一下:
按钮被点了 → pressed
玩家进区域了 → body_entered
血量变了 → health_changed
宝箱打开了 → opened
天气变了 → weather_changed
任务完成了 → quest_completed如果您觉得这篇文章有帮助,请点个赞吧~
评论
请登录后发表评论
去登录