原文地址 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分辨率來測試項目的適配效果,不要到上線才發現適配問題。
適配源碼:
- 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;
- }
5.拆分以及固定各個錨點,上,左上,右上,中,左中,右中,下,左下,右下
6.拆分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內存時間駐留過長就銷燬。關於內存優化問題,可以參考《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這個組件基類,從它擁有的類內部變量就能知道它承擔得怎樣的責任:
- // 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; )<