mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-07 00:39:00 +08:00
Merge pull request #270 from GeWuYou/docs/sdk-update-documentation
Docs/sdk update documentation
This commit is contained in:
commit
b2a5555c75
@ -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 是否按预期收敛
|
||||
|
||||
@ -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 是否关闭或减少
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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`
|
||||
|
||||
@ -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`
|
||||
|
||||
@ -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`
|
||||
|
||||
@ -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
@ -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 小时
|
||||
|
||||
---
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user