轉 Unity3D-深入剖析NGUI的遊戲UI架構

原文地址 http://www.luzexi.com/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 == nullreturn 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] != thisreturn;
  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; )<

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章