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 相容的型別 作為參數。
因此,任何 Node 或 RefCounted 物件都會自動相容,但自訂資料物件必須繼承自 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));
}