mcpField
C
csharp-signals

csharp-signals

github.com/jame581开发889161 次安装4 天前更新
代码仓库

关于 csharp-signals

专注于 Godot 4.x C# 信号用法的技能,介绍强类型信号声明、连接、清理、等待信号及常见错误。

主要功能

  1. 01强类型信号声明与发射
  2. 02强类型处理器连接
  3. 03安全断开与清理
  4. 04带超时等待信号
  5. 05跨语言信号连接模式

适用场景

  1. 01玩家状态变化时通知 HUD
  2. 02避免节点释放后残留处理器
  3. 03异步等待信号并设置超时
  4. 04将 GDScript 信号连接到 C# 监听器

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/jame581/godotprompter --skill csharp-signals

安装后在 Claude Code 中输入/csharp-signals确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
awaiting-signals.md
connecting-gdscript-signals.md
custom-signal-patterns.md
disconnecting.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);
}

命名规则:

委托名称引擎信号名称
HealthChangedEventHandlerHealthChanged
DiedEventHandlerDied
ItemCollectedEventHandlerItemCollected
PlayerSpawnedEventHandlerPlayerSpawned

省略 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# 类或结构体

评论

支持 Markdown