14 KiB
14 KiB
UI
模块概述
UI 模块负责窗口创建、资源定位、层级管理、显示/关闭、缓存、过渡动画与界面绑定代码协作。
本模块里最容易误解的一点是:
UIHolderObjectBase(通常简称 UIHolder)不是推荐手写的业务类- 项目中的大多数
UIHolder都应通过 UI 绑定工具自动生成 - 业务代码通常只编写
UIWindow<T>、UIWidget<T>、UITabWindow<T>这类逻辑类
换句话说:
- UIHolder 负责“控件引用和预制体桥接”
- UI 逻辑类负责“界面行为和生命周期”
快速开始
- 在场景中挂载
UIComponent - 为
UIComponent.uiRoot指定 UI 根预制体 - 在
UISettingEditorWindow中配置 UI 生成规则 - 选中 UI 预制体根节点,执行
GameObject/UI生成绑定 - 让生成器自动生成
UIHolder类并挂回预制体 - 在逻辑类中使用
UIWindow<T>或UIWidget<T>,其中T就是生成出来的UIHolder类型
架构说明
UIComponent
└─ UIService
├─ UIBase
├─ UIWindow<T>
├─ UIWidget<T>
├─ UITabWindow<T>
├─ UIHolderObjectBase
├─ UIMetaRegistry
├─ UIResRegistry
└─ UIHolderFactory
角色分工
UIBase:UI 逻辑生命周期基类UIWindow<T>:顶层窗口逻辑UIWidget<T>:挂在窗口或其他 Widget 下的子部件逻辑UITabWindow<T>:支持 Tab 页懒加载与切换的窗口基类UIHolderObjectBase:预制体绑定脚本基类,负责暴露控件引用、RectTransform、转场播放器UIHolderFactory:根据注册信息加载预制体并创建对应 Holder
UIBase、UIHolderObjectBase、UIWidget<T> 的关系
UI 预制体
└─ 挂载生成的 XXXHolder : UIHolderObjectBase
↑
UIWidget<XXXHolder> / UIWindow<XXXHolder>
↑
通过泛型参数 T 访问 baseui
说明:
UIHolderObjectBase挂在预制体上,持有控件引用UIWindow<T>/UIWidget<T>的泛型参数T指向该 Holder 类型- 在逻辑类内部可以通过
baseui访问生成好的控件字段
UIHolder 的作用
UIHolderObjectBase 的职责不是写业务逻辑,而是充当:
- 控件引用容器:保存按钮、文本、图片、节点等引用
- 预制体桥接层:让 UI 逻辑层不直接依赖层级查找
- 生命周期事件承载层:暴露
OnWindowInitEvent、OnWindowAfterShowEvent等事件 - 转场入口:自动寻找并驱动
IUITransitionPlayer
因此推荐做法是:
- 业务不手写具体
XXXHolder - 由 UI 绑定工具从预制体结构自动生成
UIHolder 自动生成工作流
1. 配置生成规则
先在编辑器中配置 UIGenerateConfiguration,核心配置包括:
UIPrefabRootPath:UI 预制体根目录GenerateHolderCodePath:生成代码输出目录NameSpace:生成类所在命名空间LoadType:Resources或AssetBundle
这些配置通常通过 UISettingEditorWindow 维护。
2. 按命名规则搭建 UI 预制体
生成器会扫描 UI 节点名和组件类型。例如默认规则会识别:
Btn→ 按钮组件Text→TextMeshProUGUIImg→ImageTf→TransformRect→RectTransform
例如:
Btn#Close@Text#Title@Img#Icon@
生成器会根据这些节点名推断字段名和字段类型。
3. 选中 UI 预制体根节点
支持两种常见操作方式:
- 在 Project 中选中 prefab 资源
- 或在 Prefab Mode 中编辑当前预制体
4. 执行绑定工具
菜单入口:
GameObject/UI生成绑定
执行后生成器会:
- 读取当前 UI 生成配置
- 校验预制体路径是否位于配置的 UI 根目录
- 扫描可绑定节点
- 生成
XXXHolder代码文件 - 脚本编译后自动把生成的
XXXHolder挂到目标预制体根节点 - 自动回填对应字段引用
这意味着:
- 正常情况下不需要手动创建 Holder 脚本
- 也不需要手工把字段拖到 Inspector
5. 生成代码的结果
生成的 UIHolder 类本质上:
- 继承自
UIHolderObjectBase - 带有
UIResAttribute - 包含自动生成的控件字段
形态类似:
using AlicizaX.UI.Runtime;
[UIRes(InventoryItemHolder.ResTag, EUIResLoadType.AssetBundle)]
public class InventoryItemHolder : UIHolderObjectBase
{
public const string ResTag = "UI/Inventory/InventoryItem.prefab";
[UnityEngine.SerializeField] private UnityEngine.UI.Button uiBtnClose;
[UnityEngine.SerializeField] private TMPro.TextMeshProUGUI uiTextTitle;
public UnityEngine.UI.Button BtnClose => uiBtnClose;
public TMPro.TextMeshProUGUI TextTitle => uiTextTitle;
}
上面是示意结构;实际字段名由生成规则决定。
在 UIWidget<T> 中如何引用生成的 UIHolder
关键点:
T就是生成工具输出的 Holder 类型- 逻辑类不需要自己声明控件字段
- 通过
baseui访问自动生成的 Holder 成员
例如:
using AlicizaX.UI.Runtime;
using UnityEngine;
public sealed class InventoryItemWidget : UIWidget<InventoryItemHolder>
{
protected override void OnInitialize()
{
baseui.BtnClose.onClick.AddListener(OnClickClose);
}
protected override void OnOpen()
{
baseui.TextTitle.text = "Potion";
}
private void OnClickClose()
{
Close();
Destroy();
}
}
这里的含义是:
InventoryItemHolder由 UI 绑定工具生成InventoryItemWidget是手写业务逻辑UIWidget<InventoryItemHolder>把逻辑和绑定类关联起来
核心类与接口
IUIService
负责:
- 初始化 UI 根节点
- 打开/关闭窗口
- 查询已打开窗口
- 获取层级根节点
- 注入
ITimerService
UIBase
关键生命周期:
OnInitialize()OnOpen()OnClose()OnDestroy()OnUpdate()
以及对应异步版本:
OnInitializeAsync()OnOpenAsync()OnCloseAsync()
并提供:
CreateWidgetAsync<T>()CreateWidgetSync<T>()RemoveWidget(UIBase widget)
UIWindow<T>
适合顶层窗口,通常用于:
- 主界面
- 设置页
- 背包页
- 弹窗
常用能力:
CloseSelf()- 强制关闭
- 打开后顶层排序与层级遮挡处理
UIWidget<T>
适合子部件,通常用于:
- 列表项
- 面板块
- 详情条目
- 页签子页面
公开方法:
Open(params object[] userDatas)Close()Destroy()
UITabWindow<T>
用于页签式窗口,支持:
- 预注册 Tab
- 按需懒加载
SwitchTab(int index, params object[] userDatas)
UIHolderObjectBase
核心成员:
TargetRectTransformVisibleOnWindowInitEventOnWindowBeforeShowEventOnWindowAfterShowEventOnWindowBeforeClosedEventOnWindowAfterClosedEventOnWindowDestroyEvent
UIHolderFactory
UIHolderFactory 是 UI 资源实例化与 Holder 绑定的桥梁,作用是:
- 根据
UIResRegistry中登记的资源信息定位 UI 预制体 - 调用
IResourceService或Resources加载 UI 资源 - 实例化预制体并获取对应的
UIHolderObjectBase - 把生成的 Holder 绑定到
UIWindow<T>/UIWidget<T>对应的逻辑实例上
你通常不会在业务层频繁直接调用它,因为:
- 打开窗口时,
UIService会在内部调用UIHolderFactory - 创建 Widget 时,
UIBase.CreateWidgetAsync<T>()/CreateWidgetSync<T>()也会在内部调用它
可以把它理解为:
UI 逻辑类
-> UIService / UIBase
-> UIHolderFactory
-> 加载预制体
-> 找到生成的 XXXHolder
-> 绑定到 UIWindow<T> / UIWidget<T>
典型作用场景
-
ShowUI<InventoryWindow>()UIService找到InventoryWindow对应的元数据UIHolderFactory根据InventoryWindowHolder的UIResAttribute加载预制体- 创建并返回
InventoryWindowHolder - 把 Holder 绑定给
InventoryWindow
-
CreateWidgetAsync<InventoryItemWidget>(parent)UIBase创建InventoryItemWidget的元数据UIHolderFactory加载InventoryItemHolder对应的 Widget 预制体- 把 Holder 绑定给
InventoryItemWidget
直接调用示例
虽然业务层通常不需要直接调用,但在工具代码、调试代码或特殊预加载场景下,可以这样使用:
using AlicizaX.UI.Runtime;
using Cysharp.Threading.Tasks;
using UnityEngine;
public sealed class UIHolderFactoryExample : MonoBehaviour
{
[SerializeField] private Transform previewRoot;
private async UniTaskVoid Start()
{
InventoryItemHolder holder = await UIHolderFactory.CreateUIHolderAsync<InventoryItemHolder>(previewRoot);
if (holder != null)
{
holder.TextName.text = "Preview Item";
holder.TextCount.text = "99";
}
}
}
同步版本示例:
using AlicizaX.UI.Runtime;
using UnityEngine;
public sealed class UIHolderFactorySyncExample : MonoBehaviour
{
[SerializeField] private Transform previewRoot;
private void Start()
{
InventoryItemHolder holder = UIHolderFactory.CreateUIHolderSync<InventoryItemHolder>(previewRoot);
if (holder != null)
{
holder.TextName.text = "Sync Preview";
holder.TextCount.text = "1";
}
}
}
注意事项
T必须是正确的生成型UIHolder,且继承自UIHolderObjectBase- 对应 Holder 需要已具备
UIResAttribute,通常由绑定工具自动生成 - 如果资源路径错误、预制体未挂对应 Holder,
UIHolderFactory绑定会失败 - 正常业务打开窗口和创建 Widget 时,优先走
GameApp.UI.ShowUI<T>()、CreateWidgetAsync<T>(),不建议绕过框架直接大量使用工厂
API 参考
IUIService.Initialize(Transform root, bool isOrthographic)
- 必填参数:
root - 必填参数:
isOrthographic - 返回值:无
- 说明:初始化 UI 根、Canvas、Camera 与各层级节点
UniTask<T> ShowUI<T>(params object[] userDatas) where T : UIBase
- 可选参数:
userDatas - 返回值:
UniTask<T> - 泛型约束:
T : UIBase - 说明:异步打开窗口
- 推荐:默认优先使用该方法
T ShowUISync<T>(params object[] userDatas) where T : UIBase
- 返回值:
T - 说明:同步打开窗口
- 注意:仅在资源已就绪时使用
void CloseUI<T>(bool force = false) where T : UIBase
- 可选参数:
force - 返回值:无
- 说明:关闭指定窗口
CreateWidgetAsync<T>(Transform parent, bool visible = true) where T : UIBase
- 必填参数:
parent - 可选参数:
visible - 返回值:
UniTask<T> - 泛型约束:
T : UIBase - 说明:从父 UI 创建 Widget
RemoveWidget(UIBase widget)
- 必填参数:
widget - 返回值:
UniTask - 说明:从父 UI 中移除并销毁 Widget
完整使用示例
示例 1:窗口逻辑 + 自动生成 Holder
using AlicizaX.UI.Runtime;
using UnityEngine;
[Window(UILayer.UI, fullScreen: true, cacheTime: 10)]
public sealed class InventoryWindow : UIWindow<InventoryWindowHolder>
{
protected override async Cysharp.Threading.Tasks.UniTask OnInitializeAsync()
{
baseui.BtnClose.onClick.AddListener(CloseSelf);
InventoryItemWidget item = await CreateWidgetAsync<InventoryItemWidget>(baseui.TfContent, false);
item.Open("Potion", 10);
}
protected override void OnOpen()
{
baseui.TextTitle.text = "Inventory";
}
}
说明:
InventoryWindowHolder推荐由 UI 绑定工具生成InventoryWindow由业务手写
示例 2:Widget 使用生成的 Holder
using AlicizaX.UI.Runtime;
using UnityEngine;
public sealed class InventoryItemWidget : UIWidget<InventoryItemHolder>
{
private string _itemName;
private int _count;
protected override void OnInitialize()
{
baseui.BtnUse.onClick.AddListener(OnClickUse);
}
protected override void OnOpen()
{
_itemName = (string)UserDatas[0];
_count = (int)UserDatas[1];
baseui.TextName.text = _itemName;
baseui.TextCount.text = _count.ToString();
}
protected override void OnClose()
{
baseui.TextName.text = string.Empty;
baseui.TextCount.text = string.Empty;
}
private void OnClickUse()
{
Debug.Log($"Use item: {_itemName}");
Close();
Destroy();
}
}
示例 3:TabWindow
using AlicizaX.UI.Runtime;
[Window(UILayer.UI, fullScreen: true)]
public sealed class SettingWindow : UITabWindow<SettingWindowHolder>
{
protected override void OnInitialize()
{
InitTabVirtuallyView<GraphicsTabWidget>(baseui.TfTabRoot);
InitTabVirtuallyView<AudioTabWidget>(baseui.TfTabRoot);
baseui.BtnGraphics.onClick.AddListener(() => SwitchTab(0));
baseui.BtnAudio.onClick.AddListener(() => SwitchTab(1));
}
protected override void OnOpen()
{
SwitchTab(0);
}
}
最佳实践
- 不要手写大多数 UIHolder,优先使用自动生成
- 窗口逻辑类只处理状态和行为,控件引用统一放进 Holder
OnInitialize做一次性事件绑定,OnOpen做参数刷新- 默认使用异步打开,避免首帧阻塞
- 列表项和重复块优先拆成
UIWidget<T>
常见错误
手工编写 UIHolder 导致与生成器冲突
- 现象:字段名、命名空间或资源路径不一致
- 规避:把 Holder 视为生成产物,由工具维护
在 OnOpen 中重复注册按钮事件
- 风险:窗口每次打开都会重复绑定
- 正确做法:放到
OnInitialize
把 UIWidget<T> 当顶层窗口直接 ShowUI
UIWidget<T>应由父UIBase通过CreateWidgetAsync<T>()或CreateWidgetSync<T>()创建
性能注意事项
- 首次打开大窗口优先预热资源或异步显示
- 使用自动生成 Holder 可以避免大量运行时查找和手工拖引用错误
- 高频销毁/重建的块状内容优先用
UIWidget<T>