mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-07 00:39:00 +08:00
docs(source-generators): 刷新 Godot 生成器文档
- 更新 Godot 项目元数据、GetNode 与 BindNodeSignal 专题页,按当前源码与测试收口最小接入路径、生成语义与诊断边界 - 补充 documentation-governance-and-refresh 的 RP-011 恢复点、验证结果与下一步建议
This commit is contained in:
parent
3425b299f0
commit
214f52b6c2
@ -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<T>()` / `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 说明带回专题页
|
||||
|
||||
@ -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` 是否仍残留旧叙述
|
||||
|
||||
@ -1,56 +1,45 @@
|
||||
---
|
||||
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()
|
||||
{
|
||||
__InjectGetNodes_Generated();
|
||||
__BindNodeSignals_Generated();
|
||||
}
|
||||
|
||||
@ -61,620 +50,143 @@ public partial class MainMenu : Control
|
||||
}
|
||||
```
|
||||
|
||||
### 生成的代码
|
||||
|
||||
编译器会为标记的类自动生成以下代码:
|
||||
当前生成器会产出:
|
||||
|
||||
```csharp
|
||||
// <auto-generated />
|
||||
#nullable enable
|
||||
|
||||
namespace YourNamespace;
|
||||
|
||||
partial class MainMenu
|
||||
{
|
||||
private void __BindNodeSignals_Generated()
|
||||
{
|
||||
_startButton.Pressed += OnStartButtonPressed;
|
||||
_settingsButton.Pressed += OnSettingsButtonPressed;
|
||||
_quitButton.Pressed += OnQuitButtonPressed;
|
||||
_startOreSpinBox.ValueChanged += OnStartOreValueChanged;
|
||||
}
|
||||
|
||||
private void __UnbindNodeSignals_Generated()
|
||||
{
|
||||
_startButton.Pressed -= OnStartButtonPressed;
|
||||
_settingsButton.Pressed -= OnSettingsButtonPressed;
|
||||
_quitButton.Pressed -= OnQuitButtonPressed;
|
||||
}
|
||||
_startOreSpinBox.ValueChanged -= OnStartOreValueChanged;
|
||||
}
|
||||
```
|
||||
|
||||
## 参数说明
|
||||
## 生命周期边界
|
||||
|
||||
`[BindNodeSignal]` 特性需要两个参数:
|
||||
### 它只生成辅助方法,不生成 `_Ready()` / `_ExitTree()`
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
|-----------------|--------|-----------------------------|
|
||||
| `nodeFieldName` | string | 目标节点字段名(使用 `nameof` 推荐) |
|
||||
| `signalName` | string | 目标节点上的 CLR 事件名(使用 `nameof`) |
|
||||
这是当前和 `[GetNode]` 最大的区别:
|
||||
|
||||
- `[GetNode]` 在缺少 `_Ready()` 时会补一个 override
|
||||
- `[BindNodeSignal]` 只生成 `__BindNodeSignals_Generated()` 和 `__UnbindNodeSignals_Generated()`
|
||||
|
||||
所以你需要自己决定在哪个生命周期里调用它们。
|
||||
|
||||
### 已有生命周期但没调用时会给 warning
|
||||
|
||||
如果类型已经定义了 `_Ready()` 或 `_ExitTree()`,但没有调用对应生成方法,当前会给出 warning,提醒你完成接线。
|
||||
|
||||
这意味着它更像“声明式订阅语法”,而不是“自动生命周期织入”。
|
||||
|
||||
## 当前契约
|
||||
|
||||
`[BindNodeSignal(nodeFieldName, signalName)]` 的两个参数都指向现有代码里的稳定符号:
|
||||
|
||||
- `nodeFieldName`:目标节点字段名
|
||||
- `signalName`:该节点类型上的 CLR event 名
|
||||
|
||||
最推荐的写法仍然是:
|
||||
|
||||
```csharp
|
||||
[BindNodeSignal("_startButton", "Pressed")] // 字符串字面量
|
||||
[BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))] // 推荐:nameof 表达式
|
||||
[BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))]
|
||||
```
|
||||
|
||||
## 高级用法
|
||||
这样字段或事件改名时,编译器能一起帮你更新。
|
||||
|
||||
### 带参数的事件处理
|
||||
## 当前会验证什么
|
||||
|
||||
处理带参数的事件(如 `SpinBox.ValueChanged`):
|
||||
生成器不是盲目拼字符串。按当前源码,它会在编译期验证:
|
||||
|
||||
- 方法必须是实例方法
|
||||
- `nodeFieldName` 必须能解析到当前类型里的实例字段
|
||||
- 该字段类型必须继承 `Godot.Node`
|
||||
- `signalName` 必须能解析到该字段类型上的 CLR event
|
||||
- 处理方法签名必须和 event delegate 兼容
|
||||
|
||||
例如:
|
||||
|
||||
- `Button.Pressed` 对应无参处理方法
|
||||
- `SpinBox.ValueChanged` 对应 `double` 参数
|
||||
|
||||
如果签名不匹配,会直接报错,而不是生成一个运行时才失败的订阅。
|
||||
|
||||
## 多重绑定
|
||||
|
||||
`BindNodeSignalAttribute` 允许重复标记在同一个方法上,所以一个处理方法可以绑定多个事件:
|
||||
|
||||
```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]` 特性结合使用:
|
||||
`ai-libs/CoreGrid` 里的 `GameplayHud`、`PauseMenu` 和 `OptionBrowser` 都在大量使用这种声明式绑定方式。
|
||||
|
||||
```csharp
|
||||
using GFramework.Godot.SourceGenerators.Abstractions;
|
||||
using Godot;
|
||||
## 与 GetNode 的协作边界
|
||||
|
||||
public partial class GameHud : Control
|
||||
{
|
||||
// 使用 GetNode 自动获取节点
|
||||
[GetNode]
|
||||
private Button _pauseButton = null!;
|
||||
`[BindNodeSignal]` 不负责拿到字段实例,只负责在字段已经可用的前提下做事件接线。
|
||||
|
||||
[GetNode]
|
||||
private ProgressBar _healthBar = null!;
|
||||
因此同类型同时使用时,顺序应该是:
|
||||
|
||||
[GetNode("UI/ScoreLabel")]
|
||||
private Label _scoreLabel = null!;
|
||||
1. `__InjectGetNodes_Generated()`
|
||||
2. `__BindNodeSignals_Generated()`
|
||||
3. 在 `_ExitTree()` 调用 `__UnbindNodeSignals_Generated()`
|
||||
|
||||
// 使用 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();
|
||||
}
|
||||
}
|
||||
```
|
||||
- 目标类型必须是顶层 `partial class`
|
||||
- 不支持嵌套类
|
||||
- 方法不能是 `static`
|
||||
- 节点字段必须存在且是实例字段
|
||||
- 节点字段类型必须继承 `Godot.Node`
|
||||
- 事件名必须是 CLR event,不是任意字符串
|
||||
- 如果你自己声明了 `__BindNodeSignals_Generated()` 或 `__UnbindNodeSignals_Generated()`,会触发命名冲突诊断
|
||||
|
||||
### 复杂事件处理场景
|
||||
## 什么时候适合用 `[BindNodeSignal]`
|
||||
|
||||
实现完整的 UI 事件处理:
|
||||
适合:
|
||||
|
||||
```csharp
|
||||
public partial class InventoryUI : Control
|
||||
{
|
||||
// 节点
|
||||
[GetNode]
|
||||
private ItemList _itemList = null!;
|
||||
- UI、菜单、HUD、面板类里按钮或输入事件很多
|
||||
- 你想把订阅/解绑语义放回方法声明旁边,而不是堆在 `_Ready()` / `_ExitTree()`
|
||||
- 你已经用 `[GetNode]` 或其他方式稳定拿到节点字段
|
||||
|
||||
[GetNode]
|
||||
private Button _useButton = null!;
|
||||
不适合:
|
||||
|
||||
[GetNode]
|
||||
private Button _dropButton = null!;
|
||||
- 事件目标需要在运行时动态决定
|
||||
- 你用的是 `Connect()` / `Disconnect()` 风格,而不是 CLR event
|
||||
- 你需要比“字段 + 事件名”更复杂的订阅条件
|
||||
|
||||
[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]` 会自动生成 `_Ready()` / `_ExitTree()`”
|
||||
- “它能处理所有 Godot signal 连接方式”
|
||||
- “有没有 `__UnbindNodeSignals_Generated()` 都无所谓”
|
||||
|
||||
[BindNodeSignal(nameof(_useButton), nameof(Button.Pressed))]
|
||||
private void OnUseButtonPressed()
|
||||
{
|
||||
UseSelectedItem();
|
||||
}
|
||||
当前更准确的理解是:
|
||||
|
||||
[BindNodeSignal(nameof(_dropButton), nameof(Button.Pressed))]
|
||||
private void OnDropButtonPressed()
|
||||
{
|
||||
DropSelectedItem();
|
||||
}
|
||||
- 它只生成成对的绑定/解绑辅助方法
|
||||
- 当前设计面向 CLR event,不自动调用 `Connect()` / `Disconnect()`
|
||||
- 如果要避免节点退出后残留订阅,应在 `_ExitTree()` 中显式解绑
|
||||
|
||||
[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.Core.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)
|
||||
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`
|
||||
|
||||
@ -1,66 +1,41 @@
|
||||
---
|
||||
title: GetNode 生成器
|
||||
description: 说明 [GetNode] 当前生成什么、路径如何推断,以及 _Ready 生命周期里的接入边界。
|
||||
---
|
||||
|
||||
# GetNode 生成器
|
||||
|
||||
> 自动生成 Godot 节点获取逻辑,简化节点引用代码
|
||||
`[GetNode]` 用来把 Godot 节点查找样板收敛到生成器里。它只处理“字段如何取到节点”,不负责事件订阅,也不负责其他运行时装配。
|
||||
|
||||
## 概述
|
||||
## 当前包关系
|
||||
|
||||
GetNode 生成器为标记了 `[GetNode]` 特性的字段自动生成 Godot 节点获取代码,无需手动调用 `GetNode<T>()` 方法。这在处理复杂
|
||||
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
|
||||
// <auto-generated />
|
||||
#nullable enable
|
||||
|
||||
namespace YourNamespace;
|
||||
|
||||
partial class PlayerHud
|
||||
{
|
||||
private void __InjectGetNodes_Generated()
|
||||
{
|
||||
_healthLabel = GetNode<global::Godot.Label>("%HealthLabel");
|
||||
_manaBar = GetNode<global::Godot.ProgressBar>("%ManaBar");
|
||||
_scoreLabel = GetNode<global::Godot.Label>("ScoreContainer/ScoreValue");
|
||||
_leftContainer = GetNode<global::Godot.HBoxContainer>("%LeftContainer");
|
||||
m_rightContainer = GetNode<global::Godot.HBoxContainer>("%RightContainer");
|
||||
}
|
||||
|
||||
partial void OnGetNodeReadyGenerated();
|
||||
@ -70,427 +45,154 @@ partial class PlayerHud
|
||||
__InjectGetNodes_Generated();
|
||||
OnGetNodeReadyGenerated();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 配置选项
|
||||
这个行为来自当前生成器测试,不是文档约定。
|
||||
|
||||
### 节点查找模式
|
||||
## 当前路径推断规则
|
||||
|
||||
通过 `Lookup` 参数控制节点查找方式:
|
||||
### 没写路径时
|
||||
|
||||
如果 `[GetNode]` 没有显式路径,当前默认按字段名推导唯一名路径:
|
||||
|
||||
- `_leftContainer` -> `%LeftContainer`
|
||||
- `m_rightContainer` -> `%RightContainer`
|
||||
|
||||
也就是说,默认不是普通相对路径,而是 Godot 的 `%Name` 唯一名语法。
|
||||
|
||||
### 显式路径优先
|
||||
|
||||
```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!; // 错误
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 正确
|
||||
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<Label>("%HealthLabel");
|
||||
_manaBar = GetNode<ProgressBar>("%ManaBar");
|
||||
_scoreLabel = GetNode<Label>("ScoreContainer/ScoreValue");
|
||||
}
|
||||
|
||||
// ✅ 推荐:使用 [GetNode] 特性
|
||||
[GetNode]
|
||||
private Label _healthLabel = null!;
|
||||
|
||||
[GetNode]
|
||||
private ProgressBar _manaBar = null!;
|
||||
|
||||
[GetNode("ScoreContainer/ScoreValue")]
|
||||
private Label _scoreLabel = null!;
|
||||
```
|
||||
|
||||
显式路径会直接进入生成结果,不再按字段名推断。
|
||||
|
||||
## `Lookup` 与 `Required` 的当前语义
|
||||
|
||||
### `Lookup`
|
||||
|
||||
`GetNodeAttribute.Lookup` 支持 4 个模式:
|
||||
|
||||
- `Auto`
|
||||
- `UniqueName`
|
||||
- `RelativePath`
|
||||
- `AbsolutePath`
|
||||
|
||||
对文档来说,最关键的结论是:
|
||||
|
||||
- `Auto` 在未给路径时默认走唯一名推断
|
||||
- 显式路径会结合 `Lookup` 决定最终生成的字符串
|
||||
|
||||
### `Required`
|
||||
|
||||
默认 `Required = true`,生成器会调用 `GetNode<T>()`:
|
||||
|
||||
```csharp
|
||||
[GetNode]
|
||||
private Label _title = null!;
|
||||
```
|
||||
|
||||
如果设为 `false`,生成器会改用 `GetNodeOrNull<T>()`:
|
||||
|
||||
```csharp
|
||||
[GetNode(Required = false, Lookup = NodeLookupMode.RelativePath)]
|
||||
private HBoxContainer? _rightContainer;
|
||||
```
|
||||
|
||||
当前生成结果会是:
|
||||
|
||||
```csharp
|
||||
_rightContainer = GetNodeOrNull<global::Godot.HBoxContainer>("RightContainer");
|
||||
```
|
||||
|
||||
所以可选节点最好同时用可空字段类型表达你的意图。
|
||||
|
||||
## 生命周期边界
|
||||
|
||||
### 没有 `_Ready()` 时
|
||||
|
||||
生成器会补:
|
||||
|
||||
- `__InjectGetNodes_Generated()`
|
||||
- `partial void OnGetNodeReadyGenerated()`
|
||||
- 一个 `public override void _Ready()`
|
||||
|
||||
`OnGetNodeReadyGenerated()` 只在这种“生成器自己补 `_Ready()`”的路径里出现。
|
||||
|
||||
### 已经有 `_Ready()` 时
|
||||
|
||||
如果类型已经实现了 `_Ready()`,生成器不会覆盖它,也不会再额外生成 `OnGetNodeReadyGenerated()`。你必须自己调用:
|
||||
|
||||
```csharp
|
||||
public override void _Ready()
|
||||
{
|
||||
__InjectGetNodes_Generated();
|
||||
}
|
||||
```
|
||||
|
||||
## 相关文档
|
||||
如果 `_Ready()` 存在但没有调用生成方法,当前会给出 warning,提醒你手动接入。
|
||||
|
||||
- [Source Generators 概述](./index)
|
||||
- [BindNodeSignal 生成器](./bind-node-signal-generator)
|
||||
- [ContextAware 生成器](./context-aware-generator)
|
||||
- [Godot 节点文档](https://docs.godotengine.org/en/stable/classes/class_node.html)
|
||||
## 当前强约束
|
||||
|
||||
这些约束都直接来自生成器源码和测试:
|
||||
|
||||
- 目标类型必须是顶层 `partial class`
|
||||
- 不支持嵌套类
|
||||
- 字段必须是实例字段
|
||||
- 字段不能是 `readonly`
|
||||
- 字段类型必须继承 `Godot.Node`
|
||||
- 如果无法从字段名或显式参数推断出路径,会报错
|
||||
- 如果你自己定义了 `__InjectGetNodes_Generated()`,会触发命名冲突诊断
|
||||
|
||||
## 与 BindNodeSignal 的配合顺序
|
||||
|
||||
如果同一个类型同时用了 `[GetNode]` 和 `[BindNodeSignal]`,当前推荐顺序是:
|
||||
|
||||
```csharp
|
||||
public override void _Ready()
|
||||
{
|
||||
__InjectGetNodes_Generated();
|
||||
__BindNodeSignals_Generated();
|
||||
}
|
||||
```
|
||||
|
||||
先注入节点,再绑定事件;否则 `BindNodeSignal` 对应的字段还没完成解析。
|
||||
|
||||
这也是 `ai-libs/CoreGrid` 里项目侧节点类的实际用法。
|
||||
|
||||
## 什么时候适合用 `[GetNode]`
|
||||
|
||||
适合:
|
||||
|
||||
- 节点字段很多,`GetNode<T>()` 样板明显重复
|
||||
- 你希望把“字段名到节点路径”的约定收敛到声明式特性
|
||||
- 你在 Godot `Control`、`Node`、`CanvasLayer` 等项目侧类型上频繁访问子节点
|
||||
|
||||
不适合:
|
||||
|
||||
- 目标不是 `Godot.Node`
|
||||
- 节点路径完全动态,必须在运行时决定
|
||||
- 你需要更复杂的节点查找策略,而不是字段级静态描述
|
||||
|
||||
## 与旧写法的边界
|
||||
|
||||
下面这些旧理解已经不准确:
|
||||
|
||||
- “`[GetNode]` 总会自动帮你改写 `_Ready()`”
|
||||
- “不管是否已有 `_Ready()`,都会生成 `OnGetNodeReadyGenerated()`”
|
||||
- “可选节点只是文档建议,生成结果不会变”
|
||||
|
||||
当前更准确的理解是:
|
||||
|
||||
- 只有缺少 `_Ready()` 时才会自动补 override
|
||||
- `OnGetNodeReadyGenerated()` 只存在于自动补 `_Ready()` 的路径
|
||||
- `Required = false` 会真实切换到 `GetNodeOrNull<T>()`
|
||||
|
||||
## 推荐阅读
|
||||
|
||||
1. [/zh-CN/source-generators/bind-node-signal-generator](./bind-node-signal-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`
|
||||
|
||||
@ -1,52 +1,64 @@
|
||||
---
|
||||
title: Godot 项目元数据生成器
|
||||
description: 说明 project.godot 当前会生成什么、何时生效,以及 AutoLoad 和 Input Action 的映射边界。
|
||||
---
|
||||
|
||||
# Godot 项目元数据生成器
|
||||
|
||||
> 从 `project.godot` 生成 AutoLoad 与 Input Action 的强类型访问入口。
|
||||
`GodotProjectMetadataGenerator` 读取 `project.godot`,把 Godot 工程级配置转成稳定的编译期入口。
|
||||
|
||||
## 概述
|
||||
当前只覆盖两类信息:
|
||||
|
||||
`GFramework.Godot.SourceGenerators` 会读取 Godot 项目根目录下的 `project.godot`,并把其中最常用的项目级元数据暴露为稳定的编译期
|
||||
API。
|
||||
- `[autoload]` 段生成 `GFramework.Godot.Generated.AutoLoads`
|
||||
- `[input]` 段生成 `GFramework.Godot.Generated.InputActions`
|
||||
|
||||
当前覆盖:
|
||||
它不处理场景节点注入,也不处理节点事件绑定。这两部分分别由 `/zh-CN/source-generators/get-node-generator` 和
|
||||
`/zh-CN/source-generators/bind-node-signal-generator` 负责。
|
||||
|
||||
- `[autoload]` 段:生成 `GFramework.Godot.Generated.AutoLoads`
|
||||
- `[input]` 段:生成 `GFramework.Godot.Generated.InputActions`
|
||||
## 当前包关系
|
||||
|
||||
这项能力的目标不是替代场景级生成器,而是把 Godot 工程配置和 C# 代码之间的字符串约定收敛到编译期。
|
||||
- 特性来源:`GFramework.Godot.SourceGenerators.Abstractions`
|
||||
- 生成器实现:`GFramework.Godot.SourceGenerators`
|
||||
- 运行时依赖:`GFramework.Godot`
|
||||
- 消费侧生成命名空间:`GFramework.Godot.Generated`
|
||||
|
||||
## 接入方式
|
||||
## 最小接入路径
|
||||
|
||||
### NuGet 引用
|
||||
|
||||
当项目通过 NuGet 引用 `GeWuYou.GFramework.Godot.SourceGenerators` 时,生成器会默认把项目根目录下的 `project.godot` 加入
|
||||
`AdditionalFiles`。
|
||||
常规 Godot C# 项目安装 `GeWuYou.GFramework.Godot.SourceGenerators` 后,包内 `targets` 会自动做两件事:
|
||||
|
||||
如需覆盖默认路径,可以设置:
|
||||
|
||||
- 可以改成项目根目录下的其他相对路径
|
||||
- 文件名必须仍然是 `project.godot`,否则生成器会给出警告并忽略该文件
|
||||
1. 注入 analyzer
|
||||
2. 如果项目根目录存在 `project.godot`,把它加入 `AdditionalFiles`
|
||||
|
||||
```xml
|
||||
<PropertyGroup>
|
||||
<GFrameworkGodotProjectFile>Config/project.godot</GFrameworkGodotProjectFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="GeWuYou.GFramework.Godot.SourceGenerators"
|
||||
Version="x.y.z"
|
||||
PrivateAssets="all"
|
||||
ExcludeAssets="runtime" />
|
||||
</ItemGroup>
|
||||
```
|
||||
|
||||
### 仓库内直接引用生成器
|
||||
|
||||
如果你通过 `ProjectReference(OutputItemType=Analyzer)` 直接引用生成器项目,则需要手动加入:
|
||||
如果你通过 `ProjectReference(OutputItemType=Analyzer)` 直接引用生成器项目,需要自己把 `project.godot` 放进
|
||||
`AdditionalFiles`:
|
||||
|
||||
```xml
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\GFramework.Godot.SourceGenerators\GFramework.Godot.SourceGenerators.csproj"
|
||||
OutputItemType="Analyzer"
|
||||
ReferenceOutputAssembly="false" />
|
||||
<AdditionalFiles Include="project.godot" />
|
||||
</ItemGroup>
|
||||
```
|
||||
|
||||
## AutoLoad 访问层
|
||||
## 当前会生成什么
|
||||
|
||||
### 基础行为
|
||||
### AutoLoad 入口
|
||||
|
||||
假设 `project.godot` 中声明了:
|
||||
假设 `project.godot` 中有:
|
||||
|
||||
```ini
|
||||
[autoload]
|
||||
@ -66,33 +78,14 @@ if (AutoLoads.TryGetAudioBus(out var audioBus))
|
||||
}
|
||||
```
|
||||
|
||||
- 对于能唯一映射到 C# 节点类型的条目,属性会是强类型的
|
||||
- 对于无法映射或对应非 C# 脚本的条目,属性会退化为 `Godot.Node`
|
||||
- 生成器通过 `Godot.Engine.GetMainLoop()` 与当前 `SceneTree.Root` 解析 `/root/<AutoLoadName>` 节点
|
||||
当前输出同时包含:
|
||||
|
||||
### 显式映射
|
||||
- `AutoLoads.<Name>`
|
||||
- `AutoLoads.TryGet<Name>(out TNode? value)`
|
||||
|
||||
当 AutoLoad 名称无法仅靠类名唯一推断时,可以使用 `[AutoLoad]` 明确指定:
|
||||
这些访问器最终都通过当前 `SceneTree.Root` 解析 `/root/<AutoLoadName>`。
|
||||
|
||||
```csharp
|
||||
using GFramework.Godot.SourceGenerators.Abstractions;
|
||||
using Godot;
|
||||
|
||||
[AutoLoad("GameServices")]
|
||||
public partial class GameServices : Node
|
||||
{
|
||||
}
|
||||
```
|
||||
|
||||
规则如下:
|
||||
|
||||
- 显式 `[AutoLoad]` 映射优先于隐式类名推断
|
||||
- 标记了 `[AutoLoad]` 的类型必须继承 `Godot.Node`
|
||||
- 若多个类型映射到同一个 AutoLoad,生成器会报告诊断,并退化为 `Godot.Node` 访问器,直到映射唯一
|
||||
|
||||
## Input Action 常量
|
||||
|
||||
### 基础行为
|
||||
### Input Action 常量
|
||||
|
||||
假设 `project.godot` 中有:
|
||||
|
||||
@ -114,59 +107,114 @@ if (Input.IsActionJustPressed(InputActions.MoveUp))
|
||||
}
|
||||
```
|
||||
|
||||
转换规则:
|
||||
这部分只生成稳定字符串常量,不会替你封装 `Input` 调用。
|
||||
|
||||
- `move_up` -> `MoveUp`
|
||||
- `ui_cancel` -> `UiCancel`
|
||||
- 非法字符会被清理后再转换为 PascalCase
|
||||
- 如果多个动作名落到同一个标识符,生成器会追加稳定数字后缀,例如 `MoveUp_2`
|
||||
## AutoLoad 类型推断的当前规则
|
||||
|
||||
## 与现有 Godot 生成器的关系
|
||||
### 优先级顺序
|
||||
|
||||
这项能力和现有的场景级生成器是互补的:
|
||||
当前映射顺序是:
|
||||
|
||||
- `AutoLoads` / `InputActions` 解决的是项目级元数据访问
|
||||
- `[GetNode]` 解决的是场景节点引用注入
|
||||
- `[BindNodeSignal]` 解决的是节点事件订阅样板
|
||||
1. 显式 `[AutoLoad("Name")]`
|
||||
2. 按 C# 类型名与 AutoLoad 名称做唯一匹配
|
||||
3. 无法唯一确定时退化为 `Godot.Node`
|
||||
|
||||
推荐组合方式:
|
||||
例如:
|
||||
|
||||
```csharp
|
||||
using GFramework.Godot.Generated;
|
||||
using GFramework.Godot.SourceGenerators.Abstractions;
|
||||
using Godot;
|
||||
|
||||
public partial class MainHud : Control
|
||||
[AutoLoad("GameServices")]
|
||||
public partial class GameServices : Node
|
||||
{
|
||||
[GetNode]
|
||||
private Button _startButton = null!;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
__InjectGetNodes_Generated();
|
||||
|
||||
if (Input.IsActionPressed(InputActions.UiCancel))
|
||||
{
|
||||
}
|
||||
|
||||
var services = AutoLoads.GameServices;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这类显式映射优先于按类名推断。
|
||||
|
||||
### 什么时候会退化成 `Godot.Node`
|
||||
|
||||
以下情况不会中断全部生成,但会把对应入口退化成 `Godot.Node` 并报告诊断:
|
||||
|
||||
- 多个类型显式映射到同一个 AutoLoad
|
||||
- 不同命名空间下出现同名 `Node` 类型,导致隐式推断不唯一
|
||||
- 对应条目实际无法唯一绑定到一个 C# 节点类型
|
||||
|
||||
## `project.godot` 文件约束
|
||||
|
||||
### 可以改路径,不能改文件名
|
||||
|
||||
NuGet `targets` 支持通过 `GFrameworkGodotProjectFile` 改相对路径:
|
||||
|
||||
```xml
|
||||
<PropertyGroup>
|
||||
<GFrameworkGodotProjectFile>Config/project.godot</GFrameworkGodotProjectFile>
|
||||
</PropertyGroup>
|
||||
```
|
||||
|
||||
但当前生成器按文件名识别 `project.godot`,所以:
|
||||
|
||||
- `Config/project.godot` 可以
|
||||
- `Config/game.project` 不可以
|
||||
|
||||
如果文件名不是 `project.godot`,`targets` 会给出 warning,生成器也会忽略该文件。
|
||||
|
||||
### 缺文件或空节时不会生成任何代码
|
||||
|
||||
按当前测试,下面几种情况都不会产出源码,也不会报告额外诊断:
|
||||
|
||||
- 没有把 `project.godot` 传进 `AdditionalFiles`
|
||||
- `project.godot` 是空文件
|
||||
- `[autoload]` / `[input]` 只有空节,没有有效条目
|
||||
|
||||
## 标识符与重复条目的当前语义
|
||||
|
||||
### 标识符冲突
|
||||
|
||||
如果不同名字清洗后落到同一个 C# 标识符,生成器会追加稳定后缀并报告诊断,例如:
|
||||
|
||||
- `move_up` -> `MoveUp`
|
||||
- `move-up` -> `MoveUp_2`
|
||||
|
||||
AutoLoad 名称也遵循同样的冲突处理策略。
|
||||
|
||||
### 重复条目
|
||||
|
||||
如果同一个 `project.godot` 里重复声明同名 AutoLoad 或 Input Action,当前行为是:
|
||||
|
||||
- 报告诊断
|
||||
- 只保留第一条声明参与生成
|
||||
|
||||
这和“冲突后同时生成多个重名成员”不是一回事。
|
||||
|
||||
## 与场景级生成器的边界
|
||||
|
||||
这项能力解决的是“项目级元数据入口”:
|
||||
|
||||
- `AutoLoads`
|
||||
- `InputActions`
|
||||
|
||||
场景级样板仍然需要其他生成器:
|
||||
|
||||
- 节点字段注入:`[GetNode]`
|
||||
- 节点 CLR event 订阅:`[BindNodeSignal]`
|
||||
|
||||
在 `ai-libs/CoreGrid` 中,这三类能力是并行使用的:`project.godot` 负责 AutoLoad / Input Action,具体 UI 或场景节点再通过
|
||||
`[GetNode]` 和 `[BindNodeSignal]` 处理。
|
||||
|
||||
## 诊断与约束
|
||||
|
||||
当前会重点报告以下问题:
|
||||
当前最值得记住的约束有这些:
|
||||
|
||||
- `[AutoLoad]` 标记在非 `Godot.Node` 类型上
|
||||
- 多个类型映射到同一个 AutoLoad 名称
|
||||
- 不同 AutoLoad 名称或 Input Action 名称在清洗后发生标识符冲突
|
||||
- `project.godot` 内部重复声明同名 AutoLoad 或 Input Action
|
||||
- `[AutoLoad]` 只能标在继承 `Godot.Node` 的类型上
|
||||
- 显式或隐式 AutoLoad 映射不唯一时,会退化为 `Godot.Node`
|
||||
- 标识符冲突会追加稳定后缀,而不是覆盖已有成员
|
||||
- 重复条目只保留第一条声明
|
||||
|
||||
这些诊断的目的不是阻断所有生成,而是在可能的情况下保留稳定输出,同时把不确定性显式暴露出来。
|
||||
## 推荐阅读
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [GetNode 生成器](./get-node-generator)
|
||||
- [BindNodeSignal 生成器](./bind-node-signal-generator)
|
||||
- [Godot 集成教程](../tutorials/godot-integration)
|
||||
1. [/zh-CN/source-generators/get-node-generator](./get-node-generator.md)
|
||||
2. [/zh-CN/source-generators/bind-node-signal-generator](./bind-node-signal-generator.md)
|
||||
3. [/zh-CN/tutorials/godot-integration](../tutorials/godot-integration.md)
|
||||
4. `GFramework.Godot.SourceGenerators/README.md`
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user