From 03ecbe5989c8567cf337be0c7a97c47918333c7c Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 22 Apr 2026 13:03:14 +0800 Subject: [PATCH] =?UTF-8?q?docs(godot):=20=E9=87=8D=E5=86=99=E5=9C=BA?= =?UTF-8?q?=E6=99=AF=E4=B8=8EUI=E6=8E=A5=E5=85=A5=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 更新 Godot 场景文档,按当前 factory、registry、root、provider 与 AutoScene 接线收口采用路径 - 更新 Godot UI 文档,明确 layer 语义、provider 要求、root 接线与 AutoUiPage 用法 - 同步 documentation-governance-and-refresh 跟踪与 trace 到 RP-015,并把下一步切到 signal/extensions --- ...ntation-governance-and-refresh-tracking.md | 31 +- ...umentation-governance-and-refresh-trace.md | 26 +- docs/zh-CN/godot/scene.md | 722 ++++++---------- docs/zh-CN/godot/ui.md | 782 ++++++------------ 4 files changed, 515 insertions(+), 1046 deletions(-) diff --git a/ai-plan/public/documentation-governance-and-refresh/todos/documentation-governance-and-refresh-tracking.md b/ai-plan/public/documentation-governance-and-refresh/todos/documentation-governance-and-refresh-tracking.md index e59bd2a3..e4bc6226 100644 --- a/ai-plan/public/documentation-governance-and-refresh/todos/documentation-governance-and-refresh-tracking.md +++ b/ai-plan/public/documentation-governance-and-refresh/todos/documentation-governance-and-refresh-tracking.md @@ -7,21 +7,23 @@ ## 当前恢复点 -- 恢复点编号:`DOCUMENTATION-GOVERNANCE-REFRESH-RP-014` +- 恢复点编号:`DOCUMENTATION-GOVERNANCE-REFRESH-RP-015` - 当前阶段:`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` 仍保留 `GodotSceneRouter` / `GodotUiRouter` 一类旧接线叙述,成为下一轮高优先级收口对象 + - `docs/zh-CN/godot/scene.md` 与 `docs/zh-CN/godot/ui.md` 已按当前 factory / registry / root / source-generator wiring 重写完成 + - 下一轮高优先级页面转为 `docs/zh-CN/godot/signal.md` 与 `docs/zh-CN/godot/extensions.md` ## 当前状态摘要 - 文档治理规则已收口到仓库规范,README、站点入口与采用链路不再依赖旧文档自证 - 高优先级模块入口、`core` 关键专题页与 `tutorials/godot-integration.md` 已回到“以源码 / 测试 / README 为准”的状态 -- `docs/zh-CN/godot/index.md` 与 `docs/zh-CN/godot/architecture.md` 已完成当前实现收口 -- 当前主题仍是 active topic,因为 `docs/zh-CN/godot/scene.md` 与 `docs/zh-CN/godot/ui.md` 仍保留旧 router / 工厂接线叙述,Godot 文档链路尚未完全收口 +- `docs/zh-CN/godot/index.md`、`architecture.md`、`scene.md` 与 `ui.md` 已完成当前实现收口 +- 当前主题仍是 active topic,因为 `docs/zh-CN/godot/signal.md` 与 `docs/zh-CN/godot/extensions.md` 仍可能保留旧 + `SignalBuilder` / 大而全扩展层叙述,Godot 文档链路尚未完全收口 ## 当前活跃事实 @@ -76,6 +78,13 @@ `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`,两页聚焦校验通过 - `.agents/skills/gframework-doc-refresh/SKILL.md` 已改成标准 YAML frontmatter skill,并明确支持模块输入、证据顺序、输出优先级与验证步骤 - `.agents/skills/gframework-doc-refresh/SKILL.md` 的 `description` 已加引号,修复 `Recommended command:` 中冒号导致的 invalid YAML skill 加载警告 @@ -96,10 +105,10 @@ `godot-project-generator.md`、`get-node-generator.md`、`bind-node-signal-generator.md` 与 `auto-register-exported-collections-generator.md` 已完成收口; 继续按源码、测试、`*.csproj` 与 `ai-libs/` 下已验证参考实现核对剩余 Godot 相关页面,不把旧文档当事实来源 -- Godot 场景 / UI 专题页失真风险:`docs/zh-CN/godot/scene.md` 与 `docs/zh-CN/godot/ui.md` 仍保留 `GodotSceneRouter`、 - `GodotUiRouter` 和旧工厂接线叙述,可能把已经收口的入口页再次带偏 - - 缓解措施:本轮已完成 `godot/index.md` 与 `godot/architecture.md` 收口;下一轮优先按当前 `GFramework.Godot.Scene` / - `GFramework.Godot.UI` 实现与 `ai-libs/CoreGrid` 的真实采用路径重写 `scene.md`、`ui.md` +- Godot signal / extensions 专题页失真风险:`docs/zh-CN/godot/signal.md` 与 `docs/zh-CN/godot/extensions.md` 仍可能保留 + `SignalBuilder` 或“大而全扩展层”叙述,重新把已经收口的入口页带偏 + - 缓解措施:`scene.md` 与 `ui.md` 已完成收口;下一轮优先按当前 `Signal(...)` fluent API、`NodeExtensions` 实际成员与 + `ai-libs/CoreGrid` 的使用方式重写 `signal.md`、`extensions.md` - 采用路径误导风险:根聚合包与模块边界若再次被写错,会继续误导消费者的包选择 - 缓解措施:保持“源码与包关系优先”的证据顺序,改动采用说明时同步核对包依赖与生成器 wiring - 模块映射不全风险:统一 skill 若遗漏模块别名、测试项目或 docs 栏目映射,会让后续扫描阶段直接失焦 @@ -149,11 +158,13 @@ - `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` - `rg -n "GodotSceneRouter|GodotUiRouter|CreateSignalBuilder|GetNodeX|InstallGodotModule\(" docs/zh-CN/godot -S` - `cd docs && bun run build` ## 下一步 -1. 优先重写 `docs/zh-CN/godot/scene.md` 与 `docs/zh-CN/godot/ui.md`,清掉 `GodotSceneRouter` / `GodotUiRouter` 一类旧接线叙述 -2. 视 `scene.md` / `ui.md` 收口结果,决定是否同步压缩 `docs/zh-CN/godot/signal.md` 与 `extensions.md` +1. 优先重写 `docs/zh-CN/godot/signal.md` 与 `docs/zh-CN/godot/extensions.md`,清掉旧 `SignalBuilder` / 大而全扩展层叙述 +2. 视 `signal.md` / `extensions.md` 收口结果,决定是否同步复核 `docs/zh-CN/godot/logging.md` 3. 下一次推送后重新执行 `$gframework-pr-review`,确认 PR #268 的 CodeRabbit / Greptile open thread 是否按预期收敛 diff --git a/ai-plan/public/documentation-governance-and-refresh/traces/documentation-governance-and-refresh-trace.md b/ai-plan/public/documentation-governance-and-refresh/traces/documentation-governance-and-refresh-trace.md index c741c7ac..6c2e5440 100644 --- a/ai-plan/public/documentation-governance-and-refresh/traces/documentation-governance-and-refresh-trace.md +++ b/ai-plan/public/documentation-governance-and-refresh/traces/documentation-governance-and-refresh-trace.md @@ -2,7 +2,7 @@ ## 2026-04-22 -### 当前恢复点:RP-014 +### 当前恢复点:RP-015 - 本轮从 PR #268 的最新 review 数据恢复,未发现失败检查;CTRF 报告显示 2139 个测试全部通过 - 本轮复核确认当前 PR 的 latest-head open thread 同时来自 `coderabbitai[bot]` 与 `greptile-apps[bot]` @@ -30,8 +30,14 @@ 明确把 `[GetNode]`、`[BindNodeSignal]`、`AutoLoads`、`InputActions` 收口到 `GFramework.Godot.SourceGenerators` - 本轮已重写 `docs/zh-CN/godot/architecture.md`,改成“锚点生命周期、`InstallGodotModule(...)` 执行顺序、`IGodotModule` 契约边界”的结构,不再沿用旧版 `.Wait()` 和自动阶段广播叙述 -- 本轮检索确认 Godot 栏目新的高优先级页面转为 `docs/zh-CN/godot/scene.md` 与 `docs/zh-CN/godot/ui.md`:两页仍保留 - `GodotSceneRouter` / `GodotUiRouter` 一类旧接线叙述,应作为 landing / architecture 之后的下一轮收口对象 +- 本轮已重写 `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` 时直接抛异常;这已写入两页文档,避免继续把两者描述成同一种接入模型 +- 本轮检索确认 Godot 栏目新的高优先级页面转为 `docs/zh-CN/godot/signal.md` 与 `docs/zh-CN/godot/extensions.md`: + 两页仍保留 `SignalBuilder` / 大而全扩展层叙述,应作为 scene / ui 之后的下一轮收口对象 ### 当前决策 @@ -45,8 +51,12 @@ - `godot-integration.md` 已重新成为可用的采用路径入口;后续 Godot 文档收口应优先处理 `godot/index.md` 和 `godot/architecture.md` - `godot/index.md` 与 `godot/architecture.md` 现在都必须维持“运行时包与生成器包分边界”的写法,不能再把场景注入和项目元数据生成写回 `GFramework.Godot` 运行时契约 -- `scene.md` 与 `ui.md` 的下一轮重写应以 `GFramework.Godot.Scene`、`GFramework.Godot.UI`、`ai-libs/CoreGrid` 的当前 wiring 为准, - 不再继续复刻并不存在的 `GodotSceneRouter` / `GodotUiRouter` +- `scene.md` 已明确记录“项目侧 router + Godot factory/registry/root”这一分工,后续不要再把 router 包装回 + `GFramework.Godot` 运行时 +- `ui.md` 已明确记录 `Page` 必须走 `PushAsync` / `ReplaceAsync`,`Show(..., UiLayer.Page)` 在当前实现中会抛异常; + 后续不要再把所有 UI 入口重新写回统一 `Show(...)` +- `signal.md` 与 `extensions.md` 的下一轮收口应以 `Signal(...)` fluent API 与 `NodeExtensions` 的当前成员表为准, + 不再继续复刻旧版 `SignalBuilder` 教程和泛化扩展层叙述 ### 验证 @@ -67,11 +77,13 @@ - `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` - `rg -n "GodotSceneRouter|GodotUiRouter|CreateSignalBuilder|GetNodeX|InstallGodotModule\(" docs/zh-CN/godot -S` - `cd docs && bun run build` ### 下一步 -1. 优先重写 `docs/zh-CN/godot/scene.md` 与 `docs/zh-CN/godot/ui.md`,清掉 `GodotSceneRouter` / `GodotUiRouter` 一类旧接线叙述 -2. 视 `scene.md` / `ui.md` 收口结果,决定是否同步压缩 `docs/zh-CN/godot/signal.md` 与 `extensions.md` +1. 优先重写 `docs/zh-CN/godot/signal.md` 与 `docs/zh-CN/godot/extensions.md`,清掉旧 `SignalBuilder` / 大而全扩展层叙述 +2. 视 `signal.md` / `extensions.md` 收口结果,决定是否同步复核 `docs/zh-CN/godot/logging.md` 3. 下一次推送后重新执行 `$gframework-pr-review`,确认 PR #268 的 CodeRabbit / Greptile open thread 是否关闭或减少 diff --git a/docs/zh-CN/godot/scene.md b/docs/zh-CN/godot/scene.md index f3aebbfb..85fad2eb 100644 --- a/docs/zh-CN/godot/scene.md +++ b/docs/zh-CN/godot/scene.md @@ -1,583 +1,321 @@ --- title: Godot 场景系统 -description: Godot 场景系统提供了 GFramework 场景管理与 Godot 场景树的完整集成。 +description: 以当前 GFramework.Godot 源码、Game 场景契约与 CoreGrid 接线为准,说明 PackedScene 场景工厂、行为包装和最小接入路径。 --- # Godot 场景系统 -## 概述 +`GFramework.Godot` 在场景这一层负责的是 Godot runtime 适配,而不是再提供一个 Godot 专属 router。 -Godot 场景系统是 GFramework.Godot 中连接框架场景管理与 Godot 场景树的核心组件。它提供了场景行为封装、场景工厂、场景注册表等功能,让你可以在 -Godot 项目中使用 GFramework 的场景管理系统。 +当前真正参与场景接线的核心类型是: -通过 Godot 场景系统,你可以使用 GFramework 的场景路由、生命周期管理等功能,同时保持与 Godot 场景系统的完美兼容。 +- `IGodotSceneRegistry` / `GodotSceneRegistry` +- `GodotSceneFactory` +- `SceneBehaviorFactory` +- `SceneBehaviorBase` 及其 `Node2D` / `Node3D` / `Control` / `Generic` 实现 +- 项目侧实现的 `ISceneRoot` +- 项目侧继承 `SceneRouterBase` 的 router -**主要特性**: +也就是说,Godot 集成页的重点不是“再造一套场景导航 API”,而是把 `PackedScene`、`Node` 和 `GFramework.Game` 的 +`ISceneRouter` / `ISceneBehavior` 契约接起来。 -- 场景行为封装(SceneBehavior) -- 场景工厂和注册表 -- 与 Godot PackedScene 集成 -- 多种场景行为类型(Node2D、Node3D、Control) -- 场景生命周期管理 -- 场景根节点管理 +## 当前公开入口 -## 核心概念 +### `IGodotSceneRegistry` -### 场景行为 +Godot 侧的场景资源表,底层是 `IAssetRegistry`。它只负责: -`SceneBehaviorBase` 封装了 Godot 节点的场景行为: +- `sceneKey -> PackedScene` 映射 +- 让 `GodotSceneFactory` 能按 key 实例化场景 + +框架当前不会自动扫描项目里的 `.tscn` 文件并填充 registry。 + +### `GodotSceneFactory` + +`GodotSceneFactory.Create(string sceneKey)` 的当前行为很明确: + +1. 从 `IGodotSceneRegistry` 取出 `PackedScene` +2. 调用 `Instantiate()` +3. 如果节点实现了 `ISceneBehaviorProvider`,优先返回 `provider.GetScene()` +4. 否则回退到 `SceneBehaviorFactory.Create(node, sceneKey)` + +这和旧文档里“必须有 Godot 专属 router / 专属 scene provider 才能工作”的说法不同。当前源码允许两条路径: + +- 显式 provider:项目自己决定行为对象 +- 自动包装:按节点类型回退到默认 behavior + +### `SceneBehaviorBase` + +`SceneBehaviorBase` 是当前 Godot 场景行为包装基类。它把 `ISceneBehavior` 的生命周期接到 `Node` 上: + +- `OnLoadAsync` +- `OnEnterAsync` +- `OnPauseAsync` +- `OnResumeAsync` +- `OnExitAsync` +- `OnUnloadAsync` + +如果 owner 还实现了 `IScene`,这些阶段会继续转发到业务节点;如果没有实现 `IScene`,默认 behavior 仍会处理 Godot 节点的 +process 开关和 `QueueFreeX()` 释放。 + +### `SceneBehaviorFactory` + +自动包装的选择规则来自当前实现: + +- `Node2D` -> `Node2DSceneBehavior` +- `Node3D` -> `Node3DSceneBehavior` +- `Control` -> `ControlSceneBehavior` +- 其他 `Node` -> `GenericSceneBehavior` + +这意味着 Godot runtime 确实能“自动给节点补一个 behavior”,但它不会替你补项目侧 router、root 或 registry。 + +## 最小接入路径 + +推荐按下面顺序接入。 + +### 1. 继续在项目层保留自己的 router + +`GFramework.Godot` 当前没有 `GodotSceneRouter` 类型。消费者项目的实际做法,是在项目层继承 +`GFramework.Game.Scene.SceneRouterBase`。 + +`ai-libs/CoreGrid` 的 router 就是这样: ```csharp -public abstract class SceneBehaviorBase : ISceneBehavior - where T : Node +using global::CoreGrid.global; +using LoggingTransitionHandler = GFramework.Game.Scene.Handler.LoggingTransitionHandler; + +namespace CoreGrid.scripts.core.scene; + +public partial class SceneRouter : SceneRouterBase { - protected readonly T Owner; - public string Key { get; } - public IScene Scene { get; } -} -``` + [GetUtility] private IGodotSceneRegistry _sceneRegistry = null!; -### 场景工厂 + public Node? SceneRoot => Root as Node; -`GodotSceneFactory` 负责创建场景实例: - -```csharp -public class GodotSceneFactory : ISceneFactory -{ - public ISceneBehavior Create(string sceneKey); -} -``` - -### 场景注册表 - -`IGodotSceneRegistry` 管理场景资源: - -```csharp -public interface IGodotSceneRegistry -{ - void Register(string key, PackedScene scene); - PackedScene Get(string key); -} -``` - -## 基本用法 - -### 创建场景脚本 - -```csharp -using Godot; -using GFramework.Game.Abstractions.Scene; - -public partial class MainMenuScene : Control, IScene -{ - public async ValueTask OnLoadAsync(ISceneEnterParam? param) + protected override void RegisterHandlers() { - GD.Print("加载主菜单资源"); - await Task.CompletedTask; - } - - public async ValueTask OnEnterAsync() - { - GD.Print("进入主菜单"); - Show(); - await Task.CompletedTask; - } - - public async ValueTask OnPauseAsync() - { - GD.Print("暂停主菜单"); - await Task.CompletedTask; - } - - public async ValueTask OnResumeAsync() - { - GD.Print("恢复主菜单"); - await Task.CompletedTask; - } - - public async ValueTask OnExitAsync() - { - GD.Print("退出主菜单"); - Hide(); - await Task.CompletedTask; - } - - public async ValueTask OnUnloadAsync() - { - GD.Print("卸载主菜单资源"); - await Task.CompletedTask; + __InjectContextBindings_Generated(); + RegisterHandler(new LoggingTransitionHandler()); + RegisterAroundHandler( + new SceneTransitionAnimationHandler(() => SceneTransitionManager.Instance!, _sceneRegistry.GetAll())); } } ``` -### 注册场景 +这里可以看到,Godot 适配点在 factory / registry / root / transition handler 上,而 router 仍然是项目类。 + +### 2. 注册 `IGodotSceneRegistry` 与 `ISceneFactory` + +最小 wiring 需要把 registry 和 factory 装进架构: ```csharp -using GFramework.Godot.Scene; -using Godot; - -public class GameSceneRegistry : GodotSceneRegistry -{ - publieneRegistry() - { - // 注册场景资源 - Register("MainMenu", GD.Load("res://scenes/MainMenu.tscn")); - Register("Gameplay", GD.Load("res://scenes/Gameplay.tscn")); - Register("Pause", GD.Load("res://scenes/Pause.tscn")); - } -} -``` - -### 设置场景系统 - -```csharp -using GFramework.Godot.Architecture; -using GFramework.Godot.Scene; - -public class GameArchitecture : AbstractArchitecture -{ - protected override void InstallModules() - { - // 注册场景注册表 - var sceneRegistry = new GameSceneRegistry(); - RegisterUtility(sceneRegistry); - - // 注册场景工厂 - var sceneFactory = new GodotSceneFactory(); - RegisterUtility(sceneFactory); - - // 注册场景路由 - var sceneRouter = new GodotSceneRouter(); - RegisterSystem(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(); - await sceneRouter.ReplaceAsync("MainMenu"); - } - - private async void StartGame() - { - var sceneRouter = this.GetSystem(); - await sceneRouter.ReplaceAsync("Gameplay"); - } - - private async void ShowPause() - { - var sceneRouter = this.GetSystem(); - await sceneRouter.PushAsync("Pause"); - } -} -``` - -## 高级用法 - -### 使用场景行为提供者 - -```csharp -using Godot; using GFramework.Game.Abstractions.Scene; using GFramework.Godot.Scene; +using Godot; -public partial class GameplayScene : Node2D, ISceneBehaviorProvider +public sealed class GameSceneRegistry : GodotSceneRegistry { - private GameplaySceneBehavior _behavior; + public GameSceneRegistry() + { + Register(nameof(SceneKey.MainMenu), GD.Load("res://scenes/main_menu.tscn")); + Register(nameof(SceneKey.Gameplay), GD.Load("res://scenes/gameplay.tscn")); + } +} + +architecture.RegisterUtility(new GameSceneRegistry()); +architecture.RegisterUtility(new GodotSceneFactory()); +architecture.RegisterSystem(new SceneRouter()); +``` + +项目用什么 key 类型、资源目录或配置表都可以,但最终要能落到 `sceneKey -> PackedScene`。 + +### 3. 提供 `ISceneRoot` + +`SceneRouterBase` 只负责切换编排,真正把场景节点挂到 Godot 场景树的是项目自己的 `ISceneRoot`。 + +CoreGrid 的 `SceneRoot` 当前做了两件关键事: + +- 在 `_Ready()` 时调用 `_sceneRouter.BindRoot(this)` +- 在 `AddScene` / `RemoveScene` 里把 `scene.Original` 当作 `Node` 挂入或移出树 + +最小形态可以写成: + +```csharp +public sealed class SceneRoot : Node2D, ISceneRoot +{ + [GetSystem] private ISceneRouter _sceneRouter = null!; public override void _Ready() { - _behavior = new GameplaySceneBehavior(this, "Gameplay"); + __InjectContextBindings_Generated(); + _sceneRouter.BindRoot(this); } + public void AddScene(ISceneBehavior scene) + { + if (scene.Original is not Node node) + throw new InvalidOperationException("SceneBehavior must inherit Godot Node."); + + if (node.GetParent() == null) + AddChild(node); + } + + public void RemoveScene(ISceneBehavior scene) + { + if (scene.Original is Node node && node.GetParent() == this) + RemoveChild(node); + } +} +``` + +### 4. 让场景节点提供 behavior + +当前有两种可行方式。 + +#### 方式 A:实现 `ISceneBehaviorProvider` + +如果你想显式控制 behavior 类型,直接实现 `GetScene()`: + +```csharp +public partial class GameplayRoot : Node2D, ISceneBehaviorProvider, IScene +{ + private ISceneBehavior? _scene; + public ISceneBehavior GetScene() { - return _behavior; - } -} - -// 自定义场景行为 -public class GameplaySceneBehavior : Node2DSceneBehavior -{ - public GameplaySceneBehavior(Node2D owner, string key) : base(owner, key) - { + return _scene ??= SceneBehaviorFactory.Create(this, nameof(SceneKey.Gameplay)); } - protected override async ValueTask OnLoadInternalAsync(ISceneEnterParam? param) + public ValueTask OnLoadAsync(ISceneEnterParam? param) { - GD.Print("加载游戏场景"); - // 加载游戏资源 - await Task.CompletedTask; + return ValueTask.CompletedTask; } - protected override async ValueTask OnEnterInternalAsync() + public ValueTask OnEnterAsync() { - GD.Print("进入游戏场景"); - Owner.Show(); - await Task.CompletedTask; + return ValueTask.CompletedTask; + } + + public ValueTask OnPauseAsync() + { + return ValueTask.CompletedTask; + } + + public ValueTask OnResumeAsync() + { + return ValueTask.CompletedTask; + } + + public ValueTask OnExitAsync() + { + return ValueTask.CompletedTask; + } + + public ValueTask OnUnloadAsync() + { + return ValueTask.CompletedTask; } } ``` -### 不同类型的场景行为 +#### 方式 B:用 `[AutoScene]` 让生成器补样板 + +当前更贴近真实消费者 wiring 的方式,是让 `GFramework.Godot.SourceGenerators` 生成 `SceneKeyStr` 和 `GetScene()`: ```csharp -// Node2D 场景 -public class Node2DSceneBehavior : SceneBehaviorBase -{ - public Node2DSceneBehavior(Node2D owner, string key) : base(owner, key) - { - } -} - -// Node3D 场景 -public class Node3DSceneBehavior : SceneBehaviorBase -{ - public Node3DSceneBehavior(Node3D owner, string key) : base(owner, key) - { - } -} - -// Control 场景(UI) -public class ControlSceneBehavior : SceneBehaviorBase -{ - public ControlSceneBehavior(Control owner, string key) : base(owner, key) - { - } -} -``` - -### 场景根节点管理 - -```csharp -using Godot; -using GFramework.Godot.Scene; - -public partial class SceneRoot : Node, ISceneRoot -{ - private Node _currentSceneNode; - - public void AttachScene(Node sceneNode) - { - // 移除旧场景 - if (_currentSceneNode != null) - { - RemoveChild(_currentSceneNode); - _currentSceneNode.QueueFree(); - } - - // 添加新场景 - _currentSceneNode = sceneNode; - AddChild(_currentSceneNode); - } - - public void DetachScene(Node sceneNode) - { - if (_currentSceneNode == sceneNode) - { - RemoveChild(_currentSceneNode); - _currentSceneNode = null; - } - } -} -``` - -### 场景参数传递 - -```csharp -// 定义场景参数 -public class GameplayEnterParam : ISceneEnterParam -{ - public int Level { get; set; } - public string Difficulty { get; set; } -} - -// 在场景中接收参数 -public partial class GameplayScene : Node2D, IScene -{ - private int _level; - private string _difficulty; - - public async ValueTask OnLoadAsync(ISceneEnterParam? param) - { - if (param is GameplayEnterParam gameplayParam) - { - _level = gameplayParam.Level; - _difficulty = gameplayParam.Difficulty; - GD.Print($"加载关卡 {_level},难度: {_difficulty}"); - } - - await Task.CompletedTask; - } - - // ... 其他生命周期方法 -} - -// 切换场景时传递参数 -var sceneRouter = this.GetSystem(); -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(); - await sceneRouter.ReplaceAsync("Gameplay"); - } - - private async Task PreloadNextScene() - { - var sceneFactory = this.GetUtility(); - var sceneBehavior = sceneFactory.Create("Gameplay"); - - // 预加载场景资源 - await sceneBehavior.LoadAsync(null); - - GD.Print("场景预加载完成"); - } -} -``` - -### 场景转换动画 - -```csharp -using Godot; using GFramework.Game.Abstractions.Scene; +using GFramework.Godot.SourceGenerators.Abstractions.UI; +using Godot; -public class FadeTransitionHandler : ISceneTransitionHandler +[AutoScene(nameof(SceneKey.Gameplay))] +public partial class GameplayRoot : Node2D, ISceneBehaviorProvider, IScene { - private ColorRect _fadeRect; - - public FadeTransitionHandler(ColorRect fadeRect) + public ValueTask OnLoadAsync(ISceneEnterParam? param) { - _fadeRect = fadeRect; + return ValueTask.CompletedTask; } - public async ValueTask OnBeforeExitAsync(SceneTransitionEvent @event) + public ValueTask OnEnterAsync() { - // 淡出动画 - var tween = _fadeRect.CreateTween(); - tween.TweenProperty(_fadeRect, "modulate:a", 1.0f, 0.3f); - await tween.ToSignal(tween, Tween.SignalName.Finished); + return ValueTask.CompletedTask; } - public async ValueTask OnAfterEnterAsync(SceneTransitionEvent @event) + public ValueTask OnPauseAsync() { - // 淡入动画 - var tween = _fadeRect.CreateTween(); - tween.TweenProperty(_fadeRect, "modulate:a", 0.0f, 0.3f); - await tween.ToSignal(tween, Tween.SignalName.Finished); + return ValueTask.CompletedTask; } - // ... 其他方法 -} -``` - -### 场景间通信 - -```csharp -// 通过事件通信 -public partial class GameplayScene : Node2D, IScene -{ - public async ValueTask OnEnterAsync() + public ValueTask OnResumeAsync() { - // 发送场景进入事件 - this.SendEvent(new GameplaySceneEnteredEvent()); - await Task.CompletedTask; - } -} - -// 在其他地方监听 -public partial class HUD : Control -{ - public override void _Ready() - { - this.RegisterEvent(OnGameplayEntered); + return ValueTask.CompletedTask; } - private void OnGameplayEntered(GameplaySceneEnteredEvent evt) + public ValueTask OnExitAsync() { - GD.Print("游戏场景已进入,显示 HUD"); - Show(); + return ValueTask.CompletedTask; + } + + public ValueTask OnUnloadAsync() + { + return ValueTask.CompletedTask; } } ``` -## 最佳实践 - -1. **场景脚本实现 IScene 接口**:获得完整的生命周期管理 - ```csharp - ✓ public partial class MyScene : Node2D, IScene { } - ✗ public partial class MyScene : Node2D { } // 无生命周期管理 - ``` - -2. **使用场景注册表管理场景资源**:集中管理所有场景 - ```csharp - public class GameSceneRegistry : GodotSceneRegistry - { - public GameSceneRegistry() - { - Register("MainMenu", GD.Load("res://scenes/MainMenu.tscn")); - Register("Gameplay", GD.Load("res://scenes/Gameplay.tscn")); - } - } - ``` - -3. **在 OnLoadAsync 中加载资源**:避免场景切换卡顿 - ```csharp - public async ValueTask OnLoadAsync(ISceneEnterParam? param) - { - // 异步加载资源 - await LoadTexturesAsync(); - await LoadAudioAsync(); - } - ``` - -4. **使用场景根节点管理场景树**:保持场景树结构清晰 - ```csharp - // 创建场景根节点 - var sceneRoot = new Node { Name = "SceneRoot" }; - AddChild(sceneRoot); - - // 绑定到场景路由 - sceneRouter.BindRoot(sceneRoot); - ``` - -5. **正确清理场景资源**:在 OnUnloadAsync 中释放资源 - ```csharp - public async ValueTask OnUnloadAsync() - { - // 释放资源 - _texture?.Dispose(); - _audioStream?.Dispose(); - await Task.CompletedTask; - } - ``` - -6. **使用场景参数传递数据**:避免使用全局变量 - ```csharp - ✓ await sceneRouter.ReplaceAsync("Gameplay", new GameplayEnterParam { Level = 1 }); - ✗ GlobalData.CurrentLevel = 1; // 避免全局状态 - ``` - -## 常见问题 - -### 问题:如何在 Godot 场景中使用 GFramework? - -**解答**: -场景脚本实现 `IScene` 接口: +生成器当前会补出与源码一致的 `GetScene()`: ```csharp -public partial class MyScene : Node2D, IScene +public ISceneBehavior GetScene() { - public async ValueTask OnLoadAsync(ISceneEnterParam? param) { } - public async ValueTask OnEnterAsync() { } - // ... 实现其他方法 + return __autoSceneBehavior_Generated ??= SceneBehaviorFactory.Create(this, SceneKeyStr); } ``` -### 问题:场景切换时节点如何管理? +要注意两点: -**解答**: -使用场景根节点管理: +- `[AutoScene]` 只生成方法和 key,不会替你自动补 `: ISceneBehaviorProvider` +- `IScene` 仍然是业务生命周期契约;不实现它时,默认 behavior 只会保留基础节点切换语义 + +### 5. 从业务代码发起导航 + +一旦 registry、factory、router、root 都装好,导航入口仍然是 `ISceneRouter`: ```csharp -// 场景路由会自动管理节点的添加和移除 -await sceneRouter.ReplaceAsync("NewScene"); -// 旧场景节点会被移除,新场景节点会被添加 +await sceneRouter.ReplaceAsync(nameof(SceneKey.MainMenu)); +await sceneRouter.ReplaceAsync(nameof(SceneKey.Gameplay), new GameplayEnterParam()); +await sceneRouter.PushAsync(nameof(SceneKey.PauseMenu)); +await sceneRouter.PopAsync(); ``` -### 问题:如何实现场景预加载? +## 当前边界 -**解答**: -使用场景工厂提前创建场景: +### 没有 `GodotSceneRouter` -```csharp -var sceneFactory = this.GetUtility(); -var sceneBehavior = sceneFactory.Create("NextScene"); -await sceneBehavior.LoadAsync(null); -``` +仓库当前不存在 `GodotSceneRouter` 类型。旧文档里把它写成默认入口是失真的;实际入口仍然是项目侧继承 +`SceneRouterBase` 的 router。 -### 问题:场景生命周期方法的调用顺序是什么? +### 没有自动注册所有场景 -**解答**: +当前运行时只认识你注册进 `IGodotSceneRegistry` 的 `PackedScene`。它不会扫描目录、不会从脚本类型自动反推出注册表。 -- 进入场景:`OnLoadAsync` -> `OnEnterAsync` -> `OnShow` -- 暂停场景:`OnPause` -> `OnHide` -- 恢复场景:`OnShow` -> `OnResume` -- 退出场景:`OnHide` -> `OnExitAsync` -> `OnUnloadAsync` +### provider 是“优先路径”,不是“唯一路径” -### 问题:如何在场景中访问架构组件? +`GodotSceneFactory` 会优先使用 `ISceneBehaviorProvider`,但没有 provider 时仍会按节点类型自动包装。这个行为和 UI 系统不同; +UI 工厂当前没有同等的自动回退。 -**解答**: -使用扩展方法: +### root 仍然是项目职责 -```csharp -public partial class MyScene : Node2D, IScene -{ - public async ValueTask OnEnterAsync() - { - var playerModel = this.GetModel(); - var gameSystem = this.GetSystem(); - await Task.CompletedTask; - } -} -``` +`ISceneRoot` 的实现决定: -### 问题:场景切换时如何显示加载界面? +- 节点挂到哪里 +- 移除时如何释放 +- 是否保留额外的当前视图引用 -**解答**: -使用场景转换处理器: +Godot runtime 不会替项目生成统一的 root 节点。 -```csharp -public class LoadingScreenHandler : ISceneTransitionHandler -{ - public async ValueTask OnBeforeLoadAsync(SceneTransitionEvent @event) - { - // 显示加载界面 - ShowLoadingScreen(); - await Task.CompletedTask; - } +## 继续阅读 - public async ValueTask OnAfterEnterAsync(SceneTransitionEvent @event) - { - // 隐藏加载界面 - HideLoadingScreen(); - await Task.CompletedTask; - } -} -``` - -## 相关文档 - -- [场景系统](/zh-CN/game/scene) - 核心场景管理 -- [Godot 架构集成](/zh-CN/godot/architecture) - Godot 架构基础 -- [Godot UI 系统](/zh-CN/godot/ui) - Godot UI 集成 -- [Godot 扩展](/zh-CN/godot/extensions) - Godot 扩展方法 +1. [Godot 运行时集成](./index.md) +2. [Godot 架构集成](./architecture.md) +3. [Game 场景系统](../game/scene.md) +4. [AutoScene 生成器](../source-generators/auto-scene-generator.md) diff --git a/docs/zh-CN/godot/ui.md b/docs/zh-CN/godot/ui.md index f98b461c..b1becf96 100644 --- a/docs/zh-CN/godot/ui.md +++ b/docs/zh-CN/godot/ui.md @@ -1,643 +1,351 @@ --- title: Godot UI 系统 -description: Godot UI 系统提供了 GFramework UI 管理与 Godot Control 节点的完整集成。 +description: 以当前 GFramework.Godot 源码、Game UI 契约与 CoreGrid 接线为准,说明 PackedScene UI 工厂、页面行为和层级接入路径。 --- # Godot UI 系统 -## 概述 +`GFramework.Godot.UI` 当前负责的是把 `GFramework.Game` 的 UI 路由契约接到 `Control` / `CanvasLayer` / +`PackedScene` 上,而不是定义一个 Godot 专属 router。 -Godot UI 系统是 GFramework.Godot 中连接框架 UI 管理与 Godot Control 节点的核心组件。它提供了 UI 页面行为封装、UI 工厂、UI -注册表等功能,支持多层级 UI 显示,让你可以在 Godot 项目中使用 GFramework 的 UI 管理系统。 +当前真正参与这条链路的核心类型是: -通过 Godot UI 系统,你可以使用 GFramework 的 UI 路由、生命周期管理、多层级显示等功能,同时保持与 Godot UI 系统的完美兼容。 +- `IGodotUiRegistry` / `GodotUiRegistry` +- `GodotUiFactory` +- `CanvasItemUiPageBehaviorBase` +- `UiPageBehaviorFactory` +- `Page` / `Overlay` / `Modal` / `Toast` / `Topmost` 五类 layer behavior +- 项目侧实现的 `IUiRoot` +- 项目侧继承 `UiRouterBase` 的 router -**主要特性**: +## 当前公开入口 -- UI 页面行为封装 -- UI 工厂和注册表 -- 与 Godot PackedScene 集成 -- 多层级 UI 支持(Page、Overlay、Modal、Toast、Topmost) -- UI 生命周期管理 -- UI 根节点管理 +### `IGodotUiRegistry` -## 核心概念 +Godot 侧 UI 资源表,底层是 `IAssetRegistry`。它只负责: -### UI 页面行为 +- `uiKey -> PackedScene` 映射 +- 让 `GodotUiFactory` 可以按 key 实例化 UI 页面 -`CanvasItemUiPageBehaviorBase` 封装了 Godot Control 节点的 UI 行为: +框架当前不会自动扫描 `.tscn`、不会自动根据类型名补全注册表。 + +### `GodotUiFactory` + +`GodotUiFactory.Create(string uiKey)` 的当前行为比场景工厂更严格: + +1. 从 `IGodotUiRegistry` 取出 `PackedScene` +2. 调用 `Instantiate()` +3. 节点必须实现 `IUiPageBehaviorProvider` +4. 返回 `provider.GetPage()` + +如果实例化得到的节点没有实现 `IUiPageBehaviorProvider`,当前实现会直接抛 `InvalidCastException`。这也是 UI 页面文档必须强调 +`GetPage()` / `[AutoUiPage]` 的原因。 + +### `CanvasItemUiPageBehaviorBase` + +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` +- `UiLayer.Overlay` -> `OverlayLayerUiPageBehavior` +- `UiLayer.Modal` -> `ModalLayerUiPageBehavior` +- `UiLayer.Toast` -> `ToastLayerUiPageBehavior` +- `UiLayer.Topmost` -> `TopmostLayerUiPageBehavior` + +几个容易被旧文档写偏的默认语义如下: + +- `Page` + - 不可重入,阻断输入 +- `Overlay` + - 可重入,非模态,不阻断输入;暂停时不会停掉节点处理 +- `Modal` + - 可重入,模态,阻断输入 +- `Toast` + - 可重入,非模态,不阻断输入 +- `Topmost` + - 不可重入,模态,阻断输入 + +## 最小接入路径 + +### 1. 继续在项目层保留自己的 router + +仓库当前不存在 `GodotUiRouter` 类型。实际做法仍然是项目侧继承 `GFramework.Game.UI.UiRouterBase`。 + +`ai-libs/CoreGrid` 的 `UiRouter` 目前就是: ```csharp -public abstract class CanvasItemUiPageBehaviorBase : IUiPageBehavior - where T : CanvasItem +using LoggingTransitionHandler = GFramework.Game.UI.Handler.LoggingTransitionHandler; + +namespace CoreGrid.scripts.core.ui; + +[Log] +public partial class UiRouter : UiRouterBase { - protected readonly T Owner; - public string Key { get; } - public UiLayer Layer { get; } - public bool IsReentrant { get; } + protected override void RegisterHandlers() + { + _log.Debug("Registering default transition handlers"); + RegisterHandler(new LoggingTransitionHandler()); + } } ``` -### UI 工厂 +Godot runtime 自身并不接管这层 router 的定义。 -`GodotUiFactory` 负责创建 UI 实例: +### 2. 注册 `IGodotUiRegistry` 与 `IUiFactory` + +最小 wiring 需要显式注册 UI 资源表和工厂: ```csharp -public class GodotUiFactory : IUiFactory -{ - public IUiPageBehavior Create(string uiKey); -} -``` - -### UI 层级行为 - -不同层级的 UI 有不同的行为类: - -```csharp -// Page 层(栈管理) -public class PageLayerUiPageBehavior : CanvasItemUiPageBehaviorBase -{ - public override UiLayer Layer => UiLayer.Page; - public override bool IsReentrant => false; -} - -// Modal 层(模态对话框) -public class ModalLayerUiPageBehavior : CanvasItemUiPageBehaviorBase -{ - public override UiLayer Layer => UiLayer.Modal; - public override bool IsReentrant => true; -} -``` - -## 基本用法 - -### 创建 UI 脚本 - -```csharp -using Godot; using GFramework.Game.Abstractions.UI; +using GFramework.Godot.UI; +using Godot; -public partial class MainMenuPage : Control, IUiPage +public sealed class GameUiRegistry : GodotUiRegistry { + public GameUiRegistry() + { + Register(nameof(UiKey.MainMenu), GD.Load("res://ui/main_menu.tscn")); + Register(nameof(UiKey.PauseMenu), GD.Load("res://ui/pause_menu.tscn")); + Register(nameof(UiKey.OptionsMenu), GD.Load("res://ui/options_menu.tscn")); + } +} + +architecture.RegisterUtility(new GameUiRegistry()); +architecture.RegisterUtility(new GodotUiFactory()); +architecture.RegisterSystem(new UiRouter()); +``` + +### 3. 提供 `IUiRoot` + +`UiRouterBase` 只负责页面栈、layer UI、输入仲裁和暂停语义;真正把页面挂到 Godot 容器的是项目自己的 `IUiRoot`。 + +CoreGrid 当前的 `UiRoot` 做法和源码契约一致: + +- 继承 `CanvasLayer` +- 为每个 `UiLayer` 创建一个 `Control` 容器 +- 在 `_Ready()` 时调用 `_uiRouter.BindRoot(this)` +- 在 `AddUiPage` / `RemoveUiPage` 中处理 `CanvasItem` 挂载与释放 + +最小形态可以写成: + +```csharp +public sealed class UiRoot : CanvasLayer, IUiRoot +{ + [GetSystem] private IUiRouter _uiRouter = null!; + + public override void _Ready() + { + __InjectContextBindings_Generated(); + _uiRouter.BindRoot(this); + } + + public void AddUiPage(IUiPageBehavior child) + { + AddUiPage(child, UiLayer.Page); + } + + public void AddUiPage(IUiPageBehavior child, UiLayer layer, int orderInLayer = 0) + { + if (child.View is not CanvasItem item) + throw new InvalidOperationException("UIPage View must be a Godot Node"); + + AddChild(item); + item.ZIndex = (int)layer * 100 + orderInLayer; + } + + public void RemoveUiPage(IUiPageBehavior child) + { + if (child.View is Node node && node.GetParent() == this) + RemoveChild(node); + } +} +``` + +### 4. 让页面节点提供 `GetPage()` + +因为 `GodotUiFactory` 不会自动回退到默认 behavior,页面节点必须显式提供 `GetPage()`。 + +#### 方式 A:手写 `IUiPageBehaviorProvider` + +```csharp +public partial class PauseMenu : Control, IUiPage, IUiPageBehaviorProvider +{ + private IUiPageBehavior? _page; + + public IUiPageBehavior GetPage() + { + return _page ??= UiPageBehaviorFactory.Create(this, nameof(UiKey.PauseMenu), UiLayer.Modal); + } + public void OnEnter(IUiPageEnterParam? param) { - GD.Print("进入主菜单"); - Show(); } public void OnExit() { - GD.Print("退出主菜单"); - Hide(); } public void OnPause() { - GD.Print("暂停主菜单"); } public void OnResume() { - GD.Print("恢复主菜单"); } public void OnShow() { - Show(); } public void OnHide() { - Hide(); } } ``` -### 实现 UI 页面行为提供者 +#### 方式 B:用 `[AutoUiPage]` 让生成器补样板 + +当前更贴近真实消费者 wiring 的方式,是让生成器产出 `UiKeyStr` 和 `GetPage()`: ```csharp -using Godot; using GFramework.Game.Abstractions.UI; -using GFramework.Godot.UI; - -public partial class MainMenuPage : Control, IUiPageBehaviorProvider -{ - private PageLayerUiPageBehavior _behavior; - - public override void _Ready() - { - _behavior = new PageLayerUiPageBehavior(this, "MainMenu"); - } - - public IUiPageBehavior GetPage() - { - return _behavior; - } -} -``` - -### 注册 UI - -```csharp -using GFramework.Godot.UI; +using GFramework.Godot.SourceGenerators.Abstractions.UI; using Godot; -public class GameUiRegistry : GodotUiRegistry -{ - public GameUiRegistry() - { - // 注册 UI 资源 - Register("MainMenu", GD.Load("res://ui/MainMenu.tscn")); - Register("Settings", GD.Load("res://ui/Settings.tscn")); - Register("ConfirmDialog", GD.Load("res://ui/ConfirmDialog.tscn")); - Register("Toast", GD.Load("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(uiRegistry); - - // 注册 UI 工厂 - var uiFactory = new GodotUiFactory(); - RegisterUtility(uiFactory); - - // 注册 UI 路由 - var uiRouter = new GodotUiRouter(); - RegisterSystem(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(); - await uiRouter.PushAsync("MainMenu"); - } - - private async void ShowSettings() - { - var uiRouter = this.GetSystem(); - await uiRouter.PushAsync("Settings"); - } - - private void ShowDialog() - { - var uiRouter = this.GetSystem(); - uiRouter.Show("ConfirmDialog", UiLayer.Modal); - } - - private void ShowToast(string message) - { - var uiRouter = this.GetSystem(); - 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