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 兼容类型的参数。
因此,任何 Node 或 RefCounted 都会自动兼容,但自定义数据对象需要继承自 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));
}