# 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 // #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)