Unity3D-深入剖析NGUI的游戏UI架构

Unity3D-NGUI分析,使用NGUI做UI需要注意的几个要点在此我想罗列一下,对我在U3D上做UI的一些总结,最后解剖一下NGUI的源代码,它是如果架构和运作的。

在此前我介绍了自己项目的架构方式,所以在NGUI的利用上也是同样的做法,UI逻辑的程序不被绑定在物体上。那么如何做到GUI输入消息的传递 呢,答案是:我封装了一个关于NGUI输入消息的类,由于NGUI的输入消息传递方式是U3D中的SendMessage方式,所以在每个需要接入输入的 物体上动态的绑定该封装脚本。在这个消息封装类中,加入消息传递的委托方法后,所有关于该物体的输入消息将通过封装类直接传递到方法上,再通过消息类型的 识别就可以脱离传统脚本绑定的束缚了。源码地址:GUIComponentEvent

在用NGUI制作UI时需要注意的几点:

1.每个GUI以1各UIPanel为标准,过多的UIPanel首先会导致DrawCall的增多,其次是导致UI逻辑的混乱。

2.UITexture不能使用的过于平凡,因为每个UITexture都会增加1各DrawCall,所以一般会作为背景图出现在UI上,小背景,大背景都可以。

3.图集不宜过大,过大的图集,不要把很多个GUI都放在一个图集里,在UI显示时加载资源IO速度会非常慢。我尝试了各种方式来管理图集,例如每 个GUI一个图集,大雨300*100宽度的图不做图集,抑或一个系统模块2个图集,甚至我有尝试过以整个游戏为单位划分公共图集,按钮图集,头像图集, 问题图集,但这种方式最终以图集过大IO过慢而放弃,这些图集的管理方式都是应项目而适应的,并没有固定的方式,最主要是你怎么理解程序读取资源时的IO 操作时间。

4.在开发中,尽量用Free分辨率来测试项目的适配效果,不要到上线才发现适配问题。

适配源码:

  1. float defaultWHRate = 800f / 480f;

  2. float ScreenWHRate = (float)Screen.width / (float)Screen.height;

  3. bool isUseHResize = defaultWHRate >= ScreenWHRate ? false : true;

  4. UIRoot root = GameObject.Find(“ROOT”).GetComponent<UIRoot>();

  5. if (!isUseHResize)

  6. {

  7.     float curScreenH = (float)Screen.width / defaultWHRate;

  8.     float Hrate = curScreenH / Screen.height;

  9.     root.manualHeight =(int)(480f / Hrate);

  10. }

  11. else

  12. {

  13.     root.manualHeight = 480;

  14. }

5.拆分以及固定各个锚点,上,左上,右上,中,左中,右中,下,左下,右下

6.拆分GUI层级,层级越高,显示越靠前。层级的正确拆分能有效管理GUI的显示方式。

  1. /// <summary>

  2. /// GUI层级

  3. /// </summary>

  4. public enum GUILAYER

  5. {

  6.     GUI_BACKGROUND = 0, //背景层

  7.     GUI_MENU,           //菜单层0

  8.     GUI_MENU1,           //菜单层1

  9.     GUI_PANEL,          //面板层

  10.     GUI_PANEL1,         //面板1层

  11.     GUI_PANEL2,         //面板2层

  12.     GUI_PANEL3,         //面板3层

  13.     GUI_FULL,           //满屏层

  14.     GUI_MESSAGE,        //消息层

  15.     GUI_MESSAGE1,        //消息层

  16.     GUI_GUIDE,           //引导层

  17.     GUI_LOADING,        //加载层

  18. }

8.要充分的管理GUI,不然过多的GUI会导致内存加速增长,而每次都销毁不用的GUI则会让IO过于频繁降低运行速度。我的方法是找到两者间的 中间态,给予隐藏的GUI一个缓冲带,当每次某各GUI进行隐藏时判断是否有需要销毁的GUI。或者也可以这么做,每时每刻去监控隐藏的GUI,哪些 GUI内存时间驻留过长就销毁。关于内存优化问题,可以参考《unity3d-texture图片空间和内存占用分析》《unity3d优化之路》

9.另外关于图标,像头像,物品,数量过多的,可以用打成几个图集,按一定规则进行排列,减小文件大小减少一次性读取的IO时间。

10.尽量减少不必要的UI更改,NGUI一旦有UI进行更改,它就得重新绘制MESH和贴图,比起cocos2d耗得CPU大的多。

11.如果可以不用动态字体就不要用动态字体,因为动态字体每次都会做IO操作读取相应的图片,这个是NGUI一个问题,费cpu,费内存。

