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.
Checking the stable version of the documentation...
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));
}