mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-07 00:27:16 +08:00
docs(godot): 重写场景与UI接入文档
- 更新 Godot 场景文档,按当前 factory、registry、root、provider 与 AutoScene 接线收口采用路径 - 更新 Godot UI 文档,明确 layer 语义、provider 要求、root 接线与 AutoUiPage 用法 - 同步 documentation-governance-and-refresh 跟踪与 trace 到 RP-015,并把下一步切到 signal/extensions
This commit is contained in:
parent
5d436694f8
commit
03ecbe5989
@ -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 是否按预期收敛
|
||||
|
||||
@ -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 是否关闭或减少
|
||||
|
||||
@ -1,583 +1,321 @@
|
||||
---
|
||||
title: Godot 场景系统
|
||||
description: Godot 场景系统提供了 GFramework 场景管理与 Godot 场景树的完整集成。
|
||||
description: 以当前 GFramework.Godot 源码、Game 场景契约与 CoreGrid 接线为准,说明 PackedScene 场景工厂、行为包装和最小接入路径。
|
||||
---
|
||||
|
||||
# Godot 场景系统
|
||||
|
||||
## 概述
|
||||
`GFramework.Godot` 在场景这一层负责的是 Godot runtime 适配,而不是再提供一个 Godot 专属 router。
|
||||
|
||||
Godot 场景系统是 GFramework.Godot 中连接框架场景管理与 Godot 场景树的核心组件。它提供了场景行为封装、场景工厂、场景注册表等功能,让你可以在
|
||||
Godot 项目中使用 GFramework 的场景管理系统。
|
||||
当前真正参与场景接线的核心类型是:
|
||||
|
||||
通过 Godot 场景系统,你可以使用 GFramework 的场景路由、生命周期管理等功能,同时保持与 Godot 场景系统的完美兼容。
|
||||
- `IGodotSceneRegistry` / `GodotSceneRegistry`
|
||||
- `GodotSceneFactory`
|
||||
- `SceneBehaviorFactory`
|
||||
- `SceneBehaviorBase<T>` 及其 `Node2D` / `Node3D` / `Control` / `Generic` 实现
|
||||
- 项目侧实现的 `ISceneRoot`
|
||||
- 项目侧继承 `SceneRouterBase` 的 router
|
||||
|
||||
**主要特性**:
|
||||
也就是说,Godot 集成页的重点不是“再造一套场景导航 API”,而是把 `PackedScene`、`Node` 和 `GFramework.Game` 的
|
||||
`ISceneRouter` / `ISceneBehavior` 契约接起来。
|
||||
|
||||
- 场景行为封装(SceneBehavior)
|
||||
- 场景工厂和注册表
|
||||
- 与 Godot PackedScene 集成
|
||||
- 多种场景行为类型(Node2D、Node3D、Control)
|
||||
- 场景生命周期管理
|
||||
- 场景根节点管理
|
||||
## 当前公开入口
|
||||
|
||||
## 核心概念
|
||||
### `IGodotSceneRegistry`
|
||||
|
||||
### 场景行为
|
||||
Godot 侧的场景资源表,底层是 `IAssetRegistry<PackedScene>`。它只负责:
|
||||
|
||||
`SceneBehaviorBase<T>` 封装了 Godot 节点的场景行为:
|
||||
- `sceneKey -> PackedScene` 映射
|
||||
- 让 `GodotSceneFactory` 能按 key 实例化场景
|
||||
|
||||
框架当前不会自动扫描项目里的 `.tscn` 文件并填充 registry。
|
||||
|
||||
### `GodotSceneFactory`
|
||||
|
||||
`GodotSceneFactory.Create(string sceneKey)` 的当前行为很明确:
|
||||
|
||||
1. 从 `IGodotSceneRegistry` 取出 `PackedScene`
|
||||
2. 调用 `Instantiate()`
|
||||
3. 如果节点实现了 `ISceneBehaviorProvider`,优先返回 `provider.GetScene()`
|
||||
4. 否则回退到 `SceneBehaviorFactory.Create(node, sceneKey)`
|
||||
|
||||
这和旧文档里“必须有 Godot 专属 router / 专属 scene provider 才能工作”的说法不同。当前源码允许两条路径:
|
||||
|
||||
- 显式 provider:项目自己决定行为对象
|
||||
- 自动包装:按节点类型回退到默认 behavior
|
||||
|
||||
### `SceneBehaviorBase<T>`
|
||||
|
||||
`SceneBehaviorBase<T>` 是当前 Godot 场景行为包装基类。它把 `ISceneBehavior` 的生命周期接到 `Node` 上:
|
||||
|
||||
- `OnLoadAsync`
|
||||
- `OnEnterAsync`
|
||||
- `OnPauseAsync`
|
||||
- `OnResumeAsync`
|
||||
- `OnExitAsync`
|
||||
- `OnUnloadAsync`
|
||||
|
||||
如果 owner 还实现了 `IScene`,这些阶段会继续转发到业务节点;如果没有实现 `IScene`,默认 behavior 仍会处理 Godot 节点的
|
||||
process 开关和 `QueueFreeX()` 释放。
|
||||
|
||||
### `SceneBehaviorFactory`
|
||||
|
||||
自动包装的选择规则来自当前实现:
|
||||
|
||||
- `Node2D` -> `Node2DSceneBehavior`
|
||||
- `Node3D` -> `Node3DSceneBehavior`
|
||||
- `Control` -> `ControlSceneBehavior`
|
||||
- 其他 `Node` -> `GenericSceneBehavior`
|
||||
|
||||
这意味着 Godot runtime 确实能“自动给节点补一个 behavior”,但它不会替你补项目侧 router、root 或 registry。
|
||||
|
||||
## 最小接入路径
|
||||
|
||||
推荐按下面顺序接入。
|
||||
|
||||
### 1. 继续在项目层保留自己的 router
|
||||
|
||||
`GFramework.Godot` 当前没有 `GodotSceneRouter` 类型。消费者项目的实际做法,是在项目层继承
|
||||
`GFramework.Game.Scene.SceneRouterBase`。
|
||||
|
||||
`ai-libs/CoreGrid` 的 router 就是这样:
|
||||
|
||||
```csharp
|
||||
public abstract class SceneBehaviorBase<T> : ISceneBehavior
|
||||
where T : Node
|
||||
using global::CoreGrid.global;
|
||||
using LoggingTransitionHandler = GFramework.Game.Scene.Handler.LoggingTransitionHandler;
|
||||
|
||||
namespace CoreGrid.scripts.core.scene;
|
||||
|
||||
public partial class SceneRouter : SceneRouterBase
|
||||
{
|
||||
protected readonly T Owner;
|
||||
public string Key { get; }
|
||||
public IScene Scene { get; }
|
||||
}
|
||||
```
|
||||
[GetUtility] private IGodotSceneRegistry _sceneRegistry = null!;
|
||||
|
||||
### 场景工厂
|
||||
public Node? SceneRoot => Root as Node;
|
||||
|
||||
`GodotSceneFactory` 负责创建场景实例:
|
||||
|
||||
```csharp
|
||||
public class GodotSceneFactory : ISceneFactory
|
||||
{
|
||||
public ISceneBehavior Create(string sceneKey);
|
||||
}
|
||||
```
|
||||
|
||||
### 场景注册表
|
||||
|
||||
`IGodotSceneRegistry` 管理场景资源:
|
||||
|
||||
```csharp
|
||||
public interface IGodotSceneRegistry
|
||||
{
|
||||
void Register(string key, PackedScene scene);
|
||||
PackedScene Get(string key);
|
||||
}
|
||||
```
|
||||
|
||||
## 基本用法
|
||||
|
||||
### 创建场景脚本
|
||||
|
||||
```csharp
|
||||
using Godot;
|
||||
using GFramework.Game.Abstractions.Scene;
|
||||
|
||||
public partial class MainMenuScene : Control, IScene
|
||||
{
|
||||
public async ValueTask OnLoadAsync(ISceneEnterParam? param)
|
||||
protected override void RegisterHandlers()
|
||||
{
|
||||
GD.Print("加载主菜单资源");
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async ValueTask OnEnterAsync()
|
||||
{
|
||||
GD.Print("进入主菜单");
|
||||
Show();
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async ValueTask OnPauseAsync()
|
||||
{
|
||||
GD.Print("暂停主菜单");
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async ValueTask OnResumeAsync()
|
||||
{
|
||||
GD.Print("恢复主菜单");
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async ValueTask OnExitAsync()
|
||||
{
|
||||
GD.Print("退出主菜单");
|
||||
Hide();
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async ValueTask OnUnloadAsync()
|
||||
{
|
||||
GD.Print("卸载主菜单资源");
|
||||
await Task.CompletedTask;
|
||||
__InjectContextBindings_Generated();
|
||||
RegisterHandler(new LoggingTransitionHandler());
|
||||
RegisterAroundHandler(
|
||||
new SceneTransitionAnimationHandler(() => SceneTransitionManager.Instance!, _sceneRegistry.GetAll()));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 注册场景
|
||||
这里可以看到,Godot 适配点在 factory / registry / root / transition handler 上,而 router 仍然是项目类。
|
||||
|
||||
### 2. 注册 `IGodotSceneRegistry` 与 `ISceneFactory`
|
||||
|
||||
最小 wiring 需要把 registry 和 factory 装进架构:
|
||||
|
||||
```csharp
|
||||
using GFramework.Godot.Scene;
|
||||
using Godot;
|
||||
|
||||
public class GameSceneRegistry : GodotSceneRegistry
|
||||
{
|
||||
publieneRegistry()
|
||||
{
|
||||
// 注册场景资源
|
||||
Register("MainMenu", GD.Load<PackedScene>("res://scenes/MainMenu.tscn"));
|
||||
Register("Gameplay", GD.Load<PackedScene>("res://scenes/Gameplay.tscn"));
|
||||
Register("Pause", GD.Load<PackedScene>("res://scenes/Pause.tscn"));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 设置场景系统
|
||||
|
||||
```csharp
|
||||
using GFramework.Godot.Architecture;
|
||||
using GFramework.Godot.Scene;
|
||||
|
||||
public class GameArchitecture : AbstractArchitecture
|
||||
{
|
||||
protected override void InstallModules()
|
||||
{
|
||||
// 注册场景注册表
|
||||
var sceneRegistry = new GameSceneRegistry();
|
||||
RegisterUtility<IGodotSceneRegistry>(sceneRegistry);
|
||||
|
||||
// 注册场景工厂
|
||||
var sceneFactory = new GodotSceneFactory();
|
||||
RegisterUtility<ISceneFactory>(sceneFactory);
|
||||
|
||||
// 注册场景路由
|
||||
var sceneRouter = new GodotSceneRouter();
|
||||
RegisterSystem<ISceneRouter>(sceneRouter);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 使用场景路由
|
||||
|
||||
```csharp
|
||||
using Godot;
|
||||
using GFramework.Godot.Extensions;
|
||||
|
||||
public partial class GameController : Node
|
||||
{
|
||||
public override void _Ready()
|
||||
{
|
||||
// 切换到主菜单
|
||||
SwitchToMainMenu();
|
||||
}
|
||||
|
||||
private async void SwitchToMainMenu()
|
||||
{
|
||||
var sceneRouter = this.GetSystem<ISceneRouter>();
|
||||
await sceneRouter.ReplaceAsync("MainMenu");
|
||||
}
|
||||
|
||||
private async void StartGame()
|
||||
{
|
||||
var sceneRouter = this.GetSystem<ISceneRouter>();
|
||||
await sceneRouter.ReplaceAsync("Gameplay");
|
||||
}
|
||||
|
||||
private async void ShowPause()
|
||||
{
|
||||
var sceneRouter = this.GetSystem<ISceneRouter>();
|
||||
await sceneRouter.PushAsync("Pause");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 高级用法
|
||||
|
||||
### 使用场景行为提供者
|
||||
|
||||
```csharp
|
||||
using Godot;
|
||||
using GFramework.Game.Abstractions.Scene;
|
||||
using GFramework.Godot.Scene;
|
||||
using Godot;
|
||||
|
||||
public partial class GameplayScene : Node2D, ISceneBehaviorProvider
|
||||
public sealed class GameSceneRegistry : GodotSceneRegistry
|
||||
{
|
||||
private GameplaySceneBehavior _behavior;
|
||||
public GameSceneRegistry()
|
||||
{
|
||||
Register(nameof(SceneKey.MainMenu), GD.Load<PackedScene>("res://scenes/main_menu.tscn"));
|
||||
Register(nameof(SceneKey.Gameplay), GD.Load<PackedScene>("res://scenes/gameplay.tscn"));
|
||||
}
|
||||
}
|
||||
|
||||
architecture.RegisterUtility<IGodotSceneRegistry>(new GameSceneRegistry());
|
||||
architecture.RegisterUtility<ISceneFactory>(new GodotSceneFactory());
|
||||
architecture.RegisterSystem(new SceneRouter());
|
||||
```
|
||||
|
||||
项目用什么 key 类型、资源目录或配置表都可以,但最终要能落到 `sceneKey -> PackedScene`。
|
||||
|
||||
### 3. 提供 `ISceneRoot`
|
||||
|
||||
`SceneRouterBase` 只负责切换编排,真正把场景节点挂到 Godot 场景树的是项目自己的 `ISceneRoot`。
|
||||
|
||||
CoreGrid 的 `SceneRoot` 当前做了两件关键事:
|
||||
|
||||
- 在 `_Ready()` 时调用 `_sceneRouter.BindRoot(this)`
|
||||
- 在 `AddScene` / `RemoveScene` 里把 `scene.Original` 当作 `Node` 挂入或移出树
|
||||
|
||||
最小形态可以写成:
|
||||
|
||||
```csharp
|
||||
public sealed class SceneRoot : Node2D, ISceneRoot
|
||||
{
|
||||
[GetSystem] private ISceneRouter _sceneRouter = null!;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
_behavior = new GameplaySceneBehavior(this, "Gameplay");
|
||||
__InjectContextBindings_Generated();
|
||||
_sceneRouter.BindRoot(this);
|
||||
}
|
||||
|
||||
public void AddScene(ISceneBehavior scene)
|
||||
{
|
||||
if (scene.Original is not Node node)
|
||||
throw new InvalidOperationException("SceneBehavior must inherit Godot Node.");
|
||||
|
||||
if (node.GetParent() == null)
|
||||
AddChild(node);
|
||||
}
|
||||
|
||||
public void RemoveScene(ISceneBehavior scene)
|
||||
{
|
||||
if (scene.Original is Node node && node.GetParent() == this)
|
||||
RemoveChild(node);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 让场景节点提供 behavior
|
||||
|
||||
当前有两种可行方式。
|
||||
|
||||
#### 方式 A:实现 `ISceneBehaviorProvider`
|
||||
|
||||
如果你想显式控制 behavior 类型,直接实现 `GetScene()`:
|
||||
|
||||
```csharp
|
||||
public partial class GameplayRoot : Node2D, ISceneBehaviorProvider, IScene
|
||||
{
|
||||
private ISceneBehavior? _scene;
|
||||
|
||||
public ISceneBehavior GetScene()
|
||||
{
|
||||
return _behavior;
|
||||
}
|
||||
}
|
||||
|
||||
// 自定义场景行为
|
||||
public class GameplaySceneBehavior : Node2DSceneBehavior
|
||||
{
|
||||
public GameplaySceneBehavior(Node2D owner, string key) : base(owner, key)
|
||||
{
|
||||
return _scene ??= SceneBehaviorFactory.Create(this, nameof(SceneKey.Gameplay));
|
||||
}
|
||||
|
||||
protected override async ValueTask OnLoadInternalAsync(ISceneEnterParam? param)
|
||||
public ValueTask OnLoadAsync(ISceneEnterParam? param)
|
||||
{
|
||||
GD.Print("加载游戏场景");
|
||||
// 加载游戏资源
|
||||
await Task.CompletedTask;
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
protected override async ValueTask OnEnterInternalAsync()
|
||||
public ValueTask OnEnterAsync()
|
||||
{
|
||||
GD.Print("进入游戏场景");
|
||||
Owner.Show();
|
||||
await Task.CompletedTask;
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
public ValueTask OnPauseAsync()
|
||||
{
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
public ValueTask OnResumeAsync()
|
||||
{
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
public ValueTask OnExitAsync()
|
||||
{
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
public ValueTask OnUnloadAsync()
|
||||
{
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 不同类型的场景行为
|
||||
#### 方式 B:用 `[AutoScene]` 让生成器补样板
|
||||
|
||||
当前更贴近真实消费者 wiring 的方式,是让 `GFramework.Godot.SourceGenerators` 生成 `SceneKeyStr` 和 `GetScene()`:
|
||||
|
||||
```csharp
|
||||
// Node2D 场景
|
||||
public class Node2DSceneBehavior : SceneBehaviorBase<Node2D>
|
||||
{
|
||||
public Node2DSceneBehavior(Node2D owner, string key) : base(owner, key)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
// Node3D 场景
|
||||
public class Node3DSceneBehavior : SceneBehaviorBase<Node3D>
|
||||
{
|
||||
public Node3DSceneBehavior(Node3D owner, string key) : base(owner, key)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
// Control 场景(UI)
|
||||
public class ControlSceneBehavior : SceneBehaviorBase<Control>
|
||||
{
|
||||
public ControlSceneBehavior(Control owner, string key) : base(owner, key)
|
||||
{
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 场景根节点管理
|
||||
|
||||
```csharp
|
||||
using Godot;
|
||||
using GFramework.Godot.Scene;
|
||||
|
||||
public partial class SceneRoot : Node, ISceneRoot
|
||||
{
|
||||
private Node _currentSceneNode;
|
||||
|
||||
public void AttachScene(Node sceneNode)
|
||||
{
|
||||
// 移除旧场景
|
||||
if (_currentSceneNode != null)
|
||||
{
|
||||
RemoveChild(_currentSceneNode);
|
||||
_currentSceneNode.QueueFree();
|
||||
}
|
||||
|
||||
// 添加新场景
|
||||
_currentSceneNode = sceneNode;
|
||||
AddChild(_currentSceneNode);
|
||||
}
|
||||
|
||||
public void DetachScene(Node sceneNode)
|
||||
{
|
||||
if (_currentSceneNode == sceneNode)
|
||||
{
|
||||
RemoveChild(_currentSceneNode);
|
||||
_currentSceneNode = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 场景参数传递
|
||||
|
||||
```csharp
|
||||
// 定义场景参数
|
||||
public class GameplayEnterParam : ISceneEnterParam
|
||||
{
|
||||
public int Level { get; set; }
|
||||
public string Difficulty { get; set; }
|
||||
}
|
||||
|
||||
// 在场景中接收参数
|
||||
public partial class GameplayScene : Node2D, IScene
|
||||
{
|
||||
private int _level;
|
||||
private string _difficulty;
|
||||
|
||||
public async ValueTask OnLoadAsync(ISceneEnterParam? param)
|
||||
{
|
||||
if (param is GameplayEnterParam gameplayParam)
|
||||
{
|
||||
_level = gameplayParam.Level;
|
||||
_difficulty = gameplayParam.Difficulty;
|
||||
GD.Print($"加载关卡 {_level},难度: {_difficulty}");
|
||||
}
|
||||
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
// ... 其他生命周期方法
|
||||
}
|
||||
|
||||
// 切换场景时传递参数
|
||||
var sceneRouter = this.GetSystem<ISceneRouter>();
|
||||
await sceneRouter.ReplaceAsync("Gameplay", new GameplayEnterParam
|
||||
{
|
||||
Level = 1,
|
||||
Difficulty = "Normal"
|
||||
});
|
||||
```
|
||||
|
||||
### 场景预加载
|
||||
|
||||
```csharp
|
||||
public partial class LoadingScene : Control
|
||||
{
|
||||
public override async void _Ready()
|
||||
{
|
||||
// 预加载下一个场景
|
||||
await PreloadNextScene();
|
||||
|
||||
// 切换到预加载的场景
|
||||
var sceneRouter = this.GetSystem<ISceneRouter>();
|
||||
await sceneRouter.ReplaceAsync("Gameplay");
|
||||
}
|
||||
|
||||
private async Task PreloadNextScene()
|
||||
{
|
||||
var sceneFactory = this.GetUtility<ISceneFactory>();
|
||||
var sceneBehavior = sceneFactory.Create("Gameplay");
|
||||
|
||||
// 预加载场景资源
|
||||
await sceneBehavior.LoadAsync(null);
|
||||
|
||||
GD.Print("场景预加载完成");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 场景转换动画
|
||||
|
||||
```csharp
|
||||
using Godot;
|
||||
using GFramework.Game.Abstractions.Scene;
|
||||
using GFramework.Godot.SourceGenerators.Abstractions.UI;
|
||||
using Godot;
|
||||
|
||||
public class FadeTransitionHandler : ISceneTransitionHandler
|
||||
[AutoScene(nameof(SceneKey.Gameplay))]
|
||||
public partial class GameplayRoot : Node2D, ISceneBehaviorProvider, IScene
|
||||
{
|
||||
private ColorRect _fadeRect;
|
||||
|
||||
public FadeTransitionHandler(ColorRect fadeRect)
|
||||
public ValueTask OnLoadAsync(ISceneEnterParam? param)
|
||||
{
|
||||
_fadeRect = fadeRect;
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
public async ValueTask OnBeforeExitAsync(SceneTransitionEvent @event)
|
||||
public ValueTask OnEnterAsync()
|
||||
{
|
||||
// 淡出动画
|
||||
var tween = _fadeRect.CreateTween();
|
||||
tween.TweenProperty(_fadeRect, "modulate:a", 1.0f, 0.3f);
|
||||
await tween.ToSignal(tween, Tween.SignalName.Finished);
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
public async ValueTask OnAfterEnterAsync(SceneTransitionEvent @event)
|
||||
public ValueTask OnPauseAsync()
|
||||
{
|
||||
// 淡入动画
|
||||
var tween = _fadeRect.CreateTween();
|
||||
tween.TweenProperty(_fadeRect, "modulate:a", 0.0f, 0.3f);
|
||||
await tween.ToSignal(tween, Tween.SignalName.Finished);
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
// ... 其他方法
|
||||
}
|
||||
```
|
||||
|
||||
### 场景间通信
|
||||
|
||||
```csharp
|
||||
// 通过事件通信
|
||||
public partial class GameplayScene : Node2D, IScene
|
||||
{
|
||||
public async ValueTask OnEnterAsync()
|
||||
public ValueTask OnResumeAsync()
|
||||
{
|
||||
// 发送场景进入事件
|
||||
this.SendEvent(new GameplaySceneEnteredEvent());
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
// 在其他地方监听
|
||||
public partial class HUD : Control
|
||||
{
|
||||
public override void _Ready()
|
||||
{
|
||||
this.RegisterEvent<GameplaySceneEnteredEvent>(OnGameplayEntered);
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
private void OnGameplayEntered(GameplaySceneEnteredEvent evt)
|
||||
public ValueTask OnExitAsync()
|
||||
{
|
||||
GD.Print("游戏场景已进入,显示 HUD");
|
||||
Show();
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
public ValueTask OnUnloadAsync()
|
||||
{
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **场景脚本实现 IScene 接口**:获得完整的生命周期管理
|
||||
```csharp
|
||||
✓ public partial class MyScene : Node2D, IScene { }
|
||||
✗ public partial class MyScene : Node2D { } // 无生命周期管理
|
||||
```
|
||||
|
||||
2. **使用场景注册表管理场景资源**:集中管理所有场景
|
||||
```csharp
|
||||
public class GameSceneRegistry : GodotSceneRegistry
|
||||
{
|
||||
public GameSceneRegistry()
|
||||
{
|
||||
Register("MainMenu", GD.Load<PackedScene>("res://scenes/MainMenu.tscn"));
|
||||
Register("Gameplay", GD.Load<PackedScene>("res://scenes/Gameplay.tscn"));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. **在 OnLoadAsync 中加载资源**:避免场景切换卡顿
|
||||
```csharp
|
||||
public async ValueTask OnLoadAsync(ISceneEnterParam? param)
|
||||
{
|
||||
// 异步加载资源
|
||||
await LoadTexturesAsync();
|
||||
await LoadAudioAsync();
|
||||
}
|
||||
```
|
||||
|
||||
4. **使用场景根节点管理场景树**:保持场景树结构清晰
|
||||
```csharp
|
||||
// 创建场景根节点
|
||||
var sceneRoot = new Node { Name = "SceneRoot" };
|
||||
AddChild(sceneRoot);
|
||||
|
||||
// 绑定到场景路由
|
||||
sceneRouter.BindRoot(sceneRoot);
|
||||
```
|
||||
|
||||
5. **正确清理场景资源**:在 OnUnloadAsync 中释放资源
|
||||
```csharp
|
||||
public async ValueTask OnUnloadAsync()
|
||||
{
|
||||
// 释放资源
|
||||
_texture?.Dispose();
|
||||
_audioStream?.Dispose();
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
```
|
||||
|
||||
6. **使用场景参数传递数据**:避免使用全局变量
|
||||
```csharp
|
||||
✓ await sceneRouter.ReplaceAsync("Gameplay", new GameplayEnterParam { Level = 1 });
|
||||
✗ GlobalData.CurrentLevel = 1; // 避免全局状态
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 问题:如何在 Godot 场景中使用 GFramework?
|
||||
|
||||
**解答**:
|
||||
场景脚本实现 `IScene` 接口:
|
||||
生成器当前会补出与源码一致的 `GetScene()`:
|
||||
|
||||
```csharp
|
||||
public partial class MyScene : Node2D, IScene
|
||||
public ISceneBehavior GetScene()
|
||||
{
|
||||
public async ValueTask OnLoadAsync(ISceneEnterParam? param) { }
|
||||
public async ValueTask OnEnterAsync() { }
|
||||
// ... 实现其他方法
|
||||
return __autoSceneBehavior_Generated ??= SceneBehaviorFactory.Create(this, SceneKeyStr);
|
||||
}
|
||||
```
|
||||
|
||||
### 问题:场景切换时节点如何管理?
|
||||
要注意两点:
|
||||
|
||||
**解答**:
|
||||
使用场景根节点管理:
|
||||
- `[AutoScene]` 只生成方法和 key,不会替你自动补 `: ISceneBehaviorProvider`
|
||||
- `IScene` 仍然是业务生命周期契约;不实现它时,默认 behavior 只会保留基础节点切换语义
|
||||
|
||||
### 5. 从业务代码发起导航
|
||||
|
||||
一旦 registry、factory、router、root 都装好,导航入口仍然是 `ISceneRouter`:
|
||||
|
||||
```csharp
|
||||
// 场景路由会自动管理节点的添加和移除
|
||||
await sceneRouter.ReplaceAsync("NewScene");
|
||||
// 旧场景节点会被移除,新场景节点会被添加
|
||||
await sceneRouter.ReplaceAsync(nameof(SceneKey.MainMenu));
|
||||
await sceneRouter.ReplaceAsync(nameof(SceneKey.Gameplay), new GameplayEnterParam());
|
||||
await sceneRouter.PushAsync(nameof(SceneKey.PauseMenu));
|
||||
await sceneRouter.PopAsync();
|
||||
```
|
||||
|
||||
### 问题:如何实现场景预加载?
|
||||
## 当前边界
|
||||
|
||||
**解答**:
|
||||
使用场景工厂提前创建场景:
|
||||
### 没有 `GodotSceneRouter`
|
||||
|
||||
```csharp
|
||||
var sceneFactory = this.GetUtility<ISceneFactory>();
|
||||
var sceneBehavior = sceneFactory.Create("NextScene");
|
||||
await sceneBehavior.LoadAsync(null);
|
||||
```
|
||||
仓库当前不存在 `GodotSceneRouter` 类型。旧文档里把它写成默认入口是失真的;实际入口仍然是项目侧继承
|
||||
`SceneRouterBase` 的 router。
|
||||
|
||||
### 问题:场景生命周期方法的调用顺序是什么?
|
||||
### 没有自动注册所有场景
|
||||
|
||||
**解答**:
|
||||
当前运行时只认识你注册进 `IGodotSceneRegistry` 的 `PackedScene`。它不会扫描目录、不会从脚本类型自动反推出注册表。
|
||||
|
||||
- 进入场景:`OnLoadAsync` -> `OnEnterAsync` -> `OnShow`
|
||||
- 暂停场景:`OnPause` -> `OnHide`
|
||||
- 恢复场景:`OnShow` -> `OnResume`
|
||||
- 退出场景:`OnHide` -> `OnExitAsync` -> `OnUnloadAsync`
|
||||
### provider 是“优先路径”,不是“唯一路径”
|
||||
|
||||
### 问题:如何在场景中访问架构组件?
|
||||
`GodotSceneFactory` 会优先使用 `ISceneBehaviorProvider`,但没有 provider 时仍会按节点类型自动包装。这个行为和 UI 系统不同;
|
||||
UI 工厂当前没有同等的自动回退。
|
||||
|
||||
**解答**:
|
||||
使用扩展方法:
|
||||
### root 仍然是项目职责
|
||||
|
||||
```csharp
|
||||
public partial class MyScene : Node2D, IScene
|
||||
{
|
||||
public async ValueTask OnEnterAsync()
|
||||
{
|
||||
var playerModel = this.GetModel<PlayerModel>();
|
||||
var gameSystem = this.GetSystem<GameSystem>();
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
```
|
||||
`ISceneRoot` 的实现决定:
|
||||
|
||||
### 问题:场景切换时如何显示加载界面?
|
||||
- 节点挂到哪里
|
||||
- 移除时如何释放
|
||||
- 是否保留额外的当前视图引用
|
||||
|
||||
**解答**:
|
||||
使用场景转换处理器:
|
||||
Godot runtime 不会替项目生成统一的 root 节点。
|
||||
|
||||
```csharp
|
||||
public class LoadingScreenHandler : ISceneTransitionHandler
|
||||
{
|
||||
public async ValueTask OnBeforeLoadAsync(SceneTransitionEvent @event)
|
||||
{
|
||||
// 显示加载界面
|
||||
ShowLoadingScreen();
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
## 继续阅读
|
||||
|
||||
public async ValueTask OnAfterEnterAsync(SceneTransitionEvent @event)
|
||||
{
|
||||
// 隐藏加载界面
|
||||
HideLoadingScreen();
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [场景系统](/zh-CN/game/scene) - 核心场景管理
|
||||
- [Godot 架构集成](/zh-CN/godot/architecture) - Godot 架构基础
|
||||
- [Godot UI 系统](/zh-CN/godot/ui) - Godot UI 集成
|
||||
- [Godot 扩展](/zh-CN/godot/extensions) - Godot 扩展方法
|
||||
1. [Godot 运行时集成](./index.md)
|
||||
2. [Godot 架构集成](./architecture.md)
|
||||
3. [Game 场景系统](../game/scene.md)
|
||||
4. [AutoScene 生成器](../source-generators/auto-scene-generator.md)
|
||||
|
||||
@ -1,643 +1,351 @@
|
||||
---
|
||||
title: Godot UI 系统
|
||||
description: Godot UI 系统提供了 GFramework UI 管理与 Godot Control 节点的完整集成。
|
||||
description: 以当前 GFramework.Godot 源码、Game UI 契约与 CoreGrid 接线为准,说明 PackedScene UI 工厂、页面行为和层级接入路径。
|
||||
---
|
||||
|
||||
# Godot UI 系统
|
||||
|
||||
## 概述
|
||||
`GFramework.Godot.UI` 当前负责的是把 `GFramework.Game` 的 UI 路由契约接到 `Control` / `CanvasLayer` /
|
||||
`PackedScene` 上,而不是定义一个 Godot 专属 router。
|
||||
|
||||
Godot UI 系统是 GFramework.Godot 中连接框架 UI 管理与 Godot Control 节点的核心组件。它提供了 UI 页面行为封装、UI 工厂、UI
|
||||
注册表等功能,支持多层级 UI 显示,让你可以在 Godot 项目中使用 GFramework 的 UI 管理系统。
|
||||
当前真正参与这条链路的核心类型是:
|
||||
|
||||
通过 Godot UI 系统,你可以使用 GFramework 的 UI 路由、生命周期管理、多层级显示等功能,同时保持与 Godot UI 系统的完美兼容。
|
||||
- `IGodotUiRegistry` / `GodotUiRegistry`
|
||||
- `GodotUiFactory`
|
||||
- `CanvasItemUiPageBehaviorBase<T>`
|
||||
- `UiPageBehaviorFactory`
|
||||
- `Page` / `Overlay` / `Modal` / `Toast` / `Topmost` 五类 layer behavior
|
||||
- 项目侧实现的 `IUiRoot`
|
||||
- 项目侧继承 `UiRouterBase` 的 router
|
||||
|
||||
**主要特性**:
|
||||
## 当前公开入口
|
||||
|
||||
- UI 页面行为封装
|
||||
- UI 工厂和注册表
|
||||
- 与 Godot PackedScene 集成
|
||||
- 多层级 UI 支持(Page、Overlay、Modal、Toast、Topmost)
|
||||
- UI 生命周期管理
|
||||
- UI 根节点管理
|
||||
### `IGodotUiRegistry`
|
||||
|
||||
## 核心概念
|
||||
Godot 侧 UI 资源表,底层是 `IAssetRegistry<PackedScene>`。它只负责:
|
||||
|
||||
### UI 页面行为
|
||||
- `uiKey -> PackedScene` 映射
|
||||
- 让 `GodotUiFactory` 可以按 key 实例化 UI 页面
|
||||
|
||||
`CanvasItemUiPageBehaviorBase<T>` 封装了 Godot Control 节点的 UI 行为:
|
||||
框架当前不会自动扫描 `.tscn`、不会自动根据类型名补全注册表。
|
||||
|
||||
### `GodotUiFactory`
|
||||
|
||||
`GodotUiFactory.Create(string uiKey)` 的当前行为比场景工厂更严格:
|
||||
|
||||
1. 从 `IGodotUiRegistry` 取出 `PackedScene`
|
||||
2. 调用 `Instantiate()`
|
||||
3. 节点必须实现 `IUiPageBehaviorProvider`
|
||||
4. 返回 `provider.GetPage()`
|
||||
|
||||
如果实例化得到的节点没有实现 `IUiPageBehaviorProvider`,当前实现会直接抛 `InvalidCastException`。这也是 UI 页面文档必须强调
|
||||
`GetPage()` / `[AutoUiPage]` 的原因。
|
||||
|
||||
### `CanvasItemUiPageBehaviorBase<T>`
|
||||
|
||||
Godot runtime 的页面行为包装基类。它把 `IUiPageBehavior` 的这些语义接到 `CanvasItem` 上:
|
||||
|
||||
- `Key`
|
||||
- `Layer`
|
||||
- `Handle`
|
||||
- `IsAlive`
|
||||
- `IsVisible`
|
||||
- `InteractionProfile`
|
||||
- `OnEnter` / `OnExit`
|
||||
- `OnPause` / `OnResume`
|
||||
- `OnShow` / `OnHide`
|
||||
- `TryHandleUiAction(UiInputAction action)`
|
||||
|
||||
如果 owner 同时实现了 `IUiPage`、`IUiInteractionProfileProvider`、`IUiActionHandler`,这些契约都会被页面行为继续利用。
|
||||
|
||||
### `UiPageBehaviorFactory`
|
||||
|
||||
当前 layer 到 behavior 的映射来自运行时代码本身:
|
||||
|
||||
- `UiLayer.Page` -> `PageLayerUiPageBehavior<T>`
|
||||
- `UiLayer.Overlay` -> `OverlayLayerUiPageBehavior<T>`
|
||||
- `UiLayer.Modal` -> `ModalLayerUiPageBehavior<T>`
|
||||
- `UiLayer.Toast` -> `ToastLayerUiPageBehavior<T>`
|
||||
- `UiLayer.Topmost` -> `TopmostLayerUiPageBehavior<T>`
|
||||
|
||||
几个容易被旧文档写偏的默认语义如下:
|
||||
|
||||
- `Page`
|
||||
- 不可重入,阻断输入
|
||||
- `Overlay`
|
||||
- 可重入,非模态,不阻断输入;暂停时不会停掉节点处理
|
||||
- `Modal`
|
||||
- 可重入,模态,阻断输入
|
||||
- `Toast`
|
||||
- 可重入,非模态,不阻断输入
|
||||
- `Topmost`
|
||||
- 不可重入,模态,阻断输入
|
||||
|
||||
## 最小接入路径
|
||||
|
||||
### 1. 继续在项目层保留自己的 router
|
||||
|
||||
仓库当前不存在 `GodotUiRouter` 类型。实际做法仍然是项目侧继承 `GFramework.Game.UI.UiRouterBase`。
|
||||
|
||||
`ai-libs/CoreGrid` 的 `UiRouter` 目前就是:
|
||||
|
||||
```csharp
|
||||
public abstract class CanvasItemUiPageBehaviorBase<T> : IUiPageBehavior
|
||||
where T : CanvasItem
|
||||
using LoggingTransitionHandler = GFramework.Game.UI.Handler.LoggingTransitionHandler;
|
||||
|
||||
namespace CoreGrid.scripts.core.ui;
|
||||
|
||||
[Log]
|
||||
public partial class UiRouter : UiRouterBase
|
||||
{
|
||||
protected readonly T Owner;
|
||||
public string Key { get; }
|
||||
public UiLayer Layer { get; }
|
||||
public bool IsReentrant { get; }
|
||||
protected override void RegisterHandlers()
|
||||
{
|
||||
_log.Debug("Registering default transition handlers");
|
||||
RegisterHandler(new LoggingTransitionHandler());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### UI 工厂
|
||||
Godot runtime 自身并不接管这层 router 的定义。
|
||||
|
||||
`GodotUiFactory` 负责创建 UI 实例:
|
||||
### 2. 注册 `IGodotUiRegistry` 与 `IUiFactory`
|
||||
|
||||
最小 wiring 需要显式注册 UI 资源表和工厂:
|
||||
|
||||
```csharp
|
||||
public class GodotUiFactory : IUiFactory
|
||||
{
|
||||
public IUiPageBehavior Create(string uiKey);
|
||||
}
|
||||
```
|
||||
|
||||
### UI 层级行为
|
||||
|
||||
不同层级的 UI 有不同的行为类:
|
||||
|
||||
```csharp
|
||||
// Page 层(栈管理)
|
||||
public class PageLayerUiPageBehavior : CanvasItemUiPageBehaviorBase<Control>
|
||||
{
|
||||
public override UiLayer Layer => UiLayer.Page;
|
||||
public override bool IsReentrant => false;
|
||||
}
|
||||
|
||||
// Modal 层(模态对话框)
|
||||
public class ModalLayerUiPageBehavior : CanvasItemUiPageBehaviorBase<Control>
|
||||
{
|
||||
public override UiLayer Layer => UiLayer.Modal;
|
||||
public override bool IsReentrant => true;
|
||||
}
|
||||
```
|
||||
|
||||
## 基本用法
|
||||
|
||||
### 创建 UI 脚本
|
||||
|
||||
```csharp
|
||||
using Godot;
|
||||
using GFramework.Game.Abstractions.UI;
|
||||
using GFramework.Godot.UI;
|
||||
using Godot;
|
||||
|
||||
public partial class MainMenuPage : Control, IUiPage
|
||||
public sealed class GameUiRegistry : GodotUiRegistry
|
||||
{
|
||||
public GameUiRegistry()
|
||||
{
|
||||
Register(nameof(UiKey.MainMenu), GD.Load<PackedScene>("res://ui/main_menu.tscn"));
|
||||
Register(nameof(UiKey.PauseMenu), GD.Load<PackedScene>("res://ui/pause_menu.tscn"));
|
||||
Register(nameof(UiKey.OptionsMenu), GD.Load<PackedScene>("res://ui/options_menu.tscn"));
|
||||
}
|
||||
}
|
||||
|
||||
architecture.RegisterUtility<IGodotUiRegistry>(new GameUiRegistry());
|
||||
architecture.RegisterUtility<IUiFactory>(new GodotUiFactory());
|
||||
architecture.RegisterSystem(new UiRouter());
|
||||
```
|
||||
|
||||
### 3. 提供 `IUiRoot`
|
||||
|
||||
`UiRouterBase` 只负责页面栈、layer UI、输入仲裁和暂停语义;真正把页面挂到 Godot 容器的是项目自己的 `IUiRoot`。
|
||||
|
||||
CoreGrid 当前的 `UiRoot` 做法和源码契约一致:
|
||||
|
||||
- 继承 `CanvasLayer`
|
||||
- 为每个 `UiLayer` 创建一个 `Control` 容器
|
||||
- 在 `_Ready()` 时调用 `_uiRouter.BindRoot(this)`
|
||||
- 在 `AddUiPage` / `RemoveUiPage` 中处理 `CanvasItem` 挂载与释放
|
||||
|
||||
最小形态可以写成:
|
||||
|
||||
```csharp
|
||||
public sealed class UiRoot : CanvasLayer, IUiRoot
|
||||
{
|
||||
[GetSystem] private IUiRouter _uiRouter = null!;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
__InjectContextBindings_Generated();
|
||||
_uiRouter.BindRoot(this);
|
||||
}
|
||||
|
||||
public void AddUiPage(IUiPageBehavior child)
|
||||
{
|
||||
AddUiPage(child, UiLayer.Page);
|
||||
}
|
||||
|
||||
public void AddUiPage(IUiPageBehavior child, UiLayer layer, int orderInLayer = 0)
|
||||
{
|
||||
if (child.View is not CanvasItem item)
|
||||
throw new InvalidOperationException("UIPage View must be a Godot Node");
|
||||
|
||||
AddChild(item);
|
||||
item.ZIndex = (int)layer * 100 + orderInLayer;
|
||||
}
|
||||
|
||||
public void RemoveUiPage(IUiPageBehavior child)
|
||||
{
|
||||
if (child.View is Node node && node.GetParent() == this)
|
||||
RemoveChild(node);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 让页面节点提供 `GetPage()`
|
||||
|
||||
因为 `GodotUiFactory` 不会自动回退到默认 behavior,页面节点必须显式提供 `GetPage()`。
|
||||
|
||||
#### 方式 A:手写 `IUiPageBehaviorProvider`
|
||||
|
||||
```csharp
|
||||
public partial class PauseMenu : Control, IUiPage, IUiPageBehaviorProvider
|
||||
{
|
||||
private IUiPageBehavior? _page;
|
||||
|
||||
public IUiPageBehavior GetPage()
|
||||
{
|
||||
return _page ??= UiPageBehaviorFactory.Create(this, nameof(UiKey.PauseMenu), UiLayer.Modal);
|
||||
}
|
||||
|
||||
public void OnEnter(IUiPageEnterParam? param)
|
||||
{
|
||||
GD.Print("进入主菜单");
|
||||
Show();
|
||||
}
|
||||
|
||||
public void OnExit()
|
||||
{
|
||||
GD.Print("退出主菜单");
|
||||
Hide();
|
||||
}
|
||||
|
||||
public void OnPause()
|
||||
{
|
||||
GD.Print("暂停主菜单");
|
||||
}
|
||||
|
||||
public void OnResume()
|
||||
{
|
||||
GD.Print("恢复主菜单");
|
||||
}
|
||||
|
||||
public void OnShow()
|
||||
{
|
||||
Show();
|
||||
}
|
||||
|
||||
public void OnHide()
|
||||
{
|
||||
Hide();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 实现 UI 页面行为提供者
|
||||
#### 方式 B:用 `[AutoUiPage]` 让生成器补样板
|
||||
|
||||
当前更贴近真实消费者 wiring 的方式,是让生成器产出 `UiKeyStr` 和 `GetPage()`:
|
||||
|
||||
```csharp
|
||||
using Godot;
|
||||
using GFramework.Game.Abstractions.UI;
|
||||
using GFramework.Godot.UI;
|
||||
|
||||
public partial class MainMenuPage : Control, IUiPageBehaviorProvider
|
||||
{
|
||||
private PageLayerUiPageBehavior _behavior;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
_behavior = new PageLayerUiPageBehavior(this, "MainMenu");
|
||||
}
|
||||
|
||||
public IUiPageBehavior GetPage()
|
||||
{
|
||||
return _behavior;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 注册 UI
|
||||
|
||||
```csharp
|
||||
using GFramework.Godot.UI;
|
||||
using GFramework.Godot.SourceGenerators.Abstractions.UI;
|
||||
using Godot;
|
||||
|
||||
public class GameUiRegistry : GodotUiRegistry
|
||||
{
|
||||
public GameUiRegistry()
|
||||
{
|
||||
// 注册 UI 资源
|
||||
Register("MainMenu", GD.Load<PackedScene>("res://ui/MainMenu.tscn"));
|
||||
Register("Settings", GD.Load<PackedScene>("res://ui/Settings.tscn"));
|
||||
Register("ConfirmDialog", GD.Load<PackedScene>("res://ui/ConfirmDialog.tscn"));
|
||||
Register("Toast", GD.Load<PackedScene>("res://ui/Toast.tscn"));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 设置 UI 系统
|
||||
|
||||
```csharp
|
||||
using GFramework.Godot.Architecture;
|
||||
using GFramework.Godot.UI;
|
||||
|
||||
public class GameArchitecture : AbstractArchitecture
|
||||
{
|
||||
protected override void InstallModules()
|
||||
{
|
||||
// 注册 UI 注册表
|
||||
var uiRegistry = new GameUiRegistry();
|
||||
RegisterUtility<IGodotUiRegistry>(uiRegistry);
|
||||
|
||||
// 注册 UI 工厂
|
||||
var uiFactory = new GodotUiFactory();
|
||||
RegisterUtility<IUiFactory>(uiFactory);
|
||||
|
||||
// 注册 UI 路由
|
||||
var uiRouter = new GodotUiRouter();
|
||||
RegisterSystem<IUiRouter>(uiRouter);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 使用 UI 路由
|
||||
|
||||
```csharp
|
||||
using Godot;
|
||||
using GFramework.Godot.Extensions;
|
||||
|
||||
public partial class GameController : Node
|
||||
{
|
||||
public override void _Ready()
|
||||
{
|
||||
ShowMainMenu();
|
||||
}
|
||||
|
||||
private async void ShowMainMenu()
|
||||
{
|
||||
var uiRouter = this.GetSystem<IUiRouter>();
|
||||
await uiRouter.PushAsync("MainMenu");
|
||||
}
|
||||
|
||||
private async void ShowSettings()
|
||||
{
|
||||
var uiRouter = this.GetSystem<IUiRouter>();
|
||||
await uiRouter.PushAsync("Settings");
|
||||
}
|
||||
|
||||
private void ShowDialog()
|
||||
{
|
||||
var uiRouter = this.GetSystem<IUiRouter>();
|
||||
uiRouter.Show("ConfirmDialog", UiLayer.Modal);
|
||||
}
|
||||
|
||||
private void ShowToast(string message)
|
||||
{
|
||||
var uiRouter = this.GetSystem<IUiRouter>();
|
||||
uiRouter.Show("Toast", UiLayer.Toast, new ToastParam { Message = message });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 高级用法
|
||||
|
||||
### 不同层级的 UI 行为
|
||||
|
||||
```csharp
|
||||
// Page 层 UI(栈管理,不可重入)
|
||||
public partial class MainMenuPage : Control, IUiPageBehaviorProvider
|
||||
{
|
||||
public IUiPageBehavior GetPage()
|
||||
{
|
||||
return new PageLayerUiPageBehavior(this, "MainMenu");
|
||||
}
|
||||
}
|
||||
|
||||
// Overlay 层 UI(浮层,可重入)
|
||||
public partial class InfoPanel : Control, IUiPageBehaviorProvider
|
||||
{
|
||||
public IUiPageBehavior GetPage()
|
||||
{
|
||||
return new OverlayLayerUiPageBehavior(this, "InfoPanel");
|
||||
}
|
||||
}
|
||||
|
||||
// Modal 层 UI(模态对话框,可重入)
|
||||
public partial class ConfirmDialog : Control, IUiPageBehaviorProvider
|
||||
{
|
||||
public IUiPageBehavior GetPage()
|
||||
{
|
||||
return new ModalLayerUiPageBehavior(this, "ConfirmDialog");
|
||||
}
|
||||
}
|
||||
|
||||
// Toast 层 UI(提示,可重入)
|
||||
public partial class ToastMessage : Control, IUiPageBehaviorProvider
|
||||
{
|
||||
public IUiPageBehavior GetPage()
|
||||
{
|
||||
return new ToastLayerUiPageBehavior(this, "Toast");
|
||||
}
|
||||
}
|
||||
|
||||
// Topmost 层 UI(顶层,不可重入)
|
||||
public partial class LoadingScreen : Control, IUiPageBehaviorProvider
|
||||
{
|
||||
public IUiPageBehavior GetPage()
|
||||
{
|
||||
return new TopmostLayerUiPageBehavior(this, "Loading");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### UI 参数传递
|
||||
|
||||
```csharp
|
||||
// 定义 UI 参数
|
||||
public class ConfirmDialogParam : IUiPageEnterParam
|
||||
{
|
||||
public string Title { get; set; }
|
||||
public string Message { get; set; }
|
||||
public Action OnConfirm { get; set; }
|
||||
public Action OnCancel { get; set; }
|
||||
}
|
||||
|
||||
// 在 UI 中接收参数
|
||||
public partial class ConfirmDialog : Control, IUiPage
|
||||
{
|
||||
private Label _titleLabel;
|
||||
private Label _messageLabel;
|
||||
private Action _onConfirm;
|
||||
private Action _onCancel;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
_titleLabel = GetNode<Label>("Title");
|
||||
_messageLabel = GetNode<Label>("Message");
|
||||
|
||||
GetNode<Button>("ConfirmButton").Pressed += OnConfirmPressed;
|
||||
GetNode<Button>("CancelButton").Pressed += OnCancelPressed;
|
||||
}
|
||||
|
||||
public void OnEnter(IUiPageEnterParam? param)
|
||||
{
|
||||
if (param is ConfirmDialogParam dialogParam)
|
||||
{
|
||||
_titleLabel.Text = dialogParam.Title;
|
||||
_messageLabel.Text = dialogParam.Message;
|
||||
_onConfirm = dialogParam.OnConfirm;
|
||||
_onCancel = dialogParam.OnCancel;
|
||||
}
|
||||
|
||||
Show();
|
||||
}
|
||||
|
||||
private void OnConfirmPressed()
|
||||
{
|
||||
_onConfirm?.Invoke();
|
||||
CloseDialog();
|
||||
}
|
||||
|
||||
private void OnCancelPressed()
|
||||
{
|
||||
_onCancel?.Invoke();
|
||||
CloseDialog();
|
||||
}
|
||||
|
||||
private void CloseDialog()
|
||||
{
|
||||
var uiRouter = this.GetSystem<IUiRouter>();
|
||||
if (Handle.HasValue)
|
||||
{
|
||||
uiRouter.Hide(Handle.Value, UiLayer.Modal, destroy: true);
|
||||
}
|
||||
}
|
||||
|
||||
// ... 其他生命周期方法
|
||||
}
|
||||
|
||||
// 显示对话框
|
||||
var uiRouter = this.GetSystem<IUiRouter>();
|
||||
uiRouter.Show("ConfirmDialog", UiLayer.Modal, new ConfirmDialogParam
|
||||
{
|
||||
Title = "确认",
|
||||
Message = "确定要退出吗?",
|
||||
OnConfirm = () => GD.Print("确认"),
|
||||
OnCancel = () => GD.Print("取消")
|
||||
});
|
||||
```
|
||||
|
||||
### UI 根节点管理
|
||||
|
||||
```csharp
|
||||
using Godot;
|
||||
using GFramework.Godot.UI;
|
||||
|
||||
public partial class UiRoot : CanvasLayer, IUiRoot
|
||||
{
|
||||
private Control _pageLayer;
|
||||
private Control _overlayLayer;
|
||||
private Control _modalLayer;
|
||||
private Control _toastLayer;
|
||||
private Control _topmostLayer;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
// 创建各层级容器
|
||||
_pageLayer = new Control { Name = "PageLayer" };
|
||||
_overlayLayer = new Control { Name = "OverlayLayer" };
|
||||
_modalLayer = new Control { Name = "ModalLayer" };
|
||||
_toastLayer = new Control { Name = "ToastLayer" };
|
||||
_topmostLayer = new Control { Name = "TopmostLayer" };
|
||||
|
||||
AddChild(_pageLayer);
|
||||
AddChild(_overlayLayer);
|
||||
AddChild(_modalLayer);
|
||||
AddChild(_toastLayer);
|
||||
AddChild(_topmostLayer);
|
||||
}
|
||||
|
||||
public void AttachPage(Control page, UiLayer layer)
|
||||
{
|
||||
var container = GetLayerContainer(layer);
|
||||
container.AddChild(page);
|
||||
}
|
||||
|
||||
public void DetachPage(Control page, UiLayer layer)
|
||||
{
|
||||
var container = GetLayerContainer(layer);
|
||||
container.RemoveChild(page);
|
||||
}
|
||||
|
||||
private Control GetLayerContainer(UiLayer layer)
|
||||
{
|
||||
return layer switch
|
||||
{
|
||||
UiLayer.Page => _pageLayer,
|
||||
UiLayer.Overlay => _overlayLayer,
|
||||
UiLayer.Modal => _modalLayer,
|
||||
UiLayer.Toast => _toastLayer,
|
||||
UiLayer.Topmost => _topmostLayer,
|
||||
_ => _pageLayer
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### UI 动画和过渡
|
||||
|
||||
```csharp
|
||||
public partial class AnimatedPage : Control, IUiPage
|
||||
[AutoUiPage(nameof(UiKey.MainMenu), nameof(UiLayer.Page))]
|
||||
public partial class MainMenu : Control, IUiPageBehaviorProvider, IUiPage
|
||||
{
|
||||
public void OnEnter(IUiPageEnterParam? param)
|
||||
{
|
||||
// 淡入动画
|
||||
Modulate = new Color(1, 1, 1, 0);
|
||||
Show();
|
||||
|
||||
var tween = CreateTween();
|
||||
tween.TweenProperty(this, "modulate:a", 1.0f, 0.3f);
|
||||
}
|
||||
|
||||
public void OnExit()
|
||||
{
|
||||
// 淡出动画
|
||||
var tween = CreateTween();
|
||||
tween.TweenProperty(this, "modulate:a", 0.0f, 0.3f);
|
||||
tween.TweenCallback(Callable.From(Hide));
|
||||
}
|
||||
|
||||
public void OnPause()
|
||||
{
|
||||
}
|
||||
|
||||
public void OnResume()
|
||||
{
|
||||
}
|
||||
|
||||
public void OnShow()
|
||||
{
|
||||
Show();
|
||||
}
|
||||
|
||||
public void OnHide()
|
||||
{
|
||||
Hide();
|
||||
}
|
||||
|
||||
// ... 其他方法
|
||||
}
|
||||
```
|
||||
|
||||
### UI 句柄管理
|
||||
当前生成器补出的核心样板与源码一致:
|
||||
|
||||
```csharp
|
||||
public partial class DialogManager : Node
|
||||
public IUiPageBehavior GetPage()
|
||||
{
|
||||
private UiHandle? _currentDialog;
|
||||
|
||||
public void ShowDialog(string dialogKey)
|
||||
{
|
||||
// 关闭当前对话框
|
||||
CloseCurrentDialog();
|
||||
|
||||
// 显示新对话框
|
||||
var uiRouter = this.GetSystem<IUiRouter>();
|
||||
_currentDialog = uiRouter.Show(dialogKey, UiLayer.Modal);
|
||||
}
|
||||
|
||||
public void CloseCurrentDialog()
|
||||
{
|
||||
if (_currentDialog.HasValue)
|
||||
{
|
||||
var uiRouter = this.GetSystem<IUiRouter>();
|
||||
uiRouter.Hide(_currentDialog.Value, UiLayer.Modal, destroy: true);
|
||||
_currentDialog = null;
|
||||
}
|
||||
}
|
||||
return __autoUiPageBehavior_Generated ??=
|
||||
UiPageBehaviorFactory.Create(this, UiKeyStr, UiLayer.Page);
|
||||
}
|
||||
```
|
||||
|
||||
### 多个 Toast 显示
|
||||
要注意两点:
|
||||
|
||||
- `[AutoUiPage]` 不会替你自动补 `: IUiPageBehaviorProvider`
|
||||
- UI 层级是生成器输入的一部分;`Page` / `Modal` / `Overlay` 语义不是后面再猜出来的
|
||||
|
||||
### 5. 按 layer 选择正确入口
|
||||
|
||||
Godot runtime 只是落地 `UiRouterBase` 的语义,因此入口仍然和 `GFramework.Game` 一致:
|
||||
|
||||
页面栈:
|
||||
|
||||
```csharp
|
||||
public partial class ToastManager : Node
|
||||
{
|
||||
private readonly List<UiHandle> _activeToasts = new();
|
||||
|
||||
public void ShowToast(string message, float duration = 3.0f)
|
||||
{
|
||||
var uiRouter = this.GetSystem<IUiRouter>();
|
||||
|
||||
// Toast 层支持重入,可以同时显示多个
|
||||
var handle = uiRouter.Show("Toast", UiLayer.Toast, new ToastParam
|
||||
{
|
||||
Message = message
|
||||
});
|
||||
|
||||
_activeToasts.Add(handle);
|
||||
|
||||
// 自动隐藏
|
||||
GetTree().CreateTimer(duration).Timeout += () =>
|
||||
{
|
||||
uiRouter.Hide(handle, UiLayer.Toast, destroy: true);
|
||||
_activeToasts.Remove(handle);
|
||||
};
|
||||
}
|
||||
|
||||
public void ClearAllToasts()
|
||||
{
|
||||
var uiRouter = this.GetSystem<IUiRouter>();
|
||||
|
||||
foreach (var handle in _activeToasts)
|
||||
{
|
||||
uiRouter.Hide(handle, UiLayer.Toast, destroy: true);
|
||||
}
|
||||
|
||||
_activeToasts.Clear();
|
||||
}
|
||||
}
|
||||
await uiRouter.ReplaceAsync(nameof(UiKey.MainMenu));
|
||||
await uiRouter.PushAsync(nameof(UiKey.Settings));
|
||||
await uiRouter.PopAsync();
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **UI 脚本实现 IUiPage 接口**:获得完整的生命周期管理
|
||||
```csharp
|
||||
✓ public partial class MyPage : Control, IUiPage { }
|
||||
✗ public partial class MyPage : Control { } // 无生命周期管理
|
||||
```
|
||||
|
||||
2. **使用正确的 UI 层级**:根据 UI 类型选择合适的层级
|
||||
```csharp
|
||||
✓ Page: 主要页面(主菜单、设置)
|
||||
✓ Overlay: 浮层(信息面板)
|
||||
✓ Modal: 模态对话框(确认框)
|
||||
✓ Toast: 提示消息
|
||||
✓ Topmost: 系统级(加载界面)
|
||||
```
|
||||
|
||||
3. **在 OnEnter 中显示 UI**:确保 UI 正确显示
|
||||
```csharp
|
||||
public void OnEnter(IUiPageEnterParam? param)
|
||||
{
|
||||
Show(); // 显示 UI
|
||||
// 初始化 UI 状态
|
||||
}
|
||||
```
|
||||
|
||||
4. **在 OnExit 中隐藏 UI**:确保 UI 正确隐藏
|
||||
```csharp
|
||||
public void OnExit()
|
||||
{
|
||||
Hide(); // 隐藏 UI
|
||||
// 清理 UI 状态
|
||||
}
|
||||
```
|
||||
|
||||
5. **使用 UI 句柄管理非栈 UI**:对于 Modal、Toast 等层级
|
||||
```csharp
|
||||
var handle = uiRouter.Show("Dialog", UiLayer.Modal);
|
||||
// 保存句柄以便后续关闭
|
||||
uiRouter.Hide(handle, UiLayer.Modal, destroy: true);
|
||||
```
|
||||
|
||||
6. **使用 UI 参数传递数据**:避免使用全局变量
|
||||
```csharp
|
||||
✓ uiRouter.Show("Dialog", UiLayer.Modal, new DialogParam { ... });
|
||||
✗ GlobalData.DialogMessage = "..."; // 避免全局状态
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 问题:如何在 Godot UI 中使用 GFramework?
|
||||
|
||||
**解答**:
|
||||
UI 脚本实现 `IUiPage` 和 `IUiPageBehaviorProvider` 接口:
|
||||
层级 UI:
|
||||
|
||||
```csharp
|
||||
public partial class MyPage : Control, IUiPage, IUiPageBehaviorProvider
|
||||
{
|
||||
public void OnEnter(IUiPageEnterParam? param) { }
|
||||
public IUiPageBehavior GetPage() { return new PageLayerUiPageBehavior(this, "MyPage"); }
|
||||
}
|
||||
var handle = uiRouter.Show(nameof(UiKey.PauseMenu), UiLayer.Modal);
|
||||
uiRouter.Hide(handle, UiLayer.Modal);
|
||||
```
|
||||
|
||||
### 问题:UI 层级有什么区别?
|
||||
当前实现里,`Show(..., UiLayer.Page)` 会直接抛异常;`Page` 层必须走 `PushAsync` / `ReplaceAsync`。
|
||||
|
||||
**解答**:
|
||||
## 输入与暂停语义
|
||||
|
||||
- **Page**:栈管理,不可重入,用于主要页面
|
||||
- **Overlay**:可重入,用于浮层
|
||||
- **Modal**:可重入,带遮罩,用于对话框
|
||||
- **Toast**:可重入,轻量提示
|
||||
- **Topmost**:不可重入,最高优先级
|
||||
如果页面只实现 `IUiPage`,它只有基础生命周期。
|
||||
|
||||
### 问题:如何实现 UI 动画?
|
||||
如果还需要更强的输入仲裁或暂停语义,可以像 CoreGrid 的 `PauseMenu` 一样继续实现:
|
||||
|
||||
**解答**:
|
||||
在生命周期方法中使用 Godot Tween:
|
||||
- `IUiInteractionProfileProvider`
|
||||
- `IUiActionHandler`
|
||||
|
||||
```csharp
|
||||
public void OnEnter(IUiPageEnterParam? param)
|
||||
{
|
||||
var tween = CreateTween();
|
||||
tween.TweenProperty(this, "modulate:a", 1.0f, 0.3f);
|
||||
}
|
||||
```
|
||||
当前这条链路是成立的:
|
||||
|
||||
### 问题:如何在 UI 中访问架构组件?
|
||||
1. 页面行为从 owner 读取 `UiInteractionProfile`
|
||||
2. router 根据 profile 判断动作捕获、世界输入阻断和暂停策略
|
||||
3. 如果页面实现了 `IUiActionHandler`,`TryHandleUiAction(...)` 会继续下沉到页面
|
||||
|
||||
**解答**:
|
||||
使用扩展方法:
|
||||
这也是为什么 `PauseMenu` 一类 modal 页面可以声明:
|
||||
|
||||
```csharp
|
||||
public partial class MyPage : Control, IUiPage
|
||||
{
|
||||
public void OnEnter(IUiPageEnterParam? param)
|
||||
{
|
||||
var playerModel = this.GetModel<PlayerModel>();
|
||||
var gameSystem = this.GetSystem<GameSystem>();
|
||||
}
|
||||
}
|
||||
```
|
||||
- 捕获 `Cancel`
|
||||
- 阻断 World pointer / action input
|
||||
- 在可见时持有暂停
|
||||
- 即使在暂停状态也继续处理节点逻辑
|
||||
|
||||
### 问题:如何关闭 Modal 或 Toast?
|
||||
## 当前边界
|
||||
|
||||
**解答**:
|
||||
使用 UI 句柄:
|
||||
### 没有 `GodotUiRouter`
|
||||
|
||||
```csharp
|
||||
// 显示时保存句柄
|
||||
var handle = uiRouter.Show("Dialog", UiLayer.Modal);
|
||||
仓库当前没有这个类型。旧文档把它写成默认入口是不准确的;真实入口仍然是项目侧的 `UiRouterBase` 派生类。
|
||||
|
||||
// 关闭时使用句柄
|
||||
uiRouter.Hide(handle, UiLayer.Modal, destroy: true);
|
||||
```
|
||||
### UI 工厂不会自动补 behavior
|
||||
|
||||
### 问题:UI 生命周期方法的调用顺序是什么?
|
||||
和 `GodotSceneFactory` 不同,`GodotUiFactory` 当前不会按节点类型自动创建 behavior。节点不实现
|
||||
`IUiPageBehaviorProvider` 时会直接失败。
|
||||
|
||||
**解答**:
|
||||
### `Page` 层不是 `Show(...)` 的适用对象
|
||||
|
||||
- 进入:`OnEnter` -> `OnShow`
|
||||
- 暂停:`OnPause` -> `OnHide`
|
||||
- 恢复:`OnShow` -> `OnResume`
|
||||
- 退出:`OnHide` -> `OnExit`
|
||||
`UiLayer.Page` 代表页面栈语义,而不是普通 layer UI。当前实现明确要求:
|
||||
|
||||
## 相关文档
|
||||
- `Page` 用 `PushAsync` / `ReplaceAsync`
|
||||
- `Overlay` / `Modal` / `Toast` / `Topmost` 用 `Show` / `Hide`
|
||||
|
||||
- [UI 系统](/zh-CN/game/ui) - 核心 UI 管理
|
||||
- [Godot 架构集成](/zh-CN/godot/architecture) - Godot 架构基础
|
||||
- [Godot 场景系统](/zh-CN/godot/scene) - Godot 场景集成
|
||||
- [Godot 扩展](/zh-CN/godot/extensions) - Godot 扩展方法
|
||||
### root 仍然由项目控制
|
||||
|
||||
`IUiRoot` 决定:
|
||||
|
||||
- 每个 layer 是否拆独立容器
|
||||
- 层内排序怎么算
|
||||
- 页面移除时如何释放节点
|
||||
|
||||
Godot runtime 不会替项目自动生成统一 UI 根节点。
|
||||
|
||||
## 继续阅读
|
||||
|
||||
1. [Godot 运行时集成](./index.md)
|
||||
2. [Game UI 系统](../game/ui.md)
|
||||
3. [AutoUiPage 生成器](../source-generators/auto-ui-page-generator.md)
|
||||
4. [Godot 架构集成](./architecture.md)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user