此代码库依赖:
这是一个给Unity游戏使用的UI管理器。可以以 unity package 方式接入到工程中,此做法保证了UI管理器功能的独立,可同时服务于多个项目。
此外,此UI管理器在实现了众多基础功能的同时,还方便实现界面换皮、界面开启前异步准备、返回键响应及焦点管理等特性。
同时,在代码性能、高强度操作时的安全性上都有相应的处理。
专指实现了IUILogicStack或IUILogicFixed的类(通常会继承自指定的基类),这些类主要有如下功能:
- 处理界面开启前的数据逻辑。
- 提供用于界面展示的参数。
- 对界面节点进行操作,以使其展示相应的内容。
- 侦听用户对此展示界面可交互对应的操作,并做出相应响应。
- 响应界面的生命周期事件,并进行相应的逻辑处理。
界面逻辑的类型由用于界面开启的参数指定,界面逻辑的实例由UI管理器来创建和管理。
专指在运行时通过IUILoader对象获取的界面prefab资源的实例,通常包括:
- 控制渲染层级等规则的Canvas组件。
- 界面中需要展示贴图、精灵、文本等内容,以及交互组件。
- 界面中内嵌的3D模型、粒子物资等其他Renderer渲染的内容。
Animation及Animator等动画组件。
即界面与界面之前的显示(渲染)先后顺序,使用Canvas及Renderer组件的sortingOrder属性来控制。
考虑到UI组件中可能会穿插渲染由UI摄像机拍照的3D物体(不透明物体),这些物体会优先于所有UI渲染,渲染UI内容时需要判断此UI要显示在此3D物体之前或者之后。所以所有UI界面及其中的内容要充分利用深度(体现在Z轴位置上)。
由于sortingOrder值更大的内容会显示在上层,其Z轴(相对于摄像机坐标系)位置值越小的内容也会显示在最上层,所以在此UI管理器中,将二者绑定,其规则见层级管理规则,Z轴管理规则。
全屏界面通常使用如下两种显示方式中的一种:
- 界面中显示内容覆盖整个屏幕,不会显示出此界面下面的界面及场景。
- 界面有镂空,需要显示出下面的场景,但不显示此前已打开的堆叠型的界面。
在游戏开发过程中,可能会有如下界面的开启关系:
- 用户在限时商城界面中点击一个商品,打开商品信息界面。
- 接着,此用户在商品信息界面点击了购买按钮,弹出了游戏内的支付确认界面。
如果玩家在进行上述操作的时间,恰好处于限时商城界面开启的最后几秒,并停留在支付确认界面没有操作。限时商城到期后,需要关闭限时商城界面,同时也要附带关闭所有随之打开的商品信息界面以及支付确认界面。
针对上面的情况,可以将此三个界面安排在同一个属于限时商城界面的自然分组中,在关闭界面时,调用按照自然分组关闭界面的API关闭限时商城界面时,即可关闭上述三个界面。
在上述案例中,若限时商城未到达关闭时间,但用户打开的是个限时商品的商品信息界面,随后打开对应的支付确认界面并停留。稍后此限时商品到期,需要关闭商品信息界面。此时虽然三界面同属限时商城界面的自然分组,但关闭商品信息界面时,仅会关闭此界面及后续打开的支付确认界面。
这一类型的界面使用"栈"的方式来管理,通常具备以下特点:
- 最新打开的界面显示在最上方。
- 最上方的界面关闭后,次上方的界面将显示在最上方。
- 通常是最上方的界面响应用户的操作。
堆叠型界面的界面逻辑应实现IUILogicStack接口。
这一类型的界面的sortingOrder固定,所在的Z轴位置也固定。这两个参数的计算规则详见层级管理规则,Z轴管理规则,其参数来自IUILogicFixed.SortingOrderBias,详见用于固定层级界面的展示参数。
固定层级界面的界面逻辑应实现IUILogicFixed接口。
焦点界面有两层含义:
- 当前处在最前面的界面,即用户可直接交互的界面。
- 需要响应实体返回键(开发中的ESC键)按下事件的界面。
堆叠型的界面,都是可以获得焦点的界面。
某特定时刻的焦点界面,通常是最上方的堆叠型的界面。但层级更高的固定层级界面,也可临时获取焦点,以避免所有低层级的界面获取焦点。
其中,全局唯一指的是:
- 仅有一个UI根节点。
- 从程序启动开始,始终使用一个UI根节点。
这个UI根节点的作用是:
此UI管理器中可能需要使用到的外部支持:
其中,前两项需要在调用UI管理器的初始化方法传入的参数对象中实现,后两项可根据实际项目情况及个人喜好,进行方案选择。
界面逻辑应实现该界面分类所对应的接口,UI管理器通过这些接口中的属性和方法获取用于界面展示的参数以及调用界面生命周期方法。
所以界面逻辑可继承自任何实现了项目中所需属性和方法的基类,以使界面逻辑中方便调用项目中定制的方法。
但需要注意:界面逻辑类应包含public无参构造函数。
对于游戏开发中的如下常见需求,可直接在对应的界面逻辑中编码实现:
- 打开排行榜界面时,需要在界面展示前需要完成排行榜数据的获取。
- 在显示某个界面前,需要切换到指定场景。
此方案允许开发者在加载界面节点(加载界面prefab资源)的时候同时进行界面逻辑需要的其他异步操作,并可控制此异步操作的超时时长及超时行为,并可以在异步操作失败后关闭界面。
相比于在展示界面之前或展示界面之后再进行这些异步操作,在加载界面过程中进行这些异步操作的意义在于:
- 统一了打开界面的写法:不需要在调用
UIManager.Open方法之前进行任何异步的准备(除参数外),避免了为打开某些界面而进行的特殊处理的编码。 - 避免了因没有准备好数据、场景等内容而显示异常的尴尬:界面在界面节点加载完成,且异步准备成功完成后才会显示,而在加载、异步准备过程中,有屏蔽用户操作的机制来保证当前正在进行的操作的完整与安全。
在此方案中,界面的Z轴位置取决于界面基础sortingOrder的值。
所有界面的sortingOrder的基础值,都是动态计算获得并在界面启动时动态赋值。
计算界面sortingOrder值及Z轴位置所依赖的常数有:
- 最小/最大
sortingOrder值; - 每个UI界面占用
sortingOrder的范围; - 相邻两界面之间的Z轴位置间隔。
这些常数,都以组件参数的形式定义在UIRoot组件中,结合场景中的UI相机等节点组件的参数,可得到计算界面sortingOrder值及Z轴位置所依赖的其他参数:
- 最低层级的界面与最高层级界面所对应的最远与最近的Z轴位置(低层级界面渲染优先级低,距离远);
计算sortingOrder的规则:
- 对于堆叠界面:最小
sortingOrder值 + (2 + 界面堆叠索引) * 每个UI界面占用sortingOrder的范围; - 对于固定层级界面:界面逻辑的
SortingOrderBias为负数时,其sortingOrder值为最大sortingOrder值与该负值之和;为正数时,其sortingOrder值为最小sortingOrder值与该正值之和。
计算Z轴位置的规则。
sortingOrder值越大的,Z轴位置值越小;- 对于堆叠界面和
SortingOrderBias值为正数的固定层级界面,其Z轴位置值基于最远的Z轴位置来计算; - 对于
SortingOrderBias值为负数的固定层级界面,其Z轴位置值基于最近的Z轴位置来计算。
此UI管理器认为:
- 一个界面由"用于展示的gameObject"和对应的"控制逻辑"构成。
id用于一个界面的唯一标识。
所以,UI管理器需要通过下面的参数来确定一个界面:
- 界面prefab路径,对应下面代码中
prefab_path字段。 - 界面控制逻辑的类型,对应下面代码中
logic_type字段。
UI管理器中定义了ParametersForUI类型,作为开启界面时的必需参数:
public struct ParametersForUI {
public string id;
public string prefab_path;
public Type logic_type;
}关于开启界面时这些参数使用,详见:
- 开启界面的API中指定id或提供全部所需要的参数。
- 界面开启参数获取及GameObject加载器中关于
GetParameterForUI的部分。
这部分参数均从界面逻辑的实例中获取,这些参数均定义在界面逻辑需要实现的接口中。
// 此接口为IUILogicStack与IUILogicFixed的父接口。
public interface IUILogicBase {
string MutexGroup { get; }
eUIVisibleOperateType VisibleOperateType { get; }
...
}其中:
MutexGroup:互斥组。对于互斥组相同的两个界面,在打开新的界面时,旧界面将被关闭。VisibleOperateType:指定该界面在需要被隐藏时,通过何种方式(可多选)来隐藏。参考关于界面隐藏中如何隐藏。
public interface IUILogicStack : IUILogicBase {
// 此界面是否允许多开。
bool AllowMultiple { get; }
// 此界面是否为全屏界面。
bool IsFullScreen { get; }
// 是否开户新的自然分组。
bool NewGroup { get; }
}其中:
AllowMultiple:先后开启同一个id的界面时(其中可以有其他界面),如果此界面允许多开,则正常显示新开户的同id界面,此时这个id的界面存在多个实例;如果此界面不允许多开,则会关闭之前开户的同id的界面。IsFullScreen:标记此界面是否为全屏界面。NewGroup:是否从此界面开始,创建新的自然分组。如果是,则此界面将不再属性之前界面的自然分组,此后开启的界面也会默认属于此界面相同的自然分组。
public interface IUILogicFixed : IUILogicBase {
int SortingOrderBias { get; }
}其中:
SortingOrderBias:用于指定此界面的层级:此值为负数时表示对于最高层级对应sortingOrder的偏移,即从最大的sortingOrder值往下减;此值为0或正数时表示对最低的层级对应sortingOrder的偏移,即从最小的sortingOrder值往上加。详见层级管理规则,Z轴管理规则。
- 获取界面开启参数(如果在使用界面id开启界面),参数无效则放弃开启界面。
- 获取界面逻辑实例。
- 调用界面逻辑实例的
OnCreate()方法,并传入逻辑给出的参数,如果方法返回值为false则放弃开启界面。 - 开启LoadingOverlay以避免屏蔽用户的其他操作。
- 同时开始界面节点的加载与尝试调用界面开启前的异步准备方法:
- 调用界面逻辑的
OnPrepareCheck()方法,如果方法返回false,则不再调用OnPrepareExecute()方法。 - 调用
OnPrepareExecute()方法(如果需要)。
- 调用界面逻辑的
- 等待界面节点加载完成与
OnPrepareExecute()方法(如果需要)执行完成或超时。若OnPrepareExecute()方法异步返回了false或设置了超时需要关闭界面,则放弃开启界面。 - 设置界面中
Canvas与Renderer的sortingOrder。 - 调用界面的
OnOpen()和OnShow()方法。 - 处理其他界面的隐藏、互斥等关系。
- 关闭LoadingOverlay以使用户可以进行交互。
详解:
- 界面逻辑在开启界面的流程中同步创建,
OnCreate()方法也被同步调用。此设计在一定程度上让界面逻辑中的部分逻辑脱离界面节点,并在界面节点加载前就开始执行。 - 在调用
OnCreate()方法、OnPrepareExecute()方法过程中,界面逻辑均有终止界面开启流程的可能。 - 在
OnOpen()方法中,界面节点gameObject对象将被传入到界面逻辑实例中,带有表现的界面逻辑正式启动。
生命周期方法均成对出现,且必定成对被调用。
由于界面逻辑在未开始加载界面节点时就已经开始运行,所以界面生命周期方法中,将包含纯逻辑的生命周期方法,以及界面显示相关的生命周期方法。
| 方法名称 | 触发时机 | 可重复 | 参数 |
|---|---|---|---|
OnCreate |
界面逻辑启动时 | 否 | 打开界面时传入的入口参数 |
-OnOpen |
界面节点加载完成时 | 否 | 界面gameObject对象及基础sortingOrder值 |
--OnShow |
界面节点显示时 | 是 | 是否是首次调用 |
--OnHide |
界面节点隐藏时 | 是 | 无 |
-OnClose |
界面节点关闭回收前 | 否 | 无 |
OnTerminated |
界面逻辑结束时 | 否 | 无 |
其中:
- 方法名称前的"-"表示该生命周期方法的深度,更深层的生命周期方法必会在浅层的生命周期那一对方法的两次调用之中进行。
- "可重复"表示在一次界面启动到关闭的生命周期中,一个生命周期方法是否可以被触发多次。
- 通常
OnShow()方法的首次调用会紧随OnOpen()之后,除非在此界面未成功展示的时候,有一个加载更快的全屏界面完成加载并已经显示。 - 对于一个正在显示的界面被关闭时,其
OnHide()、OnClose()和OnTerminated()会被连续调用。
注意:
- 仅
OnCreate()方法返回true的时候才可能会调用后续的生命周期方法,包括与之对应的OnTerminated()。
此操作需要在调用开启界面的API中的OpenWithHandler()方法中传入用于侦听目标界面生命周期事件的对象,此对象应实现IUIEventHandler接口:
public interface IUIEventHandler {
void OnOpened();
void OnShown();
void OnHided();
void OnClosed();
void OnTerminated();
}其中:
- 方法名称与界面逻辑中的相应方法有所不同,因为用于侦听目标界面生命周期事件的对象中的生命周期方法是在调用了界面逻辑中的相应方法之后才调用。
- 这些方法中并没有与
OnCreate()对应的方法,因为在调用UIManager.OpenWithHandler()方法时,界面逻辑的OnCreate()就已经被调用,且UIManager.OpenWithHandler()方法的返回值也表明了该界面是否会继续进行加载等后续流程。
开启界面的方法,需要以下三种参数:
- 用于指定目标界面的id或界面开启参数(必需);
- 界面逻辑启动时需要的入口参数(可选);
- 用于侦听目标界面生命周期事件的对象(可选)。
注:对于以上参数的所有组合,在UI管理器中都对应一个开启界面的方法。
在UI管理器的开启界面的一系列方法中,使用了两种方式来确定一个界面:
- 使用字符串id:方便业务逻辑直接开启界面,无需关心其开启参数。但需要调用UI管理器初始化时传入的界面开启参数获取及GameObject加载器中
GetParameterForUI方法以获取全部参数。 - 使用
ParametersForUI数据结构提供全部界面开启参数:方便项目定制。
典型方法如下:
public static class UIManager {
...
public static bool Open(string id) { }
public static bool Open(ParametersForUI cfg) { }
...
}对于UI管理器的开启界面的部分方法,允许向界面逻辑传入一个类型为object的参数(详见界面开启流程中关于OnCreate的部分)。
相关开启界面的方法如下:
public static class UIManager {
...
public static bool Open(string id, object parameter) { }
public static bool Open(ParametersForUI cfg, object parameter) { }
public static bool OpenWithHandler(IUIEventHandler handler, string id, object parameter) { }
public static bool OpenWithHandler(IUIEventHandler handler, ParametersForUI cfg, object parameter) { }
...
}UI管理器开启界面的方法中,方法名为OpenWithHandler的方法,需要传入一个侦听界面生命周期事件的对象,从而实现开启界面的逻辑中响应被开启界面的各个生命周期事件,并执行相应的逻辑。
此逻类事件侦听辑通常用于:
- 连续开启多个界面时,侦听前一个界面的关闭事件。
- 在进行较大的界面加载时,先完成“加载进度界面”的展示,之后再进行较大界面的加载,此时需要侦听较大界面的展示事件。
相关开启界面的方法如下:
public static class UIManager {
...
public static bool OpenWithHandler(IUIEventHandler handler, string id) { }
public static bool OpenWithHandler(IUIEventHandler handler, string id, object parameter) { }
public static bool OpenWithHandler(IUIEventHandler handler, ParametersForUI cfg) { }
public static bool OpenWithHandler(IUIEventHandler handler, ParametersForUI cfg, object parameter) { }
...
}需要注意:
- 界面id用于界面配置层面的唯一标识,并非界面实例的唯一标识。
- 对于可多开界面(见用于界面展示的参数中的"允许多开"),无法通过界面id来确定出唯一的界面实例。
- 按界面id关闭界面时,会关闭所有id匹配的界面。
相关方法:
public static class UIManager {
...
public bool CloseGroup(string id) { }
public bool CloseSingle(string id) { }
...
}不同于按界面id指定,使用界面逻辑实例可以绝对唯一指定到目标界面。
相关方法:
public static class UIManager {
...
public bool CloseGroup(IUILogicBase logic) { }
public bool CloseSingle(IUILogicBase logic) { }
...
}关于用于界面逻辑实例IUILogicBase,参见界面分类以及定制UI基类。
调用名称为CloseGroup()的关闭界面方法时,将会关闭目标界面以及后续界面同与目标界面属于同一自然分组的界面。
调用名称为CloseSingle()的关闭界面方法时,不会考虑界面的自然分组,仅会关闭目标界面。
由界面逻辑的用于界面展示的参数中VisibleOperateType属性来控制其隐藏方法。
VisibleOperateType属性类型eUIVisibleOperateType的定义为:
[Flags]
public enum eUIVisibleOperateType {
SetActive = 0,
LayerMask = 1,
OutOfScreen = 2
}其中:
SetActive:在未选择其他的任何一种方式时采用的控制显隐默认方式,如果采用了其他方式中的任何一种,都不会再使用此方式来控制显示与隐藏。LayerMask:使用Canvas及Renderer的Layer属性来控制显示与隐藏。在将其隐藏时,将会使用到UI根节点中的Layer For Hide参数,详见创建场景中UI根节点。OutOfScreen:使用移出屏幕显示范围的方法来隐藏界面。
引入多种隐藏界面的方法,是为了避免使用单一的SetActive方式。因为SetActive方式会造成非常多节点组件的生命周期事件调用,占用CPU时间。
焦点界面永远是需要所有需要焦点的界面中,层级(sortingOrder)最高的,所以,通常是最上方的堆叠型的界面。
固定层级界面的界面逻辑也可通过实现IUIDynamicFocusable接口,在需要的时候临时获取焦点。
相关接口的定义为:
public interface IUIDynamicFocusable : IUIFocusable {
void SetDynamicFocusAgent(IUILogicDynamicFocusAgent agent);
}
public interface IUILogicDynamicFocusAgent {
bool RequireFocus();
bool ReleaseFocus();
}在实现了IUIDynamicFocusable接口的固定层级界面的界面逻辑启动后,UI管理器会传给它一个实现了IUILogicDynamicFocusAgent接口的对象。在此界面逻辑需要焦点或释放焦点时,可以调用此对象的RequireFocus()和ReleaseFocus()方法是临时申请获取和释放焦点。
申请获取焦点后,该固定层级界面也要与其他需要焦点的界面进行层级(sortingOrder)比较,只有最高层级的需要焦点的界面,才能最终获取焦点。
场景中UI根节点的核心:UIRoot组件。其包含了如下参数:
Root Canvas:指定了UI渲染方案、屏幕适配的根Canvas;Parent For UI:所有UI界面节点将使用的父节点;Layer For Hide:用于使用Layer隐藏界面时设置的不可见的Layer;Sorting Order Min、Sorting Order Max、SortingOrder Range Per UI、Position Z Interval:用于、层级和Z轴位置的计算参数;Off Screen Position Delta:移出屏幕范围的隐藏方式中,使用的位置偏移数值。
调用UIManager.(IUILoader uiLoader, IUILoadingOverlay loadingOverlay, bool useLogicCache)方法来初始化其所需要的外部功能。
UI管理器中定义了IUILoader接口,项目需要实现其中的方法。
public interface IUILoader {
ParametersForUI GetParameterForUI(string id);
UniTask<GameObject> LoadUIObject(string path);
void UnloadUIObject(GameObject go);
}其中:
IUILoadingOverlay接口定义:
public interface IUILoadingOverlay {
void BeginLoading(string key);
void EndLoading(string key);
}界面逻辑的实例可以重复使用,以减少GC Alloc。使用界面逻辑实例缓存时,应注意:
- 界面逻辑应在各生命周期结束的方法中,清理相应成员变量、注册侦听及各种状态。
- 将项目的界面逻辑实例改为使用缓存后,可能因已存在界面逻辑代码中存在上述问题而导致在第二次或以后开启同个界面时,存在各种不可预知的错误。
- 对内存和GC不敏感的项目,建议不要开启,以降低界面逻辑出错的可能。
UI管理器对UI逻辑类的要求为:
- 实现
IUILogicStack或IUILogicFixed接口; - 应包含public无参构造函数。
此设计主要是为了方便项目对UI逻辑类的定制:
- 可根据项目的实际情况,自行设计UI逻辑类的继承关系;
- 方便项目对界面生命周期事件的封装、响应和二次委派处理。
推荐做法:
以实现IUILogicBase中VisibleOperateType属性和OnCreate()方法为例,堆叠型和固定层级界面的共用基类的基类UILogic的写法为:
public abstract class UILogicBase : IUILogicBase {
...
protected virtual eUIVisibleOperateType VisibleOperateType { get { return eUIVisibleOperateType.LayerMask; } }
...
protected virtual bool OnCreate(object para) { return true; }
...
eUIVisibleOperateType IUILogicBase.VisibleOperateType { get { return VisibleOperateType; } }
bool IUILogicBase.OnCreate(object para) {
// do something ...
return OnCreate(para);
}
}通过显式实现接口与重新定义同名虚方法/虚属性的方式,使得:
- 界面逻辑类可以按需要来override相应的生命周期方法和界面展示参数。
- 实现子类可响应生命周期事件的同时,基类也可根据项目需要,在显示实现接口的方法中进行相应处理,而子类在override方法或属性时无需关心是否需要调用
base。
相应的固定层级界面的基类:
public abstract class UIFixedLogicBase : UILogicBase, IUILogicFixed {
protected abstract int SortingOrderBias { get; }
int IUILogicFixed.SortingOrderBias { get { return SortingOrderBias; } }
}堆叠型界面的基类:
public abstract class UIStackLogicBase : UILogicBase, IUILogicStack {
protected virtual bool AllowMultiple { get { return false; } }
protected abstract bool IsFullScreen { get; }
protected abstract bool NewGroup { get; }
protected virtual void OnGetFocus() { }
protected virtual bool OnESC() { return true; }
protected virtual void OnLoseFocus() { }
bool IUILogicStack.AllowMultiple { get { return AllowMultiple; } }
bool IUILogicStack.IsFullScreen { get { return IsFullScreen; } }
bool IUILogicStack.NewGroup { get { return NewGroup; } }
void IUIFocusable.OnGetFocus() { OnGetFocus(); }
void IUIFocusable.OnLoseFocus() { OnLoseFocus(); }
bool IUIFocusable.OnESC() { return OnESC(); }
}参考初始界面逻辑代码生成工具。
特点:
- 显示在几乎所有其他UI之上;
- 整个项目内部通用;
- 通常是类似安卓Toast、有指向的气泡这类形式。
推荐做法:
- 使用
SortingOrderBias值为负数(高层级)的固定层级界面。 - 在此界面中建立数个各种形式提示的模板。
- 界面逻辑根据需要,管理并创建模板实例,并填充需要展示的内容。
- 根据实际项目情况,选取事件、注册Agent等方式封装面向逻辑的功能接口。
特点:
- 需要在加载其他内容前,完成"进度条界面"的展示;
- 在"进度条界面"展示后,打开的界面应显示在进度条界面以下的更低层级的位置。
推荐做法:
- 使用
SortingOrderBias值为负数(高层级)的固定层级界面。 - 在打开"加载进度条界面"时,侦听其生命周期中的
OnOpened事件。 - 在"加载进度条界面"完成展示的响应中,开始加载其他内容。
- 在其他内容加载过程中,通过项目中的事件/数据驱动系统实时更新加载进度。
- 在其他内容加载完成后,关闭"加载进度条界面"。
此实现需要借助此UI管理器方案的数个特性:
- 借助用于界面开启的参数来指定界面节点资源和界面逻辑类型。
- 界面逻辑的生命周期方法中,
OnOpen(GameObject ui)只给出了界面节点的gameObject实例,界面逻辑可以使用任何合理的方法来操控界面节点。
对于同一逻辑代码用于多个界面这个需求,在开启界面的API中使用界面开启的参数将多个id不同prefab_path不同的界面,使用同一个logic_type即可。
但需要注意:
- 避免多个界面的prefab差异过大;
- 此
logic_type指定的界面逻辑类,应尽量保证对多个相应的界面prefab的兼容性。