12.设置脚本执行次序,在U3D的Project setting->Script Execution Order 中。由于NGUI以UIPanel为主要渲染入口,所以,所有关于游戏渲染处理的程序最好放在渲染之后,也就是UIPanel之后。UIPanel以 LateUpdate为接口入口,所以关于渲染方面的程序还得斟酌是否方在LateUpdate里。

13.NGUI对于动态的移动旋转等的UI操作支持性很差,当有这种操作过多的时候,会使得屏幕很卡。解决办法就是,自己用程序生成面片,面片的渲染不再受到NGUI的控制。

 

以上是我能想起来的注意点,若有没想起来的,在以后的时间想到的也将补充进去。口无遮拦的说了这么多,不剖析一下源码怎么说的过去,之前对NGUI输入消息进行了封装,对2D动画序列帧进行了封装,却一直没能完整剖析它的底层源码,着实遗憾。

NGUI中UIPanel是渲染的关键,他承载了在他下面的子物体的所有渲染工作, 每个渲染元素都是由UIWidget继承而来,每个UI物体的渲染都是由面片、材质球、UV点组成,每个种材质由一个UIDrawCall完成渲染工 作,UIDrawCall中自己创建Mesh和MeshRender来进行统一的渲染工作。这些都是对NGUI底层的简单的介绍,下面将进行更加细致的分 析。

首先我们来看UIWidget这个组件基类,从它拥有的类内部变量就能知道它承担得怎样的责任:

  1. // Cached and saved values

  2. [HideInInspector][SerializeField] protected Material mMat;//材质

  3. [HideInInspector][SerializeField] protected Texture mTex;//贴图

  4. [HideInInspector][SerializeField] Color mColor = Color.white;//颜色

  5. [HideInInspector][SerializeField] Pivot mPivot = Pivot.Center;//对齐位置

  6. [HideInInspector][SerializeField] int mDepth = 0;//深度

  7. protected Transform mTrans;//坐标转换

  8. protected UIPanel mPanel;//相应的UIPanel


  9. protected bool mChanged = true;//是否更改

  10. protected bool mPlayMode = true;//模式


  11. Vector3 mDiffPos;//位置差异

  12. Quaternion mDiffRot;//旋转差异

  13. Vector3 mDiffScale;//缩放差异

  14. int mVisibleFlag = -1;//可见标志


  15. // Widget’s generated geometry

  16. UIGeometry mGeom = new UIGeometry();//多变形实例

UIWidget承担了存储显示内容,颜色调配,显示深度,显示位置,显示大小,显示角度,显示的多边形形状,归属哪个UIPanel。这 就是UIWidget所要承担的内容,在UIWidget的所有子类中都具有以上相同的属性和任务。UIWidget和UIPanel的关系非常密切,因 为UIPanel承担了UIWidget的所有渲染工作,而UIWidget只是承担了存储需要渲染数据。所以,在UIWidget在更换贴图,材质球, 甚至更换UIPanel父节点时它会及时通知UIPanel说:”我更变配置了,你得重新获取我的渲染数据”。

UIWidget中最重要的虚方法为 virtual public void OnFill(BetterList<Vector3> verts, BetterList<Vector2> uvs, BetterList<Color32> cols) { } 它是区分子类的显示内容的重要方法。它的工作就是填写如何显示,显示什么。

UIWidget中在使用OnFill方法的重要的方法是 更新渲染多边型方法:

  1. public bool UpdateGeometry (ref Matrix4x4 worldToPanel, bool parentMoved, bool generateNormals)

  2. {

  3. if (material == null) return false;


  4. if (OnUpdate() || mChanged)

  5. {

  6. mChanged = false;

  7. mGeom.Clear();

  8. OnFill(mGeom.verts, mGeom.uvs, mGeom.cols);


  9. if (mGeom.hasVertices)

  10. {

  11. Vector3 offset = pivotOffset;

  12. Vector2 scale = relativeSize;

  13. offset.x *= scale.x;

  14. offset.y *= scale.y;


  15. mGeom.ApplyOffset(offset);

  16. mGeom.ApplyTransform(worldToPanel * cachedTransform.localToWorldMatrix, generateNormals);

  17. }

  18. return true;

  19. }

  20. else if (mGeom.hasVertices &amp;&amp; parentMoved)

  21. {

  22. mGeom.ApplyTransform(worldToPanel * cachedTransform.localToWorldMatrix, generateNormals);

  23. }

  24. return false;

  25. }

它的作用就是,当需要重新组织多边型展示内容时,进行多边型的重新规划。

 

接着,我们来看看UINode,这个类很容易被人忽视,而他的作用也很重要。它是在UIPanel被告知有新的UIWidget显示元素时被创建 的,它的创建主要是为了监视被创建的UIWidget的位置,旋转,大小是否被更改,若被更改,将由UIPanel进行重新的渲染工作。

