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# 事件实现的,这是在 C# 中表示观察者模式的惯用方式。这是在 C# 中使用信号的推荐方式,也是本页的重点。

在某些情况下,需要使用较旧的 Connect()Disconnect() API。有关更多详细信息,请参阅 使用 Connect 和 Disconnect

如果在处理信号时遇到 System.ObjectDisposedException,则可能是忘记信号断开连接。有关更多详细信息,请参阅 接收者释放时自动断开连接

信号作为 C# 事件

为了提供更多的类型安全,Godot 信号也都可以通过 事件 获取。你可以用 +=-= 运算符来处理这些事件,就像其他任何事件一样。

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

此外,你可以通过节点类型的嵌套 SignalName 类来访问与之相关的信号名称。这在你想要等待一个信号时很有用,例如(参见 await 关键字 )。

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

绑定值

有时你会想在连接建立时将值绑定到信号,而不是(或者除了)在信号发出时。要做到这一点,你可以使用一个匿名函数,如下面的例子所示。

在这里,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 无法提供像事件那样强的类型安全性。然而,它们对于 connecting to signals defined by GDScript 以及 ConnectFlags 传递是必需的。

在下面的示例中,第一次按下按钮会打印 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.

在 tick 4 时,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));
}