C# 信号

有关信号的详细解释,请参阅逐步教程中的 使用信号 部分。

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

In some cases it's necessary to use the older Connect() and Disconnect() APIs. See 使用 Connect 和 Disconnect for more details.

如果在处理信号时遇到 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}!");
}

警告

如果你想在编辑器中连接到这些信号,你需要(重新)构建项目以查看它们的出现。

You can click the Build button in the upper-right corner of the editor to do so.

信号发射

要发射信号,使用 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()。These APIs don't provide as much type safety as the events. However, they're necessary for connecting to signals defined by GDScript and passing 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));
}