Attention: Here be dragons

This is the latest (unstable) version of this documentation, which may document features not available in or compatible with released stable versions of Godot.

C# 訊號

關於訊號的詳細說明,請參考按部就班教學中的 繫結訊號 章節。

訊號在 C# 中是以事件(events)來實作的,這是 C# 中實現 觀察者模式 的慣用方式。這也是本頁面推薦的使用方式與重點。

在某些情況下,需要使用較舊的 Connect()Disconnect() API。詳情請參閱 使用 Connect 與 Disconnect

如果你在處理訊號時遇到 System.ObjectDisposedException,可能是漏掉了訊號的斷線。請參閱 當接收端被釋放時自動斷線 以取得更多資訊。

訊號作為 C# 事件

為了提供更強的型別安全性,Godot 的所有訊號也都能以 事件 的形式取得。你可以如同處理其他事件一樣,使用 +=-= 運算子來連接或斷開事件。

Timer myTimer = GetNode<Timer>("Timer");
myTimer.Timeout += () => GD.Print("Timeout!");

此外,你也可以透過各節點型別所屬的巢狀 SignalName 類別來存取訊號名稱。例如,當你想要等待某個訊號時,這非常有用(請參閱 Onready 關鍵字)。

await ToSignal(GetTree(), SceneTree.SignalName.ProcessFrame);

自訂訊號作為 C# 事件

要在 C# 腳本中宣告自訂事件,請在公開的委派型別上加上 [Signal] 屬性。請注意,委派的名稱必須以 EventHandler 結尾。

[Signal]
public delegate void MySignalEventHandler();

[Signal]
public delegate void MySignalWithArgumentEventHandler(string myString);

完成上述步驟後,Godot 會在背後自動建立相應的事件。之後你就可以如同其他 Godot 訊號一樣使用這些事件。請注意,事件名稱會以你的委派名稱去除最後的 EventHandler 部分命名。

public override void _Ready()
{
    MySignal += () => GD.Print("Hello!");
    MySignalWithArgument += SayHelloTo;
}

private void SayHelloTo(string name)
{
    GD.Print($"Hello {name}!");
}

警告

如果你想在編輯器中連接這些訊號,需要(重新)建置專案,訊號才會顯示出來。

你可以點擊編輯器右上角的 建置 按鈕來進行。

訊號發送

要發送訊號,請使用 EmitSignal 方法。請注意,與引擎預設訊號一樣,你的自訂訊號名稱也會列在巢狀的 SignalName 類別下。

public void MyMethodEmittingSignals()
{
    EmitSignal(SignalName.MySignal);
    EmitSignal(SignalName.MySignalWithArgument, "World");
}

與其他 C# 事件不同,你不能使用 Invoke 來觸發與 Godot 訊號綁定的事件。

訊號支援任何 與 Variant 相容的型別 作為參數。

因此,任何 NodeRefCounted 物件都會自動相容,但自訂資料物件必須繼承自 GodotObject 或其子類別。

using Godot;

public partial class DataObject : GodotObject
{
    public string MyFirstString { get; set; }
    public string MySecondString { get; set; }
}

繫結值

有時你會希望在建立連線時就將值綁定到訊號(而不是在訊號發送時,或是兩者皆有)。這時可以使用匿名函式(Lambda),如下例所示。

這裡,Button.Pressed 訊號沒有參數。但我們希望讓「加號」與「減號」按鈕都使用同一個 ModifyValue 方法,因此在連接訊號時就綁定修飾值。

public int Value { get; private set; } = 1;

public override void _Ready()
{
    Button plusButton = GetNode<Button>("PlusButton");
    plusButton.Pressed += () => ModifyValue(1);

    Button minusButton = GetNode<Button>("MinusButton");
    minusButton.Pressed += () => ModifyValue(-1);
}

private void ModifyValue(int modifier)
{
    Value += modifier;
}

執行時建立訊號

最後,你也可以在遊戲運作時直接建立自訂訊號,請使用 AddUserSignal 方法。請注意,必須在使用該訊號(無論是連接或發送)前先呼叫此方法。另外,這種方式建立的訊號不會顯示在 SignalName 巢狀類別中。

public override void _Ready()
{
    AddUserSignal("MyCustomSignal");
    EmitSignal("MyCustomSignal");
}

使用 Connect 與 Disconnect

一般來說,不建議使用 Connect()Disconnect() 這些 API,因為它們不像事件那樣提供型別安全。但在 跨語言連接 GDScript 訊號 或需傳遞 ConnectFlags 時,這些 API 就會很有用。

