GFramework/docs/zh-CN/source-generators/bind-node-signal-generator.md
GeWuYou 38020c32a2 docs(source-generators): 添加源代码生成器完整文档
- 新增 GFramework.SourceGenerators 主文档,介绍编译时代码生成工具
- 详细说明 Log 属性生成器的使用方法和配置选项
- 完整描述 ContextAware 属性生成器的功能和测试场景配置
- 添加 GenerateEnumExtensions 属性生成器文档和使用示例
- 介绍 GetNode 生成器(Godot 专用)的节点获取功能
- 新增 BindNodeSignal 生成器文档,说明信号绑定与解绑机制
- 提供 Context Get 注入生成器的完整使用指南
- 添加诊断信息章节,涵盖所有生成器的错误提示和解决方案
- 包含性能优势对比和基准测试结果
- 提供多个完整使用示例,展示实际应用场景
- 整理最佳实践和常见问题解答
- 添加 BindNodeSignal 生成器专用文档,详细介绍其高级用法
2026-03-31 15:10:06 +08:00

681 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# BindNodeSignal 生成器
> 自动生成 Godot 节点信号绑定与解绑逻辑,消除事件订阅样板代码
## 概述
BindNodeSignal 生成器为标记了 `[BindNodeSignal]` 特性的方法自动生成节点事件绑定和解绑代码。它将 `_Ready()`
`_ExitTree()` 中重复的 `+=``-=` 样板代码收敛到生成器中统一维护。
### 核心功能
- **自动事件绑定**:在 `_Ready()` 中自动订阅节点事件
- **自动事件解绑**:在 `_ExitTree()` 中自动取消订阅
- **多事件绑定**:一个方法可以绑定到多个节点事件
- **类型安全检查**:编译时验证方法签名与事件委托的兼容性
- **与 GetNode 集成**:无缝配合 `[GetNode]` 特性使用
## 基础使用
### 标记事件处理方法
使用 `[BindNodeSignal]` 特性标记处理节点事件的方法:
```csharp
using GFramework.Godot.SourceGenerators.Abstractions;
using Godot;
public partial class MainMenu : Control
{
private Button _startButton = null!;
private Button _settingsButton = null!;
private Button _quitButton = null!;
[BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))]
private void OnStartButtonPressed()
{
StartGame();
}
[BindNodeSignal(nameof(_settingsButton), nameof(Button.Pressed))]
private void OnSettingsButtonPressed()
{
ShowSettings();
}
[BindNodeSignal(nameof(_quitButton), nameof(Button.Pressed))]
private void OnQuitButtonPressed()
{
QuitGame();
}
public override void _Ready()
{
__BindNodeSignals_Generated();
}
public override void _ExitTree()
{
__UnbindNodeSignals_Generated();
}
}
```
### 生成的代码
编译器会为标记的类自动生成以下代码:
```csharp
// <auto-generated />
#nullable enable
namespace YourNamespace;
partial class MainMenu
{
private void __BindNodeSignals_Generated()
{
_startButton.Pressed += OnStartButtonPressed;
_settingsButton.Pressed += OnSettingsButtonPressed;
_quitButton.Pressed += OnQuitButtonPressed;
}
private void __UnbindNodeSignals_Generated()
{
_startButton.Pressed -= OnStartButtonPressed;
_settingsButton.Pressed -= OnSettingsButtonPressed;
_quitButton.Pressed -= OnQuitButtonPressed;
}
}
```
## 参数说明
`[BindNodeSignal]` 特性需要两个参数:
| 参数 | 类型 | 说明 |
|-----------------|--------|-----------------------------|
| `nodeFieldName` | string | 目标节点字段名(使用 `nameof` 推荐) |
| `signalName` | string | 目标节点上的 CLR 事件名(使用 `nameof` |
```csharp
[BindNodeSignal("_startButton", "Pressed")] // 字符串字面量
[BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))] // 推荐nameof 表达式
```
## 高级用法
### 带参数的事件处理
处理带参数的事件(如 `SpinBox.ValueChanged`
```csharp
using Godot;
public partial class SettingsPanel : Control
{
private SpinBox _volumeSpinBox = null!;
private SpinBox _brightnessSpinBox = null!;
// 参数类型必须与事件委托匹配
[BindNodeSignal(nameof(_volumeSpinBox), nameof(SpinBox.ValueChanged))]
private void OnVolumeChanged(double value)
{
SetVolume((float)value);
}
[BindNodeSignal(nameof(_brightnessSpinBox), nameof(SpinBox.ValueChanged))]
private void OnBrightnessChanged(double value)
{
SetBrightness((float)value);
}
public override void _Ready()
{
__BindNodeSignals_Generated();
}
public override void _ExitTree()
{
__UnbindNodeSignals_Generated();
}
}
```
### 多事件绑定
一个方法可以同时绑定到多个节点的事件:
```csharp
public partial class MultiButtonHud : Control
{
private Button _buttonA = null!;
private Button _buttonB = null!;
private Button _buttonC = null!;
// 一个方法处理多个按钮的点击
[BindNodeSignal(nameof(_buttonA), nameof(Button.Pressed))]
[BindNodeSignal(nameof(_buttonB), nameof(Button.Pressed))]
[BindNodeSignal(nameof(_buttonC), nameof(Button.Pressed))]
private void OnAnyButtonPressed()
{
PlayClickSound();
}
public override void _Ready()
{
__BindNodeSignals_Generated();
}
public override void _ExitTree()
{
__UnbindNodeSignals_Generated();
}
}
```
### 与 [GetNode] 组合使用
推荐与 `[GetNode]` 特性结合使用:
```csharp
using GFramework.Godot.SourceGenerators.Abstractions;
using Godot;
public partial class GameHud : Control
{
// 使用 GetNode 自动获取节点
[GetNode]
private Button _pauseButton = null!;
[GetNode]
private ProgressBar _healthBar = null!;
[GetNode("UI/ScoreLabel")]
private Label _scoreLabel = null!;
// 使用 BindNodeSignal 绑定事件
[BindNodeSignal(nameof(_pauseButton), nameof(Button.Pressed))]
private void OnPauseButtonPressed()
{
TogglePause();
}
// 多事件绑定示例
[BindNodeSignal(nameof(_healthBar), nameof(Range.ValueChanged))]
private void OnHealthChanged(double value)
{
UpdateHealthDisplay(value);
}
public override void _Ready()
{
// 先注入节点,再绑定信号
__InjectGetNodes_Generated();
__BindNodeSignals_Generated();
}
public override void _ExitTree()
{
__UnbindNodeSignals_Generated();
}
}
```
### 复杂事件处理场景
实现完整的 UI 事件处理:
```csharp
public partial class InventoryUI : Control
{
// 节点
[GetNode]
private ItemList _itemList = null!;
[GetNode]
private Button _useButton = null!;
[GetNode]
private Button _dropButton = null!;
[GetNode]
private LineEdit _searchBox = null!;
// 事件处理
[BindNodeSignal(nameof(_itemList), nameof(ItemList.ItemSelected))]
private void OnItemSelected(long index)
{
SelectItem((int)index);
}
[BindNodeSignal(nameof(_itemList), nameof(ItemList.ItemActivated))]
private void OnItemActivated(long index)
{
UseItem((int)index);
}
[BindNodeSignal(nameof(_useButton), nameof(Button.Pressed))]
private void OnUseButtonPressed()
{
UseSelectedItem();
}
[BindNodeSignal(nameof(_dropButton), nameof(Button.Pressed))]
private void OnDropButtonPressed()
{
DropSelectedItem();
}
[BindNodeSignal(nameof(_searchBox), nameof(LineEdit.TextChanged))]
private void OnSearchTextChanged(string newText)
{
FilterItems(newText);
}
public override void _Ready()
{
__InjectGetNodes_Generated();
__BindNodeSignals_Generated();
InitializeInventory();
}
public override void _ExitTree()
{
__UnbindNodeSignals_Generated();
}
}
```
## 生命周期管理
### 自动生成生命周期方法
如果类没有 `_Ready()``_ExitTree()`,生成器会自动生成:
```csharp
public partial class AutoLifecycleHud : Control
{
private Button _button = null!;
[BindNodeSignal(nameof(_button), nameof(Button.Pressed))]
private void OnButtonPressed()
{
// 处理点击
}
// 无需手动声明 _Ready 和 _ExitTree
// 生成器会自动生成:
// public override void _Ready() { __BindNodeSignals_Generated(); }
// public override void _ExitTree() { __UnbindNodeSignals_Generated(); }
}
```
### 手动生命周期调用
如果已有生命周期方法,需要手动调用生成的方法:
```csharp
public partial class CustomLifecycleHud : Control
{
private Button _button = null!;
[BindNodeSignal(nameof(_button), nameof(Button.Pressed))]
private void OnButtonPressed()
{
HandlePress();
}
public override void _Ready()
{
// 必须手动调用绑定方法
__BindNodeSignals_Generated();
// 自定义初始化逻辑
InitializeUI();
}
public override void _ExitTree()
{
// 必须手动调用解绑方法
__UnbindNodeSignals_Generated();
// 自定义清理逻辑
CleanupResources();
}
}
```
**注意**:如果在 `_Ready()` 中不调用 `__BindNodeSignals_Generated()`,编译器会发出警告 `GF_Godot_BindNodeSignal_008`
## 诊断信息
生成器会在以下情况报告编译错误或警告:
### GF_Godot_BindNodeSignal_001 - 不支持嵌套类
**错误信息**`Class '{ClassName}' cannot use [BindNodeSignal] inside a nested type`
**解决方案**:将嵌套类提取为独立的类
```csharp
// ❌ 错误
public partial class Outer
{
public partial class Inner
{
[BindNodeSignal(nameof(_button), nameof(Button.Pressed))]
private void OnPressed() { } // 错误
}
}
// ✅ 正确
public partial class Inner
{
[BindNodeSignal(nameof(_button), nameof(Button.Pressed))]
private void OnPressed() { }
}
```
### GF_Godot_BindNodeSignal_002 - 不支持静态方法
**错误信息**`Method '{MethodName}' cannot be static when using [BindNodeSignal]`
**解决方案**:改为实例方法
```csharp
// ❌ 错误
[BindNodeSignal(nameof(_button), nameof(Button.Pressed))]
private static void OnPressed() { }
// ✅ 正确
[BindNodeSignal(nameof(_button), nameof(Button.Pressed))]
private void OnPressed() { }
```
### GF_Godot_BindNodeSignal_003 - 节点字段不存在
**错误信息**
`Method '{MethodName}' references node field '{FieldName}', but no matching field exists on class '{ClassName}'`
**解决方案**:确保引用的字段存在且名称正确
```csharp
// ❌ 错误_button 字段不存在
[BindNodeSignal(nameof(_button), nameof(Button.Pressed))]
private void OnPressed() { }
// ✅ 正确
private Button _button = null!;
[BindNodeSignal(nameof(_button), nameof(Button.Pressed))]
private void OnPressed() { }
```
### GF_Godot_BindNodeSignal_004 - 节点字段必须是实例字段
**错误信息**`Method '{MethodName}' references node field '{FieldName}', but that field must be an instance field`
**解决方案**:将节点字段改为实例字段(非静态)
```csharp
// ❌ 错误
private static Button _button = null!;
// ✅ 正确
private Button _button = null!;
```
### GF_Godot_BindNodeSignal_005 - 字段类型必须继承自 Godot.Node
**错误信息**`Field '{FieldName}' must be a Godot.Node type to use [BindNodeSignal]`
**解决方案**:确保字段类型继承自 `Godot.Node`
```csharp
// ❌ 错误
private string _text = null!; // string 不是 Node 类型
[BindNodeSignal(nameof(_text), "Changed")] // 错误
// ✅ 正确
private Button _button = null!; // Button 继承自 Node
[BindNodeSignal(nameof(_button), nameof(Button.Pressed))]
```
### GF_Godot_BindNodeSignal_006 - 目标事件不存在
**错误信息**`Field '{FieldName}' does not contain an event named '{EventName}'`
**解决方案**:确保事件名称正确
```csharp
private Button _button = null!;
// ❌ 错误Click 不是 Button 的事件
[BindNodeSignal(nameof(_button), "Click")]
// ✅ 正确:使用正确的事件名
[BindNodeSignal(nameof(_button), nameof(Button.Pressed))]
```
### GF_Godot_BindNodeSignal_007 - 方法签名不兼容
**错误信息**`Method '{MethodName}' is not compatible with event '{EventName}' on field '{FieldName}'`
**解决方案**:确保方法签名与事件委托匹配
```csharp
private SpinBox _spinBox = null!;
// ❌ 错误SpinBox.ValueChanged 需要 double 参数
[BindNodeSignal(nameof(_spinBox), nameof(SpinBox.ValueChanged))]
private void OnValueChanged() { } // 缺少参数
// ✅ 正确
[BindNodeSignal(nameof(_spinBox), nameof(SpinBox.ValueChanged))]
private void OnValueChanged(double value) { }
```
### GF_Godot_BindNodeSignal_008 - 需要在 _Ready 中调用绑定方法
**警告信息**
`Class '{ClassName}' defines _Ready(); call __BindNodeSignals_Generated() there to bind [BindNodeSignal] handlers`
**解决方案**:在 `_Ready()` 中手动调用 `__BindNodeSignals_Generated()`
```csharp
public override void _Ready()
{
__BindNodeSignals_Generated(); // ✅ 必须手动调用
// 其他初始化...
}
```
### GF_Godot_BindNodeSignal_009 - 需要在 _ExitTree 中调用解绑方法
**警告信息**
`Class '{ClassName}' defines _ExitTree(); call __UnbindNodeSignals_Generated() there to unbind [BindNodeSignal] handlers`
**解决方案**:在 `_ExitTree()` 中手动调用 `__UnbindNodeSignals_Generated()`
```csharp
public override void _ExitTree()
{
__UnbindNodeSignals_Generated(); // ✅ 必须手动调用
// 其他清理...
}
```
### GF_Godot_BindNodeSignal_010 - 构造参数无效
**错误信息**
`Method '{MethodName}' uses [BindNodeSignal] with an invalid '{ParameterName}' constructor argument; it must be a non-empty string literal`
**解决方案**:使用有效的字符串字面量或 nameof 表达式
```csharp
// ❌ 错误:空字符串
[BindNodeSignal("", nameof(Button.Pressed))]
// ❌ 错误null 值
[BindNodeSignal(null, nameof(Button.Pressed))]
// ✅ 正确
[BindNodeSignal(nameof(_button), nameof(Button.Pressed))]
```
## 最佳实践
### 1. 使用 nameof 表达式
使用 `nameof` 而不是字符串字面量,以获得重构支持和编译时检查:
```csharp
// ❌ 不推荐:字符串字面量
[BindNodeSignal("_button", "Pressed")]
// ✅ 推荐nameof 表达式
[BindNodeSignal(nameof(_button), nameof(Button.Pressed))]
```
### 2. 保持方法命名一致
使用统一的命名约定提高代码可读性:
```csharp
// ✅ 推荐On + 节点名 + 事件名
[BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))]
private void OnStartButtonPressed() { }
[BindNodeSignal(nameof(_volumeSlider), nameof(Slider.ValueChanged))]
private void OnVolumeSliderValueChanged(double value) { }
```
### 3. 分组相关事件处理
将相关的事件处理方法放在一起,便于维护:
```csharp
public partial class GameHud : Control
{
// UI 节点
[GetNode]
private Button _pauseButton = null!;
[GetNode]
private Button _menuButton = null!;
// UI 事件处理(放在一起)
[BindNodeSignal(nameof(_pauseButton), nameof(Button.Pressed))]
private void OnPauseButtonPressed() { }
[BindNodeSignal(nameof(_menuButton), nameof(Button.Pressed))]
private void OnMenuButtonPressed() { }
}
```
### 4. 正确处理生命周期
始终确保事件解绑,避免内存泄漏:
```csharp
public partial class SafeHud : Control
{
private Button _button = null!;
[BindNodeSignal(nameof(_button), nameof(Button.Pressed))]
private void OnButtonPressed() { }
public override void _Ready()
{
__BindNodeSignals_Generated();
}
public override void _ExitTree()
{
// 确保解绑事件
__UnbindNodeSignals_Generated();
}
}
```
### 5. 对比手动事件绑定
| 方式 | 代码量 | 可维护性 | 错误风险 | 推荐场景 |
|--------------------|-----|------|----------|------------|
| 手动 `+=` / `-=` | 多 | 中 | 高(易遗漏解绑) | 简单场景 |
| `[BindNodeSignal]` | 少 | 高 | 低(编译器检查) | 复杂 UI、频繁事件 |
```csharp
// ❌ 不推荐:手动绑定
public override void _Ready()
{
_startButton.Pressed += OnStartButtonPressed;
_settingsButton.Pressed += OnSettingsButtonPressed;
_quitButton.Pressed += OnQuitButtonPressed;
}
public override void _ExitTree()
{
// 容易遗漏解绑
_startButton.Pressed -= OnStartButtonPressed;
_quitButton.Pressed -= OnQuitButtonPressed; // 遗漏了 _settingsButton
}
// ✅ 推荐:使用 [BindNodeSignal]
[BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))]
private void OnStartButtonPressed() { }
[BindNodeSignal(nameof(_settingsButton), nameof(Button.Pressed))]
private void OnSettingsButtonPressed() { }
[BindNodeSignal(nameof(_quitButton), nameof(Button.Pressed))]
private void OnQuitButtonPressed() { }
```
### 6. 与 [ContextAware] 组合使用
在需要架构访问的场景中,与 `[ContextAware]` 结合:
```csharp
using GFramework.SourceGenerators.Abstractions.Rule;
using GFramework.Godot.SourceGenerators.Abstractions;
[ContextAware]
public partial class GameController : Node
{
[GetNode]
private Button _actionButton = null!;
private IGameModel _gameModel = null!;
[BindNodeSignal(nameof(_actionButton), nameof(Button.Pressed))]
private void OnActionButtonPressed()
{
// 可以直接使用架构功能
this.SendCommand(new PlayerActionCommand());
}
public override void _Ready()
{
__InjectContextBindings_Generated();
__InjectGetNodes_Generated();
__BindNodeSignals_Generated();
}
public override void _ExitTree()
{
__UnbindNodeSignals_Generated();
}
}
```
## 相关文档
- [Source Generators 概述](./index)
- [GetNode 生成器](./get-node-generator)
- [ContextAware 生成器](./context-aware-generator)
- [Godot 信号文档](https://docs.godotengine.org/en/stable/classes/class_signal.html)