C#-Signale

Eine ausführliche Erklärung von Signalen im Allgemeinen finden Sie im Abschnitt Signale nutzen in der Schritt-für-Schritt-Anleitung.

Signale werden mit C#-Events implementiert, dem idiomatischen Weg, um das Observer-Pattern in C# darzustellen. Dies ist der empfohlene Weg, um Signale in C# zu verwenden und der Schwerpunkt dieser Seite.

In einigen Fällen ist es notwendig, die älteren Connect() und Disconnect() APIs zu verwenden. Siehe Verbinden und Trennen verwenden für weitere Details.

Wenn Sie eine System.ObjectDisposedException bei der Behandlung eines Signals finden, könnten Sie eine Signaltrennung übersehen. Siehe Automatisches Trennen der Verbindung, wenn der Empfänger freigegeben wird für weitere Details.

Signale als C#-Events

Um mehr Typsicherheit zu bieten, sind Godot-Signale auch alle über Events verfügbar. Sie können diese Events, wie jedes andere Events, mit den Operatoren += und -= behandeln.

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

Außerdem können Sie immer auf Signalnamen zugreifen, die mit einem Node-Typ durch seine eingebettete SignalName-Klasse verbunden sind. Dies ist nützlich, wenn Sie zum Beispiel auf ein Signal warten wollen (siehe Schlüsselwort await).

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

Benutzerdefinierte Signale als C#-Ereignisse

Um ein benutzerdefiniertes Ereignis in Ihrem C#-Skript zu deklarieren, verwenden Sie das [Signal]-Attribut für einen öffentlichen Delegate-Typ. Beachten Sie, dass der Name dieses Delegate mit EventHandler enden muss.

[Signal]
public delegate void MySignalEventHandler();

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

Sobald dies geschehen ist, wird Godot die entsprechenden Events automatisch hinter den Kulissen erstellen. Sie können diese Events dann wie jedes andere Godot-Signal verwenden. Beachten Sie, dass die Events mit dem Namen Ihres Delegate ohne den abschließenden Teil EventHandler benannt werden.

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

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

Warnung

Wenn Sie eine Verbindung zu diesen Signalen im Editor herstellen wollen, müssen Sie das Projekt (neu) bauen, damit sie angezeigt werden.

Klicken Sie dazu auf den Build-Button in der oberen rechten Ecke des Editors.

Aussenden von Signalen

Um Signale auszusenden, verwenden Sie die Methode EmitSignal. Beachten Sie, dass, wie bei den von der Engine definierten Signalen, Ihre eigenen Signalnamen unter der eingebetteten Klasse SignalName aufgeführt werden.

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

Im Gegensatz zu anderen C#-Ereignissen können Sie Invoke nicht verwenden, um Events auszulösen, die an Godot-Signale gebunden sind.

Signale unterstützen Argumente eines beliebigen Variant-kompatiblen-Typs.

Folglich ist jeder Node oder jedes RefCounted automatisch kompatibel, aber eigene Datenobjekte müssen von GodotObject oder einer seiner Unterklassen erben.

using Godot;

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

Gebundene Werte

Manchmal möchten Sie Werte an ein Signal binden, wenn die Verbindung hergestellt wird, anstatt (oder zusätzlich) wenn das Signal ausgesendet wird. Zu diesem Zweck können Sie eine anonyme Funktion wie im folgenden Beispiel verwenden.

Hier nimmt das Button.Pressed-Signal kein Argument an. Aber wir wollen denselben ModifyValue für die "Plus"- und "Minus"-Buttons verwenden. Also binden wir den Modifikatorwert zu dem Zeitpunkt, zu dem wir die Signale verbinden.

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

Signalerzeugung zur Laufzeit

Schließlich können Sie eigene Signale direkt während des Spiels erstellen. Verwenden Sie dazu die Methode AddUserSignal. Seien Sie sich bewusst, dass diese Methode ausgeführt werden sollte, bevor Sie die Signale benutzen (entweder um sich mit ihnen zu verbinden oder um sie auszusenden). Beachten Sie auch, dass Signale, die auf diese Weise erstellt werden, nicht durch die eingebettete Klasse SignalName sichtbar sind.

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

Verbinden und Trennen verwenden

Im Allgemeinen ist es nicht empfehlenswert, Connect() und Disconnect() zu verwenden. Diese APIs bieten nicht so viel Typsicherheit wie Events. Sie sind jedoch notwendig für Verbinden mit Signalen, die von GDScript definiert wurden und für die Übergabe von ConnectFlags.

