文章目录
- 前言
 - 相关链接
 - 环境
 - 信号
 - 简单项目搭建
 - 默认的信号
 - 先在label里面预制接收函数
 - 添加信号
 
- 自定义无参数信号
 - 为了做区分,我们在label新增一个函数
 
- 自定义带参数信号
 - Button代码
 - label代码
 - 连接信号
 
- 自定义复杂参数信号
 - 自定义GodotObject类
 - Button
 - Label
 - 连接信号
 
- 父传子
 - Callable,信号回调
 - Button
 - Lable
 - 连接信号
 - 参数个数不对的异常问题
 - 解决异常方法
 
- 手动连接信号
 - 信号等待
 - Node注入,取代信号
 - 基于Action的信号模拟
 - Button
 
- 总结
 
前言
这里我们深入学习一下Godot的信号。对于数据流的控制一直是前端最重要的内容。
相关链接
Godot Engine 4.2 简体中文文档
环境
- visual studio 2022
 - .net core 8.0
 - godot.net 4.2.1
 - window 10
 
信号
信号就是传输数据的一种方式,信号是单向数据流,信号默认是从下往上传递数据的。即子传父
简单项目搭建

默认的信号
信号的发出和接收是需要配合的,有点像【发布订阅】模式。信号的发布是带有参数的。这里Button是发布者,Lable是订阅者。

我这里建议先在订阅者一方先新建函数,再链接信号。因为Godot在gdscript中是可以自动新建代码的,但是在.net 中需要我们手动新建代码。
先在label里面预制接收函数
using Godot;
using System;
public partial class Label : Godot.Label
{
	private int num = 0;
	// Called when the node enters the scene tree for the first time.
	public override void _Ready()
	{
		this.Text = "修改";
	}
	// Called every frame. 'delta' is the elapsed time since the previous frame.
	public override void _Process(double delta)
	{
	}
	/// <summary>
	/// 接受按钮点击
	/// </summary>
	public void RecevieButtonDown()
	{
		this.Text = $"{num}";
		num++;
	}
	
}
 
添加信号


 
 
自定义无参数信号
我们在Button的代码里面添加信号
using Godot;
using System;
public partial class Button : Godot.Button
{
    // Called when the node enters the scene tree for the first time.
    /// <summary>
    /// 添加自定义信号
    /// </summary>
    [Signal]
    public delegate void MyButtonClickEventHandler();
    public override void _Ready()
    {
        //在按钮按下时添加信号发送
        this.ButtonDown += () => EmitSignal(nameof(MyButtonClick));
    }
    // Called every frame. 'delta' is the elapsed time since the previous frame.
    public override void _Process(double delta)
    {
    }
}
 

为了做区分,我们在label新增一个函数
	/// <summary>
	/// 为了做区分,我们新增一个函数
	/// </summary>
	public void RecevieButtonDown2()
	{
		GD.Print("我是自定义无参信号");
        this.Text = $"{num}";
        num++;
    }
 

 
自定义带参数信号
这边比较复杂,需要了解C# 的delegate。
C#中委托(delegate)与事件(event)的快速理解
不理解的话那就先凑合着用好了。
Button代码
using Godot;
using System;
public partial class Button : Godot.Button
{
    // Called when the node enters the scene tree for the first time.
    /// <summary>
    /// 添加自定义信号
    /// </summary>
    [Signal]
    public delegate void MyButtonClickEventHandler();
    private int num = 0;
    /// <summary>
    /// 添加带参数型号
    /// </summary>
    [Signal]
    public delegate void AddNumberEventHandler(int number);
    public override void _Ready()
    {
        //我们给AddNumber添加功能,delegate只能添加或者删除函数,有点类似于触发器。
        //每次调用的时候,num自动++
        AddNumber += (item) => num++;
        //在按钮按下时添加信号发送
        this.ButtonDown += () =>
        {
            EmitSignal(nameof(MyButtonClick));
            //触发按钮信号
            EmitSignal(nameof(AddNumber),num);
        };
    }
    // Called every frame. 'delta' is the elapsed time since the previous frame.
    public override void _Process(double delta)
    {
    }
}
 
label代码
using Godot;
using System;
public partial class Label : Godot.Label
{
	private int num = 0;
	// Called when the node enters the scene tree for the first time.
	public override void _Ready()
	{
		this.Text = "修改";
	}
	// Called every frame. 'delta' is the elapsed time since the previous frame.
	public override void _Process(double delta)
	{
	}
	/// <summary>
	/// 接受按钮点击
	/// </summary>
	public void RecevieButtonDown()
	{
		this.Text = $"{num}";
		num++;
	}
	/// <summary>
	/// 为了做区分,我们新增一个函数
	/// </summary>
	public void RecevieButtonDown2()
	{
		GD.Print("我是自定义无参信号");
		this.Text = $"{num}";
		num++;
    }
	public void AddNumber(int number)
	{
		this.Text = $"{number}";
		GD.Print($"我是代参数信号,num:[{number}]");
	}
}
 
