游戏开发论坛

 找回密码
 立即注册
搜索
查看: 7112|回复: 1

浅谈使用NGUI的界面架构:功能介绍及NData

[复制链接]

1万

主题

1万

帖子

3万

积分

论坛元老

Rank: 8Rank: 8

积分
36572
发表于 2016-8-19 18:48:50 | 显示全部楼层 |阅读模式
140120xuqe8zs8fyjauuef.jpg

  文/kUANG tOBY(匡正)

  版权所有,转载须注明出处以及作者,Email:kuangtoby@163.com

  在我的印象中,Unity一直没有一套成熟的界面体系。现在可供选择的不外乎NGUI和UGUI,之前也用过EZGUI和2D ToolKit,但最后我还是选择了NGUI。

  很多人说NGUI不好用。我的感觉是,一个工具,只要你对它足够熟悉,就一定有一套最适合它的使用方法。当然每个工具都一定有它的无法回避的缺点和硬伤,但大部分人只是使用方式不对罢了,还没到受工具局限性影响的阶段。

  选择NGUI作为主要的界面工具主要是基于以下考虑:

  • 有较多的文档,新人容易上手
  • 功能比较全,一个手游需要的界面功能基本都有
  • 有个MVVM工具NData可以和NGUI配合使用,这样可以极大地提高开发效率,适应需求修改。这个后面会详细介绍。


  在设计界面架构的时候,我主要想实现以下几个目的:

  • 把游戏的各个界面模块集中管理,统一调度,但又必须把每个模块之间的耦合性降到最低。这样可以不同的人开发不同的模块,互不干扰,做出来的东西运行起来又不会互相冲突。
  • 把界面逻辑和界面版式尽量分离开来,让美术也可以参与界面的修改(事实证明,这个想法最后救了程序的命)。
  • 界面模块的开发必须有一套统一的流程,统一的格式,方便不同的人维护。


  在具体设计之前,首先要了解NGUI的局限性。

  NGUI有很多缺点,最受人诟病的就是性能问题和内存消耗问题。这两个问题都和NGUI的底层渲染机制有关。NGUI的渲染基于每一帧的Mesh重建,把一个UIPanel下的多个使用相同图集的UIWidget合并成一个Mesh,以此来减少draw call。NGUI本身对此做了优化,即如果一个UIPanel下的内容没有变化,就使用缓存的Mesh。但界面往往是不停变化的,这样就不可避免的每一帧都要重建Mesh,从而造成CPU的负担和多余的内存消耗。解决的方法就是把要经常变化和移动的界面放在单独的UIPanel下,去移动和变化UIPanel依附的物体。这样虽然会增加draw call,但节省了重建Mesh的性能损耗。

  因为这个问题,我把游戏模块分成一个个不同的页面,每个页面都有一个UIPanel,然后在一个统一的地方调度各个页面,这个统一的地方是一个单例类,叫做MainPageMgr。

  每个页面都有一个生命周期,即

  • 出现:准备出现->播放出现动画->动画完毕,展示在目标位置
  • 消失:准备消失->播放消失动画->完全消失


  其中出现动画和消失动画由NGUI自带的UIPlayTween组件控制,直接把TweenPosition等脚本贴在UIPanel所在的物体上,让技术美术去调整。我把这一系列动作的逻辑都放在一个TweenPage类里,只要调一个弹出或消失的方法,就让它自动运行这个流程。

  TweenPage类中有一个Bring(Boolean isBringIn)方法提供给MainPageMgr调用:
  • Bring(true)出现
  • Bring(false) 消失


  当MainPageMgr调用一个页面TweenPage的Bring方法时,页面就按照它的生命周期开始运动,并触发相应的回调方法。

  TweenPage类中有以下回调接口,供具体的页面实现相关逻辑:

  • OnPreBringIn 准备弹出的回调
  • OnBringIn 播放完弹出动画的回调
  • OnPreBringOut 准备播放消失动画的回调
  • OnBringOut 完全消失的回调


  这样,只需要在具体的页面类中重写这几个接口,就可以在这几个时间点做一些事情。例如在OnPreBring接口中实现页面的数据刷新。

  自此,页面TweenPage的框架基本成型,然后是实现MainPageMgr的统一调度。

  我的做法是把每个页面的TweenPage实例都添加到MainPageMgr中,然后在MainPageMgr中为每个TweenPage实例都提供一个弹出方法,如弹出或关闭英雄管理界面BringPageHero(Boolean isBringIn)。这个弹出方法可以根据不同页面的情况设置不同的参数,但都有一个Boolean值表示是让页面出现还是消失,并且都要调用MainPageMgr的BringPage(TweenPage page)方法。

  BringPage(TweenPage page)方法主要实现页面的统一调度管理,例如把一个页面显示到屏幕最前面,挡住其他所有页面。

  在MainPageMgr中实现List<TweenPage> pageList,用来保存当前已经打开的页面。我把页面设计成叠加遮挡模式,即一个页面弹出会叠在前一个页面上面并挡住它。pageList中就按顺序保存正处于打开状态的页面TweenPage实例。每次打开一个页面,都会给pageList中的所有TweenPage的UIPanel设置一个新的深度,并把当前已经打开的所有页面GameObject的Z轴往前移,这样可以确保夹在两个UIPanel之间的3D模型(如角色模型)显示正常。最后把要打开的页面加入到pageList中,并给它的UIPanel赋值一个当前最大的深度,使其可以遮挡前面所有的页面。让页面消失就使用相反的操作。这些操作都是用MainPageMgr的方法BringPage(Boolean isBringIn)实现。

  界面的调度逻辑基本就是这样,现在要加一个新的页面只需要新建一个继承TweenPage的类,贴上UIPanel和其他UIPlayTween组件,并在MainPageMgr中添加一个弹出方法即可。

  这里只是提供一个大致的思路,细节就不再深入说明了。

  关于NData

  NData是三年前我无意中发现的一个界面插件,这个插件开启了我做U3D界面的新篇章,刚开始用的那个感觉,就好像写JSP的人突然用上了SSH。虽然这个插件的作者已经停止更新了,但他的MVVM思想非常值得借鉴。

  在刚接触NGUI的时候,我们一般会采用在脚本中获取NGUI 组件的形式给NGUI 组件赋值。有两种选择:一种是在代码中根据路径获取NGUI 组件;另一种是在场景中,直接把组件拖到脚本上。第一种方法的的缺点是需要维护NGUI组件的路径,第二种方法的缺点是替换组件时总是需要重新拖组件。两种方法都比较不方便,这里用第二种来举例。

  打个比方,我们界面右上角要显示玩家拥有的金币总数。于是我们做了个UILabel拖进脚本,在脚本里给它的text赋值显示当前金币数量。

