diff --git a/docs/zh-CN/source-generators/bind-node-signal-generator.md b/docs/zh-CN/source-generators/bind-node-signal-generator.md new file mode 100644 index 0000000..9a10631 --- /dev/null +++ b/docs/zh-CN/source-generators/bind-node-signal-generator.md @@ -0,0 +1,680 @@ +# 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) diff --git a/docs/zh-CN/source-generators/get-node-generator.md b/docs/zh-CN/source-generators/get-node-generator.md new file mode 100644 index 0000000..9c7c9e6 --- /dev/null +++ b/docs/zh-CN/source-generators/get-node-generator.md @@ -0,0 +1,496 @@ +# GetNode 生成器 + +> 自动生成 Godot 节点获取逻辑,简化节点引用代码 + +## 概述 + +GetNode 生成器为标记了 `[GetNode]` 特性的字段自动生成 Godot 节点获取代码,无需手动调用 `GetNode()` 方法。这在处理复杂 +UI 或场景树结构时特别有用。 + +### 核心功能 + +- **自动节点获取**:根据路径或字段名自动获取节点 +- **多种查找模式**:支持唯一名、相对路径、绝对路径查找 +- **可选节点支持**:可以标记节点为可选,获取失败时返回 null +- **智能路径推导**:未显式指定路径时自动从字段名推导 +- **_Ready 钩子生成**:自动生成 `_Ready()` 方法注入节点获取逻辑 + +## 基础使用 + +### 标记节点字段 + +使用 `[GetNode]` 特性标记需要自动获取的节点字段: + +```csharp +using GFramework.Godot.SourceGenerators.Abstractions; +using Godot; + +public partial class PlayerHud : Control +{ + [GetNode] + private Label _healthLabel = null!; + + [GetNode] + private ProgressBar _manaBar = null!; + + [GetNode("ScoreContainer/ScoreValue")] + private Label _scoreLabel = null!; + + public override void _Ready() + { + __InjectGetNodes_Generated(); + _healthLabel.Text = "100"; + } +} +``` + +### 生成的代码 + +编译器会为标记的类自动生成以下代码: + +```csharp +// +#nullable enable + +namespace YourNamespace; + +partial class PlayerHud +{ + private void __InjectGetNodes_Generated() + { + _healthLabel = GetNode("%HealthLabel"); + _manaBar = GetNode("%ManaBar"); + _scoreLabel = GetNode("ScoreContainer/ScoreValue"); + } + + partial void OnGetNodeReadyGenerated(); + + public override void _Ready() + { + __InjectGetNodes_Generated(); + OnGetNodeReadyGenerated(); + } +} +``` + +## 配置选项 + +### 节点查找模式 + +通过 `Lookup` 参数控制节点查找方式: + +```csharp +public partial class GameHud : Control +{ + // 自动推断(默认):根据路径前缀自动选择 + [GetNode] + private Label _titleLabel = null!; // 默认使用唯一名 %TitleLabel + + // 唯一名查找 + [GetNode(Lookup = NodeLookupMode.UniqueName)] + private Button _startButton = null!; // %StartButton + + // 相对路径查找 + [GetNode("UI/HealthBar", Lookup = NodeLookupMode.RelativePath)] + private ProgressBar _healthBar = null!; + + // 绝对路径查找 + [GetNode("/root/Main/GameUI/Score", Lookup = NodeLookupMode.AbsolutePath)] + private Label _scoreLabel = null!; +} +``` + +### 查找模式说明 + +| 模式 | 路径前缀 | 适用场景 | +|----------------|------|----------------| +| `Auto` | 自动选择 | 默认行为,推荐用于大多数场景 | +| `UniqueName` | `%` | 场景中使用唯一名的节点 | +| `RelativePath` | 无 | 需要相对路径查找的节点 | +| `AbsolutePath` | `/` | 场景树根节点的绝对路径 | + +### 可选节点 + +对于可能不存在的节点,可以设置为非必填: + +```csharp +public partial class SettingsPanel : Control +{ + // 必须存在的节点(默认) + [GetNode] + private Label _titleLabel = null!; + + // 可选节点,可能不存在 + [GetNode(Required = false)] + private Label? _debugLabel; // 使用可空类型 + + // 显式路径的可选节点 + [GetNode("AdvancedOptions", Required = false)] + private VBoxContainer? _advancedOptions; + + public override void _Ready() + { + __InjectGetNodes_Generated(); + + // 安全地访问可选节点 + _debugLabel?.Hide(); + _advancedOptions?.Hide(); + } +} +``` + +### 路径规则 + +生成器根据字段名和配置自动推导节点路径: + +```csharp +public partial class Example : Control +{ + // 驼峰命名 → PascalCase 路径 + [GetNode] + private Label _playerNameLabel = null!; // → %PlayerNameLabel + + // m_ 前缀会被移除 + [GetNode] + private Button m_confirmButton = null!; // → %ConfirmButton + + // _ 前缀会被移除 + [GetNode] + private ProgressBar _healthBar = null!; // → %HealthBar + + // 显式路径优先于推导 + [GetNode("UI/CustomPath")] + private Label _myLabel = null!; // → UI/CustomPath +} +``` + +## 高级用法 + +### 与 [ContextAware] 组合使用 + +在 Godot 项目中结合使用 `[GetNode]` 和 `[ContextAware]`: + +```csharp +using GFramework.Godot.SourceGenerators.Abstractions; +using GFramework.SourceGenerators.Abstractions.Rule; +using Godot; + +[ContextAware] +public partial class GameController : Node +{ + [GetNode] + private Label _scoreLabel = null!; + + [GetNode("HUD/HealthBar")] + private ProgressBar _healthBar = null!; + + private IGameModel _gameModel = null!; + + public override void _Ready() + { + __InjectContextBindings_Generated(); // ContextAware 生成 + __InjectGetNodes_Generated(); // GetNode 生成 + + _gameModel.Score.Register(OnScoreChanged); + } + + private void OnScoreChanged(int newScore) + { + _scoreLabel.Text = newScore.ToString(); + } +} +``` + +### 复杂 UI 场景 + +处理复杂的嵌套 UI 结构: + +```csharp +public partial class InventoryUI : Control +{ + // 主容器 + [GetNode] + private GridContainer _itemGrid = null!; + + // 详细信息面板 + [GetNode("DetailsPanel/ItemName")] + private Label _itemNameLabel = null!; + + [GetNode("DetailsPanel/ItemDescription")] + private RichTextLabel _itemDescription = null!; + + // 操作按钮 + [GetNode("Actions/UseButton")] + private Button _useButton = null!; + + [GetNode("Actions/DropButton")] + private Button _dropButton = null!; + + // 可选的统计信息 + [GetNode("DetailsPanel/Stats", Required = false)] + private VBoxContainer? _statsContainer; + + public override void _Ready() + { + __InjectGetNodes_Generated(); + + // 使用注入的节点 + _useButton.Pressed += OnUseButtonPressed; + _dropButton.Pressed += OnDropButtonPressed; + } +} +``` + +### 手动 _Ready 调用 + +如果类已经有 `_Ready()` 方法,需要手动调用注入方法: + +```csharp +public partial class CustomHud : Control +{ + [GetNode] + private Label _statusLabel = null!; + + public override void _Ready() + { + // 必须手动调用节点注入 + __InjectGetNodes_Generated(); + + // 自定义初始化逻辑 + _statusLabel.Text = "Ready"; + InitializeOtherComponents(); + } + + partial void OnGetNodeReadyGenerated() + { + // 这个方法会被生成器调用,可以在此添加额外初始化 + } +} +``` + +**注意**:如果不手动调用 `__InjectGetNodes_Generated()`,编译器会发出警告 `GF_Godot_GetNode_006`。 + +## 诊断信息 + +生成器会在以下情况报告编译错误或警告: + +### GF_Godot_GetNode_001 - 不支持嵌套类 + +**错误信息**:`Class '{ClassName}' cannot use [GetNode] inside a nested type` + +**解决方案**:将嵌套类提取为独立的类 + +```csharp +// ❌ 错误 +public partial class Outer +{ + public partial class Inner + { + [GetNode] + private Label _label = null!; // 错误 + } +} + +// ✅ 正确 +public partial class Inner +{ + [GetNode] + private Label _label = null!; +} +``` + +### GF_Godot_GetNode_002 - 不支持静态字段 + +**错误信息**:`Field '{FieldName}' cannot be static when using [GetNode]` + +**解决方案**:改为实例字段 + +```csharp +// ❌ 错误 +[GetNode] +private static Label _label = null!; + +// ✅ 正确 +[GetNode] +private Label _label = null!; +``` + +### GF_Godot_GetNode_003 - 不支持只读字段 + +**错误信息**:`Field '{FieldName}' cannot be readonly when using [GetNode]` + +**解决方案**:移除 `readonly` 关键字 + +```csharp +// ❌ 错误 +[GetNode] +private readonly Label _label = null!; + +// ✅ 正确 +[GetNode] +private Label _label = null!; +``` + +### GF_Godot_GetNode_004 - 字段类型必须继承自 Godot.Node + +**错误信息**:`Field '{FieldName}' must be a Godot.Node type to use [GetNode]` + +**解决方案**:确保字段类型继承自 `Godot.Node` + +```csharp +// ❌ 错误 +[GetNode] +private string _text = null!; // string 不是 Node 类型 + +// ✅ 正确 +[GetNode] +private Label _label = null!; // Label 继承自 Node +``` + +### GF_Godot_GetNode_005 - 无法推导路径 + +**错误信息**:`Field '{FieldName}' does not provide a path and its name cannot be converted to a node path` + +**解决方案**:显式指定节点路径 + +```csharp +// ❌ 错误:字段名无法转换为有效路径 +[GetNode] +private Label _ = null!; + +// ✅ 正确 +[GetNode("UI/Label")] +private Label _ = null!; +``` + +### GF_Godot_GetNode_006 - 需要在 _Ready 中调用注入方法 + +**警告信息**: +`Class '{ClassName}' defines _Ready(); call __InjectGetNodes_Generated() there or remove _Ready() to use the generated hook` + +**解决方案**:在 `_Ready()` 中手动调用 `__InjectGetNodes_Generated()` + +```csharp +public partial class MyHud : Control +{ + [GetNode] + private Label _label = null!; + + public override void _Ready() + { + __InjectGetNodes_Generated(); // ✅ 必须手动调用 + // 其他初始化... + } +} +``` + +## 最佳实践 + +### 1. 使用一致的命名约定 + +保持字段名与场景树中节点名的一致性: + +```csharp +// ✅ 推荐:字段名与节点名一致 +[GetNode] +private Label _healthLabel = null!; // 场景中的节点名为 HealthLabel + +[GetNode] +private Button _startButton = null!; // 场景中的节点名为 StartButton +``` + +### 2. 优先使用唯一名查找 + +在 Godot 编辑器中为重要节点启用唯一名(Unique Name),然后使用 `[GetNode]`: + +```csharp +// Godot 场景中:%HealthBar(唯一名已启用) +// C# 代码中: +[GetNode] +private ProgressBar _healthBar = null!; // 自动使用 %HealthBar +``` + +### 3. 合理处理可选节点 + +对于可能不存在的节点,使用 `Required = false`: + +```csharp +public partial class DynamicUI : Control +{ + [GetNode] + private Label _titleLabel = null!; + + // 可选组件 + [GetNode(Required = false)] + private TextureRect? _iconImage; + + public override void _Ready() + { + __InjectGetNodes_Generated(); + + // 安全地初始化可选组件 + if (_iconImage != null) + { + _iconImage.Texture = LoadDefaultIcon(); + } + } +} +``` + +### 4. 组织复杂 UI 的路径 + +对于深层嵌套的 UI,使用显式路径: + +```csharp +public partial class ComplexUI : Control +{ + // 使用相对路径明确表达层级关系 + [GetNode("MainContent/Header/Title")] + private Label _title = null!; + + [GetNode("MainContent/Body/Stats/Health")] + private Label _healthValue = null!; + + [GetNode("MainContent/Footer/ActionButtons/Save")] + private Button _saveButton = null!; +} +``` + +### 5. 与 GetNode 方法的对比 + +| 方式 | 代码量 | 可维护性 | 类型安全 | 推荐场景 | +|----------------|-----|------|--------|-----------| +| 手动 `GetNode()` | 多 | 中 | 需要显式转换 | 简单场景 | +| `[GetNode]` 特性 | 少 | 高 | 编译时检查 | 复杂 UI、控制器 | + +```csharp +// ❌ 不推荐:手动获取 +public override void _Ready() +{ + _healthLabel = GetNode