Merge pull request #270 from GeWuYou/docs/sdk-update-documentation

Docs/sdk update documentation
This commit is contained in:
gewuyou 2026-04-22 14:14:54 +08:00 committed by GitHub
commit b2a5555c75
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 2050 additions and 6261 deletions

View File

@ -7,19 +7,26 @@
## 当前恢复点
- 恢复点编号:`DOCUMENTATION-GOVERNANCE-REFRESH-RP-010`
- 恢复点编号:`DOCUMENTATION-GOVERNANCE-REFRESH-RP-017`
- 当前阶段:`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 相关生成器页面核对
- `docs/zh-CN/godot/index.md` 已改成源码优先的模块 landing page不再把 `GetNodeX``CreateSignalBuilder``InstallGodotModule(...)` 写成默认入口
- `docs/zh-CN/godot/architecture.md` 已改成当前锚点生命周期、模块挂接顺序和接口边界说明,不再沿用旧版 `.Wait()` 叙述
- `docs/zh-CN/godot/scene.md``docs/zh-CN/godot/ui.md` 已按当前 factory / registry / root / source-generator wiring 重写完成
- `docs/zh-CN/godot/signal.md` 已按当前 `Signal(...)` / `SignalBuilder` / `[BindNodeSignal]` 分工重写完成
- `docs/zh-CN/godot/extensions.md` 已按当前 `GodotPathExtensions``NodeExtensions``SignalFluentExtensions``UnRegisterExtension` 重写完成
- `docs/zh-CN/godot/logging.md` 已按当前 provider / factory / logger 结构、Godot 控制台输出语义与 CoreGrid 架构接线重写完成
- 下一轮高优先级工作转为评估 Godot 栏目当前 active 恢复点是否可以收口并迁入 archive
## 当前状态摘要
- 文档治理规则已收口到仓库规范README、站点入口与采用链路不再依赖旧文档自证
- 高优先级模块入口与 `core` 关键专题页已回到可作为默认导航入口的状态,本轮计划中的 `core` 剩余高风险页面已完成收口
- 当前主题仍是 active topic因为 `source-generators` 栏目下的 Godot 相关页面仍可能包含与实现漂移的旧内容,且统一 skill 还需要在该场景上继续落地使用
- 高优先级模块入口、`core` 关键专题页与 `tutorials/godot-integration.md` 已回到“以源码 / 测试 / README 为准”的状态
- `docs/zh-CN/godot/index.md``architecture.md``scene.md``ui.md` 已完成当前实现收口
- 当前主题仍是 active topic因为 Godot 栏目本轮已完成 `logging.md` 收口,但仍需确认是否可以把当前阶段历史迁入
`archive/`,并在下一次推送后跟进 PR #268 的 review 线程收敛情况
## 当前活跃事实
@ -57,6 +64,41 @@
- `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()`
- `docs/zh-CN/source-generators/auto-register-exported-collections-generator.md` 已补齐 frontmatter并改成“成员形状、registry 匹配规则、null-skip 行为、编译期诊断与 CoreGrid 真实采用路径”的结构,
明确生成器依赖的是实例可读集合成员与可读 registry 成员,不要求成员必须带 `[Export]`
- `docs/zh-CN/tutorials/godot-integration.md` 已改成“包关系、`project.godot` 接线、`[GetNode]` / `[BindNodeSignal]` 协作顺序、运行时扩展边界、迁移提醒”的结构,
不再把 `GetNodeX``CreateSignalBuilder``AbstractGodotModule` 默认化叙述为当前推荐路径
- `docs/zh-CN/tutorials/index.md` 中 Godot 教程入口摘要已同步改成“项目级配置 + 生成器协作 + 生命周期边界”,不再继续宣传对象池 / 性能优化式旧范围
- `docs/zh-CN/godot/index.md` 已改成“模块定位、包关系、最小接入路径、关键入口、当前边界”的 landing page 结构,并明确把
`[GetNode]``[BindNodeSignal]``AutoLoads``InputActions` 归到 `GFramework.Godot.SourceGenerators`
- `docs/zh-CN/godot/architecture.md` 已改成“何时继承 `AbstractArchitecture`、何时使用 `InstallGodotModule(...)`、锚点生命周期、
`IGodotModule` 契约边界”的结构,不再把 `OnPhase(...)` / `OnArchitecturePhase(...)` 写成稳定自动广播
- 本轮再次执行 `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh` 校验 `godot/index.md`
`godot/architecture.md`,并执行 `cd docs && bun run build`,站点构建继续通过
- `docs/zh-CN/godot/scene.md` 已改成“公开入口、factory 实际行为、项目侧 router/root wiring、`[AutoScene]` 最小接入路径、
当前边界”的结构,明确当前没有 `GodotSceneRouter`,且 `GodotSceneFactory` 会在 provider 缺失时回退到
`SceneBehaviorFactory`
- `docs/zh-CN/godot/ui.md` 已改成“公开入口、layer behavior 语义、项目侧 router/root wiring、`[AutoUiPage]` 最小接入路径、
输入与暂停边界”的结构,明确当前没有 `GodotUiRouter`,且 `GodotUiFactory` 仍强制要求 `IUiPageBehaviorProvider`
- 本轮已执行 `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/scene.md`
`bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/ui.md`,两页聚焦校验通过
- `docs/zh-CN/godot/signal.md` 已改成“当前公开入口、动态绑定最小接入路径、与 `[BindNodeSignal]` 的分工、当前边界”的结构,
不再沿用旧 `CreateSignalBuilder(...)` / builder-pattern 教程式长篇叙述
- `docs/zh-CN/godot/extensions.md` 已改成“真实扩展分组、Node 辅助成员表、`UnRegisterWhenNodeExitTree(...)` 生命周期边界、
当前边界”的结构,不再把扩展层写成覆盖所有 Godot 开发动作的万能工具箱
- 本轮已执行 `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/signal.md`
`bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/extensions.md`,两页聚焦校验通过
- 本轮再次执行 `cd docs && bun run build` 通过,当前 Godot signal / extensions 页面改动没有破坏站点构建
- `docs/zh-CN/godot/logging.md` 已改成“当前公开入口、最小接入路径、Godot 控制台输出语义、`[Log]` 协作边界、当前限制”的结构,
不再把直接改写 `LoggerFactoryResolver.Provider``AbstractGodotModule` 或 Godot 专用日志 API 写成默认接入模型
- 本轮已执行 `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/logging.md`
`cd docs && bun run build`logging 页面聚焦校验与站点构建继续通过
- `.agents/skills/gframework-doc-refresh/SKILL.md` 已改成标准 YAML frontmatter skill并明确支持模块输入、证据顺序、输出优先级与验证步骤
- `.agents/skills/gframework-doc-refresh/SKILL.md``description` 已加引号,修复 `Recommended command:` 中冒号导致的
invalid YAML skill 加载警告
@ -74,7 +116,12 @@
- 旧专题页示例失真风险:`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``auto-register-exported-collections-generator.md`
已完成收口;
继续按源码、测试、`*.csproj``ai-libs/` 下已验证参考实现核对剩余 Godot 相关页面,不把旧文档当事实来源
- Godot 栏目归档过早风险:虽然 `logging.md` 已完成收口,但如果在推送前就把当前阶段过早归档,后续 review 跟进会缺少
清晰的 active 恢复入口
- 缓解措施:先保留当前 topic 为 active待确认本轮页面集与 PR #268 的 review 跟进节奏后,再决定是否迁入 `archive/`
- 采用路径误导风险:根聚合包与模块边界若再次被写错,会继续误导消费者的包选择
- 缓解措施:保持“源码与包关系优先”的证据顺序,改动采用说明时同步核对包依赖与生成器 wiring
- 模块映射不全风险:统一 skill 若遗漏模块别名、测试项目或 docs 栏目映射,会让后续扫描阶段直接失焦
@ -115,10 +162,25 @@
- `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`
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/source-generators/auto-register-exported-collections-generator.md`
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/tutorials/godot-integration.md`
- `rg -n "GetNodeX|CreateSignalBuilder|GodotGameArchitecture|AbstractGodotModule|InstallGodotModule\(|GFramework\.Godot\.Pool" docs/zh-CN/godot docs/zh-CN/tutorials -S`
- `cd docs && bun run build`
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/index.md`
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/architecture.md`
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/scene.md`
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/ui.md`
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/signal.md`
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/extensions.md`
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/logging.md`
- `rg -n "GodotSceneRouter|GodotUiRouter|CreateSignalBuilder|GetNodeX|InstallGodotModule\(" docs/zh-CN/godot -S`
- `cd docs && bun run build`
## 下一步
1. 继续核对 Godot 相关生成器页面,优先处理 `godot-project-generator.md``get-node-generator.md`
`bind-node-signal-generator.md`,优先用 `gframework-doc-refresh` 的模块扫描结果驱动判断
2. 下一次推送后先重新执行 `$gframework-pr-review`,确认 PR #268 的 CodeRabbit / Greptile open thread 是否按预期收敛
3. 再继续确认 `project.godot``AutoLoad` / `InputActions``GetNode` / `BindNodeSignal` 示例仍与当前包关系和生成器入口一致
1. 评估当前 Godot 栏目页面集是否已足够稳定,决定是否把本阶段 active 恢复点收口并迁入 `archive/`
2. 如需继续保持 active优先精简 tracking / trace只保留归档决策、当前风险与下一次 PR follow-up 入口
3. 下一次推送后重新执行 `$gframework-pr-review`,确认 PR #268 的 CodeRabbit / Greptile open thread 是否按预期收敛

View File