连接信号

 
 
自定义复杂参数信号

GD0202: The parameter of the delegate signature of the signal is not supported¶

想要了解更多差异,需要看这个文章。
Godot Engine 4.2 简体中文文档 编写脚本 C#/.NET

 
 
自定义GodotObject类
using Godot;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CSharpSimpleTest.models
{
    public partial class Student:GodotObject
    {
        public string Name = "小王";
        public int Age = 5;
        public Student() { }
    }
}
 
Button
using CSharpSimpleTest.models;
using Godot;
using System;
public partial class Button : Godot.Button
{
    // Called when the node enters the scene tree for the first time.
    /// <summary>
    /// 添加自定义信号
    /// </summary>
    [Signal]
    public delegate void MyButtonClickEventHandler();
    private int num = 0;
    /// <summary>
    /// 添加带参数型号
    /// </summary>
    [Signal]
    public delegate void AddNumberEventHandler(int number);
    private Student student = new Student() { Name = "小王",Age = 24};
    [Signal]
    public delegate void StudentEventHandler(Student student);  
    public override void _Ready()
    {
        //我们给AddNumber添加功能,delegate只能添加或者删除函数,有点类似于触发器。
        //每次调用的时候,num自动++
        AddNumber += (item) => num++;
        //在按钮按下时添加信号发送
        this.ButtonDown += () =>
        {
            EmitSignal(nameof(MyButtonClick));
            //触发按钮信号
            EmitSignal(nameof(AddNumber),num);
            //触发Student信号
            EmitSignal(nameof(Student),student);
        };
    }
    // Called every frame. 'delta' is the elapsed time since the previous frame.
    public override void _Process(double delta)
    {
    }
}
 
Label

using CSharpSimpleTest.models;
using Godot;
using System;
public partial class Label : Godot.Label
{
	private int num = 0;
	// Called when the node enters the scene tree for the first time.
	public override void _Ready()
	{
		this.Text = "修改";
	}
	// Called every frame. 'delta' is the elapsed time since the previous frame.
	public override void _Process(double delta)
	{
	}
	/// <summary>
	/// 接受按钮点击
	/// </summary>
	public void RecevieButtonDown()
	{
		this.Text = $"{num}";
		num++;
	}
	/// <summary>
	/// 为了做区分,我们新增一个函数
	/// </summary>
	public void RecevieButtonDown2()
	{
		GD.Print("我是自定义无参信号");
		this.Text = $"{num}";
		num++;
    }
	public void AddNumber(int number)
	{
		this.Text = $"{number}";
		GD.Print($"我是代参数信号,num:[{number}]");
	}
	/// <summary>
	/// 自定义复杂参数
	/// </summary>
	/// <param name="student"></param>
	public void ReviceStudent(Student student)
	{
		this.Text = $"student:Name[{student.Name}],Age[{student.Age}]";
	}
}
 
连接信号

至于对于的显示逻辑,是基于C# Variant这个类
C# Variant


父传子
Callable,信号回调
[教程]Godot4 GDscript Callable类型和匿名函数(lambda)的使用


Button
using Godot;
using System;
using System.Diagnostics;
public partial class test_node : Node2D
{
    // Called when the node enters the scene tree for the first time.
    private Label _lable;
    private Button _button;
    private int num = 0;
    [Signal]
    public delegate int NumAddEventHandler();
    public override void _Ready()
    {
        _lable = this.GetNode<Label>("Label");
        _button = this.GetNode<Button>("Button");
        _lable.Text = "修改";
        _button.ButtonDown += _button_ButtonDown;
        NumAdd += () => num;
    }
    public void _button_ButtonDown()
    {
        _lable.Text = $"按下修改{num}";
        GD.Print($"按下修改{num}");
        num++;
    }
    // Called every frame. 'delta' is the elapsed time since the previous frame.
    public override void _Process(double delta)
    {
        
    }
}
 
