《Unity3D高级编程之进阶主程》第四章,UI(六) - 如何架构UI框架
回顾下,前面两章着重对 UGUI 的源码进行的剖析,包括事件系统的模块和底层渲染模块以及渲染组件。这篇我们来讲讲,如何在Unity3D游戏项目中架构UI框架。
===
快速架构一个简单易用的UI框架
在前面架构章节中,我们讲述了架构的需要注意的特性,以及设计架构时所使用的抽象方法。我们在经历几个项目后,会总结所有经历过的这些项目的经验,这些经验很好的支撑了构建架构的基础。
我们从宏观的角度看UI框架。
只有从宏观的角度看问题,才能看的更明白。我们项目中拥有众多UI界面,我们要统一管理所有UI,这样才能使得每个UI界面都能得到有效的调配。不仅如此,如果每个UI界面都是可扩充的那就太棒了。UI有一个很关键的系统是输入事件系统,UI内的每个按钮都需要有一个处理输入的句柄。所以我们需要写一个统计的管理类,以及每个UI都要有统一的基类,并且每个UI按钮元素都对应一个处理输入的句柄。另外对于UI来说,有通用UI,也有非通用UI,有常用UI和非常用UI之分。接下来我们把细节规划一下。
Ⅰ.管理类。
整个UI是由N个界面构成的。这些UI界面有基本的功能,生成,展示,销毁,查找。如果说,我们分别对N个UI界面的这些功能进行编程,就会有大量的工作产生,而且维护起来的工作量也是巨大的。
我们需要用一个单例实例来管理所有的UI界面,让他们能有统一的接口进行以上的活动,创建UI管理类是最好的选择,我们可以命名它为 UIManager,这个名字符合它代表的功能。
那么 UIManager 具体里面要做些什么呢。它需要创建UI,需要查找现有的某个UI,以及需要销毁UI,以及一些UI的统一接口调用和调配工作。UIManager 承担了所有UI的管理工作,因此UI在生成出来后的实例都将存储在这里。不仅如此,一些UI常用变量也存储在里面,比如屏幕的适配标准大小,比如UI的Camera实例等等。
这样一来,第一个方向确定了,那就是UIManager是UI界面的管理员,统筹管理UI问题。它包括了UI的众多统筹需求,比如下层UI切换到上层,比如加载方式变更,比如选择性预加载UI等,都需要在UIManager里编写。
public class ScreenManager : CSingleton<ScreenManager>
{
protected Transform _transform = null;
private Dictionary<string, UIScreenBase> _DicScreens = new Dictionary<string, UIScreenBase>();
// 关闭所有界面
public void CloseAll()
{
...
}
// 是否UI正打开
public bool IsShow(string screenID)
{
...
}
// 关闭界面
public void CloseScreen(UIScreenBase screen)
{
...
}
// 创建所有界面
public T CreateMenu<T>() where T : UIScreenBase
{
...
}
// 找出某个界面
public T FindMenu<T>() where T : UIScreenBase
{
...
}
...
}
Ⅱ.基类。
项目中有很多界面,这N个界面他们有自己的共性,比如最基本的,他们都需要进行初始化,他们都需要有展示接口,他们都可以关闭,共性产生统一特征的接口,Init,Open和Close。继承基类又使得管理起来比较方便,在上面提到的 UIManager 里存储的UI实例时,可以统一使用基类的方式存储。我们可以把基类的名字称为 UIScreenBase,每个UI界面都继承自它,Screen一词很形象贴切的描述了屏幕上显示的界面。
我们将所有UI都定义为基类的子类,对有需要做特殊处理的UI界面,可以重写Init,Open和Close。为了能更方便的知道UI的状态,我们也可以定义一个UI状态,比如OpenState为打开状态,CloseState为关闭状态,HidenState为隐藏状态,PreopenState为预加载状态,以状态的形式来判断UI现在的情况。
到这里,我们的每个界面有了基类,自己成为了扩展界面功能的一个类实体,可以自主定义自己的功能性的接口,同时还会受到管理类的统一调配。做到了,既满足有序管理,又能满足自定义需求。看似简单的几行代码,里面蕴含着复杂的思考过程,抽象的意义就在于此。
public abstract class UIScreenBase : MonoBehaviour
{
protected bool mInitialized = false;
protected UIState mState = UIState.None;
public UIState State { get { return mState; } }
public delegate void OnScreenHandlerEventHandler(UIScreenBase screen);
public event OnScreenHandlerEventHandler onCloseScreen;
// 初始化
protected virtual void Init()
{
mInitialized = true;
}
//打开
public virtual void Open() {}
//关闭
public virtual void Close() {}
}
Ⅲ.输入事件响应机制。
UI中输入事件的响应机制比较重要,好的输入事件响应机制能提高更多的效率,让程序员编写逻辑的时候更加舒服。
Unity3D的UGUI输入事件响应机制建立通常有2种,一种是继承型,一种是注册型。
继承型是指事件先响应到基类,再由基类反应给父类,由父类做处理,这样UI既可以得到对输入事件的响应,也可以自行修改自己需要的逻辑。比如我们写了个处理事件的基类组件UIEventBase是父类能接受各种输入事件响应,UIEventButton是继承UIEventBase的子类,当输入事件传入时UIEventButton能做出响应,因为它继承了父类。
绑定型是指在对输入事件响应之前,我们对UI元素绑定一个事件响应的组件。比如编写一个绑定型事件类 UIEvent,当某个UI元素需要输入事件回调时,对这个物体加绑一个 UIEvent,并且对 UIEvent 里需要的相关响应事件进行赋值或注册操作函数。当输入事件响应时,由 UIEvent 来区分输入的是什么类型的事件,再分别调用响应到具体函数。
继承型和绑定型都有一个共同的特点,都需要与UI元素关联,区别是继承型融入在了各种组件内,而绑定型以独立的组件形式体现。
继承型UI事件输入响应机制需要关联到组件内,UGUI和NGUI都已经有了自己的基础的组件,所以很难在这上面使用,而在另一些比较特殊的GUI系统内可以很好的适应。比如我曾经做过一个项目,我们构建的一套新的UI系统的完全独立于UGUI和NGUI的GUI系统之外,我们将输入事件处理注入到这个系统的各个组件内,达到了输入事件处理与组件融合的效果。
绑定型的方式更适合在已经建立了GUI系统的基础上,对输入事件进行封装处理。通常在UGUI和NGUI上都会使用绑定型对输入事件处理进行封装。
例如,在UI初始化中,对需要输入事件响应的,绑定一个事件处理类,比如命名为 UIEvent,然后对事件句柄进行赋值,例如,ui_event.onclick = OnClickLogin,OnClickLogin就是响应登录按钮的事件句柄。
这样的赋值方式,让程序员写逻辑时看起来更加清爽,简洁,直观。
/// <summary>
/// UI 事件
/// </summary>
public class UI_Event : UnityEngine.EventSystems.EventTrigger
{
protected const float CLICK_INTERVAL_TIME = 0.2f; //const click interval time
protected const float CLICK_INTERVAL_POS = 2; //const click interval pos
public delegate void PointerEventDelegate ( PointerEventData eventData , UI_Event ev);
public delegate void BaseEventDelegate ( BaseEventData eventData , UI_Event ev);
public delegate void AxisEventDelegate ( AxisEventData eventData , UI_Event ev);
public Dictionary<string,object> mArg = new Dictionary<string,object>();
public BaseEventDelegate onDeselect = null;
public PointerEventDelegate onBeginDrag = null;
public PointerEventDelegate onDrag = null;
public PointerEventDelegate onEndDrag = null;
public PointerEventDelegate onDrop = null;
public AxisEventDelegate onMove = null;
public PointerEventDelegate onClick = null;
public PointerEventDelegate onDown = null;
public PointerEventDelegate onEnter = null;
public PointerEventDelegate onExit = null;
public PointerEventDelegate onUp = null;
public PointerEventDelegate onScroll = null;
public BaseEventDelegate onSelect = null;
public BaseEventDelegate onUpdateSelect = null;
public BaseEventDelegate onCancel = null;
public PointerEventDelegate onInitializePotentialDrag = null;
public BaseEventDelegate onSubmit = null;
private static PointerEventData mPointData = null;
// 设置参数
public void SetData(string key , object val)
{
mArg[key] = val;
}
// 获取参数
public D GetData<D>(string key)
{
if(mArg.ContainsKey(key))
{
return (D)mArg[key];
}
return default(D);
}
...
public static UI_Event Get(GameObject go)
{
UI_Event listener = go.GetComponent<UI_Event>();
if (listener == null) listener = go.AddComponent<UI_Event>();
return listener;
}
public override void OnBeginDrag( PointerEventData eventData ) { ... }
public override void OnDrag( PointerEventData eventData ) { ... }
public override void OnEndDrag( PointerEventData eventData ) { ... }
public override void OnDrop( PointerEventData eventData ) { ... }
public override void OnMove( AxisEventData eventData ) { ... }
public override void OnPointerClick(PointerEventData eventData)
{
...
if(onClick != null)
{
onClick(eventData , this);
}
...
}
public override void OnPointerDown (PointerEventData eventData) { ... }
public override void OnPointerEnter (PointerEventData eventData) { ... }
public override void OnPointerExit (PointerEventData eventData) { ... }
public override void OnPointerUp (PointerEventData eventData) { ... }
public override void OnScroll( PointerEventData eventData ) { ... }
}
如上代码,篇幅有限,我把事件部分最重要的部分摘了出来,组件的挂在,事件的调用,以及参数的设置。
到这里我们有了统一管理UI的管理类,有了界面的基类,有了处理输入事件句柄的事件类,就能开始拓展UI了,大部分UI界面我们都能够处理,但很多原生的组件用起来不是很好,效率也特别的差,所以我们需要构建自己的高效的UI自定义组件。
Ⅳ.自定义组件。
除了NGUI和UGUI本身的组件外,我们自己的自定义组件是必不可少的,特别是游戏项目,无论大小,都需要有自己的自定义组件,自定义组件不仅能让程序员在写逻辑时快速上手,满足项目的设计需求,而且也能起到对UI优化的作用,尤其在元素多的组件内。
下面介绍项目中最常改造的组件:
① UI动画组件。
动画在UI中扮演重要的角色,这里主要说的是Animation的K线动画。
如何让Animation在美术人员手里自如的制作,并且让程序员能方便调用是关键。
UI动画组件里应该有什么呢?我们暂时命名为 UIAnimation 好了。
首先它肯定要依赖 Unity3D 的 Animator 组件 [RequireComponent (typeof(Animator))]。
其次它要有播放(Play)接口用来播放指定动画,Play的参数包括,动画名,播放完毕后的回调函数委托。
再次他可以在无需程序调用的情况下自动播放,因此在 public 变量中需要 AutoPlay 这个参数,这样美术人员就可以在 Unity3D 界面上设置自动播放而无需程序调用了。
最后美术人员需要在自动播放时选择指定的动画名和是否循环播放,以及循环播放间隔。
这样就基本成形了,接下来要做的事就是我们对抽象的 UIAnimation 里完善以上的功能。
② 按钮播放音效组件。
在点击按钮时会需要播放音效,这是每个项目必要的组件。
功能也挺简单,当输入事件触发Click事件时发出绑定的声音文件就可以了。
不过很多项目用到的音效系统并不是Unity3D原生态的音效系统,需要自己为这些系统定制组件。
③ UI跟随3D物体组件。
项目中很多时候需要UI元素来跟随它们,比如游戏中的血条,又比如场景中建筑物头上的标志等等,因此UI跟随3D物体的组件非常必要。
它的功能实现起来也挺简单的,不断地计算3D物体在屏幕中的位置,来确定UI位置,并且在前后位置不同时再进行更改以避免不必要的移动。
④ 无限滚动页面组件。
在滚动的菜单栏里,通常类似于游戏中的背包界面,如果有几百个UI元素同时生成,或同时滚动时,效率会非常低,因为UI在每帧都需要重新构建Mesh,每一次的滚动都会引起不小的CPU消耗。
因此一个自定义的无限滚动页面组件来,替换原来的模式,让CPU花最小的代价来运行这个滚动页面是非常有必要的。
那么这个无限滚动页面组件关键点在哪呢?设想下,这么多UI元素一起生成,一起移动,都是一件很费力的事,我们需要减少UI元素的数量。
最好减少到与在屏幕上显示的数量差不多,利用看不见的UI元素,来补充能看见的元素,可以描述为一个把上下UI元素不可见时的再利用过程。
我们就拿游戏里的背包界面来举例吧,500个物品在背包界面中时,实例化,初始化,滚动都会很费劲,我们可以减少UI元素在背包界面里的显示数量。
当UI元素滚动时一部分元素被遮挡住时,不再需要他们显示了,这时我们就可以对这些元素进行再利用。
当上面有一行元素被遮挡住,可以被再利用时,我们就把他们移动到下面去,让他们变成下面的背包物品元素。
这样不断得滚动,在表现上跟真的有500个物品滚动过程一模一样。这样就可以大量地削减组件消耗的CPU,不管有多少物品在背包里面,也不会引起CPU的负担了。
⑤ 其他组件。
其他组件,比如美术数字组件,让美术制定的数字展示得更好,又比如暴击数字是特殊的图片数字等。又比如计数组件,可以让数字滚动的更加漂亮,又比如在获得游戏币时数字会像动画一样跳动由慢到快。
再比如,针对UGUI改变颜色动画时过于消耗CPU而设计的优化组件,让动画只改变组件的颜色值,由组件来改变UI元素的材质球颜色,这样能省去很多重构Mesh导致的CPU消耗。
编写自定义的UI组件的目标就是,增加更多通用的组件,减少重复劳动,让程序员在编写UI界面时更加快捷高效,同时也提升了UI的运行效率。拥有属于自己的一套自定义套件,对项目来说也是非常有价值和高效的一件事。
感谢您的耐心阅读
Thanks for your reading
版权申明
本文为博主原创文章,未经允许不得转载:
《Unity3D高级编程之进阶主程》第四章,UI(六) - 如何架构UI框架
Copyright attention
Please don't reprint without authorize.
微信公众号,文章同步推送,致力于分享一个资深程序员在北上广深拼搏中对世界的理解
QQ交流群: 777859752 (高级程序书友会)