csharp-signals について
Godot 4.xでC#からシグナルを扱うためのスキルです。型付き宣言、接続、後処理、待機方法と、よくある問題を解説します。
主な機能
- 01型付きシグナルの宣言と発行
- 02型安全なハンドラー接続
- 03安全な切断と後処理
- 04タイムアウト付きシグナル待機
- 05言語間のシグナル接続パターン
活用シーン
- 01プレイヤー状態の変化をHUDに通知
- 02ノード解放後の古いハンドラーを防止
- 03タイムアウト付きでシグナルを非同期に待機
- 04GDScriptのシグナルを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 スキルを参照してください。Godot の C# におけるシグナルは、GDScript とは異なる考え方が必要です。[Signal] で宣言するデリゲート、型安全な +=/-= による接続、そして _ExitTree() での必須の切断が特徴です。すべての例は、非推奨 API を使わず Godot 4.x を対象としています。
関連スキル: C# の規約とプロジェクトのセットアップについては csharp-godot、グローバルなシグナルハブのアーキテクチャについては 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[] などの Packed 配列相当が該当します。それ以外の 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}";
}
}
ラムダ接続
一度限りの短期間の応答にはラムダを使います。後で切断する必要がある場合は、ラムダをフィールドに保存してください。
// 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() 内で接続します。ノードが削除されて再び追加される場合は、_Ready() は一度しか実行されないため、代わりに _EnterTree() で接続してください。この時点ではノードの参照が解決され、シーンツリーを利用できます。コンストラクターやフィールド初期化子で接続すると、Godot のノードインフラストラクチャがまだ初期化されていないため、失敗することがあります。
4. シグナルの切断
GodotObject が解放されると、Godot は発行側と受信側のどちらについても、その接続を切断します。ただし、発行側が生存したまま受信側が解放された場合、クリーンアップされないケースが2 つあります。カスタム [Signal] に対する += と、変数をキャプチャするラムダです。いずれの場合も、次にシグナルが発行されると解放済みオブジェクトが呼び出され、ObjectDisposedException がスローされるため、_ExitTree() で自分で -= を使って切断してください。ツリーを出入りするノードでは、一度しか実行されない _Ready() ではなく、_EnterTree() で接続してください。
-=によるクリーンアップのパターン、Godot がクリーンアップしない 2 つのケース、SafeDisconnect ヘルパーについては、references/disconnecting.md を参照してください。
5. シグナルを待機する
await ToSignal(node, SignalName.X) は、シグナルが発火するまで一時停止します。シグナルの引数を Variant[] で返します。永久に停止しないよう、Task.WhenAny または CancellationTokenSource でタイムアウトを追加してください。
基本的な await、戻り値の抽出、2 つのタイムアウトパターンについては、references/awaiting-signals.md を参照してください。
6. カスタムシグナルのパターン
パターンは 3 つあります。型付きイベント引数(シグナルのペイロードを Resource のサブクラスでラップする)、静的イベントバス(両方のピアが C# の場合に、C# の static event を純粋な C# の代替手段として使用する)、ジェネリックシグナルヘルパー(繰り返しを減らすために、SignalName.X ごとに 1 つのヘルパーを用意する)です。
各パターンの完全なコードについては、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] またはキャプチャするラムダ式の接続を解除しない | 受信側が解放された後、次の発火時に ObjectDisposedException が発生します。メソッドハンドラーを使った組み込みシグナルは自動的にクリーンアップされます | この 2 つのケースでは、_ExitTree()(または Dispose())に対応する -= を追加します |
EmitSignal に誤った数の引数を渡す | += ハンドラーは何も表示されずに呼び出されません。Connect した受信側では「Method expected N argument(s), but called with M」と記録されます | シグナルデリゲートのパラメーター数を数え、正確に一致させます |
| 言語をまたぐシグナルに C# の静的イベントを使用する | GDScript からイベントを認識できません。エラーはなく、何も通知されません | GDScript が受信する必要のあるシグナルには Godot [Signal] を使用します |
| 同じハンドラーを二重に接続する | カスタム [Signal] では発火ごとにハンドラーが 2 回呼び出されます。組み込みシグナルでは、2 回目の += で「already connected」と記録されます | すでに接続済みか確認するか、ConnectFlags.OneShot を使用します。同じメソッドに対して += を 2 回呼び出さないでください |
ツリーの準備ができる前に _Ready() でシグナルを待機する | NullReferenceException が発生するか、発生元のノードがまだツリー内にないためシグナルが解決されません | 先に await ToSignal(GetTree(), SceneTree.SignalName.ProcessFrame) で遅延するか、_Ready() の後に呼び出すメソッドへ await を移します |
9. チェックリスト
- すべての
[Signal]デリゲート名がEventHandlerで終わっている -
EmitSignalでは、生の文字列ではなくSignalName.XxxName定数を使用している -
EmitSignalの引数の数と型が、デリゲートのシグネチャと完全に一致している - すべての
+=接続が_Ready()内にある(コンストラクターやフィールド初期化子内ではない) - カスタム
[Signal]へのすべての+=と、キャプチャするすべてのラムダ式に、対応する-=がある - シグナルの発行元がサブスクライバーより先に解放される可能性がある場合、
IsInstanceValidによるガードが設けられている - 後で切断する必要がある場合、ラムダ式をフィールドに格納している
- シグナルの
async待機が_Ready()を直接ブロックしていない。必要に応じて遅延実行している - 言語をまたぐシグナル(GDScript の発行元、C# のサブスクライバー)では、
Connect("snake_case_name", Callable.From<T>(Method))を使用している - 静的な C# イベントは、純粋な C# の通信経路でのみ使用している
- 複雑なシグナルのペイロードには、単純な C# クラスや構造体ではなく、
RefCounted派生のラッパークラスを使用している