signal 关键字
2026/4/14大约 6 分钟
最后更新日期:2026-04-16
最后同步日期:2026-04-15 | Godot 官方原文 — signal 关键字
signal 关键字
定义
signal 关键字就像给一个电台设置频道——你定义一个"频道名"(信号名),以后任何人只要调到这个频道(连接这个信号),就能在频道播报时收到消息。
在 Godot 中,信号(Signal)是节点之间互相通信的核心机制。你可以把它想象成门铃系统:你先安装一个门铃(声明信号),当有人按铃时(触发信号),所有听到铃声的人都会做出反应(执行处理方法)。signal 关键字就是用来"安装门铃"的——告诉 Godot:"我这个节点可以发出这样的事件通知。"
一句话总结
signal 的作用是"声明一个自定义事件":让你的脚本能够对外广播消息,其他脚本可以监听并响应。
函数签名
C#
// 声明无参数信号
[Signal]
public delegate void MySignalEventHandler();
// 声明带参数的信号
[Signal]
public delegate void HealthChangedEventHandler(int newHealth, int maxHealth);
// 声明带多种类型参数的信号
[Signal]
public delegate void PlayerDetectedEventHandler(Node2D player, float distance);GDScript
# 声明无参数信号
signal my_signal
# 声明带参数的信号(带类型标注)
signal health_changed(new_health: int, max_health: int)
# 声明带多种类型参数的信号
signal player_detected(player: Node2D, distance: float)参数说明
signal 不是函数,而是一个声明关键字,下表解释声明语法中的各个部分:
| 部分 | 类型 | 必需 | 说明 |
|---|---|---|---|
| 信号名称 | 标识符 | 是 | 信号的名称。GDScript 中使用 snake_case(如 health_changed),C# 中使用 PascalCase 并以 EventHandler 结尾(如 HealthChangedEventHandler) |
| 参数列表 | 各种类型 | 否 | 信号可以携带参数,用于传递事件相关的数据。参数可以有类型标注,也可以省略 |
C# 信号声明规则
在 C# 中,信号声明必须遵循以下规则:
| 规则 | 说明 |
|---|---|
必须加 [Signal] 特性 | 告诉 Godot 这是一个信号声明 |
必须是 delegate | 信号必须声明为委托类型 |
名称必须以 EventHandler 结尾 | 这是 Godot C# 的约定,Godot 会自动去掉 EventHandler 后缀作为信号名 |
返回类型必须是 void | 信号不能返回值 |
GDScript 信号声明规则
| 规则 | 说明 |
|---|---|
使用 signal 关键字 | 在脚本的顶层声明,不能放在函数内部 |
| 名称使用 snake_case | 如 health_changed、item_picked_up |
| 参数可选 | 可以省略参数名和类型 |
返回值
signal 是声明语句,不是函数调用,没有返回值。它只是定义了一个信号,供后续通过 emit(触发)和 connect(连接)使用。
代码示例
基础用法
最简单的信号声明——定义一个无参数信号并触发它:
C#
using Godot;
public partial class Player : CharacterBody2D
{
// 声明一个自定义信号:玩家跳跃时触发
[Signal]
public delegate void PlayerJumpedEventHandler();
public override void _UnhandledInput(InputEvent @event)
{
if (@event.IsActionPressed("jump"))
{
// 触发信号
EmitSignal(SignalName.PlayerJumped);
GD.Print("玩家跳了!");
// 运行结果: 玩家跳了!
}
}
}GDScript
extends CharacterBody2D
# 声明一个自定义信号:玩家跳跃时触发
signal player_jumped
func _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed("jump"):
# 触发信号
player_jumped.emit()
print("玩家跳了!")
# 运行结果: 玩家跳了!实际场景
在游戏开发中,信号最常见的用途是让组件之间解耦通信。下面的例子展示了一个角色血量系统:当血量变化时发出信号,UI 自动更新,不需要角色直接引用 UI。
C#
using Godot;
public partial class Player : CharacterBody2D
{
// 声明带参数的信号:血量变化时触发,传递当前血量和最大血量
[Signal]
public delegate void HealthChangedEventHandler(int currentHealth, int maxHealth);
// 声明无参数信号:玩家死亡时触发
[Signal]
public delegate void PlayerDiedEventHandler();
[Export] public int ExMaxHealth = 100;
private int _currentHealth;
public override void _Ready()
{
_currentHealth = ExMaxHealth;
}
public void TakeDamage(int amount)
{
_currentHealth -= amount;
// 触发血量变化信号,传递当前血量和最大血量
EmitSignal(SignalName.HealthChanged, _currentHealth, ExMaxHealth);
GD.Print($"受到 {amount} 点伤害,剩余血量: {_currentHealth}/{ExMaxHealth}");
// 运行结果: 受到 20 点伤害,剩余血量: 80/100
if (_currentHealth <= 0)
{
_currentHealth = 0;
EmitSignal(SignalName.PlayerDied);
GD.Print("玩家已死亡!");
// 运行结果: 玩家已死亡!
}
}
}GDScript
extends CharacterBody2D
# 声明带参数的信号:血量变化时触发,传递当前血量和最大血量
signal health_changed(current_health: int, max_health: int)
# 声明无参数信号:玩家死亡时触发
signal player_died
@export var ex_max_health: int = 100
var _current_health: int
func _ready() -> void:
_current_health = ex_max_health
func take_damage(amount: int) -> void:
_current_health -= amount
# 触发血量变化信号,传递当前血量和最大血量
health_changed.emit(_current_health, ex_max_health)
print("受到 %d 点伤害,剩余血量: %d/%d" % [amount, _current_health, ex_max_health])
# 运行结果: 受到 20 点伤害,剩余血量: 80/100
if _current_health <= 0:
_current_health = 0
player_died.emit()
print("玩家已死亡!")
# 运行结果: 玩家已死亡!进阶用法
结合信号与多个监听者的模式——一个信号可以同时通知 UI、音效系统、成就系统等多个模块,实现完全解耦的架构:
C#
using Godot;
public partial class GameManager : Node
{
// 声明多个信号,用于不同的事件通知
[Signal]
public delegate void ScoreChangedEventHandler(int newScore);
[Signal]
public delegate void LevelCompletedEventHandler(int levelNumber, float timeElapsed);
[Signal]
public delegate void GamePausedEventHandler(bool isPaused);
private int _score = 0;
private int _currentLevel = 1;
public override void _Ready()
{
// UI 监听分数变化
ScoreChanged += OnScoreChanged;
// 测试:加分
AddScore(100);
AddScore(250);
// 测试:完成关卡
LevelComplete(45.5f);
GD.Print("--- 游戏管理器初始化完成 ---");
// 运行结果:
// 分数更新为: 100
// 分数更新为: 350
// 关卡 1 完成!用时: 45.5 秒
// --- 游戏管理器初始化完成 ---
}
public void AddScore(int points)
{
_score += points;
EmitSignal(SignalName.ScoreChanged, _score);
}
public void LevelComplete(float timeElapsed)
{
EmitSignal(SignalName.LevelCompleted, _currentLevel, timeElapsed);
_currentLevel++;
}
private void OnScoreChanged(int newScore)
{
// 这个方法只是演示——实际项目中,UI 组件会各自监听信号
GD.Print($"分数更新为: {newScore}");
// 运行结果: 分数更新为: 100
// 运行结果: 分数更新为: 350
}
}GDScript
extends Node
# 声明多个信号,用于不同的事件通知
signal score_changed(new_score: int)
signal level_completed(level_number: int, time_elapsed: float)
signal game_paused(is_paused: bool)
var _score: int = 0
var _current_level: int = 1
func _ready() -> void:
# UI 监听分数变化
score_changed.connect(_on_score_changed)
# 测试:加分
add_score(100)
add_score(250)
# 测试:完成关卡
level_complete(45.5)
print("--- 游戏管理器初始化完成 ---")
# 运行结果:
# 分数更新为: 100
# 分数更新为: 350
# 关卡 1 完成!用时: 45.5 秒
# --- 游戏管理器初始化完成 ---
func add_score(points: int) -> void:
_score += points
score_changed.emit(_score)
func level_complete(time_elapsed: float) -> void:
level_completed.emit(_current_level, time_elapsed)
_current_level += 1
func _on_score_changed(new_score: int) -> void:
# 这个方法只是演示——实际项目中,UI 组件会各自监听信号
print("分数更新为: %d" % new_score)
# 运行结果: 分数更新为: 100
# 运行结果: 分数更新为: 350注意事项
- C# 信号名自动转换:C# 中声明的信号名必须以
EventHandler结尾(如HealthChangedEventHandler),Godot 会自动将其转换为HealthChanged作为信号名。在EmitSignal中通过SignalName.HealthChanged引用。 - 信号声明位置:GDScript 中
signal必须写在脚本的顶层(类级别),不能写在函数内部。C# 中信号委托也必须声明为类的成员。 - 信号不会自动触发:声明信号只是定义了"这个节点可以发出什么事件"。你需要手动调用
EmitSignal()(C#)或signal_name.emit()(GDScript)来触发它。 - 信号参数数量不限:一个信号可以携带任意数量的参数,但建议保持在 3 个以内,超过的话考虑传递一个字典或自定义对象。
- 信号名避免与内置信号冲突:不要声明与 Godot 内置信号同名的自定义信号(如
ready、tree_entered),否则会导致不可预期行为。 - C# 差异:C# 中使用
[Signal]特性加delegate声明,GDScript 中使用signal关键字。C# 的信号触发用EmitSignal(SignalName.XXX, args),GDScript 的信号触发用xxx.emit(args)。