HasChanged这是UINode唯一重要的方法之一,它的作用就是被UIPanel用来监视每个元素是否改变了进而进行重新渲染。

  1. public bool HasChanged ()

  2. {

  3. #if UNITY_3 || UNITY_4_0

  4. bool isActive = NGUITools.GetActive(mGo) &amp;&amp; (widget == null || (widget.enabled &amp;&amp; widget.isVisible));


  5. if (lastActive != isActive || (isActive &amp;&amp;

  6. (lastPos != trans.localPosition ||

  7. lastRot != trans.localRotation ||

  8. lastScale != trans.localScale)))

  9. {

  10. lastActive = isActive;

  11. lastPos = trans.localPosition;

  12. lastRot = trans.localRotation;

  13. lastScale = trans.localScale;

  14. return true;

  15. }

  16. #else

  17. if (widget != null &amp;&amp; widget.finalAlpha != mLastAlpha)

  18. {

  19. mLastAlpha = widget.finalAlpha;

  20. trans.hasChanged = false;

  21. return true;

  22. }

  23. else if (trans.hasChanged)

  24. {

  25. trans.hasChanged = false;

  26. return true;

  27. }

  28. #endif

  29. return false;

  30. }

接着,来看UIDrawCall,它是被NGUI隐藏起来的类。他的内部变量来看看:

  1. Transform        mTrans;            //坐标转换类

  2. Material        mSharedMat;        // 渲染材质

  3. Mesh            mMesh0;            //首个MESH

  4. Mesh            mMesh1;            //用于更换的Mesh

  5. MeshFilter        mFilter;        //绘制的MeshFilter

  6. MeshRenderer    mRen;            //渲染MeshRender组件

  7. Clipping        mClipping;        //裁剪类型

  8. Vector4            mClipRange;        //裁剪范围

  9. Vector2            mClipSoft;        //裁剪缓冲方位

  10. Material        mMat;            //实例化材质

  11. int[]            mIndices;        //做为Mesh三角型索引点

由这些内部变量可知,UIDrawCall是负责NGUI的最重要的渲染类。他制造Mesh制造Material,设置裁剪范围,为NGUI提供渲染底层。

他最重要的方法是:

  1. public void Set (BetterList&lt;Vector3&gt; verts, BetterList&lt;Vector3&gt; norms, BetterList&lt;Vector4&gt; tans, BetterList&lt;Vector2&gt; uvs, BetterList&lt;Color32&gt; cols)

  2. {

  3. int count = verts.size;


  4. // Safety check to ensure we get valid values

  5. if (count &gt; 0 &amp;&amp; (count == uvs.size &amp;&amp; count == cols.size) &amp;&amp; (count % 4) == 0)

  6. {

  7. // Cache all components

  8. if (mFilter == null) mFilter = gameObject.GetComponent&lt;MeshFilter&gt;();

  9. if (mFilter == null) mFilter = gameObject.AddComponent&lt;MeshFilter&gt;();

  10. if (mRen == null) mRen = gameObject.GetComponent&lt;MeshRenderer&gt;();


  11. if (mRen == null)

  12. {

  13. mRen = gameObject.AddComponent&lt;MeshRenderer&gt;();

  14. #if UNITY_EDITOR

  15. mRen.enabled = isActive;

  16. #endif

  17. UpdateMaterials();

  18. }

  19. else if (mMat != null &amp;&amp; mMat.mainTexture != mSharedMat.mainTexture)

  20. {

  21. UpdateMaterials();

  22. }


  23. if (verts.size &lt; 65000)

  24. {

  25. int indexCount = (count &gt;&gt; 1) * 3;

  26. bool rebuildIndices = (mIndices == null || mIndices.Length != indexCount);


  27. // Populate the index buffer

  28. if (rebuildIndices)

  29. {

  30. // It takes 6 indices to draw a quad of 4 vertices

  31. mIndices = new int[indexCount];

  32. int index = 0;


  33. for (int i = 0; i &lt; count; i += 4)

  34. {

  35. mIndices[index++] = i;

  36. mIndices[index++] = i + 1;

  37. mIndices[index++] = i + 2;


  38. mIndices[index++] = i + 2;

  39. mIndices[index++] = i + 3;

  40. mIndices[index++] = i;

  41. }

  42. }


  43. // Set the mesh values

  44. Mesh mesh = GetMesh(ref rebuildIndices, verts.size);

  45. mesh.vertices = verts.ToArray();

  46. if (norms != null) mesh.normals = norms.ToArray();

  47. if (tans != null) mesh.tangents = tans.ToArray();

  48. mesh.uv = uvs.ToArray();

  49. mesh.colors32 = cols.ToArray();

  50. if (rebuildIndices) mesh.triangles = mIndices;

  51. mesh.RecalculateBounds();

  52. mFilter.mesh = mesh;

  53. }

  54. else

  55. {

  56. if (mFilter.mesh != null) mFilter.mesh.Clear();

  57. Debug.LogError(“Too many vertices on one panel: “ + verts.size);

  58. }

  59. }

  60. else

  61. {

  62. if (mFilter.mesh != null) mFilter.mesh.Clear();

  63. Debug.LogError(“UIWidgets must fill the buffer with 4 vertices per quad. Found “ + count);

  64. }

  65. }

