mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-03-22 10:34:30 +08:00
- 将所有小写的命名空间导入更正为首字母大写格式 - 统一 GFramework 框架的命名空间引用规范 - 修复 core、ecs、godot 等模块的命名空间导入错误 - 标准化文档示例代码中的 using 语句格式 - 确保所有文档中的命名空间引用保持一致性 - 更新 global using 语句以匹配正确的命名空间格式
510 lines
13 KiB
Markdown
510 lines
13 KiB
Markdown
---
|
||
title: UI 系统
|
||
description: UI 系统提供了完整的 UI 页面管理、路由导航和多层级显示功能。
|
||
---
|
||
|
||
# UI 系统
|
||
|
||
## 概述
|
||
|
||
UI 系统是 GFramework.Game 中用于管理游戏 UI 界面的核心组件。它提供了 UI 页面的生命周期管理、基于栈的导航机制,以及多层级的
|
||
UI 显示系统(Page、Overlay、Modal、Toast、Topmost)。
|
||
|
||
通过 UI 系统,你可以轻松实现 UI 页面之间的切换,管理 UI 栈(如主菜单 -> 设置 -> 关于),以及在不同层级显示各种类型的
|
||
UI(对话框、提示、加载界面等)。
|
||
|
||
**主要特性**:
|
||
|
||
- 完整的 UI 生命周期管理
|
||
- 基于栈的 UI 导航
|
||
- 多层级 UI 显示(5 个层级)
|
||
- UI 转换管道和钩子
|
||
- 路由守卫(Route Guard)
|
||
- UI 工厂和行为模式
|
||
|
||
## 核心概念
|
||
|
||
### UI 页面接口
|
||
|
||
`IUiPage` 定义了 UI 页面的生命周期:
|
||
|
||
```csharp
|
||
public interface IUiPage
|
||
{
|
||
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)
|
||
{
|
||
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 元素
|
||
}
|
||
}
|
||
```
|
||
|
||
### 切换 UI 页面
|
||
|
||
使用 UI 路由进行导航:
|
||
|
||
```csharp
|
||
using GFramework.Core.Abstractions.Controller;
|
||
using GFramework.SourceGenerators.Abstractions.Rule;
|
||
|
||
[ContextAware]
|
||
public partial class UiController : IController
|
||
{
|
||
public async Task ShowSettings()
|
||
{
|
||
var uiRouter = this.GetSystem<IUiRouter>();
|
||
|
||
// 压入设置页面(保留当前页面)
|
||
await uiRouter.PushAsync("Settings");
|
||
}
|
||
|
||
public async Task CloseSettings()
|
||
{
|
||
var uiRouter = this.GetSystem<IUiRouter>();
|
||
|
||
// 弹出当前页面(返回上一页)
|
||
await uiRouter.PopAsync();
|
||
}
|
||
|
||
public async Task ShowMainMenu()
|
||
{
|
||
var uiRouter = this.GetSystem<IUiRouter>();
|
||
|
||
// 替换所有页面(清空 UI 栈)
|
||
await uiRouter.ReplaceAsync("MainMenu");
|
||
}
|
||
}
|
||
```
|
||
|
||
### 显示不同层级的 UI
|
||
|
||
```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);
|
||
}
|
||
}
|
||
```
|
||
|
||
## 高级用法
|
||
|
||
### UI 参数传递
|
||
|
||
```csharp
|
||
// 定义 UI 参数
|
||
public class SettingsEnterParam : IUiPageEnterParam
|
||
{
|
||
public string Category { get; set; }
|
||
}
|
||
|
||
// 在 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"
|
||
});
|
||
```
|
||
|
||
### 路由守卫
|
||
|
||
```csharp
|
||
using GFramework.Game.Abstractions.UI;
|
||
|
||
public class UnsavedChangesGuard : IUiRouteGuard
|
||
{
|
||
public async ValueTask<bool> CanLeaveAsync(
|
||
IUiPageBehavior from,
|
||
string toKey,
|
||
IUiPageEnterParam? param)
|
||
{
|
||
// 检查是否有未保存的更改
|
||
if (from.Key == "Settings" && HasUnsavedChanges())
|
||
{
|
||
var confirmed = await ShowConfirmDialog();
|
||
return confirmed;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
public async ValueTask<bool> CanEnterAsync(
|
||
string toKey,
|
||
IUiPageEnterParam? param)
|
||
{
|
||
// 进入前的验证
|
||
return true;
|
||
}
|
||
|
||
private bool HasUnsavedChanges() => true;
|
||
private async Task<bool> ShowConfirmDialog() => await Task.FromResult(true);
|
||
}
|
||
|
||
// 注册守卫
|
||
uiRouter.AddGuard(new UnsavedChangesGuard());
|
||
```
|
||
|
||
### UI 转换处理器
|
||
|
||
```csharp
|
||
using GFramework.Game.Abstractions.UI;
|
||
|
||
public class FadeTransitionHandler : IUiTransitionHandler
|
||
{
|
||
public async ValueTask OnBeforeEnterAsync(UiTransitionEvent @event)
|
||
{
|
||
Console.WriteLine($"准备进入 UI: {@event.ToKey}");
|
||
await PlayFadeIn();
|
||
}
|
||
|
||
public async ValueTask OnAfterEnterAsync(UiTransitionEvent @event)
|
||
{
|
||
Console.WriteLine($"已进入 UI: {@event.ToKey}");
|
||
}
|
||
|
||
public async ValueTask OnBeforeExitAsync(UiTransitionEvent @event)
|
||
{
|
||
Console.WriteLine($"准备退出 UI: {@event.FromKey}");
|
||
await PlayFadeOut();
|
||
}
|
||
|
||
public async ValueTask OnAfterExitAsync(UiTransitionEvent @event)
|
||
{
|
||
Console.WriteLine($"已退出 UI: {@event.FromKey}");
|
||
}
|
||
|
||
private async Task PlayFadeIn() => await Task.Delay(200);
|
||
private async Task PlayFadeOut() => await Task.Delay(200);
|
||
}
|
||
|
||
// 注册转换处理器
|
||
uiRouter.RegisterHandler(new FadeTransitionHandler());
|
||
```
|
||
|
||
### UI 句柄管理
|
||
|
||
```csharp
|
||
using GFramework.Core.Abstractions.Controller;
|
||
using GFramework.SourceGenerators.Abstractions.Rule;
|
||
|
||
[ContextAware]
|
||
public partial class DialogController : IController
|
||
{
|
||
private UiHandle? _dialogHandle;
|
||
|
||
public void ShowDialog()
|
||
{
|
||
var uiRouter = this.GetSystem<IUiRouter>();
|
||
|
||
// 显示对话框并保存句柄
|
||
_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.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.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 状态管理
|