关于 csharp-signals
专注于 Godot 4.x C# 信号用法的技能,介绍强类型信号声明、连接、清理、等待信号及常见错误。
主要功能
- 01强类型信号声明与发射
- 02强类型处理器连接
- 03安全断开与清理
- 04带超时等待信号
- 05跨语言信号连接模式
适用场景
- 01玩家状态变化时通知 HUD
- 02避免节点释放后残留处理器
- 03异步等待信号并设置超时
- 04将 GDScript 信号连接到 C# 监听器
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ jame581/ godotprompter --skill csharp-signals安装后在 Claude Code 中输入/csharp-signals确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/awaiting-signals.mdconnecting-gdscript-signals.mdcustom-signal-patterns.mddisconnecting.md详细介绍
C# 中的信号(Godot 4.x)
本技能仅适用于 C#。有关通用 C# 约定和项目设置,请参阅 csharp-godot 技能。C# 中的 Godot 信号需要采用不同于 GDScript 的思维模式:使用 [Signal] 声明委托、通过强类型 +=/-= 建立连接,并且必须在 _ExitTree() 中断开连接。所有示例均适用于 Godot 4.x,且不使用已弃用的 API。
相关技能: csharp-godot:C# 约定和项目设置;event-bus:全局信号中心架构;component-system:基于信号的组件通信。
1. 声明信号
信号在扩展 Godot 类型的 partial class 内,通过带有 [Signal] 特性的 public delegate void 声明。委托名称必须以 EventHandler 结尾——Godot 会去掉该后缀,生成引擎公开的信号名称。
using Godot;
public partial class Player : CharacterBody2D
{
// Signal name in engine: "HealthChanged"
[Signal] public delegate void HealthChangedEventHandler(int current, int maximum);
// Signal name in engine: "Died"
[Signal] public delegate void DiedEventHandler();
// Signal name in engine: "ItemCollected"
[Signal] public delegate void ItemCollectedEventHandler(string itemName);
}
命名规则:
| 委托名称 | 引擎信号名称 |
|---|---|
HealthChangedEventHandler | HealthChanged |
DiedEventHandler | Died |
ItemCollectedEventHandler | ItemCollected |
PlayerSpawnedEventHandler | PlayerSpawned |
省略 EventHandler 后缀会导致编译错误:源生成器会报告 GD0201:“委托名称必须以 ‘EventHandler’ 结尾”。
**参数类型限制:**信号参数必须与 Variant 兼容:数值类型、bool、string、枚举、Godot 结构体(Vector2、Color、…)、StringName、NodePath、GodotObject 子类、Godot.Collections.Array/Dictionary(包括泛型 Array<T>/Dictionary<TKey, TValue>)以及打包数组等效类型,例如 int[]。其他 C# 类和结构体会导致构建失败,并报告 GD0202。
2. 发出信号
使用 EmitSignal(SignalName.SignalName, args...)。嵌套类 SignalName 由 Godot 源生成器在构建时自动生成——每个已声明的信号对应一个缓存的 StringName。
using Godot;
public partial class Player : CharacterBody2D
{
[Signal] public delegate void HealthChangedEventHandler(int current, int maximum);
[Signal] public delegate void DiedEventHandler();
[Signal] public delegate void ItemCollectedEventHandler(string itemName);
[Export] public int MaxHealth { get; set; } = 100;
private int _currentHealth;
public override void _Ready()
{
_currentHealth = MaxHealth;
}
public void TakeDamage(int amount)
{
_currentHealth = Mathf.Clamp(_currentHealth - amount, 0, MaxHealth);
// Type-safe emission — SignalName.HealthChanged is a generated constant.
EmitSignal(SignalName.HealthChanged, _currentHealth, MaxHealth);
if (_currentHealth == 0)
EmitSignal(SignalName.Died);
}
public void CollectItem(string itemName)
{
EmitSignal(SignalName.ItemCollected, itemName);
}
}
EmitSignal 不会检查参数是否与委托匹配。参数数量错误时,自定义 [Signal] 的 += 处理程序会被跳过且不报错,而 Connect/GDScript 接收方会记录 Error calling from signal … Method expected N argument(s), but called with M。类型错误时,C# 处理程序会收到转换后的值或默认值(0、null);类型化的 GDScript 接收方会记录 Cannot convert argument N from X to Y,并且不会被调用。
3. 连接信号
+= 运算符(首选)
using Godot;
public partial class HudLayer : CanvasLayer
{
private Player _player;
public override void _Ready()
{
_player = GetNode<Player>("../Player");
// Connect with += — mirrors C# event syntax.
_player.HealthChanged += OnHealthChanged;
_player.Died += OnDied;
_player.ItemCollected += OnItemCollected;
}
private void OnHealthChanged(int current, int maximum)
{
GetNode<ProgressBar>("HealthBar").Value = (double)current / maximum * 100.0;
GetNode<Label>("HealthLabel").Text = $"{current} / {maximum}";
}
private void OnDied()
{
GetNode<Control>("DeathScreen").Show();
}
private void OnItemCollected(string itemName)
{
GetNode<Label>("PickupLabel").Text = $"Picked up: {itemName}";
}
}
Lambda 连接
对于一次性、短期的响应,请使用 lambda。如果之后需要断开连接,请将 lambda 存储在字段中。
// Anonymous lambda — cannot be disconnected by reference later.
_player.Died += () => GetNode<AudioStreamPlayer>("DeathSound").Play();
// Stored lambda — can be disconnected.
private Action<string> _onItemCollected;
public override void _Ready()
{
_onItemCollected = (itemName) =>
{
_collectCount++;
UpdateCollectDisplay();
};
_player.ItemCollected += _onItemCollected;
}
public override void _ExitTree()
{
_player.ItemCollected -= _onItemCollected;
}
在 _Ready() 中连接
在 _Ready() 中连接——如果节点会被移除并重新添加,则应在 _EnterTree() 中连接,因为 _Ready() 只会运行一次。此时节点引用已解析,场景树也已可用。在构造函数或字段初始化器中连接可能会失败,因为 Godot 节点基础设施尚未初始化。
4. 断开信号
释放 GodotObject 时,Godot 会断开它的信号连接,无论它是发出方还是接收方。如果接收方被释放,但发出方仍然存在,则有两种情况不会被清理:自定义 [Signal] 上的 +=,以及捕获变量的 lambda。这两种情况下,下一次发出信号都会访问已释放的对象并抛出 ObjectDisposedException,因此请在 _ExitTree() 中自行使用 -= 断开连接。对于可以离开并重新进入场景树的节点,请在 _EnterTree() 中连接,而不要在只运行一次的 _Ready() 中连接。
有关
-=清理模式、Godot 不会清理的两种情况以及 SafeDisconnect 助手,请参阅 references/disconnecting.md。
5. 等待信号
await ToSignal(node, SignalName.X) 会暂停执行,直到信号触发。返回信号参数组成的 Variant[]。使用 Task.WhenAny 或 CancellationTokenSource 添加超时,以避免永久挂起。
有关基本等待、返回值提取和两种超时模式,请参阅 references/awaiting-signals.md。
6. 自定义信号模式
有三种模式:类型化事件参数(将信号负载封装在 Resource 子类中)、静态事件总线(当两端都是 C# 时,将 C# static event 用作纯 C# 替代方案)、泛型信号辅助工具(为每个 SignalName.X 编写一个辅助工具,以减少重复代码)。
有关每种模式的完整代码,请参阅 references/custom-signal-patterns.md。
7. 从 C# 连接 GDScript 信号
当信号在 GDScript 节点中声明,而监听器使用 C# 时,请使用 node.Connect("signal_name", new Callable(this, MethodName.Handler))。生成的 MethodName 符号仍适用于 C# 方法。
有关同时使用字符串名称和
MethodName变体的完整模式,请参阅 references/connecting-gdscript-signals.md。
8. 常见错误
| 错误 | 症状 | 修复方法 |
|---|---|---|
委托名称忘记添加 EventHandler 后缀 | 构建失败,并显示 GD0201:委托名称必须以 'EventHandler' 结尾 | 重命名为 XxxEventHandler |
EmitSignal 中的参数类型错误 | C# 处理程序:不会报错,但值会被转换或设为默认值(0、null);类型化 GDScript 接收器:显示 Cannot convert argument N from X to Y,且不会调用处理程序 | 使 EmitSignal 参数与委托签名完全匹配 |
| 连接到已释放的对象 | ObjectDisposedException,或下次发出信号时静默崩溃 | 连接前使用 IsInstanceValid 进行检查;安全地保存引用 |
未断开自定义 [Signal] 或捕获 lambda | 接收器释放后,下次发出信号时出现 ObjectDisposedException;内置信号的处理程序方法会自动清理 | 对这两种情况,在 _ExitTree()(或 Dispose())中添加匹配的 -= |
向 EmitSignal 传入错误数量的参数 | += 处理程序不会被调用,且没有提示;通过 Connect 连接的接收器会记录 "Method expected N argument(s), but called with M" | 统计信号委托参数数量,并确保完全匹配 |
| 使用静态 C# 事件处理跨语言信号 | GDScript 无法观察到该事件;不会报错,只是信号静默丢失 | 对于 GDScript 必须接收的信号,请使用 Godot [Signal] |
| 多次连接同一处理程序 | 对于自定义 [Signal],每次发出信号时处理程序会运行两次;对于内置信号,第二次 += 会记录 "already connected" | 检查是否已连接,或使用 ConnectFlags.OneShot;切勿对同一个方法调用两次 += |
在场景树准备就绪前于 _Ready() 中等待信号 | 出现 NullReferenceException,或因源节点尚未加入场景树而导致信号永不触发 | 先使用 await ToSignal(GetTree(), SceneTree.SignalName.ProcessFrame) 延迟执行,或将等待移至 _Ready() 之后调用的方法中 |
9. 检查清单
- 每个
[Signal]委托名称都以EventHandler结尾 -
EmitSignal使用SignalName.XxxName常量(而非原始字符串) -
EmitSignal的参数数量和类型与委托签名完全一致 - 所有
+=连接都在_Ready()中(不在构造函数或字段初始化器中) - 每个自定义
[Signal]上的+=,以及每个捕获变量的 lambda,都有对应的-= - 在信号源可能先于订阅者被释放的情况下,使用
IsInstanceValid进行保护 - 如果之后需要断开连接,则将 lambda 存储在字段中
-
async信号等待不会直接阻塞_Ready();必要时延迟执行 - 跨语言信号(GDScript 发出,C# 订阅)使用
Connect("snake_case_name", Callable.From<T>(Method)) - 静态 C# 事件仅用于纯 C# 通信路径
- 复杂信号负载使用派生自
RefCounted的包装类,而非普通 C# 类或结构体