diff --git a/ai-plan/public/documentation-governance-and-refresh/todos/documentation-governance-and-refresh-tracking.md b/ai-plan/public/documentation-governance-and-refresh/todos/documentation-governance-and-refresh-tracking.md index a435f696..53852305 100644 --- a/ai-plan/public/documentation-governance-and-refresh/todos/documentation-governance-and-refresh-tracking.md +++ b/ai-plan/public/documentation-governance-and-refresh/todos/documentation-governance-and-refresh-tracking.md @@ -7,13 +7,13 @@ ## 当前恢复点 -- 恢复点编号:`DOCUMENTATION-GOVERNANCE-REFRESH-RP-010` +- 恢复点编号:`DOCUMENTATION-GOVERNANCE-REFRESH-RP-011` - 当前阶段:`Phase 3` - 当前焦点: - 已建立统一公开 skill:`.agents/skills/gframework-doc-refresh/` - 文档重构入口已从“按 guide/tutorial/api 类型拆 skill”收口为“按源码模块驱动文档刷新” - PR #268 的当前未解决 review 线程已进入收口:Scene/UI 标题层级修正、共享脚本 review 修复、`gframework-pr-review` 多 AI reviewer 支持补齐 - - 下一轮需要用统一 skill 推进 Godot 相关生成器页面核对 + - `Godot.SourceGenerators` 的 3 个高风险专题页已按当前实现重写,下一轮转入剩余生成器页与 PR thread 收口 ## 当前状态摘要 @@ -57,6 +57,12 @@ - `docs/zh-CN/source-generators/priority-generator.md` 已改成“生成 `IPrioritized`、priority-aware 检索 API、动态优先级边界与诊断”的结构, 不再把 `GetAllByPriority()` / `system.Init()` 当作所有场景的默认示例 - 本轮重写后再次执行 `cd docs && bun run build` 通过,当前 `source-generators` 栏目改动没有破坏站点构建 +- `docs/zh-CN/source-generators/godot-project-generator.md` 已改成“包关系、最小接入路径、AutoLoad / InputActions 生成语义、`project.godot` 文件约束与诊断边界”的结构, + 明确 `GFrameworkGodotProjectFile` 只能改相对路径、不能改文件名 +- `docs/zh-CN/source-generators/get-node-generator.md` 已改成“字段注入职责、路径推断、`Required` / `Lookup` 语义、`_Ready()` 自动补齐边界与冲突诊断”的结构, + 明确只有缺少 `_Ready()` 时才会生成 `OnGetNodeReadyGenerated()` +- `docs/zh-CN/source-generators/bind-node-signal-generator.md` 已改成“CLR event 绑定职责、生命周期接线要求、与 `[GetNode]` 的调用顺序、签名约束与命名冲突”的结构, + 明确当前不会自动生成 `_Ready()` / `_ExitTree()` - `.agents/skills/gframework-doc-refresh/SKILL.md` 已改成标准 YAML frontmatter skill,并明确支持模块输入、证据顺序、输出优先级与验证步骤 - `.agents/skills/gframework-doc-refresh/SKILL.md` 的 `description` 已加引号,修复 `Recommended command:` 中冒号导致的 invalid YAML skill 加载警告 @@ -74,6 +80,7 @@ - 旧专题页示例失真风险:`docs/zh-CN/game/*` 与 `source-generators/*` 中仍可能保留看似合理但与真实实现不一致的示例 - 缓解措施:`game/scene.md`、`ui.md`、`source-generators/context-aware-generator.md` 与 `priority-generator.md` 已完成收口; + `godot-project-generator.md`、`get-node-generator.md` 与 `bind-node-signal-generator.md` 已完成收口; 继续按源码、测试、`*.csproj` 与 `ai-libs/` 下已验证参考实现核对剩余 Godot 相关页面,不把旧文档当事实来源 - 采用路径误导风险:根聚合包与模块边界若再次被写错,会继续误导消费者的包选择 - 缓解措施:保持“源码与包关系优先”的证据顺序,改动采用说明时同步核对包依赖与生成器 wiring @@ -115,10 +122,13 @@ - `python3 .agents/skills/gframework-doc-refresh/scripts/scan_module_evidence.py Core` - `python3 .agents/skills/gframework-doc-refresh/scripts/scan_module_evidence.py Godot.SourceGenerators` - `python3 .agents/skills/gframework-doc-refresh/scripts/scan_module_evidence.py Cqrs` +- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/source-generators/godot-project-generator.md` +- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/source-generators/get-node-generator.md` +- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/source-generators/bind-node-signal-generator.md` +- `cd docs && bun run build` ## 下一步 -1. 继续核对 Godot 相关生成器页面,优先处理 `godot-project-generator.md`、`get-node-generator.md` 与 - `bind-node-signal-generator.md`,优先用 `gframework-doc-refresh` 的模块扫描结果驱动判断 +1. 继续核对 `auto-register-exported-collections-generator.md`,确认其示例、诊断与 `Godot.SourceGenerators` 当前实现一致 2. 下一次推送后先重新执行 `$gframework-pr-review`,确认 PR #268 的 CodeRabbit / Greptile open thread 是否按预期收敛 -3. 再继续确认 `project.godot`、`AutoLoad` / `InputActions`、`GetNode` / `BindNodeSignal` 示例仍与当前包关系和生成器入口一致 +3. 继续复核 `docs/zh-CN/tutorials/godot-integration.md`,避免旧教程重新把过时 Godot 说明带回专题页 diff --git a/ai-plan/public/documentation-governance-and-refresh/traces/documentation-governance-and-refresh-trace.md b/ai-plan/public/documentation-governance-and-refresh/traces/documentation-governance-and-refresh-trace.md index 729de8cb..ede927ca 100644 --- a/ai-plan/public/documentation-governance-and-refresh/traces/documentation-governance-and-refresh-trace.md +++ b/ai-plan/public/documentation-governance-and-refresh/traces/documentation-governance-and-refresh-trace.md @@ -2,7 +2,7 @@ ## 2026-04-22 -### 当前恢复点:RP-010 +### 当前恢复点:RP-011 - 本轮从 PR #268 的最新 review 数据恢复,未发现失败检查;CTRF 报告显示 2139 个测试全部通过 - 本轮复核确认当前 PR 的 latest-head open thread 同时来自 `coderabbitai[bot]` 与 `greptile-apps[bot]` @@ -16,6 +16,12 @@ - `fetch_current_pr_review.py` 的本地函数 docstring 覆盖率已补到 `44/44` - 已闭环 RP-001 到 RP-008 的执行细节已归档到 `ai-plan/public/documentation-governance-and-refresh/archive/traces/documentation-governance-and-refresh-rp-001-through-rp-008.md` +- 本轮按 `gframework-doc-refresh` 的模块扫描结果,重写了 `Godot.SourceGenerators` 的 3 个高风险专题页: + - `godot-project-generator.md` + - `get-node-generator.md` + - `bind-node-signal-generator.md` +- 新页面统一收口到“包关系、最小接入路径、真实生成语义、生命周期边界、诊断约束”,不再沿用旧教程式长篇 API 罗列 +- 本轮额外复核了 `ai-libs/CoreGrid` 的真实采用方式,确认 `[GetNode]` / `[BindNodeSignal]` 组合使用时应先注入节点再绑定事件 ### 当前决策 @@ -23,6 +29,8 @@ - `scene.md` 与 `ui.md` 的集成说明除目录布局外,也要保证标题层级能真实反映采用路径语义 - `gframework-pr-review` 继续以 latest-head unresolved thread 为主信号,同时显式声明支持的 AI reviewer 名单,避免 skill 声明与实际抓取能力再次漂移 +- `Godot.SourceGenerators` 专题页继续采用“源码 / 测试 / README 优先,`ai-libs/` 只补消费者 wiring”的证据顺序 +- `BindNodeSignal` 页面明确记录“当前不自动生成 `_Ready()` / `_ExitTree()`”,避免继续把它写成自动生命周期织入器 ### 验证 @@ -33,9 +41,13 @@ - `bash .agents/skills/gframework-doc-refresh/scripts/validate-code-blocks.sh docs/zh-CN/game/ui.md` - `bash -lc 'source .agents/skills/_shared/module-config.sh && get_readme_paths Core.SourceGenerators.Abstractions && if get_readme_paths Not.Real.Module; then exit 1; else echo unmapped-ok; fi'` - `cd docs && bun run build` +- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/source-generators/godot-project-generator.md` +- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/source-generators/get-node-generator.md` +- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/source-generators/bind-node-signal-generator.md` +- `cd docs && bun run build` ### 下一步 1. 下一次推送后重新执行 `$gframework-pr-review`,确认 PR #268 的 CodeRabbit / Greptile open thread 是否关闭或减少 2. 继续使用 `gframework-doc-refresh` 对 `Godot.SourceGenerators` 做真实模块扫描 -3. 优先刷新 `godot-project-generator.md`、`get-node-generator.md` 与 `bind-node-signal-generator.md` +3. 优先刷新 `auto-register-exported-collections-generator.md`,并复核 `tutorials/godot-integration.md` 是否仍残留旧叙述 diff --git a/docs/zh-CN/source-generators/bind-node-signal-generator.md b/docs/zh-CN/source-generators/bind-node-signal-generator.md index bdec6290..0e317db7 100644 --- a/docs/zh-CN/source-generators/bind-node-signal-generator.md +++ b/docs/zh-CN/source-generators/bind-node-signal-generator.md @@ -1,283 +1,46 @@ +--- +title: BindNodeSignal 生成器 +description: 说明 [BindNodeSignal] 当前生成什么、如何与 GetNode 协作,以及 _Ready 和 _ExitTree 的接入要求。 +--- + # BindNodeSignal 生成器 -> 自动生成 Godot 节点信号绑定与解绑逻辑,消除事件订阅样板代码 +`[BindNodeSignal]` 把 Godot CLR event 的 `+=` / `-=` 样板收敛成生成方法。它只生成“如何订阅与解绑”,不会替你查找节点,也不会自动生成完整生命周期方法。 -## 概述 +## 当前包关系 -BindNodeSignal 生成器为标记了 `[BindNodeSignal]` 特性的方法自动生成节点事件绑定和解绑代码。它将 `_Ready()` 和 -`_ExitTree()` 中重复的 `+=` 和 `-=` 样板代码收敛到生成器中统一维护。 +- 特性来源:`GFramework.Godot.SourceGenerators.Abstractions` +- 生成器实现:`GFramework.Godot.SourceGenerators` +- 目标字段基线:`nodeFieldName` 指向的字段必须继承 `Godot.Node` -### 核心功能 - -- **自动事件绑定**:在 `_Ready()` 中自动订阅节点事件 -- **自动事件解绑**:在 `_ExitTree()` 中自动取消订阅 -- **多事件绑定**:一个方法可以绑定到多个节点事件 -- **类型安全检查**:编译时验证方法签名与事件委托的兼容性 -- **与 GetNode 集成**:无缝配合 `[GetNode]` 特性使用 - -## 基础使用 - -### 标记事件处理方法 - -使用 `[BindNodeSignal]` 特性标记处理节点事件的方法: +## 最小用法 ```csharp using GFramework.Godot.SourceGenerators.Abstractions; using Godot; -public partial class MainMenu : Control +public partial class Hud : Control { + [GetNode] private Button _startButton = null!; - private Button _settingsButton = null!; - private Button _quitButton = null!; + + [GetNode] + private SpinBox _startOreSpinBox = null!; [BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))] private void OnStartButtonPressed() { - StartGame(); } - [BindNodeSignal(nameof(_settingsButton), nameof(Button.Pressed))] - private void OnSettingsButtonPressed() + [BindNodeSignal(nameof(_startOreSpinBox), nameof(SpinBox.ValueChanged))] + private void OnStartOreValueChanged(double value) { - 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() @@ -287,394 +50,143 @@ public partial class InventoryUI : Control } ``` -## 生命周期管理 - -### 自动生成生命周期方法 - -如果类没有 `_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() +private void __BindNodeSignals_Generated() { _startButton.Pressed += OnStartButtonPressed; - _settingsButton.Pressed += OnSettingsButtonPressed; - _quitButton.Pressed += OnQuitButtonPressed; + _startOreSpinBox.ValueChanged += OnStartOreValueChanged; } -public override void _ExitTree() +private void __UnbindNodeSignals_Generated() { - // 容易遗漏解绑 _startButton.Pressed -= OnStartButtonPressed; - _quitButton.Pressed -= OnQuitButtonPressed; // 遗漏了 _settingsButton + _startOreSpinBox.ValueChanged -= OnStartOreValueChanged; } - -// ✅ 推荐:使用 [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]` 结合: +### 它只生成辅助方法,不生成 `_Ready()` / `_ExitTree()` + +这是当前和 `[GetNode]` 最大的区别: + +- `[GetNode]` 在缺少 `_Ready()` 时会补一个 override +- `[BindNodeSignal]` 只生成 `__BindNodeSignals_Generated()` 和 `__UnbindNodeSignals_Generated()` + +所以你需要自己决定在哪个生命周期里调用它们。 + +### 已有生命周期但没调用时会给 warning + +如果类型已经定义了 `_Ready()` 或 `_ExitTree()`,但没有调用对应生成方法,当前会给出 warning,提醒你完成接线。 + +这意味着它更像“声明式订阅语法”,而不是“自动生命周期织入”。 + +## 当前契约 + +`[BindNodeSignal(nodeFieldName, signalName)]` 的两个参数都指向现有代码里的稳定符号: + +- `nodeFieldName`:目标节点字段名 +- `signalName`:该节点类型上的 CLR event 名 + +最推荐的写法仍然是: ```csharp -using GFramework.Core.SourceGenerators.Abstractions.Rule; -using GFramework.Godot.SourceGenerators.Abstractions; +[BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))] +``` -[ContextAware] -public partial class GameController : Node +这样字段或事件改名时,编译器能一起帮你更新。 + +## 当前会验证什么 + +生成器不是盲目拼字符串。按当前源码,它会在编译期验证: + +- 方法必须是实例方法 +- `nodeFieldName` 必须能解析到当前类型里的实例字段 +- 该字段类型必须继承 `Godot.Node` +- `signalName` 必须能解析到该字段类型上的 CLR event +- 处理方法签名必须和 event delegate 兼容 + +例如: + +- `Button.Pressed` 对应无参处理方法 +- `SpinBox.ValueChanged` 对应 `double` 参数 + +如果签名不匹配,会直接报错,而不是生成一个运行时才失败的订阅。 + +## 多重绑定 + +`BindNodeSignalAttribute` 允许重复标记在同一个方法上,所以一个处理方法可以绑定多个事件: + +```csharp +[BindNodeSignal(nameof(_buttonA), nameof(Button.Pressed))] +[BindNodeSignal(nameof(_buttonB), nameof(Button.Pressed))] +[BindNodeSignal(nameof(_buttonC), nameof(Button.Pressed))] +private void OnAnyButtonPressed() { - [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) +`ai-libs/CoreGrid` 里的 `GameplayHud`、`PauseMenu` 和 `OptionBrowser` 都在大量使用这种声明式绑定方式。 + +## 与 GetNode 的协作边界 + +`[BindNodeSignal]` 不负责拿到字段实例,只负责在字段已经可用的前提下做事件接线。 + +因此同类型同时使用时,顺序应该是: + +1. `__InjectGetNodes_Generated()` +2. `__BindNodeSignals_Generated()` +3. 在 `_ExitTree()` 调用 `__UnbindNodeSignals_Generated()` + +这是当前项目侧真实采用路径,不是文档偏好。 + +## 当前强约束 + +以下约束直接来自生成器源码与测试: + +- 目标类型必须是顶层 `partial class` +- 不支持嵌套类 +- 方法不能是 `static` +- 节点字段必须存在且是实例字段 +- 节点字段类型必须继承 `Godot.Node` +- 事件名必须是 CLR event,不是任意字符串 +- 如果你自己声明了 `__BindNodeSignals_Generated()` 或 `__UnbindNodeSignals_Generated()`,会触发命名冲突诊断 + +## 什么时候适合用 `[BindNodeSignal]` + +适合: + +- UI、菜单、HUD、面板类里按钮或输入事件很多 +- 你想把订阅/解绑语义放回方法声明旁边,而不是堆在 `_Ready()` / `_ExitTree()` +- 你已经用 `[GetNode]` 或其他方式稳定拿到节点字段 + +不适合: + +- 事件目标需要在运行时动态决定 +- 你用的是 `Connect()` / `Disconnect()` 风格,而不是 CLR event +- 你需要比“字段 + 事件名”更复杂的订阅条件 + +## 与旧写法的边界 + +下面这些旧说法已经不准确: + +- “`[BindNodeSignal]` 会自动生成 `_Ready()` / `_ExitTree()`” +- “它能处理所有 Godot signal 连接方式” +- “有没有 `__UnbindNodeSignals_Generated()` 都无所谓” + +当前更准确的理解是: + +- 它只生成成对的绑定/解绑辅助方法 +- 当前设计面向 CLR event,不自动调用 `Connect()` / `Disconnect()` +- 如果要避免节点退出后残留订阅,应在 `_ExitTree()` 中显式解绑 + +## 推荐阅读 + +1. [/zh-CN/source-generators/get-node-generator](./get-node-generator.md) +2. [/zh-CN/source-generators/godot-project-generator](./godot-project-generator.md) +3. [/zh-CN/godot/ui](../godot/ui.md) +4. `GFramework.Godot.SourceGenerators/README.md` diff --git a/docs/zh-CN/source-generators/get-node-generator.md b/docs/zh-CN/source-generators/get-node-generator.md index cbbd5834..1f317486 100644 --- a/docs/zh-CN/source-generators/get-node-generator.md +++ b/docs/zh-CN/source-generators/get-node-generator.md @@ -1,496 +1,198 @@ +--- +title: GetNode 生成器 +description: 说明 [GetNode] 当前生成什么、路径如何推断,以及 _Ready 生命周期里的接入边界。 +--- + # GetNode 生成器 -> 自动生成 Godot 节点获取逻辑,简化节点引用代码 +`[GetNode]` 用来把 Godot 节点查找样板收敛到生成器里。它只处理“字段如何取到节点”,不负责事件订阅,也不负责其他运行时装配。 -## 概述 +## 当前包关系 -GetNode 生成器为标记了 `[GetNode]` 特性的字段自动生成 Godot 节点获取代码,无需手动调用 `GetNode()` 方法。这在处理复杂 -UI 或场景树结构时特别有用。 +- 特性来源:`GFramework.Godot.SourceGenerators.Abstractions` +- 生成器实现:`GFramework.Godot.SourceGenerators` +- 目标类型基线:字段类型必须继承 `Godot.Node` -### 核心功能 - -- **自动节点获取**:根据路径或字段名自动获取节点 -- **多种查找模式**:支持唯一名、相对路径、绝对路径查找 -- **可选节点支持**:可以标记节点为可选,获取失败时返回 null -- **智能路径推导**:未显式指定路径时自动从字段名推导 -- **_Ready 钩子生成**:自动生成 `_Ready()` 方法注入节点获取逻辑 - -## 基础使用 - -### 标记节点字段 - -使用 `[GetNode]` 特性标记需要自动获取的节点字段: +## 最小用法 ```csharp using GFramework.Godot.SourceGenerators.Abstractions; using Godot; -public partial class PlayerHud : Control +public partial class TopBar : HBoxContainer { [GetNode] - private Label _healthLabel = null!; + private HBoxContainer _leftContainer = null!; [GetNode] - private ProgressBar _manaBar = null!; - - [GetNode("ScoreContainer/ScoreValue")] - private Label _scoreLabel = null!; - - public override void _Ready() - { - __InjectGetNodes_Generated(); - _healthLabel.Text = "100"; - } + private HBoxContainer m_rightContainer = null!; } ``` -### 生成的代码 - -编译器会为标记的类自动生成以下代码: +如果目标类型还没有 `_Ready()`,当前生成器会补出: ```csharp -// -#nullable enable - -namespace YourNamespace; - -partial class PlayerHud +private void __InjectGetNodes_Generated() { - 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.Core.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!; // 错误 - } + _leftContainer = GetNode("%LeftContainer"); + m_rightContainer = GetNode("%RightContainer"); } -// ✅ 正确 -public partial class Inner -{ - [GetNode] - private Label _label = null!; -} -``` +partial void OnGetNodeReadyGenerated(); -### 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