1.png

  PagePlayer.cs中:

  1.   public UILabel goldLabel;
  2.     public void SetGold( string gold )
  3.     {
  4.         goldLabel.text = gold;
  5.     }
复制代码

  后来策划需求在左下角和右下角也要显示金币,于是我们又做了两个uilabel放到相应位置,并在脚本里添加变量,把新加的uilabel拖到脚本里。每次金币的值发生变化,就要找到所有的uilabel变量给他们一一赋值。

2.png

  PagePlayer.cs中:

  1. public UILabel goldLabel;
  2.     public UILabel goldLabel1;
  3.     public UILabel goldLabel2;
  4.     public void SetGold( string gold )
  5.     {
  6.         goldLabel.text = gold;
  7.         goldLabel1.text = gold;
  8.         goldLabel2.text = gold;
  9.     }

  10. 作者:kUANG tOBY
  11. 链接:https://zhuanlan.zhihu.com/p/21306120
  12. 来源:知乎
  13. 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
复制代码

  然而可能以后又会增加其它显示金币的界面,每加一个金币的UILabel,就要去脚本里增加一个UILabel 的变量然后在金币变化的时候给它赋值。虽然麻烦,但也能把功能做出来。我们不能就此满足,偷懒是提高生产力的最大动力。

  现在想要简化这个流程,就要实现以下功能。

  1,不需要每次添加金币文字的时候都在脚本中新增一个UILabel变量,并把对应的UILabel组件拖进来。

  2,不需要每次修改金币的时候都要找到所有的金币UILabel变量去修改他们的值。

  初步的解决方案是这样子的:我希望脚本里面有一个值 gold代表的是金币数量。所有的金币UILabel 都跟这个值产生关联。只要修改这个值,所有跟他关联的UILabel 都自动发生变化。另外,在我要添加一个金币UILabel 的时候,我希望它能自动去找页面脚本中的gold变量来发生关联,而不需要我在脚本中改代码。

  具体实现的思路,就是在带有UILabel脚本的物体上加一个脚本,使其与页面脚本种的gold变量发生关联。然后给gold变量加set方法,在这个方法中发一个消息,告知所有和gold有过关联的的UILabel要发生值的改变。这样每次给gold变量赋值的时候,所有与其关联的UILabel就会自动更新显示的内容。

  本着不重复造轮子的原则,在疑似开始造轮子之前一定要Google一下。于是在网上搜出了MVVM模式,NData插件等等。并发现NData不仅可以用于UILabel,还可以用于各种NGUI组件,并有很好的绑定层级管理。

  NData就是基于MVVM模式,其中用户自定义继承EZData.Context的类,就相当于是自定义ViewModel层的内容。

  剩下的问题就是,怎样用这个工具来管理页面。

  根据上文,我把界面分成很多个TweenPage,然后在单例MainPageMgr中统一管理。对于数据,我希望把每个页面的数据也独立出来,即每个页面有一个对应的继承EZData.Context的类,这个页面相关的数据都放在这个类中,然后再由MainPageMgr来统一管理。

  例如有个游戏页面PageInGame,用来显示游戏中获得的金币,钻石和星星。现在新建一个PageInGameContext继承EZData.Context。现在PageInGame页面就有一个model层PageInGame类和一个ViewModel层PageInGameContext类。View层自然就是PageInGame物体下面的NGUI组件了。这样就形成了MVVM模式。

  1. using UnityEngine;
  2. using System.Collections;

  3. public class PageInGameContext : EZData.Context
  4. {
  5.     #region Property Gold

  6.     private readonly EZData.Property<int> _privateGoldProperty = new EZData.Property<int> ();

  7.     public EZData.Property<int> GoldProperty { get { return _privateGoldProperty; } }

  8.     public int Gold {
  9.         get    { return GoldProperty.GetValue (); }
  10.         set    { GoldProperty.SetValue (value); }
  11.     }

  12.     #endregion


  13.     #region Property Diamond

  14.     private readonly EZData.Property<int> _privateDiamondProperty = new EZData.Property<int> ();

  15.     public EZData.Property<int> DiamondProperty { get { return _privateDiamondProperty; } }

  16.     public int Diamond {
  17.         get    { return DiamondProperty.GetValue (); }
  18.         set    { DiamondProperty.SetValue (value); }
  19.     }

  20.     #endregion

  21.     #region Property Star

  22.     private readonly EZData.Property<int> _privateStarProperty = new EZData.Property<int> ();

  23.     public EZData.Property<int> StarProperty { get { return _privateStarProperty; } }

  24.     public int Star {
  25.         get    { return StarProperty.GetValue (); }
  26.         set    { StarProperty.SetValue (value); }
  27.     }

  28.     #endregion

  29. }


  30. public class PageInGame : TweenPage {

  31.     public PageInGameContext Context;

  32.   

  33.     protected override void Awake ()
  34.     {
  35.         base.Awake ();

  36.         MainPageMgr.instance.Context.pageInGame = this;
  37.         Context = MainPageMgr.instance.Context.pageInGameCtx;

  38.     }



  39.     protected override void OnPreBringIn ()
  40.     {
  41.         base.OnPreBringIn ();
  42.       

  43.     }

  44.     protected override void OnPreBringOut ()
  45.     {
  46.         base.OnPreBringOut ();
  47.       
  48.     }
  49. }
