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<string, UIGroup>字典结构来储存所有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流程

  1. 调用UIManager的OpenUIForm接口
  2. 尝试从对象池生成对应的UIForm的GameObject
    • 若从对象池取出成功,则直接跳至步骤5
    • 若失败,则到步骤3
  3. 把这次加载UIForm的序列Id记得到m_UIFormsBeingLoaded中
  4. 通过资源模块加载UIForm的资源
    • 若加载成功,从m_UIFormsBeingLoaded移除这次加载的UIForm的序列Id,把加载出来的资源注册到对象池中,跳到步骤5
    • 若加载失败,从m_UIFormsBeingLoaded移除这次加载的UIForm的序列Id,并抛出错误,终止流程
  5. 调用UIManager的InternalOpenUIForm,先调用UIForm的OnInit,然后找到把UIForm加入对应的UIGroup中,再调用UIForm的Open,然后调用UIGroup的Refresh,Refresh内部再调用UIForm的OnReveal、OnResume

CloseUIForm流程

  1. 调用UIManager的CloseUIForm接口
  2. 判断这个UIForm是否处于加载中(只有通过序列Id来关闭,才会有这个步骤,若通过UIForm关闭,则直接走步骤3)
    • 正在加载中:m_UIFormsToReleaseOnLoad和m_UIFormsBeingLoaded都加入UIForm的序列Id,终止流程
    • 不在加载中:跳到步骤3
  3. 从UIGroup中移除该UIForm,并调用UIForm的OnClose后,调用UIGroup的Refresh,Refresh内部再调用UIForm的OnCover、OnPause
  4. 把UIForm进队m_RecycleQueue
  5. 再下一次Update,会把m_RecycleQueue中所有UIForm出队,并调用OnRecycle方法
  6. 把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流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
/// <summary>
/// 刷新界面组。
/// </summary>
public void Refresh()
{
LinkedListNode<UIFormInfo> current = m_UIFormInfos.First;
bool pause = m_Pause;
bool cover = false;
int depth = UIFormCount;
while (current != null && current.Value != null)
{
LinkedListNode<UIFormInfo> next = current.Next;
current.Value.UIForm.OnDepthChanged(Depth, depth--);
if (current.Value == null)
{
return;
}

if (pause)
{
if (!current.Value.Covered)
{
current.Value.Covered = true;
current.Value.UIForm.OnCover();
if (current.Value == null)
{
return;
}
}

if (!current.Value.Paused)
{
current.Value.Paused = true;
current.Value.UIForm.OnPause();
if (current.Value == null)
{
return;
}
}
}
else
{
if (current.Value.Paused)
{
current.Value.Paused = false;
current.Value.UIForm.OnResume();
if (current.Value == null)
{
return;
}
}

if (current.Value.UIForm.PauseCoveredUIForm)
{
pause = true;
}

if (cover)
{
if (!current.Value.Covered)
{
current.Value.Covered = true;
current.Value.UIForm.OnCover();
if (current.Value == null)
{
return;
}
}
}
else
{
if (current.Value.Covered)
{
current.Value.Covered = false;
current.Value.UIForm.OnReveal();
if (current.Value == null)
{
return;
}
}

cover = true;
}
}

current = next;
}
}

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解析:开篇

个人原创,未经授权,谢绝转载!