Lable
using CSharpSimpleTest.models;
using Godot;
using System;
public partial class Label : Godot.Label
{
    private int num = 0;
    // Called when the node enters the scene tree for the first time.
    public override void _Ready()
    {
        this.Text = "修改";
    }
    // Called every frame. 'delta' is the elapsed time since the previous frame.
    public override void _Process(double delta)
    {
    }
    /// <summary>
    /// 接受按钮点击
    /// </summary>
    public void RecevieButtonDown()
    {
        this.Text = $"{num}";
        num++;
    }
    /// <summary>
    /// 为了做区分,我们新增一个函数
    /// </summary>
    public void RecevieButtonDown2()
    {
        GD.Print("我是自定义无参信号");
        this.Text = $"{num}";
        num++;
    }
    public void AddNumber(int number)
    {
        this.Text = $"{number}";
        GD.Print($"我是代参数信号,num:[{number}]");
    }
    /// <summary>
    /// 自定义复杂参数
    /// </summary>
    /// <param name="student"></param>
    public void ReviceStudent(Student student)
    {
        this.Text = $"student:Name[{student.Name}],Age[{student.Age}]";
    }
    public void CallBackTest(Callable callable, Callable callable2)
    {
        callable.Call();
        callable2.Call(23);
    }
}
 
连接信号


 
参数个数不对的异常问题
public void CallBackTest(Callable callable, Callable callable2)
{
    try
    {
        callable.Call();
        //callable2.Call(23);
        //如果我们参数个数不对,也不会在C#中抛出异常,会在Godot中抛出异常
        callable2.Call();
    }
    catch (Exception e)
    {
        GD.Print("发送异常");
        GD.Print(e.ToString());
    }
    
}
 

 
 这是个十分危险的使用,因为我们无法溯源对应的代码,也无法try catch找到异常的代码,因为这个代码是在C++中间运行的。
解决异常方法


 
手动连接信号
由于Godot 对C# 的支持不是很够,所以我们点击Go to Method的时候,是不能直接跳转到对应的代码的。
 
    private Timer timer;
    private Godot.Button btn;
    public override void _Ready()
    {
        //先获取信号
        btn = GetNode<Button>("../Button");
        //再手动接受信号
        btn.Connect("Student",new Callable(this,nameof(ReviceStudent)));
    }
 
详细可以看官方文档的最佳实践
 
信号等待


using CSharpSimpleTest.models;
using Godot;
using System;
using System.Runtime.ExceptionServices;
using System.Threading.Tasks;
public partial class Label : Godot.Label
{
    private int num = 0;
    // Called when the node enters the scene tree for the first time.
    private Timer timer;
    public override void _Ready()
    {
        //获取Timer
        timer = GetNode<Timer>("Timer");
        //启动Timer
        timer.Start();
        this.Text = "修改";
        WaitTimeout();
    }
    // Called every frame. 'delta' is the elapsed time since the previous frame.
    public override void _Process(double delta)
    {
    }
    /// <summary>
    /// 接受按钮点击
    /// </summary>
    public void RecevieButtonDown()
    {
        this.Text = $"{num}";
        num++;
    }
    /// <summary>
    /// 为了做区分,我们新增一个函数
    /// </summary>
    public void RecevieButtonDown2()
    {
        GD.Print("我是自定义无参信号");
        this.Text = $"{num}";
        num++;
    }
    public void AddNumber(int number)
    {
        this.Text = $"{number}";
        GD.Print($"我是代参数信号,num:[{number}]");
    }
    /// <summary>
    /// 自定义复杂参数
    /// </summary>
    /// <param name="student"></param>
    public void ReviceStudent(Student student)
    {
        this.Text = $"student:Name[{student.Name}],Age[{student.Age}]";
    }
    public void CallBackTest(Callable callable, Callable callable2)
    {
        callable.Call();
        //throw new Exception("error");
        //callable2.Call(23);
        //如果我们参数个数不对,也不会在C#中抛出异常,会在Godot中抛出异常
        callable2.Call();
    }
    public async Task WaitTimeout()
    {
        while (true)
        {
            await ToSignal(timer, Timer.SignalName.Timeout);
            GD.Print($"收到Timer信号,num[{num}]");
            this.Text = $"{num}";
            num++;
        }
        
    }
}
 

Node注入,取代信号
信号最大的问题就是:
- 入参不固定
 - godot.net 对C# 支持力度不够
 - 编译报错在外部C++代码,Debug难度大
 - 不符合OOP的编程逻辑
 
比如我们在Button.cs中添加如下属性
    /// <summary>
    /// 新增的姓名
    /// </summary>
    public string MyName = "我是Button";
 

 我们就可以在Lable中拿到这个属性

 
基于Action的信号模拟
Button


 
 但是这样有个问题,Action需要初始化,不然会报空异常


 

 


 当然,也可以使用event Action,因为event Action是不允许在外部重写的。

 
 
 所以event Action 是最优的写法,是最不会出现问题的。

总结
信号就是Godot中数据沟通方式。信号的出现就是为了将复杂的数据处理简单化为接口的形式。再加上Godot中的Sence,这个就有利于我们面向对象的编程习惯。
但是信号是没有参数的声明的,而且参数出现问题会外部抛出异常,所以我们最好就使用event Action 回调来代替信号。



