Im folgenden Beispiel wird beim ersten Drücken der Taste Greetings! ausgegeben. OneShot unterbricht das Signal, so dass ein erneutes Drücken des Buttons nichts bewirkt.

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

Automatisches Trennen der Verbindung, wenn der Empfänger freigegeben wird

Normalerweise trennt Godot, wenn ein GodotObjekt (wie ein Node) freigegeben wird, automatisch alle mit diesem Objekt verbundenen Verbindungen. Dies geschieht sowohl bei Signal-Emittern als auch bei Signal-Empfängern.

Ein Node mit diesem Code wird zum Beispiel "Hello!" ausgeben, wenn die Taste gedrückt wird, und sich dann selbst freigeben. Das Freigeben des Nodes unterbricht das Signal, sodass ein erneutes Drücken des Buttons nichts bewirkt:

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

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

Wenn ein Signal-Empfänger freigegeben wird, während der Signal-Emitter noch aktiv ist, wird die Verbindung in manchen Fällen nicht automatisch unterbrochen:

  • Das Signal ist mit einem Lambda-Ausdruck verbunden, der eine Variable erfasst.

  • Das Signal ist ein benutzerdefiniertes Signal.

In den folgenden Abschnitten werden diese Fälle näher erläutert und es werden Vorschläge für die manuelle Trennung der Verbindung gemacht.

Bemerkung

Die automatische Trennung ist absolut zuverlässig, wenn ein Signal-Emitter freigegeben wird, bevor einer seiner Empfänger freigegeben wird. Bei einem Projektstil, der dieses Pattern bevorzugt, sind die oben genannten Grenzen möglicherweise nicht von Belang.

Keine automatische Unterbrechung: ein Lambda-Ausdruck, der eine Variable erfasst

Wenn Sie eine Verbindung zu einem Lambda-Ausdruck herstellen, der Variablen erfasst, kann Godot nicht erkennen, dass der Lambda-Ausdruck mit der Instanz verbunden ist, die ihn erstellt hat. Dies führt in diesem Beispiel zu einem möglicherweise unerwarteten Verhalten:

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.

Bei Tick 4 versucht der Lambda-Ausdruck, auf die Property Name des Nodes zuzugreifen, aber der Node wurde bereits freigegeben. Dies verursacht die Exception.

Um die Verbindung zu trennen, behalten Sie einen Verweis auf das Delegate, das durch den Lambda-Ausdruck erzeugt wurde und übergeben diesen an -=. Zum Beispiel verbindet und trennt dieser Node die Verbindung mit Hilfe der _EnterTree und _ExitTree Lebenszyklus-Methoden:

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

In diesem Beispiel führt Free dazu, dass der Node den Baum verlässt, was _ExitTree aufruft. _ExitTree unterbricht das Signal, so dass _tick nie wieder aufgerufen wird.

Die zu verwendenden Lebenszyklus-Methoden hängen davon ab, was der Node tut. Eine andere Möglichkeit ist es, sich mit Signalen in _Ready zu verbinden und die Verbindung in Dispose zu trennen.

Bemerkung

Godot verwendet Delegate.Target, um zu bestimmen, mit welcher Instanz ein Delegate verbunden ist. Wenn ein Lambda-Ausdruck keine Variable erfasst, ist das Target des generierten Delegate die Instanz, die das Delegate erzeugt hat. Wenn eine Variable erfasst wird, zeigt das Target stattdessen auf einen generierten Typ, der die erfasste Variable speichert. Das ist es, was die Assoziation unterbricht. Wenn Sie sehen wollen, ob ein Delegate automatisch aufgeräumt wird, versuchen Sie sein Target zu überprüfen.

Callable.From wirkt sich nicht auf das Delegate.Target aus, so dass das Verbinden eines Lambdas, das Variablen mit Connect erfasst, nicht besser funktioniert als +=.

Keine automatische Trennung: ein benutzerdefiniertes Signal

Die Verbindung zu einem benutzerdefinierten Signal mit += wird nicht automatisch unterbrochen, wenn der empfangende Node freigegeben wird.

Um die Verbindung zu trennen, verwenden Sie -= zu einem geeigneten Zeitpunkt. Zum Beispiel:

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

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

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

Eine andere Lösung ist die Verwendung von Connect, das die Verbindung automatisch mit benutzerdefinierten Signalen trennt:

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

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