Skip to content
Development
Skill

/csharp-signals

Use when implementing signals in C# — [Signal] delegates, EmitSignal patterns, async signal awaiting, and event-driven architecture

From plugin
godot-prompter
54157 skills9 agents1 hook
Install
$ npx -y skills add jame581/GodotPrompter --skill csharp-signals --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/csharp-signals

Context preview

The summary Claude sees to decide when to auto-load this skill.

Use when implementing signals in C# — [Signal] delegates, EmitSignal patterns, async signal awaiting, and event-driven architecture

SKILL.md

csharp-signals.SKILL.md
name: csharp-signals
description: Use when implementing signals in C# — [Signal] delegates, EmitSignal patterns, async signal awaiting, and event-driven architecture

Signals in C# (Godot 4.x)

This skill is **C# only**. For general C# conventions and project setup, see the **csharp-godot** skill. Godot signals in C# require a different mental model from GDScript: delegates declared with `[Signal]`, strongly-typed `+=`/`-=` connections, and mandatory disconnection in `_ExitTree()`. All examples target Godot 4.x with no deprecated APIs.

> **Related skills:** **csharp-godot** for C# conventions and project setup, **event-bus** for global signal hub architecture, **component-system** for signal-based component communication.

---

1. Signal Declaration

Signals are declared as `public delegate void` with the `[Signal]` attribute inside a `partial class` that extends a Godot type. The delegate name **must** end with `EventHandler` — Godot strips that suffix to produce the signal name exposed to the engine.

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);
}

**Naming rules:**

| Delegate name | Engine signal name | |---------------------------------|---------------------| | `HealthChangedEventHandler` | `HealthChanged` | | `DiedEventHandler` | `Died` | | `ItemCollectedEventHandler` | `ItemCollected` | | `PlayerSpawnedEventHandler` | `PlayerSpawned` |

Omitting the `EventHandler` suffix compiles without error but registers no Godot signal — the signal will not appear in the editor and `EmitSignal` will throw at runtime.

**Parameter type constraints:** Signal parameters must be Godot-marshallable types: `int`, `float`, `bool`, `string`, `Vector2`, `Vector3`, `Color`, `GodotObject` subclasses, `GodotDictionary`, `GodotArray`. Plain C# classes, structs, and generics are not allowed as parameters.

---

2. Emitting Signals

Use `EmitSignal(SignalName.SignalName, args...)`. The `SignalName` nested class is auto-generated by the Godot source generators at build time — one static string constant per declared signal.

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` validates argument count and types at runtime in debug builds. Passing the wrong number of arguments raises an error immediately, making bugs easy to locate.

---

3. Connecting Signals

The `+=` operator (preferred)

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 connections

Use lambdas for one-off, short-lived responses. Store the lambda in a field if you need to disconnect it later.

// 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;
}

Connecting in `_Ready()`

Always connect inside `_Ready()`. The node's references are resolved and the scene tree is available at that point. Connecting in the constructor or field initializers may fail because Godot node infrastructure is not yet initialised.

---

4. Disconnecting Signals

Unlike GDScript (which auto-cleans on `queue_free`), **C# must disconnect explicitly** in `_ExitTree()` — otherwise the listener delegate keeps the GodotObject alive past disposal, causing leaks. Use `signal -= handler` mirroring how you `+= handler` in `_Ready()`.

> See [references/disconnecting.md](references/disconnecting.md) for the `-=` cleanup pattern, why C# differs from GDScript, and a SafeDisconnect helper.

---

5. Awaiting Signals

`await ToSignal(node, SignalName.X)` pauses until the signal fires. Return

Read more
Ships withgodot-prompter

Agentic skills framework for Godot 4.x game development. Gives AI coding agents domain-specific expertise for GDScript and C# projects.

Get the whole plugin