Godot使用CSharp中信号机制

前言

在 Godot 中,信号系统(Signals) 是实现节点间通信的核心机制,它本质上是 Godot 引擎内置的、高度优化的“事件”系统。

与 C# 原生的 delegate/event 不同,Godot 信号跨语言(GDScript/C#/VisualScript)、支持编辑器可视化连接,并能自动处理生命周期(如节点释放时自动断开连接)。

因此在 Godot 项目中应优先使用信号而非手动实现 C# event

Godot 信号的三种使用方式

  1. 连接引擎内置信号(如 Area2D.area_entered
  2. 自定义信号并发射
  3. 通过编辑器连接信号

示例场景:玩家进入区域 → 触发宝箱开启

我们将创建:

  • 一个 PlayerCharacterBody2D
  • 一个 TreasureChestArea2D + 自定义信号)

下面以 C# 脚本 为例,完整演示如何在 Godot 4.x 中使用信号系统。

为宝箱定义自定义信号

为宝箱定义自定义信号(C#)

信号的定义必须以EventHandler结束,会自动生成前面名称的信号,比如下面示例的SignalName.ChestOpened

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
using Godot;

public partial class TreasureChest : Area2D
{
// 1️⃣ 声明一个自定义信号(使用 [Signal] 特性)
[Signal]
public delegate void ChestOpenedEventHandler();

public override void _Ready()
{
// 2️⃣ 连接自己的 area_entered 信号
AreaEntered += OnAreaEntered;
}

private void OnAreaEntered(Area2D area)
{
// 检查是否是玩家(假设玩家在 "player" 分组)
if (area.IsInGroup("player"))
{
GD.Print("玩家靠近宝箱!");

// 3️⃣ 发射自定义信号
EmitSignal(SignalName.ChestOpened);

// 可选:关闭碰撞检测,防止重复触发
Monitoring = false;
}
}
}

关键点:

  • 使用 [Signal] 特性声明信号。
  • 信号名会自动生成 SignalName.ChestOpened(字符串安全)。
  • EmitSignal() 发射信号。

玩家脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
// Player.cs
using Godot;

public partial class Player : CharacterBody2D
{
public override void _Ready()
{
// 将玩家加入 "player" 分组,方便其他节点识别
AddToGroup("player");
}

// ...移动逻辑略...
}

连接信号两种方式

在 C# 中动态连接

推荐用于运行时逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// GameManager.cs 或 TreasureChest 的父节点
public partial class LevelManager : Node2D
{
private TreasureChest chest;
private UIController ui;

public override void _Ready()
{
chest = GetNode<TreasureChest>("TreasureChest");
ui = GetNode<UIController>("UI");

// 连接宝箱的 ChestOpened 信号到 UI 的方法
chest.Connect(TreasureChest.SignalName.ChestOpened, Callable.From(ui.OnChestOpened));
}
}
// UIController.cs
public partial class UIController : Control
{
public void OnChestOpened()
{
GD.Print("UI:宝箱已开启!播放动画/加金币...");
// 更新界面、播放音效等
}
}

✅ 优点:灵活,适合动态生成的对象。

在 Godot 编辑器中连接

适合静态场景

  1. 选中 TreasureChest 节点;
  2. 在右侧 “Node” 面板“Signals” 标签;
  3. 找到 ChestOpened 信号,双击;
  4. 选择目标节点(如 UIController);
  5. 输入方法名(如 OnChestOpened),点击“连接”。

Godot 会自动生成对应方法(如果不存在)。

断开信号(避免内存泄漏)

当节点被释放时,Godot 会自动断开其所有信号连接,所以通常无需手动断开。

但如果你在代码中跨场景或长期持有引用,建议显式断开:

1
2
// 在节点销毁前(如 _ExitTree)
chest.Disconnect(TreasureChest.SignalName.ChestOpened,Callable.From(ui.OnChestOpened));

重要提示

事项 说明
信号 vs C# event 优先用 Godot 信号!它与引擎深度集成,支持编辑器、序列化、跨语言。
参数传递 信号可带参数(最多 9 个),在 [Signal] 委托中定义即可。
性能 信号调用比直接方法调用稍慢,但对游戏逻辑完全够用。
类型安全 C# 中使用 SignalName.XXXCallable.From() 可获得编译时检查。

带参数的信号示例

1
2
3
4
5
6
7
8
9
10
11
[Signal]
public delegate void ItemCollectedEventHandler(string itemName, int count);

// 发射
EmitSignal(SignalName.ItemCollected, "金币", 10);

// 接收
public void OnItemCollected(string name, int count)
{
GD.Print($"获得 {count}{name}");
}

绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
private TouchArea touchArea;

// Called when the node enters the scene tree for the first time.
public override void _Ready()
{
touchArea = GetNode<TouchArea>("Area2D");
touchArea.Connect(TouchArea.SignalName.ItemCollected, new Callable(this, nameof(OnItemCollected)));
}

public void OnItemCollected(string name, int count)
{
GD.Print($"获得 {count}{name}");
}

总结

Godot 信号使用流程

  1. [Signal] 声明信号;
  2. EmitSignal() 发射;
  3. Connect() 或编辑器连接;
  4. 在目标方法中响应。

这是 Godot 官方推荐的节点通信方式,简洁、安全、高效。