平面 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();