UI 框架¶
Aesir Modules 的 UI 框架采用 Manager-of-Managers 单例模式:UIRoot 负责层级与画布,UIModule 负责面板与窗口生命周期。UI 有两种并列形态 —— Panel(面板) 经 AesirBasePanel 家族接入、挂载于共享层 Canvas;Window(窗口, Canvas 根 UI) 经 AesirBaseWindow 家族接入、预制体自带 Canvas、直接挂载于 UIRoot。
UIRoot —— UI 根节点¶
菜单 GameObject → Aesir Modules → Create UIRoot 一键构建:
- 四层 Canvas:Background(sortingOrder 100)/ Normal(200)/ Popup(300)/ Top(400),每层为 ScreenSpaceCamera 画布 + CanvasScaler + GraphicRaycaster
- UICamera:正交、depth 1、cullingMask 为 UI 与 TransparentFX 层
- EventSystem:全场景去重检查;默认挂
StandaloneInputModule(安装 Input System 包时自动替换为InputSystemUIInputModule)
层级 Canvas / UICamera / EventSystem 均为序列化引用持久化 —— 存在性判定只看引用非空,子物体重命名不会破坏引用;构建后可自由调整结构。
画布参数(分辨率 1920×1080、ScaleWithScreenSize 等)由 UICanvasConfigSO 统一配置;菜单 Assets → Create → Aesir Modules → UI → Default UICanvasConfig 生成默认配置资产,未配置时使用内置默认值。
UIModule —— 面板管理器¶
静态快捷 API(全部转发到单例):
| API | 说明 |
|---|---|
UIModule.Show<T>(payload, path) |
显示面板(可带强类型 payload 与 Resources 路径) |
UIModule.Hide<T>() |
隐藏面板(按 DestroyOnHide 决定销毁或缓存) |
UIModule.Get<T>() |
获取已注册面板实例 |
UIModule.RegisterPrefab<T>(prefab) |
注册面板预制体 |
UIModule.ContainPrefab<T>() |
查询预制体是否已注册 |
UIModule.Prewarm<T>(path) |
预实例化并隐藏,首次 Show 直接复用 |
PrewarmAll() 通过协程逐帧预实例化,分摊首次打开的实例化卡顿。
注册键 = 面板实例的实际类型
面板注册表以实例的实际类型为键。预制体上挂载的脚本是注册类型的派生类时:
- 以基类类型重复
Show:报错拒绝,不会重复实例化 - 以基类类型
Hide/Get:警告提示实际类型后静默返回 - 面板内
HideSelf()始终以实际类型调用,永远安全
最佳实践:注册、显示、关闭、获取统一使用同一类型。
面板生命周期¶
实例化(停用状态,Awake/OnEnable 不触发)
→ 挂到对应层 Canvas → Initialize → Show(payload)
→ Hide ── DestroyOnHide=false → 停用缓存(SetActive(false),下次 Show 复用)
└ DestroyOnHide=true → DestroyPanel(OnHide → OnClose → Destroy)
- 面板以停用状态实例化:
Awake/OnEnable推迟到 Show 激活时才触发,保证OnEnable可安全访问OnInit之后才有值的引用 - 面板被外部销毁 / 场景卸载时,
AesirBasePanel.OnDestroy自动反向清理 UIModule 注册表,无残留
OnClose 与 OnDestroy 的职责分界¶
| 销毁路径 | 触发的回调 |
|---|---|
Hide 且 DestroyOnHide=true(受控销毁) |
OnHide → OnClose → Destroy → OnDestroy |
Hide 且 DestroyOnHide=false |
仅 OnHide(实例缓存复用) |
场景卸载 / 外部 Destroy(gameObject) |
仅 OnDestroy |
事件解绑必须放 OnDestroy
OnClose 只在受控销毁路径调用;场景卸载、外部 Destroy 等非受控销毁只触发 OnDestroy。事件解绑与订阅释放请放在 OnDestroy(或 OnClose + OnDestroy 两处) —— 仅写在 OnClose 会在场景切换时泄漏(MiniEvent / ObservableValue 的订阅没有死引用清理兜底)。
面板基类家族:
| 基类 | 角色 | 用途 |
|---|---|---|
AesirBasePanel |
IUIPanel |
通用面板基类,虚方法 OnInit / OnShow(payload) / OnHide / OnClose,便捷方法 HideSelf() |
AesirBasePanelView<T> |
+ RAA IView |
MVP 模式面板:按 Context 类型绑定,只读访问 Model / Service |
AesirBasePanelViewController<T> |
+ RAA IController |
MVC 模式面板:可执行 Command / Query |
窗口(Canvas 根 UI)¶
与 Panel 并列的第二种 UI 形态: 预制体根节点自带 Canvas(独立渲染根), 打开时直接挂载到 UIRoot 下(不经四层 Canvas), 默认 sortingOrder 500 恒在面板四层之上 —— 适合模态弹窗、全屏流转页(设置 / 暂停 / 结算 / 加载)与需要独立排序或渲染隔离的浮层。
预制体结构约定:
XxxWindow(根: Canvas + CanvasScaler + GraphicRaycaster + 窗口脚本; 类名 = 脚本名 = 预制体名, 以 Window 结尾)
├── Mask 蒙版: Image 全屏拉伸(拦截其下一切 UI 的射线) + 可选 Button(承接点击)
└── Content 实际 UI 元素容器(框架不触碰)
静态快捷 API:
| API | 说明 |
|---|---|
UIModule.Open<T>(payload, path) |
打开窗口(已存在则置顶并重新 OnShow) |
UIModule.Close<T>() |
关闭窗口(按 DestroyOnHide 决定销毁或缓存) |
UIModule.GetWindow<T>() |
获取已注册窗口实例 |
UIModule.RegisterWindowPrefab<T>(prefab) |
注册窗口预制体(与面板共用注册表, 类型系统天然分桶) |
UIModule.PrewarmWindow<T>(path) |
预实例化并隐藏, 首次 Open 直接复用 |
生命周期与面板完全同构(AesirBaseWindow 虚方法 OnInit / OnShow(payload) / OnHide / OnClose, 便捷方法 CloseSelf(); MVP / MVC 变体 AesirBaseWindowView<T> / AesirBaseWindowViewController<T> 同款)。差异仅在根节点与挂载: 根 Canvas 挂载时统一接线 UICamera / 渲染模式 / Canvas 缩放配置, 按窗口声明的 sortingOrder 排序, 并递归设置 UI 层; 注册键 = 窗口实例的实际类型, 键语义与面板同款; 面板与窗口入口互斥, 类型误入会报错并指向正确入口。
命名后缀取 Window 而非 Canvas
按 .NET 命名惯例(派生类以基类名结尾是阅读预期), XxxCanvas 会被误读为 UnityEngine.Canvas 派生类; Unity 运行时不存在 Window 类型, 零误导。
蒙版机制(单遮 / 叠遮)¶
蒙版不是独立物体, 而是窗口预制体内的 Mask 子物体: 位于自身 Canvas 内、Content 之下、其余一切 UI 之上 —— 天然挡住本窗口以下的面板与其他窗口的射线与视觉。UIModule 按全局配置资产 UIModuleConfigSO 的 maskMode 统一调度, 每次窗口 Open / Close / 销毁后重算。配置资产由编辑器自动创建于 Assets/Resources/UIModuleConfig/, 在 Project 窗口直接编辑即可, 无需预放置 [UIModule]:
| 模式 | 语义 |
|---|---|
| 单遮(默认) | 全局仅最高层可见窗口的蒙版生效(sortingOrder 最大者, 同值取后开者), 多窗口叠加透明度不叠加 |
| 叠遮 | 各窗口蒙版独立跟随自身打开状态, 透明度逐层叠加 |
蒙版点击经 Mask 子物体的 Button 在 OnInit 自动接线, 回调虚方法 OnMaskClicked() —— 默认按 closeOnMaskClick(默认关)决定是否关闭本窗口, 子类可覆写自定义行为; 运行时经 UIModule.MaskMode 切换(初值取自 UIModuleConfigSO, 运行时切换只覆盖内存值并立即重算)。无 Mask 子物体的窗口(如全屏不透明加载页)不参与遮挡。
运行示例见包内 Samples/UI/01_BasicUsage(面板与窗口协作、蒙版单遮 / 叠遮运行时切换对照、点击蒙版关闭、全屏加载窗口 payload 自动关闭)。
Panel 与 Window 的选型对比¶
一个项目通常只选一种主形态; 两种形态 API 与生命周期完全对称, 按需混用也成立(窗口恒在面板之上):
| 维度 | Panel(面板) | Window(窗口) |
|---|---|---|
| 根节点 | RectTransform(无 Canvas) | Canvas(独立渲染根) |
| 挂载 | 四层 Canvas 之下(100–400 由层决定) | UIRoot 直下(默认 500, 恒在面板之上) |
| 渲染 | 同层共享层 Canvas, 同图集合批友好 | 独立 Canvas, 打断合批但隔离重绘 |
| 排序 | 层内由 Show 顺序决定 | sortingOrder 自治(声明任意值) |
| 蒙版 | 无 | Mask 子物体 + 单遮/叠遮调度 |
| API | Show / Hide / Get / Prewarm |
Open / Close / GetWindow / PrewarmWindow |
- 推荐 Panel: 常驻 HUD、非模态并存的信息 / 列表面板、同层批量渲染优先 —— 教学与中小项目的默认主形态
- 推荐 Window: 模态弹窗(需要挡住下面一切的输入)、全屏流转页(设置 / 暂停 / 结算 / 加载)、需要独立排序或渲染隔离的浮层
- 全 Window 化(每个界面一个独立 Canvas)也成立, 代价是放弃共享层 Canvas 的合批友好 —— 适合弹窗密集的模态化项目
可插拔资源加载¶
默认 ResourcesUILoader(预制体路径约定为面板类型名)。实现 IUIAssetLoader 可替换为其他同步可达方案:
public interface IUIAssetLoader
{
GameObject Load(string path);
}
// 注入自定义加载器
UIModule.Instance.RegisterAssetLoader(new MyAddressablesLoader());
加载契约为同步语义
Load 需同步返回预制体。Addressables 等异步管线无法在接口内表达等待 —— Handle.Result 同步等待在 WebGL 会死锁、在其他平台阻塞主线程。推荐做法:预加载完成后经自定义 loader 查缓存同步返回。预制体引用由 UIModule 注册表持有,契约不设释放方法。
DDOL 机制¶
AesirModules / UIRoot / UIModule / EventModule 均有 [SerializeField] bool dontDestroyOnLoad = true:
AesirModules宿主:运行时创建恒为 DDOLUIRoot:预放置与运行时创建统一由该字段控制UIModule:字段仅在预放置为根物体时生效;运行时自动创建时挂载于[Aesir Modules]宿主下,跟随宿主决策EventModule:预放置实例同样由该字段控制(0.31.1 起补上字段)——此前预放置在场景根的事件模块会随场景卸载被销毁,注册表整体消失
设计边界¶
- 层级体系是封闭集 ——
UILayer固定四层(Background / Normal / Popup / Top),层序基准硬编码为 100 / 200 / 300 / 400 且每次初始化强制覆盖;新增层级需修改框架源码 - 无 per-panel Canvas —— 同层多面板共享层 Canvas(同图集合批友好);需要动画隔离 / 独立渲染请在面板预制体内自行添加子 Canvas。同层内渲染顺序仅由 Show 顺序决定;需要独立 Canvas、独立排序或蒙版挡输入的完整界面请升格为窗口形态
- 蒙版只服务窗口形态 —— 单遮 / 叠遮调度只作用于窗口的
Mask子物体,面板不提供蒙版(需要遮挡输入的面板应改为窗口) - 主相机需自行排除 UI 层 —— UICamera 的 cullingMask 只含 UI 与 TransparentFX 层,主游戏相机若也包含 UI 层会重复渲染
- 不做 —— 面板 / 窗口导航栈与返回、异步加载接口、层扩展配置;SmartShowHide 伪隐藏(全屏窗口弹出时自动伪隐藏被遮挡面板)为下期候选(见包 CHANGELOG 的 Planned 节)
Binder 组件绑定(需 Odin Inspector)¶
将 UI 元素自动绑定到面板脚本,免去手写 transform.Find / GetComponent:
BinderTag挂在待绑定子物体上做标记(右键菜单GameObject → Aesir → 添加 BinderTag 标记)BinderAssistant挂在面板根(右键菜单GameObject → Aesir → 挂载 BinderAssistant),「构建绑定单元」按标记增量维护绑定列表- 「生成脚本」产出绑定代码;编译完成后自动挂载组件并执行一次绑定
生成配置(后缀列表 / 默认后缀 / 最近命名空间)经 BinderEditorSettings 持久化到 ScriptableSingleton/AesirModules/BinderEditorSettings.asset,重启编辑器后保留。
两种生成模式:
| 模式 | 行为 |
|---|---|
| 同一脚本增量(默认) | 只替换 *.cs 内「绑定字段(自动生成)」region,region 外归开发者 |
| Partial 分部类 | 自动维护文件(后缀默认 .designer.cs)+ 手写 partial 仅首次生成;Rider 用户推荐 |
生成脚本基类可下拉选择:MonoBehaviour、Aesir 面板 / 窗口家族(AesirBasePanel / AesirBasePanelView<T> / AesirBasePanelViewController<T> / AesirBaseWindow / AesirBaseWindowView<T> / AesirBaseWindowViewController<T>;Canvas 根物体上默认基类直指 AesirBaseWindow、默认脚本名取 Window 后缀,面板根保持 Panel,已带对应后缀的物体名不重复拼接)或用户以 [BinderBaseType] 标记的自定义基类;选择 Aesir 泛型基类时,「Context 类型」下拉扫描项目内 AbstractContext 派生类(自动排除 [InternalContext] 标记的示例 / 测试 Context)。
Odin 依赖
Binder 的类型选择器(组件 / 基类下拉)强依赖 Odin Inspector:运行时可见部分在 Runestone.AesirModules.OdinInspector(编辑器实现整文件 #if UNITY_EDITOR 剔除,不随 Player 构建打包),编辑器入口在 Runestone.AesirModules.Editor.OdinInspector;未安装 Odin 时整体排除,不影响 UI 框架其余功能。