GeWuYou da707c7b4f docs(game): 收口场景与UI专题文档
- 重写 game/scene 与 game/ui 专题页,按当前 router、factory、root、输入与暂停语义说明接入方式\n- 更新 documentation-governance-and-refresh 的 tracking 与 trace,记录 RP-006 与后续 source-generators 核对重点\n- 验证 docs 站点构建通过
2026-04-21 16:08:05 +08:00

294 lines
7.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
title: UI 系统
description: 说明 GFramework.Game UI 路由当前的页面栈、层级 UI、输入语义与项目侧接入方式。
---
# UI 系统
`GFramework.Game` 的 UI 系统不是单纯的“页面栈”。按当前实现,它同时覆盖:
- `UiLayer.Page` 的页面导航
- `Overlay` / `Modal` / `Toast` / `Topmost` 的层级 UI
- UI 语义动作捕获与分发
- World 输入阻断
- 由 UI 可见性驱动的暂停语义
因此,新的接入文档不应再把它写成“只有 Push/Pop 的传统页面管理器”。
## 当前公开入口
### `IUiPage`
最轻量的页面生命周期契约,暴露:
- `OnEnter`
- `OnExit`
- `OnPause`
- `OnResume`
- `OnShow`
- `OnHide`
如果你的页面逻辑只想表达这些生命周期阶段,停留在 `IUiPage` 就够了。
### `IUiPageBehavior`
路由器真正操作的运行时页面行为。相比 `IUiPage`,它还携带:
- `Key`
- `Layer`
- `Handle`
- `View`
- `IsAlive`
- `IsVisible`
- `IsModal`
- `BlocksInput`
- `InteractionProfile`
- `TryHandleUiAction(UiInputAction action)`
也就是说,页面栈和层级 UI 都是围绕 `IUiPageBehavior` 工作的,而不是只围绕 `IUiPage`
### `IUiRouter`
当前最常用的入口分成两组。
页面栈:
- `PushAsync(...)`
- `ReplaceAsync(...)`
- `PopAsync(...)`
- `ClearAsync()`
- `Peek()`
- `PeekKey()`
层级 UI
- `Show(...)`
- `Hide(...)`
- `Resume(...)`
- `ClearLayer(...)`
- `HideByKey(...)`
- `GetAllFromLayer(...)`
输入与阻断:
- `GetUiActionOwner(UiInputAction action)`
- `TryDispatchUiAction(UiInputAction action)`
- `BlocksWorldPointerInput()`
- `BlocksWorldActionInput()`
### `UiLayer`
当前层级语义如下:
- `Page`
- 页面栈层。请用 `PushAsync` / `ReplaceAsync`,不要用 `Show(...)`
- `Overlay`
- 可叠加的浮层。
- `Modal`
- 默认阻断下层输入的模态层。
- `Toast`
- 轻量提示层。
- `Topmost`
- 最顶层的系统级 UI。
### `UiTransitionPolicy` 与 `UiPopPolicy`
页面栈的两个关键策略:
- `UiTransitionPolicy.Exclusive`
- 新页面独占显示,下层页面会 `Pause + Hide`
- `UiTransitionPolicy.Overlay`
- 新页面覆盖显示,下层页面只 `Pause`
- `UiPopPolicy.Destroy`
- 弹出时直接销毁页面实例
- `UiPopPolicy.Suspend`
- 弹出时保留页面实例,供后续恢复
## UI 路由的真实语义
### 页面栈和层级 UI 是两套入口
当前源码里:
- `Page` 层属于栈语义,用 `PushAsync` / `ReplaceAsync` / `PopAsync`
- `Overlay``Modal``Toast``Topmost` 属于层级语义,用 `Show` / `Hide` / `Resume`
`Show(..., UiLayer.Page)` 在当前实现里会直接抛异常,因此旧文档里那种“所有 UI 都统一通过 Show 进入”的写法不再准确。
### 输入不是页面自己抢,而是 router 先仲裁
`UiInteractionProfile` 用来描述页面的交互契约,例如:
- 捕获哪些 `UiInputAction`
- 是否阻断 World 指针输入
- 是否阻断 World 语义动作输入
- 页面可见时是否推动暂停栈
输入层先把设备输入映射成 `UiInputAction`,再交给 `IUiRouter.TryDispatchUiAction(...)`。最终谁拥有动作捕获权,由当前可见页面和层级顺序决定。
### 页面可见性会影响暂停与阻断
这也是 UI 系统和普通页面栈最不同的地方之一。当前实现里:
- `Modal` / `Topmost` 默认具有更强的输入阻断语义
- 页面的 `InteractionProfile` 可以驱动暂停栈
- `BlocksWorldPointerInput()``BlocksWorldActionInput()` 是给项目输入层做统一判断的
如果你的项目有“打开设置页后暂停世界”“Modal 打开时地图点击失效”这类需求,优先接这个契约,而不是每个页面自己散落地写输入屏蔽逻辑。
## 最小接入路径
### 1. 提供项目自己的 router
```csharp
using GFramework.Game.UI;
using LoggingTransitionHandler = GFramework.Game.UI.Handler.LoggingTransitionHandler;
public sealed class GameUiRouter : UiRouterBase
{
protected override void RegisterHandlers()
{
RegisterHandler(new LoggingTransitionHandler());
}
}
```
### 2. 提供 `IUiFactory`
`UiRouterBase` 会通过 `IUiFactory.Create(string uiKey)` 获取页面行为实例,因此项目需要自己决定:
- `uiKey` 如何映射到页面行为
- 页面行为如何包裹具体引擎视图
- 预挂载节点、调试节点或动态实例化页面如何接入
如果你在 Godot 项目里使用 `AutoUiPage` 相关生成器,它可以帮你减少部分行为样板,但 factory / root / 实际页面注册仍然是项目职责。
### 3. 提供 `IUiRoot`
`IUiRoot` 负责把页面行为挂进真实 UI 容器:
- `AddUiPage(IUiPageBehavior child)`
- `AddUiPage(IUiPageBehavior child, UiLayer layer, int orderInLayer = 0)`
- `RemoveUiPage(IUiPageBehavior child)`
当前 `ai-libs/` 的参考实现就是在项目自己的 `CanvasLayer` 上为每个 `UiLayer` 建独立容器,再在 `_Ready()` 时执行
`_uiRouter.BindRoot(this)`
### 4. 装配 router 与 factory
```csharp
architecture.RegisterUtility<IUiFactory>(new GameUiFactory());
architecture.RegisterSystem(new GameUiRouter());
```
### 5. 在 root 就绪后绑定
```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)
{
// 项目侧决定如何把 child.View 挂到具体容器
}
public void RemoveUiPage(IUiPageBehavior child)
{
// 项目侧决定如何移除并释放视图
}
}
```
### 6. 从业务代码区分两类入口
页面栈:
```csharp
await uiRouter.ReplaceAsync("MainMenu");
await uiRouter.PushAsync("Settings", new SettingsEnterParam());
await uiRouter.PopAsync(UiPopPolicy.Destroy);
```
层级 UI
```csharp
var modalHandle = uiRouter.Show(
"ConfirmExit",
UiLayer.Modal,
new ConfirmExitParam());
uiRouter.Hide(modalHandle, UiLayer.Modal);
```
## 扩展点
### 路由守卫
如果你要在进入或离开页面前做业务检查,实现 `IUiRouteGuard`
- `CanEnterAsync(string uiKey, IUiPageEnterParam? param)`
- `CanLeaveAsync(string uiKey)`
适合放:
- 未保存设置拦截
- 新手引导期间禁用某些页面跳转
- 多层弹窗切换前的业务确认
### 过渡处理器
`IUiRouter` 当前公开的是:
- `RegisterHandler(IUiTransitionHandler handler, UiTransitionHandlerOptions? options = null)`
- `UnregisterHandler(IUiTransitionHandler handler)`
适合放:
- UI 转场动画
- 统一日志
- 栈变化埋点
### 输入适配层
如果项目已经有自己的输入系统,推荐把它适配成:
1. 设备输入 -> `UiInputAction`
2. `IUiRouter.TryDispatchUiAction(...)`
3. 若未被 UI 捕获,再决定是否把输入继续交给 World
这样可以直接复用当前路由器的动作捕获与阻断语义。
## 与旧写法的边界
以下说法不再适合作为默认指导:
- “所有 UI 都统一通过一个 Show API 管理”
- “UI 系统只有页面栈,不涉及输入阻断和暂停语义”
- “Modal / Topmost 只是视觉层级,不影响交互”
当前更准确的理解是:
- 页面栈和层级 UI 是两套入口
- 页面行为不仅有生命周期,还有输入、阻断、暂停契约
- router 是 UI 语义仲裁中心,项目输入层应主动接入它
## 推荐阅读
1. [game/index.md](./index.md)
2. [scene.md](./scene.md)
3. [../source-generators/auto-ui-page-generator.md](../source-generators/auto-ui-page-generator.md)
4. `GFramework.Game/README.md`
5. `GFramework.Game.Abstractions/README.md`