在这个方法里,它制造Mesh,MeshFilter,MeshRender,Materials。

 

最后,我们来说说最重要的UI渲染入口UIPanel。

    UIPanel的渲染步骤:

    1.当有任何形式的UI组件启动渲染时加入UIPanel的渲染队列,当有新的渲染组件需要有新的UIDrawCall时,进行生成新的UIDrawCall.

    2.对所有UIPanel的渲染队列进行检查,是否队列中渲染组件需要重新渲染,包括位移,缩放,更改图片,启用,关闭.

    3.获取渲染组件对应的UIDrawCall,更新Mesh,贴图,UV,位置,大小

    4.对需要更新的UIDrawCall进行重新渲染

    5.最后标记已经渲染的渲染组件,告诉他们已经渲染,为下次判断更新做好准备。删除不再需要渲染的UIDrawCall,销毁渲染冗余。

    注意:所有的渲染都是在LateUpdate下进行,也就是它是进行的延迟渲染。

接口源码:

  1. void LateUpdate ()

  2. {

  3. // Only the very first panel should be doing the update logic

  4. if (list[0] != this) return;


  5. // Update all panels

  6. for (int i = 0; i &lt; list.size; ++i)

  7. {

  8. UIPanel panel = list[i];

  9. panel.mUpdateTime = RealTime.time;

  10. panel.UpdateTransformMatrix();

  11. panel.UpdateLayers();

  12. panel.UpdateWidgets();

  13. }


  14. // Fill the draw calls for all of the changed materials

  15. if (mFullRebuild)

  16. {

  17. UIWidget.list.Sort(UIWidget.CompareFunc);

  18. Fill();

  19. }

  20. else

  21. {

  22. for (int i = 0; i &lt; UIDrawCall.list.size; )

  23. {

  24. UIDrawCall dc = UIDrawCall.list[i];


  25. if (dc.isDirty)

  26. {

  27. if (!Fill(dc))

  28. {

  29. DestroyDrawCall(dc, i);

  30. continue;

  31. }

  32. }

  33. ++i;

  34. }

  35. }


  36. // Update the clipping rects

  37. for (int i = 0; i &lt; list.size; ++i)

  38. {

  39. UIPanel panel = list[i];

  40. panel.UpdateDrawcalls();

  41. }

  42. mFullRebuild = false;

  43. }

Fill()接口源码:

  1. /// &lt;summary&gt;

  2. /// Fill the geometry fully, processing all widgets and re-creating all draw calls.

  3. /// &lt;/summary&gt;


  4. static void Fill ()

  5. {

  6. for (int i = UIDrawCall.list.size; i &gt; 0; )

  7. DestroyDrawCall(UIDrawCall.list[–i], i);


  8. int index = 0;

  9. UIPanel pan = null;

  10. Material mat = null;

  11. UIDrawCall dc = null;


  12. for (int i = 0; i &lt; UIWidget.list.size; )

  13. {

  14. UIWidget w = UIWidget.list[i];


  15. if (w == null)

  16. {

  17. UIWidget.list.RemoveAt(i);

  18. continue;

  19. }


  20. if (w.isVisible &amp;&amp; w.hasVertices)

  21. {

  22. if (pan != w.panel || mat != w.material)

  23. {

  24. if (pan != null &amp;&amp; mat != null &amp;&amp; mVerts.size != 0)

  25. {

  26. pan.SubmitDrawCall(dc);

  27. dc = null;

  28. }


  29. pan = w.panel;

  30. mat = w.material;

  31. }


  32. if (pan != null &amp;&amp; mat != null)

  33. {

  34. if (dc == null) dc = pan.GetDrawCall(index++, mat);

  35. w.drawCall = dc;

  36. if (pan.generateNormals) w.WriteToBuffers(mVerts, mUvs, mCols, mNorms, mTans);

  37. else w.WriteToBuffers(mVerts, mUvs, mCols, null, null);

  38. }

  39. }

  40. else w.drawCall = null;

  41. ++i;

  42. }


  43. if (mVerts.size != 0)

  44. pan.SubmitDrawCall(dc);

  45. }


转载于:https://my.oschina.net/jjyuangu/blog/349553

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值