复制代码

  在MainPageMgr中有一个MainView Context是用来管理所有页面的Context(下文中继承EZData.Context的类,都统称为Context。):

  1. public class MainPageMgr : PageMgrSingleton<MainPageMgr>
  2. {

  3.     public NguiRootContext View;
  4.     //这个代表页面模型
  5.     public MainView Context;



  6.     void Awake()
  7.     {
  8.       
  9.         Context = new MainView();
  10.         SetContext();


  11.     }

  12.     public void SetContext()
  13.     {
  14.         View.SetContext(Context);
  15.     }
  16. }

复制代码

  MainView.cs

  这样,所有的页面都可以通过MainPageMgr.instance.Context来获取所有页面的Context,如pageXXXContext,也可以获得所有页面的逻辑脚本,如pagXXX。

  在场景里,只需要把页面放在MainPageMgr的下级,然后再通过Master path来绑定到MainPageMgr中的Context就可以了。

3.png

4.png

  在开发中,可能出现不同的页面共用相同的数据,这种情况就可以直接把两个页面的Master Path绑定到同一个Context上,这样开发起来会方便很多。

  总结:

  引入NData这个插件,主要是为了减少一些对NGUI组件的操作(如获取组件和赋值等),把所有的工作都简化为改变Context中的值,来动态改变NGUI组件的显示。把各个页面的Context都统一管理,是为了更方便地获取数据,但原则上不应该在A页面的model层中去修改B页面Context中的数据,因为这样容易造成混乱。

  使用NData加NGUI,可以很快速地搭建一套页面框架。现在我已经把这两个工具专门打成插件包,开发新项目时直接导进去用,非常方便。

  相关阅读基于 Unity 中的 NGUI 插件,通用的 UI 如何设计

0

主题

1

帖子

8

积分

新手上路

Rank: 1

积分
8
发表于 2016-9-10 14:31:19 | 显示全部楼层
楼主您好,有没有demo源码让小弟学习学习呢,不胜感激
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

作品发布|文章投稿|广告合作|关于本站|游戏开发论坛 ( 闽ICP备17032699号-3 )

GMT+8, 2025-2-25 05:27

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

快速回复 返回顶部 返回列表