@ -2,7 +2,7 @@
## 2026-04-22
### 当前恢复点RP-010
### 当前恢复点RP-017
- 本轮从 PR #268 的最新 review 数据恢复未发现失败检查CTRF 报告显示 2139 个测试全部通过
- 本轮复核确认当前 PR 的 latest-head open thread 同时来自 `coderabbitai[bot]``greptile-apps[bot]`
@ -16,6 +16,37 @@
- `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]` 组合使用时应先注入节点再绑定事件
- 本轮继续收口 `auto-register-exported-collections-generator.md`,补齐 frontmatter并把“导出集合”纠正为“实例可读集合成员 + registry 成员 + 单参数实例方法”的真实契约
- 本轮已重写 `docs/zh-CN/tutorials/godot-integration.md`,把内容收口为“包关系、`project.godot` 接线、`[GetNode]` /
`[BindNodeSignal]` 协作顺序、运行时扩展边界、迁移提醒”,不再把旧 Godot API 列表当事实来源
- `docs/zh-CN/tutorials/index.md` 的 Godot 教程入口摘要已同步改成当前采用路径,避免入口页继续把教程描述成对象池 / 性能优化总览
- 本轮已重写 `docs/zh-CN/godot/index.md`,改成“模块定位、包关系、最小接入路径、关键入口、当前边界”的 landing page 结构,
明确把 `[GetNode]``[BindNodeSignal]``AutoLoads``InputActions` 收口到 `GFramework.Godot.SourceGenerators`
- 本轮已重写 `docs/zh-CN/godot/architecture.md`,改成“锚点生命周期、`InstallGodotModule(...)` 执行顺序、`IGodotModule`
契约边界”的结构,不再沿用旧版 `.Wait()` 和自动阶段广播叙述
- 本轮已重写 `docs/zh-CN/godot/scene.md`把内容收口为“公开入口、factory 真实行为、项目侧 router/root wiring、
`ISceneBehaviorProvider``[AutoScene]` 的真实关系、当前边界”,不再继续虚构 `GodotSceneRouter`
- 本轮已重写 `docs/zh-CN/godot/ui.md`把内容收口为“公开入口、layer behavior 语义、项目侧 router/root wiring、
`IUiPageBehaviorProvider``[AutoUiPage]` 的真实关系、输入与暂停边界”,不再继续虚构 `GodotUiRouter`
- 本轮额外确认 Godot Scene / UI 的关键差异:`GodotSceneFactory` 在 provider 缺失时会回退到 `SceneBehaviorFactory`
`GodotUiFactory` 仍会在缺失 `IUiPageBehaviorProvider` 时直接抛异常;这已写入两页文档,避免继续把两者描述成同一种接入模型
- 本轮已重写 `docs/zh-CN/godot/signal.md`,把内容收口为“当前公开入口、动态绑定最小接入路径、与 `[BindNodeSignal]`
的分工、当前边界”,明确当前入口是 `Signal(...)` 而不是旧 `CreateSignalBuilder(...)`
- 本轮已重写 `docs/zh-CN/godot/extensions.md`,把内容收口为“真实扩展分组、`NodeExtensions` 实际成员、`UnRegisterWhenNodeExitTree(...)`
生命周期边界、当前边界”,不再继续宣称存在覆盖所有 Godot 场景的万能扩展层
- 本轮复核 `ai-libs/CoreGrid` 的动态绑定用法后,明确把 fluent API 定位为“动态对象 / 动态 signal 的运行时连接”,而把静态控件绑定继续归到
`[BindNodeSignal]` 生成器链路
- 本轮已重写 `docs/zh-CN/godot/logging.md`,把内容收口为“当前 provider / factory / logger 结构、最小接入路径、
Godot 控制台输出语义、`[Log]` 协作边界、当前限制”,不再把直接改全局 provider 或 `AbstractGodotModule` 写成默认采用路径
- 本轮额外复核 `GFramework.Godot/Logging/*.cs``GFramework.Core.Abstractions/Logging/LoggerFactoryResolver.cs`
`GFramework.Core/Logging/CachedLoggerFactory.cs``ai-libs/CoreGrid/global/GameEntryPoint.cs`,确认当前推荐接法应以
`ArchitectureConfiguration.LoggerProperties.LoggerFactoryProvider` 为主,而不是先写 `LoggerFactoryResolver.Provider = ...`
### 当前决策
@ -23,6 +54,20 @@
- `scene.md``ui.md` 的集成说明除目录布局外,也要保证标题层级能真实反映采用路径语义
- `gframework-pr-review` 继续以 latest-head unresolved thread 为主信号,同时显式声明支持的 AI reviewer 名单,避免 skill
声明与实际抓取能力再次漂移
- `Godot.SourceGenerators` 专题页继续采用“源码 / 测试 / README 优先,`ai-libs/` 只补消费者 wiring”的证据顺序
- `BindNodeSignal` 页面明确记录“当前不自动生成 `_Ready()` / `_ExitTree()`”,避免继续把它写成自动生命周期织入器
- `auto-register-exported-collections` 页面明确区分“运行时 null 时跳过注册”和“配置错误时编译期报错”,避免旧文档把两类边界混为一谈
- `godot-integration.md` 已重新成为可用的采用路径入口;后续 Godot 文档收口应优先处理 `godot/index.md``godot/architecture.md`
- `godot/index.md``godot/architecture.md` 现在都必须维持“运行时包与生成器包分边界”的写法,不能再把场景注入和项目元数据生成写回
`GFramework.Godot` 运行时契约
- `scene.md` 已明确记录“项目侧 router + Godot factory/registry/root”这一分工后续不要再把 router 包装回
`GFramework.Godot` 运行时
- `ui.md` 已明确记录 `Page` 必须走 `PushAsync` / `ReplaceAsync``Show(..., UiLayer.Page)` 在当前实现中会抛异常;
后续不要再把所有 UI 入口重新写回统一 `Show(...)`
- `signal.md` 已明确为 `Signal(...)` / `SignalBuilder` 的轻量 fluent 包装说明页,不再继续混入生成器职责
- `extensions.md` 已明确限制在 `GodotPathExtensions``NodeExtensions``SignalFluentExtensions``UnRegisterExtension`
这四组当前存在的扩展
- `logging.md` 已完成收口;下一轮优先级转为评估当前 Godot 栏目恢复点是否可以迁入 `archive/`,并保留 PR review follow-up 入口
### 验证
@ -33,9 +78,26 @@
- `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`
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/source-generators/auto-register-exported-collections-generator.md`
- `cd docs && bun run build`
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/tutorials/godot-integration.md`
- `cd docs && bun run build`
- `rg -n "GetNodeX|CreateSignalBuilder|GodotGameArchitecture|AbstractGodotModule|InstallGodotModule\(|GFramework\\.Godot\\.Pool" docs/zh-CN/godot docs/zh-CN/tutorials -S`
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/index.md`
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/architecture.md`
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/scene.md`
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/ui.md`
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/signal.md`
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/extensions.md`
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/logging.md`
- `rg -n "GodotSceneRouter|GodotUiRouter|CreateSignalBuilder|GetNodeX|InstallGodotModule\(" docs/zh-CN/godot -S`
- `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`
1. 评估当前 Godot 栏目页面集是否已足够稳定,决定是否把当前恢复点收口并迁入 `archive/`
2. 如暂不归档,先把 active tracking / trace 进一步压缩到归档决策、当前风险与 PR 跟进入口
3. 下一次推送后重新执行 `$gframework-pr-review`,确认 PR #268 的 CodeRabbit / Greptile open thread 是否关闭或减少

View File

@ -1,612 +1,176 @@
---
title: Godot 架构集成
description: Godot 架构集成提供了 GFramework 与 Godot 引擎的无缝连接,实现生命周期同步和模块化开发
description: 说明 AbstractArchitecture、ArchitectureAnchor 和 Godot 模块挂接的当前生命周期语义,避免继续沿用旧版 `.Wait()` 接法
---
# Godot 架构集成
## 概述
Godot 架构集成是 GFramework.Godot 中连接框架与 Godot 引擎的核心组件。它提供了架构与 Godot 场景树的生命周期绑定、模块化扩展系统,以及与
Godot 节点系统的深度集成
`GFramework.Godot` 当前的架构集成目标很直接:让 `Architecture` 能安全地感知 Godot `SceneTree` 生命周期,并在需要时把
`Node` 的扩展模块挂到场景树上
通过 Godot 架构集成,你可以在 Godot 项目中使用 GFramework 的所有功能,同时保持与 Godot 引擎的完美兼容。
当前真正参与这条链路的核心类型只有三类:
**主要特性**
- `AbstractArchitecture`:在原有 `Architecture` 之上增加 Godot 生命周期绑定
- `ArchitectureAnchor`:挂在 `SceneTree.Root` 下的锚点节点,负责把 `_ExitTree()` 事件转回架构销毁
- `IGodotModule` / `AbstractGodotModule`:当模块本身需要携带 Godot `Node` 时使用
- 架构与 Godot 生命周期自动同步
- 模块化的 Godot 扩展系统
- 架构锚点节点管理
- 自动资源清理
- 热重载支持
- 与 Godot 场景树深度集成
它不是另一套独立的模块系统,也不意味着所有模块都必须改成 `InstallGodotModule(...)`
## 核心概念
## 什么时候该用 `AbstractArchitecture`
### 抽象架构
当你的架构需要满足下面任一条件时,可以让它继承 `AbstractArchitecture`
`AbstractArchitecture` 是 Godot 项目中架构的基类:
- 需要把架构生命周期绑定到 Godot `SceneTree`
- 需要在架构里安装带 `Node` 的扩展模块
- 需要通过受保护的 `ArchitectureRoot` 访问锚点节点,继续挂接 Godot 子节点
如果你只是做普通的 Model / System / Utility 注册,`AbstractArchitecture` 的主要价值仍然是“让架构知道自己何时跟随
Godot 场景树销毁”,而不是改变注册方式。
## 最小接入路径
### 常规模块仍然用 `InstallModule(...)`
当前消费者 `ai-libs/CoreGrid` 的默认做法,是保持普通模块注册方式:
```csharp
public abstract class AbstractArchitecture : Architecture
using GFramework.Core.Abstractions.Architectures;
using GFramework.Core.Abstractions.Environment;
using GFramework.Godot.Architectures;
namespace MyGame.Scripts.Core;
public sealed class GameArchitecture(
IArchitectureConfiguration configuration,
IEnvironment environment)
: AbstractArchitecture(configuration, environment)
{
protected Node ArchitectureRoot { get; }
protected abstract void InstallModules();
protected Task InstallGodotModule<TModule>(TModule module);
}
```
### 架构锚点
`ArchitectureAnchor` 是连接架构与 Godot 场景树的桥梁:
```csharp
public partial class ArchitectureAnchor : Node
{
public void Bind(Action onExit);
public override void _ExitTree();
}
```
### Godot 模块
`IGodotModule` 定义了 Godot 特定的模块接口:
```csharp
public interface IGodotModule : IArchitectureModule
{
Node Node { get; }
void OnPhase(ArchitecturePhase phase, IArchitecture architecture);
void OnAttach(Architecture architecture);
void OnDetach();
}
```
## 基本用法
### 创建 Godot 架构
```csharp
using GFramework.Godot.Architecture;
using GFramework.Core.Abstractions.Architecture;
public class GameArchitecture : AbstractArchitecture
{
// 单例实例
public static GameArchitecture Interface { get; private set; }
public GameArchitecture()
{
Interface = this;
}
protected override void InstallModules()
{
// 注册 Model
RegisterModel(new PlayerModel());
RegisterModel(new GameModel());
// 注册 System
RegisterSystem(new GameplaySystem());
RegisterSystem(new AudioSystem());
// 注册 Utility
RegisterUtility(new StorageUtility());
InstallModule(new UtilityModule());
InstallModule(new ModelModule());
InstallModule(new GameplayModule());
InstallModule(new SystemModule());
}
}
```
### 在 Godot 场景中初始化架构
这里继承 `AbstractArchitecture` 的意义,是把架构绑定到 Godot 生命周期,而不是把普通模块注册改写成 Godot 风格 API。
### 只有携带 `Node` 的模块才需要 `InstallGodotModule(...)`
如果模块本身暴露一个 Godot `Node`,并且希望由架构锚点统一托管,可以这样写:
```csharp
using Godot;
using GFramework.Godot.Architecture;
public partial class GameRoot : Node
{
private GameArchitecture _architecture;
public override void _Ready()
{
// 创建并初始化架构
_architecture = new GameArchitecture();
_architecture.InitializeAsync().AsTask().Wait();
GD.Print("架构已初始化");
}
}
```
### 使用架构锚点
架构锚点会自动创建并绑定到场景树:
```csharp
// 架构会自动创建锚点节点
// 节点名称格式: __GFramework__GameArchitecture__[HashCode]__ArchitectureAnchor__
// 当场景树销毁时,锚点会自动触发架构清理
```
## 高级用法
### 创建 Godot 模块
```csharp
using GFramework.Godot.Architecture;
using GFramework.Core.Abstractions.Architectures;
using GFramework.Godot.Architectures;
using Godot;
public class CoroutineModule : AbstractGodotModule
namespace MyGame.Scripts.Core;
public sealed class HudModule : AbstractGodotModule
{
private Node _coroutineNode;
public override Node Node => _coroutineNode;
public CoroutineModule()
private readonly Control _root = new()
{
_coroutineNode = new Node { Name = "CoroutineScheduler" };
}
Name = "HudModule"
};
public override Node Node => _root;
public override void Install(IArchitecture architecture)
{
// 注册协程调度器
var scheduler = new CoroutineScheduler(new GodotTimeSource());
architecture.RegisterSystem<ICoroutineScheduler>(scheduler);
GD.Print("协程模块已安装");
}
public override void OnPhase(ArchitecturePhase phase, IArchitecture architecture)
public override void OnAttach(GFramework.Core.Architectures.Architecture architecture)
{
if (phase == ArchitecturePhase.Ready)
{
GD.Print("协程模块已就绪");
}
}
public override void OnDetach()
{
GD.Print("协程模块已分离");
_coroutineNode?.QueueFree();
_root.QueueFree();
}
}
```
### 安装 Godot 模块
这类模块的关键点不是“注册更多框架能力”,而是“让模块节点跟着架构锚点进出场景树”。
真正调用 `InstallGodotModule(...)` 时,也应该把它放在能够接受异步挂接流程的初始化路径里,而不是继续沿用旧文档里的
`.Wait()` 叙述。
```csharp
public class GameArchitecture : AbstractArchitecture
{
protected override void InstallModules()
{
// 安装核心模块
RegisterModel(new PlayerModel());
RegisterSystem(new GameplaySystem());
## 当前生命周期
// 安装 Godot 模块
InstallGodotModule(new CoroutineModule()).Wait();
InstallGodotModule(new SceneModule()).Wait();
InstallGodotModule(new UiModule()).Wait();
}
}
```
### 初始化阶段
### 访问架构根节点
`AbstractArchitecture.OnInitialize()` 目前会按这个顺序工作:
```csharp
public class SceneModule : AbstractGodotModule
{
private Node _sceneRoot;
1. 生成唯一的锚点节点名称
2. 调用 `AttachToGodotLifecycle()`
3. 在可用的 `SceneTree` 上创建并绑定 `ArchitectureAnchor`
4. 执行你重写的 `InstallModules()`
public override Node Node => _sceneRoot;
也就是说Godot 生命周期绑定先发生,业务模块注册后发生。
public SceneModule()
{
_sceneRoot = new Node { Name = "SceneRoot" };
}
### `InstallGodotModule(...)` 的执行顺序
public override void Install(IArchitecture architecture)
{
// 访问架构根节点
if (architecture is AbstractArchitecture godotArch)
{
var root = godotArch.ArchitectureRoot;
root.AddChild(_sceneRoot);
}
}
}
```
当前实现里,`InstallGodotModule(...)` 会:
### 监听架构阶段
1. 检查模块参数是否为 `null`
2. 检查 `_anchor` 是否已初始化
3. 先执行 `module.Install(this)`
4. 把模块登记进内部 `_extensions`
5. `await anchor.WaitUntilReadyAsync()`
6. 通过 `CallDeferred(AddChild, module.Node)` 把模块节点挂到锚点下
7. 调用 `module.OnAttach(this)`
```csharp
public class AnalyticsModule : AbstractGodotModule
{
private Node _analyticsNode;
这条顺序有两个实际意义:
public override Node Node => _analyticsNode;
- 模块会在挂接节点前先完成框架侧注册
- 只有等锚点真正 ready 后,才进入需要访问 Godot 节点 API 的附加阶段
public AnalyticsModule()
{
_analyticsNode = new Node { Name = "Analytics" };
}
### 销毁阶段
public override void Install(IArchitecture architecture)
{
// 安装分析系统
}
`ArchitectureAnchor._ExitTree()` 会触发绑定好的退出回调,随后 `AbstractArchitecture` 会开始观察异步销毁流程:
public override void OnPhase(ArchitecturePhase phase, IArchitecture architecture)
{
switch (phase)
{
case ArchitecturePhase.Initializing:
GD.Print("架构正在初始化");
break;
- 防止重复销毁
- 依次调用已登记 Godot 模块的 `OnDetach()`
- 清空内部扩展列表
- 再进入基类 `DestroyAsync()`
case ArchitecturePhase.Ready:
GD.Print("架构已就绪,开始追踪");
StartTracking();
break;
如果异步销毁抛异常,当前实现会把错误写到 Godot 错误输出,而不是静默吞掉。
case ArchitecturePhase.Destroying:
GD.Prin构正在销毁停止追踪");
StopTracking();
break;
}
}
## 当前边界
private void StartTracking() { }
private void StopTracking() { }
}
```
### 没有锚点时不会偷偷安装模块
### 自定义架构配置
`GFramework.Godot.Tests/Architectures/AbstractArchitectureModuleInstallationTests.cs` 已覆盖一个关键边界:
```csharp
using GFramework.Core.Abstractions.Architecture;
using GFramework.Core.Abstractions.Environment;
- 当锚点尚未初始化时,`InstallGodotModule(...)` 会直接抛 `InvalidOperationException("Anchor not initialized")`
- 失败发生在 `module.Install(...)` 之前,因此不会留下半安装副作用
public class GameArchitecture : AbstractArchitecture
{
public GameArchitecture() : base(
configuration: CreateConfiguration(),
environment: CreateEnvironment()
)
{
}
这也是为什么文档不应该再把 `InstallGodotModule(...).Wait()` 写成一种随处可用的默认初始化方式。
private static IArchitectureConfiguration CreateConfiguration()
{
return new ArchitectureConfiguration
{
EnableLogging
LogLevel = LogLevel.Debug
};
}
### `AbstractGodotModule` 只是便捷基类,不代表自动阶段广播
private static IEnvironment CreateEnvironment()
{
return new DefaultEnvironment
{
IsDevelopment = OS.IsDebugBuild()
};
}
当前接口 `IGodotModule` 真正保证的成员只有:
protected override void InstallModules()
{
// 根据环境配置安装模块
if (Environment.IsDevelopment)
{
InstallGodotModule(new DebugModule()).Wait();
}
- `Node`
- `Install(IArchitecture architecture)`
- `OnAttach(Architecture architecture)`
- `OnDetach()`
// 安装核心模块
RegisterModel(new PlayerModel());
RegisterSystem(new GameplaySystem());
}
}
```
`AbstractGodotModule` 里虽然保留了 `OnPhase(...)` / `OnArchitecturePhase(...)` 虚方法,但它们不在当前接口契约内,也没有在
这条挂接流程里形成稳定的自动广播语义。不要把它写成当前公开保证。
### 热重载支持
### `ArchitectureRoot` 只在锚点就绪后可用
```csharp
public class GameArchitecture : AbstractArchitecture
{
private static bool _initialized;
`ArchitectureRoot` 是受保护属性,底层直接返回 `_anchor`。如果锚点尚未准备好或架构已经失效,它会抛
`InvalidOperationException("Architecture root not ready")`。因此它适合放在明确依赖锚点存在的挂接逻辑里,而不是拿来做
任意时机的全局节点查找。
protected override void OnInitialize()
{
// 防止热重载时重复初始化
if (_initialized)
{
GD.Print("架构已初始化,跳过重复初始化");
return;
}
## 继续阅读
base.OnInitialize();
_initialized = true;
}
protected override async ValueTask OnDestroyAsync()
{
await base.OnDestroyAsync();
_initialized = false;
}
}
```
### 在节点中使用架构
```csharp
using Godot;
using GFramework.Core.Abstractions.Controller;
using GFramework.Core.SourceGenerators.Abstractions.Rule;
[ContextAware]
public partial class Player : CharacterBody2D, IController
{
public override void _Ready()
{
// 使用扩展方法访问架构([ContextAware] 实现 IContextAware 接口)
var playerModel = this.GetModel<PlayerModel>();
var gameplaySystem = this.GetSystem<GameplaySystem>();
// 发送事件
this.SendEvent(new PlayerSpawnedEvent());
// 执行命令
this.SendCommand(new InitPlayerCommand());
}
public override void _Process(double delta)
{
// 在 Process 中使用架构组件
var inputSystem = this.GetSystem<InputSystem>();
var movement = inputSystem.GetMovementInput();
Velocity = movement * 200;
MoveAndSlide();
}
}
```
### 多架构支持
```csharp
// 游戏架构
public class GameArchitecture : AbstractArchitecture
{
public static GameArchitecture Interface { get; private set; }
public GameArchitecture()
{
Interface = this;
}
protected override void InstallModules()
{
RegisterModel(new PlayerModel());
RegisterSystem(new GameplaySystem());
}
}
// UI 架构
public class UiArchitecture : AbstractArchitecture
{
public static UiArchitecture Interface { get; private set; }
public UiArchitecture()
{
Interface = this;
}
protected override void InstallModules()
{
RegisterModel(new UiModel());
RegisterSystem(new UiSystem());
}
}
// 在不同节点中使用不同架构
[ContextAware]
public partial class GameNode : Node, IController
{
// 配置使用 GameArchitecture 的上下文提供者
static GameNode()
{
SetContextProvider(new GameContextProvider());
}
}
[ContextAware]
public partial class UiNode : Control, IController
{
// 配置使用 UiArchitecture 的上下文提供者
static UiNode()
{
SetContextProvider(new UiContextProvider());
}
}
```
## 最佳实践
1. **使用单例模式**:为架构提供全局访问点
```csharp
public class GameArchitecture : AbstractArchitecture
{
public static GameArchitecture Interface { get; private set; }
public GameArchitecture()
{
Interface = this;
}
}
```
2. **在根节点初始化架构**:确保架构在所有节点之前就绪
```csharp
public partial class GameRoot : Node
{
public override void _Ready()
{
new GameArchitecture().InitializeAsync().AsTask().Wait();
}
}
```
3. **使用 Godot 模块组织功能**:将相关功能封装为模块
```csharp
InstallGodotModule(new CoroutineModule()).Wait();
InstallGodotModule(new SceneModule()).Wait();
InstallGodotModule(new UiModule()).Wait();
```
4. **利用架构阶段钩子**:在适当的时机执行逻辑
```csharp
public override void OnPhase(ArchitecturePhase phase, IArchitecture architecture)
{
if (phase == ArchitecturePhase.Ready)
{
// 架构就绪后的初始化
}
}
```
5. **正确清理资源**:在 OnDetach 中释放 Godot 节点
```csharp
public override void OnDetach()
{
_node?.QueueFree();
_node = null;
}
```
6. **避免在构造函数中访问架构**:使用 _Ready 或 OnPhase
```csharp
✗ public Player()
{
var model = this.GetModel<PlayerModel>(); // 架构可能未就绪
}
✓ public override void _Ready()
{
var model = this.GetModel<PlayerModel>(); // 安全
}
```
## 常见问题
### 问题:架构什么时候初始化?
**解答**
在根节点的 `_Ready` 方法中初始化:
```csharp
public partial class GameRoot : Node
{
public override void _Ready()
{
new GameArchitecture().InitializeAsync().AsTask().Wait();
}
}
```
### 问题:如何在节点中访问架构?
**解答**
使用 `[ContextAware]` 特性或直接使用单例:
```csharp
using GFramework.Core.SourceGenerators.Abstractions.Rule;
// 方式 1: 使用 [ContextAware] 特性(推荐)
[ContextAware]
public partial class Player : Node, IController
{
public override void _Ready()
{
// 使用扩展方法访问架构([ContextAware] 实现 IContextAware 接口)
var model = this.GetModel<PlayerModel>();
var system = this.GetSystem<GameplaySystem>();
}
}
// 方式 2: 直接使用单例
public partial class Enemy : Node
{
public override void _Ready()
{
var model = GameArchitecture.Interface.GetModel<EnemyModel>();
}
}
```
**注意**
- `IController` 是标记接口,不包含任何方法
- 架构访问能力由 `[ContextAware]` 特性提供
- `[ContextAware]` 会自动生成 `Context` 属性和实现 `IContextAware` 接口
- 扩展方法(如 `this.GetModel()`)基于 `IContextAware` 接口,而非 `IController`
### 问题:架构锚点节点是什么?
**解答**
架构锚点是一个隐藏的节点,用于将架构绑定到 Godot 场景树。当场景树销毁时,锚点会自动触发架构清理。
### 问题:如何支持热重载?
**解答**
使用静态标志防止重复初始化:
```csharp
private static bool _initialized;
protected override void OnInitialize()
{
if (_initialized) return;
base.OnInitialize();
_initialized = true;
}
```
### 问题:可以有多个架构吗?
**解答**
可以,但通常一个游戏只需要一个主架构。如果需要多个架构,为每个架构提供独立的单例:
```csharp
public class GameArchitecture : AbstractArchitecture
{
public static GameArchitecture Interface { get; private set; }
}
public class UiArchitecture : AbstractArchitecture
{
public static UiArchitecture Interface { get; private set; }
}
```
### 问题Godot 模块和普通模块有什么区别?
**解答**
- **普通模块**:纯 C# 逻辑,不依赖 Godot
- **Godot 模块**:包含 Godot 节点,与场景树集成
```csharp
// 普通模块
InstallModule(new CoreModule());
// Godot 模块
InstallGodotModule(new SceneModule()).Wait();
```
## 相关文档
- [架构组件](/zh-CN/core/architecture) - 核心架构系统
- [生命周期管理](/zh-CN/core/lifecycle) - 组件生命周期
- [Godot 场景系统](/zh-CN/godot/scene) - Godot 场景集成
- [Godot UI 系统](/zh-CN/godot/ui) - Godot UI 集成
- [Godot 扩展](/zh-CN/godot/extensions) - Godot 扩展方法
1. [Godot 运行时集成](./index.md)
2. [Godot 集成教程](../tutorials/godot-integration.md)
3. [Godot 场景系统](./scene.md)
4. [Godot UI 系统](./ui.md)

View File

@ -1,328 +1,181 @@
# Godot 扩展方法 (Godot Extensions)
---
title: Godot 扩展方法
description: 以当前 GFramework.Godot.Extensions 源码为准说明路径、Node、signal 和 unregister 扩展的真实成员与边界。
---
## 概述
# Godot 扩展方法
Godot 扩展方法模块为 Godot 引擎提供了丰富的便捷扩展方法集合。这些扩展方法简化了常见的 Godot
开发任务,提高了代码的可读性和开发效率。该模块遵循流畅接口设计原则,支持链式调用。
`GFramework.Godot.Extensions` 当前并不是“覆盖所有 Godot 节点操作”的万能层。按源码看,它实际公开的扩展主要只有四组:
## 模块结构
- `GodotPathExtensions`
- `NodeExtensions`
- `SignalFluentExtensions`
- `UnRegisterExtension`
```mermaid
graph TD
A[Extensions] --> B[GodotPathExtensions]
A --> C[NodeExtensions]
A --> D[SignalFluentExtensions]
A --> E[UnRegisterExtension]
D --> F[SignalBuilder]
B --> G[路径判断扩展]
C --> H[节点生命周期]
C --> I[节点查询]
C --> J[场景树操作]
C --> K[输入控制]
C --> L[调试工具]
D --> M[信号连接系统]
E --> N[事件管理]
```
这页的重点应该是识别这些扩展各自解决什么问题,以及哪些旧文档里的“大而全能力”现在并不存在。
## 扩展模块详解
## 当前公开入口
### 1. 路径扩展 (GodotPathExtensions)
### `GodotPathExtensions`
提供 Godot 虚拟路径的判断和识别功能。
这组扩展只负责判断 Godot 虚拟路径前缀:
**主要方法:**
- `IsUserPath(this string path)`
- `IsResPath(this string path)`
- `IsGodotPath(this string path)`
- `IsUserPath()` - 判断是否为 `user://` 路径
- `IsResPath()` - 判断是否为 `res://` 路径
- `IsGodotPath()` - 判断是否为 Godot 虚拟路径
**使用示例:**
它们不做文件访问,也不解析目录结构,只是用字符串前缀判断 `user://``res://`
```csharp
string savePath = "user://save.dat";
string configPath = "res://config.json";
string logPath = "C:/logs/debug.log";
using GFramework.Godot.Extensions;
if (savePath.IsUserPath()) Console.WriteLine("用户数据路径");
if (configPath.IsResPath()) Console.WriteLine("资源路径");
if (logPath.IsGodotPath()) Console.WriteLine("Godot 虚拟路径");
else Console.WriteLine("文件系统路径");
```
### 2. 节点扩展 (NodeExtensions)
最丰富的扩展模块,提供全面的节点操作功能。
#### 节点生命周期管理
```csharp
// 安全释放节点
node.QueueFreeX(); // 延迟释放
node.FreeX(); // 立即释放
// 等待节点就绪
await node.WaitUntilReadyAsync();
// 检查节点有效性
if (node.IsValidNode()) Console.WriteLine("节点有效");
if (node.IsInvalidNode()) Console.WriteLine("节点无效");
```
#### 节点查询操作
```csharp
// 查找子节点
var sprite = node.FindChildX<Sprite2D>("Sprite");
var parent = node.GetParentX<Control>();
// 获取或创建节点
var panel = parent.GetOrCreateNode<Panel>("MainPanel");
// 遍历子节点
node.ForEachChild<Sprite2D>(sprite => {
sprite.Modulate = Colors.White;
});
```
#### 场景树操作
```csharp
// 获取根节点
var root = node.GetRootNodeX();
// 异步添加子节点
await parent.AddChildXAsync(childNode);
// 设置场景树暂停状态
node.Paused(true); // 暂停
node.Paused(false); // 恢复
```
#### 输入控制
```csharp
// 标记输入事件已处理
node.SetInputAsHandled();
// 禁用/启用输入
node.DisableInput();
node.EnableInput();
```
#### 调试工具
```csharp
// 打印节点路径
node.LogNodePath();
// 打印节点树
node.PrintTreeX();
// 安全延迟调用
node.SafeCallDeferred("UpdateUI");
```
#### 类型转换
```csharp
// 安全的类型转换
var button = node.OfType<Button>();
var sprite = childNode.OfType<Sprite2D>();
```
### 3. 信号扩展 (SignalFluentExtensions)
提供流畅的信号连接 API详见 [信号扩展](signal.md)。
**快速示例:**
```csharp
button.Signal(Button.SignalName.Pressed)
.WithFlags(GodotObject.ConnectFlags.OneShot)
.ToAndCall(new Callable(this, nameof(OnButtonPressed)));
```
### 4. 取消注册扩展 (UnRegisterExtension)
自动管理事件监听器的生命周期。
**主要方法:**
- `UnRegisterWhenNodeExitTree()` - 节点退出场景树时自动取消注册
**使用示例:**
```csharp
var unRegister = eventManager.Subscribe<GameEvent>(OnGameEvent);
unRegister.UnRegisterWhenNodeExitTree(node);
```
## 快速参考
### 常用代码片段
#### 场景节点管理
```csharp
public class GameManager : Node
if ("user://save.json".IsUserPath())
{
private Node _uiRoot;
public override void _Ready()
}
if ("res://config/gameplay.yaml".IsGodotPath())
{
}
```
### `NodeExtensions`
`NodeExtensions` 是当前扩展集合里体量最大的部分,但职责仍然比较具体,主要分成下面几类。
#### 生命周期与有效性辅助
- `QueueFreeX(this Node? node)`
- `FreeX(this Node? node)`
- `WaitUntilReadyAsync(this Node node)`
- `WaitUntilReady(this Node node, Action callback)`
- `IsValidNode(this Node? node)`
- `IsInvalidNode(this Node? node)`
这里最容易写偏的地方有两个:
- `QueueFreeX()` / `FreeX()` 会先检查 null、实例是否仍有效、是否已经进入删除队列
- `IsValidNode()` 不只要求实例还活着,还要求节点已经在 `SceneTree` 里;单纯 `new` 出来但还没挂树的节点会返回 `false`
#### 节点访问与装配辅助
- `FindChildX<T>(...)`
- `GetOrCreateNode<T>(...)`
- `AddChildXAsync(...)`
- `GetParentX<T>()`
- `GetRootNodeX()`
- `ForEachChild<T>(...)`
- `OfType<T>()`
这几组方法更偏“少量常用装配动作”,不是完整查询 DSL。
特别是 `GetOrCreateNode<T>(string path)` 的当前实现要注意:
1. 先尝试 `GetNodeOrNull<T>(path)`
2. 如果没找到,就 `new T()`
3. 把新节点直接 `AddChild(...)` 到当前节点
4. 再把 `created.Name = path`
它不会按斜杠路径逐级创建中间节点,所以不要把它当成层级化路径构建器。
#### 输入、暂停与调试辅助
- `SetInputAsHandled()`
- `Paused(bool paused = true)`
- `DisableInput()`
- `EnableInput()`
- `LogNodePath()`
- `PrintTreeX(string indent = "")`
- `SafeCallDeferred(string method)`
这些方法都很薄,基本是在现有 `Viewport` / `SceneTree` / `CallDeferred(...)` 上做便捷包装,没有额外状态机。
### `SignalFluentExtensions`
`SignalFluentExtensions` 只提供一个入口:
- `Signal(this GodotObject @object, StringName signal)`
它把目标对象和 signal 名称包装成 `SignalBuilder`。具体连接语义请看 [Godot 信号系统](./signal.md)。
### `UnRegisterExtension`
`UnRegisterExtension` 当前也只有一个公开方法:
- `UnRegisterWhenNodeExitTree(this IUnRegister unRegister, Node node)`
它做的事情很明确:把 `unRegister.UnRegister` 挂到 `node.TreeExiting` 上。这样框架侧的订阅句柄就能跟 Godot 节点生命周期对齐。
```csharp
IUnRegister subscription = eventBus.Subscribe<SettingsChangedEvent>(OnSettingsChanged);
subscription.UnRegisterWhenNodeExitTree(this);
```
它不会接管普通 Godot signal 的断开逻辑,也不会帮你推断别的释放时机。
## 最小接入路径
### 1. 节点进入树之后再做装配
如果你的节点可能在 `_Ready()` 前就被访问,先用 `WaitUntilReadyAsync()`
```csharp
using GFramework.Godot.Extensions;
using GFramework.Godot.Extensions.Signal;
using Godot;
public partial class SettingsPanel : Control
{
public override async void _Ready()
{
_uiRoot = GetNode<Node>("UI");
// 创建游戏面板
var gamePanel = _uiRoot.GetOrCreateNode<Panel>("GamePanel");
// 安全添加子节点
var player = new Player();
await AddChildXAsync(player);
// 查找并配置玩家
var sprite = player.FindChildX<Sprite2D>("Sprite");
if (sprite.IsValidNode()) sprite.Modulate = Colors.Red;
await this.WaitUntilReadyAsync();
var applyButton = FindChildX<Button>("ApplyButton");
applyButton?.Signal(Button.SignalName.Pressed)
.To(Callable.From(OnApplyPressed));
}
public void Cleanup()
private void OnApplyPressed()
{
// 安全释放所有子节点
ForEachChild<Node>(child => child.QueueFreeX());
this.SetInputAsHandled();
}
}
```
#### UI 事件处理
### 2. 框架订阅和节点生命周期一起收尾
当订阅句柄实现了 `IUnRegister`,可以把释放时机绑到节点退出树:
```csharp
public class MainMenu : Control
public override void _Ready()
{
private Button _startButton;
private Button _quitButton;
public override void _Ready()
{
_startButton = FindChildX<Button>("StartButton");
_quitButton = FindChildX<Button>("QuitButton");
// 流畅的信号连接
_startButton.Signal(BaseButton.SignalName.Pressed)
.ToAndCall(new Callable(this, nameof(OnStartPressed)));
_quitButton.Signal(BaseButton.SignalName.Pressed)
.To(new Callable(this, nameof(OnQuitPressed)));
}
private void OnStartPressed()
{
GetTree().ChangeSceneToFile("res://scenes/game.tscn");
}
private void OnQuitPressed()
{
GetTree().Quit();
}
IUnRegister subscription = _eventBus.Subscribe<SettingsChangedEvent>(OnSettingsChanged);
subscription.UnRegisterWhenNodeExitTree(this);
}
```
#### 异步场景管理
这比在多个 `_ExitTree()` / `Dispose()` 分支里手写解绑更稳定,也更符合当前扩展的职责边界。
```csharp
public class SceneManager : Node
{
public async Task<T> LoadSceneAsync<T>(string scenePath) where T : Node
{
var packedScene = GD.Load<PackedScene>(scenePath);
var instance = packedScene.Instantiate<T>();
// 等待场景加载完成
await instance.WaitUntilReadyAsync();
return instance;
}
public async Task TransitionToScene(string scenePath)
{
var newScene = await LoadSceneAsync<Node>(scenePath);
// 清理当前场景
ForEachChild<Node>(child => child.QueueFreeX());
// 加载新场景
await AddChildXAsync(newScene);
// 设置输入处理
newScene.SetInputAsHandled();
}
}
```
### 3. 只在需要时使用 signal fluent API
## 设计原则
`Signal(...)` 属于扩展集合的一部分,但它已经有独立页面。实践上可以这样分工:
### 1. 安全性
- 节点查找、ready 等待、输入处理:`NodeExtensions`
- 动态 signal 绑定:`Signal(...)`
- 框架订阅释放:`UnRegisterWhenNodeExitTree(...)`
- 路径前缀判断:`GodotPathExtensions`
- 所有节点操作都包含有效性检查
- 提供安全的类型转换方法
- 避免空引用异常
## 当前边界
### 2. 便利性
- 当前 `NodeExtensions` 没有 `GetNodeX()``CreateSignalBuilder()` 之类旧文档里提过的 API
- 它不是 router、scene factory、UI factory 或生成器的替代层
- `GetOrCreateNode<T>()` 只会创建一个直接子节点,不会递归补整条路径
- `SafeCallDeferred(...)` 只有在 `IsValidNode()``true` 时才会调用;节点未入树时不会执行
- `UnRegisterWhenNodeExitTree(...)` 只针对实现了 `IUnRegister` 的框架订阅句柄,不会自动处理 Godot 原生 `Connect(...)`
- 协程辅助扩展在 `GFramework.Godot.Coroutine` 命名空间,不属于这组 `Extensions` 页面要覆盖的核心范围
- 流畅的 API 设计
- 支持链式调用
- 减少样板代码
## 继续阅读
### 3. 一致性
- 统一的命名约定
- 一致的返回类型
- 预测性方法行为
### 4. 性能
- 避免不必要的节点查找
- 最小化内存分配
- 优化常见操作
## 最佳实践
### 1. 节点生命周期
```csharp
// ✅ 推荐:使用安全释放
node.QueueFreeX();
// ❌ 避免:直接释放可能导致错误
node.QueueFree();
```
### 2. 节点查询
```csharp
// ✅ 推荐:类型安全的查找
var button = node.FindChildX<Button>("Button");
// ❌ 避免:需要手动类型转换
var button = node.FindChild("Button") as Button;
```
### 3. 异步操作
```csharp
// ✅ 推荐:等待节点就绪
await child.WaitUntilReadyAsync();
// ❌ 避免:假设节点已就绪
child.DoSomething();
```
### 4. 事件管理
```csharp
// ✅ 推荐:自动清理事件
var unRegister = eventSystem.Subscribe(eventHandler);
unRegister.UnRegisterWhenNodeExitTree(node);
// ❌ 避免:手动管理事件生命周期
// 可能导致内存泄漏
```
- [Godot 运行时集成](./index.md)
- [Godot 信号系统](./signal.md)
- [Godot 场景系统](./scene.md)
- [Godot UI 系统](./ui.md)

