Compare commits

..

No commits in common. "b2a5555c751776bf19d42f09796a7d3d2fe46de5" and "a980a042aec6e4e4a03df42241aac689157af40c" have entirely different histories.

71 changed files with 12467 additions and 4795 deletions

View File

@ -1,52 +0,0 @@
# GFramework Skills
文档工作流的公开入口已统一为 `gframework-doc-refresh`
## 公开入口
### `gframework-doc-refresh`
按源码模块驱动文档刷新,而不是按 `guide``tutorial``api` 等类型拆入口。
适用场景:
- 刷新某个模块的 landing page
- 复核专题页是否与源码、测试、README 一致
- 评估是否需要补 API reference 或教程
- 在 adoption path 不清晰时引入 `ai-libs/` 消费者接法作为补充证据
推荐调用:
```bash
/gframework-doc-refresh <module>
```
示例:
```bash
/gframework-doc-refresh Core
/gframework-doc-refresh Godot.SourceGenerators
/gframework-doc-refresh Cqrs
```
## 共享资源
- `_shared/DOCUMENTATION_STANDARDS.md`
- 统一的文档规则、证据顺序与验证要求
- `_shared/module-map.json`
- 机器可读的模块映射表
- `_shared/module-config.sh`
- 轻量 shell 辅助函数
## 内部资源
`gframework-doc-refresh/` 下包含:
- `references/`
- 模块选择、证据顺序、输出策略
- `templates/`
- landing page、专题页、API reference、教程模板
- `scripts/`
- 模块扫描与文档验证脚本
`vitepress-*` skills 不再作为并列公开入口保留。

View File

@ -1,108 +0,0 @@
# GFramework 文档编写规范
本文件只保留跨模块稳定生效的写作与校验规则,不再维护容易失真的固定页面清单。
模块到源码、测试、README、`docs/zh-CN` 栏目以及 `ai-libs/` 参考入口的映射,统一以
`.agents/skills/_shared/module-map.json` 为准。
## 证据顺序
统一按以下顺序判断文档应写什么、删什么、保留什么:
1. 源码、公开 XML docs、`*.csproj`
2. 对应测试和 snapshot
3. 模块 `README.md`
4. 当前 `docs/zh-CN` 页面
5. `ai-libs/` 下已验证的消费者项目
6. 归档文档,仅在前述证据无法解释当前行为时回看
不要把旧文档互相抄写当成“更新”。
## 模块驱动规则
- 先按源码模块归一化输入,再决定落到 landing page、专题页、API reference、教程还是仅做校验。
- 如果用户给的是栏目名而不是源码模块名,先映射回模块;若仍有歧义,只给归一化建议,不直接生成文档。
- 文档栏目是派生输出,不是主输入源。
## `ai-libs/` 使用边界
`ai-libs/` 只用于补消费者视角证据:
- 验证真实接入目录结构
- 查最小 wiring、扩展点装配方式
- 给 adoption path 提供端到端例子
不要用 `ai-libs/` 覆盖以下事实:
- 公共 API 契约
- 当前版本支持范围
- Source Generator 诊断与生成语义
如果 `ai-libs/` 与当前源码或测试冲突,以当前仓库实现为准,并在文档里写明迁移或兼容边界。
## Markdown 规则
### 泛型与 HTML 转义
代码块外出现泛型或 XML 标签时必须转义:
- `List&lt;T&gt;`
- `Result&lt;TValue, TError&gt;`
- `&lt;summary&gt;`
- `&lt;param&gt;`
### Frontmatter
每个文档都必须包含合法 frontmatter
```yaml
---
title: 文档标题
description: 1-2 句话描述当前页面解决什么问题
---
```
### 代码块
- 始终标注语言,如 `csharp``bash``json`
- 示例只保留当前实现可追溯的最小路径
- 必要时写中文注释解释接入原因或边界,不要堆砌与代码同步无关的注释
### 链接
- 只链接到当前仓库真实存在的页面
- 站内链接优先使用 `/zh-CN/...` 形式
- 如果文档站不允许跳出 `docs/` 根目录,就不要把仓库 README 写成站内链接
## 输出优先级
统一按以下顺序决定产出:
1. 先修模块 README、landing page 与 adoption path
2. 再修失真的专题页
3. 再补 API reference
4. 最后才补教程
## 验证清单
- [ ] frontmatter 正确
- [ ] 代码块语言标记齐全
- [ ] 泛型和 XML 标签已转义
- [ ] 站内链接存在
- [ ] 示例与当前实现一致
- [ ] `ai-libs/` 只作为消费者接入参考,没有覆盖源码契约
## 验证工具
统一复用 `gframework-doc-refresh/scripts/` 下的校验脚本:
- `validate-frontmatter.sh`
- `validate-links.sh`
- `validate-code-blocks.sh`
- `validate-all.sh`
需要站点级验证时,执行:
```bash
cd docs && bun run build
```

View File

@ -1,271 +0,0 @@
#!/bin/bash
# 共享的模块配置
# 机器可读映射以 .agents/skills/_shared/module-map.json 为准。
normalize_module() {
local INPUT
INPUT="$(echo "$1" | tr '[:upper:]' '[:lower:]' | tr ' ' '-' | tr '_' '-')"
case "$INPUT" in
core|core-runtime|runtime-core|core-module)
echo "Core"
;;
core.abstractions|core-abstractions)
echo "Core.Abstractions"
;;
core.sourcegenerators|core-source-generators|core-sourcegenerators)
echo "Core.SourceGenerators"
;;
core.sourcegenerators.abstractions|core-source-generators-abstractions)
echo "Core.SourceGenerators.Abstractions"
;;
game|game-runtime|runtime-game|game-module)
echo "Game"
;;
game.abstractions|game-abstractions)
echo "Game.Abstractions"
;;
game.sourcegenerators|game-source-generators)
echo "Game.SourceGenerators"
;;
godot|godot-runtime|runtime-godot|godot-module)
echo "Godot"
;;
godot.sourcegenerators|godot-source-generators|godot-generators)
echo "Godot.SourceGenerators"
;;
godot.sourcegenerators.abstractions|godot-source-generators-abstractions)
echo "Godot.SourceGenerators.Abstractions"
;;
cqrs|mediator|cqrs-module)
echo "Cqrs"
;;
cqrs.abstractions|cqrs-abstractions)
echo "Cqrs.Abstractions"
;;
cqrs.sourcegenerators|cqrs-source-generators)
echo "Cqrs.SourceGenerators"
;;
ecs|ecs.arch|ecs-arch)
echo "Ecs.Arch"
;;
ecs.arch.abstractions|ecs-arch-abstractions)
echo "Ecs.Arch.Abstractions"
;;
sourcegenerators.common|source-generators-common)
echo "SourceGenerators.Common"
;;
*)
return 1
;;
esac
}
get_all_modules() {
cat <<'EOF'
Core
Core.Abstractions
Core.SourceGenerators
Core.SourceGenerators.Abstractions
Game
Game.Abstractions
Game.SourceGenerators
Godot
Godot.SourceGenerators
Godot.SourceGenerators.Abstractions
Cqrs
Cqrs.Abstractions
Cqrs.SourceGenerators
Ecs.Arch
Ecs.Arch.Abstractions
SourceGenerators.Common
EOF
}
is_valid_module() {
normalize_module "$1" >/dev/null 2>&1
}
get_source_dirs() {
local MODULE
MODULE="$(normalize_module "$1")" || return 1
case "$MODULE" in
Core)
echo "GFramework.Core"
;;
Core.Abstractions)
echo "GFramework.Core.Abstractions"
;;
Core.SourceGenerators)
echo "GFramework.Core.SourceGenerators"
;;
Core.SourceGenerators.Abstractions)
echo "GFramework.Core.SourceGenerators.Abstractions"
;;
Game)
echo "GFramework.Game"
;;
Game.Abstractions)
echo "GFramework.Game.Abstractions"
;;
Game.SourceGenerators)
echo "GFramework.Game.SourceGenerators"
;;
Godot)
echo "GFramework.Godot"
;;
Godot.SourceGenerators)
echo "GFramework.Godot.SourceGenerators"
;;
Godot.SourceGenerators.Abstractions)
echo "GFramework.Godot.SourceGenerators.Abstractions"
;;
Cqrs)
echo "GFramework.Cqrs"
;;
Cqrs.Abstractions)
echo "GFramework.Cqrs.Abstractions"
;;
Cqrs.SourceGenerators)
echo "GFramework.Cqrs.SourceGenerators"
;;
Ecs.Arch)
echo "GFramework.Ecs.Arch"
;;
Ecs.Arch.Abstractions)
echo "GFramework.Ecs.Arch.Abstractions"
;;
SourceGenerators.Common)
echo "GFramework.SourceGenerators.Common"
;;
esac
}
get_test_projects() {
local MODULE
MODULE="$(normalize_module "$1")" || return 1
case "$MODULE" in
Core|Core.Abstractions)
echo "GFramework.Core.Tests/GFramework.Core.Tests.csproj"
;;
Core.SourceGenerators|Core.SourceGenerators.Abstractions|Game.SourceGenerators|Cqrs.SourceGenerators|SourceGenerators.Common)
echo "GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj"
;;
Game|Game.Abstractions)
echo "GFramework.Game.Tests/GFramework.Game.Tests.csproj"
;;
Godot)
echo "GFramework.Godot.Tests/GFramework.Godot.Tests.csproj"
;;
Godot.SourceGenerators|Godot.SourceGenerators.Abstractions)
echo "GFramework.Godot.SourceGenerators.Tests/GFramework.Godot.SourceGenerators.Tests.csproj"
;;
Cqrs|Cqrs.Abstractions)
echo "GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj"
;;
Ecs.Arch|Ecs.Arch.Abstractions)
echo "GFramework.Ecs.Arch.Tests/GFramework.Ecs.Arch.Tests.csproj"
;;
esac
}
get_readme_paths() {
local MODULE
MODULE="$(normalize_module "$1")" || return 1
case "$MODULE" in
Core)
echo "GFramework.Core/README.md"
;;
Core.Abstractions)
echo "GFramework.Core.Abstractions/README.md"
;;
Core.SourceGenerators)
echo "GFramework.Core.SourceGenerators/README.md"
;;
Core.SourceGenerators.Abstractions)
echo "GFramework.Core.SourceGenerators.Abstractions/README.md"
;;
Game)
echo "GFramework.Game/README.md"
;;
Game.Abstractions)
echo "GFramework.Game.Abstractions/README.md"
;;
Game.SourceGenerators)
echo "GFramework.Game.SourceGenerators/README.md"
;;
Godot)
echo "GFramework.Godot/README.md"
;;
Godot.SourceGenerators)
echo "GFramework.Godot.SourceGenerators/README.md"
;;
Godot.SourceGenerators.Abstractions)
echo "GFramework.Godot.SourceGenerators.Abstractions/README.md"
;;
Cqrs)
echo "GFramework.Cqrs/README.md"
;;
Cqrs.Abstractions)
echo "GFramework.Cqrs.Abstractions/README.md"
;;
Cqrs.SourceGenerators)
echo "GFramework.Cqrs.SourceGenerators/README.md"
;;
Ecs.Arch)
echo "GFramework.Ecs.Arch/README.md"
;;
Ecs.Arch.Abstractions)
echo "GFramework.Ecs.Arch.Abstractions/README.md"
;;
SourceGenerators.Common)
echo "GFramework.SourceGenerators.Common/README.md"
;;
*)
return 1
;;
esac
}
infer_module_from_namespace() {
local NAMESPACE="$1"
if [[ "$NAMESPACE" == GFramework.Core.SourceGenerators.Abstractions* ]]; then
echo "Core.SourceGenerators.Abstractions"
elif [[ "$NAMESPACE" == GFramework.Core.SourceGenerators* ]]; then
echo "Core.SourceGenerators"
elif [[ "$NAMESPACE" == GFramework.Core.Abstractions* ]]; then
echo "Core.Abstractions"
elif [[ "$NAMESPACE" == GFramework.Core* ]]; then
echo "Core"
elif [[ "$NAMESPACE" == GFramework.Game.SourceGenerators* ]]; then
echo "Game.SourceGenerators"
elif [[ "$NAMESPACE" == GFramework.Game.Abstractions* ]]; then
echo "Game.Abstractions"
elif [[ "$NAMESPACE" == GFramework.Game* ]]; then
echo "Game"
elif [[ "$NAMESPACE" == GFramework.Godot.SourceGenerators.Abstractions* ]]; then
echo "Godot.SourceGenerators.Abstractions"
elif [[ "$NAMESPACE" == GFramework.Godot.SourceGenerators* ]]; then
echo "Godot.SourceGenerators"
elif [[ "$NAMESPACE" == GFramework.Godot* ]]; then
echo "Godot"
elif [[ "$NAMESPACE" == GFramework.Cqrs.SourceGenerators* ]]; then
echo "Cqrs.SourceGenerators"
elif [[ "$NAMESPACE" == GFramework.Cqrs.Abstractions* ]]; then
echo "Cqrs.Abstractions"
elif [[ "$NAMESPACE" == GFramework.Cqrs* ]]; then
echo "Cqrs"
elif [[ "$NAMESPACE" == GFramework.Ecs.Arch.Abstractions* ]]; then
echo "Ecs.Arch.Abstractions"
elif [[ "$NAMESPACE" == GFramework.Ecs.Arch* ]]; then
echo "Ecs.Arch"
elif [[ "$NAMESPACE" == GFramework.SourceGenerators.Common* ]]; then
echo "SourceGenerators.Common"
else
return 1
fi
}

View File

@ -1,434 +0,0 @@
{
"version": 1,
"description": "Canonical documentation refresh module map for GFramework skills.",
"modules": {
"Core": {
"aliases": ["core", "core-runtime", "runtime-core", "core module"],
"source_paths": ["GFramework.Core"],
"project_file": "GFramework.Core/GFramework.Core.csproj",
"test_projects": ["GFramework.Core.Tests/GFramework.Core.Tests.csproj"],
"readme_paths": ["GFramework.Core/README.md"],
"docs": {
"landing": ["docs/zh-CN/core/index.md"],
"topics": [
"docs/zh-CN/core/architecture.md",
"docs/zh-CN/core/context.md",
"docs/zh-CN/core/lifecycle.md",
"docs/zh-CN/core/events.md",
"docs/zh-CN/core/property.md",
"docs/zh-CN/core/logging.md",
"docs/zh-CN/core/state-management.md",
"docs/zh-CN/core/coroutine.md"
],
"fallback": [
"docs/zh-CN/getting-started/quick-start.md",
"docs/zh-CN/api-reference/index.md"
]
},
"ai_libs": {
"paths": [
"ai-libs/CoreGrid/CoreGrid.csproj",
"ai-libs/CoreGrid/global",
"ai-libs/CoreGrid/docs"
],
"search_hints": [
"rg -n \"Architecture|RegisterModel|RegisterSystem|BindableProperty|EventBus\" ai-libs/CoreGrid"
]
}
},
"Core.Abstractions": {
"aliases": ["core.abstractions", "core-abstractions", "core abstractions"],
"source_paths": ["GFramework.Core.Abstractions"],
"project_file": "GFramework.Core.Abstractions/GFramework.Core.Abstractions.csproj",
"test_projects": ["GFramework.Core.Tests/GFramework.Core.Tests.csproj"],
"readme_paths": ["GFramework.Core.Abstractions/README.md"],
"docs": {
"landing": ["docs/zh-CN/abstractions/core-abstractions.md"],
"topics": [
"docs/zh-CN/core/index.md",
"docs/zh-CN/core/architecture.md",
"docs/zh-CN/core/context.md"
],
"fallback": ["docs/zh-CN/api-reference/index.md"]
},
"ai_libs": {
"paths": ["ai-libs/CoreGrid/CoreGrid.csproj"],
"search_hints": [
"rg -n \"GFramework\\.Core\\.Abstractions|IArchitecture|IModel|ISystem\" ai-libs/CoreGrid"
]
}
},
"Core.SourceGenerators": {
"aliases": [
"core.sourcegenerators",
"core-sourcegenerators",
"core-source-generators",
"core source generators"
],
"source_paths": ["GFramework.Core.SourceGenerators"],
"project_file": "GFramework.Core.SourceGenerators/GFramework.Core.SourceGenerators.csproj",
"test_projects": ["GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj"],
"readme_paths": ["GFramework.Core.SourceGenerators/README.md"],
"docs": {
"landing": ["docs/zh-CN/source-generators/index.md"],
"topics": [
"docs/zh-CN/source-generators/context-aware-generator.md",
"docs/zh-CN/source-generators/context-get-generator.md",
"docs/zh-CN/source-generators/priority-generator.md",
"docs/zh-CN/source-generators/logging-generator.md"
],
"fallback": ["docs/zh-CN/api-reference/index.md"]
},
"ai_libs": {
"paths": ["ai-libs/CoreGrid/global", "ai-libs/CoreGrid/scripts"],
"search_hints": [
"rg -n \"ContextAware|ContextGet|Priority|GeneratedLogger|Log\" ai-libs/CoreGrid"
]
}
},
"Core.SourceGenerators.Abstractions": {
"aliases": [
"core.sourcegenerators.abstractions",
"core-source-generators-abstractions",
"core source generators abstractions"
],
"source_paths": ["GFramework.Core.SourceGenerators.Abstractions"],
"project_file": "GFramework.Core.SourceGenerators.Abstractions/GFramework.Core.SourceGenerators.Abstractions.csproj",
"test_projects": ["GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj"],
"readme_paths": [],
"docs": {
"landing": ["docs/zh-CN/source-generators/index.md"],
"topics": [
"docs/zh-CN/source-generators/context-aware-generator.md",
"docs/zh-CN/source-generators/context-get-generator.md",
"docs/zh-CN/source-generators/priority-generator.md"
],
"fallback": ["docs/zh-CN/api-reference/index.md"]
},
"ai_libs": {
"paths": ["ai-libs/CoreGrid/global"],
"search_hints": [
"rg -n \"GFramework\\.Core\\.SourceGenerators\\.Abstractions|\\[ContextAware\\]|\\[Priority\\]\" ai-libs/CoreGrid"
]
}
},
"Game": {
"aliases": ["game", "game-runtime", "runtime-game", "game module"],
"source_paths": ["GFramework.Game"],
"project_file": "GFramework.Game/GFramework.Game.csproj",
"test_projects": ["GFramework.Game.Tests/GFramework.Game.Tests.csproj"],
"readme_paths": ["GFramework.Game/README.md"],
"docs": {
"landing": ["docs/zh-CN/game/index.md"],
"topics": [
"docs/zh-CN/game/scene.md",
"docs/zh-CN/game/ui.md",
"docs/zh-CN/game/data.md",
"docs/zh-CN/game/storage.md",
"docs/zh-CN/game/serialization.md",
"docs/zh-CN/game/setting.md",
"docs/zh-CN/game/config-system.md"
],
"fallback": [
"docs/zh-CN/getting-started/quick-start.md",
"docs/zh-CN/api-reference/index.md"
]
},
"ai_libs": {
"paths": [
"ai-libs/CoreGrid/global",
"ai-libs/CoreGrid/scenes",
"ai-libs/CoreGrid/addons"
],
"search_hints": [
"rg -n \"SceneRouter|UiRouter|Setting|Storage|Serialization|Config\" ai-libs/CoreGrid"
]
}
},
"Game.Abstractions": {
"aliases": ["game.abstractions", "game-abstractions", "game abstractions"],
"source_paths": ["GFramework.Game.Abstractions"],
"project_file": "GFramework.Game.Abstractions/GFramework.Game.Abstractions.csproj",
"test_projects": ["GFramework.Game.Tests/GFramework.Game.Tests.csproj"],
"readme_paths": ["GFramework.Game.Abstractions/README.md"],
"docs": {
"landing": ["docs/zh-CN/abstractions/game-abstractions.md"],
"topics": [
"docs/zh-CN/game/index.md",
"docs/zh-CN/game/scene.md",
"docs/zh-CN/game/ui.md"
],
"fallback": ["docs/zh-CN/api-reference/index.md"]
},
"ai_libs": {
"paths": ["ai-libs/CoreGrid/global", "ai-libs/CoreGrid/scenes"],
"search_hints": [
"rg -n \"ISceneFactory|ISceneRoot|UiInteractionProfile|IUiRoot\" ai-libs/CoreGrid"
]
}
},
"Game.SourceGenerators": {
"aliases": [
"game.sourcegenerators",
"game-source-generators",
"game source generators"
],
"source_paths": ["GFramework.Game.SourceGenerators"],
"project_file": "GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj",
"test_projects": ["GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj"],
"readme_paths": ["GFramework.Game.SourceGenerators/README.md"],
"docs": {
"landing": ["docs/zh-CN/source-generators/index.md"],
"topics": [
"docs/zh-CN/source-generators/auto-scene-generator.md",
"docs/zh-CN/source-generators/auto-ui-page-generator.md"
],
"fallback": ["docs/zh-CN/game/index.md"]
},
"ai_libs": {
"paths": ["ai-libs/CoreGrid/scenes", "ai-libs/CoreGrid/addons"],
"search_hints": [
"rg -n \"AutoScene|AutoUiPage|SceneRouter|UiRouter\" ai-libs/CoreGrid"
]
}
},
"Godot": {
"aliases": ["godot", "godot-runtime", "runtime-godot", "godot module"],
"source_paths": ["GFramework.Godot"],
"project_file": "GFramework.Godot/GFramework.Godot.csproj",
"test_projects": ["GFramework.Godot.Tests/GFramework.Godot.Tests.csproj"],
"readme_paths": ["GFramework.Godot/README.md"],
"docs": {
"landing": ["docs/zh-CN/godot/index.md"],
"topics": [
"docs/zh-CN/godot/architecture.md",
"docs/zh-CN/godot/scene.md",
"docs/zh-CN/godot/ui.md",
"docs/zh-CN/godot/storage.md",
"docs/zh-CN/godot/setting.md",
"docs/zh-CN/godot/signal.md"
],
"fallback": [
"docs/zh-CN/source-generators/index.md",
"docs/zh-CN/api-reference/index.md"
]
},
"ai_libs": {
"paths": [
"ai-libs/CoreGrid/project.godot",
"ai-libs/CoreGrid/global",
"ai-libs/CoreGrid/scenes"
],
"search_hints": [
"rg -n \"AutoLoad|InputActions|Node|Signal|PackedScene|Godot\" ai-libs/CoreGrid"
]
}
},
"Godot.SourceGenerators": {
"aliases": [
"godot.sourcegenerators",
"godot-source-generators",
"godot source generators",
"godot generators"
],
"source_paths": ["GFramework.Godot.SourceGenerators"],
"project_file": "GFramework.Godot.SourceGenerators/GFramework.Godot.SourceGenerators.csproj",
"test_projects": ["GFramework.Godot.SourceGenerators.Tests/GFramework.Godot.SourceGenerators.Tests.csproj"],
"readme_paths": ["GFramework.Godot.SourceGenerators/README.md"],
"docs": {
"landing": ["docs/zh-CN/source-generators/index.md"],
"topics": [
"docs/zh-CN/source-generators/godot-project-generator.md",
"docs/zh-CN/source-generators/get-node-generator.md",
"docs/zh-CN/source-generators/bind-node-signal-generator.md",
"docs/zh-CN/source-generators/auto-register-exported-collections-generator.md"
],
"fallback": ["docs/zh-CN/godot/index.md"]
},
"ai_libs": {
"paths": [
"ai-libs/CoreGrid/project.godot",
"ai-libs/CoreGrid/global",
"ai-libs/CoreGrid/scenes"
],
"search_hints": [
"rg -n \"GetNode|BindNodeSignal|AutoRegisterExported|project\\.godot|AutoLoad\" ai-libs/CoreGrid"
]
}
},
"Godot.SourceGenerators.Abstractions": {
"aliases": [
"godot.sourcegenerators.abstractions",
"godot-source-generators-abstractions",
"godot source generators abstractions"
],
"source_paths": ["GFramework.Godot.SourceGenerators.Abstractions"],
"project_file": "GFramework.Godot.SourceGenerators.Abstractions/GFramework.Godot.SourceGenerators.Abstractions.csproj",
"test_projects": ["GFramework.Godot.SourceGenerators.Tests/GFramework.Godot.SourceGenerators.Tests.csproj"],
"readme_paths": [],
"docs": {
"landing": ["docs/zh-CN/source-generators/index.md"],
"topics": [
"docs/zh-CN/source-generators/godot-project-generator.md",
"docs/zh-CN/source-generators/get-node-generator.md",
"docs/zh-CN/source-generators/bind-node-signal-generator.md"
],
"fallback": ["docs/zh-CN/godot/index.md"]
},
"ai_libs": {
"paths": ["ai-libs/CoreGrid/project.godot", "ai-libs/CoreGrid/global"],
"search_hints": [
"rg -n \"GFramework\\.Godot\\.SourceGenerators\\.Abstractions|GetNode|BindNodeSignal\" ai-libs/CoreGrid"
]
}
},
"Cqrs": {
"aliases": ["cqrs", "mediator", "cqrs module"],
"source_paths": ["GFramework.Cqrs"],
"project_file": "GFramework.Cqrs/GFramework.Cqrs.csproj",
"test_projects": ["GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj"],
"readme_paths": ["GFramework.Cqrs/README.md"],
"docs": {
"landing": ["docs/zh-CN/core/cqrs.md"],
"topics": [
"docs/zh-CN/core/command.md",
"docs/zh-CN/core/query.md",
"docs/zh-CN/core/cqrs.md"
],
"fallback": [
"docs/zh-CN/core/index.md",
"docs/zh-CN/api-reference/index.md"
]
},
"ai_libs": {
"paths": ["ai-libs/CoreGrid/global", "ai-libs/CoreGrid/scripts"],
"search_hints": [
"rg -n \"CommandHandler|QueryHandler|RegisterCqrs|PipelineBehavior\" ai-libs/CoreGrid"
]
}
},
"Cqrs.Abstractions": {
"aliases": ["cqrs.abstractions", "cqrs-abstractions", "cqrs abstractions"],
"source_paths": ["GFramework.Cqrs.Abstractions"],
"project_file": "GFramework.Cqrs.Abstractions/GFramework.Cqrs.Abstractions.csproj",
"test_projects": ["GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj"],
"readme_paths": ["GFramework.Cqrs.Abstractions/README.md"],
"docs": {
"landing": ["docs/zh-CN/core/cqrs.md"],
"topics": [
"docs/zh-CN/core/command.md",
"docs/zh-CN/core/query.md"
],
"fallback": ["docs/zh-CN/api-reference/index.md"]
},
"ai_libs": {
"paths": ["ai-libs/CoreGrid/global"],
"search_hints": [
"rg -n \"GFramework\\.Cqrs\\.Abstractions|ICommand|IQuery|IRequest\" ai-libs/CoreGrid"
]
}
},
"Cqrs.SourceGenerators": {
"aliases": [
"cqrs.sourcegenerators",
"cqrs-source-generators",
"cqrs source generators"
],
"source_paths": ["GFramework.Cqrs.SourceGenerators"],
"project_file": "GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj",
"test_projects": ["GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj"],
"readme_paths": ["GFramework.Cqrs.SourceGenerators/README.md"],
"docs": {
"landing": ["docs/zh-CN/source-generators/index.md"],
"topics": [],
"fallback": [
"docs/zh-CN/core/cqrs.md",
"docs/zh-CN/api-reference/index.md"
]
},
"ai_libs": {
"paths": ["ai-libs/CoreGrid/global"],
"search_hints": [
"rg -n \"GFramework\\.Cqrs\\.SourceGenerators|RequestHandler|PipelineBehavior\" ai-libs/CoreGrid"
]
}
},
"Ecs.Arch": {
"aliases": ["ecs.arch", "ecs-arch", "ecs arch", "ecs"],
"source_paths": ["GFramework.Ecs.Arch"],
"project_file": "GFramework.Ecs.Arch/GFramework.Ecs.Arch.csproj",
"test_projects": ["GFramework.Ecs.Arch.Tests/GFramework.Ecs.Arch.Tests.csproj"],
"readme_paths": ["GFramework.Ecs.Arch/README.md"],
"docs": {
"landing": ["docs/zh-CN/ecs/index.md"],
"topics": ["docs/zh-CN/ecs/arch.md"],
"fallback": [
"docs/zh-CN/core/index.md",
"docs/zh-CN/api-reference/index.md"
]
},
"ai_libs": {
"paths": ["ai-libs/CoreGrid/scripts", "ai-libs/CoreGrid/global"],
"search_hints": [
"rg -n \"Arch\\.Core|World|SystemGroup|QueryDescription\" ai-libs/CoreGrid"
]
}
},
"Ecs.Arch.Abstractions": {
"aliases": [
"ecs.arch.abstractions",
"ecs-arch-abstractions",
"ecs arch abstractions"
],
"source_paths": ["GFramework.Ecs.Arch.Abstractions"],
"project_file": "GFramework.Ecs.Arch.Abstractions/GFramework.Ecs.Arch.Abstractions.csproj",
"test_projects": ["GFramework.Ecs.Arch.Tests/GFramework.Ecs.Arch.Tests.csproj"],
"readme_paths": [],
"docs": {
"landing": ["docs/zh-CN/ecs/index.md"],
"topics": ["docs/zh-CN/ecs/arch.md"],
"fallback": ["docs/zh-CN/api-reference/index.md"]
},
"ai_libs": {
"paths": ["ai-libs/CoreGrid/scripts"],
"search_hints": [
"rg -n \"GFramework\\.Ecs\\.Arch\\.Abstractions|IArchSystem|IArchModel\" ai-libs/CoreGrid"
]
}
},
"SourceGenerators.Common": {
"aliases": [
"sourcegenerators.common",
"source-generators-common",
"source generators common"
],
"source_paths": ["GFramework.SourceGenerators.Common"],
"project_file": "GFramework.SourceGenerators.Common/GFramework.SourceGenerators.Common.csproj",
"test_projects": ["GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj"],
"readme_paths": [],
"docs": {
"landing": ["docs/zh-CN/source-generators/index.md"],
"topics": [],
"fallback": ["docs/zh-CN/api-reference/index.md"]
},
"ai_libs": {
"paths": [],
"search_hints": []
}
}
},
"docs_section_aliases": {
"core": ["Core"],
"abstractions": ["Core.Abstractions", "Game.Abstractions", "Ecs.Arch.Abstractions"],
"game": ["Game"],
"godot": ["Godot"],
"cqrs": ["Cqrs"],
"ecs": ["Ecs.Arch"],
"source-generators": [
"Core.SourceGenerators",
"Game.SourceGenerators",
"Cqrs.SourceGenerators",
"Godot.SourceGenerators"
]
}
}

View File

@ -1,205 +0,0 @@
---
name: gframework-doc-refresh
description: "Refresh or reassess GFramework documentation for a source module such as Core, Game, Godot, Cqrs, Ecs.Arch, or their generator/abstraction packages. Use this when the user asks to update module docs, re-evaluate landing pages, fix outdated topic pages, refresh API reference coverage, verify adoption paths against source/tests/README, or compare current docs with ai-libs consumer wiring. Recommended command: /gframework-doc-refresh <module>."
---
# Purpose
Use this skill to refresh GFramework documentation from source-first evidence.
The public entry is module-driven, not doc-type-driven:
- Input: a source module or a resolvable docs section alias
- Output: the minimal documentation update set needed for that module
- Evidence: code, tests, README, current docs, then `ai-libs/`
Do not start by deciding “this is an API doc task” or “this is a tutorial task”.
Decide that only after the module scan.
# Triggers
Use this skill when the user asks things like:
- `refresh docs for Core`
- `update Game module docs`
- `根据 Godot 模块源码刷新文档`
- `重新评估 Cqrs 模块文档并更新`
- `核对 Godot.SourceGenerators 的文档状态`
- `看看 source-generators 栏目哪些页面已经失真`
Recommended command form:
```bash
/gframework-doc-refresh <module>
```
# Supported Modules
Canonical module names:
- `Core`
- `Core.Abstractions`
- `Core.SourceGenerators`
- `Core.SourceGenerators.Abstractions`
- `Game`
- `Game.Abstractions`
- `Game.SourceGenerators`
- `Godot`
- `Godot.SourceGenerators`
- `Godot.SourceGenerators.Abstractions`
- `Cqrs`
- `Cqrs.Abstractions`
- `Cqrs.SourceGenerators`
- `Ecs.Arch`
- `Ecs.Arch.Abstractions`
- `SourceGenerators.Common`
The canonical mapping lives in `.agents/skills/_shared/module-map.json`.
If the user supplies a docs section name:
- resolve it back to a source module first
- if it maps to multiple modules, stop at normalization guidance and do not draft docs yet
# Workflow
## 1. Normalize the input
Run:
```bash
python3 .agents/skills/gframework-doc-refresh/scripts/scan_module_evidence.py <module>
```
The script normalizes aliases, reports ambiguity, and prints the module's evidence surface.
If the result is ambiguous:
- return the candidate modules
- ask the user to pick the intended source module
- do not continue into document generation
## 2. Scan the evidence surface
For the resolved module, inspect:
- source directories
- `*.csproj`
- relevant test projects
- sibling `README.md`
- mapped `docs/zh-CN` landing pages and topic pages
- optional `ai-libs/` consumer evidence when needed
Always confirm the actual files in the repository.
Do not assume the mapping is enough on its own.
## 3. Decide whether `ai-libs/` is needed
Use `ai-libs/` when:
- adoption path is unclear from source and README alone
- extension points need a real consumer wiring example
- current docs have concepts but lack an end-to-end integration path
Do not rely on `ai-libs/` for:
- public API contract definitions
- generator diagnostics or semantic guarantees
- claims about what the current version officially supports
If `ai-libs/` conflicts with current source or tests, keep source/tests as the contract and document the migration boundary.
## 4. Judge the documentation state
Classify the module into one or more of these states:
- missing landing page
- stale landing page
- stale topic page
- missing or stale API reference coverage
- stale tutorial/example
- validation-only
Base this on evidence, not on the previous docs shape.
## 5. Choose the output set
Always prioritize:
1. README / landing page / adoption path
2. topic pages
3. API reference
4. tutorials
If the module only needs validation or relinking, do not generate extra pages.
## 6. Draft or update docs
Load only the template that matches the output you selected:
- `templates/module-landing.md`
- `templates/topic-refresh.md`
- `templates/api-reference.md`
- `templates/tutorial.md`
Keep examples minimal, current, and traceable to source or tests.
## 7. Validate
Run the internal validators as needed:
```bash
bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh <file-or-directory>
```
For site-level confirmation after doc edits:
```bash
cd docs && bun run build
```
# Evidence Order
Use this exact priority:
1. source code, XML docs, `*.csproj`
2. tests and snapshots
3. module `README.md`
4. current `docs/zh-CN` pages
5. verified `ai-libs/` consumers
6. archived docs only as fallback context
# Output Rules
- Prefer correcting the adoption path over expanding page count.
- Do not copy wording from outdated docs just to keep page volume.
- Escape generics outside code blocks.
- Keep internal links real and current.
- Mark code blocks with explicit languages.
- Use the smallest example that demonstrates the current contract.
- Consumer examples may align with `ai-libs/`, but must not exceed the current module contract.
# Validation
Use the shared standards in `.agents/skills/_shared/DOCUMENTATION_STANDARDS.md`.
When this skill changes public docs, prefer:
1. focused validator on touched pages
2. `cd docs && bun run build`
When this skill changes the skill system itself:
1. validate `SKILL.md` frontmatter exists
2. run the module scan script for representative modules
3. confirm obsolete `vitepress-*` public entries are gone
# References
Read these only when needed:
- `.agents/skills/_shared/DOCUMENTATION_STANDARDS.md`
- `.agents/skills/_shared/module-map.json`
- `references/module-selection.md`
- `references/evidence-and-ai-libs.md`
- `references/output-strategy.md`

View File

@ -1,4 +0,0 @@
interface:
display_name: "GFramework Doc Refresh"
short_description: "Refresh module docs from code-first evidence"
default_prompt: "Use $gframework-doc-refresh to refresh a GFramework module's docs from source, tests, README, current docs, and ai-libs evidence."

View File

@ -1,35 +0,0 @@
# Evidence And `ai-libs`
The evidence order is fixed:
1. source code, XML docs, `*.csproj`
2. tests and snapshots
3. module README
4. current `docs/zh-CN`
5. `ai-libs/`
6. archived docs
## When To Use `ai-libs`
Use `ai-libs/` to answer questions like:
- How is this extension point wired in a real project?
- What does the minimal project layout look like?
- Which project-side files need to exist for this module to work end to end?
## When Not To Use `ai-libs`
Do not use `ai-libs/` as the primary source for:
- public API semantics
- exact generator output guarantees
- supported package matrix
- diagnostics behavior
## Conflict Rule
If `ai-libs/` drifts from the current repo:
- trust source and tests
- mention the drift as a compatibility or migration note
- do not document old consumer behavior as if it were still the contract

View File

@ -1,29 +0,0 @@
# Module Selection
Use `.agents/skills/_shared/module-map.json` as the canonical source for:
- supported modules
- aliases
- source paths
- test projects
- README paths
- docs landing/topic/fallback pages
- `ai-libs/` reference roots
Selection rules:
1. Prefer explicit canonical module names.
2. Resolve docs section aliases back to source modules before scanning docs.
3. If an alias maps to multiple modules, stop and return the candidate list.
4. If a module has no dedicated docs section, fall back to the nearest existing landing page or API index instead of inventing a fake section.
Representative ambiguous inputs:
- `source-generators` -> likely one of `Core.SourceGenerators`, `Game.SourceGenerators`, `Cqrs.SourceGenerators`, `Godot.SourceGenerators`
- `abstractions` -> likely one of `Core.Abstractions`, `Game.Abstractions`, `Ecs.Arch.Abstractions`
Representative resolvable aliases:
- `core-abstractions` -> `Core.Abstractions`
- `godot generators` -> `Godot.SourceGenerators`
- `ecs` -> `Ecs.Arch`

View File

@ -1,38 +0,0 @@
# Output Strategy
The module scan determines the document type.
Use this priority:
1. fix README / landing page / adoption path
2. fix stale topic pages
3. add or refresh API reference coverage
4. add or refresh tutorials
## Landing Page Checklist
- module purpose
- package relationship
- minimum adoption path
- real entry points
- next-reading links
## Topic Page Checklist
- current role
- public entry points
- minimum example
- compatibility or migration boundary
- related pages
## API Reference Checklist
- only for types or members that materially help consumers
- grounded in XML docs and source
- no speculative examples
## Tutorial Checklist
- only after the landing path is accurate
- keep the scenario traceable to source/tests or `ai-libs/`
- explain why each step exists, not just the code shape

View File

@ -1,226 +0,0 @@
#!/usr/bin/env python3
"""Normalize a GFramework docs module input and report its evidence surface."""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
from typing import Any
SCRIPT_DIR = Path(__file__).resolve().parent
REPO_ROOT = SCRIPT_DIR.parents[3]
MODULE_MAP_PATH = REPO_ROOT / ".agents/skills/_shared/module-map.json"
def load_module_map() -> dict[str, Any]:
return json.loads(MODULE_MAP_PATH.read_text(encoding="utf-8"))
def normalize_key(value: str) -> str:
return value.strip().lower().replace("_", "-").replace(" ", "-")
def resolve_module(raw_input: str, module_map: dict[str, Any]) -> dict[str, Any]:
modules = module_map["modules"]
docs_section_aliases = module_map.get("docs_section_aliases", {})
normalized = normalize_key(raw_input)
for canonical_name in modules:
if normalize_key(canonical_name) == normalized:
return {"status": "ok", "module": canonical_name, "reason": "canonical"}
for canonical_name, config in modules.items():
aliases = config.get("aliases", [])
if normalized in {normalize_key(alias) for alias in aliases}:
return {"status": "ok", "module": canonical_name, "reason": "alias"}
if normalized in docs_section_aliases:
candidates = docs_section_aliases[normalized]
if len(candidates) == 1:
return {"status": "ok", "module": candidates[0], "reason": "docs_section"}
return {
"status": "ambiguous",
"reason": "docs_section",
"input": raw_input,
"candidates": candidates,
}
fuzzy = [
canonical_name
for canonical_name in modules
if normalized in normalize_key(canonical_name) or normalize_key(canonical_name) in normalized
]
if fuzzy:
return {"status": "unknown", "reason": "closest_match", "input": raw_input, "candidates": fuzzy}
return {"status": "unknown", "reason": "no_match", "input": raw_input, "candidates": []}
def collect_path_state(paths: list[str]) -> list[dict[str, Any]]:
states: list[dict[str, Any]] = []
for relative_path in paths:
absolute_path = REPO_ROOT / relative_path
states.append(
{
"path": relative_path,
"exists": absolute_path.exists(),
"kind": "dir" if absolute_path.is_dir() else "file",
}
)
return states
def assess_docs(module_config: dict[str, Any]) -> list[str]:
docs_config = module_config["docs"]
landing = collect_path_state(docs_config.get("landing", []))
topics = collect_path_state(docs_config.get("topics", []))
assessment: list[str] = []
if landing and not any(item["exists"] for item in landing):
assessment.append("landing_missing")
elif landing:
assessment.append("landing_present")
if not topics:
assessment.append("topic_docs_not_mapped")
else:
existing_topics = sum(1 for item in topics if item["exists"])
if existing_topics == 0:
assessment.append("topic_docs_missing")
elif existing_topics < len(topics):
assessment.append("topic_docs_partial")
else:
assessment.append("topic_docs_present")
return assessment
def build_report(module_name: str, module_config: dict[str, Any]) -> dict[str, Any]:
source_paths = collect_path_state(module_config.get("source_paths", []))
test_projects = collect_path_state(module_config.get("test_projects", []))
readmes = collect_path_state(module_config.get("readme_paths", []))
docs_config = module_config["docs"]
ai_libs = module_config.get("ai_libs", {})
report = {
"status": "ok",
"module": module_name,
"source_paths": source_paths,
"project_file": collect_path_state([module_config["project_file"]])[0],
"test_projects": test_projects,
"readme_paths": readmes,
"docs": {
"landing": collect_path_state(docs_config.get("landing", [])),
"topics": collect_path_state(docs_config.get("topics", [])),
"fallback": collect_path_state(docs_config.get("fallback", []))
},
"ai_libs": {
"paths": collect_path_state(ai_libs.get("paths", [])),
"search_hints": ai_libs.get("search_hints", []),
},
"assessment": assess_docs(module_config),
}
if readmes and not any(item["exists"] for item in readmes):
report["assessment"].append("readme_missing")
if test_projects and not any(item["exists"] for item in test_projects):
report["assessment"].append("tests_missing")
if not ai_libs.get("paths"):
report["assessment"].append("ai_libs_optional")
if not docs_config.get("topics"):
report["assessment"].append("fallback_docs_only")
return report
def print_text_report(report: dict[str, Any]) -> None:
if report["status"] != "ok":
print(json.dumps(report, ensure_ascii=False, indent=2))
return
print(f"module: {report['module']}")
print("assessment:")
for item in report["assessment"]:
print(f" - {item}")
print("source:")
for item in report["source_paths"]:
print(f" - {'OK' if item['exists'] else 'MISS'} {item['path']}")
project_file = report["project_file"]
print(f"project: {'OK' if project_file['exists'] else 'MISS'} {project_file['path']}")
print("tests:")
for item in report["test_projects"]:
print(f" - {'OK' if item['exists'] else 'MISS'} {item['path']}")
print("readme:")
if report["readme_paths"]:
for item in report["readme_paths"]:
print(f" - {'OK' if item['exists'] else 'MISS'} {item['path']}")
else:
print(" - none mapped")
print("docs landing:")
for item in report["docs"]["landing"]:
print(f" - {'OK' if item['exists'] else 'MISS'} {item['path']}")
print("docs topics:")
if report["docs"]["topics"]:
for item in report["docs"]["topics"]:
print(f" - {'OK' if item['exists'] else 'MISS'} {item['path']}")
else:
print(" - none mapped")
print("docs fallback:")
for item in report["docs"]["fallback"]:
print(f" - {'OK' if item['exists'] else 'MISS'} {item['path']}")
print("ai-libs:")
if report["ai_libs"]["paths"]:
for item in report["ai_libs"]["paths"]:
print(f" - {'OK' if item['exists'] else 'MISS'} {item['path']}")
else:
print(" - none mapped")
if report["ai_libs"]["search_hints"]:
print("ai-libs search hints:")
for item in report["ai_libs"]["search_hints"]:
print(f" - {item}")
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("module", help="Canonical module name, alias, or docs section name.")
parser.add_argument("--json", action="store_true", help="Emit JSON instead of text.")
args = parser.parse_args()
module_map = load_module_map()
resolution = resolve_module(args.module, module_map)
if resolution["status"] != "ok":
if args.json:
print(json.dumps(resolution, ensure_ascii=False, indent=2))
else:
print(json.dumps(resolution, ensure_ascii=False, indent=2))
return 1
report = build_report(resolution["module"], module_map["modules"][resolution["module"]])
report["resolution"] = resolution
if args.json:
print(json.dumps(report, ensure_ascii=False, indent=2))
else:
print_text_report(report)
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@ -1,67 +0,0 @@
#!/bin/bash
# 运行统一文档校验脚本集合。
set -e
TARGET="$1"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [ -z "$TARGET" ]; then
echo "用法: $0 <文件或目录路径>"
exit 1
fi
if [ ! -e "$TARGET" ]; then
echo "错误: 路径不存在: $TARGET"
exit 1
fi
if [ -f "$TARGET" ]; then
FILES=("$TARGET")
else
mapfile -t FILES < <(find "$TARGET" -type f -name "*.md" | sort)
fi
if [ ${#FILES[@]} -eq 0 ]; then
echo "未找到 Markdown 文件"
exit 0
fi
TOTAL_ERRORS=0
FAILED_FILES=0
for FILE in "${FILES[@]}"; do
FILE_ERRORS=0
echo "验证: $FILE"
if ! bash "$SCRIPT_DIR/validate-frontmatter.sh" "$FILE"; then
FILE_ERRORS=$((FILE_ERRORS + 1))
fi
if ! bash "$SCRIPT_DIR/validate-links.sh" "$FILE"; then
FILE_ERRORS=$((FILE_ERRORS + 1))
fi
if ! bash "$SCRIPT_DIR/validate-code-blocks.sh" "$FILE"; then
FILE_ERRORS=$((FILE_ERRORS + 1))
fi
if [ $FILE_ERRORS -eq 0 ]; then
echo "$FILE"
else
echo "$FILE"
FAILED_FILES=$((FAILED_FILES + 1))
fi
TOTAL_ERRORS=$((TOTAL_ERRORS + FILE_ERRORS))
echo ""
done
if [ $TOTAL_ERRORS -eq 0 ]; then
echo "✓ 所有验证通过"
exit 0
fi
echo "✗ 验证失败:$FAILED_FILES 个文件存在问题"
exit 1

View File

@ -1,62 +0,0 @@
#!/bin/bash
# 验证 Markdown 代码块是否闭合并带有语言标记。
set -e
FILE="$1"
if [ -z "$FILE" ]; then
echo "用法: $0 <文件路径>"
exit 1
fi
if [ ! -f "$FILE" ]; then
echo "错误: 文件不存在: $FILE"
exit 1
fi
ERROR_COUNT=0
WARNING_COUNT=0
CODE_FENCE_COUNT=$(grep -c '^```' "$FILE" || true)
if [ $((CODE_FENCE_COUNT % 2)) -ne 0 ]; then
echo "✗ 错误: 存在未闭合的代码块"
ERROR_COUNT=$((ERROR_COUNT + 1))
fi
LINE_NUMBER=0
IN_CODE_BLOCK=0
while IFS= read -r LINE || [ -n "$LINE" ]; do
LINE_NUMBER=$((LINE_NUMBER + 1))
if [[ "$LINE" =~ ^\`\`\`(cs|c#|C#)$ ]]; then
echo "⚠ 警告: 第 $LINE_NUMBER 行使用了非标准 C# 标记,建议改为 csharp"
WARNING_COUNT=$((WARNING_COUNT + 1))
fi
if [[ "$LINE" =~ ^\`\`\` ]]; then
if [ "$IN_CODE_BLOCK" -eq 0 ]; then
if [[ "$LINE" == '```' ]]; then
echo "⚠ 警告: 第 $LINE_NUMBER 行的代码块缺少语言标记"
WARNING_COUNT=$((WARNING_COUNT + 1))
fi
IN_CODE_BLOCK=1
else
IN_CODE_BLOCK=0
fi
fi
done < "$FILE"
if [ $ERROR_COUNT -eq 0 ] && [ $WARNING_COUNT -eq 0 ]; then
echo "✓ 代码块验证通过"
exit 0
fi
if [ $ERROR_COUNT -eq 0 ]; then
echo "⚠ 代码块验证通过,但有 $WARNING_COUNT 个警告"
exit 0
fi
echo "✗ 代码块验证失败($ERROR_COUNT 个错误,$WARNING_COUNT 个警告)"
exit 1

View File

@ -1,40 +0,0 @@
#!/bin/bash
# 验证 Markdown frontmatter。
set -e
FILE="$1"
if [ -z "$FILE" ]; then
echo "用法: $0 <文件路径>"
exit 1
fi
if [ ! -f "$FILE" ]; then
echo "错误: 文件不存在: $FILE"
exit 1
fi
if ! head -n 5 "$FILE" | grep -q "^---$"; then
echo "✗ 错误: 文件缺少 frontmatter"
exit 1
fi
FRONTMATTER=$(sed -n '/^---$/,/^---$/p' "$FILE" | sed '1d;$d')
if [ -z "$FRONTMATTER" ]; then
echo "✗ 错误: frontmatter 为空"
exit 1
fi
if ! echo "$FRONTMATTER" | grep -q "^title:"; then
echo "✗ 错误: 缺少必需字段: title"
exit 1
fi
if ! echo "$FRONTMATTER" | grep -q "^description:"; then
echo "✗ 错误: 缺少必需字段: description"
exit 1
fi
echo "✓ Frontmatter 验证通过"

View File

@ -1,27 +0,0 @@
---
title: {{API_TITLE}}
description: {{API_DESCRIPTION}}
outline: deep
---
# {{API_TITLE}}
## 概述
{{API_OVERVIEW}}
## 适用范围
{{API_SCOPE}}
## 关键成员
{{KEY_MEMBERS}}
## 最小示例
{{MINIMUM_EXAMPLE}}
## 相关类型
{{RELATED_TYPES}}

View File

@ -1,30 +0,0 @@
---
title: {{MODULE_TITLE}}
description: {{MODULE_DESCRIPTION}}
---
# {{MODULE_TITLE}}
## 模块定位
{{MODULE_POSITIONING}}
## 包关系
{{PACKAGE_RELATIONSHIP}}
## 最小接入路径
{{MINIMUM_ADOPTION_PATH}}
## 关键入口
{{KEY_ENTRY_POINTS}}
## 当前边界
{{CURRENT_BOUNDARIES}}
## 继续阅读
{{NEXT_READING}}

View File

@ -1,26 +0,0 @@
---
title: {{TOPIC_TITLE}}
description: {{TOPIC_DESCRIPTION}}
---
# {{TOPIC_TITLE}}
## 当前角色
{{CURRENT_ROLE}}
## 公开入口
{{PUBLIC_ENTRY_POINTS}}
## 最小示例
{{MINIMUM_EXAMPLE}}
## 兼容与迁移边界
{{COMPATIBILITY_BOUNDARY}}
## 相关页面
{{RELATED_PAGES}}

View File

@ -1,30 +0,0 @@
---
title: {{TUTORIAL_TITLE}}
description: {{TUTORIAL_DESCRIPTION}}
---
# {{TUTORIAL_TITLE}}
## 学习目标
{{LEARNING_OBJECTIVES}}
## 前置条件
{{PREREQUISITES}}
## 步骤
{{STEP_SEQUENCE}}
## 完整代码
{{FULL_CODE}}
## 验证结果
{{EXPECTED_RESULT}}
## 继续阅读
{{NEXT_READING}}

View File

@ -1,4 +0,0 @@
interface:
display_name: "GFramework PR Review"
short_description: "Inspect the current PR and AI review findings"
default_prompt: "Use $gframework-pr-review to inspect the current branch PR through the GitHub API, prioritize unresolved review threads on the latest head commit from supported AI reviewers such as CodeRabbit and greptile-apps, and summarize failed checks or failed tests."

469
.claude/skills/README.md Normal file
View File

@ -0,0 +1,469 @@
# VitePress 文档生成 Skills 系统
为 GFramework 项目提供自动化的 VitePress 文档生成能力。
## 概述
这是一套专门为 GFramework 项目设计的文档生成 skills能够根据 C# 源代码自动生成高质量的 VitePress 文档。系统采用模块化设计,每个 skill 专注于特定的文档生成任务。
## 可用 Skills
### 1. vitepress-api-doc - API 文档生成
为单个 C# 文件生成 API 参考文档。
**用途**
- 类、接口、枚举的 API 文档
- 方法、属性、事件的详细说明
- 基于 XML 注释生成文档
**调用方式**
```bash
/vitepress-api-doc <C# 文件路径>
```
**示例**
```bash
/vitepress-api-doc GFramework.Core/architecture/Architecture.cs
```
**输出位置**`docs/zh-CN/api-reference/<模块>/<文件名>.md`
[详细文档](./vitepress-api-doc/SKILL.md)
---
### 2. vitepress-guide - 功能指南生成
生成功能模块的使用指南文档。
**用途**
- 核心功能模块的使用说明
- 设计模式和架构概念
- 最佳实践和常见问题
**调用方式**
```bash
/vitepress-guide <主题> <目标模块>
```
**示例**
```bash
/vitepress-guide "事件系统" Core
/vitepress-guide "IoC 容器" Core
```
**输出位置**`docs/zh-CN/<模块>/<主题>.md`
[详细文档](./vitepress-guide/SKILL.md)
---
### 3. vitepress-tutorial - 分步教程生成
生成分步教程文档,适合初学者学习。
**用途**
- 框架入门教程
- 功能实现教程
- 问题解决方案
**调用方式**
```bash
/vitepress-tutorial <教程主题>
```
**示例**
```bash
/vitepress-tutorial "创建第一个 System"
/vitepress-tutorial "使用事件系统"
```
**输出位置**`docs/zh-CN/tutorials/<主题>.md`
[详细文档](./vitepress-tutorial/SKILL.md)
---
### 4. vitepress-batch-api - 批量 API 文档生成
为整个模块批量生成 API 文档。
**用途**
- 初始化模块文档
- 更新整个模块的文档
- 快速生成大量文档
**调用方式**
```bash
/vitepress-batch-api <模块名>
```
**示例**
```bash
/vitepress-batch-api Core
/vitepress-batch-api Godot
```
**输出位置**`docs/zh-CN/api-reference/<模块>/`
[详细文档](./vitepress-batch-api/SKILL.md)
---
### 5. vitepress-validate - 文档验证
验证文档的质量和规范性。
**用途**
- Frontmatter 格式验证
- 内部链接有效性检查
- 代码块语法验证
- 标点符号规范检查
**调用方式**
```bash
/vitepress-validate <文件或目录路径>
```
**示例**
```bash
/vitepress-validate docs/zh-CN/api-reference/core/architecture.md
/vitepress-validate docs/zh-CN/
```
[详细文档](./vitepress-validate/SKILL.md)
---
## 快速开始
### 1. 生成单个 API 文档
```bash
# 为 Architecture 类生成文档
/vitepress-api-doc GFramework.Core/architecture/Architecture.cs
```
### 2. 批量生成模块文档
```bash
# 为整个 Core 模块生成文档
/vitepress-batch-api Core
```
### 3. 生成功能指南
```bash
# 生成事件系统使用指南
/vitepress-guide "事件系统" Core
```
### 4. 生成教程
```bash
# 生成创建 Model 的教程
/vitepress-tutorial "创建第一个 Model"
```
### 5. 验证文档
```bash
# 验证生成的文档
/vitepress-validate docs/zh-CN/api-reference/core/
```
## 工作流程
### 典型工作流程
```mermaid
graph TD
A[开始] --> B{文档类型?}
B -->|API 文档| C[/vitepress-api-doc]
B -->|功能指南| D[/vitepress-guide]
B -->|教程| E[/vitepress-tutorial]
C --> F[/vitepress-validate]
D --> F
E --> F
F --> G{验证通过?}
G -->|是| H[完成]
G -->|否| I[修复问题]
I --> F
```
### 推荐流程
1. **初始化模块文档**
```bash
/vitepress-batch-api Core
```
2. **生成功能指南**
```bash
/vitepress-guide "IoC 容器" Core
/vitepress-guide "事件系统" Core
```
3. **生成教程**
```bash
/vitepress-tutorial "创建第一个 Model"
/vitepress-tutorial "使用命令系统"
```
4. **验证所有文档**
```bash
/vitepress-validate docs/zh-CN/
```
## 目录结构
```
.claude/skills/
├── README.md # 本文件
├── _shared/ # 共享资源
│ └── scripts/ # 共享脚本
│ ├── update-vitepress-nav.sh # 更新导航配置
│ ├── parse-csharp-xml.sh # 解析 XML 注释
│ └── generate-examples.sh # 生成代码示例
├── vitepress-api-doc/ # API 文档生成
│ ├── SKILL.md # Skill 说明
│ ├── template.md # 文档模板
│ └── examples/ # 示例文档
│ ├── class-example.md
│ ├── interface-example.md
│ └── enum-example.md
├── vitepress-guide/ # 功能指南生成
│ ├── SKILL.md
│ ├── template.md
│ └── examples/
│ └── guide-example.md
├── vitepress-tutorial/ # 教程生成
│ ├── SKILL.md
│ ├── template.md
│ └── examples/
│ └── tutorial-example.md
├── vitepress-batch-api/ # 批量 API 文档生成
│ ├── SKILL.md
│ └── scripts/
│ └── batch-generate.sh
└── vitepress-validate/ # 文档验证
├── SKILL.md
└── scripts/
├── validate-frontmatter.sh
├── validate-links.sh
├── validate-code-blocks.sh
└── validate-all.sh
```
## 设计原则
### 1. 单一职责
每个 skill 专注于一个特定任务:
- `vitepress-api-doc` - 单文件 API 文档
- `vitepress-guide` - 功能指南
- `vitepress-tutorial` - 分步教程
- `vitepress-batch-api` - 批量生成
- `vitepress-validate` - 质量验证
### 2. 模块化设计
- 共享脚本放在 `_shared/scripts/`
- 每个 skill 独立维护
- 可以单独使用或组合使用
### 3. 基于源代码
- 仅使用 XML 注释,不添加 AI 补充
- 保持文档与代码同步
- 代码示例由 AI 自动生成
### 4. 质量保证
- 所有生成的文档都应通过验证
- 遵循 VitePress 规范
- 保持一致的文档风格
## 文档规范
### Frontmatter 格式
```yaml
---
title: 文档标题
description: 简短描述1-2 句话)
outline: deep # 可选
---
```
### 代码块标记
- C# 代码使用 `csharp`
- Bash 脚本使用 `bash`
- JSON 使用 `json`
- YAML 使用 `yaml`
### 泛型符号转义
在正文中使用 HTML 实体:
- `List<T>``List&lt;T&gt;`
- 代码块内保持原样
### 中文标点符号
- 中文句子使用全角标点:,。!?
- 英文句子使用半角标点:,.!?
- 代码周围使用半角符号
## 共享脚本
### update-vitepress-nav.sh
更新 VitePress 侧边栏导航配置。
**用法**
```bash
.claude/skills/_shared/scripts/update-vitepress-nav.sh <文件路径> <标题>
```
### parse-csharp-xml.sh
解析 C# XML 文档注释。
**用法**
```bash
.claude/skills/_shared/scripts/parse-csharp-xml.sh <C# 文件路径>
```
### generate-examples.sh
生成代码示例。
**用法**
```bash
.claude/skills/_shared/scripts/generate-examples.sh <类型名> <命名空间>
```
## 最佳实践
### 1. 文档生成顺序
1. 先生成 API 文档(基础)
2. 再生成功能指南(概念)
3. 最后生成教程(实践)
### 2. 保持文档同步
- 修改代码后及时更新文档
- 使用单文件生成更新特定文档
- 定期批量验证所有文档
### 3. 质量控制
- 生成后立即验证
- 修复所有错误和警告
- 确保链接有效
### 4. 版本控制
- 将生成的文档提交到 Git
- 在 PR 中包含文档更新
- 保持文档与代码版本一致
## 故障排除
### 问题:生成的文档缺少内容
**原因**:源代码缺少 XML 注释
**解决方案**
1. 在源代码中添加 XML 注释
2. 重新生成文档
### 问题:验证失败
**原因**:文档格式不符合规范
**解决方案**
1. 查看验证错误信息
2. 根据提示修复问题
3. 重新验证
### 问题:链接损坏
**原因**:文件路径错误或文件不存在
**解决方案**
1. 检查链接的目标文件是否存在
2. 修正文件路径
3. 重新验证
### 问题:批量生成速度慢
**原因**:文件数量多
**解决方案**
1. 使用 `--parallel` 选项(如果支持)
2. 分批生成
3. 仅生成修改的文件
## 扩展开发
### 添加新 Skill
1. 在 `.claude/skills/` 下创建新目录
2. 创建 `SKILL.md` 说明文档
3. 创建必要的模板和脚本
4. 在本 README 中添加说明
### 修改现有 Skill
1. 更新 `SKILL.md` 文档
2. 修改模板或脚本
3. 更新示例文档
4. 测试修改后的功能
## 贡献指南
### 报告问题
在 GitHub Issues 中报告问题,包含:
- 使用的 skill 名称
- 输入参数
- 预期结果
- 实际结果
- 错误信息
### 提交改进
1. Fork 项目
2. 创建功能分支
3. 提交修改
4. 创建 Pull Request
## 版本历史
- v1.0.0 (2025-01-XX) - 初始版本
- 5 个核心 skills
- 3 个共享脚本
- 完整的文档和示例
## 许可证
与 GFramework 项目保持一致。
## 联系方式
如有问题或建议,请通过以下方式联系:
- GitHub Issues
- 项目讨论区
---
**注意**:本 skills 系统专为 GFramework 项目设计,使用前请确保了解项目结构和文档规范。

View File

@ -0,0 +1,205 @@
# GFramework 文档编写规范
## Markdown 语法规范
### 1. 泛型标记转义
在 Markdown 文档中,所有泛型标记必须转义,否则会被 VitePress 误认为 HTML 标签。
**错误示例**:
```markdown
`Option<T>` 是一个泛型类型
`Result<TValue, TError>` 表示结果
public class Repository<TData> { }
```
**正确示例**:
```markdown
`Option&lt;T&gt;` 是一个泛型类型
`Result&lt;TValue, TError&gt;` 表示结果
public class Repository&lt;TData&gt; { }
```
**常见泛型标记**:
- `<T>``&lt;T&gt;`
- `<TResult>``&lt;TResult&gt;`
- `<TValue>``&lt;TValue&gt;`
- `<TError>``&lt;TError&gt;`
- `<TSaveData>``&lt;TSaveData&gt;`
- `<TData>``&lt;TData&gt;`
- `<TNode>``&lt;TNode&gt;`
### 2. HTML 标签转义
如果需要在文档中显示 HTML 标签,必须转义:
- `<summary>``&lt;summary&gt;`
- `<param>``&lt;param&gt;`
- `<returns>``&lt;returns&gt;`
### 3. 链接验证
**内部链接规则**:
- 使用相对路径: `/zh-CN/core/events`
- 确保目标文件存在
- 不要链接到尚未创建的页面
**已存在的文档路径**:
**Core 模块**:
- `/zh-CN/core/architecture` - 架构系统
- `/zh-CN/core/ioc` - IoC 容器
- `/zh-CN/core/events` - 事件系统
- `/zh-CN/core/command` - 命令系统
- `/zh-CN/core/query` - 查询系统
- `/zh-CN/core/model` - Model 系统
- `/zh-CN/core/system` - System 系统
- `/zh-CN/core/utility` - Utility 系统
- `/zh-CN/core/controller` - Controller 系统
- `/zh-CN/core/logging` - 日志系统
- `/zh-CN/core/pool` - 对象池
- `/zh-CN/core/property` - 可绑定属性
- `/zh-CN/core/lifecycle` - 生命周期管理
- `/zh-CN/core/coroutine` - 协程系统
- `/zh-CN/core/resource` - 资源管理
- `/zh-CN/core/state-machine` - 状态机
- `/zh-CN/core/cqrs` - CQRS 与 Mediator
- `/zh-CN/core/functional` - 函数式编程
- `/zh-CN/core/pause` - 暂停管理
- `/zh-CN/core/configuration` - 配置管理
- `/zh-CN/core/ecs` - ECS 系统集成
- `/zh-CN/core/extensions` - 扩展方法
- `/zh-CN/core/rule` - 规则系统
- `/zh-CN/core/environment` - 环境系统
- `/zh-CN/core/context` - 上下文系统
- `/zh-CN/core/async-initialization` - 异步初始化
**Game 模块**:
- `/zh-CN/game/scene` - 场景系统
- `/zh-CN/game/ui` - UI 系统
- `/zh-CN/game/data` - 数据与存档
- `/zh-CN/game/storage` - 存储系统
- `/zh-CN/game/serialization` - 序列化系统
- `/zh-CN/game/setting` - 设置系统
**Godot 模块**:
- `/zh-CN/godot/architecture` - Godot 架构集成
- `/zh-CN/godot/scene` - Godot 场景系统
- `/zh-CN/godot/ui` - Godot UI 系统
- `/zh-CN/godot/pool` - Godot 节点池
- `/zh-CN/godot/resource` - Godot 资源仓储
- `/zh-CN/godot/logging` - Godot 日志系统
- `/zh-CN/godot/pause` - Godot 暂停处理
- `/zh-CN/godot/extensions` - Godot 扩展
- `/zh-CN/godot/coroutine` - Godot 协程
- `/zh-CN/godot/signal` - Godot 信号
- `/zh-CN/godot/storage` - Godot 存储
**教程**:
- `/zh-CN/tutorials/coroutine-tutorial` - 协程系统教程
- `/zh-CN/tutorials/state-machine-tutorial` - 状态机教程
- `/zh-CN/tutorials/resource-management` - 资源管理教程
- `/zh-CN/tutorials/save-system` - 存档系统教程
- `/zh-CN/tutorials/godot-complete-project` - Godot 完整项目
- `/zh-CN/tutorials/functional-programming` - 函数式编程实践
- `/zh-CN/tutorials/pause-system` - 暂停系统实现
- `/zh-CN/tutorials/data-migration` - 数据迁移实践
- `/zh-CN/tutorials/godot-integration` - Godot 集成
- `/zh-CN/tutorials/advanced-patterns` - 高级模式
**其他**:
- `/zh-CN/getting-started/quick-start` - 快速开始
- `/zh-CN/getting-started/installation` - 安装指南
- `/zh-CN/best-practices/architecture-patterns` - 架构模式
**不存在的路径** (不要链接):
- `/zh-CN/best-practices/performance` - 尚未创建
- `/zh-CN/core/serializer` - 错误路径,应使用 `/zh-CN/game/serialization`
## 代码块规范
### 1. 代码块语言标识
始终指定代码块的语言:
```markdown
\`\`\`csharp
public class Example { }
\`\`\`
\`\`\`bash
npm install
\`\`\`
```
### 2. 代码注释
代码示例应包含中文注释:
```csharp
// 创建玩家实体
var player = new Player
{
Name = "玩家1", // 玩家名称
Level = 1 // 初始等级
};
```
## Frontmatter 规范
每个文档必须包含正确的 frontmatter:
```yaml
---
title: 文档标题
description: 简短描述1-2 句话)
---
```
## 文档结构规范
### 指南文档结构
1. 概述
2. 核心概念
3. 基本用法
4. 高级用法
5. 最佳实践
6. 常见问题
7. 相关文档
### 教程文档结构
1. 学习目标
2. 前置条件
3. 步骤 1-N (3-7 步)
4. 完整代码
5. 运行结果
6. 下一步
7. 相关文档
## 验证清单
生成文档后,必须检查:
- [ ] 所有泛型标记已转义 (`<T>``&lt;T&gt;`)
- [ ] 所有内部链接指向存在的页面
- [ ] Frontmatter 格式正确
- [ ] 代码块指定了语言
- [ ] 代码包含中文注释
- [ ] 文档结构完整
- [ ] 没有 HTML 标签错误
## 自动修复脚本
如果文档已生成,可以使用以下脚本修复常见问题:
```bash
# 修复泛型标记
sed -i 's/<T>/\&lt;T\&gt;/g' file.md
sed -i 's/<TResult>/\&lt;TResult\&gt;/g' file.md
sed -i 's/<TValue>/\&lt;TValue\&gt;/g' file.md
sed -i 's/<TError>/\&lt;TError\&gt;/g' file.md
# 验证构建
cd docs && bun run build
```

View File

@ -0,0 +1,84 @@
#!/bin/bash
# 共享的模块配置
# 用于统一管理 GFramework 项目的模块映射关系
# 根据模块名获取源代码目录
get_source_dir() {
local MODULE="$1"
case "$MODULE" in
Core)
echo "GFramework.Core"
;;
Game)
echo "GFramework.Game"
;;
Godot)
echo "GFramework.Godot"
;;
SourceGenerators)
echo "GFramework.SourceGenerators"
;;
*)
echo ""
return 1
;;
esac
}
# 根据模块名获取文档输出目录
get_docs_dir() {
local MODULE="$1"
case "$MODULE" in
Core)
echo "docs/zh-CN/api-reference/core"
;;
Game)
echo "docs/zh-CN/api-reference/game"
;;
Godot)
echo "docs/zh-CN/api-reference/godot"
;;
SourceGenerators)
echo "docs/zh-CN/api-reference/source-generators"
;;
*)
echo ""
return 1
;;
esac
}
# 根据命名空间推断模块名
infer_module_from_namespace() {
local NAMESPACE="$1"
if [[ "$NAMESPACE" == GFramework.Core* ]]; then
echo "Core"
elif [[ "$NAMESPACE" == GFramework.Game* ]]; then
echo "Game"
elif [[ "$NAMESPACE" == GFramework.Godot* ]]; then
echo "Godot"
elif [[ "$NAMESPACE" == GFramework.SourceGenerators* ]]; then
echo "SourceGenerators"
else
echo ""
return 1
fi
}
# 获取所有可用模块列表
get_all_modules() {
echo "Core Game Godot SourceGenerators"
}
# 验证模块名是否有效
is_valid_module() {
local MODULE="$1"
case "$MODULE" in
Core|Game|Godot|SourceGenerators)
return 0
;;
*)
return 1
;;
esac
}

View File

@ -0,0 +1,210 @@
# VitePress API 文档生成
为单个 C# 类、接口或枚举生成符合 VitePress 标准的 API 参考文档。
## 用途
此 skill 用于从 C# 源代码文件自动生成结构化的 API 文档,包括:
- 类型概述和命名空间信息
- 构造函数、方法、属性的详细说明
- 基于 XML 文档注释的描述
- 自动生成的使用示例
- 相关类型的交叉引用
## 调用方式
```bash
/vitepress-api-doc <C# 文件路径>
```
**示例**
```bash
/vitepress-api-doc GFramework.Core/architecture/Architecture.cs
```
## 工作流程
1. **读取源代码文件**
- 验证文件存在且为 C# 文件
- 读取完整的源代码内容
2. **解析代码结构**
- 提取命名空间、类名、访问修饰符
- 识别类型class/interface/enum/struct
- 解析继承关系和实现的接口
- 提取所有公共成员(构造函数、方法、属性、事件、字段)
3. **提取 XML 文档注释**
- 解析 `/// <summary>` 标签(类型和成员描述)
- 解析 `/// <param>` 标签(参数说明)
- 解析 `/// <returns>` 标签(返回值说明)
- 解析 `/// <exception>` 标签(异常说明)
- 解析 `/// <example>` 标签(示例代码)
- 解析 `/// <see cref=""/>` 标签(交叉引用)
4. **生成 Markdown 文档**
- 根据 `template.md` 填充内容
- 转义泛型符号(`<T>``&lt;T&gt;`
- 生成使用示例(基于 API 签名)
- 添加相关文档链接
5. **确定输出路径**
- 根据命名空间确定模块Core/Game/Godot/SourceGenerators
- 输出到 `docs/zh-CN/api-reference/<模块>/<类名>.md`
6. **更新 VitePress 配置**
- 调用共享脚本 `update-vitepress-nav.sh`
- 在侧边栏配置中添加新文档条目
7. **验证文档质量**
- 检查 Frontmatter 格式
- 验证内部链接
- 确保代码块语法正确
## 输出规范
### Frontmatter 格式
```yaml
---
title: 类名
description: 从 XML <summary> 提取的简短描述
outline: deep
---
```
### 文档结构
1. **标题**:使用类名作为一级标题
2. **概述**XML summary 内容
3. **命名空间和程序集信息**
4. **继承链**(如果适用)
5. **构造函数**(如果有)
6. **公共方法**(按字母顺序)
7. **公共属性**(按字母顺序)
8. **公共事件**(如果有)
9. **使用示例**(自动生成)
10. **另请参阅**(相关类型链接)
### 代码块格式
所有 C# 代码块必须使用:
```markdown
\`\`\`csharp
// 代码内容
\`\`\`
```
### 泛型符号转义
- `List<T>``List&lt;T&gt;`
- `Dictionary<K, V>``Dictionary&lt;K, V&gt;`
- `IEnumerable<T>``IEnumerable&lt;T&gt;`
### 内部链接格式
- 相对路径:`[Architecture](./architecture.md)`
- 绝对路径:`[Core 架构](/zh-CN/core/architecture)`
- 锚点链接:`[构造函数](#构造函数)`
## 前置条件
1. 项目必须有 VitePress 配置文件(`docs/.vitepress/config.mts`
2. 目标 C# 文件必须存在且可读
3. C# 文件必须包含 XML 文档注释(`///`
4. 文件必须包含至少一个公共类型
## 配置选项
### 自动检测模块
根据命名空间自动确定模块:
- `GFramework.Core.*``core`
- `GFramework.Game.*``game`
- `GFramework.Godot.*``godot`
- `GFramework.SourceGenerators.*``source-generators`
### 示例生成策略
- **基本用法**:最简单的 API 调用
- **常见场景**:实际应用案例
- **高级用法**:复杂配置(如果适用)
## 示例输出
参考 `examples/` 目录中的示例文档:
- `class-example.md` - 类文档示例
- `interface-example.md` - 接口文档示例
- `enum-example.md` - 枚举文档示例
## 注意事项
1. **仅使用 XML 注释**:不对缺失的注释进行 AI 补充
2. **仅提取公共成员**:忽略 `internal``private``protected` 成员
3. **保持文档同步**:文档内容直接来源于代码,确保准确性
4. **遵循项目风格**:参考现有文档的格式和术语
## 相关 Skills
- `/vitepress-validate` - 验证生成的文档质量
- `/vitepress-batch-api` - 批量生成整个模块的 API 文档
## 技术细节
### XML 注释标签映射
| XML 标签 | Markdown 输出 |
|---------|--------------|
| `<summary>` | 概述章节 |
| `<param name="x">` | 参数列表 |
| `<returns>` | 返回值说明 |
| `<exception cref="T">` | 异常列表 |
| `<example>` | 示例代码块 |
| `<see cref="T"/>` | 内部链接 |
| `<remarks>` | 备注章节 |
### 成员签名格式
**方法**
```markdown
### MethodName
描述内容
**签名**
\`\`\`csharp
public ReturnType MethodName(ParamType param)
\`\`\`
**参数**
- `param` (ParamType): 参数说明
**返回值**
- (ReturnType): 返回值说明
```
**属性**
```markdown
### PropertyName
描述内容
**类型**`PropertyType`
**访问**get / set
```
## 故障排除
### 问题:找不到 XML 注释
**解决方案**:确保 C# 文件包含 `///` 注释,而不是 `//``/* */`
### 问题:泛型符号显示错误
**解决方案**VitePress 配置中已包含 `safeGenericEscapePlugin`,确保正确转义
### 问题:侧边栏未更新
**解决方案**:检查 `update-vitepress-nav.sh` 脚本是否正确执行
## 版本历史
- v1.0.0 - 初始版本,支持类、接口、枚举的文档生成

View File

@ -0,0 +1,252 @@
---
title: Architecture
description: 架构基类,提供系统、模型、工具等组件的注册与管理功能。专注于生命周期管理、初始化流程控制和架构阶段转换。
outline: deep
---
# Architecture
## 概述
架构基类,提供系统、模型、工具等组件的注册与管理功能。专注于生命周期管理、初始化流程控制和架构阶段转换。
**命名空间**`GFramework.Core.architecture`
**程序集**`GFramework.Core`
**继承**`Object``Architecture`
**实现**`IArchitecture`
## 构造函数
### Architecture
创建架构实例。
**签名**
```csharp
public Architecture(
IArchitectureConfiguration? configuration = null,
IEnvironment? environment = null,
IArchitectureServices? services = null,
IArchitectureContext? context = null
)
```
**参数**
- `configuration` (IArchitectureConfiguration?): 架构配置对象,为 null 时使用默认配置
- `environment` (IEnvironment?): 环境配置对象,为 null 时使用默认环境
- `services` (IArchitectureServices?): 架构服务对象,为 null 时创建新实例
- `context` (IArchitectureContext?): 架构上下文对象,为 null 时创建新实例
## 公共方法
### Initialize
同步初始化架构,阻塞当前线程直到初始化完成。
**签名**
```csharp
public void Initialize()
```
**特点**
- 阻塞式初始化
- 适用于简单场景或控制台应用
- 初始化失败时抛出异常并进入 `FailedInitialization` 阶段
### InitializeAsync
异步初始化架构,返回 Task 以便调用者可以等待初始化完成。
**签名**
```csharp
public async Task InitializeAsync()
```
**返回值**
- (Task): 表示异步初始化操作的任务
**特点**
- 非阻塞式初始化
- 支持异步组件初始化
- 适用于需要异步加载资源的场景
### InstallModule
安装架构模块,用于扩展架构功能。
**签名**
```csharp
public IArchitectureModule InstallModule(IArchitectureModule module)
```
**参数**
- `module` (IArchitectureModule): 要安装的模块实例
**返回值**
- (IArchitectureModule): 返回安装的模块实例
### RegisterSystem
注册系统组件到架构中。
**签名**
```csharp
public void RegisterSystem<TSystem>(TSystem system) where TSystem : ISystem
```
**类型参数**
- `TSystem`: 系统类型,必须实现 ISystem 接口
**参数**
- `system` (TSystem): 要注册的系统实例
### RegisterModel
注册模型组件到架构中。
**签名**
```csharp
public void RegisterModel<TModel>(TModel model) where TModel : IModel
```
**类型参数**
- `TModel`: 模型类型,必须实现 IModel 接口
**参数**
- `model` (TModel): 要注册的模型实例
### RegisterUtility
注册工具组件到架构中。
**签名**
```csharp
public void RegisterUtility<TUtility>(TUtility utility) where TUtility : IUtility
```
**类型参数**
- `TUtility`: 工具类型,必须实现 IUtility 接口
**参数**
- `utility` (TUtility): 要注册的工具实例
### SendCommand
发送并执行命令。
**签名**
```csharp
public void SendCommand(ICommand command)
```
**参数**
- `command` (ICommand): 要执行的命令实例
### SendCommand&lt;TResult&gt;
发送并执行带返回值的命令。
**签名**
```csharp
public TResult SendCommand<TResult>(ICommand<TResult> command)
```
**类型参数**
- `TResult`: 命令返回值类型
**参数**
- `command` (ICommand&lt;TResult&gt;): 要执行的命令实例
**返回值**
- (TResult): 命令执行结果
## 公共属性
### CurrentPhase
获取当前架构的阶段。
**类型**`ArchitecturePhase`
**访问**get
### Context
获取架构上下文,提供对架构服务的访问。
**类型**`IArchitectureContext`
**访问**get
### IsReady
获取一个布尔值,指示当前架构是否处于就绪状态。
**类型**`bool`
**访问**get
## 使用示例
### 基本用法
```csharp
// 1. 定义你的架构(继承 Architecture 基类)
public class GameArchitecture : Architecture
{
protected override void Init()
{
// 注册 Model
RegisterModel(new PlayerModel());
RegisterModel(new InventoryModel());
// 注册 System
RegisterSystem(new GameplaySystem());
RegisterSystem(new SaveSystem());
// 注册 Utility
RegisterUtility(new StorageUtility());
RegisterUtility(new TimeUtility());
}
}
// 2. 创建并初始化架构
var architecture = new GameArchitecture();
architecture.Initialize();
// 3. 等待架构就绪
await architecture.WaitUntilReadyAsync();
```
### 异步初始化
```csharp
var architecture = new GameArchitecture();
await architecture.InitializeAsync(); // 异步等待初始化完成
// 检查架构是否已就绪
if (architecture.IsReady)
{
Console.WriteLine("架构已就绪,可以开始游戏");
}
```
### 使用自定义配置
```csharp
var config = new ArchitectureConfiguration
{
ArchitectureProperties = new ArchitectureProperties
{
StrictPhaseValidation = true, // 启用严格阶段验证
AllowLateRegistration = false // 禁止就绪后注册组件
}
};
var architecture = new GameArchitecture(configuration: config);
architecture.Initialize();
```
## 另请参阅
- [IArchitecture](./iarchitecture.md) - 架构接口
- [ArchitecturePhase](./architecture-phase.md) - 架构阶段枚举
- [IArchitectureModule](./iarchitecture-module.md) - 架构模块接口
- [架构组件](/zh-CN/core/architecture) - 架构使用指南

View File

@ -0,0 +1,193 @@
---
title: ArchitecturePhase
description: 架构阶段枚举,定义了架构生命周期的各个阶段。
outline: deep
---
# ArchitecturePhase
## 概述
架构阶段枚举,定义了架构生命周期的各个阶段。
**命名空间**`GFramework.Core.Abstractions.enums`
**程序集**`GFramework.Core.Abstractions`
**基础类型**`Enum`
## 枚举值
### None
初始阶段,架构尚未开始初始化。
**值**`0`
### BeforeUtilityInit
工具初始化前阶段。
**值**`1`
### AfterUtilityInit
工具初始化后阶段。
**值**`2`
### BeforeModelInit
模型初始化前阶段。
**值**`3`
### AfterModelInit
模型初始化后阶段。
**值**`4`
### BeforeSystemInit
系统初始化前阶段。
**值**`5`
### AfterSystemInit
系统初始化后阶段。
**值**`6`
### Ready
就绪状态,架构已完全初始化并可以使用。
**值**`7`
### FailedInitialization
初始化失败状态。
**值**`8`
### Destroying
正在销毁阶段。
**值**`9`
### Destroyed
已销毁阶段。
**值**`10`
## 使用示例
### 检查架构阶段
```csharp
var architecture = new GameArchitecture();
architecture.Initialize();
// 检查架构是否已就绪
if (architecture.CurrentPhase == ArchitecturePhase.Ready)
{
Console.WriteLine("架构已就绪,可以开始游戏");
}
```
### 监听阶段变化
```csharp
public class PhaseMonitor : IArchitectureLifecycle
{
public void OnPhase(ArchitecturePhase phase, IArchitecture architecture)
{
switch (phase)
{
case ArchitecturePhase.BeforeUtilityInit:
Console.WriteLine("开始初始化工具");
break;
case ArchitecturePhase.AfterUtilityInit:
Console.WriteLine("工具初始化完成");
break;
case ArchitecturePhase.BeforeModelInit:
Console.WriteLine("开始初始化模型");
break;
case ArchitecturePhase.AfterModelInit:
Console.WriteLine("模型初始化完成");
break;
case ArchitecturePhase.BeforeSystemInit:
Console.WriteLine("开始初始化系统");
break;
case ArchitecturePhase.AfterSystemInit:
Console.WriteLine("系统初始化完成");
break;
case ArchitecturePhase.Ready:
Console.WriteLine("架构就绪");
break;
case ArchitecturePhase.FailedInitialization:
Console.WriteLine("架构初始化失败");
break;
case ArchitecturePhase.Destroying:
Console.WriteLine("架构正在销毁");
break;
case ArchitecturePhase.Destroyed:
Console.WriteLine("架构已销毁");
break;
}
}
}
// 注册监听器
var architecture = new GameArchitecture();
architecture.RegisterLifecycleHook(new PhaseMonitor());
architecture.Initialize();
```
### 等待特定阶段
```csharp
public async Task WaitForReady(IArchitecture architecture)
{
while (architecture.CurrentPhase != ArchitecturePhase.Ready)
{
if (architecture.CurrentPhase == ArchitecturePhase.FailedInitialization)
{
throw new Exception("架构初始化失败");
}
await Task.Delay(100);
}
Console.WriteLine("架构已就绪");
}
```
## 阶段转换顺序
正常初始化流程的阶段转换顺序:
1. `None``BeforeUtilityInit`
2. `BeforeUtilityInit``AfterUtilityInit`
3. `AfterUtilityInit``BeforeModelInit`
4. `BeforeModelInit``AfterModelInit`
5. `AfterModelInit``BeforeSystemInit`
6. `BeforeSystemInit``AfterSystemInit`
7. `AfterSystemInit``Ready`
销毁流程的阶段转换顺序:
1. `Ready``Destroying`
2. `Destroying``Destroyed`
异常流程:
- 任何阶段 → `FailedInitialization`(初始化过程中发生异常)
## 另请参阅
- [Architecture](./architecture.md) - 架构基类
- [IArchitectureLifecycle](./iarchitecture-lifecycle.md) - 生命周期钩子接口
- [架构组件](/zh-CN/core/architecture) - 架构使用指南

View File

@ -0,0 +1,290 @@
---
title: IArchitecture
description: 架构接口,定义了框架的核心功能契约。
outline: deep
---
# IArchitecture
## 概述
架构接口,定义了框架的核心功能契约。
**命名空间**`GFramework.Core.Abstractions.architecture`
**程序集**`GFramework.Core.Abstractions`
**实现类**[Architecture](./architecture.md)
## 公共方法
### RegisterSystem&lt;TSystem&gt;
注册系统组件到架构中。
**签名**
```csharp
void RegisterSystem<TSystem>(TSystem system) where TSystem : ISystem
```
**类型参数**
- `TSystem`: 系统类型,必须实现 ISystem 接口
**参数**
- `system` (TSystem): 要注册的系统实例
### RegisterModel&lt;TModel&gt;
注册模型组件到架构中。
**签名**
```csharp
void RegisterModel<TModel>(TModel model) where TModel : IModel
```
**类型参数**
- `TModel`: 模型类型,必须实现 IModel 接口
**参数**
- `model` (TModel): 要注册的模型实例
### RegisterUtility&lt;TUtility&gt;
注册工具组件到架构中。
**签名**
```csharp
void RegisterUtility<TUtility>(TUtility utility) where TUtility : IUtility
```
**类型参数**
- `TUtility`: 工具类型,必须实现 IUtility 接口
**参数**
- `utility` (TUtility): 要注册的工具实例
### GetModel&lt;T&gt;
从容器中获取已注册的模型。
**签名**
```csharp
T GetModel<T>() where T : class, IModel
```
**类型参数**
- `T`: 模型类型
**返回值**
- (T): 模型实例
### GetSystem&lt;T&gt;
从容器中获取已注册的系统。
**签名**
```csharp
T GetSystem<T>() where T : class, ISystem
```
**类型参数**
- `T`: 系统类型
**返回值**
- (T): 系统实例
### GetUtility&lt;T&gt;
从容器中获取已注册的工具。
**签名**
```csharp
T GetUtility<T>() where T : class, IUtility
```
**类型参数**
- `T`: 工具类型
**返回值**
- (T): 工具实例
### SendCommand
发送并执行命令。
**签名**
```csharp
void SendCommand(ICommand command)
```
**参数**
- `command` (ICommand): 要执行的命令实例
### SendCommand&lt;TResult&gt;
发送并执行带返回值的命令。
**签名**
```csharp
TResult SendCommand<TResult>(ICommand<TResult> command)
```
**类型参数**
- `TResult`: 命令返回值类型
**参数**
- `command` (ICommand&lt;TResult&gt;): 要执行的命令实例
**返回值**
- (TResult): 命令执行结果
### SendQuery&lt;TResult&gt;
发送并执行查询。
**签名**
```csharp
TResult SendQuery<TResult>(IQuery<TResult> query)
```
**类型参数**
- `TResult`: 查询返回值类型
**参数**
- `query` (IQuery&lt;TResult&gt;): 要执行的查询实例
**返回值**
- (TResult): 查询结果
### SendEvent&lt;T&gt;
发送事件(无参数)。
**签名**
```csharp
void SendEvent<T>() where T : new()
```
**类型参数**
- `T`: 事件类型,必须有无参构造函数
### SendEvent&lt;T&gt;
发送事件(带参数)。
**签名**
```csharp
void SendEvent<T>(T e)
```
**类型参数**
- `T`: 事件类型
**参数**
- `e` (T): 事件实例
### RegisterEvent&lt;T&gt;
注册事件监听器。
**签名**
```csharp
IUnRegister RegisterEvent<T>(Action<T> onEvent)
```
**类型参数**
- `T`: 事件类型
**参数**
- `onEvent` (Action&lt;T&gt;): 事件处理回调
**返回值**
- (IUnRegister): 用于注销事件的对象
### UnRegisterEvent&lt;T&gt;
注销事件监听器。
**签名**
```csharp
void UnRegisterEvent<T>(Action<T> onEvent)
```
**类型参数**
- `T`: 事件类型
**参数**
- `onEvent` (Action&lt;T&gt;): 要注销的事件处理回调
## 公共属性
### CurrentPhase
获取当前架构的阶段。
**类型**`ArchitecturePhase`
**访问**get
### Context
获取架构上下文。
**类型**`IArchitectureContext`
**访问**get
## 使用示例
### 在 Controller 中使用
```csharp
public class GameController : IController
{
public IArchitecture GetArchitecture() => GameArchitecture.Interface;
public void Start()
{
// 获取 Model
var playerModel = this.GetModel<PlayerModel>();
// 发送命令
this.SendCommand(new StartGameCommand());
// 发送查询
var score = this.SendQuery(new GetScoreQuery());
// 注册事件
this.RegisterEvent<PlayerDiedEvent>(OnPlayerDied);
}
private void OnPlayerDied(PlayerDiedEvent e)
{
// 处理玩家死亡事件
}
}
```
### 实现自定义架构
```csharp
public class GameArchitecture : Architecture
{
// 单例访问
public static IArchitecture Interface { get; private set; }
protected override void Init()
{
Interface = this;
// 注册组件
RegisterModel(new PlayerModel());
RegisterSystem(new GameplaySystem());
RegisterUtility(new StorageUtility());
}
}
```
## 另请参阅
- [Architecture](./architecture.md) - 架构基类实现
- [IModel](./imodel.md) - 模型接口
- [ISystem](./isystem.md) - 系统接口
- [IUtility](./iutility.md) - 工具接口
- [架构组件](/zh-CN/core/architecture) - 架构使用指南

View File

@ -0,0 +1,37 @@
---
title: {{CLASS_NAME}}
description: {{XML_SUMMARY}}
outline: deep
---
# {{CLASS_NAME}}
## 概述
{{XML_SUMMARY}}
**命名空间**`{{NAMESPACE}}`
**程序集**`{{ASSEMBLY}}`
{{INHERITANCE_CHAIN}}
## 构造函数
{{CONSTRUCTORS}}
## 公共方法
{{PUBLIC_METHODS}}
## 公共属性
{{PUBLIC_PROPERTIES}}
{{PUBLIC_EVENTS}}
## 使用示例
{{AUTO_GENERATED_EXAMPLES}}
## 另请参阅
{{RELATED_TYPES}}

View File

@ -0,0 +1,364 @@
# VitePress 批量 API 文档生成
为整个模块批量生成 API 参考文档,提高文档生成效率。
## 用途
此 skill 用于批量生成模块的 API 文档,适用于:
- 初始化模块文档
- 更新整个模块的文档
- 为新模块快速生成文档
- 重新生成所有 API 文档
## 调用方式
```bash
/vitepress-batch-api <模块名>
```
**示例**
```bash
/vitepress-batch-api Core
/vitepress-batch-api Game
/vitepress-batch-api Godot
/vitepress-batch-api SourceGenerators
```
## 工作流程
1. **扫描模块目录**
- 根据模块名确定源代码目录
- 递归扫描所有 C# 文件
2. **过滤目标文件**
- 仅包含公共类型public class/interface/enum/struct
- 排除内部类型internal
- 排除生成的代码(*.g.cs、*.Designer.cs
- 排除测试文件(*.Tests.cs
3. **批量生成文档**
- 为每个类型调用 `/vitepress-api-doc`
- 显示进度信息
- 收集生成结果
4. **生成模块索引页**
- 创建 `index.md` 列出所有 API
- 按类别分组(类、接口、枚举)
- 添加简短描述
5. **批量更新导航**
- 在 VitePress 配置中添加所有新文档
- 保持字母顺序
- 更新模块索引
6. **生成摘要报告**
- 统计生成的文档数量
- 列出成功和失败的文件
- 提供验证建议
## 输出规范
### 模块索引页格式
```markdown
---
title: Core API 参考
description: GFramework.Core 模块的 API 参考文档
---
# Core API 参考
## 概述
GFramework.Core 是框架的核心模块,提供架构基础、依赖注入、事件系统等核心功能。
## 类
- [Architecture](./architecture.md) - 架构基类
- [ArchitectureConfiguration](./architecture-configuration.md) - 架构配置
- [IocContainer](./ioc-container.md) - IoC 容器
## 接口
- [IArchitecture](./iarchitecture.md) - 架构接口
- [IModel](./imodel.md) - 模型接口
- [ISystem](./isystem.md) - 系统接口
## 枚举
- [ArchitecturePhase](./architecture-phase.md) - 架构阶段
```
### 目录结构
```
docs/zh-CN/api-reference/
├── core/
│ ├── index.md # 模块索引
│ ├── architecture.md
│ ├── iarchitecture.md
│ └── ...
├── game/
│ ├── index.md
│ └── ...
└── godot/
├── index.md
└── ...
```
## 模块映射
### 源代码目录映射
| 模块名 | 源代码目录 | 输出目录 |
|--------|-----------|---------|
| Core | `GFramework.Core/` | `docs/zh-CN/api-reference/core/` |
| Game | `GFramework.Game/` | `docs/zh-CN/api-reference/game/` |
| Godot | `GFramework.Godot/` | `docs/zh-CN/api-reference/godot/` |
| SourceGenerators | `GFramework.SourceGenerators/` | `docs/zh-CN/api-reference/source-generators/` |
### 命名空间映射
- `GFramework.Core.*` → Core 模块
- `GFramework.Game.*` → Game 模块
- `GFramework.Godot.*` → Godot 模块
- `GFramework.SourceGenerators.*` → SourceGenerators 模块
## 过滤规则
### 包含的文件
- 公共类public class
- 公共接口public interface
- 公共枚举public enum
- 公共结构体public struct
### 排除的文件
- 内部类型internal
- 生成的代码(`*.g.cs``*.Designer.cs`
- 测试文件(`*.Tests.cs``*Test.cs`
- 临时文件(`*.tmp.cs`
- 编译器生成的文件(`AssemblyInfo.cs`
### 排除的类型
- 编译器生成的类型(`<>c__DisplayClass`
- 匿名类型
- 嵌套的私有类型
## 批量处理脚本
### batch-generate.sh
```bash
#!/bin/bash
# 批量生成 API 文档
# 用法: batch-generate.sh <模块名>
set -e
MODULE="$1"
if [ -z "$MODULE" ]; then
echo "用法: $0 <模块名>"
echo "可用模块: Core, Game, Godot, SourceGenerators"
exit 1
fi
# 确定源代码目录
case "$MODULE" in
Core)
SOURCE_DIR="GFramework.Core"
;;
Game)
SOURCE_DIR="GFramework.Game"
;;
Godot)
SOURCE_DIR="GFramework.Godot"
;;
SourceGenerators)
SOURCE_DIR="GFramework.SourceGenerators"
;;
*)
echo "错误: 未知的模块: $MODULE"
exit 1
;;
esac
if [ ! -d "$SOURCE_DIR" ]; then
echo "错误: 源代码目录不存在: $SOURCE_DIR"
exit 1
fi
echo "=========================================="
echo "批量生成 $MODULE 模块的 API 文档"
echo "=========================================="
echo ""
# 查找所有 C# 文件
FILES=$(find "$SOURCE_DIR" -name "*.cs" -type f \
! -name "*.g.cs" \
! -name "*.Designer.cs" \
! -name "*Test.cs" \
! -name "*.Tests.cs" \
! -name "AssemblyInfo.cs")
FILE_COUNT=$(echo "$FILES" | wc -l)
echo "找到 $FILE_COUNT 个文件"
echo ""
GENERATED=0
SKIPPED=0
FAILED=0
for FILE in $FILES; do
echo "处理: $FILE"
# 检查是否包含公共类型
if ! grep -q "public \(class\|interface\|enum\|struct\)" "$FILE"; then
echo " ⊘ 跳过(无公共类型)"
SKIPPED=$((SKIPPED + 1))
continue
fi
# 调用 vitepress-api-doc由 AI 执行)
# /vitepress-api-doc "$FILE"
if [ $? -eq 0 ]; then
echo " ✓ 生成成功"
GENERATED=$((GENERATED + 1))
else
echo " ✗ 生成失败"
FAILED=$((FAILED + 1))
fi
echo ""
done
echo "=========================================="
echo "批量生成完成"
echo "=========================================="
echo "总文件数: $FILE_COUNT"
echo "生成成功: $GENERATED"
echo "跳过: $SKIPPED"
echo "失败: $FAILED"
echo ""
if [ $FAILED -eq 0 ]; then
echo "✓ 所有文档生成成功"
exit 0
else
echo "✗ 部分文档生成失败"
exit 1
fi
```
## 配置选项
### 过滤选项
```bash
# 包含内部类型
/vitepress-batch-api Core --include-internal
# 包含生成的代码
/vitepress-batch-api Core --include-generated
# 自定义过滤规则
/vitepress-batch-api Core --exclude "*.Tests.cs" --exclude "*.g.cs"
```
### 输出选项
```bash
# 指定输出目录
/vitepress-batch-api Core --output docs/zh-CN/api-reference/core/
# 覆盖现有文档
/vitepress-batch-api Core --force
# 仅生成索引页
/vitepress-batch-api Core --index-only
```
### 并行处理
```bash
# 并行生成(加快速度)
/vitepress-batch-api Core --parallel 4
```
## 进度显示
### 实时进度
```
========================================
批量生成 Core 模块的 API 文档
========================================
找到 45 个文件
[1/45] 处理: GFramework.Core/architecture/Architecture.cs
✓ 生成成功
[2/45] 处理: GFramework.Core/architecture/IArchitecture.cs
✓ 生成成功
[3/45] 处理: GFramework.Core/command/Command.cs
⊘ 跳过(无公共类型)
...
[45/45] 处理: GFramework.Core/utility/Utility.cs
✓ 生成成功
========================================
批量生成完成
========================================
总文件数: 45
生成成功: 38
跳过: 5
失败: 2
✗ 部分文档生成失败
失败的文件:
- GFramework.Core/internal/InternalClass.cs (缺少 XML 注释)
- GFramework.Core/legacy/LegacyClass.cs (解析错误)
```
## 前置条件
1. 模块源代码目录存在
2. 源代码文件包含 XML 文档注释
3. 有足够的磁盘空间存储生成的文档
## 相关 Skills
- `/vitepress-api-doc` - 单文件 API 文档生成
- `/vitepress-validate` - 验证生成的文档
- `/vitepress-guide` - 生成功能指南
## 最佳实践
1. **首次生成**:使用批量生成快速创建所有文档
2. **增量更新**:修改代码后使用单文件生成更新对应文档
3. **定期验证**:批量生成后运行验证确保质量
4. **版本控制**:将生成的文档提交到版本控制系统
## 故障排除
### 问题:部分文件生成失败
**解决方案**:检查失败文件的 XML 注释是否完整,手动修复后重新生成
### 问题:生成速度慢
**解决方案**:使用 `--parallel` 选项启用并行处理
### 问题:生成的文档过多
**解决方案**:使用过滤选项排除不需要的文件
## 版本历史
- v1.0.0 - 初始版本,支持批量 API 文档生成

View File

@ -0,0 +1,81 @@
#!/bin/bash
# 批量生成 API 文档
# 用法: batch-generate.sh <模块名>
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=../../_shared/module-config.sh
source "$SCRIPT_DIR/../../_shared/module-config.sh"
MODULE="$1"
if [ -z "$MODULE" ]; then
echo "用法: $0 <模块名>"
echo "可用模块: $(get_all_modules)"
exit 1
fi
# 验证模块名
if ! is_valid_module "$MODULE"; then
echo "错误: 未知的模块: $MODULE"
echo "可用模块: $(get_all_modules)"
exit 1
fi
# 获取源代码目录
SOURCE_DIR=$(get_source_dir "$MODULE")
if [ ! -d "$SOURCE_DIR" ]; then
echo "错误: 源代码目录不存在: $SOURCE_DIR"
exit 1
fi
echo "=========================================="
echo "批量生成 $MODULE 模块的 API 文档"
echo "=========================================="
echo ""
# 查找所有 C# 文件
mapfile -t FILES < <(find "$SOURCE_DIR" -name "*.cs" -type f \
! -name "*.g.cs" \
! -name "*.Designer.cs" \
! -name "*Test.cs" \
! -name "*.Tests.cs" \
! -name "AssemblyInfo.cs")
FILE_COUNT=${#FILES[@]}
echo "找到 $FILE_COUNT 个文件"
echo ""
GENERATED=0
SKIPPED=0
FAILED=0
for FILE in "${FILES[@]}"; do
echo "处理: $FILE"
# 检查是否包含公共类型
if ! grep -q "public \(class\|interface\|enum\|struct\)" "$FILE"; then
echo " ⊘ 跳过(无公共类型)"
SKIPPED=$((SKIPPED + 1))
continue
fi
# 注意: 实际的文档生成由 AI 调用 /vitepress-api-doc 完成
# 此脚本仅用于扫描和过滤文件
echo " → 待生成"
GENERATED=$((GENERATED + 1))
echo ""
done
echo "=========================================="
echo "扫描完成"
echo "=========================================="
echo "总文件数: $FILE_COUNT"
echo "待生成: $GENERATED"
echo "跳过: $SKIPPED"
echo ""
exit 0

View File

@ -0,0 +1,52 @@
---
name: vitepress-doc-generator
description: Generate standardized VitePress documentation from source code.
disable-model-invocation: true
---
# Role
You are a technical documentation generator specialized in VitePress.
# Objective
Analyze the provided code context and generate a structured VitePress-compatible Markdown document.
# Output Requirements
1. Output MUST be valid Markdown only.
2. Include frontmatter:
---
title: <Module Name>
outline: deep
---
3. No explanations.
4. No conversational text.
5. No emoji.
6. Use Chinese.
7. Use structured headings.
# Required Structure
# 模块概述
- 模块职责
- 设计目标
# 核心类说明
## 类名
### 职责
### 主要方法
### 依赖关系
# 设计模式分析
# 可扩展性说明
# 使用示例(如适用)
# Self-Validation
Before returning output, verify:
- Frontmatter exists
- All required sections exist
- No extra commentary text

View File

@ -0,0 +1,256 @@
# VitePress 功能指南生成
生成功能模块的使用指南文档,包括概念说明、用法示例和最佳实践。
## 用途
此 skill 用于生成结构化的功能指南文档,适用于:
- 核心功能模块的使用说明
- 设计模式和架构概念
- 系统功能的详细介绍
- 最佳实践和常见问题
## 调用方式
```bash
/vitepress-guide <主题> <目标模块>
```
**示例**
```bash
/vitepress-guide "事件系统" Core
/vitepress-guide "IoC 容器" Core
/vitepress-guide "Godot 节点扩展" Godot
```
## 工作流程
1. **收集需求**
- 询问用户指南主题
- 确定目标受众(初学者/进阶/专家)
- 了解重点内容(概念/用法/最佳实践)
2. **搜索相关资源**
- 搜索相关代码文件
- 查找现有文档
- 识别相关类型和接口
3. **生成指南结构**
- 根据 `template.md` 创建文档框架
- 填充概述和核心概念
- 添加基本用法和高级用法
- 补充最佳实践和常见问题
4. **生成代码示例**
- 基本用法示例(最简单的场景)
- 常见场景示例(实际应用)
- 高级用法示例(复杂配置)
5. **确定输出路径**
- 保存到 `docs/zh-CN/<模块>/`
- 文件名使用小写加连字符(如 `event-system.md`
6. **更新导航配置**
- 在 VitePress 侧边栏中添加新指南
## 输出规范
### Frontmatter 格式
```yaml
---
title: 指南标题
description: 简短描述1-2 句话)
---
```
### 文档结构
1. **概述**:功能的简介和用途
2. **核心概念**:关键概念和术语解释
3. **基本用法**:最简单的使用方式
4. **高级用法**:复杂场景和配置
5. **最佳实践**:推荐的使用方式
6. **常见问题**FAQ 和故障排除
### 章节示例
**概述**
```markdown
## 概述
事件系统提供了一种松耦合的组件间通信机制。通过事件,不同的组件可以在不直接引用彼此的情况下进行交互。
**主要特性**
- 类型安全的事件
- 自动内存管理
- 支持事件优先级
- 线程安全
```
**核心概念**
```markdown
## 核心概念
### 事件类型
事件是一个普通的 C# 类,用于携带事件数据:
\`\`\`csharp
public class PlayerDiedEvent
{
public int PlayerId { get; set; }
public string Reason { get; set; }
}
\`\`\`
### 事件发送
通过架构发送事件:
\`\`\`csharp
this.SendEvent(new PlayerDiedEvent
{
PlayerId = 1,
Reason = "Fall damage"
});
\`\`\`
### 事件监听
注册事件监听器:
\`\`\`csharp
this.RegisterEvent<PlayerDiedEvent>(OnPlayerDied);
\`\`\`
```
## 模板变量
- `{{GUIDE_TITLE}}` - 指南标题
- `{{GUIDE_DESCRIPTION}}` - 简短描述
- `{{OVERVIEW}}` - 概述内容
- `{{CORE_CONCEPTS}}` - 核心概念
- `{{BASIC_USAGE}}` - 基本用法
- `{{ADVANCED_USAGE}}` - 高级用法
- `{{BEST_PRACTICES}}` - 最佳实践
- `{{FAQ}}` - 常见问题
## 示例输出
参考 `examples/guide-example.md`,该示例基于现有的 IoC 容器文档创建。
## 内容要求
### 概述部分
- 1-2 段简介
- 列出主要特性3-5 个)
- 说明适用场景
### 核心概念部分
- 使用三级标题(###)分隔不同概念
- 每个概念包含简短说明和代码示例
- 解释关键术语
### 基本用法部分
- 提供完整的可运行示例
- 从最简单的场景开始
- 逐步增加复杂度
- 包含必要的 using 语句
### 高级用法部分
- 展示复杂场景
- 说明配置选项
- 提供性能优化建议
### 最佳实践部分
- 使用编号列表
- 每条实践包含简短说明
- 提供正反示例(✓ 推荐 / ✗ 不推荐)
### 常见问题部分
- 使用问答格式
- 提供具体的解决方案
- 包含相关链接
## 写作风格
### 语气
- 友好、专业
- 使用第二人称("你"
- 避免过于技术化的术语
### 代码示例
- 完整且可运行
- 包含注释说明关键步骤
- 使用有意义的变量名
- 遵循项目代码风格
### 格式
- 使用 Markdown 标准格式
- 代码块使用语法高亮
- 重要内容使用粗体或引用块
- 适当使用列表和表格
## 配置选项
### 目标受众
```bash
# 初学者(更多解释,简单示例)
/vitepress-guide "事件系统" Core --audience beginner
# 进阶(平衡解释和示例)
/vitepress-guide "事件系统" Core --audience intermediate
# 专家(简洁说明,复杂示例)
/vitepress-guide "事件系统" Core --audience expert
```
### 重点内容
```bash
# 侧重概念
/vitepress-guide "事件系统" Core --focus concepts
# 侧重用法
/vitepress-guide "事件系统" Core --focus usage
# 侧重最佳实践
/vitepress-guide "事件系统" Core --focus best-practices
```
## 前置条件
1. 了解指南主题的基本概念
2. 能够访问相关代码文件
3. 了解项目的代码风格和术语
## 相关 Skills
- `/vitepress-api-doc` - 生成 API 参考文档
- `/vitepress-tutorial` - 生成分步教程
- `/vitepress-validate` - 验证生成的文档
## 最佳实践
1. **先搜索后编写**:查看现有文档和代码,保持一致性
2. **使用真实示例**:基于项目实际代码创建示例
3. **保持简洁**:每个章节聚焦一个主题
4. **提供完整代码**:确保示例可以直接运行
5. **添加交叉引用**:链接到相关的 API 文档和其他指南
## 故障排除
### 问题:不确定指南应该包含哪些内容
**解决方案**:参考 `examples/guide-example.md` 和现有的指南文档
### 问题:代码示例过于复杂
**解决方案**:将复杂示例拆分为多个简单示例,逐步增加复杂度
### 问题:概念解释不清晰
**解决方案**:使用类比、图表或分步说明来辅助解释
## 版本历史
- v1.0.0 - 初始版本,支持功能指南生成

View File

@ -0,0 +1,283 @@
---
title: IoC 容器使用指南
description: IoC控制反转容器提供了轻量级的依赖注入功能用于管理框架中各种组件的注册和获取。
---
# IoC 容器使用指南
## 概述
IoCInversion of Control控制反转包提供了一个轻量级的依赖注入容器用于管理框架中各种组件的注册和获取。通过 IoC 容器,可以实现组件间的解耦,便于测试和维护。
IoC 容器是 GFramework 架构的核心组件之一,为整个框架提供依赖管理和组件解析服务。
**主要特性**
- 类型安全的依赖管理
- 支持单例和多实例注册
- 线程安全操作
- 容器冻结保护
- 自动接口注册
## 核心概念
### 依赖注入
依赖注入是一种设计模式,通过容器管理对象的创建和依赖关系,而不是在代码中直接创建对象。
```csharp
// 不使用依赖注入
public class GameController
{
private PlayerModel model = new PlayerModel(); // 硬编码依赖
}
// 使用依赖注入
public class GameController : IController
{
public IArchitecture GetArchitecture() => GameArchitecture.Interface;
public void Start()
{
var model = this.GetModel<PlayerModel>(); // 从容器获取
}
}
```
### 容器注册
在架构初始化时,将组件注册到容器中:
```csharp
public class GameArchitecture : Architecture
{
protected override void Init()
{
// 注册 Model
RegisterModel(new PlayerModel());
// 注册 System
RegisterSystem(new GameplaySystem());
// 注册 Utility
RegisterUtility(new StorageUtility());
}
}
```
### 容器解析
通过扩展方法从容器中获取已注册的组件:
```csharp
// 在 Controller 中
var playerModel = this.GetModel<PlayerModel>();
var gameplaySystem = this.GetSystem<GameplaySystem>();
var storageUtility = this.GetUtility<StorageUtility>();
```
## 基本用法
### 注册组件
```csharp
var container = new IocContainer();
// 注册单例(一个类型只能有一个实例)
container.RegisterSingleton<IPlayerModel>(new PlayerModel());
// 注册多实例(一个类型可以有多个实例)
container.RegisterPlurality<IEnemy>(new Goblin());
container.RegisterPlurality<IEnemy>(new Orc());
container.RegisterPlurality<IEnemy>(new Dragon());
```
### 获取组件
```csharp
// 获取单例
var playerModel = container.Get<IPlayerModel>();
// 获取多实例集合
var enemies = container.GetAll<IEnemy>(); // 返回 List<IEnemy>
```
### 在架构中使用
```csharp
public class GameArchitecture : Architecture
{
protected override void Init()
{
// 注册组件
RegisterModel(new PlayerModel());
RegisterModel(new InventoryModel());
RegisterSystem(new GameplaySystem());
}
}
// 在 Controller 中使用
public class GameController : IController
{
public IArchitecture GetArchitecture() => GameArchitecture.Interface;
public void Start()
{
// 通过扩展方法获取组件
var playerModel = this.GetModel<PlayerModel>();
var inventoryModel = this.GetModel<InventoryModel>();
var gameplaySystem = this.GetSystem<GameplaySystem>();
}
}
```
## 高级用法
### 容器冻结
容器在架构初始化完成后会被冻结,防止运行时修改:
```csharp
var container = new IocContainer();
container.Register<IPlayerModel>(new PlayerModel());
// 冻结容器
container.Freeze();
// 以下操作会抛出 InvalidOperationException
// container.Register<IGameSystem>(new GameSystem());
```
### 多实例管理
```csharp
// 注册多个同类型实例
container.RegisterPlurality<IWeapon>(new Sword());
container.RegisterPlurality<IWeapon>(new Bow());
container.RegisterPlurality<IWeapon>(new Staff());
// 获取所有实例
var allWeapons = container.GetAll<IWeapon>();
foreach (var weapon in allWeapons)
{
weapon.Attack();
}
```
### 接口自动注册
注册实例时,容器会自动将其注册到所有实现的接口:
```csharp
public class PlayerModel : IModel, IPlayerModel, IDisposable
{
// ...
}
// 注册实例
container.Register<PlayerModel>(new PlayerModel());
// 可以通过任何接口获取
var model1 = container.Get<IModel>();
var model2 = container.Get<IPlayerModel>();
var model3 = container.Get<IDisposable>();
// 以上三个变量指向同一个实例
```
### 线程安全操作
容器的所有操作都是线程安全的:
```csharp
// 多线程环境下安全使用
Parallel.For(0, 100, i =>
{
var model = container.Get<IPlayerModel>();
model.DoSomething();
});
```
## 最佳实践
1. **使用接口注册**:优先使用接口类型注册,而不是具体类型
```csharp
✓ container.Register<IPlayerModel>(new PlayerModel());
✗ container.Register<PlayerModel>(new PlayerModel());
```
2. **单例 vs 多实例**:根据需求选择合适的注册方式
- 单例:全局唯一的服务(如配置、管理器)
- 多实例:可以有多个实例的对象(如敌人、道具)
3. **避免循环依赖**:组件之间不应该相互依赖
```csharp
✗ System A 依赖 System BSystem B 又依赖 System A
✓ 使用事件系统进行通信,避免直接依赖
```
4. **在 Init 中注册**:所有组件应该在架构的 `Init()` 方法中注册
```csharp
protected override void Init()
{
// 在这里注册所有组件
RegisterModel(new PlayerModel());
RegisterSystem(new GameplaySystem());
}
```
5. **使用扩展方法**:通过扩展方法获取组件,代码更简洁
```csharp
✓ var model = this.GetModel<PlayerModel>();
✗ var model = this.GetArchitecture().GetModel<PlayerModel>();
```
6. **不要在运行时注册**:容器冻结后不应该再注册新组件
```csharp
✗ 在游戏运行时动态注册组件
✓ 在架构初始化时注册所有需要的组件
```
## 常见问题
### 问题:如何判断使用单例还是多实例?
**解答**
- 使用单例(`RegisterSingleton`):全局唯一的服务,如 PlayerModel、GameConfiguration
- 使用多实例(`RegisterPlurality`):可以有多个实例的对象,如 Enemy、Weapon
### 问题:容器冻结后如何添加新组件?
**解答**
容器冻结是为了保护架构稳定性。如果需要动态添加组件,应该:
1. 在架构初始化时预先注册所有可能需要的组件
2. 使用对象池模式管理动态对象
3. 考虑使用工厂模式创建临时对象
### 问题:如何处理组件的生命周期?
**解答**
- 实现 `IDisposable` 接口的组件会在架构销毁时自动释放
- 架构会按注册的逆序销毁组件
- 不需要手动管理组件的生命周期
### 问题:可以在容器中注册值类型吗?
**解答**
可以,但会发生装箱。建议将值类型包装在类中:
```csharp
// 不推荐
container.Register<int>(42);
// 推荐
public class GameConfig
{
public int MaxPlayers { get; set; } = 42;
}
container.Register<GameConfig>(new GameConfig());
```
## 相关文档
- [架构组件](/zh-CN/core/architecture) - 架构基础
- [Model 层](/zh-CN/core/model) - 数据模型
- [System 层](/zh-CN/core/system) - 业务系统
- [Utility 工具类](/zh-CN/core/utility) - 工具类

View File

@ -0,0 +1,34 @@
---
title: {{GUIDE_TITLE}}
description: {{GUIDE_DESCRIPTION}}
---
# {{GUIDE_TITLE}}
## 概述
{{OVERVIEW}}
## 核心概念
{{CORE_CONCEPTS}}
## 基本用法
{{BASIC_USAGE}}
## 高级用法
{{ADVANCED_USAGE}}
## 最佳实践
{{BEST_PRACTICES}}
## 常见问题
{{FAQ}}
## 相关文档
{{RELATED_DOCS}}

View File

@ -0,0 +1,253 @@
# VitePress 教程生成
生成分步教程文档,适合初学者学习框架功能。
## 用途
此 skill 用于生成结构化的分步教程,适用于:
- 框架入门教程
- 功能实现教程
- 最佳实践演示
- 问题解决方案
## 调用方式
```bash
/vitepress-tutorial <教程主题>
```
**示例**
```bash
/vitepress-tutorial "创建第一个 System"
/vitepress-tutorial "实现自定义命令"
/vitepress-tutorial "使用事件系统"
```
## 工作流程
1. **收集需求**
- 询问用户教程主题
- 确定学习目标
- 了解前置知识要求
2. **设计教程步骤**
- 将任务分解为 3-7 个步骤
- 每步聚焦一个具体任务
- 确保步骤之间逻辑连贯
3. **生成教程内容**
- 根据 `template.md` 创建文档框架
- 为每步编写详细说明和代码
- 添加完整的可运行代码
- 说明预期结果
4. **确定输出路径**
- 保存到 `docs/zh-CN/tutorials/`
- 文件名使用小写加连字符
5. **更新导航配置**
- 在 VitePress 侧边栏中添加新教程
## 输出规范
### Frontmatter 格式
```yaml
---
title: 教程标题
description: 简短描述1 句话说明学习内容)
---
```
### 文档结构
1. **学习目标**:完成教程后能够掌握的技能
2. **前置条件**:需要的前置知识和环境
3. **步骤 1-N**分步说明3-7 步)
4. **完整代码**:汇总所有代码
5. **运行结果**:预期输出和效果
6. **下一步**:后续学习建议
### 步骤格式
每个步骤应包含:
- 步骤标题(简短、动词开头)
- 步骤说明(为什么要这样做)
- 代码示例(完整且可运行)
- 代码解释(关键部分的说明)
**示例**
```markdown
## 步骤 1创建 Model 类
首先,我们需要创建一个 Model 来存储玩家数据。Model 负责管理应用的数据和状态。
\`\`\`csharp
using GFramework.Core.Abstractions.model;
using GFramework.Core.Abstractions.property;
public class PlayerModel : IModel
{
// 玩家名称(可绑定属性)
public BindableProperty<string> Name { get; } = new("Player");
// 玩家生命值
public BindableProperty<int> Health { get; } = new(100);
// 玩家金币
public BindableProperty<int> Gold { get; } = new(0);
public void Init() { }
}
\`\`\`
**代码说明**
- `BindableProperty<T>` 是可绑定属性,值变化时会自动通知监听者
- `Init()` 方法在 Model 注册到架构时被调用
- 使用属性初始化器设置默认值
```
## 模板变量
- `{{TUTORIAL_TITLE}}` - 教程标题
- `{{TUTORIAL_DESCRIPTION}}` - 简短描述
- `{{LEARNING_OBJECTIVES}}` - 学习目标
- `{{PREREQUISITES}}` - 前置条件
- `{{STEP_N_TITLE}}` - 步骤标题
- `{{STEP_N_CONTENT}}` - 步骤内容
- `{{FULL_CODE}}` - 完整代码
- `{{EXPECTED_OUTPUT}}` - 预期输出
- `{{NEXT_STEPS}}` - 下一步建议
## 示例输出
参考 `examples/tutorial-example.md`,该示例基于现有的教程文档创建。
## 内容要求
### 学习目标
- 使用列表格式
- 3-5 个具体的学习目标
- 使用"能够..."句式
**示例**
```markdown
## 学习目标
完成本教程后,你将能够:
- 创建自定义的 Model 类
- 在架构中注册 Model
- 从 Controller 中访问 Model
- 使用可绑定属性管理数据
```
### 前置条件
- 列出必需的知识
- 说明环境要求
- 提供相关文档链接
**示例**
```markdown
## 前置条件
- 已安装 GFramework.Core NuGet 包
- 了解 C# 基础语法
- 阅读过[架构概览](/zh-CN/getting-started)
```
### 步骤内容
- 每步 100-300 字说明
- 包含完整的代码示例
- 解释关键代码的作用
- 使用注释标注重要部分
### 完整代码
- 汇总所有步骤的代码
- 确保可以直接复制运行
- 包含必要的 using 语句
- 添加文件结构说明
### 运行结果
- 描述预期的输出
- 如果有界面,提供截图或描述
- 说明如何验证结果正确
### 下一步
- 推荐 2-3 个后续教程
- 提供相关文档链接
- 建议进阶学习方向
## 写作风格
### 语气
- 友好、鼓励性
- 使用第二人称("你"
- 避免假设读者已有高级知识
### 步骤说明
- 使用主动语态
- 步骤标题使用动词开头
- 说明"为什么"而不仅是"怎么做"
### 代码示例
- 完整且可运行
- 包含详细注释
- 使用有意义的变量名
- 遵循项目代码风格
## 配置选项
### 教程难度
```bash
# 初学者(更多解释,简单示例)
/vitepress-tutorial "创建第一个 System" --level beginner
# 中级(平衡解释和复杂度)
/vitepress-tutorial "实现自定义命令" --level intermediate
# 高级(简洁说明,复杂示例)
/vitepress-tutorial "架构模块开发" --level advanced
```
### 步骤数量
```bash
# 指定步骤数量3-7 步)
/vitepress-tutorial "使用事件系统" --steps 5
```
## 前置条件
1. 了解教程主题的基本概念
2. 能够访问相关代码文件
3. 了解目标受众的知识水平
## 相关 Skills
- `/vitepress-api-doc` - 生成 API 参考文档
- `/vitepress-guide` - 生成功能指南
- `/vitepress-validate` - 验证生成的文档
## 最佳实践
1. **从简单开始**:第一步应该是最简单的操作
2. **逐步增加复杂度**:每步在前一步基础上增加新内容
3. **提供完整代码**:确保每步的代码都可以运行
4. **解释关键概念**:不要假设读者已经了解所有术语
5. **测试教程**:确保按照步骤操作能够得到预期结果
## 故障排除
### 问题:步骤过多,教程太长
**解决方案**:将教程拆分为多个小教程,或合并相似的步骤
### 问题:代码示例不完整
**解决方案**:在"完整代码"章节提供所有文件的完整代码
### 问题:读者反馈步骤不清晰
**解决方案**:增加更多说明,使用截图或图表辅助
## 版本历史
- v1.0.0 - 初始版本,支持分步教程生成

View File

@ -0,0 +1,347 @@
---
title: 创建第一个 Model
description: 学习如何创建和使用 Model 来管理应用数据
---
# 创建第一个 Model
## 学习目标
完成本教程后,你将能够:
- 理解 Model 在架构中的作用
- 创建自定义的 Model 类
- 在架构中注册 Model
- 从 Controller 中访问 Model
- 使用可绑定属性管理数据
## 前置条件
- 已安装 GFramework.Core NuGet 包
- 了解 C# 基础语法
- 阅读过[架构概览](/zh-CN/getting-started)
## 步骤 1创建 Model 类
首先,我们需要创建一个 Model 来存储玩家数据。Model 负责管理应用的数据和状态。
```csharp
using GFramework.Core.Abstractions.model;
using GFramework.Core.Abstractions.property;
namespace MyGame.Models
{
/// <summary>
/// 玩家数据模型
/// </summary>
public class PlayerModel : IModel
{
// 玩家名称(可绑定属性)
public BindableProperty<string> Name { get; } = new("Player");
// 玩家生命值
public BindableProperty<int> Health { get; } = new(100);
// 玩家金币
public BindableProperty<int> Gold { get; } = new(0);
// 玩家等级
public BindableProperty<int> Level { get; } = new(1);
/// <summary>
/// Model 初始化方法
/// </summary>
public void Init()
{
// 在这里可以进行初始化操作
// 例如:从配置文件加载默认值
}
}
}
```
**代码说明**
- `IModel` 接口标识这是一个数据模型
- `BindableProperty<T>` 是可绑定属性,值变化时会自动通知监听者
- `Init()` 方法在 Model 注册到架构时被调用
- 使用属性初始化器设置默认值
## 步骤 2在架构中注册 Model
创建架构类并注册 Model
```csharp
using GFramework.Core.architecture;
using MyGame.Models;
namespace MyGame
{
/// <summary>
/// 游戏架构
/// </summary>
public class GameArchitecture : Architecture
{
// 单例访问点
public static IArchitecture Interface { get; private set; }
/// <summary>
/// 初始化架构
/// </summary>
protected override void Init()
{
Interface = this;
// 注册 Model
RegisterModel(new PlayerModel());
}
}
}
```
**代码说明**
- 继承 `Architecture` 基类
- 在 `Init()` 方法中注册 Model
- 提供静态属性 `Interface` 用于全局访问架构
## 步骤 3创建 Controller 访问 Model
创建 Controller 来使用 Model
```csharp
using GFramework.Core.Abstractions.architecture;
using GFramework.Core.Abstractions.controller;
using GFramework.Core.extensions;
using MyGame.Models;
namespace MyGame.Controllers
{
/// <summary>
/// 游戏控制器
/// </summary>
public class GameController : IController
{
/// <summary>
/// 获取架构实例
/// </summary>
public IArchitecture GetArchitecture() => GameArchitecture.Interface;
/// <summary>
/// 初始化玩家数据
/// </summary>
public void InitializePlayer()
{
// 获取 PlayerModel
var playerModel = this.GetModel<PlayerModel>();
// 设置玩家数据
playerModel.Name.Value = "勇者";
playerModel.Health.Value = 100;
playerModel.Gold.Value = 50;
playerModel.Level.Value = 1;
// 监听属性变化
playerModel.Health.RegisterOnValueChanged(health =>
{
Console.WriteLine($"玩家生命值变化: {health}");
if (health <= 0)
{
Console.WriteLine("玩家死亡!");
}
});
}
/// <summary>
/// 玩家受到伤害
/// </summary>
public void TakeDamage(int damage)
{
var playerModel = this.GetModel<PlayerModel>();
playerModel.Health.Value -= damage;
}
/// <summary>
/// 玩家获得金币
/// </summary>
public void AddGold(int amount)
{
var playerModel = this.GetModel<PlayerModel>();
playerModel.Gold.Value += amount;
}
}
}
```
**代码说明**
- 实现 `IController` 接口
- 通过 `this.GetModel<T>()` 扩展方法获取 Model
- 使用 `.Value` 访问和修改属性值
- 使用 `RegisterOnValueChanged` 监听属性变化
## 步骤 4初始化并使用架构
在程序入口点初始化架构:
```csharp
using MyGame;
using MyGame.Controllers;
// 1. 创建并初始化架构
var architecture = new GameArchitecture();
architecture.Initialize();
// 2. 等待架构就绪
await architecture.WaitUntilReadyAsync();
// 3. 创建 Controller 并使用
var gameController = new GameController();
// 初始化玩家
gameController.InitializePlayer();
// 玩家受到伤害
gameController.TakeDamage(20);
// 输出: 玩家生命值变化: 80
// 玩家获得金币
gameController.AddGold(100);
```
**代码说明**
- 创建架构实例并调用 `Initialize()`
- 使用 `WaitUntilReadyAsync()` 等待架构就绪
- 创建 Controller 实例并调用方法
## 完整代码
### PlayerModel.cs
```csharp
using GFramework.Core.Abstractions.model;
using GFramework.Core.Abstractions.property;
namespace MyGame.Models
{
public class PlayerModel : IModel
{
public BindableProperty<string> Name { get; } = new("Player");
public BindableProperty<int> Health { get; } = new(100);
public BindableProperty<int> Gold { get; } = new(0);
public BindableProperty<int> Level { get; } = new(1);
public void Init() { }
}
}
```
### GameArchitecture.cs
```csharp
using GFramework.Core.architecture;
using MyGame.Models;
namespace MyGame
{
public class GameArchitecture : Architecture
{
public static IArchitecture Interface { get; private set; }
protected override void Init()
{
Interface = this;
RegisterModel(new PlayerModel());
}
}
}
```
### GameController.cs
```csharp
using GFramework.Core.Abstractions.architecture;
using GFramework.Core.Abstractions.controller;
using GFramework.Core.extensions;
using MyGame.Models;
namespace MyGame.Controllers
{
public class GameController : IController
{
public IArchitecture GetArchitecture() => GameArchitecture.Interface;
public void InitializePlayer()
{
var playerModel = this.GetModel<PlayerModel>();
playerModel.Name.Value = "勇者";
playerModel.Health.Value = 100;
playerModel.Gold.Value = 50;
playerModel.Level.Value = 1;
playerModel.Health.RegisterOnValueChanged(health =>
{
Console.WriteLine($"玩家生命值变化: {health}");
if (health <= 0)
{
Console.WriteLine("玩家死亡!");
}
});
}
public void TakeDamage(int damage)
{
var playerModel = this.GetModel<PlayerModel>();
playerModel.Health.Value -= damage;
}
public void AddGold(int amount)
{
var playerModel = this.GetModel<PlayerModel>();
playerModel.Gold.Value += amount;
}
}
}
```
### Program.cs
```csharp
using MyGame;
using MyGame.Controllers;
var architecture = new GameArchitecture();
architecture.Initialize();
await architecture.WaitUntilReadyAsync();
var gameController = new GameController();
gameController.InitializePlayer();
gameController.TakeDamage(20);
gameController.AddGold(100);
```
## 运行结果
运行程序后,你将看到以下输出:
```
玩家生命值变化: 100
玩家生命值变化: 80
```
**验证步骤**
1. 程序成功启动,没有异常
2. 控制台输出生命值变化信息
3. 玩家数据正确更新
## 下一步
恭喜!你已经学会了如何创建和使用 Model。接下来可以学习
- [创建第一个 System](/zh-CN/tutorials/create-first-system) - 学习如何创建业务逻辑层
- [使用命令系统](/zh-CN/tutorials/use-command-system) - 学习如何封装操作
- [使用事件系统](/zh-CN/tutorials/use-event-system) - 学习组件间通信
## 相关文档
- [Model 层](/zh-CN/core/model) - Model 详细说明
- [属性系统](/zh-CN/core/property) - 可绑定属性详解
- [架构组件](/zh-CN/core/architecture) - 架构基础
- [Controller 层](/zh-CN/core/controller) - Controller 详细说明

View File

@ -0,0 +1,42 @@
---
title: {{TUTORIAL_TITLE}}
description: {{TUTORIAL_DESCRIPTION}}
---
# {{TUTORIAL_TITLE}}
## 学习目标
{{LEARNING_OBJECTIVES}}
## 前置条件
{{PREREQUISITES}}
## 步骤 1{{STEP_1_TITLE}}
{{STEP_1_CONTENT}}
## 步骤 2{{STEP_2_TITLE}}
{{STEP_2_CONTENT}}
## 步骤 3{{STEP_3_TITLE}}
{{STEP_3_CONTENT}}
## 完整代码
{{FULL_CODE}}
## 运行结果
{{EXPECTED_OUTPUT}}
## 下一步
{{NEXT_STEPS}}
## 相关文档
{{RELATED_DOCS}}

View File

@ -0,0 +1,297 @@
# VitePress 文档验证
验证 VitePress 文档的质量和规范性,确保文档符合项目标准。
## 用途
此 skill 用于验证 Markdown 文档的格式和内容,包括:
- Frontmatter 格式正确性
- 内部链接有效性
- 代码块语法标记
- 标题层级结构
- 中文标点符号规范
- 泛型符号转义
## 调用方式
```bash
# 验证单个文件
/vitepress-validate <文件路径>
# 验证整个目录
/vitepress-validate <目录路径>
# 验证所有文档
/vitepress-validate docs/zh-CN/
```
**示例**
```bash
/vitepress-validate docs/zh-CN/api-reference/core/architecture.md
/vitepress-validate docs/zh-CN/core/
```
## 验证项
### 1. Frontmatter 验证
**检查项**
- YAML 语法正确性
- 必需字段存在(`title``description`
- 字段值类型正确
- `outline` 字段值有效(`deep``[2,3]` 等)
**示例**
```yaml
---
title: Architecture # 必需
description: 架构基类说明 # 必需
outline: deep # 可选,但值必须有效
---
```
### 2. 内部链接验证
**检查项**
- 相对路径链接指向的文件存在
- 绝对路径链接格式正确
- 锚点链接对应的标题存在
- 没有损坏的链接
**有效链接格式**
- `[文本](./file.md)` - 相对路径
- `[文本](/zh-CN/core/architecture)` - 绝对路径
- `[文本](#标题)` - 锚点链接
- `[文本](./file.md#标题)` - 组合链接
### 3. 代码块验证
**检查项**
- 代码块有语法标记(```csharp、```bash 等)
- C# 代码块使用 `csharp` 标记(不是 `cs``c#`
- 代码块正确闭合
- 没有未闭合的反引号
**正确格式**
```markdown
\`\`\`csharp
public class Example { }
\`\`\`
```
**错误格式**
```markdown
\`\`\`cs // 应该使用 csharp
public class Example { }
\`\`\`
```
### 4. 标题层级验证
**检查项**
- 标题层级不跳级(不能从 `#` 直接跳到 `###`
- 每个文档只有一个一级标题(`#`
- 标题层级递增合理
**正确示例**
```markdown
# 一级标题
## 二级标题
### 三级标题
## 另一个二级标题
```
**错误示例**
```markdown
# 一级标题
### 三级标题 ❌ 跳过了二级标题
```
### 5. 中文标点符号验证
**检查项**
- 中文句子使用全角标点(,。!?)
- 英文句子使用半角标点(,.!?
- 代码和技术术语周围使用半角符号
- 括号使用规范
**规范示例**
- "这是一个示例。" ✓(中文全角句号)
- "This is an example." ✓(英文半角句号)
- "`Architecture` 类提供了..." ✓(代码周围半角)
### 6. 泛型符号验证
**检查项**
- 泛型符号正确转义(`<T>``&lt;T&gt;`
- 仅在代码块外转义
- 代码块内保持原样
**正确示例**
```markdown
`List&lt;T&gt;` 是一个泛型类。
\`\`\`csharp
List<T> items = new List<T>(); // 代码块内不转义
\`\`\`
```
## 验证脚本
### validate-frontmatter.sh
验证 Frontmatter 格式。
**用法**
```bash
.claude/skills/vitepress-validate/scripts/validate-frontmatter.sh <文件路径>
```
### validate-links.sh
验证内部链接有效性。
**用法**
```bash
.claude/skills/vitepress-validate/scripts/validate-links.sh <文件路径>
```
### validate-code-blocks.sh
验证代码块语法。
**用法**
```bash
.claude/skills/vitepress-validate/scripts/validate-code-blocks.sh <文件路径>
```
### validate-all.sh
执行所有验证。
**用法**
```bash
.claude/skills/vitepress-validate/scripts/validate-all.sh <文件或目录路径>
```
## 输出格式
### 验证通过
```
✓ docs/zh-CN/core/architecture.md
- Frontmatter: 通过
- 内部链接: 通过
- 代码块: 通过
- 标题层级: 通过
- 标点符号: 通过
- 泛型符号: 通过
```
### 验证失败
```
✗ docs/zh-CN/core/architecture.md
- Frontmatter: 失败
× 缺少必需字段: description
- 内部链接: 失败
× 损坏的链接: ./missing-file.md (第 45 行)
- 代码块: 警告
⚠ 使用了 'cs' 标记,建议使用 'csharp' (第 78 行)
- 标题层级: 通过
- 标点符号: 警告
⚠ 中文句子使用了半角句号 (第 102 行)
- 泛型符号: 失败
× 未转义的泛型符号: List<T> (第 120 行)
```
## 修复建议
验证失败时skill 会提供具体的修复建议:
**示例**
```
修复建议:
1. 在 Frontmatter 中添加 description 字段
2. 修复或删除损坏的链接: ./missing-file.md
3. 将代码块标记从 'cs' 改为 'csharp'
4. 将第 102 行的半角句号改为全角句号
5. 将第 120 行的 List<T> 改为 List&lt;T&gt;
```
## 配置选项
### 严格模式
启用严格模式时,警告也会导致验证失败。
```bash
/vitepress-validate --strict docs/zh-CN/
```
### 忽略特定检查
```bash
# 忽略标点符号检查
/vitepress-validate --ignore-punctuation docs/zh-CN/
# 忽略多个检查
/vitepress-validate --ignore-punctuation --ignore-generics docs/zh-CN/
```
## 集成到工作流
### 生成后自动验证
```bash
# 1. 生成 API 文档
/vitepress-api-doc GFramework.Core/architecture/Architecture.cs
# 2. 自动验证生成的文档
/vitepress-validate docs/zh-CN/api-reference/core/architecture.md
```
### 批量验证
```bash
# 验证所有 API 文档
/vitepress-validate docs/zh-CN/api-reference/
# 验证所有文档
/vitepress-validate docs/zh-CN/
```
## 退出代码
- `0` - 所有验证通过
- `1` - 存在错误
- `2` - 仅存在警告(非严格模式下仍返回 0
## 相关 Skills
- `/vitepress-api-doc` - 生成 API 文档后自动验证
- `/vitepress-guide` - 生成指南文档后自动验证
- `/vitepress-tutorial` - 生成教程文档后自动验证
## 最佳实践
1. **生成后立即验证**:每次生成文档后立即运行验证
2. **定期批量验证**:定期验证所有文档,确保一致性
3. **修复所有错误**:不要忽略验证错误,及时修复
4. **关注警告**:警告虽不致命,但应该重视并修复
5. **使用严格模式**:在 CI/CD 中使用严格模式确保质量
## 故障排除
### 问题:误报泛型符号错误
**解决方案**:确保泛型符号在代码块外正确转义,代码块内保持原样
### 问题:中文标点符号检查过于严格
**解决方案**:使用 `--ignore-punctuation` 选项,或手动调整规则
### 问题:链接验证失败但文件确实存在
**解决方案**:检查文件路径大小写,确保路径完全匹配
## 版本历史
- v1.0.0 - 初始版本,支持 6 项基本验证

View File

@ -0,0 +1,109 @@
#!/bin/bash
# 执行所有验证
# 用法: validate-all.sh <文件或目录路径>
set -e
TARGET="$1"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [ -z "$TARGET" ]; then
echo "用法: $0 <文件或目录路径>"
exit 1
fi
if [ ! -e "$TARGET" ]; then
echo "错误: 路径不存在: $TARGET"
exit 1
fi
echo "=========================================="
echo "VitePress 文档验证"
echo "=========================================="
echo ""
# 收集所有 Markdown 文件
if [ -f "$TARGET" ]; then
FILES=("$TARGET")
elif [ -d "$TARGET" ]; then
mapfile -t FILES < <(find "$TARGET" -name "*.md" -type f)
else
echo "错误: 无效的路径: $TARGET"
exit 1
fi
if [ ${#FILES[@]} -eq 0 ]; then
echo "未找到 Markdown 文件"
exit 0
fi
echo "找到 ${#FILES[@]} 个文件"
echo ""
TOTAL_ERRORS=0
TOTAL_WARNINGS=0
PASSED_FILES=0
FAILED_FILES=0
for FILE in "${FILES[@]}"; do
echo "验证: $FILE"
echo "----------------------------------------"
FILE_ERRORS=0
FILE_WARNINGS=0
# 1. Frontmatter 验证
if bash "$SCRIPT_DIR/validate-frontmatter.sh" "$FILE" 2>&1 | grep -q "✗"; then
FILE_ERRORS=$((FILE_ERRORS + 1))
fi
# 2. 链接验证
if bash "$SCRIPT_DIR/validate-links.sh" "$FILE" 2>&1 | grep -q "✗"; then
FILE_ERRORS=$((FILE_ERRORS + 1))
fi
# 3. 代码块验证
OUTPUT=$(bash "$SCRIPT_DIR/validate-code-blocks.sh" "$FILE" 2>&1 || true)
if echo "$OUTPUT" | grep -q "✗"; then
FILE_ERRORS=$((FILE_ERRORS + 1))
fi
if echo "$OUTPUT" | grep -q "⚠"; then
FILE_WARNINGS=$((FILE_WARNINGS + 1))
fi
# 统计结果
if [ $FILE_ERRORS -eq 0 ]; then
echo "✓ 验证通过"
PASSED_FILES=$((PASSED_FILES + 1))
else
echo "✗ 验证失败($FILE_ERRORS 个错误)"
FAILED_FILES=$((FAILED_FILES + 1))
fi
if [ $FILE_WARNINGS -gt 0 ]; then
echo "$FILE_WARNINGS 个警告"
fi
TOTAL_ERRORS=$((TOTAL_ERRORS + FILE_ERRORS))
TOTAL_WARNINGS=$((TOTAL_WARNINGS + FILE_WARNINGS))
echo ""
done
echo "=========================================="
echo "验证摘要"
echo "=========================================="
echo "总文件数: ${#FILES[@]}"
echo "通过: $PASSED_FILES"
echo "失败: $FAILED_FILES"
echo "总错误数: $TOTAL_ERRORS"
echo "总警告数: $TOTAL_WARNINGS"
echo ""
if [ $TOTAL_ERRORS -eq 0 ]; then
echo "✓ 所有验证通过"
exit 0
else
echo "✗ 验证失败"
exit 1
fi

View File

@ -0,0 +1,64 @@
#!/bin/bash
# 验证代码块语法
# 用法: validate-code-blocks.sh <文件路径>
set -e
FILE="$1"
if [ -z "$FILE" ]; then
echo "用法: $0 <文件路径>"
exit 1
fi
if [ ! -f "$FILE" ]; then
echo "错误: 文件不存在: $FILE"
exit 1
fi
echo "验证代码块语法: $FILE"
ERROR_COUNT=0
WARNING_COUNT=0
# 检查未闭合的代码块
OPEN_COUNT=$(grep -c '^```' "$FILE" || true)
if [ $((OPEN_COUNT % 2)) -ne 0 ]; then
echo "✗ 错误: 存在未闭合的代码块"
ERROR_COUNT=$((ERROR_COUNT + 1))
fi
# 检查 C# 代码块标记
LINE_NUM=0
while IFS= read -r LINE; do
LINE_NUM=$((LINE_NUM + 1))
# 检查是否使用了错误的 C# 标记
if echo "$LINE" | grep -qE '^```(cs|c#|C#)$'; then
echo "⚠ 警告: 第 $LINE_NUM 行使用了非标准标记,建议使用 'csharp'"
echo " 当前: $LINE"
WARNING_COUNT=$((WARNING_COUNT + 1))
fi
# 检查代码块是否有语言标记
if echo "$LINE" | grep -qE '^```$'; then
# 检查下一行是否是代码(简单启发式:不是空行且不是 ```
NEXT_LINE=$(sed -n "$((LINE_NUM + 1))p" "$FILE")
if [ -n "$NEXT_LINE" ] && ! echo "$NEXT_LINE" | grep -qE '^```'; then
echo "⚠ 警告: 第 $LINE_NUM 行的代码块缺少语言标记"
WARNING_COUNT=$((WARNING_COUNT + 1))
fi
fi
done < "$FILE"
# 输出结果
if [ $ERROR_COUNT -eq 0 ] && [ $WARNING_COUNT -eq 0 ]; then
echo "✓ 代码块验证通过"
exit 0
elif [ $ERROR_COUNT -eq 0 ]; then
echo "⚠ 代码块验证通过(有 $WARNING_COUNT 个警告)"
exit 0
else
echo "✗ 代码块验证失败($ERROR_COUNT 个错误,$WARNING_COUNT 个警告)"
exit 1
fi

View File

@ -0,0 +1,57 @@
#!/bin/bash
# 验证 Frontmatter 格式
# 用法: validate-frontmatter.sh <文件路径>
set -e
FILE="$1"
if [ -z "$FILE" ]; then
echo "用法: $0 <文件路径>"
exit 1
fi
if [ ! -f "$FILE" ]; then
echo "错误: 文件不存在: $FILE"
exit 1
fi
echo "验证 Frontmatter: $FILE"
# 检查是否有 Frontmatter限制在前几行避免匹配正文中的 '---'
if ! head -n 5 "$FILE" | grep -q "^---$"; then
echo "✗ 错误: 文件缺少 Frontmatter"
exit 1
fi
# 提取 Frontmatter 内容(第一个 --- 到第二个 --- 之间)
FRONTMATTER=$(sed -n '/^---$/,/^---$/p' "$FILE" | sed '1d;$d')
if [ -z "$FRONTMATTER" ]; then
echo "✗ 错误: Frontmatter 为空"
exit 1
fi
# 检查必需字段: title
if ! echo "$FRONTMATTER" | grep -q "^title:"; then
echo "✗ 错误: 缺少必需字段: title"
exit 1
fi
# 检查必需字段: description
if ! echo "$FRONTMATTER" | grep -q "^description:"; then
echo "✗ 错误: 缺少必需字段: description"
exit 1
fi
# 检查 outline 字段值(如果存在)
if echo "$FRONTMATTER" | grep -q "^outline:"; then
OUTLINE_VALUE=$(echo "$FRONTMATTER" | grep "^outline:" | sed 's/outline:\s*//')
if [ "$OUTLINE_VALUE" != "deep" ] && [ "$OUTLINE_VALUE" != "false" ] && ! echo "$OUTLINE_VALUE" | grep -qE '^\[.*\]$'; then
echo "⚠ 警告: outline 字段值可能无效: $OUTLINE_VALUE"
echo " 有效值: deep, false, [2,3]"
fi
fi
echo "✓ Frontmatter 验证通过"
exit 0

View File

@ -1,9 +1,11 @@
#!/bin/bash
# 验证 Markdown 内部链接是否指向当前仓库中的真实页面。
# 验证内部链接有效性
# 用法: validate-links.sh <文件路径>
set -e
FILE="$1"
BASE_DIR="docs/zh-CN"
if [ -z "$FILE" ]; then
echo "用法: $0 <文件路径>"
@ -15,40 +17,58 @@ if [ ! -f "$FILE" ]; then
exit 1
fi
echo "验证内部链接: $FILE"
# 获取文件所在目录
FILE_DIR=$(dirname "$FILE")
# 提取所有 Markdown 链接
LINKS=$(grep -oP '\[([^\]]+)\]\(([^)]+)\)' "$FILE" | grep -oP '\(([^)]+)\)' | sed 's/[()]//g' || true)
if [ -z "$LINKS" ]; then
echo "✓ 未找到需要验证的链接"
echo "✓ 未找到链接"
exit 0
fi
ERROR_COUNT=0
while IFS= read -r LINK; do
if [[ "$LINK" =~ ^https?:// ]] || [[ "$LINK" =~ ^mailto: ]] || [[ "$LINK" =~ ^# ]]; then
# 跳过外部链接
if [[ "$LINK" =~ ^https?:// ]]; then
continue
fi
# 跳过锚点链接(仅 #开头)
if [[ "$LINK" =~ ^# ]]; then
continue
fi
# 移除锚点部分
LINK_PATH=$(echo "$LINK" | sed 's/#.*//')
# 跳过空路径
if [ -z "$LINK_PATH" ]; then
continue
fi
if [[ "$LINK_PATH" =~ ^/ ]]; then
# 处理相对路径
if [[ "$LINK_PATH" =~ ^\. ]]; then
TARGET="$FILE_DIR/$LINK_PATH"
# 处理绝对路径
elif [[ "$LINK_PATH" =~ ^/ ]]; then
TARGET="docs$LINK_PATH"
if [[ ! "$TARGET" =~ \.[A-Za-z0-9]+$ ]]; then
# 如果没有扩展名,尝试添加 .md
if [[ ! "$TARGET" =~ \. ]]; then
TARGET="$TARGET.md"
fi
elif [[ "$LINK_PATH" =~ ^\. ]]; then
TARGET="$FILE_DIR/$LINK_PATH"
else
TARGET="$FILE_DIR/$LINK_PATH"
fi
# 规范化路径
TARGET=$(realpath -m "$TARGET" 2>/dev/null || echo "$TARGET")
# 检查文件是否存在
if [ ! -f "$TARGET" ] && [ ! -d "$TARGET" ]; then
echo "✗ 损坏的链接: $LINK"
echo " 目标不存在: $TARGET"
@ -57,9 +77,9 @@ while IFS= read -r LINK; do
done <<< "$LINKS"
if [ $ERROR_COUNT -eq 0 ]; then
echo "✓ 链接验证通过"
echo "✓ 内部链接验证通过"
exit 0
else
echo "✗ 发现 $ERROR_COUNT 个损坏的链接"
exit 1
fi
echo "✗ 共发现 $ERROR_COUNT 个损坏链接"
exit 1

View File

@ -1,6 +1,6 @@
---
name: gframework-pr-review
description: Repository-specific GitHub PR review workflow for the GFramework repo. Use when Codex needs to inspect the GitHub pull request for the current branch, extract AI review findings from CodeRabbit or greptile-apps, read failed checks, MegaLinter warnings, or failed test signals from the PR page, and then verify which findings should be fixed in the local codebase. Trigger explicitly with $gframework-pr-review or with prompts such as "look at the current PR", "extract CodeRabbit comments", "extract Greptile comments", or "check Failed Tests on the PR".
description: Repository-specific GitHub PR review workflow for the GFramework repo. Use when Codex needs to inspect the GitHub pull request for the current branch, extract CodeRabbit summary/comments, read failed checks, MegaLinter warnings, or failed test signals from the PR page, and then verify which findings should be fixed in the local codebase. Trigger explicitly with $gframework-pr-review or with prompts such as "look at the current PR", "extract CodeRabbit comments", or "check Failed Tests on the PR".
---
# GFramework PR Review
@ -16,10 +16,8 @@ Shortcut: `$gframework-pr-review`
3. Run `scripts/fetch_current_pr_review.py` to:
- locate the PR for the current branch through the GitHub PR API
- fetch PR metadata, issue comments, reviews, and review comments through the GitHub API
- extract CodeRabbit-specific summary blocks such as `Summary by CodeRabbit` and actionable-comment rollups when present
- extract `Summary by CodeRabbit`、GitHub Actions bot comments such as `MegaLinter analysis: Success with warnings`、and CTRF test reports from issue comments
- parse the latest CodeRabbit review body itself, including folded sections such as `🧹 Nitpick comments (N)` and the overall AI-agent prompt
- capture unresolved latest-head review threads for supported AI reviewers, including both `coderabbitai[bot]` and `greptile-apps[bot]`
- surface which supported AI reviewers currently have open latest-commit review threads, even when they do not use CodeRabbit-style issue comments
- fetch the latest head commit review threads from the GitHub PR API
- prefer unresolved review threads on the latest head commit over older summary-only signals
- extract failed checks, MegaLinter detailed issues, and test-report signals such as `Failed Tests` or `No failed tests in this run`
@ -51,7 +49,6 @@ Shortcut: `$gframework-pr-review`
The script should produce:
- PR metadata: number, title, state, branch, URL
- Supported AI reviewer summary, including latest reviews and open-thread counts for `coderabbitai[bot]` and `greptile-apps[bot]`
- CodeRabbit summary block from issue comments when available
- Folded latest-review sections such as `Nitpick comments (N)` when CodeRabbit puts them in the review body instead of issue comments
- Parsed latest head-review threads, with unresolved threads clearly separated
@ -69,7 +66,6 @@ The script should produce:
- If GitHub access fails because of proxy configuration, rerun the fetch with proxy variables removed.
- Prefer GitHub API results over PR HTML. The PR HTML page is now a fallback/debugging source, not the primary source of truth.
- If the summary block and the latest head review threads disagree, trust the latest unresolved head-review threads and treat older summary findings as stale until re-verified locally.
- Do not assume every AI reviewer behaves like CodeRabbit. `greptile-apps[bot]` findings may exist only as latest-head review threads, without CodeRabbit-style issue comments or folded review-body sections.
- Treat GitHub Actions comments with `Success with warnings` as actionable review input when they include concrete linter diagnostics such as `MegaLinter` detailed issues; do not skip them just because the parent check is green.
- Do not assume all CodeRabbit findings live in issue comments. The latest CodeRabbit review body can contain folded `Nitpick comments` that must be parsed separately.
- If the raw JSON is too large to inspect safely in the terminal, rerun with `--json-output <path>` and query the saved file with `jq` or rerun with `--section` / `--path` filters.
@ -80,6 +76,5 @@ The script should produce:
- 'Use FPR'
- `Use $gframework-pr-review on the current branch`
- `Check the current PR and extract CodeRabbit suggestions`
- `Check the current PR and extract Greptile suggestions`
- `Look for Failed Tests on the PR page`
- `先用 $gframework-pr-review 看当前分支 PR`

View File

@ -0,0 +1,4 @@
interface:
display_name: "GFramework PR Review"
short_description: "Inspect the current PR and CodeRabbit findings"
default_prompt: "Use $gframework-pr-review to inspect the current branch PR through the GitHub API, prioritize unresolved review threads on the latest head commit, and summarize failed checks or failed tests."

View File

@ -25,26 +25,11 @@ DEFAULT_WINDOWS_GIT = "/mnt/d/Tool/Development Tools/Git/cmd/git.exe"
GIT_ENVIRONMENT_KEY = "GFRAMEWORK_WINDOWS_GIT"
USER_AGENT = "codex-gframework-pr-review"
CODERABBIT_LOGIN = "coderabbitai[bot]"
GREPTILE_LOGIN = "greptile-apps[bot]"
GITHUB_ACTIONS_LOGIN = "github-actions[bot]"
REVIEW_COMMENT_ADDRESSED_MARKER = "<!-- <review_comment_addressed> -->"
VISIBLE_ADDRESSED_IN_COMMIT_PATTERN = re.compile(r"\s*Addressed in commit\s+[0-9a-f]{7,40}", re.I)
DEFAULT_REQUEST_TIMEOUT_SECONDS = 60
REQUEST_TIMEOUT_ENVIRONMENT_KEY = "GFRAMEWORK_PR_REVIEW_TIMEOUT_SECONDS"
SUPPORTED_AI_REVIEWERS = (
{
"slug": "coderabbit",
"login": CODERABBIT_LOGIN,
"display_name": "CodeRabbit",
"supports_review_body_parsing": True,
},
{
"slug": "greptile",
"login": GREPTILE_LOGIN,
"display_name": "Greptile",
"supports_review_body_parsing": False,
},
)
DISPLAY_SECTION_CHOICES = (
"pr",
"failed-checks",
@ -59,7 +44,6 @@ DISPLAY_SECTION_CHOICES = (
def resolve_git_command() -> str:
"""Resolve the git executable to use for this repository."""
candidates = [
os.environ.get(GIT_ENVIRONMENT_KEY),
DEFAULT_WINDOWS_GIT,
@ -84,7 +68,6 @@ def resolve_git_command() -> str:
def resolve_request_timeout_seconds() -> int:
"""Return the GitHub request timeout in seconds."""
configured_timeout = os.environ.get(REQUEST_TIMEOUT_ENVIRONMENT_KEY)
if not configured_timeout:
return DEFAULT_REQUEST_TIMEOUT_SECONDS
@ -103,7 +86,6 @@ def resolve_request_timeout_seconds() -> int:
def run_command(args: list[str]) -> str:
"""Run a command and return stdout, raising on failure."""
process = subprocess.run(args, capture_output=True, text=True, check=False)
if process.returncode != 0:
stderr = process.stderr.strip()
@ -112,12 +94,10 @@ def run_command(args: list[str]) -> str:
def get_current_branch() -> str:
"""Return the current git branch name."""
return run_command([resolve_git_command(), "rev-parse", "--abbrev-ref", "HEAD"])
def open_url(url: str, accept: str) -> tuple[str, Any]:
"""Open a URL with proxy variables disabled and return decoded text plus headers."""
opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))
request = urllib.request.Request(url, headers={"Accept": accept, "User-Agent": USER_AGENT})
with opener.open(request, timeout=resolve_request_timeout_seconds()) as response:
@ -125,13 +105,11 @@ def open_url(url: str, accept: str) -> tuple[str, Any]:
def fetch_json(url: str) -> tuple[Any, Any]:
"""Fetch a JSON payload and its response headers from GitHub."""
text, headers = open_url(url, accept="application/vnd.github+json")
return json.loads(text), headers
def extract_next_link(headers: Any) -> str | None:
"""Extract the next-page link from GitHub pagination headers."""
link_header = headers.get("Link")
if not link_header:
return None
@ -141,7 +119,6 @@ def extract_next_link(headers: Any) -> str | None:
def fetch_paged_json(url: str) -> list[dict[str, Any]]:
"""Fetch every page from a paginated GitHub API endpoint."""
items: list[dict[str, Any]] = []
next_url: str | None = url
while next_url:
@ -156,7 +133,6 @@ def fetch_paged_json(url: str) -> list[dict[str, Any]]:
def fetch_pull_request_metadata(pr_number: int) -> dict[str, Any]:
"""Fetch normalized metadata for a pull request."""
payload, _ = fetch_json(f"https://api.github.com/repos/{OWNER}/{REPO}/pulls/{pr_number}")
if not isinstance(payload, dict):
raise RuntimeError("Failed to fetch GitHub PR metadata.")
@ -172,7 +148,6 @@ def fetch_pull_request_metadata(pr_number: int) -> dict[str, Any]:
def resolve_pr_number(branch: str) -> int:
"""Resolve the most recently updated PR number for a branch."""
head_query = urllib.parse.quote(f"{OWNER}:{branch}")
payload, _ = fetch_json(f"https://api.github.com/repos/{OWNER}/{REPO}/pulls?state=all&head={head_query}")
if not isinstance(payload, list):
@ -187,12 +162,10 @@ def resolve_pr_number(branch: str) -> int:
def collapse_whitespace(text: str) -> str:
"""Collapse repeated whitespace into single spaces."""
return re.sub(r"\s+", " ", text).strip()
def truncate_text(text: str, max_length: int) -> str:
"""Collapse whitespace and truncate long text for CLI display."""
collapsed = collapse_whitespace(text)
if max_length <= 0 or len(collapsed) <= max_length:
return collapsed
@ -201,17 +174,14 @@ def truncate_text(text: str, max_length: int) -> str:
def strip_tags(text: str) -> str:
"""Remove HTML tags and normalize whitespace."""
return collapse_whitespace(re.sub(r"<[^>]+>", " ", text))
def strip_markdown_links(text: str) -> str:
"""Drop Markdown link targets while keeping visible link text."""
return re.sub(r"\[([^\]]+)\]\([^)]+\)", r"\1", text)
def extract_section(text: str, start_marker: str, end_markers: list[str]) -> str | None:
"""Extract text between a start marker and the earliest matching end marker."""
start = text.find(start_marker)
if start < 0:
return None
@ -226,7 +196,6 @@ def extract_section(text: str, start_marker: str, end_markers: list[str]) -> str
def parse_failed_checks(summary_block: str) -> list[dict[str, str]]:
"""Parse CodeRabbit summary rows for failed checks."""
failed_section = extract_section(
summary_block,
"### ❌ Failed checks",
@ -258,7 +227,6 @@ def parse_failed_checks(summary_block: str) -> list[dict[str, str]]:
def parse_actionable_comments(actionable_block: str) -> dict[str, Any]:
"""Parse CodeRabbit actionable comments from its issue-comment rollup."""
comment_count_match = re.search(r"Actionable comments posted:\s*(\d+)", actionable_block)
count = int(comment_count_match.group(1)) if comment_count_match else 0
@ -283,7 +251,6 @@ def parse_actionable_comments(actionable_block: str) -> dict[str, Any]:
def parse_comment_cards(comment_block: str) -> list[dict[str, str]]:
"""Parse CodeRabbit comment cards from a grouped Markdown block."""
comments: list[dict[str, str]] = []
pattern = re.compile(
r"<summary>"
@ -320,7 +287,6 @@ def parse_comment_cards(comment_block: str) -> list[dict[str, str]]:
def normalize_review_body_for_parsing(review_body: str) -> str:
"""Normalize a review body before structured section parsing."""
# CodeRabbit sometimes wraps structured HTML sections in markdown blockquotes,
# such as the CAUTION block used for outside-diff comments. Remove the quote
# prefixes for parsing while leaving the original raw body unchanged for output.
@ -328,7 +294,6 @@ def normalize_review_body_for_parsing(review_body: str) -> str:
def find_section_block_end(review_body: str, block_start: int) -> int:
"""Find the end boundary for a nested <details> section."""
depth = 1
for tag_match in re.finditer(r"<details>|</details>", review_body[block_start:]):
tag = tag_match.group(0)
@ -343,7 +308,6 @@ def find_section_block_end(review_body: str, block_start: int) -> int:
def parse_review_comment_group(review_body: str, section_name: str) -> dict[str, Any]:
"""Parse a folded review-body section into structured comments."""
section_match = re.search(
rf"<summary>[^<]*{re.escape(section_name)} \((?P<count>\d+)\)</summary><blockquote>\s*",
review_body,
@ -363,7 +327,6 @@ def parse_review_comment_group(review_body: str, section_name: str) -> dict[str,
def parse_latest_review_body(review_body: str) -> dict[str, Any]:
"""Parse the latest CodeRabbit review body for grouped comment sections."""
normalized_review_body = normalize_review_body_for_parsing(review_body)
actionable_count_match = re.search(r"\*\*Actionable comments posted:\s*(\d+)\*\*", normalized_review_body)
prompt_match = re.search(
@ -385,7 +348,6 @@ def parse_latest_review_body(review_body: str) -> dict[str, Any]:
def parse_megalinter_comment(comment_body: str) -> dict[str, Any]:
"""Parse a MegaLinter issue comment into structured report fields."""
normalized_body = html.unescape(comment_body).strip()
summary_match = re.search(
r"##\s*(?P<badges>.*?)\[MegaLinter\]\([^)]+\)\s+analysis:\s+\[(?P<status>[^\]]+)\]\((?P<run_url>[^)]+)\)",
@ -440,7 +402,6 @@ def parse_megalinter_comment(comment_body: str) -> dict[str, Any]:
def parse_test_report(block: str) -> dict[str, Any]:
"""Parse a CTRF or GitHub test-reporter comment block."""
report: dict[str, Any] = {
"raw": block.strip(),
"stats": {},
@ -481,7 +442,6 @@ def parse_test_report(block: str) -> dict[str, Any]:
def fetch_issue_comments(pr_number: int) -> list[dict[str, Any]]:
"""Fetch issue comments for a pull request."""
return fetch_paged_json(f"https://api.github.com/repos/{OWNER}/{REPO}/issues/{pr_number}/comments?per_page=100")
@ -490,7 +450,6 @@ def select_latest_comment_body(
predicate: Any,
required_user: str | None = None,
) -> str:
"""Return the latest matching issue-comment body."""
matching_comments = []
for comment in comments:
body = html.unescape(str(comment.get("body", "")))
@ -513,7 +472,6 @@ def select_comment_bodies(
predicate: Any,
required_user: str | None = None,
) -> list[str]:
"""Return all matching issue-comment bodies in chronological order."""
matching_comments = []
for comment in comments:
body = html.unescape(str(comment.get("body", "")))
@ -529,7 +487,6 @@ def select_comment_bodies(
def summarize_review_comment(comment: dict[str, Any]) -> dict[str, Any]:
"""Normalize a GitHub review comment into the output shape used by the skill."""
return {
"id": comment.get("id"),
"path": comment.get("path") or "",
@ -545,7 +502,6 @@ def summarize_review_comment(comment: dict[str, Any]) -> dict[str, Any]:
def classify_review_thread_status(latest_comment: dict[str, Any]) -> str:
"""Classify whether a review thread is still open or already addressed."""
body = latest_comment.get("body") or ""
author = latest_comment.get("user") or ""
if author == CODERABBIT_LOGIN and REVIEW_COMMENT_ADDRESSED_MARKER in body:
@ -554,12 +510,10 @@ def classify_review_thread_status(latest_comment: dict[str, Any]) -> str:
def contains_visible_addressed_commit_text(body: str) -> bool:
"""Detect visible addressed-in-commit text that does not close the thread by itself."""
return bool(VISIBLE_ADDRESSED_IN_COMMIT_PATTERN.search(body))
def build_latest_commit_review_threads(comments: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""Group review comments into normalized latest-commit review threads."""
comment_threads: dict[int, dict[str, Any]] = {}
# GitHub review replies point to the root comment id. Grouping them first lets
@ -610,7 +564,6 @@ def select_latest_submitted_review(
required_user: str | None = None,
prefer_non_empty_body: bool = False,
) -> dict[str, Any] | None:
"""Select the newest submitted review, optionally filtered by user."""
filtered_reviews = [review for review in reviews if review.get("submitted_at")]
if required_user is not None:
filtered_reviews = [review for review in filtered_reviews if review.get("user", {}).get("login") == required_user]
@ -626,43 +579,7 @@ def select_latest_submitted_review(
return max(filtered_reviews, key=lambda review: review.get("submitted_at", ""))
def summarize_submitted_review(review: dict[str, Any] | None) -> dict[str, Any]:
"""Normalize a submitted review into a stable JSON shape."""
if review is None:
return {
"id": None,
"state": "",
"submitted_at": "",
"commit_id": "",
"user": "",
"body": "",
}
return {
"id": review.get("id"),
"state": review.get("state") or "",
"submitted_at": review.get("submitted_at") or "",
"commit_id": review.get("commit_id") or "",
"user": review.get("user", {}).get("login") or "",
"body": review.get("body") or "",
}
def build_open_thread_counts_by_user(open_threads: list[dict[str, Any]]) -> dict[str, int]:
"""Count open latest-commit threads by their root-comment author."""
counts: dict[str, int] = {}
for thread in open_threads:
root_user = str(thread.get("root_comment", {}).get("user") or "")
if not root_user:
continue
counts[root_user] = counts.get(root_user, 0) + 1
return counts
def fetch_latest_commit_review(pr_number: int) -> dict[str, Any]:
"""Fetch the latest commit review, grouped threads, and AI-reviewer summaries."""
api_base = f"https://api.github.com/repos/{OWNER}/{REPO}/pulls/{pr_number}"
commits = fetch_paged_json(f"{api_base}/commits?per_page=100")
reviews = fetch_paged_json(f"{api_base}/reviews?per_page=100")
@ -683,37 +600,47 @@ def fetch_latest_commit_review(pr_number: int) -> dict[str, Any]:
]
candidate_reviews = latest_commit_reviews or [review for review in reviews if review.get("submitted_at")]
latest_review = select_latest_submitted_review(candidate_reviews)
latest_reviews_by_user: dict[str, dict[str, Any]] = {}
for agent in SUPPORTED_AI_REVIEWERS:
latest_reviews_by_user[agent["login"]] = summarize_submitted_review(
select_latest_submitted_review(
candidate_reviews,
required_user=agent["login"],
prefer_non_empty_body=True,
)
)
latest_coderabbit_review_with_body = select_latest_submitted_review(
candidate_reviews,
required_user=CODERABBIT_LOGIN,
prefer_non_empty_body=True,
)
latest_commit_comments = [comment for comment in comments if comment.get("commit_id") == latest_commit_sha]
threads = build_latest_commit_review_threads(latest_commit_comments)
open_threads = [thread for thread in threads if thread["status"] == "open"]
open_thread_counts_by_user = build_open_thread_counts_by_user(open_threads)
return {
"latest_commit": {
"sha": latest_commit_sha,
"message": latest_commit.get("commit", {}).get("message", ""),
},
"latest_review": summarize_submitted_review(latest_review),
"latest_coderabbit_review_with_body": latest_reviews_by_user.get(CODERABBIT_LOGIN, {}),
"latest_reviews_by_user": latest_reviews_by_user,
"open_thread_counts_by_user": open_thread_counts_by_user,
"latest_review": {
"id": latest_review.get("id") if latest_review else None,
"state": latest_review.get("state") if latest_review else "",
"submitted_at": latest_review.get("submitted_at") if latest_review else "",
"commit_id": latest_review.get("commit_id") if latest_review else "",
"user": latest_review.get("user", {}).get("login") if latest_review else "",
"body": latest_review.get("body") if latest_review else "",
},
"latest_coderabbit_review_with_body": {
"id": latest_coderabbit_review_with_body.get("id") if latest_coderabbit_review_with_body else None,
"state": latest_coderabbit_review_with_body.get("state") if latest_coderabbit_review_with_body else "",
"submitted_at": (
latest_coderabbit_review_with_body.get("submitted_at") if latest_coderabbit_review_with_body else ""
),
"commit_id": latest_coderabbit_review_with_body.get("commit_id") if latest_coderabbit_review_with_body else "",
"user": latest_coderabbit_review_with_body.get("user", {}).get("login")
if latest_coderabbit_review_with_body
else "",
"body": latest_coderabbit_review_with_body.get("body") if latest_coderabbit_review_with_body else "",
},
"threads": threads,
"open_threads": open_threads,
}
def build_result(pr_number: int, branch: str) -> dict[str, Any]:
"""Build the full review result payload for the selected PR."""
warnings: list[str] = []
pull_request_metadata = fetch_pull_request_metadata(pr_number)
issue_comments = fetch_issue_comments(pr_number)
@ -746,26 +673,8 @@ def build_result(pr_number: int, branch: str) -> dict[str, Any]:
latest_commit_review: dict[str, Any] = {}
coderabbit_review: dict[str, Any] = {}
review_agents: list[dict[str, Any]] = []
try:
latest_commit_review = fetch_latest_commit_review(pr_number)
latest_reviews_by_user = latest_commit_review.get("latest_reviews_by_user", {})
open_thread_counts_by_user = latest_commit_review.get("open_thread_counts_by_user", {})
review_agents = [
{
"slug": agent["slug"],
"login": agent["login"],
"display_name": agent["display_name"],
"supports_review_body_parsing": agent["supports_review_body_parsing"],
"latest_review": latest_reviews_by_user.get(agent["login"], {}),
"open_thread_count": int(open_thread_counts_by_user.get(agent["login"], 0)),
"detected": bool(
latest_reviews_by_user.get(agent["login"], {}).get("id")
or open_thread_counts_by_user.get(agent["login"], 0)
),
}
for agent in SUPPORTED_AI_REVIEWERS
]
latest_review = latest_commit_review.get("latest_coderabbit_review_with_body", {})
latest_review_body = str(latest_review.get("body") or "")
if latest_review.get("user") == CODERABBIT_LOGIN and latest_review_body:
@ -814,7 +723,6 @@ def build_result(pr_number: int, branch: str) -> dict[str, Any]:
},
"coderabbit_comments": parse_actionable_comments(actionable_block) if actionable_block else {},
"coderabbit_review": coderabbit_review,
"review_agents": review_agents,
"latest_commit_review": latest_commit_review,
"megalinter_report": parse_megalinter_comment(megalinter_block) if megalinter_block else {},
"test_reports": [parse_test_report(block) for block in test_blocks],
@ -823,7 +731,6 @@ def build_result(pr_number: int, branch: str) -> dict[str, Any]:
def write_json_output(result: dict[str, Any], output_path: str) -> str:
"""Write the full JSON result to disk and return the destination path."""
destination_path = Path(output_path).expanduser()
destination_path.parent.mkdir(parents=True, exist_ok=True)
destination_path.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8")
@ -831,12 +738,10 @@ def write_json_output(result: dict[str, Any], output_path: str) -> str:
def normalize_path_filters(path_filters: list[str] | None) -> list[str]:
"""Normalize CLI path filters to slash-separated fragments."""
return [path_filter.replace("\\", "/") for path_filter in (path_filters or []) if path_filter.strip()]
def path_matches_filters(path: str, normalized_path_filters: list[str]) -> bool:
"""Return whether a path matches any requested filter fragment."""
if not normalized_path_filters:
return True
@ -848,7 +753,6 @@ def filter_comments_by_path(
comments: list[dict[str, Any]],
normalized_path_filters: list[str],
) -> list[dict[str, Any]]:
"""Filter parsed comments by CLI path fragment."""
return [comment for comment in comments if path_matches_filters(str(comment.get("path") or ""), normalized_path_filters)]
@ -856,7 +760,6 @@ def filter_threads_by_path(
threads: list[dict[str, Any]],
normalized_path_filters: list[str],
) -> list[dict[str, Any]]:
"""Filter parsed review threads by CLI path fragment."""
return [thread for thread in threads if path_matches_filters(str(thread.get("path") or ""), normalized_path_filters)]
@ -868,7 +771,6 @@ def format_text(
max_description_length: int = 400,
json_output_path: str | None = None,
) -> str:
"""Format the result payload into concise text output."""
lines: list[str] = []
selected_sections = set(sections or DISPLAY_SECTION_CHOICES)
normalized_path_filters = normalize_path_filters(path_filters)
@ -963,7 +865,6 @@ def format_text(
latest_review = latest_commit_review.get("latest_review", {})
open_threads = latest_commit_review.get("open_threads", [])
visible_open_threads = filter_threads_by_path(open_threads, normalized_path_filters)
review_agents = [agent for agent in result.get("review_agents", []) if agent.get("detected")]
if latest_commit and "open-threads" in selected_sections:
lines.append("")
lines.append(f"Latest reviewed commit: {latest_commit.get('sha', '')}")
@ -973,21 +874,6 @@ def format_text(
f"{latest_review.get('state', '')} by {latest_review.get('user', '')} "
f"at {latest_review.get('submitted_at', '')}"
)
if review_agents:
lines.append("Detected AI reviewers on latest commit:")
for agent in review_agents:
latest_agent_review = agent.get("latest_review", {})
lines.append(
"- "
f"{agent.get('display_name', '')} ({agent.get('login', '')}): "
f"open_threads={agent.get('open_thread_count', 0)}"
+ (
f", latest_review={latest_agent_review.get('state', '')} "
f"at {latest_agent_review.get('submitted_at', '')}"
if latest_agent_review.get("submitted_at")
else ""
)
)
lines.append(
"Latest commit review threads: "
@ -1075,7 +961,6 @@ def format_text(
def parse_args() -> argparse.Namespace:
"""Parse CLI arguments."""
parser = argparse.ArgumentParser()
parser.add_argument("--branch", help="Override the current branch name.")
parser.add_argument("--pr", type=int, help="Fetch a specific PR number instead of resolving from branch.")
@ -1105,7 +990,6 @@ def parse_args() -> argparse.Namespace:
def main() -> None:
"""Run the CLI entry point."""
args = parse_args()
if args.pr is not None:
pr_number = args.pr

View File

@ -32,9 +32,6 @@ All AI agents and contributors must follow these rules when writing, reviewing,
`更新``补充``重构`.
- Each commit body bullet MUST describe one independent change point; avoid repeated or redundant descriptions.
- Keep technical terms in English when they are established project terms, such as `API``Model``System`.
- When composing a multi-line commit body from shell commands, contributors MUST NOT rely on Bash `$"..."` quoting for
newline escapes, because it passes literal `\n` sequences to Git. Use multiple `-m` flags or ANSI-C `$'...'`
quoting so the commit body contains real line breaks.
- If a new task starts while the current branch is `main`, contributors MUST first try to update local `main` from the
remote, then create and switch to a dedicated branch before making substantive changes.
- The branch naming rule for a new task branch is `<type>/<topic-or-scope>`, where `<type>` should match the intended

View File

@ -1,73 +0,0 @@
# Documentation Governance And Refresh Trace Archive RP-001 Through RP-008
> This archive preserves closed recovery-point history that no longer needs to stay in the default boot trace.
> The active trace should point here instead of repeating these completed stages.
## RP-001 Local-Plan Migration
- 迁移 `local-plan/` 中的 durable recovery state 到
`ai-plan/public/documentation-governance-and-refresh/`
- 建立 `todos/``traces/``archive/todos/``archive/traces/`
- 在 `ai-plan/public/README.md` 中建立
`docs/sdk-update-documentation``documentation-governance-and-refresh` 的映射
- 同步记录 `ai-plan-governance` 主题的迁移结论
## RP-002 Column Landing Pages
- 复核 `docs/zh-CN/core/index.md``game/index.md``source-generators/index.md`
- 对照模块 README 与包拆分关系,重写三个栏目 landing page
- 修正 VitePress dead-link 检查中指向 `docs/` 目录外 README 的链接方式
- 验证:`cd docs && bun run build`
## RP-003 Core Topic Pages
- 核对并重写 `architecture.md``context.md``lifecycle.md``command.md``query.md``cqrs.md`
- 移除旧 `Init()`、属性式 `CommandBus` / `QueryBus`、旧 `Input` 赋值式示例和已移除的
`RegisterMediatorBehavior` 说明
- 将旧 command / query 体系说明收口为兼容路径,并把新功能推荐迁到 `GFramework.Cqrs`
- 验证:`cd docs && bun run build`
## RP-004 PR Review Script Follow-Up
- 修复 `gframework-pr-review` 把空 `APPROVED` review body 误选为 CodeRabbit review body 的解析路径
- 改为在同一提交上优先选择最新非空 CodeRabbit review body
- 补齐 `docs/zh-CN/core/index.md``Godot``Source Generators` 栏目入口链接
- 修正 active trace 重复标题,消除 `MD024/no-duplicate-heading` 噪音
- 验证:
- `python3 .codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --format json`
- `cd docs && bun run build`
## RP-005 Remaining Core High-Risk Topics
- 核对 `events.md``property.md``state-management.md``coroutine.md``logging.md`
- 重写 `events.md``property.md``logging.md`
- 明确 `BindableProperty<T>.Comparer` 按闭合泛型共享,不是实例级配置
- 确认 `state-management.md``coroutine.md` 当前仍可保留
- 验证:`cd docs && bun run build`
## RP-006 Game Scene And UI Topics
- 核对 `docs/zh-CN/game/scene.md``docs/zh-CN/game/ui.md`
- 重写场景路由文档,明确 `ISceneFactory``ISceneRoot`、项目侧 router 与过渡处理器的职责边界
- 重写 UI 文档,明确 Page 栈、层级 UI、输入仲裁、World 阻断与暂停语义
- 验证:`cd docs && bun run build`
## RP-007 Core Source Generator Topics
- 核对 `context-aware-generator.md``priority-generator.md`
- 重写 `[ContextAware]` 文档说明当前生成成员、provider/实例缓存语义与 `ContextAwareBase` 边界
- 重写 `[Priority]` 文档,说明只生成 `IPrioritized`,排序效果取决于调用方是否走 priority-aware API
- 验证:`cd docs && bun run build`
## RP-008 Unified Documentation Refresh Skill
- 删除旧 `vitepress-*` 公开 skill 定义,建立统一 `.agents/skills/gframework-doc-refresh/`
- 新增 `.agents/skills/_shared/module-map.json`,按源码模块而不是文档类型驱动刷新
- 重写共享文档标准,固定证据顺序:源码 / XML docs / `*.csproj`、测试、README、当前 docs、`ai-libs/`、归档文档
- 新增 `scan_module_evidence.py`支持模块别名归一化、docs 栏目歧义检测和证据面扫描
- 更新 `.agents/skills/README.md`,将统一入口作为推荐工作流
- 验证:
- `python3 -B .agents/skills/gframework-doc-refresh/scripts/scan_module_evidence.py Core`
- `python3 -B .agents/skills/gframework-doc-refresh/scripts/scan_module_evidence.py Godot.SourceGenerators`
- `python3 -B .agents/skills/gframework-doc-refresh/scripts/scan_module_evidence.py Cqrs`
- `python3 -B .agents/skills/gframework-doc-refresh/scripts/scan_module_evidence.py source-generators --json`

View File

@ -7,34 +7,25 @@
## 当前恢复点
- 恢复点编号:`DOCUMENTATION-GOVERNANCE-REFRESH-RP-017`
- 恢复点编号:`DOCUMENTATION-GOVERNANCE-REFRESH-RP-005`
- 当前阶段:`Phase 3`
- 当前焦点:
- 已建立统一公开 skill`.agents/skills/gframework-doc-refresh/`
- 文档重构入口已从“按 guide/tutorial/api 类型拆 skill”收口为“按源码模块驱动文档刷新”
- `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
- 已完成 `docs/zh-CN/core/events.md``property.md``logging.md` 的专题页重写
- 已按源码与测试复核 `docs/zh-CN/core/state-management.md``coroutine.md`,当前内容与实现基本一致,无需再做
机械改写
- 下一轮需要把重心转到 `docs/zh-CN/game/*``docs/zh-CN/source-generators/*` 的专题页核对
## 当前状态摘要
- 文档治理规则已收口到仓库规范README、站点入口与采用链路不再依赖旧文档自证
- 高优先级模块入口、`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 线程收敛情况
- 高优先级模块入口与 `core` 关键专题页已回到可作为默认导航入口的状态,本轮计划中的 `core` 剩余高风险页面已完成收口
- 当前主题仍是 active topic因为 `game``source-generators` 栏目下仍可能包含与实现漂移的旧内容
## 当前活跃事实
- 旧 `local-plan/` 的详细 todo 与 trace 已迁入主题内 `archive/`
- 当前分支 `docs/sdk-update-documentation` 已在 `ai-plan/public/README.md` 建立 topic 映射
- active 跟踪文件只保留当前恢复点、活跃事实、风险与下一步,不再重复保存已完成阶段的长篇历史
- active trace 已把 RP-001 到 RP-008 的闭环历史归档到
`ai-plan/public/documentation-governance-and-refresh/archive/traces/documentation-governance-and-refresh-rp-001-through-rp-008.md`
- `core``game``source-generators` 三个栏目入口页现在都以模块 README 与当前包拆分为准
- `docs` 站点构建已验证通过,修正了 VitePress 对 `docs/` 目录外相对链接的 dead-link 检查问题
- `core` 关键专题页已移除 `Init()`、属性式 `CommandBus` / `QueryBus`、旧 `Input` 赋值式示例和已移除的
@ -43,144 +34,38 @@
- `documentation-governance-and-refresh` active trace 已把重复的 `### 下一步` 标题改成带恢复点标识的唯一标题,消除
`MD024/no-duplicate-heading` 告警
- `gframework-pr-review` 脚本已修复“空 `APPROVED` review 覆盖非空 CodeRabbit review body”的解析路径当前分支可重新提取 Nitpick comments
- `gframework-pr-review` 现在显式把 `coderabbitai[bot]``greptile-apps[bot]` 视为支持的 AI reviewer并在输出中单独列出
reviewer 元数据与 latest-head open thread 计数,不再只把 `greptile-apps` 混在通用 thread 列表里
- `.agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py` 已为全部函数补齐 docstring本地 AST 统计为
`44/44`,文件级 docstring coverage 为 `100%`
- `docs/zh-CN/core/events.md``property.md``logging.md` 已改成“当前角色、最常用入口、边界和迁移建议”的结构,
不再复刻旧版大而全 API 列表
- `docs/zh-CN/core/property.md` 已明确记录 `BindableProperty<T>.Comparer` 的闭合泛型级共享语义,避免文档继续误导读者把
`WithComparer(...)` 当成实例级配置
- `docs/zh-CN/core/state-management.md``coroutine.md` 已按当前 runtime / 测试重新核对,当前内容可继续保留
- `docs/zh-CN/game/scene.md` 已改成“真实公开入口、场景栈语义、factory/root 装配、过渡处理器与守卫扩展点”的结构,
不再暗示框架自带统一场景注册与完整引擎装配;本轮已补充项目侧目录布局、文件命名、最小 wiring 与兼容说明,并把
“推荐目录与文件约定(项目侧)” 收口为 “最小接入路径” 下的子节
- `docs/zh-CN/game/ui.md` 已改成“Page 栈、layer UI、输入动作仲裁、World 阻断与暂停语义”的结构,明确 `Show(...)`
不适用于 `UiLayer.Page`;本轮已补充 router、factory、root、page behavior、params 与 views 的推荐放置约定,并修复
“最小接入路径” 空节与标题层级错位问题
- 本轮重写后再次执行 `cd docs && bun run build` 通过,当前 `game` 栏目入口与专题页改动没有破坏站点构建
- `docs/zh-CN/source-generators/context-aware-generator.md` 已改成“真实生成成员、provider/实例缓存语义、与 `ContextAwareBase` 的边界、测试接法”的结构,
不再用旧版简化生成代码替代当前实现
- `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 加载警告
- `.agents/skills/gframework-doc-refresh/scripts/validate-code-blocks.sh` 已改成基于 `IN_CODE_BLOCK` 跟踪 opening /
closing fence避免把 closing fence 误报成缺少语言标记
- `.agents/skills/_shared/module-config.sh``get_readme_paths()` 已补齐 `Core.SourceGenerators.Abstractions`
`Godot.SourceGenerators.Abstractions``Ecs.Arch.Abstractions``SourceGenerators.Common`,并在未映射模块时返回
非零退出码
- `.agents/skills/_shared/module-map.json` 已收口为源码模块映射表覆盖源码目录、测试项目、README、`docs/zh-CN` 栏目与 `ai-libs/` 参考入口
- 旧 `vitepress-api-doc``vitepress-batch-api``vitepress-doc-generator``vitepress-guide``vitepress-tutorial``vitepress-validate`
已不再保留为可用公开 skill 定义文件
- `ai-libs/` 已纳入统一 skill 的标准证据链,只作为消费者接入参考,不再替代源码与测试契约
## 当前风险
- 旧专题页示例失真风险:`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/`
- 缓解措施:继续按源码、测试、`*.csproj``ai-libs/` 下已验证参考实现核对,不把旧文档当事实来源
- 采用路径误导风险:根聚合包与模块边界若再次被写错,会继续误导消费者的包选择
- 缓解措施:保持“源码与包关系优先”的证据顺序,改动采用说明时同步核对包依赖与生成器 wiring
- 模块映射不全风险:统一 skill 若遗漏模块别名、测试项目或 docs 栏目映射,会让后续扫描阶段直接失焦
- 缓解措施:以当前 `*.csproj` 族为 canonical module list统一维护 `.agents/skills/_shared/module-map.json`
- `ai-libs/` 漂移风险:参考项目若滞后于当前实现,可能把过时 wiring 重新带回文档
- 缓解措施:在 skill 中固定“源码/测试优先,`ai-libs/` 只补 adoption path”的证据顺序
- 旧模板迁移失真风险:旧 `vitepress-*` skill 的模板和规范若原样沿用,可能继续输出过时结构
- 缓解措施:只迁移可复用骨架,把输出优先级和证据规则重写进统一 skill
- 统一入口过宽风险:若 `gframework-doc-refresh` 的触发描述过宽,可能在模块不明确时误进入文档生成
- 缓解措施:要求先做模块归一化;遇到栏目别名歧义时只返回建议,不直接生成文档
- Active 入口回膨胀风险:后续若把栏目级重写过程直接追加到 active 文档,会再次拖慢恢复
- 缓解措施:阶段完成并验证后,继续把细节迁入本 topic 的 `archive/`
- review 跟进遗漏风险:如果 PR review 抓取继续优先选中空 review body会漏掉 CodeRabbit 的 Nitpick 和
linter 跟进项
- 缓解措施:保持当前“最新提交 + 最新非空 CodeRabbit review body”解析策略并在有疑点时以 API 实抓结果复核
- reviewer 适配漂移风险:若后续新增 AI reviewer 但脚本仍只维护固定 bot 名单可能再次出现“线程能看见、skill 却未声明覆盖”的偏差
- 缓解措施:当前已显式支持 `coderabbitai[bot]``greptile-apps[bot]`;新增 reviewer 时同步更新
`.agents/skills/gframework-pr-review/SKILL.md``agents/openai.yaml` 与抓取脚本常量表
## 活跃文档
- 历史跟踪归档:[documentation-governance-and-refresh-history-through-2026-04-18.md](../archive/todos/documentation-governance-and-refresh-history-through-2026-04-18.md)
- 历史 trace 归档:[documentation-governance-and-refresh-history-through-2026-04-18.md](../archive/traces/documentation-governance-and-refresh-history-through-2026-04-18.md)
- RP-001 到 RP-008 trace 归档:[documentation-governance-and-refresh-rp-001-through-rp-008.md](../archive/traces/documentation-governance-and-refresh-rp-001-through-rp-008.md)
## 验证说明
- 旧 `local-plan/` 的详细实施历史与文档站构建结果已迁入主题内归档
- active 跟踪文件已按 `ai-plan` 治理规则精简为当前恢复入口
- `cd docs && bun run build`
- `python3 -B .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --branch docs/sdk-update-documentation --format json --json-output /tmp/current-pr-review.json`
- `python3 -B .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --branch docs/sdk-update-documentation --section open-threads`
- `python3 -B -c "import ast, pathlib; path=pathlib.Path('.agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py'); tree=ast.parse(path.read_text(encoding='utf-8')); funcs=[node for node in ast.walk(tree) if isinstance(node,(ast.FunctionDef, ast.AsyncFunctionDef))]; documented=sum(1 for node in funcs if ast.get_docstring(node)); print(f'functions={len(funcs)} documented={documented} coverage={documented/len(funcs):.2%}')"`
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-code-blocks.sh docs/zh-CN/game/scene.md`
- `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'`
- `python3 -c "import pathlib, yaml; text = pathlib.Path('.agents/skills/gframework-doc-refresh/SKILL.md').read_text(); yaml.safe_load(text.split('---', 2)[1]); print('yaml-ok')"`
- `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`
- `python3 .codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --format json`
## 下一步
1. 评估当前 Godot 栏目页面集是否已足够稳定,决定是否把本阶段 active 恢复点收口并迁入 `archive/`
2. 如需继续保持 active优先精简 tracking / trace只保留归档决策、当前风险与下一次 PR follow-up 入口
3. 下一次推送后重新执行 `$gframework-pr-review`,确认 PR #268 的 CodeRabbit / Greptile open thread 是否按预期收敛
1. 继续核对 `docs/zh-CN/game/*`,优先处理仍引用旧安装方式、旧状态系统或旧 UI / Scene 接法的页面
2. 再推进 `docs/zh-CN/source-generators/*`,重点核对生成器 wiring、包关系与最小接入示例
3. 若 active trace 再积累新的已完成阶段,按恢复点粒度迁入 `archive/traces/`,避免默认启动入口再次膨胀

View File

@ -1,103 +1,129 @@
# Documentation Governance And Refresh Trace
# Documentation Governance And Refresh 追踪
## 2026-04-22
## 2026-04-19
### 当前恢复点RP-017
### 阶段local-plan 迁移收口RP-001
- 本轮从 PR #268 的最新 review 数据恢复未发现失败检查CTRF 报告显示 2139 个测试全部通过
- 本轮复核确认当前 PR 的 latest-head open thread 同时来自 `coderabbitai[bot]``greptile-apps[bot]`
- 已本地修复仍然成立的 review
- `docs/zh-CN/game/scene.md` 把“推荐目录与文件约定(项目侧)”降为“最小接入路径”下的子节
- `docs/zh-CN/game/ui.md` 为“最小接入路径”补充导语,并修复同级标题错位
- `.agents/skills/gframework-doc-refresh/scripts/validate-code-blocks.sh` 改成 opening / closing fence 状态机
- `.agents/skills/_shared/module-config.sh` 补齐缺失模块映射,并让未映射模块返回非零退出码
- `gframework-pr-review` 已从文案和输出模型两侧补齐多 reviewer 支持:当前 JSON 会单独给出 `review_agents`
以及 `open_thread_counts_by_user`,文本输出会显式列出 CodeRabbit / Greptile
- `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 = ...`
- 复核当前工作树后确认worktree 根目录仅剩一个 legacy `local-plan/`,其内容属于文档治理与重写主题的
durable recovery state不应继续作为独立根目录入口存在
- 按 `ai-plan` 治理规则建立 `ai-plan/public/documentation-governance-and-refresh/` 主题目录,并补齐:
- `todos/`
- `traces/`
- `archive/todos/`
- `archive/traces/`
- 将原 `local-plan` 中的详细 tracking / trace 迁入主题内历史归档,并为 active 入口只保留当前恢复点、
活跃事实、风险与下一步
- 在 `ai-plan/public/README.md` 中建立
`docs/sdk-update-documentation` -> `documentation-governance-and-refresh` 的 worktree 映射
- 同步更新 `ai-plan-governance` 的 tracking / trace记录本次迁移已验证当前工作树不再依赖 worktree-root
`local-plan/`
### 当前决策
### Archive Context
- active trace 只保留当前恢复点、关键事实、验证和下一步;完成阶段继续进入 `archive/traces/`
- `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 入口
- 历史跟踪归档:
- `ai-plan/public/documentation-governance-and-refresh/archive/todos/documentation-governance-and-refresh-history-through-2026-04-18.md`
- 历史 trace 归档:
- `ai-plan/public/documentation-governance-and-refresh/archive/traces/documentation-governance-and-refresh-history-through-2026-04-18.md`
### 验证
### 下一步RP-001
- `python3 -B .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --branch docs/sdk-update-documentation --format json --json-output /tmp/current-pr-review.json`
- `python3 -B .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --branch docs/sdk-update-documentation --section open-threads`
- `python3 -B -c "import ast, pathlib; path=pathlib.Path('.agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py'); tree=ast.parse(path.read_text(encoding='utf-8')); funcs=[node for node in ast.walk(tree) if isinstance(node,(ast.FunctionDef, ast.AsyncFunctionDef))]; documented=sum(1 for node in funcs if ast.get_docstring(node)); print(f'functions={len(funcs)} documented={documented} coverage={documented/len(funcs):.2%}')"`
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-code-blocks.sh docs/zh-CN/game/scene.md`
- `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`
1. 后续继续该主题时,只从 `ai-plan/public/documentation-governance-and-refresh/` 进入,不再恢复 `local-plan/`
2. 若 active 入口再次积累多轮已完成且已验证阶段,继续按同一模式迁入该主题自己的 `archive/`
## 2026-04-21
### 阶段:栏目 landing page 收口RP-002
- 依据 `ai-plan/public/README.md` 的 worktree 映射恢复 `documentation-governance-and-refresh` 主题,并确认该分支下一步应优先处理 `docs/zh-CN/core/*``game/*``source-generators/*`
- 复核 `docs/zh-CN/core/index.md``docs/zh-CN/game/index.md``docs/zh-CN/source-generators/index.md` 后确认:这三页仍保留旧版“大而全教程”结构,与当前模块 README、包拆分关系和推荐接入路径明显漂移
- 对照 `GFramework.Core/README.md``GFramework.Game/README.md``GFramework.Core.SourceGenerators/README.md`
`GFramework.Game.SourceGenerators/README.md``GFramework.Cqrs.SourceGenerators/README.md`
`GFramework.Godot.SourceGenerators/README.md`,重写三个栏目 landing page使其回到“模块定位、包关系、最小接入路径、继续阅读”的可信入口形态
- 首次执行 `cd docs && bun run build` 时发现 VitePress 会把跳到 `docs/` 目录外的相对链接判定为 dead link因此将 landing page 末尾的模块 README 入口改为纯文本路径提示而非站内链接
- 第二次执行 `cd docs && bun run build` 通过,说明当前 landing page 重写没有破坏站点构建
### 当前结论
- 当前默认导航入口已显著收敛,但专题页仍需逐页按源码与测试继续核对
- 后续优先级应从 `core` 专题页开始,再向 `game``source-generators` 扩展
### 下一步RP-002
1. 审核 `docs/zh-CN/core/architecture.md``context.md``lifecycle.md``command.md``query.md``cqrs.md`
2. 记录每页的失真点、真实 API 名称与应保留的最小示例
3. 完成一轮专题页重写后再次执行 `cd docs && bun run build`
### 补充2026-04-21 内容引用迁移
- 按当前文档治理主题,继续清理活跃规范与面向读者的内容入口中的旧参考仓库命名
- `AGENTS.md` 已把“secondary evidence source”从特定项目名收口为 `ai-libs/` 下的已验证只读参考实现
- `GFramework.Game/README.md``GFramework.Game.Abstractions/README.md`
`docs/zh-CN/game/index.md` 已同步改为 `ai-libs/` 参考表述,并去掉特定参考项目名称与项目内类型名线索
- `documentation-governance-and-refresh` active tracking 已同步把风险缓解中的参考来源更新为
`ai-libs/` 下已验证参考实现
- 下一次专题页重写时,继续沿用同一表述,不再把特定参考项目名写入新的活跃文档入口
### 补充2026-04-21 Core 专题页收口RP-003
- 复核 `docs/zh-CN/core/architecture.md``context.md``lifecycle.md``command.md``query.md``cqrs.md`
后确认:这些页面仍大量保留旧 API 叙述,例如 `Init()`、属性式 `CommandBus` / `QueryBus`、旧 `Input`
赋值式命令/查询示例,以及已移除的 `RegisterMediatorBehavior`
- 对照 `Architecture``ArchitectureContext``IArchitectureContext``ContextAwareBase`、旧
`AbstractCommand` / `AbstractQuery` 基类和 `GFramework.Cqrs/README.md` 后,重写上述六个页面
- 新版专题页将结构统一为“当前角色、真实公开入口、最小示例、兼容边界、迁移方向”,避免继续复刻旧版大而全教程
- `core/context.md` 已明确把 `GameContext` 收束为兼容回退路径,而不是新代码的推荐接法
- `core/command.md``core/query.md` 已明确旧体系仍可用,但新功能应优先走 `GFramework.Cqrs`
- `core/cqrs.md` 已与当前 runtime / generator / handler 注册语义对齐,并明确 `RegisterCqrsPipelineBehavior<TBehavior>()`
是公开入口
- 执行 `cd docs && bun run build` 通过,说明本轮 `core` 专题页重写没有破坏文档站构建
### 下一步RP-003
### 补充2026-04-21 PR review 跟进收口RP-004
- 通过 `gframework-pr-review` 复查当前分支 PR 时发现:脚本把同一 head commit 上空 body 的 `APPROVED`
review 误当成“最新 review body”导致 `Nitpick comments` 未被结构化提取
- 对照 GitHub API 的 review 列表后,确认真正包含 `Nitpick comments (2)` 的是更早 3 秒提交的
`COMMENTED` review因此调整脚本为“保持最新 review 元数据输出不变,但解析时优先选择同一提交上的最新非空
CodeRabbit review body”
- 根据重新提取的 Nitpick 内容,补齐 `docs/zh-CN/core/index.md``Godot``Source Generators`
栏目的可点击链接
- 顺手修正 active trace 中重复的 `### 下一步` 标题,消除 `MD024/no-duplicate-heading` 告警,避免后续 PR
review 再次把文档治理入口本身标成噪音
### 验证RP-004
- `python3 .codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --format json`
- `cd docs && bun run build`
### 下一步
### 下一步RP-004
1. 评估当前 Godot 栏目页面集是否已足够稳定,决定是否把当前恢复点收口并迁入 `archive/`
2. 如暂不归档,先把 active tracking / trace 进一步压缩到归档决策、当前风险与 PR 跟进入口
3. 下一次推送后重新执行 `$gframework-pr-review`,确认 PR #268 的 CodeRabbit / Greptile open thread 是否关闭或减少
1. 继续处理 `docs/zh-CN/core/events.md``property.md``state-management.md``coroutine.md``logging.md`
2. 若 active trace 继续累计多个已完成恢复点,按 `archive/traces/` 粒度归档旧阶段细节
3. 保持 PR review 跟进时优先验证最新未解决线程、非空 CodeRabbit review body 与 MegaLinter 明确告警
### 阶段Core 剩余高风险专题页核对RP-005
- 依据 `documentation-governance-and-refresh` active tracking 的恢复点,继续核对
`docs/zh-CN/core/events.md``property.md``state-management.md``coroutine.md``logging.md`
- 对照 `GFramework.Core/Events/*``Property/*``Logging/*``StateManagement/*``Coroutine/*` 以及对应测试后确认:
- `events.md``property.md``logging.md` 仍带有旧版“大而全 API 列表”写法,与当前公开入口和推荐边界不匹配
- `state-management.md``coroutine.md` 已和当前 runtime / 测试语义基本对齐,本轮无需为了统一文风做额外重写
- 重写 `events.md`,使其回到“上下文入口、`EventBus` / `EnhancedEventBus`、优先级传播、局部事件对象、与 Store / CQRS
的边界”的当前结构
- 重写 `property.md`,使其回到“字段级响应式值、何时继续使用 `BindableProperty<T>`、何时切到 `Store<TState>`”的当前结构,
并补充 `BindableProperty<T>.Comparer` 按闭合泛型共享的兼容注意点
- 重写 `logging.md`,使其回到“`LoggerFactoryResolver` 默认行为、`ArchitectureConfiguration` 日志 provider 配置、
`IStructuredLogger` / `LogContext`、provider 替换边界”的当前结构
- 执行 `cd docs && bun run build` 通过,说明本轮 `core` 专题页收口没有破坏文档站构建
### 当前结论RP-005
- 本轮计划中的 `core` 剩余高风险页面已完成核对;`state-management``coroutine` 经复核后可继续保留
- `core` 栏目下一步不再需要围绕这五页反复停留,后续重心应转到 `docs/zh-CN/game/*``docs/zh-CN/source-generators/*`
### 下一步RP-005
1. 继续核对 `docs/zh-CN/game/*`,优先处理仍引用旧安装方式、旧状态系统或旧 UI / Scene 接法的页面
2. 再推进 `docs/zh-CN/source-generators/*`,重点核对生成器 wiring、包关系与最小接入示例
3. 若 active trace 继续累计多个已完成恢复点,按 `archive/traces/` 粒度归档旧阶段细节

View File

@ -1,262 +1,658 @@
---
title: 场景系统
description: 说明 GFramework.Game 场景路由的当前入口、项目侧接入职责与扩展边界
description: 场景系统提供了完整的场景生命周期管理、路由导航和转换控制功能
---
# 场景系统
`GFramework.Game` 的场景系统是“路由基类 + 场景契约 + 过渡管线”的组合,不是替你包办注册表、节点树和引擎对象装配的
一体化方案。
## 概述
框架当前负责的是:
场景系统是 GFramework.Game 中用于管理游戏场景的核心组件。它提供了场景的加载、卸载、切换、暂停和恢复等完整生命周期管理,以及基于栈的场景导航机制。
- 场景栈管理
- `Load -> Enter -> Pause -> Resume -> Exit -> Unload` 生命周期顺序
- 路由守卫与过渡处理器执行时机
- `SceneRouterBase` 这一层的默认切换编排
通过场景系统,你可以轻松实现场景之间的平滑切换,管理场景栈(如主菜单 -> 游戏 -> 暂停菜单),并在场景转换时执行自定义逻辑。
项目或引擎适配层仍然需要自己提供
**主要特性**
- `ISceneFactory`
- `ISceneRoot`
- 具体的 `ISceneBehavior` / `IScene`
- 场景键和资源、节点、预制体之间的映射关系
- 完整的场景生命周期管理
- 基于栈的场景导航
- 场景转换管道和钩子
- 路由守卫Route Guard
- 场景工厂和行为模式
- 异步加载和卸载
如果你把它理解为“可复用的场景路由底座”而不是“现成的完整场景框架”,后续接法会更贴近源码。
## 核心概念
## 当前公开入
### 场景接
### `IScene`
业务场景生命周期契约,描述加载、进入、暂停、恢复、退出、卸载这六个阶段。
### `ISceneBehavior`
路由器直接操作的运行时对象。它除了场景生命周期外,还携带:
- `Key`
- `Original`
- `IsLoaded`
- `IsActive`
- `IsTransitioning`
如果你的引擎对象本身就能承担这些语义,可以直接实现 `ISceneBehavior`。如果你更想把业务逻辑放在纯 C# 场景类中,也可以由
项目侧行为包装器承载真正的引擎节点,再把业务场景逻辑委托出去。
### `ISceneRouter`
当前公开的路由接口,重点入口是:
- `BindRoot(ISceneRoot root)`
- `ReplaceAsync(string sceneKey, ISceneEnterParam? param = null)`
- `PushAsync(string sceneKey, ISceneEnterParam? param = null)`
- `PopAsync()`
- `ClearAsync()`
- `Contains(string sceneKey)`
### `SceneRouterBase`
`GFramework.Game` 提供的默认实现基类。它会:
- 在 `OnInit()` 中获取 `ISceneFactory`
- 通过 `SemaphoreSlim` 串行化切换
- 调用守卫、过渡处理器和环绕处理器
- 维护场景栈与恢复顺序
通常项目不会直接修改框架里的 `SceneRouterBase`,而是在项目层继承它。
## 场景栈的真实语义
按当前实现,最常用的三个动作语义如下:
- `ReplaceAsync`
- 清空整个栈,再加载并进入目标场景。
- `PushAsync`
- 先检查守卫,再创建新场景,挂到 `ISceneRoot`,执行 `OnLoadAsync()`,暂停当前栈顶,最后让新场景 `OnEnterAsync()`
- `PopAsync`
- 对栈顶执行离开检查,通过后退出并卸载它,再从 `ISceneRoot` 移除,然后恢复新的栈顶。
当前还有两个容易被旧文档误导的点:
- `SceneRouterBase` 默认不允许同一个 `sceneKey` 在栈中重复存在;内部会先做 `Contains(sceneKey)` 检查
- 框架不会替你实现“场景键 -> 具体场景实例”的注册逻辑;这仍然是 `ISceneFactory` 或项目注册表的职责
## 最小接入路径
推荐按下面的顺序接入。
### 推荐目录与文件约定(项目侧)
场景系统的目录结构不由框架强制,但建议把“路由编排、实例创建、引擎挂载、业务场景”分开放置,避免后续把
`SceneRouterBase` 派生类写成巨型协调器。
```text
Game/Scene/
GameSceneRouter.cs
GameSceneFactory.cs
SceneRoot.cs
Scenes/
GameplayScene.cs
PauseMenuScene.cs
Params/
GameplayEnterParam.cs
Registry/
SceneRegistry.cs
```
推荐约定如下:
- `GameSceneRouter.cs`:项目侧 router继承 `SceneRouterBase`,只注册 guard、transition handler 和 around handler
- `GameSceneFactory.cs`:实现 `ISceneFactory`,负责 `sceneKey -> ISceneBehavior` 的映射与实例创建
- `SceneRoot.cs`:实现 `ISceneRoot`,负责把行为对象对应的引擎节点挂到场景容器并移除
- `Scenes/*`:放具体业务场景、行为包装器或引擎节点包装类型
- `Params/*`:放实现 `ISceneEnterParam` 的进入参数,按业务场景拆分
- `Registry/*`:如果项目已有场景表或资源表,建议收口在这里,再由 `GameSceneFactory` 使用
最小 wiring 通常是:
`IScene` 定义了场景的完整生命周期:
```csharp
architecture.RegisterUtility<ISceneFactory>(new GameSceneFactory());
architecture.RegisterSystem(new GameSceneRouter());
```
然后在 `SceneRoot` 的引擎生命周期就绪点调用 `BindRoot(this)`。如果项目已有不同的资源目录、节点层级或场景注册表,
保留原结构即可;只要最终能提供 `ISceneFactory``ISceneRoot``ISceneBehavior`,就不需要为了框架重排所有文件。
### 1. 准备项目自己的 router
```csharp
using GFramework.Game.Scene;
using LoggingTransitionHandler = GFramework.Game.Scene.Handler.LoggingTransitionHandler;
public sealed class GameSceneRouter : SceneRouterBase
public interface IScene
{
protected override void RegisterHandlers()
ValueTask OnLoadAsync(ISceneEnterParam? param); // 加载资源
ValueTask OnEnterAsync(); // 进入场景
ValueTask OnPauseAsync(); // 暂停场景
ValueTask OnResumeAsync(); // 恢复场景
ValueTask OnExitAsync(); // 退出场景
ValueTask OnUnloadAsync(); // 卸载资源
}
```
### 场景路由
`ISceneRouter` 管理场景的导航和切换:
```csharp
public interface ISceneRouter : ISystem
{
ISceneBehavior? Current { get; } // 当前场景
string? CurrentKey { get; } // 当前场景键
IEnumerable<ISceneBehavior> Stack { get; } // 场景栈
bool IsTransitioning { get; } // 是否正在切换
ValueTask ReplaceAsync(string sceneKey, ISceneEnterParam? param = null);
ValueTask PushAsync(string sceneKey, ISceneEnterParam? param = null);
ValueTask PopAsync();
ValueTask ClearAsync();
}
```
### 场景行为
`ISceneBehavior` 封装了场景的具体实现和引擎集成:
```csharp
public interface ISceneBehavior
{
string Key { get; } // 场景唯一标识
IScene Scene { get; } // 场景实例
ValueTask LoadAsync(ISceneEnterParam? param);
ValueTask UnloadAsync();
}
```
## 基本用法
### 定义场景
实现 `IScene` 接口创建场景:
```csharp
using GFramework.Game.Abstractions.Scene;
public class MainMenuScene : IScene
{
public async ValueTask OnLoadAsync(ISceneEnterParam? param)
{
RegisterHandler(new LoggingTransitionHandler());
// 加载场景资源
Console.WriteLine("加载主菜单资源");
await Task.Delay(100); // 模拟加载
}
public async ValueTask OnEnterAsync()
{
// 进入场景
Console.WriteLine("进入主菜单");
// 显示 UI、播放音乐等
await Task.CompletedTask;
}
public async ValueTask OnPauseAsync()
{
// 暂停场景
Console.WriteLine("暂停主菜单");
await Task.CompletedTask;
}
public async ValueTask OnResumeAsync()
{
// 恢复场景
Console.WriteLine("恢复主菜单");
await Task.CompletedTask;
}
public async ValueTask OnExitAsync()
{
// 退出场景
Console.WriteLine("退出主菜单");
// 隐藏 UI、停止音乐等
await Task.CompletedTask;
}
public async ValueTask OnUnloadAsync()
{
// 卸载场景资源
Console.WriteLine("卸载主菜单资源");
await Task.Delay(50); // 模拟卸载
}
}
```
这一步只解决“切换流程怎么跑”,不解决“场景从哪来”。
### 注册场景
### 2. 提供 `ISceneFactory`
`SceneRouterBase` 会在初始化阶段通过 `GetUtility<ISceneFactory>()` 获取工厂,因此项目必须先注册它。
工厂的职责通常是:
- 按 `sceneKey` 找到项目自己的注册表、预制体或资源描述
- 创建或获取 `ISceneBehavior`
- 决定行为对象如何包裹引擎节点与业务场景逻辑
如果项目里已经有场景注册表,也建议把它收口在 factory 内部,而不是让文档继续暗示框架自带统一注册中心。
### 3. 提供 `ISceneRoot`
`ISceneRoot` 只做两件事:
- `AddScene(ISceneBehavior scene)`
- `RemoveScene(ISceneBehavior scene)`
也就是说root 是“挂载/移除容器”,不是路由器本身。当前 `ai-libs/` 参考实现也是在项目自己的 Godot 节点里实现
`ISceneRoot`,并在 `_Ready()` 时调用 `BindRoot(this)`
### 4. 把 router 和 factory 装进架构
在场景注册表中注册场景:
```csharp
architecture.RegisterUtility<ISceneFactory>(new GameSceneFactory());
architecture.RegisterSystem(new GameSceneRouter());
```
using GFramework.Game.Abstractions.Scene;
如果你的项目还需要动画、黑幕或 loading 过渡,可以继续在 `RegisterHandlers()` 里补自己的处理器。
### 5. 在 root 就绪后绑定
```csharp
public sealed class SceneRoot : Node2D, ISceneRoot
public class GameSceneRegistry : IGameSceneRegistry
{
[GetSystem] private ISceneRouter _sceneRouter = null!;
private readonly Dictionary<string, Type> _scenes = new();
public override void _Ready()
public GameSceneRegistry()
{
__InjectContextBindings_Generated();
_sceneRouter.BindRoot(this);
// 注册场景
Register("MainMenu", typeof(MainMenuScene));
Register("Gameplay", typeof(GameplayScene));
Register("Pause", typeof(PauseScene));
}
public void AddScene(ISceneBehavior scene)
public void Register(string key, Type sceneType)
{
// 项目侧决定如何把 scene.Original 挂进引擎节点树
_scenes[key] = sceneType;
}
public void RemoveScene(ISceneBehavior scene)
public Type? GetSceneType(string key)
{
// 项目侧决定如何移除并释放引擎对象
return _scenes.TryGetValue(key, out var type) ? type : null;
}
}
```
### 6. 从业务代码发起导航
### 切换场景
使用场景路由进行导航:
```csharp
await sceneRouter.ReplaceAsync(
"Gameplay",
new GameplayEnterParam
{
Seed = "new-game"
});
using GFramework.Core.Abstractions.Controller;
using GFramework.Core.SourceGenerators.Abstractions.Rule;
await sceneRouter.PushAsync("PauseMenu");
await sceneRouter.PopAsync();
[ContextAware]
public partial class GameController : IController
{
public async Task StartGame()
{
var sceneRouter = this.GetSystem<ISceneRouter>();
// 替换当前场景(清空场景栈)
await sceneRouter.ReplaceAsync("Gameplay");
}
public async Task ShowPauseMenu()
{
var sceneRouter = this.GetSystem<ISceneRouter>();
// 压入新场景(保留当前场景)
await sceneRouter.PushAsync("Pause");
}
public async Task ClosePauseMenu()
{
var sceneRouter = this.GetSystem<ISceneRouter>();
// 弹出当前场景(恢复上一个场景)
await sceneRouter.PopAsync();
}
}
```
## 扩展点
## 高级用法
### 场景参数传递
通过 `ISceneEnterParam` 传递数据:
```csharp
// 定义场景参数
public class GameplayEnterParam : ISceneEnterParam
{
public int Level { get; set; }
public string Difficulty { get; set; }
}
// 在场景中接收参数
public class GameplayScene : IScene
{
private int _level;
private string _difficulty;
public async ValueTask OnLoadAsync(ISceneEnterParam? param)
{
if (param is GameplayEnterParam gameplayParam)
{
_level = gameplayParam.Level;
_difficulty = gameplayParam.Difficulty;
Console.WriteLine($"加载关卡 {_level},难度: {_difficulty}");
}
await Task.CompletedTask;
}
// ... 其他生命周期方法
}
// 切换场景时传递参数
await sceneRouter.ReplaceAsync("Gameplay", new GameplayEnterParam
{
Level = 1,
Difficulty = "Normal"
});
```
### 路由守卫
如果你要在进入或离开场景前做业务检查,实现 `ISceneRouteGuard`
使用路由守卫控制场景切换
- `CanEnterAsync(string sceneKey, ISceneEnterParam? param)`
- `CanLeaveAsync(string sceneKey)`
```csharp
using GFramework.Game.Abstractions.Scene;
适合放:
public class SaveGameGuard : ISceneRouteGuard
{
public async ValueTask<bool> CanLeaveAsync(
ISceneBehavior from,
string toKey,
ISceneEnterParam? param)
{
// 离开游戏场景前检查是否需要保存
if (from.Key == "Gameplay")
{
var needsSave = CheckIfNeedsSave();
if (needsSave)
{
await SaveGameAsync();
}
}
- 未保存进度拦截
- 场景解锁条件检查
- 新手引导流程限制
return true; // 允许离开
}
### 过渡处理器
public async ValueTask<bool> CanEnterAsync(
string toKey,
ISceneEnterParam? param)
{
// 进入场景前的验证
if (toKey == "Gameplay")
{
// 检查是否满足进入条件
var canEnter = CheckGameplayRequirements();
return canEnter;
}
`SceneRouterBase` 公开了:
return true;
}
- `RegisterHandler(ISceneTransitionHandler handler, SceneTransitionHandlerOptions? options = null)`
- `RegisterAroundHandler(ISceneAroundTransitionHandler handler, SceneTransitionHandlerOptions? options = null)`
private bool CheckIfNeedsSave() => true;
private async Task SaveGameAsync() => await Task.Delay(100);
private bool CheckGameplayRequirements() => true;
}
适合放:
// 注册守卫
sceneRouter.AddGuard(new SaveGameGuard());
```
- 日志
- 黑幕、淡入淡出或 loading 动画
- 切场前后的指标采集
### 场景转换处理器
如果你的项目已经有复杂引擎过渡逻辑,优先把这些逻辑放进 handler而不是把 `SceneRouterBase` 派生类本身做成巨型协调器。
自定义场景转换逻辑:
## 与旧写法的边界
```csharp
using GFramework.Game.Abstractions.Scene;
下面这些说法不再适合作为默认接入指导:
public class FadeTransitionHandler : ISceneTransitionHandler
{
public async ValueTask OnBeforeLoadAsync(SceneTransitionEvent @event)
{
Console.WriteLine($"准备加载场景: {@event.ToKey}");
// 显示加载画面
await ShowLoadingScreen();
}
- “框架会帮你直接注册和发现所有场景类型”
- “只要写一个 `IScene` 就能自动接入所有引擎对象”
- “场景系统本身自带统一注册表和完整项目结构”
public async ValueTask OnAfterLoadAsync(SceneTransitionEvent @event)
{
Console.WriteLine($"场景加载完成: {@event.ToKey}");
await Task.CompletedTask;
}
当前更准确的理解是:
public async ValueTask OnBeforeEnterAsync(SceneTransitionEvent @event)
{
Console.WriteLine($"准备进入场景: {@event.ToKey}");
// 播放淡入动画
await PlayFadeIn();
}
- 框架提供通用场景切换编排
- 项目提供 factory、root、资源映射和具体引擎装配
- 文档中的最小示例应优先说明职责边界,而不是继续堆叠大而全教程
public async ValueTask OnAfterEnterAsync(SceneTransitionEvent @event)
{
Console.WriteLine($"已进入场景: {@event.ToKey}");
// 隐藏加载画面
await HideLoadingScreen();
}
## 推荐阅读
public async ValueTask OnBeforeExitAsync(SceneTransitionEvent @event)
{
Console.WriteLine($"准备退出场景: {@event.FromKey}");
// 播放淡出动画
await PlayFadeOut();
}
1. [game/index.md](./index.md)
2. [ui.md](./ui.md)
3. `GFramework.Game/README.md`
4. `GFramework.Game.Abstractions/README.md`
public async ValueTask OnAfterExitAsync(SceneTransitionEvent @event)
{
Console.WriteLine($"已退出场景: {@event.FromKey}");
await Task.CompletedTask;
}
private async Task ShowLoadingScreen() => await Task.Delay(100);
private async Task HideLoadingScreen() => await Task.Delay(100);
private async Task PlayFadeIn() => await Task.Delay(200);
private async Task PlayFadeOut() => await Task.Delay(200);
}
// 注册转换处理器
sceneRouter.AddTransitionHandler(new FadeTransitionHandler());
```
### 场景栈管理
```csharp
using GFramework.Core.Abstractions.Controller;
using GFramework.Core.SourceGenerators.Abstractions.Rule;
[ContextAware]
public partial class SceneNavigationController : IController
{
public async Task NavigateToSettings()
{
var sceneRouter = this.GetSystem<ISceneRouter>();
// 检查场景是否已在栈中
if (sceneRouter.Contains("Settings"))
{
Console.WriteLine("设置场景已打开");
return;
}
// 压入设置场景
await sceneRouter.PushAsync("Settings");
}
public void ShowSceneStack()
{
var sceneRouter = this.GetSystem<ISceneRouter>();
Console.WriteLine("当前场景栈:");
foreach (var scene in sceneRouter.Stack)
{
Console.WriteLine($"- {scene.Key}");
}
}
public async Task ReturnToMainMenu()
{
var sceneRouter = this.GetSystem<ISceneRouter>();
// 清空所有场景并加载主菜单
await sceneRouter.ClearAsync();
await sceneRouter.ReplaceAsync("MainMenu");
}
}
```
### 场景加载进度
```csharp
public class GameplayScene : IScene
{
public async ValueTask OnLoadAsync(ISceneEnterParam? param)
{
var resourceManager = GetResourceManager();
// 加载多个资源并报告进度
var resources = new[]
{
"textures/player.png",
"textures/enemy.png",
"audio/bgm.mp3",
"models/level.obj"
};
for (int i = 0; i < resources.Length; i++)
{
await resourceManager.LoadAsync<object>(resources[i]);
// 报告进度
var progress = (i + 1) / (float)resources.Length;
ReportProgress(progress);
}
}
private void ReportProgress(float progress)
{
// 发送进度事件
Console.WriteLine($"加载进度: {progress * 100:F0}%");
}
// ... 其他生命周期方法
}
```
### 场景预加载
```csharp
using GFramework.Core.Abstractions.Controller;
using GFramework.Core.SourceGenerators.Abstractions.Rule;
[ContextAware]
public partial class PreloadController : IController
{
public async Task PreloadNextLevel()
{
var sceneFactory = this.GetUtility<ISceneFactory>();
// 预加载下一关场景
var scene = sceneFactory.Create("Level2");
await scene.OnLoadAsync(null);
Console.WriteLine("下一关预加载完成");
}
}
```
## 最佳实践
1. **在 OnLoad 中加载资源,在 OnUnload 中释放**:保持资源管理清晰
```csharp
public async ValueTask OnLoadAsync(ISceneEnterParam? param)
{
_texture = await LoadTextureAsync("player.png");
}
public async ValueTask OnUnloadAsync()
{
_texture?.Dispose();
_texture = null;
}
```
2. **使用 Push/Pop 管理临时场景**:如暂停菜单、设置界面
```csharp
// 打开暂停菜单(保留游戏场景)
await sceneRouter.PushAsync("Pause");
// 关闭暂停菜单(恢复游戏场景)
await sceneRouter.PopAsync();
```
3. **使用 Replace 切换主要场景**:如从菜单到游戏
```csharp
// 开始游戏(清空场景栈)
await sceneRouter.ReplaceAsync("Gameplay");
```
4. **在 OnPause/OnResume 中管理状态**:暂停和恢复游戏逻辑
```csharp
public async ValueTask OnPauseAsync()
{
// 暂停游戏逻辑
_gameTimer.Pause();
_audioSystem.PauseBGM();
}
public async ValueTask OnResumeAsync()
{
// 恢复游戏逻辑
_gameTimer.Resume();
_audioSystem.ResumeBGM();
}
```
5. **使用路由守卫处理业务逻辑**:如保存检查、权限验证
```csharp
public async ValueTask<bool> CanLeaveAsync(...)
{
if (HasUnsavedChanges())
{
var confirmed = await ShowSaveDialog();
if (confirmed)
{
await SaveAsync();
}
return confirmed;
}
return true;
}
```
6. **避免在场景切换时阻塞**:使用异步操作
```csharp
✓ await sceneRouter.ReplaceAsync("Gameplay");
✗ sceneRouter.ReplaceAsync("Gameplay").Wait(); // 可能死锁
```
## 常见问题
### 问题Replace、Push、Pop 有什么区别?
**解答**
- **Replace**:清空场景栈,加载新场景(用于主要场景切换)
- **Push**:压入新场景,暂停当前场景(用于临时场景)
- **Pop**:弹出当前场景,恢复上一个场景(用于关闭临时场景)
```csharp
// 场景栈示例
await sceneRouter.ReplaceAsync("MainMenu"); // [MainMenu]
await sceneRouter.PushAsync("Settings"); // [MainMenu, Settings]
await sceneRouter.PushAsync("About"); // [MainMenu, Settings, About]
await sceneRouter.PopAsync(); // [MainMenu, Settings]
await sceneRouter.PopAsync(); // [MainMenu]
```
### 问题:如何在场景之间传递数据?
**解答**
有几种方式:
1. **通过场景参数**
```csharp
await sceneRouter.ReplaceAsync("Gameplay", new GameplayEnterParam
{
Level = 5
});
```
2. **通过 Model**
```csharp
var gameModel = this.GetModel<GameModel>();
gameModel.CurrentLevel = 5;
await sceneRouter.ReplaceAsync("Gameplay");
```
3. **通过事件**
```csharp
this.SendEvent(new LevelSelectedEvent { Level = 5 });
await sceneRouter.ReplaceAsync("Gameplay");
```
### 问题:场景切换时如何显示加载画面?
**解答**
使用场景转换处理器:
```csharp
public class LoadingScreenHandler : ISceneTransitionHandler
{
public async ValueTask OnBeforeLoadAsync(SceneTransitionEvent @event)
{
await ShowLoadingScreen();
}
public async ValueTask OnAfterEnterAsync(SceneTransitionEvent @event)
{
await HideLoadingScreen();
}
// ... 其他方法
}
```
### 问题:如何防止用户在场景切换时操作?
**解答**
检查 `IsTransitioning` 状态:
```csharp
public async Task ChangeScene(string sceneKey)
{
var sceneRouter = this.GetSystem<ISceneRouter>();
if (sceneRouter.IsTransitioning)
{
Console.WriteLine("场景正在切换中,请稍候");
return;
}
await sceneRouter.ReplaceAsync(sceneKey);
}
```
### 问题:场景切换失败怎么办?
**解答**
使用 try-catch 捕获异常:
```csharp
try
{
await sceneRouter.ReplaceAsync("Gameplay");
}
catch (Exception ex)
{
Console.WriteLine($"场景切换失败: {ex.Message}");
// 回退到安全场景
await sceneRouter.ReplaceAsync("MainMenu");
}
```
### 问题:如何实现场景预加载?
**解答**
在后台预先加载场景资源:
```csharp
// 在当前场景中预加载下一个场景
var factory = this.GetUtility<ISceneFactory>();
var nextScene = factory.Create("NextLevel");
await nextScene.OnLoadAsync(null);
// 稍后快速切换
await sceneRouter.ReplaceAsync("NextLevel");
```
## 相关文档
- [UI 系统](/zh-CN/game/ui) - UI 页面管理
- [资源管理系统](/zh-CN/core/resource) - 场景资源加载
- [状态机系统](/zh-CN/core/state-machine) - 场景状态管理
- [Godot 场景系统](/zh-CN/godot/scene) - Godot 引擎集成
- [存档系统实现教程](/zh-CN/tutorials/save-system) - 场景切换时保存数据

View File

@ -1,333 +1,509 @@
---
title: UI 系统
description: 说明 GFramework.Game UI 路由当前的页面栈、层级 UI、输入语义与项目侧接入方式
description: UI 系统提供了完整的 UI 页面管理、路由导航和多层级显示功能
---
# UI 系统
`GFramework.Game` 的 UI 系统不是单纯的“页面栈”。按当前实现,它同时覆盖:
## 概述
- `UiLayer.Page` 的页面导航
- `Overlay` / `Modal` / `Toast` / `Topmost` 的层级 UI
- UI 语义动作捕获与分发
- World 输入阻断
- 由 UI 可见性驱动的暂停语义
UI 系统是 GFramework.Game 中用于管理游戏 UI 界面的核心组件。它提供了 UI 页面的生命周期管理、基于栈的导航机制,以及多层级的
UI 显示系统Page、Overlay、Modal、Toast、Topmost
因此,新的接入文档不应再把它写成“只有 Push/Pop 的传统页面管理器”。
通过 UI 系统,你可以轻松实现 UI 页面之间的切换,管理 UI 栈(如主菜单 -> 设置 -> 关于),以及在不同层级显示各种类型的
UI对话框、提示、加载界面等
## 当前公开入口
**主要特性**
### `IUiPage`
- 完整的 UI 生命周期管理
- 基于栈的 UI 导航
- 多层级 UI 显示5 个层级)
- UI 转换管道和钩子
- 路由守卫Route Guard
- UI 工厂和行为模式
最轻量的页面生命周期契约,暴露:
## 核心概念
- `OnEnter`
- `OnExit`
- `OnPause`
- `OnResume`
- `OnShow`
- `OnHide`
### UI 页面接口
如果你的页面逻辑只想表达这些生命周期阶段,停留在 `IUiPage` 就够了。
### `IUiPageBehavior`
路由器真正操作的运行时页面行为。相比 `IUiPage`,它还携带:
- `Key`
- `Layer`
- `Handle`
- `View`
- `IsAlive`
- `IsVisible`
- `IsModal`
- `BlocksInput`
- `InteractionProfile`
- `TryHandleUiAction(UiInputAction action)`
也就是说,页面栈和层级 UI 都是围绕 `IUiPageBehavior` 工作的,而不是只围绕 `IUiPage`
### `IUiRouter`
当前最常用的入口分成两组。
页面栈:
- `PushAsync(...)`
- `ReplaceAsync(...)`
- `PopAsync(...)`
- `ClearAsync()`
- `Peek()`
- `PeekKey()`
层级 UI
- `Show(...)`
- `Hide(...)`
- `Resume(...)`
- `ClearLayer(...)`
- `HideByKey(...)`
- `GetAllFromLayer(...)`
输入与阻断:
- `GetUiActionOwner(UiInputAction action)`
- `TryDispatchUiAction(UiInputAction action)`
- `BlocksWorldPointerInput()`
- `BlocksWorldActionInput()`
### `UiLayer`
当前层级语义如下:
- `Page`
- 页面栈层。请用 `PushAsync` / `ReplaceAsync`,不要用 `Show(...)`
- `Overlay`
- 可叠加的浮层。
- `Modal`
- 默认阻断下层输入的模态层。
- `Toast`
- 轻量提示层。
- `Topmost`
- 最顶层的系统级 UI。
### `UiTransitionPolicy``UiPopPolicy`
页面栈的两个关键策略:
- `UiTransitionPolicy.Exclusive`
- 新页面独占显示,下层页面会 `Pause + Hide`
- `UiTransitionPolicy.Overlay`
- 新页面覆盖显示,下层页面只 `Pause`
- `UiPopPolicy.Destroy`
- 弹出时直接销毁页面实例
- `UiPopPolicy.Suspend`
- 弹出时保留页面实例,供后续恢复
## UI 路由的真实语义
### 页面栈和层级 UI 是两套入口
当前源码里:
- `Page` 层属于栈语义,用 `PushAsync` / `ReplaceAsync` / `PopAsync`
- `Overlay``Modal``Toast``Topmost` 属于层级语义,用 `Show` / `Hide` / `Resume`
`Show(..., UiLayer.Page)` 在当前实现里会直接抛异常,因此旧文档里那种“所有 UI 都统一通过 Show 进入”的写法不再准确。
### 输入不是页面自己抢,而是 router 先仲裁
`UiInteractionProfile` 用来描述页面的交互契约,例如:
- 捕获哪些 `UiInputAction`
- 是否阻断 World 指针输入
- 是否阻断 World 语义动作输入
- 页面可见时是否推动暂停栈
输入层先把设备输入映射成 `UiInputAction`,再交给 `IUiRouter.TryDispatchUiAction(...)`。最终谁拥有动作捕获权,由当前可见页面和层级顺序决定。
### 页面可见性会影响暂停与阻断
这也是 UI 系统和普通页面栈最不同的地方之一。当前实现里:
- `Modal` / `Topmost` 默认具有更强的输入阻断语义
- 页面的 `InteractionProfile` 可以驱动暂停栈
- `BlocksWorldPointerInput()``BlocksWorldActionInput()` 是给项目输入层做统一判断的
如果你的项目有“打开设置页后暂停世界”“Modal 打开时地图点击失效”这类需求,优先接这个契约,而不是每个页面自己散落地写输入屏蔽逻辑。
## 最小接入路径
推荐按下面的顺序接入。
### 推荐目录与文件约定(项目侧)
UI 系统的接入文件建议按“路由、工厂、根节点、页面行为、入参”拆开。这样可以让 `UiRouterBase` 只承担编排职责,
把引擎节点创建和页面业务逻辑留在项目侧。
```text
Game/UI/
GameUiRouter.cs
GameUiFactory.cs
UiRoot.cs
Pages/
MainMenuPageBehavior.cs
SettingsPageBehavior.cs
Params/
SettingsEnterParam.cs
Views/
MainMenuView.cs
```
推荐约定如下:
- `GameUiRouter.cs`:项目侧 router继承 `UiRouterBase`,只注册 UI transition handler 与 guard
- `GameUiFactory.cs`:实现 `IUiFactory`,负责 `uiKey -> IUiPageBehavior` 的映射与实例创建
- `UiRoot.cs`:实现 `IUiRoot`,负责按 `UiLayer` 把页面行为挂到真实 UI 容器
- `Pages/*PageBehavior.cs`:放实现 `IUiPageBehavior` 的页面行为;使用 Godot 生成器时可由 `AutoUiPage` 相关样板补齐
- `Params/*EnterParam.cs`:放实现 `IUiPageEnterParam` 的页面入参
- `Views/*`:放项目引擎层视图包装或节点引用,不建议把导航决策写在视图里
最小 wiring 通常是:
`IUiPage` 定义了 UI 页面的生命周期:
```csharp
architecture.RegisterUtility<IUiFactory>(new GameUiFactory());
architecture.RegisterSystem(new GameUiRouter());
```
随后在 `UiRoot` 的引擎生命周期就绪点调用 `_uiRouter.BindRoot(this)`。如果项目已经按功能域组织 UI 文件,也可以保留
原目录;关键是让 `*Router` 只做编排、`*Factory` 只做映射与创建、`*Root` 只做容器挂载,页面行为只表达页面自身语义。
### 1. 提供项目自己的 router
```csharp
using GFramework.Game.UI;
using LoggingTransitionHandler = GFramework.Game.UI.Handler.LoggingTransitionHandler;
public sealed class GameUiRouter : UiRouterBase
public interface IUiPage
{
protected override void RegisterHandlers()
void OnEnter(IUiPageEnterParam? param); // 进入页面
void OnExit(); // 退出页面
void OnPause(); // 暂停页面
void OnResume(); // 恢复页面
void OnShow(); // 显示页面
void OnHide(); // 隐藏页面
}
```
### UI 路由
`IUiRouter` 管理 UI 的导航和切换:
```csharp
public interface IUiRouter : ISystem
{
int Count { get; } // UI 栈深度
IUiPageBehavior? Peek(); // 栈顶 UI
ValueTask PushAsync(string uiKey, IUiPageEnterParam? param = null);
ValueTask PopAsync(UiPopPolicy policy = UiPopPolicy.Destroy);
ValueTask ReplaceAsync(string uiKey, IUiPageEnterParam? param = null);
ValueTask ClearAsync();
}
```
### UI 层级
UI 系统支持 5 个显示层级:
```csharp
public enum UiLayer
{
Page, // 页面层(栈管理,不可重入)
Overlay, // 浮层(可重入,对话框等)
Modal, // 模态层(可重入,带遮罩)
Toast, // 提示层(可重入,轻量提示)
Topmost // 顶层(不可重入,系统级)
}
```
## 基本用法
### 定义 UI 页面
实现 `IUiPage` 接口创建 UI 页面:
```csharp
using GFramework.Game.Abstractions.UI;
public class MainMenuPage : IUiPage
{
public void OnEnter(IUiPageEnterParam? param)
{
RegisterHandler(new LoggingTransitionHandler());
Console.WriteLine("进入主菜单");
// 初始化 UI、绑定事件
}
public void OnExit()
{
Console.WriteLine("退出主菜单");
// 清理资源、解绑事件
}
public void OnPause()
{
Console.WriteLine("暂停主菜单");
// 暂停动画、停止交互
}
public void OnResume()
{
Console.WriteLine("恢复主菜单");
// 恢复动画、启用交互
}
public void OnShow()
{
Console.WriteLine("显示主菜单");
// 显示 UI 元素
}
public void OnHide()
{
Console.WriteLine("隐藏主菜单");
// 隐藏 UI 元素
}
}
```
### 2. 提供 `IUiFactory`
### 切换 UI 页面
`UiRouterBase` 会通过 `IUiFactory.Create(string uiKey)` 获取页面行为实例,因此项目需要自己决定:
- `uiKey` 如何映射到页面行为
- 页面行为如何包裹具体引擎视图
- 预挂载节点、调试节点或动态实例化页面如何接入
如果你在 Godot 项目里使用 `AutoUiPage` 相关生成器,它可以帮你减少部分行为样板,但 factory / root / 实际页面注册仍然是项目职责。
### 3. 提供 `IUiRoot`
`IUiRoot` 负责把页面行为挂进真实 UI 容器:
- `AddUiPage(IUiPageBehavior child)`
- `AddUiPage(IUiPageBehavior child, UiLayer layer, int orderInLayer = 0)`
- `RemoveUiPage(IUiPageBehavior child)`
当前 `ai-libs/` 的参考实现就是在项目自己的 `CanvasLayer` 上为每个 `UiLayer` 建独立容器,再在 `_Ready()` 时执行
`_uiRouter.BindRoot(this)`
### 4. 装配 router 与 factory
使用 UI 路由进行导航:
```csharp
architecture.RegisterUtility<IUiFactory>(new GameUiFactory());
architecture.RegisterSystem(new GameUiRouter());
```
using GFramework.Core.Abstractions.Controller;
using GFramework.Core.SourceGenerators.Abstractions.Rule;
### 5. 在 root 就绪后绑定
```csharp
public sealed class UiRoot : CanvasLayer, IUiRoot
[ContextAware]
public partial class UiController : IController
{
[GetSystem] private IUiRouter _uiRouter = null!;
public override void _Ready()
public async Task ShowSettings()
{
__InjectContextBindings_Generated();
_uiRouter.BindRoot(this);
var uiRouter = this.GetSystem<IUiRouter>();
// 压入设置页面(保留当前页面)
await uiRouter.PushAsync("Settings");
}
public void AddUiPage(IUiPageBehavior child)
public async Task CloseSettings()
{
AddUiPage(child, UiLayer.Page);
var uiRouter = this.GetSystem<IUiRouter>();
// 弹出当前页面(返回上一页)
await uiRouter.PopAsync();
}
public void AddUiPage(IUiPageBehavior child, UiLayer layer, int orderInLayer = 0)
public async Task ShowMainMenu()
{
// 项目侧决定如何把 child.View 挂到具体容器
}
var uiRouter = this.GetSystem<IUiRouter>();
public void RemoveUiPage(IUiPageBehavior child)
{
// 项目侧决定如何移除并释放视图
// 替换所有页面(清空 UI 栈)
await uiRouter.ReplaceAsync("MainMenu");
}
}
```
### 6. 从业务代码区分两类入口
页面栈:
### 显示不同层级的 UI
```csharp
await uiRouter.ReplaceAsync("MainMenu");
await uiRouter.PushAsync("Settings", new SettingsEnterParam());
await uiRouter.PopAsync(UiPopPolicy.Destroy);
[ContextAware]
public partial class UiController : IController
{
public void ShowDialog()
{
var uiRouter = this.GetSystem<IUiRouter>();
// 在 Modal 层显示对话框
var handle = uiRouter.Show("ConfirmDialog", UiLayer.Modal);
}
public void ShowToast(string message)
{
var uiRouter = this.GetSystem<IUiRouter>();
// 在 Toast 层显示提示
var handle = uiRouter.Show("ToastMessage", UiLayer.Toast,
new ToastParam { Message = message });
}
public void ShowLoading()
{
var uiRouter = this.GetSystem<IUiRouter>();
// 在 Topmost 层显示加载界面
var handle = uiRouter.Show("LoadingScreen", UiLayer.Topmost);
}
}
```
层级 UI
## 高级用法
### UI 参数传递
```csharp
var modalHandle = uiRouter.Show(
"ConfirmExit",
UiLayer.Modal,
new ConfirmExitParam());
// 定义 UI 参数
public class SettingsEnterParam : IUiPageEnterParam
{
public string Category { get; set; }
}
uiRouter.Hide(modalHandle, UiLayer.Modal);
// 在 UI 中接收参数
public class SettingsPage : IUiPage
{
private string _category;
public void OnEnter(IUiPageEnterParam? param)
{
if (param is SettingsEnterParam settingsParam)
{
_category = settingsParam.Category;
Console.WriteLine($"打开设置分类: {_category}");
}
}
// ... 其他生命周期方法
}
// 传递参数
await uiRouter.PushAsync("Settings", new SettingsEnterParam
{
Category = "Audio"
});
```
## 扩展点
### 路由守卫
如果你要在进入或离开页面前做业务检查,实现 `IUiRouteGuard`
```csharp
using GFramework.Game.Abstractions.UI;
- `CanEnterAsync(string uiKey, IUiPageEnterParam? param)`
- `CanLeaveAsync(string uiKey)`
public class UnsavedChangesGuard : IUiRouteGuard
{
public async ValueTask<bool> CanLeaveAsync(
IUiPageBehavior from,
string toKey,
IUiPageEnterParam? param)
{
// 检查是否有未保存的更改
if (from.Key == "Settings" && HasUnsavedChanges())
{
var confirmed = await ShowConfirmDialog();
return confirmed;
}
适合放:
return true;
}
- 未保存设置拦截
- 新手引导期间禁用某些页面跳转
- 多层弹窗切换前的业务确认
public async ValueTask<bool> CanEnterAsync(
string toKey,
IUiPageEnterParam? param)
{
// 进入前的验证
return true;
}
### 过渡处理器
private bool HasUnsavedChanges() => true;
private async Task<bool> ShowConfirmDialog() => await Task.FromResult(true);
}
`IUiRouter` 当前公开的是:
// 注册守卫
uiRouter.AddGuard(new UnsavedChangesGuard());
```
- `RegisterHandler(IUiTransitionHandler handler, UiTransitionHandlerOptions? options = null)`
- `UnregisterHandler(IUiTransitionHandler handler)`
### UI 转换处理器
适合放:
```csharp
using GFramework.Game.Abstractions.UI;
- UI 转场动画
- 统一日志
- 栈变化埋点
public class FadeTransitionHandler : IUiTransitionHandler
{
public async ValueTask OnBeforeEnterAsync(UiTransitionEvent @event)
{
Console.WriteLine($"准备进入 UI: {@event.ToKey}");
await PlayFadeIn();
}
### 输入适配层
public async ValueTask OnAfterEnterAsync(UiTransitionEvent @event)
{
Console.WriteLine($"已进入 UI: {@event.ToKey}");
}
如果项目已经有自己的输入系统,推荐把它适配成:
public async ValueTask OnBeforeExitAsync(UiTransitionEvent @event)
{
Console.WriteLine($"准备退出 UI: {@event.FromKey}");
await PlayFadeOut();
}
1. 设备输入 -> `UiInputAction`
2. `IUiRouter.TryDispatchUiAction(...)`
3. 若未被 UI 捕获,再决定是否把输入继续交给 World
public async ValueTask OnAfterExitAsync(UiTransitionEvent @event)
{
Console.WriteLine($"已退出 UI: {@event.FromKey}");
}
这样可以直接复用当前路由器的动作捕获与阻断语义。
private async Task PlayFadeIn() => await Task.Delay(200);
private async Task PlayFadeOut() => await Task.Delay(200);
}
## 与旧写法的边界
// 注册转换处理器
uiRouter.RegisterHandler(new FadeTransitionHandler());
```
以下说法不再适合作为默认指导:
### UI 句柄管理
- “所有 UI 都统一通过一个 Show API 管理”
- “UI 系统只有页面栈,不涉及输入阻断和暂停语义”
- “Modal / Topmost 只是视觉层级,不影响交互”
```csharp
using GFramework.Core.Abstractions.Controller;
using GFramework.Core.SourceGenerators.Abstractions.Rule;
当前更准确的理解是:
[ContextAware]
public partial class DialogController : IController
{
private UiHandle? _dialogHandle;
- 页面栈和层级 UI 是两套入口
- 页面行为不仅有生命周期,还有输入、阻断、暂停契约
- router 是 UI 语义仲裁中心,项目输入层应主动接入它
public void ShowDialog()
{
var uiRouter = this.GetSystem<IUiRouter>();
## 推荐阅读
// 显示对话框并保存句柄
_dialogHandle = uiRouter.Show("ConfirmDialog", UiLayer.Modal);
}
1. [game/index.md](./index.md)
2. [scene.md](./scene.md)
3. [../source-generators/auto-ui-page-generator.md](../source-generators/auto-ui-page-generator.md)
4. `GFramework.Game/README.md`
5. `GFramework.Game.Abstractions/README.md`
public void CloseDialog()
{
if (_dialogHandle.HasValue)
{
var uiRouter = this.GetSystem<IUiRouter>();
// 使用句柄关闭对话框
uiRouter.Hide(_dialogHandle.Value, UiLayer.Modal, destroy: true);
_dialogHandle = null;
}
}
}
```
### UI 栈管理
```csharp
using GFramework.Core.Abstractions.Controller;
using GFramework.Core.SourceGenerators.Abstractions.Rule;
[ContextAware]
public partial class NavigationController : IController
{
public void ShowUiStack()
{
var uiRouter = this.GetSystem<IUiRouter>();
Console.WriteLine($"UI 栈深度: {uiRouter.Count}");
var current = uiRouter.Peek();
if (current != null)
{
Console.WriteLine($"当前 UI: {current.Key}");
}
}
public bool IsSettingsOpen()
{
var uiRouter = this.GetSystem<IUiRouter>();
return uiRouter.Contains("Settings");
}
public bool IsTopPage(string uiKey)
{
var uiRouter = this.GetSystem<IUiRouter>();
return uiRouter.IsTop(uiKey);
}
}
```
### 多层级 UI 管理
```csharp
using GFramework.Core.Abstractions.Controller;
using GFramework.Core.SourceGenerators.Abstractions.Rule;
[ContextAware]
public partial class LayerController : IController
{
public void ShowMultipleToasts()
{
var uiRouter = this.GetSystem<IUiRouter>();
// Toast 层支持重入,可以同时显示多个
uiRouter.Show("Toast1", UiLayer.Toast);
uiRouter.Show("Toast2", UiLayer.Toast);
uiRouter.Show("Toast3", UiLayer.Toast);
}
public void ClearAllToasts()
{
var uiRouter = this.GetSystem<IUiRouter>();
// 清空 Toast 层的所有 UI
uiRouter.ClearLayer(UiLayer.Toast, destroy: true);
}
public void HideAllDialogs()
{
var uiRouter = this.GetSystem<IUiRouter>();
// 隐藏 Modal 层的所有对话框
uiRouter.HideByKey("ConfirmDialog", UiLayer.Modal, hideAll: true);
}
}
```
## 最佳实践
1. **使用合适的层级**:根据 UI 类型选择正确的层级
```csharp
✓ Page: 主要页面(主菜单、设置、游戏界面)
✓ Overlay: 浮层(信息面板、小窗口)
✓ Modal: 模态对话框(确认框、输入框)
✓ Toast: 轻量提示(消息、通知)
✓ Topmost: 系统级(加载界面、全屏遮罩)
```
2. **使用 Push/Pop 管理临时 UI**:如设置、帮助页面
```csharp
// 打开设置(保留当前页面)
await uiRouter.PushAsync("Settings");
// 关闭设置(返回上一页)
await uiRouter.PopAsync();
```
3. **使用 Replace 切换主要页面**:如从菜单到游戏
```csharp
// 开始游戏(清空 UI 栈)
await uiRouter.ReplaceAsync("Gameplay");
```
4. **在 OnEnter/OnExit 中管理资源**:保持资源管理清晰
```csharp
public void OnEnter(IUiPageEnterParam? param)
{
// 加载资源、绑定事件
BindEvents();
}
public void OnExit()
{
// 清理资源、解绑事件
UnbindEvents();
}
```
5. **使用句柄管理非栈 UI**:对于 Overlay、Modal、Toast 层
```csharp
// 保存句柄
var handle = uiRouter.Show("Dialog", UiLayer.Modal);
// 使用句柄关闭
uiRouter.Hide(handle, UiLayer.Modal, destroy: true);
```
6. **避免在 UI 切换时阻塞**:使用异步操作
```csharp
✓ await uiRouter.PushAsync("Settings");
✗ uiRouter.PushAsync("Settings").Wait(); // 可能死锁
```
## 常见问题
### 问题Push、Pop、Replace 有什么区别?
**解答**
- **Push**:压入新 UI暂停当前 UI用于临时页面
- **Pop**:弹出当前 UI恢复上一个 UI用于关闭临时页面
- **Replace**:清空 UI 栈,加载新 UI用于主要页面切换
### 问题:什么时候使用不同的 UI 层级?
**解答**
- **Page**:主要页面,使用栈管理
- **Overlay**:浮层,可叠加显示
- **Modal**:模态对话框,阻挡下层交互
- **Toast**:轻量提示,不阻挡交互
- **Topmost**:系统级,最高优先级
### 问题:如何在 UI 之间传递数据?
**解答**
1. 通过 UI 参数
2. 通过 Model
3. 通过事件
### 问题UI 切换时如何显示过渡动画?
**解答**
使用 UI 转换处理器在 `OnBeforeEnter`/`OnAfterExit` 中播放动画。
### 问题:如何防止用户在 UI 切换时操作?
**解答**
在转换处理器中显示遮罩或禁用输入。
## 相关文档
- [场景系统](/zh-CN/game/scene) - 场景管理
- [Godot UI 系统](/zh-CN/godot/ui) - Godot 引擎集成
- [事件系统](/zh-CN/core/events) - UI 事件通信
- [状态机系统](/zh-CN/core/state-machine) - UI 状态管理

View File

@ -1,176 +1,612 @@
---
title: Godot 架构集成
description: 说明 AbstractArchitecture、ArchitectureAnchor 和 Godot 模块挂接的当前生命周期语义,避免继续沿用旧版 `.Wait()` 接法
description: Godot 架构集成提供了 GFramework 与 Godot 引擎的无缝连接,实现生命周期同步和模块化开发
---
# Godot 架构集成
## 概述
`GFramework.Godot` 当前的架构集成目标很直接:让 `Architecture` 能安全地感知 Godot `SceneTree` 生命周期,并在需要时把
`Node` 的扩展模块挂到场景树上
Godot 架构集成是 GFramework.Godot 中连接框架与 Godot 引擎的核心组件。它提供了架构与 Godot 场景树的生命周期绑定、模块化扩展系统,以及与
Godot 节点系统的深度集成
当前真正参与这条链路的核心类型只有三类:
通过 Godot 架构集成,你可以在 Godot 项目中使用 GFramework 的所有功能,同时保持与 Godot 引擎的完美兼容。
- `AbstractArchitecture`:在原有 `Architecture` 之上增加 Godot 生命周期绑定
- `ArchitectureAnchor`:挂在 `SceneTree.Root` 下的锚点节点,负责把 `_ExitTree()` 事件转回架构销毁
- `IGodotModule` / `AbstractGodotModule`:当模块本身需要携带 Godot `Node` 时使用
**主要特性**
它不是另一套独立的模块系统,也不意味着所有模块都必须改成 `InstallGodotModule(...)`
- 架构与 Godot 生命周期自动同步
- 模块化的 Godot 扩展系统
- 架构锚点节点管理
- 自动资源清理
- 热重载支持
- 与 Godot 场景树深度集成
## 什么时候该用 `AbstractArchitecture`
## 核心概念
当你的架构需要满足下面任一条件时,可以让它继承 `AbstractArchitecture`
### 抽象架构
- 需要把架构生命周期绑定到 Godot `SceneTree`
- 需要在架构里安装带 `Node` 的扩展模块
- 需要通过受保护的 `ArchitectureRoot` 访问锚点节点,继续挂接 Godot 子节点
如果你只是做普通的 Model / System / Utility 注册,`AbstractArchitecture` 的主要价值仍然是“让架构知道自己何时跟随
Godot 场景树销毁”,而不是改变注册方式。
## 最小接入路径
### 常规模块仍然用 `InstallModule(...)`
当前消费者 `ai-libs/CoreGrid` 的默认做法,是保持普通模块注册方式:
`AbstractArchitecture` 是 Godot 项目中架构的基类:
```csharp
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)
public abstract class AbstractArchitecture : Architecture
{
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()
{
InstallModule(new UtilityModule());
InstallModule(new ModelModule());
InstallModule(new GameplayModule());
InstallModule(new SystemModule());
// 注册 Model
RegisterModel(new PlayerModel());
RegisterModel(new GameModel());
// 注册 System
RegisterSystem(new GameplaySystem());
RegisterSystem(new AudioSystem());
// 注册 Utility
RegisterUtility(new StorageUtility());
}
}
```
这里继承 `AbstractArchitecture` 的意义,是把架构绑定到 Godot 生命周期,而不是把普通模块注册改写成 Godot 风格 API。
### 只有携带 `Node` 的模块才需要 `InstallGodotModule(...)`
如果模块本身暴露一个 Godot `Node`,并且希望由架构锚点统一托管,可以这样写:
### 在 Godot 场景中初始化架构
```csharp
using GFramework.Core.Abstractions.Architectures;
using GFramework.Godot.Architectures;
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 Godot;
namespace MyGame.Scripts.Core;
public sealed class HudModule : AbstractGodotModule
public class CoroutineModule : AbstractGodotModule
{
private readonly Control _root = new()
{
Name = "HudModule"
};
private Node _coroutineNode;
public override Node Node => _root;
public override Node Node => _coroutineNode;
public CoroutineModule()
{
_coroutineNode = new Node { Name = "CoroutineScheduler" };
}
public override void Install(IArchitecture architecture)
{
// 注册协程调度器
var scheduler = new CoroutineScheduler(new GodotTimeSource());
architecture.RegisterSystem<ICoroutineScheduler>(scheduler);
GD.Print("协程模块已安装");
}
public override void OnAttach(GFramework.Core.Architectures.Architecture architecture)
public override void OnPhase(ArchitecturePhase phase, IArchitecture architecture)
{
if (phase == ArchitecturePhase.Ready)
{
GD.Print("协程模块已就绪");
}
}
public override void OnDetach()
{
_root.QueueFree();
GD.Print("协程模块已分离");
_coroutineNode?.QueueFree();
}
}
```
这类模块的关键点不是“注册更多框架能力”,而是“让模块节点跟着架构锚点进出场景树”。
真正调用 `InstallGodotModule(...)` 时,也应该把它放在能够接受异步挂接流程的初始化路径里,而不是继续沿用旧文档里的
`.Wait()` 叙述。
### 安装 Godot 模块
## 当前生命周期
```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()` 目前会按这个顺序工作:
### 访问架构根节点
1. 生成唯一的锚点节点名称
2. 调用 `AttachToGodotLifecycle()`
3. 在可用的 `SceneTree` 上创建并绑定 `ArchitectureAnchor`
4. 执行你重写的 `InstallModules()`
```csharp
public class SceneModule : AbstractGodotModule
{
private Node _sceneRoot;
也就是说Godot 生命周期绑定先发生,业务模块注册后发生。
public override Node Node => _sceneRoot;
### `InstallGodotModule(...)` 的执行顺序
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);
}
}
}
```
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;
- 模块会在挂接节点前先完成框架侧注册
- 只有等锚点真正 ready 后,才进入需要访问 Godot 节点 API 的附加阶段
public override Node Node => _analyticsNode;
### 销毁阶段
public AnalyticsModule()
{
_analyticsNode = new Node { Name = "Analytics" };
}
`ArchitectureAnchor._ExitTree()` 会触发绑定好的退出回调,随后 `AbstractArchitecture` 会开始观察异步销毁流程:
public override void Install(IArchitecture architecture)
{
// 安装分析系统
}
- 防止重复销毁
- 依次调用已登记 Godot 模块的 `OnDetach()`
- 清空内部扩展列表
- 再进入基类 `DestroyAsync()`
public override void OnPhase(ArchitecturePhase phase, IArchitecture architecture)
{
switch (phase)
{
case ArchitecturePhase.Initializing:
GD.Print("架构正在初始化");
break;
如果异步销毁抛异常,当前实现会把错误写到 Godot 错误输出,而不是静默吞掉。
case ArchitecturePhase.Ready:
GD.Print("架构已就绪,开始追踪");
StartTracking();
break;
## 当前边界
case ArchitecturePhase.Destroying:
GD.Prin构正在销毁停止追踪");
StopTracking();
break;
}
}
### 没有锚点时不会偷偷安装模块
private void StartTracking() { }
private void StopTracking() { }
}
```
`GFramework.Godot.Tests/Architectures/AbstractArchitectureModuleInstallationTests.cs` 已覆盖一个关键边界:
### 自定义架构配置
- 当锚点尚未初始化时,`InstallGodotModule(...)` 会直接抛 `InvalidOperationException("Anchor not initialized")`
- 失败发生在 `module.Install(...)` 之前,因此不会留下半安装副作用
```csharp
using GFramework.Core.Abstractions.Architecture;
using GFramework.Core.Abstractions.Environment;
这也是为什么文档不应该再把 `InstallGodotModule(...).Wait()` 写成一种随处可用的默认初始化方式。
public class GameArchitecture : AbstractArchitecture
{
public GameArchitecture() : base(
configuration: CreateConfiguration(),
environment: CreateEnvironment()
)
{
}
### `AbstractGodotModule` 只是便捷基类,不代表自动阶段广播
private static IArchitectureConfiguration CreateConfiguration()
{
return new ArchitectureConfiguration
{
EnableLogging
LogLevel = LogLevel.Debug
};
}
当前接口 `IGodotModule` 真正保证的成员只有:
private static IEnvironment CreateEnvironment()
{
return new DefaultEnvironment
{
IsDevelopment = OS.IsDebugBuild()
};
}
- `Node`
- `Install(IArchitecture architecture)`
- `OnAttach(Architecture architecture)`
- `OnDetach()`
protected override void InstallModules()
{
// 根据环境配置安装模块
if (Environment.IsDevelopment)
{
InstallGodotModule(new DebugModule()).Wait();
}
`AbstractGodotModule` 里虽然保留了 `OnPhase(...)` / `OnArchitecturePhase(...)` 虚方法,但它们不在当前接口契约内,也没有在
这条挂接流程里形成稳定的自动广播语义。不要把它写成当前公开保证。
// 安装核心模块
RegisterModel(new PlayerModel());
RegisterSystem(new GameplaySystem());
}
}
```
### `ArchitectureRoot` 只在锚点就绪后可用
### 热重载支持
`ArchitectureRoot` 是受保护属性,底层直接返回 `_anchor`。如果锚点尚未准备好或架构已经失效,它会抛
`InvalidOperationException("Architecture root not ready")`。因此它适合放在明确依赖锚点存在的挂接逻辑里,而不是拿来做
任意时机的全局节点查找。
```csharp
public class GameArchitecture : AbstractArchitecture
{
private static bool _initialized;
## 继续阅读
protected override void OnInitialize()
{
// 防止热重载时重复初始化
if (_initialized)
{
GD.Print("架构已初始化,跳过重复初始化");
return;
}
1. [Godot 运行时集成](./index.md)
2. [Godot 集成教程](../tutorials/godot-integration.md)
3. [Godot 场景系统](./scene.md)
4. [Godot UI 系统](./ui.md)
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 扩展方法

View File

@ -1,181 +1,328 @@
---
title: Godot 扩展方法
description: 以当前 GFramework.Godot.Extensions 源码为准说明路径、Node、signal 和 unregister 扩展的真实成员与边界。
---
# Godot 扩展方法 (Godot Extensions)
# Godot 扩展方法
## 概述
`GFramework.Godot.Extensions` 当前并不是“覆盖所有 Godot 节点操作”的万能层。按源码看,它实际公开的扩展主要只有四组:
Godot 扩展方法模块为 Godot 引擎提供了丰富的便捷扩展方法集合。这些扩展方法简化了常见的 Godot
开发任务,提高了代码的可读性和开发效率。该模块遵循流畅接口设计原则,支持链式调用。
- `GodotPathExtensions`
- `NodeExtensions`
- `SignalFluentExtensions`
- `UnRegisterExtension`
## 模块结构
这页的重点应该是识别这些扩展各自解决什么问题,以及哪些旧文档里的“大而全能力”现在并不存在。
## 当前公开入口
### `GodotPathExtensions`
这组扩展只负责判断 Godot 虚拟路径前缀:
- `IsUserPath(this string path)`
- `IsResPath(this string path)`
- `IsGodotPath(this string path)`
它们不做文件访问,也不解析目录结构,只是用字符串前缀判断 `user://``res://`
```csharp
using GFramework.Godot.Extensions;
if ("user://save.json".IsUserPath())
{
}
if ("res://config/gameplay.yaml".IsGodotPath())
{
}
```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[事件管理]
```
### `NodeExtensions`
## 扩展模块详解
`NodeExtensions` 是当前扩展集合里体量最大的部分,但职责仍然比较具体,主要分成下面几类。
### 1. 路径扩展 (GodotPathExtensions)
#### 生命周期与有效性辅助
提供 Godot 虚拟路径的判断和识别功能。
- `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)`
**主要方法:**
这里最容易写偏的地方有两个:
- `IsUserPath()` - 判断是否为 `user://` 路径
- `IsResPath()` - 判断是否为 `res://` 路径
- `IsGodotPath()` - 判断是否为 Godot 虚拟路径
- `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);
string savePath = "user://save.dat";
string configPath = "res://config.json";
string logPath = "C:/logs/debug.log";
if (savePath.IsUserPath()) Console.WriteLine("用户数据路径");
if (configPath.IsResPath()) Console.WriteLine("资源路径");
if (logPath.IsGodotPath()) Console.WriteLine("Godot 虚拟路径");
else Console.WriteLine("文件系统路径");
```
它不会接管普通 Godot signal 的断开逻辑,也不会帮你推断别的释放时机。
### 2. 节点扩展 (NodeExtensions)
## 最小接入路径
最丰富的扩展模块,提供全面的节点操作功能。
### 1. 节点进入树之后再做装配
如果你的节点可能在 `_Ready()` 前就被访问,先用 `WaitUntilReadyAsync()`
#### 节点生命周期管理
```csharp
using GFramework.Godot.Extensions;
using GFramework.Godot.Extensions.Signal;
using Godot;
// 安全释放节点
node.QueueFreeX(); // 延迟释放
node.FreeX(); // 立即释放
public partial class SettingsPanel : Control
// 等待节点就绪
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
{
public override async void _Ready()
private Node _uiRoot;
public override void _Ready()
{
await this.WaitUntilReadyAsync();
var applyButton = FindChildX<Button>("ApplyButton");
applyButton?.Signal(Button.SignalName.Pressed)
.To(Callable.From(OnApplyPressed));
_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;
}
private void OnApplyPressed()
public void Cleanup()
{
this.SetInputAsHandled();
// 安全释放所有子节点
ForEachChild<Node>(child => child.QueueFreeX());
}
}
```
### 2. 框架订阅和节点生命周期一起收尾
当订阅句柄实现了 `IUnRegister`,可以把释放时机绑到节点退出树:
#### UI 事件处理
```csharp
public override void _Ready()
public class MainMenu : Control
{
IUnRegister subscription = _eventBus.Subscribe<SettingsChangedEvent>(OnSettingsChanged);
subscription.UnRegisterWhenNodeExitTree(this);
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();
}
}
```
这比在多个 `_ExitTree()` / `Dispose()` 分支里手写解绑更稳定,也更符合当前扩展的职责边界。
#### 异步场景管理
### 3. 只在需要时使用 signal fluent API
```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();
}
}
```
`Signal(...)` 属于扩展集合的一部分,但它已经有独立页面。实践上可以这样分工:
## 设计原则
- 节点查找、ready 等待、输入处理:`NodeExtensions`
- 动态 signal 绑定:`Signal(...)`
- 框架订阅释放:`UnRegisterWhenNodeExitTree(...)`
- 路径前缀判断:`GodotPathExtensions`
### 1. 安全性
## 当前边界
- 所有节点操作都包含有效性检查
- 提供安全的类型转换方法
- 避免空引用异常
- 当前 `NodeExtensions` 没有 `GetNodeX()``CreateSignalBuilder()` 之类旧文档里提过的 API
- 它不是 router、scene factory、UI factory 或生成器的替代层
- `GetOrCreateNode<T>()` 只会创建一个直接子节点,不会递归补整条路径
- `SafeCallDeferred(...)` 只有在 `IsValidNode()``true` 时才会调用;节点未入树时不会执行
- `UnRegisterWhenNodeExitTree(...)` 只针对实现了 `IUnRegister` 的框架订阅句柄,不会自动处理 Godot 原生 `Connect(...)`
- 协程辅助扩展在 `GFramework.Godot.Coroutine` 命名空间,不属于这组 `Extensions` 页面要覆盖的核心范围
### 2. 便利性
## 继续阅读
- 流畅的 API 设计
- 支持链式调用
- 减少样板代码
- [Godot 运行时集成](./index.md)
- [Godot 信号系统](./signal.md)
- [Godot 场景系统](./scene.md)
- [Godot UI 系统](./ui.md)
### 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);
// ❌ 避免:手动管理事件生命周期
// 可能导致内存泄漏
```

File diff suppressed because it is too large Load Diff

View File

@ -1,40 +1,43 @@
---
title: Godot 日志系统
description: 以当前 GFramework.Godot.Logging 源码与 CoreGrid 接线为准,说明 Godot 日志 provider、控制台输出语义与接入边界
description: Godot 日志系统提供了 GFramework 日志功能与 Godot 引擎控制台的完整集成
---
# Godot 日志系统
`GFramework.Godot` 当前的日志能力很收敛:它不是一套独立于 Core 的新日志框架,而是把现有 `ILogger` 调用面接到
Godot 控制台。
## 概述
换句话说Godot 侧真正新增的是 provider / factory / logger 这层输出适配,而不是新的日志 API。业务代码仍然继续使用
`LoggerFactoryResolver.Provider.CreateLogger(...)``[Log]` 生成的 `ILogger` 字段
Godot 日志系统是 GFramework.Godot 中连接框架日志功能与 Godot 引擎控制台的核心组件。它提供了与 Godot
控制台的深度集成,支持彩色输出、多级别日志记录,以及与 GFramework 日志系统的无缝对接
## 当前公开入口
通过 Godot 日志系统,你可以在 Godot 项目中使用统一的日志接口,日志会自动输出到 Godot 编辑器控制台,并根据日志级别使用不同的颜色和输出方式。
### `GodotLogger`
**主要特性**
`GodotLogger` 继承自 `AbstractLogger`,负责把日志写到 Godot 的输出 API
- 与 Godot 控制台深度集成
- 支持彩色日志输出
- 多级别日志记录Trace、Debug、Info、Warning、Error、Fatal
- 日志缓存机制
- 时间戳和格式化支持
- 异常信息记录
## 核心概念
### GodotLogger
`GodotLogger` 是 Godot 平台的日志记录器实现,继承自 `AbstractLogger`
```csharp
public sealed class GodotLogger(
string? name = null,
LogLevel minLevel = LogLevel.Info)
: AbstractLogger(name ?? RootLoggerName, minLevel)
public sealed class GodotLogger : AbstractLogger
{
public GodotLogger(string? name = null, LogLevel minLevel = LogLevel.Info);
protected override void Write(LogLevel level, string message, Exception? exception);
}
```
当前实现里的几个关键语义:
### GodotLoggerFactory
- 时间戳使用 `DateTime.UtcNow`
- 输出前缀格式是 `[yyyy-MM-dd HH:mm:ss.fff] LEVEL [LoggerName]`
- `exception` 不会被单独结构化处理,而是直接追加到消息后面
- `Trace` / `Debug``GD.PrintRich(...)`
- `Info` / `Warning` / `Error` / `Fatal` 分别走 Godot 自身的普通、警告和错误输出通道
### `GodotLoggerFactory`
`GodotLoggerFactory` 只负责按名称和最小级别创建 `GodotLogger`
`GodotLoggerFactory` 用于创建 Godot 日志记录器实例:
```csharp
public class GodotLoggerFactory : ILoggerFactory
@ -43,11 +46,9 @@ public class GodotLoggerFactory : ILoggerFactory
}
```
它本身不做缓存,也不额外增加过滤规则。
### GodotLoggerFactoryProvider
### `GodotLoggerFactoryProvider`
`GodotLoggerFactoryProvider` 是当前最常用的接入点:
`GodotLoggerFactoryProvider` 提供日志工厂实例,并支持日志缓存:
```csharp
public sealed class GodotLoggerFactoryProvider : ILoggerFactoryProvider
@ -57,144 +58,571 @@ public sealed class GodotLoggerFactoryProvider : ILoggerFactoryProvider
}
```
它内部用 `CachedLoggerFactory` 包装 `GodotLoggerFactory`。缓存 key 由 `name``MinLevel` 共同组成,所以:
## 基本用法
- 同名、同 `MinLevel` 的 logger 会复用实例
- 调整 `MinLevel` 后,新创建的 logger 会走新的缓存 key
- 已经持有的旧 logger 不会被原地改写
### 配置 Godot 日志系统
## 最小接入路径
### 1. 在 `ArchitectureConfiguration` 中挂上 Godot provider
当前仓库里更稳的接法,不是到处直接改全局 `LoggerFactoryResolver.Provider`,而是在架构配置里显式提供
`LoggerProperties.LoggerFactoryProvider``ai-libs/CoreGrid/global/GameEntryPoint.cs` 现在就是这样接的。
在架构初始化时配置日志提供程序:
```csharp
using GFramework.Core.Abstractions.Environment;
using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Abstractions.Properties;
using GFramework.Core.Architectures;
using GFramework.Godot.Architecture;
using GFramework.Godot.Logging;
var architecture = new GameArchitecture(
new ArchitectureConfiguration
{
LoggerProperties = new LoggerProperties
{
LoggerFactoryProvider = new GodotLoggerFactoryProvider
{
MinLevel = LogLevel.Debug
}
}
},
environment);
architecture.Initialize();
```
这样做的好处是:
- 日志 provider 和架构启动配置放在同一个入口
- 不会把“Godot 控制台输出”误写成全局静态默认前提
- 和 `ArchitectureConfiguration` 默认使用 `ConsoleLoggerFactoryProvider` 的 Core 接线方式保持一致
### 2. 业务代码继续使用标准 `ILogger`
配置好 provider 之后Godot 节点、System、Model、router、factory 都继续通过统一入口拿 logger
```csharp
using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Logging;
using Godot;
using GFramework.Core.Abstractions.Logging;
public partial class SettingsPanel : Control
public class GameArchitecture : AbstractArchitecture
{
private static readonly ILogger Log =
LoggerFactoryResolver.Provider.CreateLogger(nameof(SettingsPanel));
public static GameArchitecture Interface { get; private set; }
public override void _Ready()
public GameArchitecture()
{
Log.Info("SettingsPanel ready.");
Interface = this;
// 配置 Godot 日志系统
LoggerFactoryResolver.Provider = new GodotLoggerFactoryProvider
{
MinLevel = LogLevel.Debug // 设置最小日志级别
};
}
protected override void InstallModules()
{
var logger = LoggerFactoryResolver.Provider.CreateLogger("GameArchitecture");
logger.Info("游戏架构初始化开始");
RegisterModel(new PlayerModel());
RegisterSystem(new GameplaySystem());
logger.Info("游戏架构初始化完成");
}
}
```
如果你已经在用 `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
using GFramework.Game.Scene.Handler;
using GFramework.Game.UI.Handler;
using Godot;
using GFramework.Core.Logging;
using GFramework.Core.Abstractions.Logging;
RegisterHandler(new LoggingTransitionHandler());
public partial class Player : CharacterBody2D
{
private ILogger _logger;
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("玩家状态异常");
}
}
```
这也说明 Godot 日志页不需要重新定义一套“Godot 专用场景日志接口”;现有 Game 运行时日志在 Godot 宿主里本来就会复用
这套 provider。
### 记录不同级别的日志
## Godot 控制台输出语义
```csharp
var logger = LoggerFactoryResolver.Provider.CreateLogger("GameSystem");
当前 `GodotLogger.Write(...)` 的级别映射如下:
// Trace - 最详细的跟踪信息(灰色)
logger.Trace("执行函数: UpdatePlayerPosition");
| 日志级别 | Godot 输出 API | 当前行为 |
| --- | --- | --- |
| `Trace` | `GD.PrintRich(...)` | 使用灰色富文本输出 |
| `Debug` | `GD.PrintRich(...)` | 使用青色富文本输出 |
| `Info` | `GD.Print(...)` | 普通控制台输出 |
| `Warning` | `GD.PushWarning(...)` | 进入 Godot 警告通道 |
| `Error` | `GD.PrintErr(...)` | 输出到错误流 |
| `Fatal` | `GD.PushError(...)` | 进入 Godot 错误通道 |
// Debug - 调试信息(青色)
logger.Debug("当前帧率: {0}", Engine.GetFramesPerSecond());
异常追加格式也来自当前实现本身:
// Info - 一般信息(白色)
logger.Info("游戏开始");
```text
[2026-04-22 10:30:47.012] ERROR [SaveSystem] 保存游戏失败
System.IO.IOException: ...
// Warning - 警告信息(黄色)
logger.Warn("资源加载缓慢: {0}ms", loadTime);
// Error - 错误信息(红色)
logger.Error("无法加载配置文件");
// Fatal - 致命错误(红色,使用 PushError
logger.Fatal("游戏崩溃");
```
如果你需要 JSON formatter、rolling file、namespace 级过滤、structured sink 组合,这已经超出
`GFramework.Godot.Logging` 当前职责,应该回到 [Core 日志系统](../core/logging.md) 设计 provider 组合。
### 记录异常信息
## 什么时候用手写 logger什么时候用 `[Log]`
```csharp
var logger = LoggerFactoryResolver.Provider.CreateLogger("SaveSystem");
- 手写 `LoggerFactoryResolver.Provider.CreateLogger(...)`
- 少量入口类
- 需要自己控制字段名、静态/实例生命周期
- 想明确看到 logger 初始化位置
- 用 `[Log]`
- Godot 节点、controller、system 上有大量重复 logger 字段样板
- 你已经引用 `GFramework.Core.SourceGenerators`
- 想把 logger 字段生成交给编译期
try
{
SaveGame();
}
catch (Exception ex)
{
// 记录异常信息
logger.Error("保存游戏失败", ex);
}
```
这里的边界要分清:
## 高级用法
- Godot provider来自 `GFramework.Godot`
- `[Log]` 生成器:来自 `GFramework.Core.SourceGenerators`
### 在 System 中使用日志
它们是可组合关系,不是上下位替代关系。
```csharp
using GFramework.Core.System;
using GFramework.Core.Logging;
using GFramework.Core.Abstractions.Logging;
## 当前边界
public class CombatSystem : AbstractSystem
{
private ILogger _logger;
- 当前推荐接法是把 `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而不是假定当前实现会自动转换
protected override void OnInit()
{
_logger = LoggerFactoryResolver.Provider.CreateLogger("CombatSystem");
_logger.Info("战斗系统初始化完成");
}
## 继续阅读
public void ProcessCombat(Entity attacker, Entity target, float damage)
{
_logger.Debug("战斗处理: {0} 攻击 {1}, 伤害: {2}",
attacker.Name, target.Name, damage);
- [Core 日志系统](../core/logging.md)
- [日志生成器](../source-generators/logging-generator.md)
- [Godot 运行时集成](./index.md)
- [Godot 场景系统](./scene.md)
- [Godot UI 系统](./ui.md)
if (damage > 100)
{
_logger.Warn("高伤害攻击: {0}", damage);
}
}
protected override void OnDestroy()
{
_logger.Info("战斗系统已销毁");
}
}
```
### 在 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) - 架构最佳实践

View File

@ -1,321 +1,583 @@
---
title: Godot 场景系统
description: 以当前 GFramework.Godot 源码、Game 场景契约与 CoreGrid 接线为准,说明 PackedScene 场景工厂、行为包装和最小接入路径
description: Godot 场景系统提供了 GFramework 场景管理与 Godot 场景树的完整集成
---
# Godot 场景系统
`GFramework.Godot` 在场景这一层负责的是 Godot runtime 适配,而不是再提供一个 Godot 专属 router。
## 概述
当前真正参与场景接线的核心类型是:
Godot 场景系统是 GFramework.Godot 中连接框架场景管理与 Godot 场景树的核心组件。它提供了场景行为封装、场景工厂、场景注册表等功能,让你可以在
Godot 项目中使用 GFramework 的场景管理系统。
- `IGodotSceneRegistry` / `GodotSceneRegistry`
- `GodotSceneFactory`
- `SceneBehaviorFactory`
- `SceneBehaviorBase<T>` 及其 `Node2D` / `Node3D` / `Control` / `Generic` 实现
- 项目侧实现的 `ISceneRoot`
- 项目侧继承 `SceneRouterBase` 的 router
通过 Godot 场景系统,你可以使用 GFramework 的场景路由、生命周期管理等功能,同时保持与 Godot 场景系统的完美兼容。
也就是说Godot 集成页的重点不是“再造一套场景导航 API”而是把 `PackedScene``Node``GFramework.Game`
`ISceneRouter` / `ISceneBehavior` 契约接起来。
**主要特性**
## 当前公开入口
- 场景行为封装SceneBehavior
- 场景工厂和注册表
- 与 Godot PackedScene 集成
- 多种场景行为类型Node2D、Node3D、Control
- 场景生命周期管理
- 场景根节点管理
### `IGodotSceneRegistry`
## 核心概念
Godot 侧的场景资源表,底层是 `IAssetRegistry<PackedScene>`。它只负责:
### 场景行为
- `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 就是这样:
`SceneBehaviorBase<T>` 封装了 Godot 节点的场景行为:
```csharp
using global::CoreGrid.global;
using LoggingTransitionHandler = GFramework.Game.Scene.Handler.LoggingTransitionHandler;
namespace CoreGrid.scripts.core.scene;
public partial class SceneRouter : SceneRouterBase
public abstract class SceneBehaviorBase<T> : ISceneBehavior
where T : Node
{
[GetUtility] private IGodotSceneRegistry _sceneRegistry = null!;
protected readonly T Owner;
public string Key { get; }
public IScene Scene { get; }
}
```
public Node? SceneRoot => Root as Node;
### 场景工厂
protected override void RegisterHandlers()
`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)
{
__InjectContextBindings_Generated();
RegisterHandler(new LoggingTransitionHandler());
RegisterAroundHandler(
new SceneTransitionAnimationHandler(() => SceneTransitionManager.Instance!, _sceneRegistry.GetAll()));
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;
}
}
```
这里可以看到Godot 适配点在 factory / registry / root / transition handler 上,而 router 仍然是项目类。
### 2. 注册 `IGodotSceneRegistry``ISceneFactory`
最小 wiring 需要把 registry 和 factory 装进架构:
### 注册场景
```csharp
using GFramework.Game.Abstractions.Scene;
using GFramework.Godot.Scene;
using Godot;
public sealed class GameSceneRegistry : GodotSceneRegistry
public class GameSceneRegistry : GodotSceneRegistry
{
public GameSceneRegistry()
publieneRegistry()
{
Register(nameof(SceneKey.MainMenu), GD.Load<PackedScene>("res://scenes/main_menu.tscn"));
Register(nameof(SceneKey.Gameplay), GD.Load<PackedScene>("res://scenes/gameplay.tscn"));
// 注册场景资源
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"));
}
}
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
using GFramework.Godot.Architecture;
using GFramework.Godot.Scene;
public class GameArchitecture : AbstractArchitecture
{
[GetSystem] private ISceneRouter _sceneRouter = null!;
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;
public partial class GameplayScene : Node2D, ISceneBehaviorProvider
{
private GameplaySceneBehavior _behavior;
public override void _Ready()
{
__InjectContextBindings_Generated();
_sceneRouter.BindRoot(this);
_behavior = new GameplaySceneBehavior(this, "Gameplay");
}
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 _scene ??= SceneBehaviorFactory.Create(this, nameof(SceneKey.Gameplay));
return _behavior;
}
}
// 自定义场景行为
public class GameplaySceneBehavior : Node2DSceneBehavior
{
public GameplaySceneBehavior(Node2D owner, string key) : base(owner, key)
{
}
public ValueTask OnLoadAsync(ISceneEnterParam? param)
protected override async ValueTask OnLoadInternalAsync(ISceneEnterParam? param)
{
return ValueTask.CompletedTask;
GD.Print("加载游戏场景");
// 加载游戏资源
await Task.CompletedTask;
}
public ValueTask OnEnterAsync()
protected override async ValueTask OnEnterInternalAsync()
{
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;
GD.Print("进入游戏场景");
Owner.Show();
await Task.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 GFramework.Game.Abstractions.Scene;
using GFramework.Godot.SourceGenerators.Abstractions.UI;
using Godot;
using GFramework.Godot.Scene;
[AutoScene(nameof(SceneKey.Gameplay))]
public partial class GameplayRoot : Node2D, ISceneBehaviorProvider, IScene
public partial class SceneRoot : Node, ISceneRoot
{
public ValueTask OnLoadAsync(ISceneEnterParam? param)
private Node _currentSceneNode;
public void AttachScene(Node sceneNode)
{
return ValueTask.CompletedTask;
// 移除旧场景
if (_currentSceneNode != null)
{
RemoveChild(_currentSceneNode);
_currentSceneNode.QueueFree();
}
// 添加新场景
_currentSceneNode = sceneNode;
AddChild(_currentSceneNode);
}
public ValueTask OnEnterAsync()
public void DetachScene(Node sceneNode)
{
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;
if (_currentSceneNode == sceneNode)
{
RemoveChild(_currentSceneNode);
_currentSceneNode = null;
}
}
}
```
生成器当前会补出与源码一致的 `GetScene()`
### 场景参数传递
```csharp
public ISceneBehavior GetScene()
// 定义场景参数
public class GameplayEnterParam : ISceneEnterParam
{
return __autoSceneBehavior_Generated ??= SceneBehaviorFactory.Create(this, SceneKeyStr);
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("场景预加载完成");
}
}
```
要注意两点:
- `[AutoScene]` 只生成方法和 key不会替你自动补 `: ISceneBehaviorProvider`
- `IScene` 仍然是业务生命周期契约;不实现它时,默认 behavior 只会保留基础节点切换语义
### 5. 从业务代码发起导航
一旦 registry、factory、router、root 都装好,导航入口仍然是 `ISceneRouter`
### 场景转换动画
```csharp
await sceneRouter.ReplaceAsync(nameof(SceneKey.MainMenu));
await sceneRouter.ReplaceAsync(nameof(SceneKey.Gameplay), new GameplayEnterParam());
await sceneRouter.PushAsync(nameof(SceneKey.PauseMenu));
await sceneRouter.PopAsync();
using Godot;
using GFramework.Game.Abstractions.Scene;
public class FadeTransitionHandler : ISceneTransitionHandler
{
private ColorRect _fadeRect;
public FadeTransitionHandler(ColorRect fadeRect)
{
_fadeRect = fadeRect;
}
public async ValueTask OnBeforeExitAsync(SceneTransitionEvent @event)
{
// 淡出动画
var tween = _fadeRect.CreateTween();
tween.TweenProperty(_fadeRect, "modulate:a", 1.0f, 0.3f);
await tween.ToSignal(tween, Tween.SignalName.Finished);
}
public async ValueTask OnAfterEnterAsync(SceneTransitionEvent @event)
{
// 淡入动画
var tween = _fadeRect.CreateTween();
tween.TweenProperty(_fadeRect, "modulate:a", 0.0f, 0.3f);
await tween.ToSignal(tween, Tween.SignalName.Finished);
}
// ... 其他方法
}
```
## 当前边界
### 场景间通信
### 没有 `GodotSceneRouter`
```csharp
// 通过事件通信
public partial class GameplayScene : Node2D, IScene
{
public async ValueTask OnEnterAsync()
{
// 发送场景进入事件
this.SendEvent(new GameplaySceneEnteredEvent());
await Task.CompletedTask;
}
}
仓库当前不存在 `GodotSceneRouter` 类型。旧文档里把它写成默认入口是失真的;实际入口仍然是项目侧继承
`SceneRouterBase` 的 router。
// 在其他地方监听
public partial class HUD : Control
{
public override void _Ready()
{
this.RegisterEvent<GameplaySceneEnteredEvent>(OnGameplayEntered);
}
### 没有自动注册所有场景
private void OnGameplayEntered(GameplaySceneEnteredEvent evt)
{
GD.Print("游戏场景已进入,显示 HUD");
Show();
}
}
```
当前运行时只认识你注册进 `IGodotSceneRegistry``PackedScene`。它不会扫描目录、不会从脚本类型自动反推出注册表。
## 最佳实践
### provider 是“优先路径”,不是“唯一路径”
1. **场景脚本实现 IScene 接口**:获得完整的生命周期管理
```csharp
✓ public partial class MyScene : Node2D, IScene { }
✗ public partial class MyScene : Node2D { } // 无生命周期管理
```
`GodotSceneFactory` 会优先使用 `ISceneBehaviorProvider`,但没有 provider 时仍会按节点类型自动包装。这个行为和 UI 系统不同;
UI 工厂当前没有同等的自动回退。
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"));
}
}
```
### root 仍然是项目职责
3. **在 OnLoadAsync 中加载资源**:避免场景切换卡顿
```csharp
public async ValueTask OnLoadAsync(ISceneEnterParam? param)
{
// 异步加载资源
await LoadTexturesAsync();
await LoadAudioAsync();
}
```
`ISceneRoot` 的实现决定:
4. **使用场景根节点管理场景树**:保持场景树结构清晰
```csharp
// 创建场景根节点
var sceneRoot = new Node { Name = "SceneRoot" };
AddChild(sceneRoot);
- 节点挂到哪里
- 移除时如何释放
- 是否保留额外的当前视图引用
// 绑定到场景路由
sceneRouter.BindRoot(sceneRoot);
```
Godot runtime 不会替项目生成统一的 root 节点。
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; // 避免全局状态
```
1. [Godot 运行时集成](./index.md)
2. [Godot 架构集成](./architecture.md)
3. [Game 场景系统](../game/scene.md)
4. [AutoScene 生成器](../source-generators/auto-scene-generator.md)
## 常见问题
### 问题:如何在 Godot 场景中使用 GFramework
**解答**
场景脚本实现 `IScene` 接口:
```csharp
public partial class MyScene : Node2D, IScene
{
public async ValueTask OnLoadAsync(ISceneEnterParam? param) { }
public async ValueTask OnEnterAsync() { }
// ... 实现其他方法
}
```
### 问题:场景切换时节点如何管理?
**解答**
使用场景根节点管理:
```csharp
// 场景路由会自动管理节点的添加和移除
await sceneRouter.ReplaceAsync("NewScene");
// 旧场景节点会被移除,新场景节点会被添加
```
### 问题:如何实现场景预加载?
**解答**
使用场景工厂提前创建场景:
```csharp
var sceneFactory = this.GetUtility<ISceneFactory>();
var sceneBehavior = sceneFactory.Create("NextScene");
await sceneBehavior.LoadAsync(null);
```
### 问题:场景生命周期方法的调用顺序是什么?
**解答**
- 进入场景:`OnLoadAsync` -> `OnEnterAsync` -> `OnShow`
- 暂停场景:`OnPause` -> `OnHide`
- 恢复场景:`OnShow` -> `OnResume`
- 退出场景:`OnHide` -> `OnExitAsync` -> `OnUnloadAsync`
### 问题:如何在场景中访问架构组件?
**解答**
使用扩展方法:
```csharp
public partial class MyScene : Node2D, IScene
{
public async ValueTask OnEnterAsync()
{
var playerModel = this.GetModel<PlayerModel>();
var gameSystem = this.GetSystem<GameSystem>();
await Task.CompletedTask;
}
}
```
### 问题:场景切换时如何显示加载界面?
**解答**
使用场景转换处理器:
```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 扩展方法

View File

@ -1,158 +1,420 @@
---
title: Godot 信号系统
description: 以当前 GFramework.Godot 源码与 CoreGrid 的动态绑定用法为准,说明 Signal(...) fluent API、SignalBuilder 行为与接入边界。
---
# 信号连接系统 (Signal Connection System)
# Godot 信号系统
## 概述
`GFramework.Godot` 当前提供的信号能力很收敛:它不是另一套事件系统,也不是自动生成绑定代码的入口,而是对
`GodotObject.Connect(...)` 的一层 fluent 包装
信号连接系统是 Godot 扩展方法模块中的一个专门子模块,提供流畅、类型安全的 Godot 信号连接 API。该系统采用构建器模式Builder
Pattern和流畅接口设计Fluent Interface大大简化了信号订阅代码提高了代码的可读性和可维护性
当前真正公开的入口只有两个:
## 核心类
- `SignalFluentExtensions.Signal(...)`
- `SignalBuilder`
### SignalBuilder
如果你需要的是场景节点字段注入和静态 signal 自动绑订,请看
`GFramework.Godot.SourceGenerators``[GetNode]``[BindNodeSignal]`,不要把它们和这里的运行时 fluent API 混成同一层。
信号连接构建器,负责构建和执行信号连接操作。
## 当前公开入口
**特性:**
### `Signal(...)`
- 支持链式调用
- 可配置连接标志
- 支持连接后立即调用
- 返回原始对象以便继续操作
`Signal(...)` 是定义在 `GodotObject` 上的扩展方法:
### 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 对象创建信号构建器。
```csharp
public static SignalBuilder Signal(this GodotObject @object, StringName signal)
```
它只做一件事:基于目标对象和 signal 名称创建一个 `SignalBuilder`。这意味着当前 fluent API 不只适用于 `Node`,也适用于
其他 Godot 对象。
**参数:**
### `SignalBuilder`
- `@object` - 扩展方法的目标对象
- `signal` - 要连接的信号名称
`SignalBuilder` 的当前行为来自运行时代码本身:
## 实际应用场景
- `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` 的控制权
最小示例:
### UI 事件处理
```csharp
using GFramework.Godot.Extensions.Signal;
using Godot;
public partial class SettingsPanel : Control
public class MainMenu : Control
{
public override void _Ready()
{
var applyButton = GetNode<Button>("%ApplyButton");
applyButton.Signal(Button.SignalName.Pressed)
.To(Callable.From(OnApplyPressed));
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)));
}
private void OnApplyPressed()
private void OnStartPressed()
{
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
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()
public class Player : CharacterBody2D
{
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
_quitConfirmDialog.Signal("Confirmed")
.To(Callable.From(OnQuitConfirmDialogConfirmed))
.End();
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();
}
}
```
## 什么时候用 fluent API什么时候用生成器
## 设计模式分析
- 用 `Signal(...)`
- 动态节点
- 动态 signal 名称
- 想保留手写 `Callable` 和连接 flags
- 用 `[BindNodeSignal]`
- 节点字段和 signal 都是静态已知
- 你已经在用 `[GetNode]`
- 希望把 `_Ready()` 里的重复绑定样板交给生成器
### Builder Pattern
这两条路径是互补关系,不是前后代际关系。当前源码没有“先用 `CreateSignalBuilder(...)`,再升级到生成器”这种迁移链。
SignalBuilder 实现了构建器模式:
## 当前边界
- 分步构建复杂的信号连接
- 支持链式调用
- 延迟执行到最终调用时
- 当前入口是 `Signal(...)`,不是旧文档里的 `CreateSignalBuilder(...)`
- 这里不会自动生成 `_Ready()` / `_ExitTree()`,这类能力属于 `GFramework.Godot.SourceGenerators`
- `SignalBuilder` 不提供取消订阅 token也不会替你包装 `Disconnect(...)`
- `End()` 只返回原始对象,不会提交额外配置,也不是必须调用的终止步骤
- signal 名称是否合法、callable 签名是否匹配,仍然遵循 Godot 自身运行时规则
- `ToAndCall(...)` 会在完成连接后立刻执行 handler如果 handler 有副作用,需要你自己确认时机
### Fluent Interface
## 继续阅读
流畅接口设计:
- [Godot 运行时集成](./index.md)
- [Godot 扩展方法](./extensions.md)
- [Godot 集成教程](../tutorials/godot-integration.md)
- [BindNodeSignal 生成器](../source-generators/bind-node-signal-generator.md)
- 方法链式调用
- 可读性强
- 表达力强
### 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);
```

View File

@ -1,351 +1,643 @@
---
title: Godot UI 系统
description: 以当前 GFramework.Godot 源码、Game UI 契约与 CoreGrid 接线为准,说明 PackedScene UI 工厂、页面行为和层级接入路径
description: Godot UI 系统提供了 GFramework UI 管理与 Godot Control 节点的完整集成
---
# 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 管理系统。
- `IGodotUiRegistry` / `GodotUiRegistry`
- `GodotUiFactory`
- `CanvasItemUiPageBehaviorBase<T>`
- `UiPageBehaviorFactory`
- `Page` / `Overlay` / `Modal` / `Toast` / `Topmost` 五类 layer behavior
- 项目侧实现的 `IUiRoot`
- 项目侧继承 `UiRouterBase` 的 router
通过 Godot UI 系统,你可以使用 GFramework 的 UI 路由、生命周期管理、多层级显示等功能,同时保持与 Godot UI 系统的完美兼容。
## 当前公开入口
**主要特性**
### `IGodotUiRegistry`
- UI 页面行为封装
- UI 工厂和注册表
- 与 Godot PackedScene 集成
- 多层级 UI 支持Page、Overlay、Modal、Toast、Topmost
- UI 生命周期管理
- UI 根节点管理
Godot 侧 UI 资源表,底层是 `IAssetRegistry<PackedScene>`。它只负责:
## 核心概念
- `uiKey -> PackedScene` 映射
- 让 `GodotUiFactory` 可以按 key 实例化 UI 页面
### 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` 目前就是:
`CanvasItemUiPageBehaviorBase<T>` 封装了 Godot Control 节点的 UI 行为:
```csharp
using LoggingTransitionHandler = GFramework.Game.UI.Handler.LoggingTransitionHandler;
namespace CoreGrid.scripts.core.ui;
[Log]
public partial class UiRouter : UiRouterBase
public abstract class CanvasItemUiPageBehaviorBase<T> : IUiPageBehavior
where T : CanvasItem
{
protected override void RegisterHandlers()
protected readonly T Owner;
public string Key { get; }
public UiLayer Layer { get; }
public bool IsReentrant { get; }
}
```
### UI 工厂
`GodotUiFactory` 负责创建 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;
public partial class MainMenuPage : Control, IUiPage
{
public void OnEnter(IUiPageEnterParam? param)
{
_log.Debug("Registering default transition handlers");
RegisterHandler(new LoggingTransitionHandler());
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();
}
}
```
Godot runtime 自身并不接管这层 router 的定义。
### 2. 注册 `IGodotUiRegistry``IUiFactory`
最小 wiring 需要显式注册 UI 资源表和工厂:
### 实现 UI 页面行为提供者
```csharp
using Godot;
using GFramework.Game.Abstractions.UI;
using GFramework.Godot.UI;
using Godot;
public sealed class GameUiRegistry : GodotUiRegistry
public partial class MainMenuPage : Control, IUiPageBehaviorProvider
{
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!;
private PageLayerUiPageBehavior _behavior;
public override void _Ready()
{
__InjectContextBindings_Generated();
_uiRouter.BindRoot(this);
_behavior = new PageLayerUiPageBehavior(this, "MainMenu");
}
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)
{
}
public void OnExit()
{
}
public void OnPause()
{
}
public void OnResume()
{
}
public void OnShow()
{
}
public void OnHide()
{
return _behavior;
}
}
```
#### 方式 B`[AutoUiPage]` 让生成器补样板
当前更贴近真实消费者 wiring 的方式,是让生成器产出 `UiKeyStr``GetPage()`
### 注册 UI
```csharp
using GFramework.Game.Abstractions.UI;
using GFramework.Godot.SourceGenerators.Abstractions.UI;
using GFramework.Godot.UI;
using Godot;
[AutoUiPage(nameof(UiKey.MainMenu), nameof(UiLayer.Page))]
public partial class MainMenu : Control, IUiPageBehaviorProvider, IUiPage
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
{
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()
{
}
public void OnPause()
{
}
public void OnResume()
{
// 淡出动画
var tween = CreateTween();
tween.TweenProperty(this, "modulate:a", 0.0f, 0.3f);
tween.TweenCallback(Callable.From(Hide));
}
public void OnShow()
{
Show();
}
public void OnHide()
{
Hide();
}
// ... 其他方法
}
```
### UI 句柄管理
```csharp
public partial class DialogManager : Node
{
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;
}
}
}
```
当前生成器补出的核心样板与源码一致:
### 多个 Toast 显示
```csharp
public IUiPageBehavior GetPage()
public partial class ToastManager : Node
{
return __autoUiPageBehavior_Generated ??=
UiPageBehaviorFactory.Create(this, UiKeyStr, UiLayer.Page);
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();
}
}
```
要注意两点:
## 最佳实践
- `[AutoUiPage]` 不会替你自动补 `: IUiPageBehaviorProvider`
- UI 层级是生成器输入的一部分;`Page` / `Modal` / `Overlay` 语义不是后面再猜出来的
1. **UI 脚本实现 IUiPage 接口**:获得完整的生命周期管理
```csharp
✓ public partial class MyPage : Control, IUiPage { }
✗ public partial class MyPage : Control { } // 无生命周期管理
```
### 5. 按 layer 选择正确入口
2. **使用正确的 UI 层级**:根据 UI 类型选择合适的层级
```csharp
✓ Page: 主要页面(主菜单、设置)
✓ Overlay: 浮层(信息面板)
✓ Modal: 模态对话框(确认框)
✓ Toast: 提示消息
✓ Topmost: 系统级(加载界面)
```
Godot runtime 只是落地 `UiRouterBase` 的语义,因此入口仍然和 `GFramework.Game` 一致:
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` 接口:
```csharp
await uiRouter.ReplaceAsync(nameof(UiKey.MainMenu));
await uiRouter.PushAsync(nameof(UiKey.Settings));
await uiRouter.PopAsync();
public partial class MyPage : Control, IUiPage, IUiPageBehaviorProvider
{
public void OnEnter(IUiPageEnterParam? param) { }
public IUiPageBehavior GetPage() { return new PageLayerUiPageBehavior(this, "MyPage"); }
}
```
层级 UI
### 问题UI 层级有什么区别?
**解答**
- **Page**:栈管理,不可重入,用于主要页面
- **Overlay**:可重入,用于浮层
- **Modal**:可重入,带遮罩,用于对话框
- **Toast**:可重入,轻量提示
- **Topmost**:不可重入,最高优先级
### 问题:如何实现 UI 动画?
**解答**
在生命周期方法中使用 Godot Tween
```csharp
var handle = uiRouter.Show(nameof(UiKey.PauseMenu), UiLayer.Modal);
uiRouter.Hide(handle, UiLayer.Modal);
public void OnEnter(IUiPageEnterParam? param)
{
var tween = CreateTween();
tween.TweenProperty(this, "modulate:a", 1.0f, 0.3f);
}
```
当前实现里,`Show(..., UiLayer.Page)` 会直接抛异常;`Page` 层必须走 `PushAsync` / `ReplaceAsync`
### 问题:如何在 UI 中访问架构组件?
## 输入与暂停语义
**解答**
使用扩展方法:
如果页面只实现 `IUiPage`,它只有基础生命周期。
```csharp
public partial class MyPage : Control, IUiPage
{
public void OnEnter(IUiPageEnterParam? param)
{
var playerModel = this.GetModel<PlayerModel>();
var gameSystem = this.GetSystem<GameSystem>();
}
}
```
如果还需要更强的输入仲裁或暂停语义,可以像 CoreGrid 的 `PauseMenu` 一样继续实现:
### 问题:如何关闭 Modal 或 Toast
- `IUiInteractionProfileProvider`
- `IUiActionHandler`
**解答**
使用 UI 句柄:
当前这条链路是成立的:
```csharp
// 显示时保存句柄
var handle = uiRouter.Show("Dialog", UiLayer.Modal);
1. 页面行为从 owner 读取 `UiInteractionProfile`
2. router 根据 profile 判断动作捕获、世界输入阻断和暂停策略
3. 如果页面实现了 `IUiActionHandler``TryHandleUiAction(...)` 会继续下沉到页面
// 关闭时使用句柄
uiRouter.Hide(handle, UiLayer.Modal, destroy: true);
```
这也是为什么 `PauseMenu` 一类 modal 页面可以声明:
### 问题UI 生命周期方法的调用顺序是什么?
- 捕获 `Cancel`
- 阻断 World pointer / action input
- 在可见时持有暂停
- 即使在暂停状态也继续处理节点逻辑
**解答**
## 当前边界
- 进入:`OnEnter` -> `OnShow`
- 暂停:`OnPause` -> `OnHide`
- 恢复:`OnShow` -> `OnResume`
- 退出:`OnHide` -> `OnExit`
### 没有 `GodotUiRouter`
## 相关文档
仓库当前没有这个类型。旧文档把它写成默认入口是不准确的;真实入口仍然是项目侧的 `UiRouterBase` 派生类。
### UI 工厂不会自动补 behavior
`GodotSceneFactory` 不同,`GodotUiFactory` 当前不会按节点类型自动创建 behavior。节点不实现
`IUiPageBehaviorProvider` 时会直接失败。
### `Page` 层不是 `Show(...)` 的适用对象
`UiLayer.Page` 代表页面栈语义,而不是普通 layer UI。当前实现明确要求
- `Page``PushAsync` / `ReplaceAsync`
- `Overlay` / `Modal` / `Toast` / `Topmost``Show` / `Hide`
### 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)
- [UI 系统](/zh-CN/game/ui) - 核心 UI 管理
- [Godot 架构集成](/zh-CN/godot/architecture) - Godot 架构基础
- [Godot 场景系统](/zh-CN/godot/scene) - Godot 场景集成
- [Godot 扩展](/zh-CN/godot/extensions) - Godot 扩展方法

View File

@ -1,26 +1,21 @@
---
title: AutoRegisterExportedCollections 生成器
description: 说明批量注册生成器当前会生成什么、可匹配哪些集合与注册器成员,以及 null-skip 与编译期诊断的边界。
---
# AutoRegisterExportedCollections 生成器
`[AutoRegisterExportedCollections]` 用来把“遍历一组配置并逐项调用 registry 方法”的启动样板收敛成一个生成方法
> 为 Godot 导出集合生成批量注册方法,收敛启动入口里的重复 `foreach + Registry(...)` 样板。
它最常见的落点确实是 Godot Inspector 导出的数组,但当前生成器真正依赖的不是 `[Export]` 本身,而是:
## 概述
- 宿主类型被标记了 `[AutoRegisterExportedCollections]`
- 某个实例字段或可读实例属性被标记了 `[RegisterExportedCollection(...)]`
- 该成员可枚举,且元素类型可在编译期推导
- 目标 registry 成员存在,并能找到兼容的单参数实例方法
在游戏启动入口中,常见的一类样板是:
## 当前包关系
- 在 Inspector 中导出一批配置、资源映射或预制体条目
- 从某个 Registry 成员拿到注册器
- 遍历集合逐项调用 `Register(...)` / `Registry(...)`
- 特性来源:`GFramework.Godot.SourceGenerators.Abstractions.UI`
- 生成器实现:`GFramework.Godot.SourceGenerators`
- 典型消费者Godot 启动入口、资源入口节点、配置引导节点
`AutoRegisterExportedCollections` 会把这类样板收敛成声明式配置。
## 最小用法
它特别适合 `GameEntryPoint`、资源根节点、配置引导节点这类“导出即注册”的场景。
相关特性当前位于 `GFramework.Godot.SourceGenerators.Abstractions.UI` 命名空间。
## 基础使用
```csharp
using System.Collections.Generic;
@ -44,6 +39,13 @@ 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
{
@ -55,169 +57,119 @@ public partial class GameEntryPoint : Node
public override void _Ready()
{
_textureRegistry ??= ResolveTextureRegistry();
_textureRegistry ??= new TextureRegistry();
__RegisterExportedCollections_Generated();
}
private static IAssetRegistry<Texture2D> ResolveTextureRegistry()
{
throw new NotImplementedException();
}
}
```
当前生成器不会自动调用 `__RegisterExportedCollections_Generated()`。你需要在 registry 成员和集合成员都准备好之后手动调用。
为了让示例具备完整的调用路径,这里在 `_Ready()` 里先初始化了 `_textureRegistry`
实际项目里,这个字段通常来自架构容器、服务定位或外部注入;关键点是调用 `__RegisterExportedCollections_Generated()`
之前,注册器成员必须已经可用,否则生成代码会按设计静默跳过注册。
## 当前会生成什么
对于上面的成员,当前生成器会产出:
## 生成的代码
```csharp
private void __RegisterExportedCollections_Generated()
// <auto-generated />
#nullable enable
partial class GameEntryPoint
{
if (this._textureConfigs is not null && this._textureRegistry is not null)
private void __RegisterExportedCollections_Generated()
{
foreach (var __generatedItem in this._textureConfigs)
if (this._textureConfigs is not null && this._textureRegistry is not null)
{
this._textureRegistry.Registry(__generatedItem);
foreach (var __generatedItem in this._textureConfigs)
{
this._textureRegistry.Registry(__generatedItem);
}
}
}
}
```
最重要的运行时语义只有两条:
## 参数说明
- 集合成员为 `null` 时,本次注册直接跳过
- registry 成员为 `null` 时,本次注册直接跳过
### `[AutoRegisterExportedCollections]`
这里的“跳过”只针对运行时 `null` 情况;配置错误、方法不匹配、元素类型无法推导等问题都会在编译期直接给出诊断,而不是静默吞掉
类级标记,声明该类型允许生成 `__RegisterExportedCollections_Generated()`
## 当前支持的成员形状
### `[RegisterExportedCollection(registryMemberName, registerMethodName)]`
### 集合成员
| 参数 | 类型 | 说明 |
|----------------------|----------|----------------------------------|
| `registryMemberName` | `string` | 当前类型上用于执行注册的字段或属性名 |
| `registerMethodName` | `string` | 注册方法名,例如 `Register``Registry` |
`[RegisterExportedCollection]` 可以标在:
推荐优先使用 `nameof(...)` 表达式,而不是手写字符串。
- 实例字段
- 可读、非索引器的实例属性
## 支持的匹配规则
它们不必一定带 `[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`
- 不支持嵌套类
- 生成器不会自动接入 `_Ready()` 或其他生命周期方法
- 宿主类型若已声明 `__RegisterExportedCollections_Generated()`,会触发命名冲突诊断
- 只有当至少一个成员成功通过验证时,才会生成方法
- 生成器不会自动调用 `__RegisterExportedCollections_Generated()`
- 非泛型 `IEnumerable` 之类无法推导元素类型的集合不受支持
- 注册方法必须对宿主类型可访问
## 诊断速查
## 诊断信息
| 诊断 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` 构造参数无效 |
| 诊断 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` 参数无效 |
## 何时适合用它
## 调用时机建议
适合
推荐在以下时机之一调用生成方法
- 启动入口里有多组“集合 -> registry”的重复注册代码
- 每个元素都只需要一次简单的单参数注册
- 你想把“注册到哪个 registry、调用哪个方法”直接挂在成员声明上
- `_Ready()` 中,且在注册器字段已经准备好之后
- 启动入口的显式 `Initialize()``Bootstrap()` 方法中
- 测试中的装配阶段
适合:
要在构造函数中调用,因为此时 Godot 导出字段和外部依赖通常还未准备完毕。
- 注册流程需要排序、过滤、去重或事务式回滚
- 每个元素注册前后还要插入复杂副作用
- 注册规则依赖运行时动态上下文,而不是静态成员绑定
## 相关文档
## 推荐阅读
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`
- [源码生成器总览](./index)
- [游戏内容配置系统](/zh-CN/game/config-system)

View File

@ -1,44 +1,216 @@
---
title: BindNodeSignal 生成器
description: 说明 [BindNodeSignal] 当前生成什么、如何与 GetNode 协作,以及 _Ready 和 _ExitTree 的接入要求。
---
# BindNodeSignal 生成器
`[BindNodeSignal]` 把 Godot CLR event 的 `+=` / `-=` 样板收敛成生成方法。它只生成“如何订阅与解绑”,不会替你查找节点,也不会自动生成完整生命周期方法。
> 自动生成 Godot 节点信号绑定与解绑逻辑,消除事件订阅样板代码
## 当前包关系
## 概述
- 特性来源:`GFramework.Godot.SourceGenerators.Abstractions`
- 生成器实现:`GFramework.Godot.SourceGenerators`
- 目标字段基线:`nodeFieldName` 指向的字段必须继承 `Godot.Node`
BindNodeSignal 生成器为标记了 `[BindNodeSignal]` 特性的方法自动生成节点事件绑定和解绑代码。它将 `_Ready()`
`_ExitTree()` 中重复的 `+=``-=` 样板代码收敛到生成器中统一维护。
## 最小用法
### 核心功能
- **自动事件绑定**:在 `_Ready()` 中自动订阅节点事件
- **自动事件解绑**:在 `_ExitTree()` 中自动取消订阅
- **多事件绑定**:一个方法可以绑定到多个节点事件
- **类型安全检查**:编译时验证方法签名与事件委托的兼容性
- **与 GetNode 集成**:无缝配合 `[GetNode]` 特性使用
## 基础使用
### 标记事件处理方法
使用 `[BindNodeSignal]` 特性标记处理节点事件的方法:
```csharp
using GFramework.Godot.SourceGenerators.Abstractions;
using Godot;
public partial class Hud : Control
public partial class MainMenu : Control
{
[GetNode]
private Button _startButton = null!;
[GetNode]
private SpinBox _startOreSpinBox = null!;
private Button _settingsButton = null!;
private Button _quitButton = null!;
[BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))]
private void OnStartButtonPressed()
{
StartGame();
}
[BindNodeSignal(nameof(_startOreSpinBox), nameof(SpinBox.ValueChanged))]
private void OnStartOreValueChanged(double value)
[BindNodeSignal(nameof(_settingsButton), nameof(Button.Pressed))]
private void OnSettingsButtonPressed()
{
ShowSettings();
}
[BindNodeSignal(nameof(_quitButton), nameof(Button.Pressed))]
private void OnQuitButtonPressed()
{
QuitGame();
}
public override void _Ready()
{
__BindNodeSignals_Generated();
}
public override void _ExitTree()
{
__UnbindNodeSignals_Generated();
}
}
```
### 生成的代码
编译器会为标记的类自动生成以下代码:
```csharp
// <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();
}
@ -50,143 +222,459 @@ public partial class Hud : Control
}
```
当前生成器会产出:
### 复杂事件处理场景
实现完整的 UI 事件处理:
```csharp
private void __BindNodeSignals_Generated()
public partial class InventoryUI : Control
{
// 节点
[GetNode]
private ItemList _itemList = null!;
[GetNode]
private Button _useButton = null!;
[GetNode]
private Button _dropButton = null!;
[GetNode]
private LineEdit _searchBox = null!;
// 事件处理
[BindNodeSignal(nameof(_itemList), nameof(ItemList.ItemSelected))]
private void OnItemSelected(long index)
{
SelectItem((int)index);
}
[BindNodeSignal(nameof(_itemList), nameof(ItemList.ItemActivated))]
private void OnItemActivated(long index)
{
UseItem((int)index);
}
[BindNodeSignal(nameof(_useButton), nameof(Button.Pressed))]
private void OnUseButtonPressed()
{
UseSelectedItem();
}
[BindNodeSignal(nameof(_dropButton), nameof(Button.Pressed))]
private void OnDropButtonPressed()
{
DropSelectedItem();
}
[BindNodeSignal(nameof(_searchBox), nameof(LineEdit.TextChanged))]
private void OnSearchTextChanged(string newText)
{
FilterItems(newText);
}
public override void _Ready()
{
__InjectGetNodes_Generated();
__BindNodeSignals_Generated();
InitializeInventory();
}
public override void _ExitTree()
{
__UnbindNodeSignals_Generated();
}
}
```
## 生命周期管理
### 自动生成生命周期方法
如果类没有 `_Ready()``_ExitTree()`,生成器会自动生成:
```csharp
public partial class AutoLifecycleHud : Control
{
private Button _button = null!;
[BindNodeSignal(nameof(_button), nameof(Button.Pressed))]
private void OnButtonPressed()
{
// 处理点击
}
// 无需手动声明 _Ready 和 _ExitTree
// 生成器会自动生成:
// public override void _Ready() { __BindNodeSignals_Generated(); }
// public override void _ExitTree() { __UnbindNodeSignals_Generated(); }
}
```
### 手动生命周期调用
如果已有生命周期方法,需要手动调用生成的方法:
```csharp
public partial class CustomLifecycleHud : Control
{
private Button _button = null!;
[BindNodeSignal(nameof(_button), nameof(Button.Pressed))]
private void OnButtonPressed()
{
HandlePress();
}
public override void _Ready()
{
// 必须手动调用绑定方法
__BindNodeSignals_Generated();
// 自定义初始化逻辑
InitializeUI();
}
public override void _ExitTree()
{
// 必须手动调用解绑方法
__UnbindNodeSignals_Generated();
// 自定义清理逻辑
CleanupResources();
}
}
```
**注意**:如果在 `_Ready()` 中不调用 `__BindNodeSignals_Generated()`,编译器会发出警告 `GF_Godot_BindNodeSignal_008`
## 诊断信息
生成器会在以下情况报告编译错误或警告:
### GF_Godot_BindNodeSignal_001 - 不支持嵌套类
**错误信息**`Class '{ClassName}' cannot use [BindNodeSignal] inside a nested type`
**解决方案**:将嵌套类提取为独立的类
```csharp
// ❌ 错误
public partial class Outer
{
public partial class Inner
{
[BindNodeSignal(nameof(_button), nameof(Button.Pressed))]
private void OnPressed() { } // 错误
}
}
// ✅ 正确
public partial class Inner
{
[BindNodeSignal(nameof(_button), nameof(Button.Pressed))]
private void OnPressed() { }
}
```
### GF_Godot_BindNodeSignal_002 - 不支持静态方法
**错误信息**`Method '{MethodName}' cannot be static when using [BindNodeSignal]`
**解决方案**:改为实例方法
```csharp
// ❌ 错误
[BindNodeSignal(nameof(_button), nameof(Button.Pressed))]
private static void OnPressed() { }
// ✅ 正确
[BindNodeSignal(nameof(_button), nameof(Button.Pressed))]
private void OnPressed() { }
```
### GF_Godot_BindNodeSignal_003 - 节点字段不存在
**错误信息**
`Method '{MethodName}' references node field '{FieldName}', but no matching field exists on class '{ClassName}'`
**解决方案**:确保引用的字段存在且名称正确
```csharp
// ❌ 错误_button 字段不存在
[BindNodeSignal(nameof(_button), nameof(Button.Pressed))]
private void OnPressed() { }
// ✅ 正确
private Button _button = null!;
[BindNodeSignal(nameof(_button), nameof(Button.Pressed))]
private void OnPressed() { }
```
### GF_Godot_BindNodeSignal_004 - 节点字段必须是实例字段
**错误信息**`Method '{MethodName}' references node field '{FieldName}', but that field must be an instance field`
**解决方案**:将节点字段改为实例字段(非静态)
```csharp
// ❌ 错误
private static Button _button = null!;
// ✅ 正确
private Button _button = null!;
```
### GF_Godot_BindNodeSignal_005 - 字段类型必须继承自 Godot.Node
**错误信息**`Field '{FieldName}' must be a Godot.Node type to use [BindNodeSignal]`
**解决方案**:确保字段类型继承自 `Godot.Node`
```csharp
// ❌ 错误
private string _text = null!; // string 不是 Node 类型
[BindNodeSignal(nameof(_text), "Changed")] // 错误
// ✅ 正确
private Button _button = null!; // Button 继承自 Node
[BindNodeSignal(nameof(_button), nameof(Button.Pressed))]
```
### GF_Godot_BindNodeSignal_006 - 目标事件不存在
**错误信息**`Field '{FieldName}' does not contain an event named '{EventName}'`
**解决方案**:确保事件名称正确
```csharp
private Button _button = null!;
// ❌ 错误Click 不是 Button 的事件
[BindNodeSignal(nameof(_button), "Click")]
// ✅ 正确:使用正确的事件名
[BindNodeSignal(nameof(_button), nameof(Button.Pressed))]
```
### GF_Godot_BindNodeSignal_007 - 方法签名不兼容
**错误信息**`Method '{MethodName}' is not compatible with event '{EventName}' on field '{FieldName}'`
**解决方案**:确保方法签名与事件委托匹配
```csharp
private SpinBox _spinBox = null!;
// ❌ 错误SpinBox.ValueChanged 需要 double 参数
[BindNodeSignal(nameof(_spinBox), nameof(SpinBox.ValueChanged))]
private void OnValueChanged() { } // 缺少参数
// ✅ 正确
[BindNodeSignal(nameof(_spinBox), nameof(SpinBox.ValueChanged))]
private void OnValueChanged(double value) { }
```
### GF_Godot_BindNodeSignal_008 - 需要在 _Ready 中调用绑定方法
**警告信息**
`Class '{ClassName}' defines _Ready(); call __BindNodeSignals_Generated() there to bind [BindNodeSignal] handlers`
**解决方案**:在 `_Ready()` 中手动调用 `__BindNodeSignals_Generated()`
```csharp
public override void _Ready()
{
__BindNodeSignals_Generated(); // ✅ 必须手动调用
// 其他初始化...
}
```
### GF_Godot_BindNodeSignal_009 - 需要在 _ExitTree 中调用解绑方法
**警告信息**
`Class '{ClassName}' defines _ExitTree(); call __UnbindNodeSignals_Generated() there to unbind [BindNodeSignal] handlers`
**解决方案**:在 `_ExitTree()` 中手动调用 `__UnbindNodeSignals_Generated()`
```csharp
public override void _ExitTree()
{
__UnbindNodeSignals_Generated(); // ✅ 必须手动调用
// 其他清理...
}
```
### GF_Godot_BindNodeSignal_010 - 构造参数无效
**错误信息**
`Method '{MethodName}' uses [BindNodeSignal] with an invalid '{ParameterName}' constructor argument; it must be a non-empty string literal`
**解决方案**:使用有效的字符串字面量或 nameof 表达式
```csharp
// ❌ 错误:空字符串
[BindNodeSignal("", nameof(Button.Pressed))]
// ❌ 错误null 值
[BindNodeSignal(null, nameof(Button.Pressed))]
// ✅ 正确
[BindNodeSignal(nameof(_button), nameof(Button.Pressed))]
```
## 最佳实践
### 1. 使用 nameof 表达式
使用 `nameof` 而不是字符串字面量,以获得重构支持和编译时检查:
```csharp
// ❌ 不推荐:字符串字面量
[BindNodeSignal("_button", "Pressed")]
// ✅ 推荐nameof 表达式
[BindNodeSignal(nameof(_button), nameof(Button.Pressed))]
```
### 2. 保持方法命名一致
使用统一的命名约定提高代码可读性:
```csharp
// ✅ 推荐On + 节点名 + 事件名
[BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))]
private void OnStartButtonPressed() { }
[BindNodeSignal(nameof(_volumeSlider), nameof(Slider.ValueChanged))]
private void OnVolumeSliderValueChanged(double value) { }
```
### 3. 分组相关事件处理
将相关的事件处理方法放在一起,便于维护:
```csharp
public partial class GameHud : Control
{
// UI 节点
[GetNode]
private Button _pauseButton = null!;
[GetNode]
private Button _menuButton = null!;
// UI 事件处理(放在一起)
[BindNodeSignal(nameof(_pauseButton), nameof(Button.Pressed))]
private void OnPauseButtonPressed() { }
[BindNodeSignal(nameof(_menuButton), nameof(Button.Pressed))]
private void OnMenuButtonPressed() { }
}
```
### 4. 正确处理生命周期
始终确保事件解绑,避免内存泄漏:
```csharp
public partial class SafeHud : Control
{
private Button _button = null!;
[BindNodeSignal(nameof(_button), nameof(Button.Pressed))]
private void OnButtonPressed() { }
public override void _Ready()
{
__BindNodeSignals_Generated();
}
public override void _ExitTree()
{
// 确保解绑事件
__UnbindNodeSignals_Generated();
}
}
```
### 5. 对比手动事件绑定
| 方式 | 代码量 | 可维护性 | 错误风险 | 推荐场景 |
|--------------------|-----|------|----------|------------|
| 手动 `+=` / `-=` | 多 | 中 | 高(易遗漏解绑) | 简单场景 |
| `[BindNodeSignal]` | 少 | 高 | 低(编译器检查) | 复杂 UI、频繁事件 |
```csharp
// ❌ 不推荐:手动绑定
public override void _Ready()
{
_startButton.Pressed += OnStartButtonPressed;
_startOreSpinBox.ValueChanged += OnStartOreValueChanged;
_settingsButton.Pressed += OnSettingsButtonPressed;
_quitButton.Pressed += OnQuitButtonPressed;
}
private void __UnbindNodeSignals_Generated()
public override void _ExitTree()
{
// 容易遗漏解绑
_startButton.Pressed -= OnStartButtonPressed;
_startOreSpinBox.ValueChanged -= OnStartOreValueChanged;
_quitButton.Pressed -= OnQuitButtonPressed; // 遗漏了 _settingsButton
}
```
## 生命周期边界
### 它只生成辅助方法,不生成 `_Ready()` / `_ExitTree()`
这是当前和 `[GetNode]` 最大的区别:
- `[GetNode]` 在缺少 `_Ready()` 时会补一个 override
- `[BindNodeSignal]` 只生成 `__BindNodeSignals_Generated()``__UnbindNodeSignals_Generated()`
所以你需要自己决定在哪个生命周期里调用它们。
### 已有生命周期但没调用时会给 warning
如果类型已经定义了 `_Ready()``_ExitTree()`,但没有调用对应生成方法,当前会给出 warning提醒你完成接线。
这意味着它更像“声明式订阅语法”,而不是“自动生命周期织入”。
## 当前契约
`[BindNodeSignal(nodeFieldName, signalName)]` 的两个参数都指向现有代码里的稳定符号:
- `nodeFieldName`:目标节点字段名
- `signalName`:该节点类型上的 CLR event 名
最推荐的写法仍然是:
```csharp
// ✅ 推荐:使用 [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] 组合使用
## 当前会验证什么
生成器不是盲目拼字符串。按当前源码,它会在编译期验证:
- 方法必须是实例方法
- `nodeFieldName` 必须能解析到当前类型里的实例字段
- 该字段类型必须继承 `Godot.Node`
- `signalName` 必须能解析到该字段类型上的 CLR event
- 处理方法签名必须和 event delegate 兼容
例如:
- `Button.Pressed` 对应无参处理方法
- `SpinBox.ValueChanged` 对应 `double` 参数
如果签名不匹配,会直接报错,而不是生成一个运行时才失败的订阅。
## 多重绑定
`BindNodeSignalAttribute` 允许重复标记在同一个方法上,所以一个处理方法可以绑定多个事件:
在需要架构访问的场景中,与 `[ContextAware]` 结合:
```csharp
[BindNodeSignal(nameof(_buttonA), nameof(Button.Pressed))]
[BindNodeSignal(nameof(_buttonB), nameof(Button.Pressed))]
[BindNodeSignal(nameof(_buttonC), nameof(Button.Pressed))]
private void OnAnyButtonPressed()
using GFramework.Core.SourceGenerators.Abstractions.Rule;
using GFramework.Godot.SourceGenerators.Abstractions;
[ContextAware]
public partial class GameController : Node
{
[GetNode]
private Button _actionButton = null!;
private IGameModel _gameModel = null!;
[BindNodeSignal(nameof(_actionButton), nameof(Button.Pressed))]
private void OnActionButtonPressed()
{
// 可以直接使用架构功能
this.SendCommand(new PlayerActionCommand());
}
public override void _Ready()
{
__InjectContextBindings_Generated();
__InjectGetNodes_Generated();
__BindNodeSignals_Generated();
}
public override void _ExitTree()
{
__UnbindNodeSignals_Generated();
}
}
```
当前生成器会为每个特性都生成一条 `+=` 和一条 `-=`
## 相关文档
`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`
- [Source Generators 概述](./index)
- [GetNode 生成器](./get-node-generator)
- [ContextAware 生成器](./context-aware-generator)
- [Godot 信号文档](https://docs.godotengine.org/en/stable/classes/class_signal.html)

View File

@ -1,198 +1,403 @@
---
title: ContextAware 生成器
description: 说明 [ContextAware] 当前会生成什么、何时使用、与 ContextAwareBase 的边界以及测试场景。
---
# ContextAware 生成器
`[ContextAware]``GFramework.Core.SourceGenerators` 中最常用的一类生成器。它的职责很明确:
> 自动实现 IContextAware 接口,提供架构上下文访问能力
- 为当前类型自动补齐 `IContextAware`
- 提供可复用的上下文懒加载入口
- 让类型可以直接使用 `this.GetSystem<T>()``this.GetModel<T>()``this.GetUtility<T>()` 等扩展方法
## 概述
它不负责注册服务,也不会替你决定应该取哪个 `System` / `Model`。它解决的是“当前类型如何拿到架构上下文”。
ContextAware 生成器为标记了 `[ContextAware]` 属性的类自动生成 `IContextAware` 接口实现,使类能够便捷地访问架构上下文(
`IArchitectureContext`)。这是 GFramework 中最常用的源码生成器之一,几乎所有需要与架构交互的组件都会使用它。
## 当前包关系
### 核心功能
- 特性来源:`GFramework.Core.SourceGenerators.Abstractions`
- 生成器实现:`GFramework.Core.SourceGenerators`
- 运行时接口:`GFramework.Core.Abstractions.Rule.IContextAware`
- 常用扩展方法:`GFramework.Core.Extensions`
- **自动接口实现**:无需手动实现 `IContextAware` 接口的 `SetContext()``GetContext()` 方法
- **懒加载上下文**`Context` 属性在首次访问时自动初始化
- **默认提供者**:使用 `GameContextProvider` 作为默认上下文提供者
- **测试友好**:支持通过 `SetContextProvider()` 配置自定义上下文提供者
如果只安装运行时 `GFramework.Core` 而没有安装 `Core.SourceGenerators``[ContextAware]` 本身不会生效。
## 基础使用
## 最小用法
### 标记类
使用 `[ContextAware]` 属性标记需要访问架构上下文的类:
```csharp
using GFramework.Core.Abstractions.Controller;
using GFramework.Core.Extensions;
using GFramework.Core.SourceGenerators.Abstractions.Rule;
using GFramework.Core.Abstractions.Controller;
[ContextAware]
public partial class PlayerController : IController
{
public void Initialize()
{
var playerModel = this.GetModel<IPlayerModel>();
var combatSystem = this.GetSystem<ICombatSystem>();
// 使用扩展方法访问架构([ContextAware] 实现 IContextAware 接口)
var playerModel = this.GetModel<PlayerModel>();
var combatSystem = this.GetSystem<CombatSystem>();
combatSystem.Bind(playerModel);
this.SendEvent(new PlayerInitializedEvent());
}
public void Attack(Enemy target)
{
var damage = this.GetUtility<DamageCalculator>().Calculate(this, target);
this.SendCommand(new DealDamageCommand(target, damage));
}
}
```
当前最重要的前置条件只有两个:
### 必要条件
- 必须是 `class`
- 必须声明为 `partial`
标记的类必须满足以下条件:
如果缺少这两个条件,生成器不会补代码。
## 当前会生成什么
按当前源码,`[ContextAware]` 会为目标类型生成:
- `IContextAware` 的显式接口实现
- 受保护的 `Context` 属性
- 类型级静态 `SetContextProvider(...)`
- 类型级静态 `ResetContextProvider()`
- 一个实例级 `_context` 缓存字段
- 一个类型级共享的 `_contextProvider`
- 一个类型级锁 `_contextSync`
这意味着它不是“每次访问都现查当前架构”。当前行为更接近:
1. 先看当前实例是否已经缓存了上下文
2. 如果没有,就在同步锁内取共享 provider
3. 若 provider 为空,则回退到 `GameContextProvider`
4. 把拿到的上下文缓存到当前实例
## 当前语义里最容易误解的点
### provider 是按类型共享,不是按实例共享
`SetContextProvider(...)` 影响的是“这个生成类型的后续实例或尚未初始化上下文的实例”,不是全仓库所有 `[ContextAware]`
类型共享同一个 provider。
### provider 切换不会自动刷新已缓存实例
一旦某个实例已经把上下文缓存进 `_context`,后续再调用:
- `SetContextProvider(...)`
- `ResetContextProvider()`
都不会自动改写这个实例的已缓存上下文。
如果你确实要覆盖某个现有实例的上下文,应显式调用:
1. **必须是 `partial` 类**:生成器需要生成部分类代码
2. **必须是 `class` 类型**:不能是 `struct``interface`
```csharp
((IContextAware)controller).SetContext(context);
// ✅ 正确
[ContextAware]
public partial class MyController { }
// ❌ 错误:缺少 partial 关键字
[ContextAware]
public class MyController { }
// ❌ 错误:不能用于 struct
[ContextAware]
public partial struct MyStruct { }
```
### 生成路径和 `ContextAwareBase` 不是一回事
## 生成的代码
当前源码里两者的默认回退策略不同:
- `[ContextAware]` 生成实现
- 通过共享 provider 回退,默认 provider 是 `GameContextProvider`
- 带同步锁,支持 `SetContextProvider(...)` / `ResetContextProvider()`
- `ContextAwareBase`
- 只维护简单的实例级缓存
- 不维护共享 provider
- 默认直接回退到 `GameContext.GetFirstArchitectureContext()`
因此,旧文档里把两条路径混写成“只是写法不同”已经不准确。
## 何时使用 `[ContextAware]`
优先用于这些场景:
- 你的类型不是 `AbstractSystem``AbstractModel``AbstractCommand` 这类已经继承 `ContextAwareBase` 的框架基类
- 你希望在测试中显式切换 provider
- 你需要在同一生成类型上统一切换上下文来源
- 你在 Godot 节点、Controller、ViewModel、包装器类型上只想获得上下文访问能力
典型例子:
- `IController` 实现
- Godot `Node` / `Control` 的项目侧包装器
- 不继承框架基类但要访问架构的辅助类型
## 何时改用 `ContextAwareBase`
以下场景优先考虑 `ContextAwareBase` 或已经继承它的框架基类:
- 你本来就继承 `AbstractSystem``AbstractModel``AbstractCommand``AbstractQuery`
- 你不需要类型级共享 provider
- 你只需要简单的实例级上下文缓存
- 调用线程模型已经天然串行,不需要生成实现那套 provider 切换与同步语义
如果一个类型已经通过继承链拿到了 `ContextAwareBase`,通常没必要再额外标 `[ContextAware]`
## 与 Context Get 注入的关系
`[GetModel]``[GetSystem]``[GetUtility]``[GetService]` 这类字段注入生成器,并不是独立工作的。
按当前 `ContextGetGenerator` 的判定规则,目标类型必须满足以下三者之一:
- 标记了 `[ContextAware]`
- 实现了 `IContextAware`
- 继承了 `ContextAwareBase`
所以更准确的理解是:
- `[ContextAware]` 负责“让类型成为 context-aware 类型”
- Context Get 系列特性负责“在这个前提下继续减少字段取值样板”
## 测试场景
如果测试里不想依赖默认全局上下文,推荐显式配置 provider
编译器会为标记的类自动生成以下代码:
```csharp
PlayerController.SetContextProvider(new TestContextProvider(testArchitecture.Context));
// <auto-generated/>
#nullable enable
try
namespace YourNamespace;
partial class PlayerController : global::GFramework.Core.Abstractions.Rule.IContextAware
{
var controller = new PlayerController();
controller.Initialize();
private global::GFramework.Core.Abstractions.Architecture.IArchitectureContext? _context;
private static global::GFramework.Core.Abstractions.Architecture.IArchitectureContextProvider? _contextProvider;
/// <summary>
/// 自动获取的架构上下文(懒加载,默认使用 GameContextProvider
/// </summary>
protected global::GFramework.Core.Abstractions.Architecture.IArchitectureContext Context
{
get
{
if (_context == null)
{
_contextProvider ??= new global::GFramework.Core.Architecture.GameContextProvider();
_context = _contextProvider.GetContext();
}
return _context;
}
}
/// <summary>
/// 配置上下文提供者(用于测试或多架构场景)
/// </summary>
/// <param name="provider">上下文提供者实例</param>
public static void SetContextProvider(global::GFramework.Core.Abstractions.Architecture.IArchitectureContextProvider provider)
{
_contextProvider = provider;
}
/// <summary>
/// 重置上下文提供者为默认值(用于测试清理)
/// </summary>
public static void ResetContextProvider()
{
_contextProvider = null;
}
void global::GFramework.Core.Abstractions.Rule.IContextAware.SetContext(global::GFramework.Core.Abstractions.Architecture.IArchitectureContext context)
{
_context = context;
}
global::GFramework.Core.Abstractions.Architecture.IArchitectureContext global::GFramework.Core.Abstractions.Rule.IContextAware.GetContext()
{
return Context;
}
}
finally
```
### 代码解析
生成的代码包含以下关键部分:
1. **私有字段**
- `_context`:缓存的上下文实例
- `_contextProvider`:静态上下文提供者(所有实例共享)
2. **Context 属性**
- `protected` 访问级别,子类可访问
- 懒加载:首次访问时自动初始化
- 使用 `GameContextProvider` 作为默认提供者
3. **配置方法**
- `SetContextProvider()`:设置自定义上下文提供者
- `ResetContextProvider()`:重置为默认提供者
4. **显式接口实现**
- `IContextAware.SetContext()`:允许外部设置上下文
- `IContextAware.GetContext()`:返回当前上下文
## 配置上下文提供者
### 测试场景
在单元测试中,通常需要使用自定义的上下文提供者:
```csharp
[Test]
public async Task TestPlayerController()
{
// 创建测试架构
var testArchitecture = new TestArchitecture();
await testArchitecture.InitAsync();
// 配置自定义上下文提供者
PlayerController.SetContextProvider(new TestContextProvider(testArchitecture));
try
{
// 测试代码
var controller = new PlayerController();
controller.Initialize();
// 验证...
}
finally
{
// 清理:重置上下文提供者
PlayerController.ResetContextProvider();
}
}
```
### 多架构场景
在某些高级场景中,可能需要同时运行多个架构实例:
```csharp
public class MultiArchitectureManager
{
private readonly Dictionary<string, IArchitecture> _architectures = new();
public void SwitchToArchitecture(string name)
{
var architecture = _architectures[name];
var provider = new ScopedContextProvider(architecture);
// 为所有使用 [ContextAware] 的类切换上下文
PlayerController.SetContextProvider(provider);
EnemyController.SetContextProvider(provider);
// ...
}
}
```
## 使用场景
### 何时使用 [ContextAware]
推荐在以下场景使用 `[ContextAware]` 属性:
1. **Controller 层**:需要协调多个 Model/System 的控制器
2. **Command/Query 实现**:需要访问架构服务的命令或查询
3. **自定义组件**:不继承框架基类但需要上下文访问的组件
```csharp
[ContextAware]
public partial class GameFlowController : IController
{
public async Task StartGame()
{
var saveSystem = this.GetSystem<SaveSystem>();
var uiSystem = this.GetSystem<UISystem>();
await saveSystem.LoadAsync();
await uiSystem.ShowMainMenuAsync();
}
}
```
### 与 IController 配合使用
在 Godot 项目中,控制器通常同时实现 `IController` 和使用 `[ContextAware]`
```csharp
using GFramework.Core.Abstractions.Controller;
using GFramework.Core.SourceGenerators.Abstractions.Rule;
[ContextAware]
public partial class PlayerController : Node, IController
{
public override void _Ready()
{
// 使用扩展方法访问架构([ContextAware] 实现 IContextAware 接口)
var playerModel = this.GetModel<PlayerModel>();
var combatSystem = this.GetSystem<CombatSystem>();
}
}
```
**说明**
- `IController` 是标记接口,标识这是一个控制器
- `[ContextAware]` 提供架构访问能力
- 两者配合使用是推荐的模式
### 何时继承 ContextAwareBase
如果类需要更多框架功能(如生命周期管理),应继承 `ContextAwareBase`
```csharp
// 推荐:需要生命周期管理时继承基类
public class PlayerModel : AbstractModel
{
// AbstractModel 已经继承了 ContextAwareBase
protected override void OnInit()
{
var config = this.GetUtility<ConfigLoader>().Load<PlayerConfig>();
}
}
// 推荐:简单组件使用属性
[ContextAware]
public partial class SimpleHelper
{
public void DoSomething()
{
this.SendEvent(new SomethingHappenedEvent());
}
}
```
## 与 IContextAware 接口的关系
生成的代码实现了 `IContextAware` 接口:
```csharp
namespace GFramework.Core.Abstractions.Rule;
public interface IContextAware
{
void SetContext(IArchitectureContext context);
IArchitectureContext GetContext();
}
```
这意味着标记了 `[ContextAware]` 的类可以:
1. **被架构自动注入上下文**:实现 `IContextAware` 的类在注册到架构时会自动调用 `SetContext()`
2. **参与依赖注入**:可以作为 `IContextAware` 类型注入到其他组件
3. **支持上下文传递**:可以通过 `GetContext()` 将上下文传递给其他组件
## 最佳实践
### 1. 始终使用 partial 关键字
```csharp
// ✅ 正确
[ContextAware]
public partial class MyController { }
// ❌ 错误:编译器会报错
[ContextAware]
public class MyController { }
```
### 2. 在测试中清理上下文提供者
```csharp
[TearDown]
public void TearDown()
{
// 避免测试之间的状态污染
PlayerController.ResetContextProvider();
EnemyController.ResetContextProvider();
}
```
需要注意两点:
### 3. 避免在构造函数中访问 Context
- `ResetContextProvider()` 只会重置共享 provider不会清除已创建实例上的 `_context`
- 如果测试要复用同一实例并切换上下文,应该显式调用 `((IContextAware)instance).SetContext(...)`
```csharp
[ContextAware]
public partial class MyController
{
// ❌ 错误:构造函数执行时上下文可能未初始化
public MyController()
{
var model = this.GetModel<SomeModel>(); // 可能为 null
}
## 诊断与约束
// ✅ 正确:在初始化方法中访问
public void Initialize()
{
var model = this.GetModel<SomeModel>(); // 安全
}
}
```
当前文档里最值得记住的约束只有这些:
### 4. 优先使用 Context 属性而非接口方法
- 非 `class` 会触发 `GF_Rule_001`
- 非 `partial` 不会生成实现,并会触发公共 partial 约束诊断
- 嵌套、字段注入等其他错误通常由对应的 Context Get 生成器和其诊断补充报告
```csharp
[ContextAware]
public partial class MyController
{
public void DoSomething()
{
// ✅ 推荐:使用扩展方法
var model = this.GetModel<SomeModel>();
## 与旧写法的边界
// ❌ 不推荐:显式调用接口方法
var context = ((IContextAware)this).GetContext();
var model2 = context.GetModel<SomeModel>();
}
}
```
下面这些旧说法已经不够准确:
## 诊断信息
- “`[ContextAware]` 只是帮你补一个简单的 `GetContext()`
- “切换 provider 后,已有实例会自动跟着切换”
- “`[ContextAware]``ContextAwareBase` 的默认行为完全一致”
生成器会在以下情况报告编译错误:
当前更准确的理解是:
### GFSG001: 类必须是 partial
- 生成实现带有实例缓存、类型级共享 provider 和同步锁
- provider 切换只影响尚未缓存上下文的实例
- `ContextAwareBase` 是更轻量的实例级缓存路径
```csharp
[ContextAware]
public class MyController { } // 错误:缺少 partial 关键字
```
## 推荐阅读
**解决方案**:添加 `partial` 关键字
1. [context-get-generator.md](./context-get-generator.md)
2. [logging-generator.md](./logging-generator.md)
3. [../core/index.md](../core/index.md)
4. `GFramework.Core.SourceGenerators/README.md`
```csharp
[ContextAware]
public partial class MyController { } // ✅ 正确
```
### GFSG002: ContextAware 只能用于类
```csharp
[ContextAware]
public partial struct MyStruct { } // 错误:不能用于 struct
```
**解决方案**:将 `struct` 改为 `class`
```csharp
[ContextAware]
public partial class MyClass { } // ✅ 正确
```
## 相关文档
- [Source Generators 概述](./index)
- [架构上下文](../core/context)
- [IContextAware 接口](../core/rule)
- [日志生成器](./logging-generator)

View File

@ -1,198 +1,496 @@
---
title: GetNode 生成器
description: 说明 [GetNode] 当前生成什么、路径如何推断,以及 _Ready 生命周期里的接入边界。
---
# GetNode 生成器
`[GetNode]` 用来把 Godot 节点查找样板收敛到生成器里。它只处理“字段如何取到节点”,不负责事件订阅,也不负责其他运行时装配。
> 自动生成 Godot 节点获取逻辑,简化节点引用代码
## 当前包关系
## 概述
- 特性来源:`GFramework.Godot.SourceGenerators.Abstractions`
- 生成器实现:`GFramework.Godot.SourceGenerators`
- 目标类型基线:字段类型必须继承 `Godot.Node`
GetNode 生成器为标记了 `[GetNode]` 特性的字段自动生成 Godot 节点获取代码,无需手动调用 `GetNode<T>()` 方法。这在处理复杂
UI 或场景树结构时特别有用。
## 最小用法
### 核心功能
- **自动节点获取**:根据路径或字段名自动获取节点
- **多种查找模式**:支持唯一名、相对路径、绝对路径查找
- **可选节点支持**:可以标记节点为可选,获取失败时返回 null
- **智能路径推导**:未显式指定路径时自动从字段名推导
- **_Ready 钩子生成**:自动生成 `_Ready()` 方法注入节点获取逻辑
## 基础使用
### 标记节点字段
使用 `[GetNode]` 特性标记需要自动获取的节点字段:
```csharp
using GFramework.Godot.SourceGenerators.Abstractions;
using Godot;
public partial class TopBar : HBoxContainer
public partial class PlayerHud : Control
{
[GetNode]
private HBoxContainer _leftContainer = null!;
private Label _healthLabel = null!;
[GetNode]
private HBoxContainer m_rightContainer = null!;
private ProgressBar _manaBar = null!;
[GetNode("ScoreContainer/ScoreValue")]
private Label _scoreLabel = null!;
public override void _Ready()
{
__InjectGetNodes_Generated();
_healthLabel.Text = "100";
}
}
```
如果目标类型还没有 `_Ready()`,当前生成器会补出:
### 生成的代码
编译器会为标记的类自动生成以下代码:
```csharp
private void __InjectGetNodes_Generated()
// <auto-generated />
#nullable enable
namespace YourNamespace;
partial class PlayerHud
{
_leftContainer = GetNode<global::Godot.HBoxContainer>("%LeftContainer");
m_rightContainer = GetNode<global::Godot.HBoxContainer>("%RightContainer");
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!; // 错误
}
}
partial void OnGetNodeReadyGenerated();
// ✅ 正确
public partial class Inner
{
[GetNode]
private Label _label = null!;
}
```
### GF_Godot_GetNode_002 - 不支持静态字段
**错误信息**`Field '{FieldName}' cannot be static when using [GetNode]`
**解决方案**:改为实例字段
```csharp
// ❌ 错误
[GetNode]
private static Label _label = null!;
// ✅ 正确
[GetNode]
private Label _label = null!;
```
### GF_Godot_GetNode_003 - 不支持只读字段
**错误信息**`Field '{FieldName}' cannot be readonly when using [GetNode]`
**解决方案**:移除 `readonly` 关键字
```csharp
// ❌ 错误
[GetNode]
private readonly Label _label = null!;
// ✅ 正确
[GetNode]
private Label _label = null!;
```
### GF_Godot_GetNode_004 - 字段类型必须继承自 Godot.Node
**错误信息**`Field '{FieldName}' must be a Godot.Node type to use [GetNode]`
**解决方案**:确保字段类型继承自 `Godot.Node`
```csharp
// ❌ 错误
[GetNode]
private string _text = null!; // string 不是 Node 类型
// ✅ 正确
[GetNode]
private Label _label = null!; // Label 继承自 Node
```
### GF_Godot_GetNode_005 - 无法推导路径
**错误信息**`Field '{FieldName}' does not provide a path and its name cannot be converted to a node path`
**解决方案**:显式指定节点路径
```csharp
// ❌ 错误:字段名无法转换为有效路径
[GetNode]
private Label _ = null!;
// ✅ 正确
[GetNode("UI/Label")]
private Label _ = null!;
```
### GF_Godot_GetNode_006 - 需要在 _Ready 中调用注入方法
**警告信息**
`Class '{ClassName}' defines _Ready(); call __InjectGetNodes_Generated() there or remove _Ready() to use the generated hook`
**解决方案**:在 `_Ready()` 中手动调用 `__InjectGetNodes_Generated()`
```csharp
public partial class MyHud : Control
{
[GetNode]
private Label _label = null!;
public override void _Ready()
{
__InjectGetNodes_Generated(); // ✅ 必须手动调用
// 其他初始化...
}
}
```
## 最佳实践
### 1. 使用一致的命名约定
保持字段名与场景树中节点名的一致性:
```csharp
// ✅ 推荐:字段名与节点名一致
[GetNode]
private Label _healthLabel = null!; // 场景中的节点名为 HealthLabel
[GetNode]
private Button _startButton = null!; // 场景中的节点名为 StartButton
```
### 2. 优先使用唯一名查找
在 Godot 编辑器中为重要节点启用唯一名Unique Name然后使用 `[GetNode]`
```csharp
// Godot 场景中:%HealthBar唯一名已启用
// C# 代码中:
[GetNode]
private ProgressBar _healthBar = null!; // 自动使用 %HealthBar
```
### 3. 合理处理可选节点
对于可能不存在的节点,使用 `Required = false`
```csharp
public partial class DynamicUI : Control
{
[GetNode]
private Label _titleLabel = null!;
// 可选组件
[GetNode(Required = false)]
private TextureRect? _iconImage;
public override void _Ready()
{
__InjectGetNodes_Generated();
// 安全地初始化可选组件
if (_iconImage != null)
{
_iconImage.Texture = LoadDefaultIcon();
}
}
}
```
### 4. 组织复杂 UI 的路径
对于深层嵌套的 UI使用显式路径
```csharp
public partial class ComplexUI : Control
{
// 使用相对路径明确表达层级关系
[GetNode("MainContent/Header/Title")]
private Label _title = null!;
[GetNode("MainContent/Body/Stats/Health")]
private Label _healthValue = null!;
[GetNode("MainContent/Footer/ActionButtons/Save")]
private Button _saveButton = null!;
}
```
### 5. 与 GetNode 方法的对比
| 方式 | 代码量 | 可维护性 | 类型安全 | 推荐场景 |
|----------------|-----|------|--------|-----------|
| 手动 `GetNode()` | 多 | 中 | 需要显式转换 | 简单场景 |
| `[GetNode]` 特性 | 少 | 高 | 编译时检查 | 复杂 UI、控制器 |
```csharp
// ❌ 不推荐:手动获取
public override void _Ready()
{
__InjectGetNodes_Generated();
OnGetNodeReadyGenerated();
_healthLabel = GetNode<Label>("%HealthLabel");
_manaBar = GetNode<ProgressBar>("%ManaBar");
_scoreLabel = GetNode<Label>("ScoreContainer/ScoreValue");
}
```
这个行为来自当前生成器测试,不是文档约定。
// ✅ 推荐:使用 [GetNode] 特性
[GetNode]
private Label _healthLabel = null!;
## 当前路径推断规则
[GetNode]
private ProgressBar _manaBar = null!;
### 没写路径时
如果 `[GetNode]` 没有显式路径,当前默认按字段名推导唯一名路径:
- `_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提醒你手动接入。
## 相关文档
## 当前强约束
这些约束都直接来自生成器源码和测试:
- 目标类型必须是顶层 `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`
- [Source Generators 概述](./index)
- [BindNodeSignal 生成器](./bind-node-signal-generator)
- [ContextAware 生成器](./context-aware-generator)
- [Godot 节点文档](https://docs.godotengine.org/en/stable/classes/class_node.html)

View File

@ -1,64 +1,52 @@
---
title: Godot 项目元数据生成器
description: 说明 project.godot 当前会生成什么、何时生效,以及 AutoLoad 和 Input Action 的映射边界。
---
# Godot 项目元数据生成器
`GodotProjectMetadataGenerator` 读取 `project.godot`,把 Godot 工程级配置转成稳定的编译期入口。
> 从 `project.godot` 生成 AutoLoad 与 Input Action 的强类型访问入口。
当前只覆盖两类信息:
## 概述
- `[autoload]` 段生成 `GFramework.Godot.Generated.AutoLoads`
- `[input]` 段生成 `GFramework.Godot.Generated.InputActions`
`GFramework.Godot.SourceGenerators` 会读取 Godot 项目根目录下的 `project.godot`,并把其中最常用的项目级元数据暴露为稳定的编译期
API。
它不处理场景节点注入,也不处理节点事件绑定。这两部分分别由 `/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`
- 特性来源:`GFramework.Godot.SourceGenerators.Abstractions`
- 生成器实现:`GFramework.Godot.SourceGenerators`
- 运行时依赖:`GFramework.Godot`
- 消费侧生成命名空间:`GFramework.Godot.Generated`
这项能力的目标不是替代场景级生成器,而是把 Godot 工程配置和 C# 代码之间的字符串约定收敛到编译期。
## 最小接入路径
## 接入方式
### NuGet 引用
常规 Godot C# 项目安装 `GeWuYou.GFramework.Godot.SourceGenerators` 后,包内 `targets` 会自动做两件事:
当项目通过 NuGet 引用 `GeWuYou.GFramework.Godot.SourceGenerators` 时,生成器会默认把项目根目录下的 `project.godot` 加入
`AdditionalFiles`
1. 注入 analyzer
2. 如果项目根目录存在 `project.godot`,把它加入 `AdditionalFiles`
如需覆盖默认路径,可以设置:
- 可以改成项目根目录下的其他相对路径
- 文件名必须仍然是 `project.godot`,否则生成器会给出警告并忽略该文件
```xml
<ItemGroup>
<PackageReference Include="GeWuYou.GFramework.Godot.SourceGenerators"
Version="x.y.z"
PrivateAssets="all"
ExcludeAssets="runtime" />
</ItemGroup>
<PropertyGroup>
<GFrameworkGodotProjectFile>Config/project.godot</GFrameworkGodotProjectFile>
</PropertyGroup>
```
### 仓库内直接引用生成器
如果你通过 `ProjectReference(OutputItemType=Analyzer)` 直接引用生成器项目,需要自己把 `project.godot` 放进
`AdditionalFiles`
如果你通过 `ProjectReference(OutputItemType=Analyzer)` 直接引用生成器项目,则需要手动加入:
```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]
@ -78,14 +66,33 @@ 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)`
### 显式映射
这些访问器最终都通过当前 `SceneTree.Root` 解析 `/root/<AutoLoadName>`
当 AutoLoad 名称无法仅靠类名唯一推断时,可以使用 `[AutoLoad]` 明确指定:
### Input Action 常量
```csharp
using GFramework.Godot.SourceGenerators.Abstractions;
using Godot;
[AutoLoad("GameServices")]
public partial class GameServices : Node
{
}
```
规则如下:
- 显式 `[AutoLoad]` 映射优先于隐式类名推断
- 标记了 `[AutoLoad]` 的类型必须继承 `Godot.Node`
- 若多个类型映射到同一个 AutoLoad生成器会报告诊断并退化为 `Godot.Node` 访问器,直到映射唯一
## Input Action 常量
### 基础行为
假设 `project.godot` 中有:
@ -107,114 +114,59 @@ if (Input.IsActionJustPressed(InputActions.MoveUp))
}
```
这部分只生成稳定字符串常量,不会替你封装 `Input` 调用。
转换规则:
## AutoLoad 类型推断的当前规则
- `move_up` -> `MoveUp`
- `ui_cancel` -> `UiCancel`
- 非法字符会被清理后再转换为 PascalCase
- 如果多个动作名落到同一个标识符,生成器会追加稳定数字后缀,例如 `MoveUp_2`
### 优先级顺序
## 与现有 Godot 生成器的关系
当前映射顺序是
这项能力和现有的场景级生成器是互补的
1. 显式 `[AutoLoad("Name")]`
2. 按 C# 类型名与 AutoLoad 名称做唯一匹配
3. 无法唯一确定时退化为 `Godot.Node`
- `AutoLoads` / `InputActions` 解决的是项目级元数据访问
- `[GetNode]` 解决的是场景节点引用注入
- `[BindNodeSignal]` 解决的是节点事件订阅样板
例如
推荐组合方式
```csharp
using GFramework.Godot.Generated;
using GFramework.Godot.SourceGenerators.Abstractions;
using Godot;
[AutoLoad("GameServices")]
public partial class GameServices : Node
public partial class MainHud : Control
{
[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 映射不唯一时,会退化为 `Godot.Node`
- 标识符冲突会追加稳定后缀,而不是覆盖已有成员
- 重复条目只保留第一条声明
- `[AutoLoad]` 标记在非 `Godot.Node` 类型上
- 多个类型映射到同一个 AutoLoad 名称
- 不同 AutoLoad 名称或 Input Action 名称在清洗后发生标识符冲突
- `project.godot` 内部重复声明同名 AutoLoad 或 Input Action
## 推荐阅读
这些诊断的目的不是阻断所有生成,而是在可能的情况下保留稳定输出,同时把不确定性显式暴露出来。
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`
## 相关文档
- [GetNode 生成器](./get-node-generator)
- [BindNodeSignal 生成器](./bind-node-signal-generator)
- [Godot 集成教程](../tutorials/godot-integration)

View File

@ -1,104 +1,250 @@
---
title: Priority 生成器
description: 说明 [Priority] 当前会生成什么、何时生效、应配合哪些优先级 API 使用,以及动态优先级的边界。
---
# Priority 生成器
`[Priority]` 的职责很简单:为目标类型自动生成 `IPrioritized.Priority`
> 自动实现 IPrioritized 接口,为类添加优先级标记
它本身不是调度器,也不会自动改变系统、服务或处理器的执行顺序。只有调用方使用了“按优先级排序”的检索入口,生成出来的
`Priority` 才会真正影响顺序。
Priority 生成器通过源代码生成器自动实现 `IPrioritized` 接口,简化优先级标记和排序逻辑的实现。
## 当前包关系
## 概述
- 特性来源:`GFramework.Core.SourceGenerators.Abstractions`
- 生成器实现:`GFramework.Core.SourceGenerators`
- 运行时契约:`GFramework.Core.Abstractions.Bases.IPrioritized`
- 预定义常量:`GFramework.Core.Abstractions.Bases.PriorityGroup`
### 核心功能
## 最小用法
- **自动实现接口**:自动实现 `IPrioritized` 接口的 `Priority` 属性
- **优先级标记**:通过特性参数指定优先级值
- **编译时生成**:在编译时生成代码,零运行时开销
- **类型安全**:编译时类型检查,避免运行时错误
### 适用场景
- 系统初始化顺序控制
- 事件处理器优先级排序
- 服务注册顺序管理
- 需要按优先级排序的任何场景
## 基础使用
### 标记优先级
使用 `[Priority]` 特性为类标记优先级:
```csharp
using GFramework.Core.Abstractions.Bases;
using GFramework.Core.SourceGenerators.Abstractions.Bases;
[Priority(PriorityGroup.High)]
public partial class SaveSystem : AbstractSystem
[Priority(10)]
public partial class MySystem
{
protected override void OnInit()
{
}
// 自动生成 IPrioritized 接口实现
}
```
当前生成器会补出:
### 生成代码
编译器会自动生成如下代码:
```csharp
public int Priority => PriorityGroup.High;
// <auto-generated/>
#nullable enable
namespace YourNamespace;
partial class MySystem : global::GFramework.Core.Abstractions.Bases.IPrioritized
{
/// <summary>
/// 获取优先级值: 10
/// </summary>
public int Priority => 10;
}
```
优先级值越小,优先级越高。
### 使用生成的优先级
## 当前真正会读取优先级的入口
### `IIocContainer`
如果你直接在容器层取集合,使用:
```csharp
var handlers = container.GetAllByPriority<IMyHandler>();
```
### `IArchitectureContext`
当前推荐按组件类别使用这些 API
- `GetServicesByPriority<TService>()`
- `GetSystemsByPriority<TSystem>()`
- `GetModelsByPriority<TModel>()`
- `GetUtilitiesByPriority<TUtility>()`
### `IContextAware` 扩展方法
如果你已经在 `[ContextAware]` 类型或 `ContextAwareBase` 派生类型里,直接用:
- `this.GetServicesByPriority<TService>()`
- `this.GetSystemsByPriority<TSystem>()`
- `this.GetModelsByPriority<TModel>()`
- `this.GetUtilitiesByPriority<TUtility>()`
这比旧文档里反复出现的 `this.GetAllByPriority<T>()` 更贴近当前公开扩展方法。
## 最小接入示例
### 系统排序
生成的 `Priority` 属性可用于排序:
```csharp
using GFramework.Core.Abstractions.Bases;
using GFramework.Core.Abstractions.Systems;
using GFramework.Core.Extensions;
using GFramework.Core.SourceGenerators.Abstractions.Bases;
using GFramework.Core.SourceGenerators.Abstractions.Rule;
// 获取所有实现了 IPrioritized 的系统
var systems = new List<IPrioritized> { system1, system2, system3 };
// 按优先级排序(值越小,优先级越高)
var sorted = systems.OrderBy(s => s.Priority).ToList();
// 依次初始化
foreach (var system in sorted)
{
system.Initialize();
}
```
## 优先级值语义
### 值的含义
| 优先级值范围 | 含义 | 使用场景 |
|--------|-------|------------------|
| 负数 | 高优先级 | 核心系统、关键事件处理器 |
| 0 | 默认优先级 | 普通系统、一般事件处理器 |
| 正数 | 低优先级 | 可延迟初始化的系统、非关键处理器 |
### PriorityGroup 常量
推荐使用 `PriorityGroup` 预定义常量来标记优先级:
```csharp
using GFramework.Core.Abstractions.Bases;
using GFramework.Core.SourceGenerators.Abstractions.Bases;
[Priority(PriorityGroup.Critical)] // -100
public partial class InputSystem : AbstractSystem { }
[Priority(PriorityGroup.High)] // -50
public partial class PhysicsSystem : AbstractSystem { }
[Priority(PriorityGroup.Normal)] // 0
public partial class GameplaySystem : AbstractSystem { }
[Priority(PriorityGroup.Low)] // 50
public partial class AudioSystem : AbstractSystem { }
[Priority(PriorityGroup.Deferred)] // 100
public partial class CleanupSystem : AbstractSystem { }
```
**PriorityGroup 常量定义**
```csharp
namespace GFramework.Core.Abstractions.Bases;
public static class PriorityGroup
{
public const int Critical = -100; // 关键:输入、网络等
public const int High = -50; // 高:物理、碰撞等
public const int Normal = 0; // 正常:游戏逻辑等
public const int Low = 50; // 低:音频、特效等
public const int Deferred = 100; // 延迟:清理、统计等
}
```
## 使用场景
### 系统初始化顺序
控制系统初始化的顺序,确保依赖关系正确:
```csharp
using GFramework.Core.Abstractions.Systems;
using GFramework.Core.SourceGenerators.Abstractions.Bases;
using GFramework.Core.Abstractions.Bases;
// 输入系统最先初始化
[Priority(PriorityGroup.Critical)]
public partial class InputSystem : AbstractSystem
{
protected override void OnInit()
{
// 初始化输入处理
}
}
[ContextAware]
public partial class SystemBootstrapper : IController
// 物理系统次之
[Priority(PriorityGroup.High)]
public partial class PhysicsSystem : AbstractSystem
{
public void Start()
protected override void OnInit()
{
var systems = this.GetSystemsByPriority<ISystem>();
// 初始化物理引擎
}
}
// 游戏逻辑系统在中间
[Priority(PriorityGroup.Normal)]
public partial class GameplaySystem : AbstractSystem
{
protected override void OnInit()
{
// 初始化游戏逻辑
}
}
// 音频系统可以稍后
[Priority(PriorityGroup.Low)]
public partial class AudioSystem : AbstractSystem
{
protected override void OnInit()
{
// 初始化音频引擎
}
}
```
在架构中按优先级初始化:
```csharp
public class GameArchitecture : Architecture
{
protected override void InitSystems()
{
// 获取所有系统并按优先级排序
var systems = this.GetAllByPriority<ISystem>();
foreach (var system in systems)
{
system.Initialize();
system.Init();
}
}
}
```
### 事件处理器优先级
控制事件处理器的执行顺序:
```csharp
using GFramework.Core.Abstractions.Events;
// 关键事件处理器最先执行
[Priority(PriorityGroup.Critical)]
public partial class CriticalEventHandler : IEventHandler<CriticalEvent>
{
public void Handle(CriticalEvent e)
{
// 处理关键事件
}
}
// 普通处理器在中间执行
[Priority(PriorityGroup.Normal)]
public partial class NormalEventHandler : IEventHandler<CriticalEvent>
{
public void Handle(CriticalEvent e)
{
// 处理普通逻辑
}
}
// 日志记录器最后执行
[Priority(PriorityGroup.Deferred)]
public partial class EventLogger : IEventHandler<CriticalEvent>
{
public void Handle(CriticalEvent e)
{
// 记录日志
}
}
```
事件总线按优先级调用处理器:
```csharp
public class EventBus : IEventBus
{
public void Send<TEvent>(TEvent e) where TEvent : IEvent
{
// 获取所有处理器并按优先级排序
var handlers = this.GetAllByPriority<IEventHandler<TEvent>>();
foreach (var handler in handlers)
{
handler.Handle(e);
}
}
}
@ -106,111 +252,496 @@ public partial class SystemBootstrapper : IController
### 服务排序
控制多个服务实现的优先级:
```csharp
// 高优先级服务
[Priority(PriorityGroup.High)]
public partial class PremiumSaveMigration : ISaveMigration
public partial class PremiumService : IService
{
public void Execute()
{
// 优先执行
}
}
// 默认服务
[Priority(PriorityGroup.Normal)]
public partial class DefaultService : IService
{
public void Execute()
{
// 默认执行
}
}
// 后备服务
[Priority(PriorityGroup.Low)]
public partial class MetricsSaveMigration : ISaveMigration
public partial class FallbackService : IService
{
public void Execute()
{
// 最后备选
}
}
var migrations = architecture.Context.GetServicesByPriority<ISaveMigration>();
```
## `PriorityGroup` 的角色
## 与 PriorityUsageAnalyzer 集成
当前仓库提供了这些预定义常量:
### GF_Priority_Usage_001 诊断
- `PriorityGroup.Critical`
- `PriorityGroup.High`
- `PriorityGroup.Normal`
- `PriorityGroup.Low`
- `PriorityGroup.Deferred`
`PriorityUsageAnalyzer` 分析器会检测应该使用 `GetAllByPriority<T>()` 而非 `GetAll<T>()` 的场景:
文档不应该把这些值解释成硬编码的生命周期阶段。它们只是团队共享的排序语义常量,具体“高优先级意味着先做什么”仍然取决于
调用方对排序结果的使用方式。
如果项目有更细粒度的排序约定,也可以直接传 `int`,或在项目层自定义自己的优先级常量。
## 何时使用 `[Priority]`
适合以下场景:
- 类型顺序在编译期就能确定
- 你不想手写 `IPrioritized`
- 同一类型的所有实例都应共享同一个优先级
常见例子:
- 初始化顺序明确的系统
- 顺序敏感的服务实现
- 有先后要求的处理器或迁移器
## 何时不要使用 `[Priority]`
以下场景应改为手写 `IPrioritized`
- 优先级要依赖运行时配置
- 优先级要根据环境、开关或状态动态变化
- 你已经手动实现了 `IPrioritized`
例如:
**错误示例**
```csharp
public sealed class DynamicPrioritySystem : IPrioritized
// ❌ 不推荐:可能未按优先级排序
var systems = context.GetAll<ISystem>();
```
**正确示例**
```csharp
// ✅ 推荐:确保按优先级排序
var systems = context.GetAllByPriority<ISystem>();
```
### 分析器规则
当满足以下条件时,分析器会报告 `GF_Priority_Usage_001` 诊断:
1. 类型实现了 `IPrioritized` 接口
2. 使用了 `GetAll<T>()` 方法
3. 建议改用 `GetAllByPriority<T>()` 方法
## 诊断信息
### GF_Priority_001 - 只能应用于类
**错误信息**`Priority attribute can only be applied to classes`
**场景**:将 `[Priority]` 特性应用于非类类型
```csharp
[Priority(10)]
public interface IMyInterface // ❌ 错误
{
private readonly bool _enabled;
}
public DynamicPrioritySystem(bool enabled)
{
_enabled = enabled;
}
public int Priority => _enabled ? PriorityGroup.High : PriorityGroup.Deferred;
[Priority(10)]
public struct MyStruct // ❌ 错误
{
}
```
## 当前诊断与约束
**解决方案**:只在类上使用 `[Priority]` 特性
`[Priority]` 当前有几条直接约束:
```csharp
[Priority(10)]
public partial class MyClass // ✅ 正确
{
}
```
- `GF_Priority_001`
- 只能标在 `class`
- `GF_Priority_002`
- 目标类型已经手写实现 `IPrioritized`
- `GF_Priority_003`
- 类型必须是 `partial`
- `GF_Priority_004`
- 特性值缺失或无效
- `GF_Priority_005`
- 不支持嵌套类
### GF_Priority_002 - 已实现 IPrioritized 接口
对文档而言,最关键的结论是:
**错误信息**`Type '{ClassName}' already implements IPrioritized interface`
- `partial` 是强约束
- 顶层类是强约束
- 手写实现与生成实现只能二选一
**场景**:类已手动实现 `IPrioritized` 接口
## 与旧写法的边界
```csharp
[Priority(10)]
public partial class MySystem : IPrioritized // ❌ 冲突
{
public int Priority => 10; // 手动实现
}
```
下面这些旧写法或旧表述已经不再适合作为默认指导:
**解决方案**:移除 `[Priority]` 特性或移除手动实现
- 在 `IContextAware` 类型里统一写 `this.GetAllByPriority<T>()`
- 继续用 `system.Init()` 作为系统初始化示例
- 把 `[Priority]` 写成“标了就会自动改变执行顺序”
```csharp
// 方案1移除特性使用手动实现
public partial class MySystem : IPrioritized
{
public int Priority => 10;
}
当前更准确的理解是:
// 方案2移除手动实现使用生成器
[Priority(10)]
public partial class MySystem
{
// 生成器自动实现
}
```
- `[Priority]` 只生成 `Priority`
- 排序效果依赖容器、上下文或扩展方法是否走了 priority-aware API
- `IContextAware` 路径更推荐按组件类别使用 `GetSystemsByPriority` / `GetServicesByPriority` 等入口
### GF_Priority_003 - 必须声明为 partial
## 推荐阅读
**错误信息**`Class '{ClassName}' must be declared as partial`
1. [context-aware-generator.md](./context-aware-generator.md)
2. [context-get-generator.md](./context-get-generator.md)
3. [../core/index.md](../core/index.md)
4. `GFramework.Core.SourceGenerators/README.md`
**场景**:类未声明为 `partial`
```csharp
[Priority(10)]
public class MySystem // ❌ 缺少 partial
{
}
```
**解决方案**:添加 `partial` 关键字
```csharp
[Priority(10)]
public partial class MySystem // ✅ 正确
{
}
```
### GF_Priority_004 - 优先级值无效
**错误信息**`Priority value is invalid`
**场景**:特性参数无效或未提供
```csharp
[Priority] // ❌ 缺少参数
public partial class MySystem
{
}
```
**解决方案**:提供有效的优先级值
```csharp
[Priority(10)] // ✅ 正确
public partial class MySystem
{
}
```
### GF_Priority_005 - 不支持嵌套类
**错误信息**`Nested class '{ClassName}' is not supported`
**场景**:在嵌套类中使用 `[Priority]` 特性
```csharp
public partial class OuterClass
{
[Priority(10)]
public partial class InnerClass // ❌ 错误
{
}
}
```
**解决方案**:将嵌套类提取为独立的类
```csharp
[Priority(10)]
public partial class InnerClass // ✅ 正确
{
}
```
## 最佳实践
### 1. 使用 PriorityGroup 常量
避免使用魔法数字,优先使用预定义常量:
```csharp
// ✅ 推荐:使用常量
[Priority(PriorityGroup.Critical)]
public partial class InputSystem { }
// ❌ 不推荐:魔法数字
[Priority(-100)]
public partial class InputSystem { }
```
### 2. 预留优先级间隔
为未来扩展预留间隔:
```csharp
public static class SystemPriority
{
public const int Input = -100;
public const int PrePhysics = -90; // 预留扩展
public const int Physics = -80;
public const int PostPhysics = -70; // 预留扩展
public const int Gameplay = 0;
public const int PostGameplay = 10; // 预留扩展
public const int Audio = 50;
public const int Cleanup = 100;
}
```
### 3. 文档化优先级语义
为自定义优先级值添加注释:
```csharp
/// <summary>
/// 输入系统,优先级 -100需要最先初始化以接收输入事件
/// </summary>
[Priority(PriorityGroup.Critical)]
public partial class InputSystem : AbstractSystem
{
}
```
### 4. 避免优先级冲突
当多个类有相同优先级时,执行顺序不确定。应避免依赖特定顺序:
```csharp
// ❌ 不推荐:相同优先级,顺序不确定
[Priority(0)]
public partial class SystemA { }
[Priority(0)]
public partial class SystemB { }
// ✅ 推荐:明确区分优先级
[Priority(-10)]
public partial class SystemA { }
[Priority(0)]
public partial class SystemB { }
```
### 5. 只在真正需要排序的场景使用
不要滥用优先级特性,只在确实需要排序的场景使用:
```csharp
// ✅ 推荐:需要排序的系统
[Priority(PriorityGroup.Critical)]
public partial class InputSystem : AbstractSystem { }
// ❌ 不推荐:不需要排序的工具类
[Priority(10)]
public static partial class MathHelper // 静态工具类无需优先级
{
public static int Add(int a, int b) => a + b;
}
```
## 高级场景
### 泛型类支持
Priority 特性支持泛型类:
```csharp
[Priority(20)]
public partial class GenericSystem<T> : ISystem
{
public void Init()
{
// 泛型系统的初始化
}
}
```
### 与其他特性组合
Priority 可以与其他源代码生成器特性组合使用:
```csharp
using GFramework.Core.SourceGenerators.Abstractions.Bases;
using GFramework.Core.SourceGenerators.Abstractions.Logging;
using GFramework.Core.SourceGenerators.Abstractions.Rule;
[Priority(PriorityGroup.High)]
[Log]
[ContextAware]
public partial class HighPrioritySystem : AbstractSystem
{
protected override void OnInit()
{
Logger.Info("High priority system initialized");
}
}
```
### 运行时优先级查询
可以在运行时查询优先级值:
```csharp
public void ProcessSystems()
{
var systems = this.GetAllByPriority<ISystem>();
foreach (var system in systems)
{
if (system is IPrioritized prioritized)
{
Console.WriteLine($"System: {system.GetType().Name}, Priority: {prioritized.Priority}");
}
system.Init();
}
}
```
## 常见问题
### Q: 优先级值可以是任意整数吗?
**A**: 是的,优先级值可以是任何 `int` 类型的值。推荐使用 `PriorityGroup` 预定义常量或在项目中定义自己的优先级常量。
### Q: 多个类有相同优先级会怎样?
**A**: 当多个类有相同的优先级值时,它们的相对顺序是不确定的。建议为每个类设置不同的优先级值,或在文档中明确说明相同优先级类的执行顺序不保证。
### Q: 可以在运行时改变优先级吗?
**A**: 不可以。`Priority` 属性是只读的,值在编译时确定。如果需要运行时改变优先级,应手动实现 `IPrioritized` 接口。
```csharp
public class DynamicPrioritySystem : IPrioritized
{
private int _priority;
public int Priority => _priority;
public void SetPriority(int value)
{
_priority = value;
}
}
```
### Q: 优先级支持继承吗?
**A**: `[Priority]` 特性标记为 `Inherited = false`,不会被子类继承。每个子类需要独立标记优先级。
```csharp
[Priority(10)]
public partial class BaseSystem { }
public partial class DerivedSystem : BaseSystem // 不会继承 Priority = 10
{
// 需要重新标记
// [Priority(20)]
}
```
### Q: 如何在不使用特性的情况下实现优先级?
**A**: 可以手动实现 `IPrioritized` 接口:
```csharp
public class ManualPrioritySystem : IPrioritized
{
public int Priority => 10;
// 手动实现提供更大的灵活性
public int Priority => _config.Enabled ? 10 : 100;
}
```
### Q: 负数优先级和正数优先级的区别是什么?
**A**: 优先级值用于排序,通常值越小优先级越高:
- 负数(-100高优先级最先执行
- 零0默认优先级
- 正数100低优先级最后执行
具体含义取决于使用场景,但推荐遵循 `PriorityGroup` 定义的语义。
## 实际应用示例
### 游戏系统架构
完整的游戏系统初始化示例:
```csharp
using GFramework.Core.Abstractions.Systems;
using GFramework.Core.SourceGenerators.Abstractions.Bases;
using GFramework.Core.Abstractions.Bases;
// 输入系统(最先初始化)
[Priority(PriorityGroup.Critical)]
[Log]
public partial class InputSystem : AbstractSystem
{
protected override void OnInit()
{
Logger.Info("Input system initialized");
}
}
// 物理系统
[Priority(PriorityGroup.High)]
[Log]
public partial class PhysicsSystem : AbstractSystem
{
protected override void OnInit()
{
Logger.Info("Physics system initialized");
}
}
// 游戏逻辑系统
[Priority(PriorityGroup.Normal)]
[Log]
[ContextAware]
public partial class GameplaySystem : AbstractSystem
{
protected override void OnInit()
{
Logger.Info("Gameplay system initialized");
}
}
// 音频系统
[Priority(PriorityGroup.Low)]
[Log]
public partial class AudioSystem : AbstractSystem
{
protected override void OnInit()
{
Logger.Info("Audio system initialized");
}
}
// 清理系统(最后执行)
[Priority(PriorityGroup.Deferred)]
[Log]
public partial class CleanupSystem : AbstractSystem
{
protected override void OnInit()
{
Logger.Info("Cleanup system initialized");
}
}
```
在架构中初始化:
```csharp
public class GameArchitecture : Architecture
{
protected override async Task InitAsync()
{
// 获取所有系统并按优先级排序
var systems = this.GetAllByPriority<ISystem>();
foreach (var system in systems)
{
await system.InitAsync();
}
}
}
```
## 相关文档
- [Source Generators 概述](./index.md)
- [ContextAware 生成器](./context-aware-generator.md)
- [系统初始化](../core/system.md)

File diff suppressed because it is too large Load Diff

View File

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