UI 模块结构
UIGroup
UIGroup 是 GF 的 UI 模块的一大功能,它提供了游戏中常见的 UI 栈,当不同的 UIFrom 实例处于同一个 UIGroup 中时,会以栈的形式去维护他们的层级和生命周期,一个 UIGroup 中永远只有一个 UIForm 处于最上层,已存在的界面会被新打开的界面覆盖,而且会收到被覆盖事件。UIGroup 内部并没有直接以栈结构去维护上述功能,而是用链表的方式,表头代表栈顶,表尾代表栈底,这样的好处是 UIGroup 可以提供接口可以激活任意界面到最上层,虽然这一操作不符合栈的结构,但无疑能为我们的业务带来更大的灵活性。另外我们还可以通过 UIGroup 去控制一组相关联的 UI 的整体层级以及暂停状态。
当我们有一系列相关联的 UI,他们同一时刻只有一个处于最上层,且需要感知相互间的打开关闭状态时,我们就可以把他们归到同一个 UIGroup 中。
UIGroup 直接受 UIManager 管理,各个 UIGroup 管理各自的 UIForm。
-
对外提供属性 Get
-
Name:UIGroup 的名字
-
Depth:UIGroup 的深度
-
Pause:UIGroup 的暂停状态
-
UIFormCount:包含界面数量
-
CurrentUIForm:当前界面
-
以上属性中其中 Depth 和 Pause 属性可以动态 Set
-
GetUIForm、GetUIForms、GetAllUIForms 方法获取 UIGroup 中的 UIForm
-
AddUIForm 和 RemoveUIForm 在打开和关闭界面时,把 UIForm 从 UIGroup 中添加和移除
-
RefocusUIForm:激活 UIGroup 中任意界面到最上层
-
Refresh:UIGroup 的核心逻辑,根据链表顺序以及 UIForm 的属性,去调用 UIForm 的 OnDepthChanged、OnCover、OnReveal、OnPause、OnResume 这些方法。
UIFormInfo
UIGroup 的内部类,UIGroup 不直接引用 UIForm,而是引用 UIFormInfo,UIFormInfo 内部包含了 UIForm 的引用,另外还有该 UIForm 的 Paused 和 Covered 两个状态,因为这两个状态不会直接储存在 UIForm 中,则用 UIFormInfo 这个类对 UIForm 包装了一层,以方便 UIGroup 的 Refresh 操作时可以知道 UIForm 的状态。
UIForm
UIForm 是 UI 窗口类,被 UIGroup 直接管理,每个 UI 窗口都会有一个 UIForm 实例。
类内信息
- SerialId:该 UI 实例的序列 ID,每个界面被打开之后,关闭之前都有一个唯一的序列 ID,主要是作为相同 UI 面板的不同实例、界面关闭被对象池回收后又取出来作为新界面打开等情况下的唯一标识。
- UIFormAssetName:界面预制体的资源路径。
- Handle:界面的 GameObject 对象,Handle 返回的是 Object 类型,因为 GameObject 对象需要被 UIManager 内部的对象池回收,而 UIManager 处于 GF 层,是不会对 Unity 的类型 “GameObject” 感知的,所以这里以 Object 类型来持有。
- UIGroup:所属的 UIGroup。
- DepthInUIGroup:这个界面在 UIGroup 中的深度。
- PauseCoveredUIForm:当这个值为 True 的时候,打开这个界面,在 UIGroup 的栈结构中,比这个界面低(更接近栈底)的 UIForm,会被调用 OnPause,而关闭这个界面时,比这个界面低(更接近栈底)的 UIForm,会被调用 OnResume。
- Logic:该 UI 的具体逻辑类 UIFormLogic
生命周期方法
-
OnInit & OnRecycle
-
OnInit:调用 UIManager 的 OpenUIForm 初次打开 UI 时被调用
-
OnRecycle:调用 UIManager 的 CloseUIForm 关闭 UI 时被调用
-
OnOpen & OnClose
-
OnOpen:调用 UIManager 的 OpenUIForm 初次打开 UI 时被调用,在 OnInit 之后
-
OnClose:调用 UIManager 的 CloseUIForm 关闭 UI 时被调用,在 OnRecycle 之前
-
OnCover & OnReveal
-
OnCover:同一个 UIGroup 中,有其他界面覆盖到这个界面后,被调用
-
OnReveal:同一个 UIGroup 中,这个界面从被覆盖的状态恢复到处于最上层后,被调用
-
OnPause & OnResume
-
OnPause:当 UIGroup 的 Pause 属性被设置为 True 时,UIGroup 中的所有 UIForm 的 OnPause 都被调用,或者当界面被另一个属性 PauseCoveredUIForm 为 True 的界面覆盖且当前界面 Pause 状态为 False 时,OnPause 被调用
-
OnResume:当界面 Pause 状态为 True 时,从被其他属性 PauseCoveredUIForm 为 True 的界面取消覆盖,且所属 UIGroup 的 Pause 属性也为 False 的时候,OnResume 被调用
-
OnUpdate:界面打开后,只要不处于 Pause 状态,就会被每帧调用
其他方法
- OnDepthChanged:当界面在当前的 UIGroup 中的深度发生变化时,被调用
- OnRefocus:当界面被强制(不遵循栈的规则,从不靠近栈顶的位置,直接回到栈顶)聚焦时,会被调用
UIFormLogic
UIFormLogic 为 UI 界面的具体逻辑类,类内有 UIForm 的所有生命周期方法,与之一一对应,框架将会调用到 UIForm 里的生命周期方法,而 UIForm 再调用到 UIFormLogic 中对应的方法,游戏业务层不对 UIForm 做扩展,而是对 UIFormLogic 继承进行扩展,可以根据具体使用的 UI 方案(ugui 或 ngui 或其他)来继承 UIFormLogic 实现相应的 UguiForm 类等,而游戏中各个具体窗口再继承这个 UI 方案类,实现界面的具体逻辑。
UIManager
UIManager 是外部访问框架 UI 模块的入口。
查询接口
提供 HasUIGroup、GetUIGroup、GetAllUIGroups、HasUIForm、GetUIForm、GetUIForms、GetAllLoadedUIForms、GetAllLoadingUIFormSerialIds、IsLoadingUIForm、IsValidUIForm 方法以对 UIGroup 和 UIForm 进行查询。
操作接口
提供 AddUIGroup、OpenUIForm、CloseUIForm、CloseAllLoadedUIForms、CloseAllLoadingUIForms、RefocusUIForm 对 UIGroup 和 UIForm 进行操作。
UIGroup 的管理
UIManager 内部以 Dictionary 字典结构来储存所有 UIGroup,UIManager 的 Update 方法中会遍历这个字典,调用所有 UIGroup 的 Update 方法,而 UIGroup 的 Update 方法再调用 UIForm 的 Update 方法。
UIManager 对外提供的 UIForm 相关的查询方法,都会遍历这个字典,从所有 UIGroup 找出一个或所有符合条件的 UIForm。
UIManager 的资源管理
UIManager 内部会用 GF 的对象池模块创建一个对象池,用于缓存 UIForm 对象的 GameObject 实例,外部调用 OpenUIForm 来打开 UI 时,会先尝试从对象池获取该界面,若对象池中有同类型的空闲实例,则直接取出使用,若没有则从资源模块加载,加载成功后,会注册到对象池中,再交给 UIManager 使用。而调用 CloseUIForm 来关闭 UI 时,UIForm 会被加到 Queue 类型的字段 m_RecycleQueue 中,在下一次 Update 时,会把队列所有元素取出,回收到对象池中。
UIManager 中的 m_Serial
UIManager 内维护了一个私有字段 m_Serial,每次调用 OpenUIForm 的时候,m_Serial 都会自增 1,他表示了每个 UIForm 在其生命周期内的唯一标识符,即使是同一个 UIForm 实例,被关闭后放回对象池,再被取出来使用,其 m_Serial 也会发生变化,而且 m_Serial 在调用 OpenUIForm 时立刻生成,不需要等资源加载完毕。
OpenUIForm 流程
- 调用 UIManager 的 OpenUIForm 接口
- 尝试从对象池生成对应的 UIForm 的 GameObject
- 若从对象池取出成功,则直接跳至步骤 5
- 若失败,则到步骤 3
- 把这次加载 UIForm 的序列 Id 记得到 m_UIFormsBeingLoaded 中
- 通过资源模块加载 UIForm 的资源
- 若加载成功,从 m_UIFormsBeingLoaded 移除这次加载的 UIForm 的序列 Id,把加载出来的资源注册到对象池中,跳到步骤 5
- 若加载失败,从 m_UIFormsBeingLoaded 移除这次加载的 UIForm 的序列 Id,并抛出错误,终止流程
- 调用 UIManager 的 InternalOpenUIForm,先调用 UIForm 的 OnInit,然后找到把 UIForm 加入对应的 UIGroup 中,再调用 UIForm 的 Open,然后调用 UIGroup 的 Refresh,Refresh 内部再调用 UIForm 的 OnReveal、OnResume
CloseUIForm 流程
- 调用 UIManager 的 CloseUIForm 接口
- 判断这个 UIForm 是否处于加载中(只有通过序列 Id 来关闭,才会有这个步骤,若通过 UIForm 关闭,则直接走步骤 3)
- 正在加载中:m_UIFormsToReleaseOnLoad 和 m_UIFormsBeingLoaded 都加入 UIForm 的序列 Id,终止流程
- 不在加载中:跳到步骤 3
- 从 UIGroup 中移除该 UIForm,并调用 UIForm 的 OnClose 后,调用 UIGroup 的 Refresh,Refresh 内部再调用 UIForm 的 OnCover、OnPause
- 把 UIForm 进队 m_RecycleQueue
- 再下一次 Update,会把 m_RecycleQueue 中所有 UIForm 出队,并调用 OnRecycle 方法
- 把 UIForm 归还给对象池
关闭正在加载的 UIForm
在游戏业务中,如果加载资源方式是异步加载,那么我们可能会遇到在打开 UI 后,需要等待一段时间去加载,而在这期间又触发了特定逻辑,需要取消这个界面的打开,GF 对这种情况也提供了很好的支持,只需要在调用 OpenUIForm 时,保存返回的序列 Id,调用 CloseUIForm 传入这个 Id,即可在资源加载完成后销毁这个资源(Unity 并不支持取消加载,只能在加载后销毁)。
上文 OpenUIForm 和 CloseUIForm 流程中,可以看到有两个特殊字段 m_UIFormsBeingLoaded 和 m_UIFormsToReleaseOnLoad,m_UIFormsBeingLoaded 记录了正在加载的 Id,而 m_UIFormsToReleaseOnLoad 记录了需要在加载完毕直接销毁的 Id,在 CloseUIForm 时,会去判断传入的 Id 是否在 m_UIFormsBeingLoaded 中,如果是则从其中移除,然后把 Id 加入到 m_UIFormsToReleaseOnLoad 中,在资源加载后的回调中,会判断 m_UIFormsToReleaseOnLoad 是否包含加载的 UIForm 的 Id,若包含则直接销毁,不会走 InternalOpenUIForm 流程。
打开单一 UIForm 的生命周期流程
在同一个 UIGroup 中相继打开两个 UIForm 的生命周期流程
UIGroup 中刷新 UI 流程
UIGroup 中的 Refresh 方法的具体实现,个人认为是 UI 模块中最值得拿出来细读的一段代码,该方法以正序遍历内部链表结构,也就是从层级最高的界面逐个迭代到层级最低的界面,并根据实际情况调用 UIForm 的生命周期方法。
注意上面流程图中条件判断包括有 Covered、Paused 和 cover、pause,Covered、Paused 是指当前正在迭代的 UIForm 的状态,根据其状态决定是否需要执行 OnCover、OnPause、OnReveal、OnResume,而 cover 和 pause 是记录着从第一次迭代开始就记录着的状态,只要迭代过程中出现过一次 true,则后面的迭代中 cover 或 pause 肯定为 true,因为上面的界面处于 Covered 或 Paused 状态时,下面的界面必然也处于 Covered 或 Paused 状态,所以 cover 或 pause 肯定为 true 时,不会执行 OnReveal 或 OnResume。
最后
GameFramework 解析 系列目录:GameFramework 解析:开篇
个人原创,未经授权,谢绝转载!