1. 程式人生 > >Unity3D:NGUI 深入剖析NGUI的遊戲UI架構

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

轉自:http://www.manew.com/blog-97-238.html

 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.能不用自帶的UIDraggablePanel就不用,自己寫才是最適合自己專案的。 5.在開發中,儘量用Free解析度來測試專案的適配效果,不要到上線才發現適配問題。 適配原始碼:         float defaultWHRate = 800f / 480f;         float ScreenWHRate = (float)Screen.width / (float)Screen.height;         bool isUseHResize = defaultWHRate >= ScreenWHRate ? false : true;                 UIRoot root = GameObject.Find("ROOT").GetComponent<UIRoot>();         if (!isUseHResize)         {             float curScreenH = (float)Screen.width / defaultWHRate;             float Hrate = curScreenH / Screen.height;             root.manualHeight =(int)(480f / Hrate);         }         else         {             root.manualHeight = 480;         } 6.拆分以及固定各個錨點,上,左上,右上,中,左中,右中,下,左下,右下 7.拆分GUI層級,層級越高,顯示越靠前。層級的正確拆分能有效管理GUI的顯示方式。 /// <summary> /// GUI層級 /// </summary> public enum GUILAYER {     GUI_BACKGROUND = 0, //背景層     GUI_MENU,           //選單層0     GUI_MENU1,           //選單層1     GUI_PANEL,          //面板層     GUI_PANEL1,         //面板1層     GUI_PANEL2,         //面板2層     GUI_PANEL3,         //面板3層     GUI_FULL,           //滿屏層     GUI_MESSAGE,        //訊息層     GUI_MESSAGE1,        //訊息層     GUI_GUIDE,           //引導層     GUI_LOADING,        //載入層 } 8.要充分的管理GUI,不然過多的GUI會導致記憶體加速增長,而每次都銷燬不用的GUI則會讓IO過於頻繁降低執行速度。我的方法是找到兩者間的中間態,給予隱藏的GUI一個緩衝帶,當每次某各GUI進行隱藏時判斷是否有需要銷燬的GUI。或者也可以這麼做,每時每刻去監控隱藏的GUI,哪些GUI記憶體時間駐留過長就銷燬。 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這個元件基類,從它擁有的類內部變數就能知道它承擔得怎樣的責任:     // Cached and saved values     [HideInInspector][SerializeField] protected Material mMat;//材質     [HideInInspector][SerializeField] protected Texture mTex;//貼圖     [HideInInspector][SerializeField] Color mColor = Color.white;//顏色     [HideInInspector][SerializeField] Pivot mPivot = Pivot.Center;//對齊位置     [HideInInspector][SerializeField] int mDepth = 0;//深度     protected Transform mTrans;//座標轉換     protected UIPanel mPanel;//相應的UIPanel     protected bool mChanged = true;//是否更改     protected bool mPlayMode = true;//模式     Vector3 mDiffPos;//位置差異     Quaternion mDiffRot;//旋轉差異     Vector3 mDiffScale;//縮放差異     int mVisibleFlag = -1;//可見標誌     // Widget's generated geometry     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方法的重要的方法是 更新渲染多邊型方法:     public bool UpdateGeometry (ref Matrix4x4 worldToPanel, bool parentMoved, bool generateNormals)     {         if (material == null) return false;         if (OnUpdate() || mChanged)         {             mChanged = false;             mGeom.Clear();             OnFill(mGeom.verts, mGeom.uvs, mGeom.cols);             if (mGeom.hasVertices)             {                 Vector3 offset = pivotOffset;                 Vector2 scale = relativeSize;                 offset.x *= scale.x;                 offset.y *= scale.y;                 mGeom.ApplyOffset(offset);                 mGeom.ApplyTransform(worldToPanel * cachedTransform.localToWorldMatrix, generateNormals);             }             return true;         }         else if (mGeom.hasVertices && parentMoved)         {             mGeom.ApplyTransform(worldToPanel * cachedTransform.localToWorldMatrix, generateNormals);         }         return false;     }     它的作用就是,當需要重新組織多邊型展示內容時,進行多邊型的重新規劃。     接著,我們來看看UINode,這個類很容易被人忽視,而他的作用也很重要。它是在UIPanel被告知有新的UIWidget顯示元素時被建立的,它的建立主要是為了監視被建立的UIWidget的位置,旋轉,大小是否被更改,若被更改,將由UIPanel進行重新的渲染工作。     HasChanged這是UINode唯一重要的方法之一,它的作用就是被UIPanel用來監視每個元素是否改變了進而進行重新渲染。     public bool HasChanged ()     { #if UNITY_3 || UNITY_4_0         bool isActive = NGUITools.GetActive(mGo) && (widget == null || (widget.enabled && widget.isVisible));         if (lastActive != isActive || (isActive &&             (lastPos != trans.localPosition ||              lastRot != trans.localRotation ||              lastScale != trans.localScale)))         {             lastActive = isActive;             lastPos = trans.localPosition;             lastRot = trans.localRotation;             lastScale = trans.localScale;             return true;         } #else         if (widget != null && widget.finalAlpha != mLastAlpha)         {             mLastAlpha = widget.finalAlpha;             trans.hasChanged = false;             return true;         }         else if (trans.hasChanged)         {             trans.hasChanged = false;             return true;         } #endif         return false;     }     接著,來看UIDrawCall,它是被NGUI隱藏起來的類。他的內部變數來看看:     Transform        mTrans;            //座標轉換類     Material        mSharedMat;        // 渲染材質     Mesh            mMesh0;            //首個MESH     Mesh            mMesh1;            //用於更換的Mesh     MeshFilter        mFilter;        //繪製的MeshFilter     MeshRenderer    mRen;            //渲染MeshRender元件     Clipping        mClipping;        //裁剪型別     Vector4            mClipRange;        //裁剪範圍     Vector2            mClipSoft;        //裁剪緩衝方位     Material        mMat;            //例項化材質     int[]            mIndices;        //做為Mesh三角型索引點     由這些內部變數可知,UIDrawCall是負責NGUI的最重要的渲染類。他製造Mesh製造Material,設定裁剪範圍,為NGUI提供渲染底層。     他最重要的方法是:     public void Set (BetterList<Vector3> verts, BetterList<Vector3> norms, BetterList<Vector4> tans, BetterList<Vector2> uvs, BetterList<Color32> cols)     {         int count = verts.size;         // Safety check to ensure we get valid values         if (count > 0 && (count == uvs.size && count == cols.size) && (count % 4) == 0)         {             // Cache all components             if (mFilter == null) mFilter = gameObject.GetComponent<MeshFilter>();             if (mFilter == null) mFilter = gameObject.AddComponent<MeshFilter>();             if (mRen == null) mRen = gameObject.GetComponent<MeshRenderer>();             if (mRen == null)             {                 mRen = gameObject.AddComponent<MeshRenderer>(); #if UNITY_EDITOR                 mRen.enabled = isActive; #endif                 UpdateMaterials();             }             else if (mMat != null && mMat.mainTexture != mSharedMat.mainTexture)             {                 UpdateMaterials();             }             if (verts.size < 65000)             {                 int indexCount = (count >> 1) * 3;                 bool rebuildIndices = (mIndices == null || mIndices.Length != indexCount);                 // Populate the index buffer                 if (rebuildIndices)                 {                     // It takes 6 indices to draw a quad of 4 vertices                     mIndices = new int[indexCount];                     int index = 0;                     for (int i = 0; i < count; i += 4)                     {                         mIndices[index++] = i;                         mIndices[index++] = i + 1;                         mIndices[index++] = i + 2;                         mIndices[index++] = i + 2;                         mIndices[index++] = i + 3;                         mIndices[index++] = i;                     }                 }                 // Set the mesh values                 Mesh mesh = GetMesh(ref rebuildIndices, verts.size);                 mesh.vertices = verts.ToArray();                 if (norms != null) mesh.normals = norms.ToArray();                 if (tans != null) mesh.tangents = tans.ToArray();                 mesh.uv = uvs.ToArray();                 mesh.colors32 = cols.ToArray();                 if (rebuildIndices) mesh.triangles = mIndices;                 mesh.RecalculateBounds();                 mFilter.mesh = mesh;             }             else             {                 if (mFilter.mesh != null) mFilter.mesh.Clear();                 Debug.LogError("Too many vertices on one panel: " + verts.size);             }         }         else         {             if (mFilter.mesh != null) mFilter.mesh.Clear();             Debug.LogError("UIWidgets must fill the buffer with 4 vertices per quad. Found " + count);         }     }     在這個方法裡,它製造Mesh,MeshFilter,MeshRender,Materials。     最後,我們來說說最重要的UI渲染入口UIPanel。     UIPanel的渲染步驟:     1.當有任何形式的UI元件啟動渲染時加入UIPanel的渲染佇列,當有新的渲染元件需要有新的UIDrawCall時,進行生成新的UIDrawCall.     2.對所有UIPanel的渲染佇列進行檢查,是否佇列中渲染元件需要重新渲染,包括位移,縮放,更改圖片,啟用,關閉.     3.獲取渲染元件對應的UIDrawCall,更新Mesh,貼圖,UV,位置,大小     4.對需要更新的UIDrawCall進行重新渲染     5.最後標記已經渲染的渲染元件,告訴他們已經渲染,為下次判斷更新做好準備。刪除不再需要渲染的UIDrawCall,銷燬渲染冗餘。     注意:所有的渲染都是在LateUpdate下進行,也就是它是進行的延遲渲染。     介面原始碼:     void LateUpdate ()     {         // Only the very first panel should be doing the update logic         if (list[0] != this) return;         // Update all panels         for (int i = 0; i < list.size; ++i)         {             UIPanel panel = list[i];             panel.mUpdateTime = RealTime.time;             panel.UpdateTransformMatrix();             panel.UpdateLayers();             panel.UpdateWidgets();         }         // Fill the draw calls for all of the changed materials         if (mFullRebuild)         {             UIWidget.list.Sort(UIWidget.CompareFunc);             Fill();         }         else         {             for (int i = 0; i < UIDrawCall.list.size; )             {                 UIDrawCall dc = UIDrawCall.list[i];                 if (dc.isDirty)                 {                     if (!Fill(dc))                     {                         DestroyDrawCall(dc, i);                         continue;                     }                 }                 ++i;             }         }         // Update the clipping rects         for (int i = 0; i < list.size; ++i)         {             UIPanel panel = list[i];             panel.UpdateDrawcalls();         }         mFullRebuild = false;     }     Fill()介面原始碼:     /// <summary>     /// Fill the geometry fully, processing all widgets and re-creating all draw calls.     /// </summary>     static void Fill ()     {         for (int i = UIDrawCall.list.size; i > 0; )             DestroyDrawCall(UIDrawCall.list[--i], i);         int index = 0;         UIPanel pan = null;         Material mat = null;         UIDrawCall dc = null;         for (int i = 0; i < UIWidget.list.size; )         {             UIWidget w = UIWidget.list[i];             if (w == null)             {                 UIWidget.list.RemoveAt(i);                 continue;             }             if (w.isVisible && w.hasVertices)             {                 if (pan != w.panel || mat != w.material)                 {                     if (pan != null && mat != null && mVerts.size != 0)                     {                         pan.SubmitDrawCall(dc);                         dc = null;                     }                     pan = w.panel;                     mat = w.material;                 }                 if (pan != null && mat != null)                 {                     if (dc == null) dc = pan.GetDrawCall(index++, mat);                     w.drawCall = dc;                     if (pan.generateNormals) w.WriteToBuffers(mVerts, mUvs, mCols, mNorms, mTans);                     else w.WriteToBuffers(mVerts, mUvs, mCols, null, null);                 }             }             else w.drawCall = null;             ++i;         }         if (mVerts.size != 0)             pan.SubmitDrawCall(dc);     }