File diff suppressed because it is too large Load Diff

View File

@ -1,43 +1,40 @@
---
title: Godot 日志系统
description: Godot 日志系统提供了 GFramework 日志功能与 Godot 引擎控制台的完整集成
description: 以当前 GFramework.Godot.Logging 源码与 CoreGrid 接线为准,说明 Godot 日志 provider、控制台输出语义与接入边界
---
# Godot 日志系统
## 概述
`GFramework.Godot` 当前的日志能力很收敛:它不是一套独立于 Core 的新日志框架,而是把现有 `ILogger` 调用面接到
Godot 控制台。
Godot 日志系统是 GFramework.Godot 中连接框架日志功能与 Godot 引擎控制台的核心组件。它提供了与 Godot
控制台的深度集成,支持彩色输出、多级别日志记录,以及与 GFramework 日志系统的无缝对接
换句话说Godot 侧真正新增的是 provider / factory / logger 这层输出适配,而不是新的日志 API。业务代码仍然继续使用
`LoggerFactoryResolver.Provider.CreateLogger(...)``[Log]` 生成的 `ILogger` 字段
通过 Godot 日志系统,你可以在 Godot 项目中使用统一的日志接口,日志会自动输出到 Godot 编辑器控制台,并根据日志级别使用不同的颜色和输出方式。
## 当前公开入口
**主要特性**
### `GodotLogger`
- 与 Godot 控制台深度集成
- 支持彩色日志输出
- 多级别日志记录Trace、Debug、Info、Warning、Error、Fatal
- 日志缓存机制
- 时间戳和格式化支持
- 异常信息记录
## 核心概念
### GodotLogger
`GodotLogger` 是 Godot 平台的日志记录器实现,继承自 `AbstractLogger`
`GodotLogger` 继承自 `AbstractLogger`,负责把日志写到 Godot 的输出 API
```csharp
public sealed class GodotLogger : AbstractLogger
{
public GodotLogger(string? name = null, LogLevel minLevel = LogLevel.Info);
protected override void Write(LogLevel level, string message, Exception? exception);
}
public sealed class GodotLogger(
string? name = null,
LogLevel minLevel = LogLevel.Info)
: AbstractLogger(name ?? RootLoggerName, minLevel)
```
### GodotLoggerFactory
当前实现里的几个关键语义:
`GodotLoggerFactory` 用于创建 Godot 日志记录器实例:
- 时间戳使用 `DateTime.UtcNow`
- 输出前缀格式是 `[yyyy-MM-dd HH:mm:ss.fff] LEVEL [LoggerName]`
- `exception` 不会被单独结构化处理,而是直接追加到消息后面
- `Trace` / `Debug``GD.PrintRich(...)`
- `Info` / `Warning` / `Error` / `Fatal` 分别走 Godot 自身的普通、警告和错误输出通道
### `GodotLoggerFactory`
`GodotLoggerFactory` 只负责按名称和最小级别创建 `GodotLogger`
```csharp
public class GodotLoggerFactory : ILoggerFactory
@ -46,9 +43,11 @@ public class GodotLoggerFactory : ILoggerFactory
}
```
### GodotLoggerFactoryProvider
它本身不做缓存,也不额外增加过滤规则。
`GodotLoggerFactoryProvider` 提供日志工厂实例,并支持日志缓存:
### `GodotLoggerFactoryProvider`
`GodotLoggerFactoryProvider` 是当前最常用的接入点:
```csharp
public sealed class GodotLoggerFactoryProvider : ILoggerFactoryProvider
@ -58,571 +57,144 @@ public sealed class GodotLoggerFactoryProvider : ILoggerFactoryProvider
}
```
## 基本用法
它内部用 `CachedLoggerFactory` 包装 `GodotLoggerFactory`。缓存 key 由 `name``MinLevel` 共同组成,所以:
### 配置 Godot 日志系统
- 同名、同 `MinLevel` 的 logger 会复用实例
- 调整 `MinLevel` 后,新创建的 logger 会走新的缓存 key
- 已经持有的旧 logger 不会被原地改写
在架构初始化时配置日志提供程序:
## 最小接入路径
### 1. 在 `ArchitectureConfiguration` 中挂上 Godot provider
当前仓库里更稳的接法,不是到处直接改全局 `LoggerFactoryResolver.Provider`,而是在架构配置里显式提供
`LoggerProperties.LoggerFactoryProvider``ai-libs/CoreGrid/global/GameEntryPoint.cs` 现在就是这样接的。
```csharp
using GFramework.Godot.Architecture;
using GFramework.Godot.Logging;
using GFramework.Core.Logging;
using GFramework.Core.Abstractions.Environment;
using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Abstractions.Properties;
using GFramework.Core.Architectures;
using GFramework.Godot.Logging;
public class GameArchitecture : AbstractArchitecture
{
public static GameArchitecture Interface { get; private set; }
public GameArchitecture()
var architecture = new GameArchitecture(
new ArchitectureConfiguration
{
Interface = this;
// 配置 Godot 日志系统
LoggerFactoryResolver.Provider = new GodotLoggerFactoryProvider
LoggerProperties = new LoggerProperties
{
MinLevel = LogLevel.Debug // 设置最小日志级别
};
}
LoggerFactoryProvider = new GodotLoggerFactoryProvider
{
MinLevel = LogLevel.Debug
}
}
},
environment);
protected override void InstallModules()
{
var logger = LoggerFactoryResolver.Provider.CreateLogger("GameArchitecture");
logger.Info("游戏架构初始化开始");
RegisterModel(new PlayerModel());
RegisterSystem(new GameplaySystem());
logger.Info("游戏架构初始化完成");
}
}
architecture.Initialize();
```
### 创建和使用日志记录器
这样做的好处是:
- 日志 provider 和架构启动配置放在同一个入口
- 不会把“Godot 控制台输出”误写成全局静态默认前提
- 和 `ArchitectureConfiguration` 默认使用 `ConsoleLoggerFactoryProvider` 的 Core 接线方式保持一致
### 2. 业务代码继续使用标准 `ILogger`
配置好 provider 之后Godot 节点、System、Model、router、factory 都继续通过统一入口拿 logger
```csharp
using Godot;
using GFramework.Core.Logging;
using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Logging;
using Godot;
public partial class Player : CharacterBody2D
public partial class SettingsPanel : Control
{
private ILogger _logger;
private static readonly ILogger Log =
LoggerFactoryResolver.Provider.CreateLogger(nameof(SettingsPanel));
public override void _Ready()
{
// 创建日志记录器
_logger = LoggerFactoryResolver.Provider.CreateLogger("Player");
_logger.Info("玩家初始化");
_logger.Debug("玩家位置: {0}", Position);
}
public override void _Process(double delta)
{
if (_logger.IsDebugEnabled())
{
_logger.Debug("玩家速度: {0}", Velocity);
}
}
private void TakeDamage(float damage)
{
_logger.Warn("玩家受到伤害: {0}", damage);
}
private void OnError()
{
_logger.Error("玩家状态异常");
Log.Info("SettingsPanel ready.");
}
}
```
### 记录不同级别的日志
如果你已经在用 `GFramework.Core.SourceGenerators`,也可以继续让 `[Log]` 生成字段。Godot provider 只改变输出落点,
不会改变 `[Log]` 的生成契约。
### 3. Scene / UI 迁移日志会自动复用同一套 provider
`GFramework.Game.Scene.Handler.LoggingTransitionHandler`
`GFramework.Game.UI.Handler.LoggingTransitionHandler` 都是普通 `ILogger` 使用者。只要当前架构挂的是
`GodotLoggerFactoryProvider`,这些迁移日志就会直接进 Godot 控制台。
```csharp
var logger = LoggerFactoryResolver.Provider.CreateLogger("GameSystem");
using GFramework.Game.Scene.Handler;
using GFramework.Game.UI.Handler;
// Trace - 最详细的跟踪信息(灰色)
logger.Trace("执行函数: UpdatePlayerPosition");
// Debug - 调试信息(青色)
logger.Debug("当前帧率: {0}", Engine.GetFramesPerSecond());
// Info - 一般信息(白色)
logger.Info("游戏开始");
// Warning - 警告信息(黄色)
logger.Warn("资源加载缓慢: {0}ms", loadTime);
// Error - 错误信息(红色)
logger.Error("无法加载配置文件");
// Fatal - 致命错误(红色,使用 PushError
logger.Fatal("游戏崩溃");
RegisterHandler(new LoggingTransitionHandler());
```
### 记录异常信息
这也说明 Godot 日志页不需要重新定义一套“Godot 专用场景日志接口”;现有 Game 运行时日志在 Godot 宿主里本来就会复用
这套 provider。
```csharp
var logger = LoggerFactoryResolver.Provider.CreateLogger("SaveSystem");
## Godot 控制台输出语义
try
{
SaveGame();
}
catch (Exception ex)
{
// 记录异常信息
logger.Error("保存游戏失败", ex);
}
当前 `GodotLogger.Write(...)` 的级别映射如下:
| 日志级别 | Godot 输出 API | 当前行为 |
| --- | --- | --- |
| `Trace` | `GD.PrintRich(...)` | 使用灰色富文本输出 |
| `Debug` | `GD.PrintRich(...)` | 使用青色富文本输出 |
| `Info` | `GD.Print(...)` | 普通控制台输出 |
| `Warning` | `GD.PushWarning(...)` | 进入 Godot 警告通道 |
| `Error` | `GD.PrintErr(...)` | 输出到错误流 |
| `Fatal` | `GD.PushError(...)` | 进入 Godot 错误通道 |
异常追加格式也来自当前实现本身:
```text
[2026-04-22 10:30:47.012] ERROR [SaveSystem] 保存游戏失败
System.IO.IOException: ...
```
## 高级用法
如果你需要 JSON formatter、rolling file、namespace 级过滤、structured sink 组合,这已经超出
`GFramework.Godot.Logging` 当前职责,应该回到 [Core 日志系统](../core/logging.md) 设计 provider 组合。
### 在 System 中使用日志
## 什么时候用手写 logger什么时候用 `[Log]`
```csharp
using GFramework.Core.System;
using GFramework.Core.Logging;
using GFramework.Core.Abstractions.Logging;
- 手写 `LoggerFactoryResolver.Provider.CreateLogger(...)`
- 少量入口类
- 需要自己控制字段名、静态/实例生命周期
- 想明确看到 logger 初始化位置
- 用 `[Log]`
- Godot 节点、controller、system 上有大量重复 logger 字段样板
- 你已经引用 `GFramework.Core.SourceGenerators`
- 想把 logger 字段生成交给编译期
public class CombatSystem : AbstractSystem
{
private ILogger _logger;
这里的边界要分清:
protected override void OnInit()
{
_logger = LoggerFactoryResolver.Provider.CreateLogger("CombatSystem");
_logger.Info("战斗系统初始化完成");
}
- Godot provider来自 `GFramework.Godot`
- `[Log]` 生成器:来自 `GFramework.Core.SourceGenerators`
public void ProcessCombat(Entity attacker, Entity target, float damage)
{
_logger.Debug("战斗处理: {0} 攻击 {1}, 伤害: {2}",
attacker.Name, target.Name, damage);
它们是可组合关系,不是上下位替代关系。
if (damage > 100)
{
_logger.Warn("高伤害攻击: {0}", damage);
}
}
## 当前边界
protected override void OnDestroy()
{
_logger.Info("战斗系统已销毁");
}
}
```
- 当前推荐接法是把 `GodotLoggerFactoryProvider` 放进 `ArchitectureConfiguration.LoggerProperties`;直接赋值
`LoggerFactoryResolver.Provider` 仍然可用,但不该再写成默认采用路径
- `GFramework.Godot.Logging` 只解决 Godot 控制台输出不提供文件落盘、JSON formatter、异步 appender 或按 namespace
的复杂过滤
- `GodotLogger` 只改变输出方式,不改变 `ILogger` 接口本身;业务代码不需要切换到 Godot 专用日志 API
- `[Log]``[ContextAware]` 这类字段注入能力不属于 `GFramework.Godot.Logging`
- Scene / UI 的 `LoggingTransitionHandler` 位于 `GFramework.Game`Godot 侧只是通过 provider 让它们输出到 Godot 控制台
- 当前 `GodotLogger` 使用的是 UTC 时间戳;如果项目需要本地时区展示,需要自定义 provider / logger而不是假定当前实现会自动转换
### 在 Model 中使用日志
## 继续阅读
```csharp
using GFramework.Core.Model;
using GFramework.Core.Logging;
using GFramework.Core.Abstractions.Logging;
public class PlayerModel : AbstractModel
{
private ILogger _logger;
private int _health;
protected override void OnInit()
{
_logger = LoggerFactoryResolver.Provider.CreateLogger("PlayerModel");
_logger.Info("玩家模型初始化");
_health = 100;
}
public void SetHealth(int value)
{
var oldHealth = _health;
_health = value;
_logger.Debug("玩家生命值变化: {0} -> {1}", oldHealth, _health);
if (_health <= 0)
{
_logger.Warn("玩家生命值归零");
}
}
}
```
### 条件日志记录
```csharp
var logger = LoggerFactoryResolver.Provider.CreateLogger("PerformanceMonitor");
// 检查日志级别是否启用,避免不必要的字符串格式化
if (logger.IsDebugEnabled())
{
var stats = CalculateComplexStats(); // 耗时操作
logger.Debug("性能统计: {0}", stats);
}
// 简化写法
if (logger.IsTraceEnabled())
{
logger.Trace("详细的执行流程信息");
}
```
### 分类日志记录
```csharp
// 为不同模块创建独立的日志记录器
var networkLogger = LoggerFactoryResolver.Provider.CreateLogger("Network");
var databaseLogger = LoggerFactoryResolver.Provider.CreateLogger("Database");
var aiLogger = LoggerFactoryResolver.Provider.CreateLogger("AI");
networkLogger.Info("连接到服务器");
databaseLogger.Debug("查询用户数据");
aiLogger.Trace("AI 决策树遍历");
```
### 自定义日志级别
```csharp
// 在开发环境使用 Debug 级别
#if DEBUG
LoggerFactoryResolver.Provider = new GodotLoggerFactoryProvider
{
MinLevel = LogLevel.Debug
};
#else
// 在生产环境使用 Info 级别
LoggerFactoryResolver.Provider = new GodotLoggerFactoryProvider
{
MinLevel = LogLevel.Info
};
#endif
```
### 在 Godot 模块中使用日志
```csharp
using GFramework.Godot.Architecture;
using GFramework.Core.Logging;
using GFramework.Core.Abstractions.Logging;
using Godot;
public class SceneModule : AbstractGodotModule
{
private ILogger _logger;
private Node _sceneRoot;
public override Node Node => _sceneRoot;
public SceneModule()
{
_sceneRoot = new Node { Name = "SceneRoot" };
_logger = LoggerFactoryResolver.Provider.CreateLogger("SceneModule");
}
public override void Install(IArchitecture architecture)
{
_logger.Info("场景模块安装开始");
// 安装场景系统
var sceneSystem = new SceneSystem();
architecture.RegisterSystem<ISceneSystem>(sceneSystem);
_logger.Info("场景模块安装完成");
}
public override void OnPhase(ArchitecturePhase phase, IArchitecture architecture)
{
_logger.Debug("场景模块阶段: {0}", phase);
if (phase == ArchitecturePhase.Ready)
{
_logger.Info("场景模块已就绪");
}
}
public override void OnDetach()
{
_logger.Info("场景模块已分离");
_sceneRoot?.QueueFree();
}
}
```
## 日志输出格式
### 输出格式说明
Godot 日志系统使用以下格式输出日志:
```
[时间戳] 日志级别 [日志器名称] 日志消息
```
**示例输出**
```
[2025-01-09 10:30:45.123] INFO [GameArchitecture] 游戏架构初始化开始
[2025-01-09 10:30:45.456] DEBUG [Player] 玩家位置: (100, 200)
[2025-01-09 10:30:46.789] WARNING [CombatSystem] 高伤害攻击: 150
[2025-01-09 10:30:47.012] ERROR [SaveSystem] 保存游戏失败
```
### 日志级别与 Godot 输出方法
| 日志级别 | Godot 方法 | 颜色 | 说明 |
|-------------|------------------|----|----------|
| **Trace** | `GD.PrintRich` | 灰色 | 最详细的跟踪信息 |
| **Debug** | `GD.PrintRich` | 青色 | 调试信息 |
| **Info** | `GD.Print` | 白色 | 一般信息 |
| **Warning** | `GD.PushWarning` | 黄色 | 警告信息 |
| **Error** | `GD.PrintErr` | 红色 | 错误信息 |
| **Fatal** | `GD.PushError` | 红色 | 致命错误 |
### 异常信息格式
当记录异常时,异常信息会附加到日志消息后:
```
[2025-01-09 10:30:47.012] ERROR [SaveSystem] 保存游戏失败
System.IO.IOException: 文件访问被拒绝
at SaveSystem.SaveGame() in SaveSystem.cs:line 42
```
## 最佳实践
1. **在架构初始化时配置日志系统**
```csharp
public GameArchitecture()
{
LoggerFactoryResolver.Provider = new GodotLoggerFactoryProvider
{
MinLevel = LogLevel.Debug
};
}
```
2. **为每个类创建独立的日志记录器**
```csharp
private ILogger _logger;
public override void _Ready()
{
_logger = LoggerFactoryResolver.Provider.CreateLogger(GetType().Name);
}
```
3. **使用合适的日志级别**
- `Trace`:详细的执行流程,仅在深度调试时使用
- `Debug`:调试信息,开发阶段使用
- `Info`:重要的业务流程和状态变化
- `Warning`:潜在问题但不影响功能
- `Error`:错误但程序可以继续运行
- `Fatal`:严重错误,程序无法继续
4. **检查日志级别避免性能损失**
```csharp
if (_logger.IsDebugEnabled())
{
var expensiveData = CalculateExpensiveData();
_logger.Debug("数据: {0}", expensiveData);
}
```
5. **提供有意义的上下文信息**
```csharp
// ✗ 不好
logger.Error("错误");
// ✓ 好
logger.Error("加载场景失败: SceneKey={0}, Path={1}", sceneKey, scenePath);
```
6. **记录异常时提供上下文**
```csharp
try
{
LoadScene(sceneKey);
}
catch (Exception ex)
{
logger.Error($"加载场景失败: {sceneKey}", ex);
}
```
7. **使用分类日志记录器**
```csharp
var networkLogger = LoggerFactoryResolver.Provider.CreateLogger("Network");
var aiLogger = LoggerFactoryResolver.Provider.CreateLogger("AI");
```
8. **在生命周期方法中记录关键事件**
```csharp
protected override void OnInit()
{
_logger.Info("系统初始化完成");
}
protected override void OnDestroy()
{
_logger.Info("系统已销毁");
}
```
## 性能考虑
1. **日志缓存**
- `GodotLoggerFactoryProvider` 使用 `CachedLoggerFactory` 缓存日志记录器实例
- 相同名称和级别的日志记录器会被复用
2. **级别检查**
- 日志方法会自动检查日志级别
- 低于最小级别的日志不会被处理
3. **字符串格式化**
- 使用参数化日志避免不必要的字符串拼接
```csharp
// ✗ 不好 - 总是执行字符串拼接
logger.Debug("位置: " + position.ToString());
// ✓ 好 - 只在 Debug 启用时格式化
logger.Debug("位置: {0}", position);
```
4. **条件日志**
- 对于耗时的数据计算,先检查日志级别
```csharp
if (logger.IsDebugEnabled())
{
var stats = CalculateComplexStats();
logger.Debug("统计: {0}", stats);
}
```
## 常见问题
### 问题:如何配置 Godot 日志系统?
**解答**
在架构构造函数中配置日志提供程序:
```csharp
public GameArchitecture()
{
LoggerFactoryResolver.Provider = new GodotLoggerFactoryProvider
{
MinLevel = LogLevel.Debug
};
}
```
### 问题:日志没有输出到 Godot 控制台?
**解答**
检查以下几点:
1. 确认已配置 `GodotLoggerFactoryProvider`
2. 检查日志级别是否低于最小级别
3. 确认使用了正确的日志记录器
```csharp
// 确认配置
LoggerFactoryResolver.Provider = new GodotLoggerFactoryProvider
{
MinLevel = LogLevel.Trace // 设置为最低级别测试
};
// 创建日志记录器
var logger = LoggerFactoryResolver.Provider.CreateLogger("Test");
logger.Info("测试日志"); // 应该能看到输出
```
### 问题:如何在不同环境使用不同的日志级别?
**解答**
使用条件编译或环境检测:
```csharp
public GameArchitecture()
{
var minLevel = OS.IsDebugBuild() ? LogLevel.Debug : LogLevel.Info;
LoggerFactoryResolver.Provider = new GodotLoggerFactoryProvider
{
MinLevel = minLevel
};
}
```
### 问题:如何禁用某个模块的日志?
**解答**
为该模块创建一个高级别的日志记录器:
```csharp
// 只记录 Error 及以上级别
var logger = new GodotLogger("VerboseModule", LogLevel.Error);
```
### 问题:日志输出影响性能怎么办?
**解答**
1. 提高最小日志级别
2. 使用条件日志
3. 避免在高频调用的方法中记录日志
```csharp
// 提高日志级别
LoggerFactoryResolver.Provider = new GodotLoggerFactoryProvider
{
MinLevel = LogLevel.Warning // 只记录警告及以上
};
// 使用条件日志
if (_logger.IsDebugEnabled())
{
_logger.Debug("高频数据: {0}", data);
}
// 避免在 _Process 中频繁记录
public override void _Process(double delta)
{
// ✗ 不好 - 每帧都记录
// _logger.Debug("帧更新");
// ✓ 好 - 只在特定条件下记录
if (someErrorCondition)
{
_logger.Error("检测到错误");
}
}
```
### 问题:如何记录结构化日志?
**解答**
使用参数化日志或 `IStructuredLogger` 接口:
```csharp
// 参数化日志
logger.Info("玩家登录: UserId={0}, UserName={1}, Level={2}",
userId, userName, level);
// 使用结构化日志(如果实现了 IStructuredLogger
if (logger is IStructuredLogger structuredLogger)
{
structuredLogger.Log(LogLevel.Info, "玩家登录",
("UserId", userId),
("UserName", userName),
("Level", level));
}
```
## 相关文档
- [核心日志系统](/zh-CN/core/logging) - GFramework 核心日志功能
- [Godot 架构集成](/zh-CN/godot/architecture) - Godot 架构系统
- [Godot 扩展](/zh-CN/godot/extensions) - Godot 扩展方法
- [最佳实践](/zh-CN/best-practices/architecture-patterns) - 架构最佳实践
- [Core 日志系统](../core/logging.md)
- [日志生成器](../source-generators/logging-generator.md)
- [Godot 运行时集成](./index.md)
- [Godot 场景系统](./scene.md)
- [Godot UI 系统](./ui.md)

View File

@ -1,583 +1,321 @@
---
title: Godot 场景系统
description: Godot 场景系统提供了 GFramework 场景管理与 Godot 场景树的完整集成
description: 以当前 GFramework.Godot 源码、Game 场景契约与 CoreGrid 接线为准,说明 PackedScene 场景工厂、行为包装和最小接入路径
---
# Godot 场景系统
## 概述
`GFramework.Godot` 在场景这一层负责的是 Godot runtime 适配,而不是再提供一个 Godot 专属 router。
Godot 场景系统是 GFramework.Godot 中连接框架场景管理与 Godot 场景树的核心组件。它提供了场景行为封装、场景工厂、场景注册表等功能,让你可以在
Godot 项目中使用 GFramework 的场景管理系统。
当前真正参与场景接线的核心类型是:
通过 Godot 场景系统,你可以使用 GFramework 的场景路由、生命周期管理等功能,同时保持与 Godot 场景系统的完美兼容。
- `IGodotSceneRegistry` / `GodotSceneRegistry`
- `GodotSceneFactory`
- `SceneBehaviorFactory`
- `SceneBehaviorBase<T>` 及其 `Node2D` / `Node3D` / `Control` / `Generic` 实现
- 项目侧实现的 `ISceneRoot`
- 项目侧继承 `SceneRouterBase` 的 router
**主要特性**
也就是说Godot 集成页的重点不是“再造一套场景导航 API”而是把 `PackedScene``Node``GFramework.Game`
`ISceneRouter` / `ISceneBehavior` 契约接起来。
- 场景行为封装SceneBehavior
- 场景工厂和注册表
- 与 Godot PackedScene 集成
- 多种场景行为类型Node2D、Node3D、Control
- 场景生命周期管理
- 场景根节点管理
## 当前公开入口
## 核心概念
### `IGodotSceneRegistry`
### 场景行为
Godot 侧的场景资源表,底层是 `IAssetRegistry<PackedScene>`。它只负责:
`SceneBehaviorBase<T>` 封装了 Godot 节点的场景行为:
- `sceneKey -> PackedScene` 映射
- 让 `GodotSceneFactory` 能按 key 实例化场景
框架当前不会自动扫描项目里的 `.tscn` 文件并填充 registry。
### `GodotSceneFactory`
`GodotSceneFactory.Create(string sceneKey)` 的当前行为很明确:
1. 从 `IGodotSceneRegistry` 取出 `PackedScene`
2. 调用 `Instantiate()`
3. 如果节点实现了 `ISceneBehaviorProvider`,优先返回 `provider.GetScene()`
4. 否则回退到 `SceneBehaviorFactory.Create(node, sceneKey)`
这和旧文档里“必须有 Godot 专属 router / 专属 scene provider 才能工作”的说法不同。当前源码允许两条路径:
- 显式 provider项目自己决定行为对象
- 自动包装:按节点类型回退到默认 behavior
### `SceneBehaviorBase<T>`
`SceneBehaviorBase<T>` 是当前 Godot 场景行为包装基类。它把 `ISceneBehavior` 的生命周期接到 `Node` 上:
- `OnLoadAsync`
- `OnEnterAsync`
- `OnPauseAsync`
- `OnResumeAsync`
- `OnExitAsync`
- `OnUnloadAsync`
如果 owner 还实现了 `IScene`,这些阶段会继续转发到业务节点;如果没有实现 `IScene`,默认 behavior 仍会处理 Godot 节点的
process 开关和 `QueueFreeX()` 释放。
### `SceneBehaviorFactory`
自动包装的选择规则来自当前实现:
- `Node2D` -> `Node2DSceneBehavior`
- `Node3D` -> `Node3DSceneBehavior`
- `Control` -> `ControlSceneBehavior`
- 其他 `Node` -> `GenericSceneBehavior`
这意味着 Godot runtime 确实能“自动给节点补一个 behavior”但它不会替你补项目侧 router、root 或 registry。
## 最小接入路径
推荐按下面顺序接入。
### 1. 继续在项目层保留自己的 router
`GFramework.Godot` 当前没有 `GodotSceneRouter` 类型。消费者项目的实际做法,是在项目层继承
`GFramework.Game.Scene.SceneRouterBase`
`ai-libs/CoreGrid` 的 router 就是这样:
```csharp
public abstract class SceneBehaviorBase<T> : ISceneBehavior
where T : Node
using global::CoreGrid.global;
using LoggingTransitionHandler = GFramework.Game.Scene.Handler.LoggingTransitionHandler;
namespace CoreGrid.scripts.core.scene;
public partial class SceneRouter : SceneRouterBase
{
protected readonly T Owner;
public string Key { get; }
public IScene Scene { get; }
}
```
[GetUtility] private IGodotSceneRegistry _sceneRegistry = null!;
### 场景工厂
public Node? SceneRoot => Root as Node;
`GodotSceneFactory` 负责创建场景实例:
```csharp
public class GodotSceneFactory : ISceneFactory
{
public ISceneBehavior Create(string sceneKey);
}
```
### 场景注册表
`IGodotSceneRegistry` 管理场景资源:
```csharp
public interface IGodotSceneRegistry
{
void Register(string key, PackedScene scene);
PackedScene Get(string key);
}
```
## 基本用法
### 创建场景脚本
```csharp
using Godot;
using GFramework.Game.Abstractions.Scene;
public partial class MainMenuScene : Control, IScene
{
public async ValueTask OnLoadAsync(ISceneEnterParam? param)
protected override void RegisterHandlers()
{
GD.Print("加载主菜单资源");
await Task.CompletedTask;
}
public async ValueTask OnEnterAsync()
{
GD.Print("进入主菜单");
Show();
await Task.CompletedTask;
}
public async ValueTask OnPauseAsync()
{
GD.Print("暂停主菜单");
await Task.CompletedTask;
}
public async ValueTask OnResumeAsync()
{
GD.Print("恢复主菜单");
await Task.CompletedTask;
}
public async ValueTask OnExitAsync()
{
GD.Print("退出主菜单");
Hide();
await Task.CompletedTask;
}
public async ValueTask OnUnloadAsync()
{
GD.Print("卸载主菜单资源");
await Task.CompletedTask;
__InjectContextBindings_Generated();
RegisterHandler(new LoggingTransitionHandler());
RegisterAroundHandler(
new SceneTransitionAnimationHandler(() => SceneTransitionManager.Instance!, _sceneRegistry.GetAll()));
}
}
```
### 注册场景
这里可以看到Godot 适配点在 factory / registry / root / transition handler 上,而 router 仍然是项目类。
### 2. 注册 `IGodotSceneRegistry``ISceneFactory`
最小 wiring 需要把 registry 和 factory 装进架构:
```csharp
using GFramework.Godot.Scene;
using Godot;
public class GameSceneRegistry : GodotSceneRegistry
{
publieneRegistry()
{
// 注册场景资源
Register("MainMenu", GD.Load<PackedScene>("res://scenes/MainMenu.tscn"));
Register("Gameplay", GD.Load<PackedScene>("res://scenes/Gameplay.tscn"));
Register("Pause", GD.Load<PackedScene>("res://scenes/Pause.tscn"));
}
}
```
### 设置场景系统
```csharp
using GFramework.Godot.Architecture;
using GFramework.Godot.Scene;
public class GameArchitecture : AbstractArchitecture
{
protected override void InstallModules()
{
// 注册场景注册表
var sceneRegistry = new GameSceneRegistry();
RegisterUtility<IGodotSceneRegistry>(sceneRegistry);
// 注册场景工厂
var sceneFactory = new GodotSceneFactory();
RegisterUtility<ISceneFactory>(sceneFactory);
// 注册场景路由
var sceneRouter = new GodotSceneRouter();
RegisterSystem<ISceneRouter>(sceneRouter);
}
}
```
### 使用场景路由
```csharp
using Godot;
using GFramework.Godot.Extensions;
public partial class GameController : Node
{
public override void _Ready()
{
// 切换到主菜单
SwitchToMainMenu();
}
private async void SwitchToMainMenu()
{
var sceneRouter = this.GetSystem<ISceneRouter>();
await sceneRouter.ReplaceAsync("MainMenu");
}
private async void StartGame()
{
var sceneRouter = this.GetSystem<ISceneRouter>();
await sceneRouter.ReplaceAsync("Gameplay");
}
private async void ShowPause()
{
var sceneRouter = this.GetSystem<ISceneRouter>();
await sceneRouter.PushAsync("Pause");
}
}
```
## 高级用法
### 使用场景行为提供者
```csharp
using Godot;
using GFramework.Game.Abstractions.Scene;
using GFramework.Godot.Scene;
using Godot;
public partial class GameplayScene : Node2D, ISceneBehaviorProvider
public sealed class GameSceneRegistry : GodotSceneRegistry
{
private GameplaySceneBehavior _behavior;
public GameSceneRegistry()
{
Register(nameof(SceneKey.MainMenu), GD.Load<PackedScene>("res://scenes/main_menu.tscn"));
Register(nameof(SceneKey.Gameplay), GD.Load<PackedScene>("res://scenes/gameplay.tscn"));
}
}
architecture.RegisterUtility<IGodotSceneRegistry>(new GameSceneRegistry());
architecture.RegisterUtility<ISceneFactory>(new GodotSceneFactory());
architecture.RegisterSystem(new SceneRouter());
```
项目用什么 key 类型、资源目录或配置表都可以,但最终要能落到 `sceneKey -> PackedScene`
### 3. 提供 `ISceneRoot`
`SceneRouterBase` 只负责切换编排,真正把场景节点挂到 Godot 场景树的是项目自己的 `ISceneRoot`
CoreGrid 的 `SceneRoot` 当前做了两件关键事:
- 在 `_Ready()` 时调用 `_sceneRouter.BindRoot(this)`
- 在 `AddScene` / `RemoveScene` 里把 `scene.Original` 当作 `Node` 挂入或移出树
最小形态可以写成:
```csharp
public sealed class SceneRoot : Node2D, ISceneRoot
{
[GetSystem] private ISceneRouter _sceneRouter = null!;
public override void _Ready()
{
_behavior = new GameplaySceneBehavior(this, "Gameplay");
__InjectContextBindings_Generated();
_sceneRouter.BindRoot(this);
}
public void AddScene(ISceneBehavior scene)
{
if (scene.Original is not Node node)
throw new InvalidOperationException("SceneBehavior must inherit Godot Node.");
if (node.GetParent() == null)
AddChild(node);
}
public void RemoveScene(ISceneBehavior scene)
{
if (scene.Original is Node node && node.GetParent() == this)
RemoveChild(node);
}
}
```
### 4. 让场景节点提供 behavior
当前有两种可行方式。
#### 方式 A实现 `ISceneBehaviorProvider`
如果你想显式控制 behavior 类型,直接实现 `GetScene()`
```csharp
public partial class GameplayRoot : Node2D, ISceneBehaviorProvider, IScene
{
private ISceneBehavior? _scene;
public ISceneBehavior GetScene()
{
return _behavior;
}
}
// 自定义场景行为
public class GameplaySceneBehavior : Node2DSceneBehavior
{
public GameplaySceneBehavior(Node2D owner, string key) : base(owner, key)
{
return _scene ??= SceneBehaviorFactory.Create(this, nameof(SceneKey.Gameplay));
}
protected override async ValueTask OnLoadInternalAsync(ISceneEnterParam? param)
public ValueTask OnLoadAsync(ISceneEnterParam? param)
{
GD.Print("加载游戏场景");
// 加载游戏资源
await Task.CompletedTask;
return ValueTask.CompletedTask;
}
protected override async ValueTask OnEnterInternalAsync()
public ValueTask OnEnterAsync()
{
GD.Print("进入游戏场景");
Owner.Show();
await Task.CompletedTask;
return ValueTask.CompletedTask;
}
public ValueTask OnPauseAsync()
{
return ValueTask.CompletedTask;
}
public ValueTask OnResumeAsync()
{
return ValueTask.CompletedTask;
}
public ValueTask OnExitAsync()
{
return ValueTask.CompletedTask;
}
public ValueTask OnUnloadAsync()
{
return ValueTask.CompletedTask;
}
}
```
### 不同类型的场景行为
#### 方式 B`[AutoScene]` 让生成器补样板
当前更贴近真实消费者 wiring 的方式,是让 `GFramework.Godot.SourceGenerators` 生成 `SceneKeyStr``GetScene()`
```csharp
// Node2D 场景
public class Node2DSceneBehavior : SceneBehaviorBase<Node2D>
{
public Node2DSceneBehavior(Node2D owner, string key) : base(owner, key)
{
}
}
// Node3D 场景
public class Node3DSceneBehavior : SceneBehaviorBase<Node3D>
{
public Node3DSceneBehavior(Node3D owner, string key) : base(owner, key)
{
}
}
// Control 场景UI
public class ControlSceneBehavior : SceneBehaviorBase<Control>
{
public ControlSceneBehavior(Control owner, string key) : base(owner, key)
{
}
}
```
### 场景根节点管理
```csharp
using Godot;
using GFramework.Godot.Scene;
public partial class SceneRoot : Node, ISceneRoot
{
private Node _currentSceneNode;
public void AttachScene(Node sceneNode)
{
// 移除旧场景
if (_currentSceneNode != null)
{
RemoveChild(_currentSceneNode);
_currentSceneNode.QueueFree();
}
// 添加新场景
_currentSceneNode = sceneNode;
AddChild(_currentSceneNode);
}
public void DetachScene(Node sceneNode)
{
if (_currentSceneNode == sceneNode)
{
RemoveChild(_currentSceneNode);
_currentSceneNode = null;
}
}
}
```
### 场景参数传递
```csharp
// 定义场景参数
public class GameplayEnterParam : ISceneEnterParam
{
public int Level { get; set; }
public string Difficulty { get; set; }
}
// 在场景中接收参数
public partial class GameplayScene : Node2D, IScene
{
private int _level;
private string _difficulty;
public async ValueTask OnLoadAsync(ISceneEnterParam? param)
{
if (param is GameplayEnterParam gameplayParam)
{
_level = gameplayParam.Level;
_difficulty = gameplayParam.Difficulty;
GD.Print($"加载关卡 {_level},难度: {_difficulty}");
}
await Task.CompletedTask;
}
// ... 其他生命周期方法
}
// 切换场景时传递参数
var sceneRouter = this.GetSystem<ISceneRouter>();
await sceneRouter.ReplaceAsync("Gameplay", new GameplayEnterParam
{
Level = 1,
Difficulty = "Normal"
});
```
### 场景预加载
```csharp
public partial class LoadingScene : Control
{
public override async void _Ready()
{
// 预加载下一个场景
await PreloadNextScene();
// 切换到预加载的场景
var sceneRouter = this.GetSystem<ISceneRouter>();
await sceneRouter.ReplaceAsync("Gameplay");
}
private async Task PreloadNextScene()
{
var sceneFactory = this.GetUtility<ISceneFactory>();
var sceneBehavior = sceneFactory.Create("Gameplay");
// 预加载场景资源
await sceneBehavior.LoadAsync(null);
GD.Print("场景预加载完成");
}
}
```
### 场景转换动画
```csharp
using Godot;
using GFramework.Game.Abstractions.Scene;
using GFramework.Godot.SourceGenerators.Abstractions.UI;
using Godot;
public class FadeTransitionHandler : ISceneTransitionHandler
[AutoScene(nameof(SceneKey.Gameplay))]
public partial class GameplayRoot : Node2D, ISceneBehaviorProvider, IScene
{
private ColorRect _fadeRect;
public FadeTransitionHandler(ColorRect fadeRect)
public ValueTask OnLoadAsync(ISceneEnterParam? param)
{
_fadeRect = fadeRect;
return ValueTask.CompletedTask;
}
public async ValueTask OnBeforeExitAsync(SceneTransitionEvent @event)
public ValueTask OnEnterAsync()
{
// 淡出动画
var tween = _fadeRect.CreateTween();
tween.TweenProperty(_fadeRect, "modulate:a", 1.0f, 0.3f);
await tween.ToSignal(tween, Tween.SignalName.Finished);
return ValueTask.CompletedTask;
}
public async ValueTask OnAfterEnterAsync(SceneTransitionEvent @event)
public ValueTask OnPauseAsync()
{
// 淡入动画
var tween = _fadeRect.CreateTween();
tween.TweenProperty(_fadeRect, "modulate:a", 0.0f, 0.3f);
await tween.ToSignal(tween, Tween.SignalName.Finished);
return ValueTask.CompletedTask;
}
// ... 其他方法
}
```
### 场景间通信
```csharp
// 通过事件通信
public partial class GameplayScene : Node2D, IScene
{
public async ValueTask OnEnterAsync()
public ValueTask OnResumeAsync()
{
// 发送场景进入事件
this.SendEvent(new GameplaySceneEnteredEvent());
await Task.CompletedTask;
}
}
// 在其他地方监听
public partial class HUD : Control
{
public override void _Ready()
{
this.RegisterEvent<GameplaySceneEnteredEvent>(OnGameplayEntered);
return ValueTask.CompletedTask;
}
private void OnGameplayEntered(GameplaySceneEnteredEvent evt)
public ValueTask OnExitAsync()
{
GD.Print("游戏场景已进入,显示 HUD");
Show();
return ValueTask.CompletedTask;
}
public ValueTask OnUnloadAsync()
{
return ValueTask.CompletedTask;
}
}
```
## 最佳实践
1. **场景脚本实现 IScene 接口**:获得完整的生命周期管理
```csharp
✓ public partial class MyScene : Node2D, IScene { }
✗ public partial class MyScene : Node2D { } // 无生命周期管理
```
2. **使用场景注册表管理场景资源**:集中管理所有场景
```csharp
public class GameSceneRegistry : GodotSceneRegistry
{
public GameSceneRegistry()
{
Register("MainMenu", GD.Load<PackedScene>("res://scenes/MainMenu.tscn"));
Register("Gameplay", GD.Load<PackedScene>("res://scenes/Gameplay.tscn"));
}
}
```
3. **在 OnLoadAsync 中加载资源**:避免场景切换卡顿
```csharp
public async ValueTask OnLoadAsync(ISceneEnterParam? param)
{
// 异步加载资源
await LoadTexturesAsync();
await LoadAudioAsync();
}
```
4. **使用场景根节点管理场景树**:保持场景树结构清晰
```csharp
// 创建场景根节点
var sceneRoot = new Node { Name = "SceneRoot" };
AddChild(sceneRoot);
// 绑定到场景路由
sceneRouter.BindRoot(sceneRoot);
```
5. **正确清理场景资源**:在 OnUnloadAsync 中释放资源
```csharp
public async ValueTask OnUnloadAsync()
{
// 释放资源
_texture?.Dispose();
_audioStream?.Dispose();
await Task.CompletedTask;
}
```
6. **使用场景参数传递数据**:避免使用全局变量
```csharp
✓ await sceneRouter.ReplaceAsync("Gameplay", new GameplayEnterParam { Level = 1 });
✗ GlobalData.CurrentLevel = 1; // 避免全局状态
```
## 常见问题
### 问题:如何在 Godot 场景中使用 GFramework
**解答**
场景脚本实现 `IScene` 接口:
生成器当前会补出与源码一致的 `GetScene()`
```csharp
public partial class MyScene : Node2D, IScene
public ISceneBehavior GetScene()
{
public async ValueTask OnLoadAsync(ISceneEnterParam? param) { }
public async ValueTask OnEnterAsync() { }
// ... 实现其他方法
return __autoSceneBehavior_Generated ??= SceneBehaviorFactory.Create(this, SceneKeyStr);
}
```
### 问题:场景切换时节点如何管理?
要注意两点:
**解答**
使用场景根节点管理:
- `[AutoScene]` 只生成方法和 key不会替你自动补 `: ISceneBehaviorProvider`
- `IScene` 仍然是业务生命周期契约;不实现它时,默认 behavior 只会保留基础节点切换语义
### 5. 从业务代码发起导航
一旦 registry、factory、router、root 都装好,导航入口仍然是 `ISceneRouter`
```csharp
// 场景路由会自动管理节点的添加和移除
await sceneRouter.ReplaceAsync("NewScene");
// 旧场景节点会被移除,新场景节点会被添加
await sceneRouter.ReplaceAsync(nameof(SceneKey.MainMenu));
await sceneRouter.ReplaceAsync(nameof(SceneKey.Gameplay), new GameplayEnterParam());
await sceneRouter.PushAsync(nameof(SceneKey.PauseMenu));
await sceneRouter.PopAsync();
```
### 问题:如何实现场景预加载?
## 当前边界
**解答**
使用场景工厂提前创建场景:
### 没有 `GodotSceneRouter`
```csharp
var sceneFactory = this.GetUtility<ISceneFactory>();
var sceneBehavior = sceneFactory.Create("NextScene");
await sceneBehavior.LoadAsync(null);
```
仓库当前不存在 `GodotSceneRouter` 类型。旧文档里把它写成默认入口是失真的;实际入口仍然是项目侧继承
`SceneRouterBase` 的 router。
### 问题:场景生命周期方法的调用顺序是什么?
### 没有自动注册所有场景
**解答**
当前运行时只认识你注册进 `IGodotSceneRegistry``PackedScene`。它不会扫描目录、不会从脚本类型自动反推出注册表。
- 进入场景:`OnLoadAsync` -> `OnEnterAsync` -> `OnShow`
- 暂停场景:`OnPause` -> `OnHide`
- 恢复场景:`OnShow` -> `OnResume`
- 退出场景:`OnHide` -> `OnExitAsync` -> `OnUnloadAsync`
### provider 是“优先路径”,不是“唯一路径”
### 问题:如何在场景中访问架构组件?
`GodotSceneFactory` 会优先使用 `ISceneBehaviorProvider`,但没有 provider 时仍会按节点类型自动包装。这个行为和 UI 系统不同;
UI 工厂当前没有同等的自动回退。
**解答**
使用扩展方法:
### root 仍然是项目职责
```csharp
public partial class MyScene : Node2D, IScene
{
public async ValueTask OnEnterAsync()
{
var playerModel = this.GetModel<PlayerModel>();
var gameSystem = this.GetSystem<GameSystem>();
await Task.CompletedTask;
}
}
```
`ISceneRoot` 的实现决定:
### 问题:场景切换时如何显示加载界面?
- 节点挂到哪里
- 移除时如何释放
- 是否保留额外的当前视图引用
**解答**
使用场景转换处理器:
Godot runtime 不会替项目生成统一的 root 节点。
```csharp
public class LoadingScreenHandler : ISceneTransitionHandler
{
public async ValueTask OnBeforeLoadAsync(SceneTransitionEvent @event)
{
// 显示加载界面
ShowLoadingScreen();
await Task.CompletedTask;
}
## 继续阅读
public async ValueTask OnAfterEnterAsync(SceneTransitionEvent @event)
{
// 隐藏加载界面
HideLoadingScreen();
await Task.CompletedTask;
}
}
```
## 相关文档
- [场景系统](/zh-CN/game/scene) - 核心场景管理
- [Godot 架构集成](/zh-CN/godot/architecture) - Godot 架构基础
- [Godot UI 系统](/zh-CN/godot/ui) - Godot UI 集成
- [Godot 扩展](/zh-CN/godot/extensions) - Godot 扩展方法
1. [Godot 运行时集成](./index.md)
2. [Godot 架构集成](./architecture.md)
3. [Game 场景系统](../game/scene.md)
4. [AutoScene 生成器](../source-generators/auto-scene-generator.md)

View File

@ -1,420 +1,158 @@
# 信号连接系统 (Signal Connection System)
---
title: Godot 信号系统
description: 以当前 GFramework.Godot 源码与 CoreGrid 的动态绑定用法为准,说明 Signal(...) fluent API、SignalBuilder 行为与接入边界。
---
## 概述
# Godot 信号系统
信号连接系统是 Godot 扩展方法模块中的一个专门子模块,提供流畅、类型安全的 Godot 信号连接 API。该系统采用构建器模式Builder
Pattern和流畅接口设计Fluent Interface大大简化了信号订阅代码提高了代码的可读性和可维护性
`GFramework.Godot` 当前提供的信号能力很收敛:它不是另一套事件系统,也不是自动生成绑定代码的入口,而是对
`GodotObject.Connect(...)` 的一层 fluent 包装
## 核心类
当前真正公开的入口只有两个:
### SignalBuilder
- `SignalFluentExtensions.Signal(...)`
- `SignalBuilder`
信号连接构建器,负责构建和执行信号连接操作。
如果你需要的是场景节点字段注入和静态 signal 自动绑订,请看
`GFramework.Godot.SourceGenerators``[GetNode]``[BindNodeSignal]`,不要把它们和这里的运行时 fluent API 混成同一层。
**特性:**
## 当前公开入口
- 支持链式调用
- 可配置连接标志
- 支持连接后立即调用
- 返回原始对象以便继续操作
### `Signal(...)`
### SignalFluentExtensions
`GodotObject` 提供信号连接扩展方法,创建 `SignalBuilder` 实例。
## 架构设计
```mermaid
graph TD
A[GodotObject] --> B[SignalFluentExtensions]
B --> C[Signal Extension Method]
C --> D[SignalBuilder]
D --> E[WithFlags]
D --> F[To]
D --> G[ToAndCall]
D --> H[End]
F --> I[Connect Signal]
G --> J[Connect + Call]
H --> K[Return GodotObject]
L[ConnectFlags] --> E
M[Callable] --> F
M --> G
```
## 使用示例
### 基本信号连接
```csharp
// 基本连接
button.Signal(Button.SignalName.Pressed)
.To(new Callable(this, nameof(OnButtonPressed)));
// 带连接标志
timer.Signal(Timer.SignalName.Timeout)
.WithFlags(GodotObject.ConnectFlags.OneShot)
.To(new Callable(this, nameof(OnTimerTimeout)));
```
### 连接并立即调用
```csharp
// 连接信号并立即调用一次
button.Signal(Button.SignalName.Pressed)
.ToAndCall(new Callable(this, nameof(OnButtonPressed)));
// 连接带参数的信号并立即调用
area2D.Signal(Area2D.SignalName.BodyEntered)
.ToAndCall(new Callable(this, nameof(OnBodyEntered)), new Variant[] { node });
```
### 复杂的连接链
```csharp
// 设置连接标志并连接
player.Signal(Player.SignalName.HealthChanged)
.WithFlags(GodotObject.ConnectFlags.Deferred)
.To(new Callable(this, nameof(OnHealthChanged)));
// 连接多个信号
var button = GetNode<Button>("StartButton");
button.Signal(Button.SignalName.Pressed)
.WithFlags(GodotObject.ConnectFlags.OneShot)
.ToAndCall(new Callable(this, nameof(OnGameStarted)));
```
## API 详细说明
### SignalBuilder 构造函数
```csharp
public SignalBuilder(GodotObject target, StringName signal)
```
**参数:**
- `target` - 要连接信号的 Godot 对象
- `signal` - 要连接的信号名称
### SignalBuilder 方法
#### WithFlags
设置连接标志。
```csharp
public SignalBuilder WithFlags(GodotObject.ConnectFlags flags)
```
**参数:**
- `flags` - Godot 连接标志枚举值
**常用的连接标志:**
- `ConnectFlags.Deferred` - 延迟调用
- `ConnectFlags.OneShot` - 一次性连接
- `ConnectFlags.ConnectPersisted` - 连接持久化
- `ConnectFlags.ReferenceCounted` - 引用计数
#### To
连接信号到指定的可调用对象。
```csharp
public SignalBuilder To(Callable callable, GodotObject.ConnectFlags? flags = null)
```
**参数:**
- `callable` - 要连接的可调用对象
- `flags` - 可选的连接标志,覆盖之前设置的标志
#### ToAndCall
连接信号并立即调用一次。
```csharp
public SignalBuilder ToAndCall(Callable callable, GodotObject.ConnectFlags? flags = null, params Variant[] args)
```
**参数:**
- `callable` - 要连接的可调用对象
- `flags` - 可选的连接标志
- `args` - 调用时传递的参数
#### End
显式结束构建,返回原始对象。
```csharp
public GodotObject End()
```
### SignalFluentExtensions 扩展方法
#### Signal
为 Godot 对象创建信号构建器。
`Signal(...)` 是定义在 `GodotObject` 上的扩展方法:
```csharp
public static SignalBuilder Signal(this GodotObject @object, StringName signal)
```
**参数:**
它只做一件事:基于目标对象和 signal 名称创建一个 `SignalBuilder`。这意味着当前 fluent API 不只适用于 `Node`,也适用于
其他 Godot 对象。
- `@object` - 扩展方法的目标对象
- `signal` - 要连接的信号名称
### `SignalBuilder`
## 实际应用场景
`SignalBuilder` 的当前行为来自运行时代码本身:
### UI 事件处理
- `WithFlags(GodotObject.ConnectFlags flags)`
- 把 flags 保存到 builder 内部,作为后续 `To(...)` / `ToAndCall(...)` 的默认连接选项
- `To(Callable callable, GodotObject.ConnectFlags? flags = null)`
- 优先使用参数传入的 flags如果没有再回退到之前 `WithFlags(...)` 保存的值
- 最终直接调用 `target.Connect(signal, callable)``target.Connect(signal, callable, (uint)flags)`
- `ToAndCall(Callable callable, GodotObject.ConnectFlags? flags = null, params Variant[] args)`
- 先执行 `To(...)`
- 再立即执行一次 `callable.Call(args)`
- `End()`
- 返回原始 `GodotObject`
- 主要用于在 fluent 语句结束后重新拿回目标对象,而不是增加新的信号语义
可以把它理解成“对原生 `Connect(...)` 做顺手的链式包装”,而不是带订阅管理、自动解绑、诊断系统的高层抽象。
## 最小接入路径
### 1. 动态绑定时直接用 `Signal(...)`
适合这类场景:
- 运行时创建的节点或弹窗
- signal 名称需要按条件选择
- 你就是想保留手写 `Callable` 的控制权
最小示例:
```csharp
public class MainMenu : Control
using GFramework.Godot.Extensions.Signal;
using Godot;
public partial class SettingsPanel : Control
{
public override void _Ready()
{
var startButton = GetNode<Button>("StartButton");
var quitButton = GetNode<Button>("QuitButton");
var settingsButton = GetNode<Button>("SettingsButton");
// 开始按钮 - 一次性连接并立即禁用
startButton.Signal(Button.SignalName.Pressed)
.WithFlags(GodotObject.ConnectFlags.OneShot)
.ToAndCall(new Callable(this, nameof(OnStartPressed)));
// 退出按钮
quitButton.Signal(Button.SignalName.Pressed)
.To(new Callable(this, nameof(OnQuitPressed)));
// 设置按钮 - 延迟调用避免嵌套问题
settingsButton.Signal(Button.SignalName.Pressed)
.WithFlags(GodotObject.ConnectFlags.Deferred)
.To(new Callable(this, nameof(OnSettingsPressed)));
var applyButton = GetNode<Button>("%ApplyButton");
applyButton.Signal(Button.SignalName.Pressed)
.To(Callable.From(OnApplyPressed));
}
private void OnStartPressed()
private void OnApplyPressed()
{
GetTree().ChangeSceneToFile("res://scenes/game.tscn");
}
private void OnQuitPressed()
{
GetTree().Quit();
}
private void OnSettingsPressed()
{
// 打开设置面板
GetNode<Control>("SettingsPanel").Show();
}
}
```
### 游戏逻辑事件
### 2. 需要连接 flags 时,用 `WithFlags(...)`
`SignalBuilder` 不会解释 flags 的业务含义,只是把它们原样传给 Godot。
```csharp
public class Player : CharacterBody2D
button.Signal(Button.SignalName.Pressed)
.WithFlags(GodotObject.ConnectFlags.OneShot)
.To(Callable.From(OnStartPressed));
```
如果某一次连接想覆盖默认 flags可以直接在 `To(...)` / `ToAndCall(...)` 上传第二个参数。
### 3. 只有在“连接后立即跑一次”时才用 `ToAndCall(...)`
`ToAndCall(...)` 的语义很直接:先连,再立刻调一次 handler。它适合“先补一次当前状态再继续监听变化”的场景。
```csharp
slider.Signal(Range.SignalName.ValueChanged)
.ToAndCall(Callable.From<double>(OnVolumeChanged), args: [(Variant)slider.Value]);
```
这类调用要求 handler 对“初始化时主动调用一次”是安全的;如果你的处理逻辑不是幂等的,继续用 `To(...)` 更稳妥。
### 4. 静态场景绑定优先交给 `[BindNodeSignal]`
`GFramework.Godot.SourceGenerators/README.md``ai-libs/CoreGrid` 的当前接法看,静态场景按钮、滑条、菜单项这类固定
节点,更常见的路径仍然是 `[BindNodeSignal]`
```csharp
[BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))]
private void OnStartPressed()
{
private HealthComponent _health;
private AnimationPlayer _animPlayer;
public override void _Ready()
{
_health = GetNode<HealthComponent>("HealthComponent");
_animPlayer = GetNode<AnimationPlayer>("AnimationPlayer");
// 生命值变化 - 延迟处理避免在动画中修改状态
_health.Signal(HealthComponent.SignalName.HealthChanged)
.WithFlags(GodotObject.ConnectFlags.Deferred)
.To(new Callable(this, nameof(OnHealthChanged)));
// 死亡事件 - 一次性连接
_health.Signal(HealthComponent.SignalName.Died)
.WithFlags(GodotObject.ConnectFlags.OneShot)
.To(new Callable(this, nameof(OnDied)));
}
private void OnHealthChanged(float newHealth, float maxHealth)
{
// 更新UI或状态
UpdateHealthBar(newHealth / maxHealth);
// 播放受伤动画
if (newHealth < _health.PreviousHealth)
{
_animPlayer.Play("hurt");
}
}
private void OnDied()
{
// 播放死亡动画
_animPlayer.Play("death");
// 游戏结束
GetTree().CallDeferred(SceneTree.MethodName.Quit);
}
}
```
### 音频管理
`Signal(...)` 更常出现在这些动态或补充性绑定里:
- 对话框确认 / 取消等运行时实例
- 运行时选出的 signal 名称
- 需要临时追加监听的 dock、panel、overlay
`ai-libs/CoreGrid` 当前就有这类用法:
```csharp
public class AudioManager : Node
{
private AudioStreamPlayer _bgmPlayer;
private AudioStreamPlayer _sfxPlayer;
public override void _Ready()
{
_bgmPlayer = GetNode<AudioStreamPlayer>("BGMPlayer");
_sfxPlayer = GetNode<AudioStreamPlayer>("SFXPlayer");
// 背景音乐播放完成
_bgmPlayer.Signal(AudioStreamPlayer.SignalName.Finished)
.To(new Callable(this, nameof(OnBGMFinished)));
// 音效播放完成 - 延迟清理
_sfxPlayer.Signal(AudioStreamPlayer.SignalName.Finished)
.WithFlags(GodotObject.ConnectFlags.Deferred)
.To(new Callable(this, nameof(OnSFXFinished)));
}
private void OnBGMFinished()
{
// 循环播放背景音乐
PlayBGM(_currentBGM);
}
private void OnSFXFinished()
{
// 清理音效资源或播放队列中的下一个音效
CleanupSFXResources();
}
}
_quitConfirmDialog.Signal("Confirmed")
.To(Callable.From(OnQuitConfirmDialogConfirmed))
.End();
```
## 设计模式分析
## 什么时候用 fluent API什么时候用生成器
### Builder Pattern
- 用 `Signal(...)`
- 动态节点
- 动态 signal 名称
- 想保留手写 `Callable` 和连接 flags
- 用 `[BindNodeSignal]`
- 节点字段和 signal 都是静态已知
- 你已经在用 `[GetNode]`
- 希望把 `_Ready()` 里的重复绑定样板交给生成器
SignalBuilder 实现了构建器模式:
这两条路径是互补关系,不是前后代际关系。当前源码没有“先用 `CreateSignalBuilder(...)`,再升级到生成器”这种迁移链。
- 分步构建复杂的信号连接
- 支持链式调用
- 延迟执行到最终调用时
## 当前边界
### Fluent Interface
- 当前入口是 `Signal(...)`,不是旧文档里的 `CreateSignalBuilder(...)`
- 这里不会自动生成 `_Ready()` / `_ExitTree()`,这类能力属于 `GFramework.Godot.SourceGenerators`
- `SignalBuilder` 不提供取消订阅 token也不会替你包装 `Disconnect(...)`
- `End()` 只返回原始对象,不会提交额外配置,也不是必须调用的终止步骤
- signal 名称是否合法、callable 签名是否匹配,仍然遵循 Godot 自身运行时规则
- `ToAndCall(...)` 会在完成连接后立刻执行 handler如果 handler 有副作用,需要你自己确认时机
流畅接口设计:
## 继续阅读
- 方法链式调用
- 可读性强
- 表达力强
### Extension Method Pattern
扩展方法模式:
- 为现有类型添加功能
- 不修改原始类
- 保持向后兼容
## 与原生 API 对比
### 原生 Godot API
```csharp
// 传统方式
button.Connect(Button.SignalName.Pressed, new Callable(this, nameof(OnButtonPressed)));
// 带标志的方式
button.Connect(Button.SignalName.Pressed, new Callable(this, nameof(OnButtonPressed)), (uint)GodotObject.ConnectFlags.OneShot);
// 连接并立即调用
button.Connect(Button.SignalName.Pressed, new Callable(this, nameof(OnButtonPressed)));
new Callable(this, nameof(OnButtonPressed)).Call();
```
### 信号连接系统 API
```csharp
// 流畅方式
button.Signal(Button.SignalName.Pressed)
.To(new Callable(this, nameof(OnButtonPressed)));
// 带标志的方式
button.Signal(Button.SignalName.Pressed)
.WithFlags(GodotObject.ConnectFlags.OneShot)
.To(new Callable(this, nameof(OnButtonPressed)));
// 连接并立即调用
button.Signal(Button.SignalName.Pressed)
.ToAndCall(new Callable(this, nameof(OnButtonPressed)));
```
## 性能考虑
### 内存分配
- SignalBuilder 是轻量级对象
- 创建开销很小
- 使用后可被垃圾回收
### 调用开销
- 与原生 API 性能基本相同
- 主要开销在方法链调用
- 运行时性能无差异
### 推荐做法
- 避免在热循环中创建大量 SignalBuilder
- 适合 UI 事件、游戏逻辑等场景
- 可以放心使用,性能影响可忽略
## 最佳实践
### 1. 选择合适的连接标志
```csharp
// UI 事件通常使用延迟调用
button.Signal(Button.SignalName.Pressed)
.WithFlags(GodotObject.ConnectFlags.Deferred)
.To(callable);
// 一次性事件使用一次性标志
dialog.Signal(CustomDialog.SignalName.Accepted)
.WithFlags(GodotObject.ConnectFlags.OneShot)
.To(callable);
```
### 2. 合理使用 ToAndCall
```csharp
// ✅ 适合:初始化时立即触发
settingsSlider.Signal(Slider.SignalName.ValueChanged)
.ToAndCall(new Callable(this, nameof(OnSettingsChanged)), initialSliderValue);
// ❌ 避免:重复连接并调用
button.Signal(Button.SignalName.Pressed)
.ToAndCall(new Callable(this, nameof(OnButtonPressed))); // 可能不必要
```
### 3. 链式调用可读性
```csharp
// ✅ 推荐:清晰的链式调用
player.Signal(Player.SignalName.HealthChanged)
.WithFlags(GodotObject.ConnectFlags.Deferred)
.To(new Callable(this, nameof(UpdateHealthUI)));
// ❌ 避免:过度嵌套
node.Signal(CustomSignal.Signal1).WithFlags(Flags1).To(callable1)
.Signal(CustomSignal.Signal2).WithFlags(Flags2).To(callable2);
```
- [Godot 运行时集成](./index.md)
- [Godot 扩展方法](./extensions.md)
- [Godot 集成教程](../tutorials/godot-integration.md)
- [BindNodeSignal 生成器](../source-generators/bind-node-signal-generator.md)

View File

@ -1,643 +1,351 @@
---
title: Godot UI 系统
description: Godot UI 系统提供了 GFramework UI 管理与 Godot Control 节点的完整集成
description: 以当前 GFramework.Godot 源码、Game UI 契约与 CoreGrid 接线为准,说明 PackedScene UI 工厂、页面行为和层级接入路径
---
# Godot UI 系统
## 概述
`GFramework.Godot.UI` 当前负责的是把 `GFramework.Game` 的 UI 路由契约接到 `Control` / `CanvasLayer` /
`PackedScene` 上,而不是定义一个 Godot 专属 router。
Godot UI 系统是 GFramework.Godot 中连接框架 UI 管理与 Godot Control 节点的核心组件。它提供了 UI 页面行为封装、UI 工厂、UI
注册表等功能,支持多层级 UI 显示,让你可以在 Godot 项目中使用 GFramework 的 UI 管理系统。
当前真正参与这条链路的核心类型是:
通过 Godot UI 系统,你可以使用 GFramework 的 UI 路由、生命周期管理、多层级显示等功能,同时保持与 Godot UI 系统的完美兼容。
- `IGodotUiRegistry` / `GodotUiRegistry`
- `GodotUiFactory`
- `CanvasItemUiPageBehaviorBase<T>`
- `UiPageBehaviorFactory`
- `Page` / `Overlay` / `Modal` / `Toast` / `Topmost` 五类 layer behavior
- 项目侧实现的 `IUiRoot`
- 项目侧继承 `UiRouterBase` 的 router
**主要特性**
## 当前公开入口
- UI 页面行为封装
- UI 工厂和注册表
- 与 Godot PackedScene 集成
- 多层级 UI 支持Page、Overlay、Modal、Toast、Topmost
- UI 生命周期管理
- UI 根节点管理
### `IGodotUiRegistry`
## 核心概念
Godot 侧 UI 资源表,底层是 `IAssetRegistry<PackedScene>`。它只负责:
### UI 页面行为
- `uiKey -> PackedScene` 映射
- 让 `GodotUiFactory` 可以按 key 实例化 UI 页面
`CanvasItemUiPageBehaviorBase<T>` 封装了 Godot Control 节点的 UI 行为:
框架当前不会自动扫描 `.tscn`、不会自动根据类型名补全注册表。
### `GodotUiFactory`
`GodotUiFactory.Create(string uiKey)` 的当前行为比场景工厂更严格:
1. 从 `IGodotUiRegistry` 取出 `PackedScene`
2. 调用 `Instantiate()`
3. 节点必须实现 `IUiPageBehaviorProvider`
4. 返回 `provider.GetPage()`
如果实例化得到的节点没有实现 `IUiPageBehaviorProvider`,当前实现会直接抛 `InvalidCastException`。这也是 UI 页面文档必须强调
`GetPage()` / `[AutoUiPage]` 的原因。
### `CanvasItemUiPageBehaviorBase<T>`
Godot runtime 的页面行为包装基类。它把 `IUiPageBehavior` 的这些语义接到 `CanvasItem` 上:
- `Key`
- `Layer`
- `Handle`
- `IsAlive`
- `IsVisible`
- `InteractionProfile`
- `OnEnter` / `OnExit`
- `OnPause` / `OnResume`
- `OnShow` / `OnHide`
- `TryHandleUiAction(UiInputAction action)`
如果 owner 同时实现了 `IUiPage``IUiInteractionProfileProvider``IUiActionHandler`,这些契约都会被页面行为继续利用。
### `UiPageBehaviorFactory`
当前 layer 到 behavior 的映射来自运行时代码本身:
- `UiLayer.Page` -> `PageLayerUiPageBehavior<T>`
- `UiLayer.Overlay` -> `OverlayLayerUiPageBehavior<T>`
- `UiLayer.Modal` -> `ModalLayerUiPageBehavior<T>`
- `UiLayer.Toast` -> `ToastLayerUiPageBehavior<T>`
- `UiLayer.Topmost` -> `TopmostLayerUiPageBehavior<T>`
几个容易被旧文档写偏的默认语义如下:
- `Page`
- 不可重入,阻断输入
- `Overlay`
- 可重入,非模态,不阻断输入;暂停时不会停掉节点处理
- `Modal`
- 可重入,模态,阻断输入
- `Toast`
- 可重入,非模态,不阻断输入
- `Topmost`
- 不可重入,模态,阻断输入
## 最小接入路径
### 1. 继续在项目层保留自己的 router
仓库当前不存在 `GodotUiRouter` 类型。实际做法仍然是项目侧继承 `GFramework.Game.UI.UiRouterBase`
`ai-libs/CoreGrid``UiRouter` 目前就是:
```csharp
public abstract class CanvasItemUiPageBehaviorBase<T> : IUiPageBehavior
where T : CanvasItem
using LoggingTransitionHandler = GFramework.Game.UI.Handler.LoggingTransitionHandler;
namespace CoreGrid.scripts.core.ui;
[Log]
public partial class UiRouter : UiRouterBase
{
protected readonly T Owner;
public string Key { get; }
public UiLayer Layer { get; }
public bool IsReentrant { get; }
protected override void RegisterHandlers()
{
_log.Debug("Registering default transition handlers");
RegisterHandler(new LoggingTransitionHandler());
}
}
```
### UI 工厂
Godot runtime 自身并不接管这层 router 的定义。
`GodotUiFactory` 负责创建 UI 实例:
### 2. 注册 `IGodotUiRegistry``IUiFactory`
最小 wiring 需要显式注册 UI 资源表和工厂:
```csharp
public class GodotUiFactory : IUiFactory
{
public IUiPageBehavior Create(string uiKey);
}
```
### UI 层级行为
不同层级的 UI 有不同的行为类:
```csharp
// Page 层(栈管理)
public class PageLayerUiPageBehavior : CanvasItemUiPageBehaviorBase<Control>
{
public override UiLayer Layer => UiLayer.Page;
public override bool IsReentrant => false;
}
// Modal 层(模态对话框)
public class ModalLayerUiPageBehavior : CanvasItemUiPageBehaviorBase<Control>
{
public override UiLayer Layer => UiLayer.Modal;
public override bool IsReentrant => true;
}
```
## 基本用法
### 创建 UI 脚本
```csharp
using Godot;
using GFramework.Game.Abstractions.UI;
using GFramework.Godot.UI;
using Godot;
public partial class MainMenuPage : Control, IUiPage
public sealed class GameUiRegistry : GodotUiRegistry
{
public GameUiRegistry()
{
Register(nameof(UiKey.MainMenu), GD.Load<PackedScene>("res://ui/main_menu.tscn"));
Register(nameof(UiKey.PauseMenu), GD.Load<PackedScene>("res://ui/pause_menu.tscn"));
Register(nameof(UiKey.OptionsMenu), GD.Load<PackedScene>("res://ui/options_menu.tscn"));
}
}
architecture.RegisterUtility<IGodotUiRegistry>(new GameUiRegistry());
architecture.RegisterUtility<IUiFactory>(new GodotUiFactory());
architecture.RegisterSystem(new UiRouter());
```
### 3. 提供 `IUiRoot`
`UiRouterBase` 只负责页面栈、layer UI、输入仲裁和暂停语义真正把页面挂到 Godot 容器的是项目自己的 `IUiRoot`
CoreGrid 当前的 `UiRoot` 做法和源码契约一致:
- 继承 `CanvasLayer`
- 为每个 `UiLayer` 创建一个 `Control` 容器
- 在 `_Ready()` 时调用 `_uiRouter.BindRoot(this)`
- 在 `AddUiPage` / `RemoveUiPage` 中处理 `CanvasItem` 挂载与释放
最小形态可以写成:
```csharp
public sealed class UiRoot : CanvasLayer, IUiRoot
{
[GetSystem] private IUiRouter _uiRouter = null!;
public override void _Ready()
{
__InjectContextBindings_Generated();
_uiRouter.BindRoot(this);
}
public void AddUiPage(IUiPageBehavior child)
{
AddUiPage(child, UiLayer.Page);
}
public void AddUiPage(IUiPageBehavior child, UiLayer layer, int orderInLayer = 0)
{
if (child.View is not CanvasItem item)
throw new InvalidOperationException("UIPage View must be a Godot Node");
AddChild(item);
item.ZIndex = (int)layer * 100 + orderInLayer;
}
public void RemoveUiPage(IUiPageBehavior child)
{
if (child.View is Node node && node.GetParent() == this)
RemoveChild(node);
}
}
```
### 4. 让页面节点提供 `GetPage()`
因为 `GodotUiFactory` 不会自动回退到默认 behavior页面节点必须显式提供 `GetPage()`
#### 方式 A手写 `IUiPageBehaviorProvider`
```csharp
public partial class PauseMenu : Control, IUiPage, IUiPageBehaviorProvider
{
private IUiPageBehavior? _page;
public IUiPageBehavior GetPage()
{
return _page ??= UiPageBehaviorFactory.Create(this, nameof(UiKey.PauseMenu), UiLayer.Modal);
}
public void OnEnter(IUiPageEnterParam? param)
{
GD.Print("进入主菜单");
Show();
}
public void OnExit()
{
GD.Print("退出主菜单");
Hide();
}
public void OnPause()
{
GD.Print("暂停主菜单");
}
public void OnResume()
{
GD.Print("恢复主菜单");
}
public void OnShow()
{
Show();
}
public void OnHide()
{
Hide();
}
}
```
### 实现 UI 页面行为提供者
#### 方式 B`[AutoUiPage]` 让生成器补样板
当前更贴近真实消费者 wiring 的方式,是让生成器产出 `UiKeyStr``GetPage()`
```csharp
using Godot;
using GFramework.Game.Abstractions.UI;
using GFramework.Godot.UI;
public partial class MainMenuPage : Control, IUiPageBehaviorProvider
{
private PageLayerUiPageBehavior _behavior;
public override void _Ready()
{
_behavior = new PageLayerUiPageBehavior(this, "MainMenu");
}
public IUiPageBehavior GetPage()
{
return _behavior;
}
}
```
### 注册 UI
```csharp
using GFramework.Godot.UI;
using GFramework.Godot.SourceGenerators.Abstractions.UI;
using Godot;
public class GameUiRegistry : GodotUiRegistry
{
public GameUiRegistry()
{
// 注册 UI 资源
Register("MainMenu", GD.Load<PackedScene>("res://ui/MainMenu.tscn"));
Register("Settings", GD.Load<PackedScene>("res://ui/Settings.tscn"));
Register("ConfirmDialog", GD.Load<PackedScene>("res://ui/ConfirmDialog.tscn"));
Register("Toast", GD.Load<PackedScene>("res://ui/Toast.tscn"));
}
}
```
### 设置 UI 系统
```csharp
using GFramework.Godot.Architecture;
using GFramework.Godot.UI;
public class GameArchitecture : AbstractArchitecture
{
protected override void InstallModules()
{
// 注册 UI 注册表
var uiRegistry = new GameUiRegistry();
RegisterUtility<IGodotUiRegistry>(uiRegistry);
// 注册 UI 工厂
var uiFactory = new GodotUiFactory();
RegisterUtility<IUiFactory>(uiFactory);
// 注册 UI 路由
var uiRouter = new GodotUiRouter();
RegisterSystem<IUiRouter>(uiRouter);
}
}
```
### 使用 UI 路由
```csharp
using Godot;
using GFramework.Godot.Extensions;
public partial class GameController : Node
{
public override void _Ready()
{
ShowMainMenu();
}
private async void ShowMainMenu()
{
var uiRouter = this.GetSystem<IUiRouter>();
await uiRouter.PushAsync("MainMenu");
}
private async void ShowSettings()
{
var uiRouter = this.GetSystem<IUiRouter>();
await uiRouter.PushAsync("Settings");
}
private void ShowDialog()
{
var uiRouter = this.GetSystem<IUiRouter>();
uiRouter.Show("ConfirmDialog", UiLayer.Modal);
}
private void ShowToast(string message)
{
var uiRouter = this.GetSystem<IUiRouter>();
uiRouter.Show("Toast", UiLayer.Toast, new ToastParam { Message = message });
}
}
```
## 高级用法
### 不同层级的 UI 行为
```csharp
// Page 层 UI栈管理不可重入
public partial class MainMenuPage : Control, IUiPageBehaviorProvider
{
public IUiPageBehavior GetPage()
{
return new PageLayerUiPageBehavior(this, "MainMenu");
}
}
// Overlay 层 UI浮层可重入
public partial class InfoPanel : Control, IUiPageBehaviorProvider
{
public IUiPageBehavior GetPage()
{
return new OverlayLayerUiPageBehavior(this, "InfoPanel");
}
}
// Modal 层 UI模态对话框可重入
public partial class ConfirmDialog : Control, IUiPageBehaviorProvider
{
public IUiPageBehavior GetPage()
{
return new ModalLayerUiPageBehavior(this, "ConfirmDialog");
}
}
// Toast 层 UI提示可重入
public partial class ToastMessage : Control, IUiPageBehaviorProvider
{
public IUiPageBehavior GetPage()
{
return new ToastLayerUiPageBehavior(this, "Toast");
}
}
// Topmost 层 UI顶层不可重入
public partial class LoadingScreen : Control, IUiPageBehaviorProvider
{
public IUiPageBehavior GetPage()
{
return new TopmostLayerUiPageBehavior(this, "Loading");
}
}
```
### UI 参数传递
```csharp
// 定义 UI 参数
public class ConfirmDialogParam : IUiPageEnterParam
{
public string Title { get; set; }
public string Message { get; set; }
public Action OnConfirm { get; set; }
public Action OnCancel { get; set; }
}
// 在 UI 中接收参数
public partial class ConfirmDialog : Control, IUiPage
{
private Label _titleLabel;
private Label _messageLabel;
private Action _onConfirm;
private Action _onCancel;
public override void _Ready()
{
_titleLabel = GetNode<Label>("Title");
_messageLabel = GetNode<Label>("Message");
GetNode<Button>("ConfirmButton").Pressed += OnConfirmPressed;
GetNode<Button>("CancelButton").Pressed += OnCancelPressed;
}
public void OnEnter(IUiPageEnterParam? param)
{
if (param is ConfirmDialogParam dialogParam)
{
_titleLabel.Text = dialogParam.Title;
_messageLabel.Text = dialogParam.Message;
_onConfirm = dialogParam.OnConfirm;
_onCancel = dialogParam.OnCancel;
}
Show();
}
private void OnConfirmPressed()
{
_onConfirm?.Invoke();
CloseDialog();
}
private void OnCancelPressed()
{
_onCancel?.Invoke();
CloseDialog();
}
private void CloseDialog()
{
var uiRouter = this.GetSystem<IUiRouter>();
if (Handle.HasValue)
{
uiRouter.Hide(Handle.Value, UiLayer.Modal, destroy: true);
}
}
// ... 其他生命周期方法
}
// 显示对话框
var uiRouter = this.GetSystem<IUiRouter>();
uiRouter.Show("ConfirmDialog", UiLayer.Modal, new ConfirmDialogParam
{
Title = "确认",
Message = "确定要退出吗?",
OnConfirm = () => GD.Print("确认"),
OnCancel = () => GD.Print("取消")
});
```
### UI 根节点管理
```csharp
using Godot;
using GFramework.Godot.UI;
public partial class UiRoot : CanvasLayer, IUiRoot
{
private Control _pageLayer;
private Control _overlayLayer;
private Control _modalLayer;
private Control _toastLayer;
private Control _topmostLayer;
public override void _Ready()
{
// 创建各层级容器
_pageLayer = new Control { Name = "PageLayer" };
_overlayLayer = new Control { Name = "OverlayLayer" };
_modalLayer = new Control { Name = "ModalLayer" };
_toastLayer = new Control { Name = "ToastLayer" };
_topmostLayer = new Control { Name = "TopmostLayer" };
AddChild(_pageLayer);
AddChild(_overlayLayer);
AddChild(_modalLayer);
AddChild(_toastLayer);
AddChild(_topmostLayer);
}
public void AttachPage(Control page, UiLayer layer)
{
var container = GetLayerContainer(layer);
container.AddChild(page);
}
public void DetachPage(Control page, UiLayer layer)
{
var container = GetLayerContainer(layer);
container.RemoveChild(page);
}
private Control GetLayerContainer(UiLayer layer)
{
return layer switch
{
UiLayer.Page => _pageLayer,
UiLayer.Overlay => _overlayLayer,
UiLayer.Modal => _modalLayer,
UiLayer.Toast => _toastLayer,
UiLayer.Topmost => _topmostLayer,
_ => _pageLayer
};
}
}
```
### UI 动画和过渡
```csharp
public partial class AnimatedPage : Control, IUiPage
[AutoUiPage(nameof(UiKey.MainMenu), nameof(UiLayer.Page))]
public partial class MainMenu : Control, IUiPageBehaviorProvider, IUiPage
{
public void OnEnter(IUiPageEnterParam? param)
{
// 淡入动画
Modulate = new Color(1, 1, 1, 0);
Show();
var tween = CreateTween();
tween.TweenProperty(this, "modulate:a", 1.0f, 0.3f);
}
public void OnExit()
{
// 淡出动画
var tween = CreateTween();
tween.TweenProperty(this, "modulate:a", 0.0f, 0.3f);
tween.TweenCallback(Callable.From(Hide));
}
public void OnPause()
{
}
public void OnResume()
{
}
public void OnShow()
{
Show();
}
public void OnHide()
{
Hide();
}
// ... 其他方法
}
```
### UI 句柄管理
当前生成器补出的核心样板与源码一致:
```csharp
public partial class DialogManager : Node
public IUiPageBehavior GetPage()
{
private UiHandle? _currentDialog;
public void ShowDialog(string dialogKey)
{
// 关闭当前对话框
CloseCurrentDialog();
// 显示新对话框
var uiRouter = this.GetSystem<IUiRouter>();
_currentDialog = uiRouter.Show(dialogKey, UiLayer.Modal);
}
public void CloseCurrentDialog()
{
if (_currentDialog.HasValue)
{
var uiRouter = this.GetSystem<IUiRouter>();
uiRouter.Hide(_currentDialog.Value, UiLayer.Modal, destroy: true);
_currentDialog = null;
}
}
return __autoUiPageBehavior_Generated ??=
UiPageBehaviorFactory.Create(this, UiKeyStr, UiLayer.Page);
}
```
### 多个 Toast 显示
要注意两点:
- `[AutoUiPage]` 不会替你自动补 `: IUiPageBehaviorProvider`
- UI 层级是生成器输入的一部分;`Page` / `Modal` / `Overlay` 语义不是后面再猜出来的
### 5. 按 layer 选择正确入口
Godot runtime 只是落地 `UiRouterBase` 的语义,因此入口仍然和 `GFramework.Game` 一致:
页面栈:
```csharp
public partial class ToastManager : Node
{
private readonly List<UiHandle> _activeToasts = new();
public void ShowToast(string message, float duration = 3.0f)
{
var uiRouter = this.GetSystem<IUiRouter>();
// Toast 层支持重入,可以同时显示多个
var handle = uiRouter.Show("Toast", UiLayer.Toast, new ToastParam
{
Message = message
});
_activeToasts.Add(handle);
// 自动隐藏
GetTree().CreateTimer(duration).Timeout += () =>
{
uiRouter.Hide(handle, UiLayer.Toast, destroy: true);
_activeToasts.Remove(handle);
};
}
public void ClearAllToasts()
{
var uiRouter = this.GetSystem<IUiRouter>();
foreach (var handle in _activeToasts)
{
uiRouter.Hide(handle, UiLayer.Toast, destroy: true);
}
_activeToasts.Clear();
}
}
await uiRouter.ReplaceAsync(nameof(UiKey.MainMenu));
await uiRouter.PushAsync(nameof(UiKey.Settings));
await uiRouter.PopAsync();
```
## 最佳实践
1. **UI 脚本实现 IUiPage 接口**:获得完整的生命周期管理
```csharp
✓ public partial class MyPage : Control, IUiPage { }
✗ public partial class MyPage : Control { } // 无生命周期管理
```
2. **使用正确的 UI 层级**:根据 UI 类型选择合适的层级
```csharp
✓ Page: 主要页面(主菜单、设置)
✓ Overlay: 浮层(信息面板)
✓ Modal: 模态对话框(确认框)
✓ Toast: 提示消息
✓ Topmost: 系统级(加载界面)
```
3. **在 OnEnter 中显示 UI**:确保 UI 正确显示
```csharp
public void OnEnter(IUiPageEnterParam? param)
{
Show(); // 显示 UI
// 初始化 UI 状态
}
```
4. **在 OnExit 中隐藏 UI**:确保 UI 正确隐藏
```csharp
public void OnExit()
{
Hide(); // 隐藏 UI
// 清理 UI 状态
}
```
5. **使用 UI 句柄管理非栈 UI**:对于 Modal、Toast 等层级
```csharp
var handle = uiRouter.Show("Dialog", UiLayer.Modal);
// 保存句柄以便后续关闭
uiRouter.Hide(handle, UiLayer.Modal, destroy: true);
```
6. **使用 UI 参数传递数据**:避免使用全局变量
```csharp
✓ uiRouter.Show("Dialog", UiLayer.Modal, new DialogParam { ... });
✗ GlobalData.DialogMessage = "..."; // 避免全局状态
```
## 常见问题
### 问题:如何在 Godot UI 中使用 GFramework
**解答**
UI 脚本实现 `IUiPage``IUiPageBehaviorProvider` 接口:
层级 UI
```csharp
public partial class MyPage : Control, IUiPage, IUiPageBehaviorProvider
{
public void OnEnter(IUiPageEnterParam? param) { }
public IUiPageBehavior GetPage() { return new PageLayerUiPageBehavior(this, "MyPage"); }
}
var handle = uiRouter.Show(nameof(UiKey.PauseMenu), UiLayer.Modal);
uiRouter.Hide(handle, UiLayer.Modal);
```
### 问题UI 层级有什么区别?
当前实现里,`Show(..., UiLayer.Page)` 会直接抛异常;`Page` 层必须走 `PushAsync` / `ReplaceAsync`
**解答**
## 输入与暂停语义
- **Page**:栈管理,不可重入,用于主要页面
- **Overlay**:可重入,用于浮层
- **Modal**:可重入,带遮罩,用于对话框
- **Toast**:可重入,轻量提示
- **Topmost**:不可重入,最高优先级
如果页面只实现 `IUiPage`,它只有基础生命周期。
### 问题:如何实现 UI 动画?
如果还需要更强的输入仲裁或暂停语义,可以像 CoreGrid 的 `PauseMenu` 一样继续实现:
**解答**
在生命周期方法中使用 Godot Tween
- `IUiInteractionProfileProvider`
- `IUiActionHandler`
```csharp
public void OnEnter(IUiPageEnterParam? param)
{
var tween = CreateTween();
tween.TweenProperty(this, "modulate:a", 1.0f, 0.3f);
}
```
当前这条链路是成立的:
### 问题:如何在 UI 中访问架构组件?
1. 页面行为从 owner 读取 `UiInteractionProfile`
2. router 根据 profile 判断动作捕获、世界输入阻断和暂停策略
3. 如果页面实现了 `IUiActionHandler``TryHandleUiAction(...)` 会继续下沉到页面
**解答**
使用扩展方法:
这也是为什么 `PauseMenu` 一类 modal 页面可以声明:
```csharp
public partial class MyPage : Control, IUiPage
{
public void OnEnter(IUiPageEnterParam? param)
{
var playerModel = this.GetModel<PlayerModel>();
var gameSystem = this.GetSystem<GameSystem>();
}
}
```
- 捕获 `Cancel`
- 阻断 World pointer / action input
- 在可见时持有暂停
- 即使在暂停状态也继续处理节点逻辑
### 问题:如何关闭 Modal 或 Toast
## 当前边界
**解答**
使用 UI 句柄:
### 没有 `GodotUiRouter`
```csharp
// 显示时保存句柄
var handle = uiRouter.Show("Dialog", UiLayer.Modal);
仓库当前没有这个类型。旧文档把它写成默认入口是不准确的;真实入口仍然是项目侧的 `UiRouterBase` 派生类。
// 关闭时使用句柄
uiRouter.Hide(handle, UiLayer.Modal, destroy: true);
```
### UI 工厂不会自动补 behavior
### 问题UI 生命周期方法的调用顺序是什么?
`GodotSceneFactory` 不同,`GodotUiFactory` 当前不会按节点类型自动创建 behavior。节点不实现
`IUiPageBehaviorProvider` 时会直接失败。
**解答**
### `Page` 层不是 `Show(...)` 的适用对象
- 进入:`OnEnter` -> `OnShow`
- 暂停:`OnPause` -> `OnHide`
- 恢复:`OnShow` -> `OnResume`
- 退出:`OnHide` -> `OnExit`
`UiLayer.Page` 代表页面栈语义,而不是普通 layer UI。当前实现明确要求
## 相关文档
- `Page``PushAsync` / `ReplaceAsync`
- `Overlay` / `Modal` / `Toast` / `Topmost``Show` / `Hide`
- [UI 系统](/zh-CN/game/ui) - 核心 UI 管理
- [Godot 架构集成](/zh-CN/godot/architecture) - Godot 架构基础
- [Godot 场景系统](/zh-CN/godot/scene) - Godot 场景集成
- [Godot 扩展](/zh-CN/godot/extensions) - Godot 扩展方法
### root 仍然由项目控制
`IUiRoot` 决定:
- 每个 layer 是否拆独立容器
- 层内排序怎么算
- 页面移除时如何释放节点
Godot runtime 不会替项目自动生成统一 UI 根节点。
## 继续阅读
1. [Godot 运行时集成](./index.md)
2. [Game UI 系统](../game/ui.md)
3. [AutoUiPage 生成器](../source-generators/auto-ui-page-generator.md)
4. [Godot 架构集成](./architecture.md)

View File

@ -1,21 +1,26 @@
---
title: AutoRegisterExportedCollections 生成器
description: 说明批量注册生成器当前会生成什么、可匹配哪些集合与注册器成员,以及 null-skip 与编译期诊断的边界。
---
# AutoRegisterExportedCollections 生成器
> 为 Godot 导出集合生成批量注册方法,收敛启动入口里的重复 `foreach + Registry(...)` 样板。
`[AutoRegisterExportedCollections]` 用来把“遍历一组配置并逐项调用 registry 方法”的启动样板收敛成一个生成方法
## 概述
它最常见的落点确实是 Godot Inspector 导出的数组,但当前生成器真正依赖的不是 `[Export]` 本身,而是:
在游戏启动入口中,常见的一类样板是:
- 宿主类型被标记了 `[AutoRegisterExportedCollections]`
- 某个实例字段或可读实例属性被标记了 `[RegisterExportedCollection(...)]`
- 该成员可枚举,且元素类型可在编译期推导
- 目标 registry 成员存在,并能找到兼容的单参数实例方法
- 在 Inspector 中导出一批配置、资源映射或预制体条目
- 从某个 Registry 成员拿到注册器
- 遍历集合逐项调用 `Register(...)` / `Registry(...)`
## 当前包关系
`AutoRegisterExportedCollections` 会把这类样板收敛成声明式配置。
- 特性来源:`GFramework.Godot.SourceGenerators.Abstractions.UI`
- 生成器实现:`GFramework.Godot.SourceGenerators`
- 典型消费者Godot 启动入口、资源入口节点、配置引导节点
它特别适合 `GameEntryPoint`、资源根节点、配置引导节点这类“导出即注册”的场景。
相关特性当前位于 `GFramework.Godot.SourceGenerators.Abstractions.UI` 命名空间。
## 基础使用
## 最小用法
```csharp
using System.Collections.Generic;
@ -39,13 +44,6 @@ public sealed class TextureConfig : Resource, IKeyValue<string, Texture2D>
{
}
public sealed class TextureRegistry : IAssetRegistry<Texture2D>
{
public void Registry(IKeyValue<string, Texture2D> mapping)
{
}
}
[AutoRegisterExportedCollections]
public partial class GameEntryPoint : Node
{
@ -57,119 +55,169 @@ public partial class GameEntryPoint : Node
public override void _Ready()
{
_textureRegistry ??= new TextureRegistry();
_textureRegistry ??= ResolveTextureRegistry();
__RegisterExportedCollections_Generated();
}
private static IAssetRegistry<Texture2D> ResolveTextureRegistry()
{
throw new NotImplementedException();
}
}
```
为了让示例具备完整的调用路径,这里在 `_Ready()` 里先初始化了 `_textureRegistry`
实际项目里,这个字段通常来自架构容器、服务定位或外部注入;关键点是调用 `__RegisterExportedCollections_Generated()`
之前,注册器成员必须已经可用,否则生成代码会按设计静默跳过注册。
当前生成器不会自动调用 `__RegisterExportedCollections_Generated()`。你需要在 registry 成员和集合成员都准备好之后手动调用。
## 生成的代码
## 当前会生成什么
对于上面的成员,当前生成器会产出:
```csharp
// <auto-generated />
#nullable enable
partial class GameEntryPoint
private void __RegisterExportedCollections_Generated()
{
private void __RegisterExportedCollections_Generated()
if (this._textureConfigs is not null && this._textureRegistry is not null)
{
if (this._textureConfigs is not null && this._textureRegistry is not null)
foreach (var __generatedItem in this._textureConfigs)
{
foreach (var __generatedItem in this._textureConfigs)
{
this._textureRegistry.Registry(__generatedItem);
}
this._textureRegistry.Registry(__generatedItem);
}
}
}
```
## 参数说明
最重要的运行时语义只有两条:
### `[AutoRegisterExportedCollections]`
- 集合成员为 `null` 时,本次注册直接跳过
- registry 成员为 `null` 时,本次注册直接跳过
类级标记,声明该类型允许生成 `__RegisterExportedCollections_Generated()`
这里的“跳过”只针对运行时 `null` 情况;配置错误、方法不匹配、元素类型无法推导等问题都会在编译期直接给出诊断,而不是静默吞掉
### `[RegisterExportedCollection(registryMemberName, registerMethodName)]`
## 当前支持的成员形状
| 参数 | 类型 | 说明 |
|----------------------|----------|----------------------------------|
| `registryMemberName` | `string` | 当前类型上用于执行注册的字段或属性名 |
| `registerMethodName` | `string` | 注册方法名,例如 `Register``Registry` |
### 集合成员
推荐优先使用 `nameof(...)` 表达式,而不是手写字符串。
`[RegisterExportedCollection]` 可以标在:
## 支持的匹配规则
- 实例字段
- 可读、非索引器的实例属性
生成器会在编译期验证:
它们不必一定带 `[Export]`,但在 Godot 项目里通常会配合 `[Export]` 使用。
- 集合成员必须是实例字段,或可读的实例属性
- 集合类型必须可枚举
- 集合元素类型必须能在编译期推导
- 注册器成员必须是实例字段,或可读的实例属性
- 注册方法必须是单参数实例方法,且参数类型能接收集合元素类型
### registry 成员
当前版本还支持从以下位置解析注册方法
`registryMemberName` 指向的目标也必须是:
- 注册器具体类型本身
- 注册器基类
- 注册器实现的接口
- 实例字段,或
- 可读、非索引器的实例属性
静态字段、静态属性、只写属性都不受支持。
## 当前匹配规则
### 可枚举集合
集合成员必须实现 `System.Collections.IEnumerable`,并且生成器还要能推导出元素类型。
因此:
- `List<int>``Godot.Collections.Array<TextureConfig>` 这类泛型集合可以
- 非泛型 `IEnumerable` / `ArrayList` 这类只能枚举 `object` 的集合不可以
### 注册方法
当前会查找名称匹配、且满足以下条件的方法:
- 实例方法
- 只有一个参数
- 对宿主类型可访问
- 参数类型能接收集合元素类型
查找范围不只限于 registry 具体类型本身,还包括:
- 基类
- 直接实现的接口
- 继承链上的接口
这意味着像 `IAssetRegistry<T>` 继承 `IRegistry<TKey, TValue>` 的项目结构也能正常生成,不必再把注册器字段改成具体实现类型。
所以像下面这种接口继承链是受支持的:
## 适用场景
```csharp
[RegisterExportedCollection(nameof(_registry), "Registry")]
public List<IntConfig>? Values { get; } = new();
```
推荐用于:
只要 `_registry` 的接口链上能找到兼容的 `Registry(...)` 即可。
- `GameEntryPoint` 中的资源注册
- 场景启动时的配置条目注册
- Inspector 预配置的纹理、音频、Prefab、场景映射批量接入
### 明确不支持的情况
不推荐用于:
当前测试明确覆盖了这些边界:
- 注册前需要复杂过滤、去重、排序、条件判断的集合
- 需要记录失败项、错误聚合或回滚逻辑的批量导入
- 每个元素注册时都依赖额外上下文或副作用控制的流程
- 只显式实现接口方法,未在具体类型上暴露可访问成员
- 注册方法存在,但对宿主类型不可访问
- 集合元素类型无法推导
- registry 成员不存在
- 注册方法名存在但签名不兼容
这些情况都会直接触发编译期诊断。
## 真实采用路径
`ai-libs/CoreGrid/global/GameEntryPoint.cs` 是当前最直接的消费者参考:
- `UiPageConfigs`
- `GameSceneConfigs`
- `PrefabSceneConfigs`
- `TextureConfigs`
这几个 `Array<T>` 成员都通过 `[RegisterExportedCollection(...)]` 声明 registry 目标,并在 `_Ready()` 里调用
`__RegisterExportedCollections_Generated()`
这个例子说明两件事:
1. 这项能力适合“启动时集中接入一批静态配置”的节点
2. 生成器只负责循环调用,不负责 registry 的获取、生命周期或错误恢复
## 使用约束
- 目标类型必须是 `partial class`
当前最重要的约束有这些:
- 宿主类型必须是顶层 `partial class`
- 不支持嵌套类
- 生成器不会自动调用 `__RegisterExportedCollections_Generated()`
- 非泛型 `IEnumerable` 之类无法推导元素类型的集合不受支持
- 注册方法必须对宿主类型可访问
- 生成器不会自动接入 `_Ready()` 或其他生命周期方法
- 宿主类型若已声明 `__RegisterExportedCollections_Generated()`,会触发命名冲突诊断
- 只有当至少一个成员成功通过验证时,才会生成方法
## 诊断信息
## 诊断速查
| 诊断 ID | 含义 |
|-----------------------|-------------------------------------------------------------|
| `GF_Common_Class_001` | 目标类型不是 `partial`,生成被跳过 |
| `GF_Common_Class_002` | 宿主类型已声明 `__RegisterExportedCollections_Generated()`,与生成代码冲突 |
| `GF_AutoExport_001` | `AutoRegisterExportedCollections` 不支持嵌套类 |
| `GF_AutoExport_002` | 指定的注册器成员不存在 |
| `GF_AutoExport_003` | 注册器成员上找不到兼容的注册方法 |
| `GF_AutoExport_004` | 被标记的成员不是可枚举集合 |
| `GF_AutoExport_005` | 无法推导集合元素类型 |
| `GF_AutoExport_006` | 集合成员不是实例可读成员 |
| `GF_AutoExport_007` | 注册器成员不是实例可读成员 |
| `GF_AutoExport_008` | `RegisterExportedCollectionAttribute` 参数无效 |
| 诊断 ID | 含义 |
| --- | --- |
| `GF_Common_Class_001` | 宿主类型不是 `partial class` |
| `GF_Common_Class_002` | 已手写 `__RegisterExportedCollections_Generated()`,与生成代码冲突 |
| `GF_AutoExport_001` | 不支持嵌套类 |
| `GF_AutoExport_002` | 指定的 registry 成员不存在 |
| `GF_AutoExport_003` | 找不到兼容且可访问的注册方法 |
| `GF_AutoExport_004` | 被标记成员不可枚举 |
| `GF_AutoExport_005` | 无法安全推导集合元素类型 |
| `GF_AutoExport_006` | 集合成员不是实例可读成员 |
| `GF_AutoExport_007` | registry 成员不是实例可读成员 |
| `GF_AutoExport_008` | `RegisterExportedCollectionAttribute` 构造参数无效 |
## 调用时机建议
## 何时适合用它
推荐在以下时机之一调用生成方法
适合
- `_Ready()` 中,且在注册器字段已经准备好之后
- 启动入口的显式 `Initialize()``Bootstrap()` 方法中
- 测试中的装配阶段
- 启动入口里有多组“集合 -> registry”的重复注册代码
- 每个元素都只需要一次简单的单参数注册
- 你想把“注册到哪个 registry、调用哪个方法”直接挂在成员声明上
要在构造函数中调用,因为此时 Godot 导出字段和外部依赖通常还未准备完毕。
适合:
## 相关文档
- 注册流程需要排序、过滤、去重或事务式回滚
- 每个元素注册前后还要插入复杂副作用
- 注册规则依赖运行时动态上下文,而不是静态成员绑定
- [源码生成器总览](./index)
- [游戏内容配置系统](/zh-CN/game/config-system)
## 推荐阅读
1. [/zh-CN/source-generators/index](./index.md)
2. [/zh-CN/game/config-system](../game/config-system.md)
3. [/zh-CN/source-generators/godot-project-generator](./godot-project-generator.md)
4. `GFramework.Godot.SourceGenerators/README.md`

View File

@ -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
// <auto-generated />
#nullable enable
namespace YourNamespace;
partial class MainMenu
{
private void __BindNodeSignals_Generated()
{
_startButton.Pressed += OnStartButtonPressed;
_settingsButton.Pressed += OnSettingsButtonPressed;
_quitButton.Pressed += OnQuitButtonPressed;
}
private void __UnbindNodeSignals_Generated()
{
_startButton.Pressed -= OnStartButtonPressed;
_settingsButton.Pressed -= OnSettingsButtonPressed;
_quitButton.Pressed -= OnQuitButtonPressed;
}
}
```
## 参数说明
`[BindNodeSignal]` 特性需要两个参数:
| 参数 | 类型 | 说明 |
|-----------------|--------|-----------------------------|
| `nodeFieldName` | string | 目标节点字段名(使用 `nameof` 推荐) |
| `signalName` | string | 目标节点上的 CLR 事件名(使用 `nameof` |
```csharp
[BindNodeSignal("_startButton", "Pressed")] // 字符串字面量
[BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))] // 推荐nameof 表达式
```
## 高级用法
### 带参数的事件处理
处理带参数的事件(如 `SpinBox.ValueChanged`
```csharp
using Godot;
public partial class SettingsPanel : Control
{
private SpinBox _volumeSpinBox = null!;
private SpinBox _brightnessSpinBox = null!;
// 参数类型必须与事件委托匹配
[BindNodeSignal(nameof(_volumeSpinBox), nameof(SpinBox.ValueChanged))]
private void OnVolumeChanged(double value)
{
SetVolume((float)value);
}
[BindNodeSignal(nameof(_brightnessSpinBox), nameof(SpinBox.ValueChanged))]
private void OnBrightnessChanged(double value)
{
SetBrightness((float)value);
}
public override void _Ready()
{
__BindNodeSignals_Generated();
}
public override void _ExitTree()
{
__UnbindNodeSignals_Generated();
}
}
```
### 多事件绑定
一个方法可以同时绑定到多个节点的事件:
```csharp
public partial class MultiButtonHud : Control
{
private Button _buttonA = null!;
private Button _buttonB = null!;
private Button _buttonC = null!;
// 一个方法处理多个按钮的点击
[BindNodeSignal(nameof(_buttonA), nameof(Button.Pressed))]
[BindNodeSignal(nameof(_buttonB), nameof(Button.Pressed))]
[BindNodeSignal(nameof(_buttonC), nameof(Button.Pressed))]
private void OnAnyButtonPressed()
{
PlayClickSound();
}
public override void _Ready()
{
__BindNodeSignals_Generated();
}
public override void _ExitTree()
{
__UnbindNodeSignals_Generated();
}
}
```
### 与 [GetNode] 组合使用
推荐与 `[GetNode]` 特性结合使用:
```csharp
using GFramework.Godot.SourceGenerators.Abstractions;
using Godot;
public partial class GameHud : Control
{
// 使用 GetNode 自动获取节点
[GetNode]
private Button _pauseButton = null!;
[GetNode]
private ProgressBar _healthBar = null!;
[GetNode("UI/ScoreLabel")]
private Label _scoreLabel = null!;
// 使用 BindNodeSignal 绑定事件
[BindNodeSignal(nameof(_pauseButton), nameof(Button.Pressed))]
private void OnPauseButtonPressed()
{
TogglePause();
}
// 多事件绑定示例
[BindNodeSignal(nameof(_healthBar), nameof(Range.ValueChanged))]
private void OnHealthChanged(double value)
{
UpdateHealthDisplay(value);
}
public override void _Ready()
{
// 先注入节点,再绑定信号
__InjectGetNodes_Generated();
__BindNodeSignals_Generated();
}
public override void _ExitTree()
{
__UnbindNodeSignals_Generated();
}
}
```
### 复杂事件处理场景
实现完整的 UI 事件处理:
```csharp
public partial class InventoryUI : Control
{
// 节点
[GetNode]
private ItemList _itemList = null!;
[GetNode]
private Button _useButton = null!;
[GetNode]
private Button _dropButton = null!;
[GetNode]
private LineEdit _searchBox = null!;
// 事件处理
[BindNodeSignal(nameof(_itemList), nameof(ItemList.ItemSelected))]
private void OnItemSelected(long index)
{
SelectItem((int)index);
}
[BindNodeSignal(nameof(_itemList), nameof(ItemList.ItemActivated))]
private void OnItemActivated(long index)
{
UseItem((int)index);
}
[BindNodeSignal(nameof(_useButton), nameof(Button.Pressed))]
private void OnUseButtonPressed()
{
UseSelectedItem();
}
[BindNodeSignal(nameof(_dropButton), nameof(Button.Pressed))]
private void OnDropButtonPressed()
{
DropSelectedItem();
}
[BindNodeSignal(nameof(_searchBox), nameof(LineEdit.TextChanged))]
private void OnSearchTextChanged(string newText)
{
FilterItems(newText);
}
public override void _Ready()
{
__InjectGetNodes_Generated();
__BindNodeSignals_Generated();
InitializeInventory();
}
public override void _ExitTree()
@ -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`

View File

@ -1,496 +1,198 @@
---
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()
{
private void __InjectGetNodes_Generated()
{
_healthLabel = GetNode<global::Godot.Label>("%HealthLabel");
_manaBar = GetNode<global::Godot.ProgressBar>("%ManaBar");
_scoreLabel = GetNode<global::Godot.Label>("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<global::Godot.HBoxContainer>("%LeftContainer");
m_rightContainer = GetNode<global::Godot.HBoxContainer>("%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<Label>("%HealthLabel");
_manaBar = GetNode<ProgressBar>("%ManaBar");
_scoreLabel = GetNode<Label>("ScoreContainer/ScoreValue");
__InjectGetNodes_Generated();
OnGetNodeReadyGenerated();
}
```
// ✅ 推荐:使用 [GetNode] 特性
[GetNode]
private Label _healthLabel = null!;
这个行为来自当前生成器测试,不是文档约定。
[GetNode]
private ProgressBar _manaBar = null!;
## 当前路径推断规则
### 没写路径时
如果 `[GetNode]` 没有显式路径,当前默认按字段名推导唯一名路径:
- `_leftContainer` -> `%LeftContainer`
- `m_rightContainer` -> `%RightContainer`
也就是说,默认不是普通相对路径,而是 Godot 的 `%Name` 唯一名语法。
### 显式路径优先
```csharp
[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`

View File

@ -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`

File diff suppressed because it is too large Load Diff

View File

@ -28,24 +28,22 @@
### [Godot 集成教程](./godot-integration.md)
> 深入学习 GFramework 与 Godot 引擎的深度集成,掌握高级开发技巧
> 按当前源码和真实项目接线,完成 Godot 项目级配置、场景节点生成器接入与运行时生命周期协作
**适合人群**
- 已完成基础教程的开发者
- 需要优化 Godot 项目性能的开发者
- 希望实现复杂游戏系统的架构师
- 正在把现有 Godot C# 项目接入 GFramework 的开发者
- 需要厘清 `project.godot``[GetNode]``[BindNodeSignal]` 边界的维护者
**学习内容**
- 节点生命周期管理
- 信号系统集成与桥接
- 资源管理优化策略
- 对象池化系统实现
- 性能优化最佳实践
- 调试与测试方法
- `GeWuYou.GFramework.Godot` 与生成器包的职责划分
- `project.godot``AutoLoads` / `InputActions` 的生成链路
- `[GetNode]``[BindNodeSignal]``_Ready()` / `_ExitTree()` 的协作顺序
- 常见旧写法迁移边界与后续阅读入口
**预计时间**3-4 小时
**预计时间**1-2 小时
---