docs(game): 收口场景与UI专题文档

- 重写 game/scene 与 game/ui 专题页,按当前 router、factory、root、输入与暂停语义说明接入方式\n- 更新 documentation-governance-and-refresh 的 tracking 与 trace,记录 RP-006 与后续 source-generators 核对重点\n- 验证 docs 站点构建通过
This commit is contained in:
GeWuYou 2026-04-21 16:08:05 +08:00
parent 9ccfed3ad9
commit da707c7b4f
4 changed files with 425 additions and 1042 deletions

View File

@ -7,19 +7,20 @@
## 当前恢复点
- 恢复点编号:`DOCUMENTATION-GOVERNANCE-REFRESH-RP-005`
- 恢复点编号:`DOCUMENTATION-GOVERNANCE-REFRESH-RP-006`
- 当前阶段:`Phase 3`
- 当前焦点:
- 已完成 `docs/zh-CN/core/events.md``property.md``logging.md` 的专题页重写
- 已按源码与测试复核 `docs/zh-CN/core/state-management.md``coroutine.md`,当前内容与实现基本一致,无需再做
机械改写
- 下一轮需要把重心转到 `docs/zh-CN/game/*``docs/zh-CN/source-generators/*` 的专题页核对
- 已完成 `docs/zh-CN/game/scene.md``ui.md` 的专题页重写,当前内容已回到“项目自接 factory/root + router 基类”的真实边界
- 下一轮需要把重心转到 `docs/zh-CN/source-generators/*` 的专题页核对
## 当前状态摘要
- 文档治理规则已收口到仓库规范README、站点入口与采用链路不再依赖旧文档自证
- 高优先级模块入口与 `core` 关键专题页已回到可作为默认导航入口的状态,本轮计划中的 `core` 剩余高风险页面已完成收口
- 当前主题仍是 active topic因为 `game` 与 `source-generators` 栏目下仍可能包含与实现漂移的旧内容
- 当前主题仍是 active topic因为 `source-generators` 栏目下仍可能包含与实现漂移的旧内容
## 当前活跃事实
@ -39,11 +40,17 @@
- `docs/zh-CN/core/property.md` 已明确记录 `BindableProperty<T>.Comparer` 的闭合泛型级共享语义,避免文档继续误导读者把
`WithComparer(...)` 当成实例级配置
- `docs/zh-CN/core/state-management.md``coroutine.md` 已按当前 runtime / 测试重新核对,当前内容可继续保留
- `docs/zh-CN/game/scene.md` 已改成“真实公开入口、场景栈语义、factory/root 装配、过渡处理器与守卫扩展点”的结构,
不再暗示框架自带统一场景注册与完整引擎装配
- `docs/zh-CN/game/ui.md` 已改成“Page 栈、layer UI、输入动作仲裁、World 阻断与暂停语义”的结构,明确 `Show(...)`
不适用于 `UiLayer.Page`
- 本轮重写后再次执行 `cd docs && bun run build` 通过,当前 `game` 栏目入口与专题页改动没有破坏站点构建
## 当前风险
- 旧专题页示例失真风险:`docs/zh-CN/game/*``source-generators/*` 中仍可能保留看似合理但与真实实现不一致的示例
- 缓解措施:继续按源码、测试、`*.csproj``ai-libs/` 下已验证参考实现核对,不把旧文档当事实来源
- 缓解措施:`game/scene.md``ui.md` 已完成收口;继续按源码、测试、`*.csproj``ai-libs/` 下已验证参考实现核对
`source-generators/*`,不把旧文档当事实来源
- 采用路径误导风险:根聚合包与模块边界若再次被写错,会继续误导消费者的包选择
- 缓解措施:保持“源码与包关系优先”的证据顺序,改动采用说明时同步核对包依赖与生成器 wiring
- Active 入口回膨胀风险:后续若把栏目级重写过程直接追加到 active 文档,会再次拖慢恢复
@ -66,6 +73,7 @@
## 下一步
1. 继续核对 `docs/zh-CN/game/*`,优先处理仍引用旧安装方式、旧状态系统或旧 UI / Scene 接法的页面
2. 再推进 `docs/zh-CN/source-generators/*`,重点核对生成器 wiring、包关系与最小接入示例
1. 继续核对 `docs/zh-CN/source-generators/*`,优先处理仍引用旧初始化方式、旧聚合包名或过时生成器 wiring 的页面
2. 重点复核 `priority-generator.md``context-aware-generator.md` 与 Godot 相关生成器页面,确认示例仍与当前 runtime /
generator 入口一致
3. 若 active trace 再积累新的已完成阶段,按恢复点粒度迁入 `archive/traces/`,避免默认启动入口再次膨胀

View File

@ -127,3 +127,28 @@
1. 继续核对 `docs/zh-CN/game/*`,优先处理仍引用旧安装方式、旧状态系统或旧 UI / Scene 接法的页面
2. 再推进 `docs/zh-CN/source-generators/*`,重点核对生成器 wiring、包关系与最小接入示例
3. 若 active trace 继续累计多个已完成恢复点,按 `archive/traces/` 粒度归档旧阶段细节
### 阶段Game Scene / UI 专题页收口RP-006
- 依据 `documentation-governance-and-refresh` active tracking 的下一步,优先复核 `docs/zh-CN/game/scene.md`
`docs/zh-CN/game/ui.md`
- 对照 `GFramework.Game.Abstractions/Scene/*``GFramework.Game.Abstractions/UI/*``GFramework.Game/Scene/SceneRouterBase.cs`
`GFramework.Game/UI/UiRouterBase.cs``GFramework.Game/README.md``ai-libs/CoreGrid` 参考接法后确认:
- `scene.md` 仍把场景系统写成框架自带完整注册/装配的一体化方案,没有突出 `ISceneFactory``ISceneRoot` 和项目侧
router 派生类的责任边界
- `ui.md` 仍按旧教程式结构展开,没有清楚区分 `Page` 栈与 `Overlay/Modal/Toast/Topmost` 层级 UI也缺少当前
`UiInteractionProfile``TryDispatchUiAction(...)` 与 World 输入阻断语义
- 重写 `scene.md`,使其回到“当前公开入口、场景栈语义、最小接入路径、守卫/过渡处理器扩展点、与旧写法的边界”的结构
- 重写 `ui.md`,使其回到“页面栈与层级 UI 分流、输入仲裁、暂停/阻断语义、最小接入路径、扩展点”的结构
- 新版两页都明确了factory、root、引擎节点与注册表仍由项目或适配层提供框架当前提供的是 router 基类与通用编排
### 验证RP-006
- `cd docs && bun run build`
### 下一步RP-006
1. 继续核对 `docs/zh-CN/source-generators/*`,优先处理仍引用旧初始化方式、旧聚合包名或过时 generator wiring 的页面
2. 重点复核 `priority-generator.md``context-aware-generator.md` 与 Godot 相关生成器页面,确认示例仍与当前 runtime /
generator 入口一致
3. 若 `source-generators` 出现多页连续收口结果,再按恢复点粒度整理 active trace避免默认入口继续膨胀

View File

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

View File

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