在下例中,第一次按下按鈕會顯示 Greetings!。由於使用了 OneShot,訊號會在觸發後自動斷開,因此再次按下按鈕不會有任何反應。

public override void _Ready()
{
    Button button = GetNode<Button>("GreetButton");
    button.Connect(Button.SignalName.Pressed, Callable.From(OnButtonPressed), (uint)GodotObject.ConnectFlags.OneShot);
}

public void OnButtonPressed()
{
    GD.Print("Greetings!");
}

當接收端被釋放時自動斷線

通常當任何 GodotObject``(例如任何 ``Node)被釋放時,Godot 會自動斷開與該物件相關的所有訊號連線,無論是作為發送端還是接收端。

例如,一個節點如果寫了下列程式碼,按下按鈕時會印出「Hello!」,然後自我釋放。釋放該節點時訊號會自動斷開,因此再次按下按鈕不會有任何效果:

public override void _Ready()
{
    Button myButton = GetNode<Button>("../MyButton");
    myButton.Pressed += SayHello;
}

private void SayHello()
{
    GD.Print("Hello!");
    Free();
}

當訊號接收端被釋放而發送端仍存在時,有些情況下自動斷線不會發生:

  • 該訊號連接到一個捕捉變數的 Lambda 匿名函式。

  • 該訊號為自訂訊號。

以下章節將更詳細解釋這些情況,並提供如何手動斷線的建議。

備註

如果訊號發送端在所有接收端被釋放前就被釋放,自動斷線是完全可靠的。若你的專案風格偏好這種模式,上述限制可能就不會造成困擾。

無法自動斷線:捕捉變數的 Lambda 匿名函式

如果你連接到一個捕捉變數的 Lambda 匿名函式,Godot 無法辨識該 Lambda 屬於哪個實例,可能導致下例這樣的非預期行為:

Timer myTimer = GetNode<Timer>("../Timer");
int x = 0;
myTimer.Timeout += () =>
{
    x++; // This lambda expression captures x.
    GD.Print($"Tick {x} my name is {Name}");
    if (x == 3)
    {
        GD.Print("Time's up!");
        Free();
    }
};
Tick 1, my name is ExampleNode
Tick 2, my name is ExampleNode
Tick 3, my name is ExampleNode
Time's up!
[...] System.ObjectDisposedException: Cannot access a disposed object.

在第 4 次 tick 時,Lambda 嘗試存取該節點的 Name 屬性,但該節點已經被釋放,這會導致例外。

為了正確斷線,請保留 Lambda 產生的委派參考,並在適當時機用 -= 斷開。如下例所示,該節點於 _EnterTree 連線,於 _ExitTree 斷線:

[Export]
public Timer MyTimer { get; set; }

private Action _tick;

public override void _EnterTree()
{
    int x = 0;
    _tick = () =>
    {
        x++;
        GD.Print($"Tick {x} my name is {Name}");
        if (x == 3)
        {
            GD.Print("Time's up!");
            Free();
        }
    };
    MyTimer.Timeout += _tick;
}

public override void _ExitTree()
{
    MyTimer.Timeout -= _tick;
}

在此例中,呼叫 Free 會讓節點離開場景樹,進而觸發 _ExitTree。在 _ExitTree 會將訊號斷線,因此 _tick 不會再被呼叫。

實際應使用哪個生命週期方法,取決於節點的用途。另一種作法是在 _Ready 連線,在 Dispose 斷線。

備註

Godot 會使用 Delegate.Target 判斷委派關聯的實例。當 Lambda 沒有捕捉變數時,委派的 Target 會指向建立它的實例;但如果有捕捉變數,Target 會指向存放該變數的產生型別,這就會導致關聯失效。想要確認委派會否自動清理,可檢查其 Target

Callable.From 並不會影響 Delegate.Target。因此,使用 Connect 連接捕捉變數的 Lambda,效果與 += 相同,不會有更好的自動清理。

無法自動斷線:自訂訊號

如果用 += 連接自訂訊號,當接收端節點被釋放時並不會自動斷線。

要斷開連線,請在適當時機使用 -=。例如:

[Export]
public MyClass Target { get; set; }

public override void _EnterTree()
{
    Target.MySignal += OnMySignal;
}

public override void _ExitTree()
{
    Target.MySignal -= OnMySignal;
}

另一個解決方式是使用 Connect,這樣自訂訊號也能自動斷線:

[Export]
public MyClass Target { get; set; }

public override void _EnterTree()
{
    Target.Connect(MyClass.SignalName.MySignal, Callable.From(OnMySignal));
}