平面 UI 的使用
本章节介绍如何在 非 VR 项目 中使用 Fink Framework 的平面 UI 系统。
平面 UI 由 UIManager 统一管理,支持自动代码生成、自动控件绑定以及基于 UniTask 的异步加载流程。
1. 核心概念
框架启动时,UIManager 会自动创建:
- UICamera:独立的 UI 摄像机(URP 环境下会自动加入 Camera Stack)。
- MainCanvas:主画布,根据环境设置自动适配(ScreenSpace / WorldSpace)。
- EventSystem:根据输入系统(旧版/New Input System/XR)自动创建对应的事件系统。
UI 层级 (MainLayer)
MainCanvas 下默认包含四个挂载点,用于控制面板的遮挡关系:
- Bottom:最底层(背景图、大地图)
- Middle:默认层(常规功能面板)
- Top:上层(弹窗、Toast)
- System:系统层(网络加载圈、强制下线提示)
2. 显示面板
所有面板必须放置在 Resources 目录下(默认路径 UI/Panels/),或通过自定义路径加载。
2.1 异步显示(推荐使用 UniTask)
这是最常用的方式,支持 await 等待面板加载完成。
// 默认加载路径:Resources/UI/Panels/BagPanel
await UIManager.Instance.ShowPanelAsync<BagPanel>();
// 指定层级
await UIManager.Instance.ShowPanelAsync<BagPanel>(layer: E_MainLayer.Top);
// 获取面板实例进行操作
var panel = await UIManager.Instance.ShowPanelAsync<BagPanel>();
panel.UpdateData();
2.2 回调形式显示
如果不使用 async/await,可以使用回调:
UIManager.Instance.ShowPanelCallback<BagPanel>(
callback: (panel) => {
panel.InitView();
},
layer: E_MainLayer.Middle
);
2.3 同步显示
注意:仅当确认资源已加载(或在 Editor 模式下调试)时使用,否则会因资源未准备好而报错或卡顿。
// 如果预制体很大,不要使用此方法
var panel = UIManager.Instance.ShowPanel<BagPanel>();
2.4 自定义加载路径
若面板不在默认的 UI/Panels/ 下,需传入 fullPath(支持 res://, ab:// 等前缀):
await UIManager.Instance.ShowPanelAsync<BagPanel>(
fullPath: "res://UI/Special/MySpecialPanel"
);
3. 面板生命周期
BasePanel 提供了完善的生命周期钩子:
| 方法 | 说明 | 调用时机 | 用途 |
|---|---|---|---|
| Awake | 初始化 | 预制体实例化时 | 获取控件引用(自动生成) |
| ShowMe | 首次显示 | 仅面板第一次创建并显示时调用 | 数据初始化、一次性事件绑定 |
| OnShow | 每次显示 | 面板创建时、以及从隐藏状态变为显示时调用 | 刷新 UI、播放入场动画 |
| HideMe | 隐藏逻辑 | 调用 HidePanel 时调用 | 播放退场动画 |
| OnHide | 每次隐藏 | 调用 HidePanel 时调用 | 暂停逻辑、停止音频 |
| OnDestroyPanel | 销毁前 | 面板被 Destroy 前调用 | 移除全局事件监听、防止内存泄漏 |
开发建议:
- 在
ShowMe中做一次性的初始化。 - 在
OnShow中做 UI 刷新(因为面板可能只是被SetActive(true)而非重新创建)。
4. 隐藏与关闭
4.1 隐藏(保留实例)
面板会被 SetActive(false),保留内存,下次打开无需加载。
UIManager.Instance.HidePanel<BagPanel>();
4.2 隐藏并销毁
销毁面板 GameObject,释放内存。
UIManager.Instance.HidePanel<BagPanel>(isDestroy: true);
4.3 隐藏特定层级
// 隐藏 Top 层所有面板
UIManager.Instance.HidePanelsInLayer(E_MainLayer.Top, isDestroy: false);
5. 控件交互与自动绑定
BasePanel 会遍历所有子节点,将 UI 控件存入字典。
注意:为了性能和规范,Fink Framework 有一份默认忽略列表。如果你的控件名叫 Image, Text (TMP), Background, Label 等,框架会认为它们是纯装饰控件,不会自动绑定事件。请给交互控件起一个独特的名字(如 BtnClose, InputName)。
5.1 获取控件引用
// 获取名为 "UserIcon" 的 Image 组件
Image icon = GetControl<Image>("UserIcon");
5.2 自动事件响应(重写基类方法)
你无需手动 AddListener,只需重写 BasePanel 提供的虚方法,根据控件名判断逻辑。
按钮点击 (Button)
protected override void ClickBtn(string btnName)
{
switch (btnName)
{
case "BtnLogin":
Login();
break;
case "BtnClose":
UIManager.Instance.HidePanel<LoginPanel>();
break;
}
}
输入框改变 (InputField / TMP_InputField)
protected override void InputValueChange(string inputName, string value)
{
if (inputName == "InputAccount")
{
Debug.Log("当前输入:" + value);
}
}
滑条改变 (Slider)
protected override void SliderValueChange(string sliderName, float value)
{
if (sliderName == "SliderVolume")
AudioManager.Instance.SetVolume(value);
}
Toggle 状态改变
protected override void ToggleValueChange(string toggleName, bool value)
{
if (toggleName == "ToggleMute")
AudioManager.Instance.SetMute(value);
}
6. 获取已存在的面板
若需要跨脚本获取一个已打开(或已加载但隐藏)的面板:
UIManager.Instance.GetPanel<BagPanel>(panel =>
{
// 如果面板正在异步加载中,这里会等待加载完成后才回调
// 如果面板从未加载过,则回调不会执行,并打印警告
panel.RefreshMoney();
});
7. 高级功能
7.1 句柄式加载 (Handle)
用于需要轮询进度或取消加载的场景:
var op = UIManager.Instance.LoadPanelHandle<BagPanel>();
// 在 Update 中
if (!op.IsDone)
Debug.Log($"Loading: {op.Progress}");
else
op.Panel.Refresh();
7.2 自定义 EventTrigger
为图片或文字添加点击、悬停事件:
UIManager.Instance.AddCustomEventListener(
GetControl<Image>("HeroAvatar"),
EventTriggerType.PointerEnter,
(data) => Debug.Log("鼠标悬停在头像上")
);
7.3 清空所有面板
通常在场景切换时使用,销毁所有 UI 释放内存:
UIManager.Instance.ClearAllPanels();