入门Godot C#开发——信号篇
2024-07-16
关于
作为一个Godot初学者,了解Godot的一些基础知识但苦于不懂C#,故写此博文来记录自己的学习过程。如有错误,还请指正。
什么是信号?
信号 signal
是Godot提供的用于解耦节点与节点的方法。它是`观察者模式`的一种良好实现。
本文仅涉及使用C#与Godot的信号系统交互的过程。使用GDScript与信号交互的过程,请参考官方文档。
如何定义信号?
C#使用“委托” delegate
来处理信号连接。例如,
using Godot;
using System;
public partial class LearnSignal : Node
{
[Signal] public delegate void TestSignalEventHandler();
}
在我们的例子里,我们在类LearnSignal中,定义了一个名为TestSignal的信号。
可能有些难以理解。但,在那之前,不如让我们来了解了解什么是“委托”吧。
那什么是Delegate?
在C#中,委托(`delegate`)是一种类型,它安全地封装了一个方法的引用。它类似于函数指针,但更安全和灵活。委托可以指向一个或多个方法,并且可以在运行时动态地更改指向的方法。这使得委托成为实现回调和事件监听等设计模式的理想选择。
在Godot里,信号系统在C#中的实现,也是基于上述delegate机制。
如何定义Delegate?
委托通过delegate关键字定义,后面跟着返回类型、委托名称以及参数列表(如果有的话)。
例如,
using Godot;
using System;
// 学习 C# 中“委托”机制。此为单委托
public partial class LearnSignal : Node
{
// 定义一个委托,名为MyDelegate,接受参数string message,返回值void
public delegate void MyDelegate(string message);
// 打印消息。这是用于被委托调用的方法
public void ShowMessage(string msg)
{
GD.Print("ShowMessage: " + msg);
}
// 接下来,在_Ready()方法中,我们创建一个委托实例,并将ShowMessage方法作为参数传递给它。
public override void _Ready()
{
// 创建委托实例
MyDelegate myDelegate = new MyDelegate(ShowMessage);
// 调用委托实例
myDelegate("Hello World");
}
}
// 学习多委托机制
public partial class LearnSignal : Node
{
// 写几个函数来调用吧
void Func1(string msg)
{
GD.Print("调用了Func1" + msg);
}
void Func2(string msg)
{
GD.Print("调用了Func2" + msg);
}
void Func3(string msg)
{
GD.Print("调用了Func3" + msg);
}
void Func4(string msg)
{
GD.Print("调用了Func4" + msg);
}
// 我们可以在EnterTree里定义多个委托实例,然后制作“组合委托”
public override void _EnterTree()
{
MyDelegate delegate1 = Func1;
MyDelegate delegate2 = Func2;
MyDelegate delegate3 = Func3;
MyDelegate delegate4 = Func4; // 创建了四个委托实例
// 组合它们吧。我们定义的委托可以被简单的“+”操作符组合
// 使用 + - 操作符可以添加或者移除委托。
MyDelegate multidelegate = delegate1 + delegate2 + delegate3 + delegate4;
// 另一种定义方式是:
multidelegate = new MyDelegate(Func1);
multidelegate += Func2;
multidelegate += Func3;
multidelegate += Func4;
// 调用它吧,这将同时调用Func1,Func2,Func3,Func4
// 顺带一提,委托是引用传递的,所以你可以在任何地方添加或者移除委托
multidelegate("Delegate Hello World");
}
}
总而言之,你可以把委托当成一个盒子。你可以直接把满足条件的函数塞进去,在想要的时候调用委托就会执行这盒子里的所有函数。
你可以为一个委托添加多个函数——你甚至可以多次把一个函数连接到委托。
你所加入的函数会按“`先进入先调用`”的顺序被调用。
你可以使用`-`来断开函数和委托的连接。这会优先移除委托链链尾的函数,也就是后进入委托链的方法。
尝试移除委托中一个不存在的函数`不会有任何异常和错误`发生,你可以放心使用`-`操作符。
有了委托,我们就可以去了解Godot的信号机制了。
开始了解Godot的信号吧
Godot的C#信号基于Delegate,但多了一些限制:
你的委托必须以`"EventHandler"`结尾。
你的委托接受的参数必须与`Variant`兼容,或继承自`GodotObject`
举个例子:我们定义了简单的信号,但它好像并不能运行。
这是为什么呢?答案很明显,我们定义的委托/信号,没有以“`EventHandler`”结尾!让我们修改一下代码:
看,我们的引擎正确识别了我们的信号。
我们定义了MyDelegateEventHandler, 引擎正确的将我们的信号的名称识别为了“MyDelegate”,也识别了它接受的参数。
连接信号至函数
信号本质上就是Delegate。
不过,为了让Godot满意,我们最好用Godot推荐的办法来连接信号。
using Godot;
using System;
// 学习 C# 与 Godot 信号的对接
public partial class LearnSignal : Node
{
[Signal]
public delegate void MyDelegateEventHandler(string message);
// 定义3个用于被信号连接的函数
void Func1(string msg) {
GD.Print("Func 1 Message:" + msg);
}
void Func2(string msg) {
GD.Print("Func 2 Message:" + msg);
}
void Func3(string msg) {
GD.Print("Func 3 Message:" + msg);
}
public override void _Ready(){
MyDelegate += Func1; // 连接信号MyDelegate到Func1
MyDelegate += Func2; // 连接信号MyDelegate到Func2
MyDelegate += Func3; // 连接信号MyDelegate到Func3
EmitSignal(nameof(MyDelegate), "Hello World!"); // 发射信号
}
}
有注意到吗?我们没有显式声明“MyDelegate”这个变量,但我们能直接使用它!
这得益于我们先前使用的[Signal]修饰。在这种语境下,C#会自动为我们生成一个名为“MyDelegate”的Event变量。
这也是为什么要有“EventHandler”的限制的原因之一。
只有遵守规则,才能让戈多的“魔法”正常工作。
等待信号 (await)
如果要用await等待我们刚定义的信号,使用以下方法:
await ToSignal(this, SignalName.MyDelegate);
还请不要搞混 SignalName.MyDelegate 和 MyDelegate。ToSignal方法用于制作Awaiter供await方法使用。详情还请见文档。
发射信号
发射信号可以用EmitSignal方法来使用。
EmitSignal(nameof(MyDelegate), "Hello World!"); // 发射信号
EmitSignal("MyDelegate", "Hello World!"); // 这么写也行,但是不推荐
我们输入MyDelegate,这个刚刚生成的Event的名称,以及为信号提供的参数,使用EmitSignal就可以释放该信号了。很简单。
另外,Invoke
不能被用来触发与 Godot 信号绑定的事件,这与其他C#事件不同。(尽管我不知道这是什么意思...)
还有更多...
动态创建信号、异步Await、诸如此类,还有很多本文没提及的内容。我会在后续的文章里补充。
今天的学习到此为止吧。重点了解了C#中委托机制的用法——它作为观察者模式的一个实现,相当的灵活且可靠。后续我大概还会继续探索它的其他用法吧。
本文是我作为初学者的学习笔记。如有错误,还请指正。