FairyGUI的使用技巧和優化建議

這是侑虎科技第468篇文章,感謝作者黃程供稿。歡迎轉發分享,未經作者授權請勿轉載。如果您有任何獨到的見解或者發現也歡迎聯繫我們,一起探討。(QQ羣:465082844)

作者聯繫方式:[email protected],作者也是U Sparkle活動參與者,UWA歡迎更多開發朋友加入U Sparkle開發者計劃,這個舞臺有你更精彩!


FairyGUI是一支持跨平臺的遊戲GUI解決方案。由編輯器和平臺SDK兩大部分組成。跨平臺的編輯器提供了UI素材的管理和編輯功能,配合各平臺的SDK可快速方便的構建針對各平臺進行優化了的UI界面。
具體介紹和教學請移步FairyGUI的官網:http://fairygui.com

我們團隊是在2017年從Cocos2d-X向Unity的轉型過程中接觸並應用了FairyGUI的,當時的背景是團隊中美術無3D和Unity開發經驗,程序則一半人員處於轉型學習Unity階段,NGUI和UGUI都是全新的東西,相關人員對轉用Unity做UI編輯的學習情況不理想。而之前Cocos2d-x開發時相當部分的UI製作流程是美術設計並切圖,需要程序進行代碼層拼裝實現,美術確認並調整的流程限制了開發效率,這也是我們在新項目中想優化和解決的問題。

當美術主管建議並引入了FairyGUI,經測試評估以後,我們發現這個解決方案非常適合我們團隊。比起Unity的3D環境,美術更習慣有點類似Flash的編輯界面,美術可以在編輯器內完成絕大部分的UI界面設計和測試工作,尤其是動效部分。基本上不需要程序介入就可以所見即所得地完成70%的設計工作。而程序節省了拼界面和做動效的過程,可以專注於業務層的編寫。

經過1年半的開發和學習,已經順利應用到產品中了。當然也遇到了不少坑,也總結出一些使用技巧,在此整理了幾個,希望能給大家以參考。


一、像素點擊測試功能注意點

當我們在編輯器內製作的時候,可以使用任意包內的圖片作爲當前組件點擊測試用的Mask,並且在編輯器內預覽是沒有問題的。但在實際運行時,該Mask圖片需要包含在該組件的當前包內,如果在別的包比如Common包內時,相關檢測用數據文件不會導出,像素點擊測試無效。

比如fishItem01這樣的圖作爲通用背景,可能放在Bg或者Common之類的包內被其他包的資源引用。在List Demo包的List1組件內,bg圖片引用了Bg包內的fishItem01圖片作爲按鈕背景,並且同時該圖片作爲像素點擊測試用Mask使用。預覽正常,但實際運行,像素測試功能無效。
請輸入圖片描述
通過查看FairyGUI的源代碼發現原因如下:

3.0版本以前的源碼:
請輸入圖片描述
3.0及以後版本的源碼:
請輸入圖片描述
在此可以看到都是在初始化組件時,通過packageItem.owner去獲得這個測試資源的,這個owner是UIPackage類型,是當前組件(packageItem)所在的包。當沒有在當前包內找到hitTestId指向的資源時將忽略hitArea的設置,自然像素點擊測試失效。將該資源從外部包拷貝一份到相應組件所在包後問題解決。

但教程中並沒有提到這個問題。
請輸入圖片描述
那麼在此進一步分析hitTest的原理以及資源的管理方式。(以下分析基於3.0以前版本,3.0以後只是進行了導出資源的二進制化,基本原理應該還是一致的)

觀察之前代碼發現hitArea是一個PixelHitTest類型,數據來源於UIPackage.GetPixelHitData函數,該函數通過 _hitTestDatas成員,以ItemId爲Key獲取數據,而_hitTestDatas內數據由以下代碼初始化。

可以看到系統載入了一個”hittest.bytes”文件。那麼在使用到了像素測試的包導出後都能找到一個叫“包名@hittest.bytes”的文件。事實上如果使用了外部包內的資源做像素測試Mask,那麼根本不會導出這個文件。自然無法進行像素測試了。

打開這個文件,會發現是以文本形式記錄的二進制數據。由於解析是while循環,可以判斷該文件內會集合多個hitTest的資源。

繼續分析ba.ReadString函數,開頭的ushort保存了一個hitAreaData的id名的長度,在此是6,後面6個byte:“7337 7578 6a74”則是這個id。Ascii編碼轉換後是”s7uxjt”。並且使用該id名作爲key,將測試數據保存在“_hitTestDatas”字典型內。正因爲只有Item的id而沒有包名,所以只能在當前包內尋找資源。

而打開使用了hitTest的組件文件可以看到hitTest引用了內部的”n9_ockd”組件,該組件是一個image,src=“s7uxjt”。
請輸入圖片描述
打開這個組件所在包文件“package.xml”,搜索” s7uxjt”,就可以找到這個文件了。就是指向那張用於像素測試的圖。
請輸入圖片描述
回到”hittest.bytes”文件繼續分析,根據PixelHitTestData.Load函數,後續的一個int是空白未使用,再後續一個int是像素寬度,因爲是大端存儲,在這兒是”0xaa” = 170, scale = 1/“0x02”, 因此整個圖寬340px,pixels總數0x00000f86(3974)個字節,後續則是從圖片編碼轉換後的測試數據。我們使用的圖片是340X374,一共是127160個像素,到目前爲止圖片實際像素點和hitTestData數據並不相符。

進一步分析檢測方法:
請輸入圖片描述
點擊的本地座標點localPoint根據測試區域的偏移和scale屬性,被映射到原圖1/2的區域,因爲一般不需要真正點對點精度的檢測,採取原圖一半精度的檢測也足夠了。並且因爲只要檢測是否有顏色值,1個bit足夠,因此1個字節可以存放8個點的信息,於是通過pos -> pos2, pos3的計算,可以找到在_data數據中找到該點的bit位並返回hit判斷。(其他邊界檢查不累述了)

PS:測試圖片127160個像素,映射後除以4,等於31790個測試點,按位編碼除以8後,3973.75 取整後剛好3974個字節。


二、動效播放TimeScale的問題

使用FairyGUI 2.4.0以前版本動效播放都一切正常沒問題。但是升級到3.0.0版本後發現動效播放不完整現象,頭上一段完全沒有播放。查看教程以後發現有這麼一段:
請輸入圖片描述
嘗試將該動效的ignoreEngineTimeScale設置爲false後,播放正常。

接下來跟蹤ignoreEngineTimeScale相關代碼調查出了什麼問題。進一步查看Github上的代碼更新發現2018年8月15日的一個提交:


該修改去除了對dt即Time.unscaledDeltaTime的上限設置,於是dt可以超過0.1f秒。於是在這兒打印了一下這個時間,發現在遊戲開始啓動的一段時間這個值非常高,甚至超過1秒。因此後續動畫播放計算加上這個dt以後相當於略過了這1秒時間。而該修改之前會強制dt最大0.1f,因此雖然也是略過了0.1秒,但是還基本看不太出問題。

繼續調查代碼,ignoreEngineTimeScale是在8月2日加入的,之前2.3.1版本使用外部的DoTween,2.4.0開始使用內置GTween。因此該問題2.4.0版本開始受影響。

而我這兒比較大的Time.unscaledDeltaTime時間其實是遊戲啓動後執行腳本的Start。一般情況下,Start內會大量一次性的初始化造成暫短的卡頓。並且往往Start初始化完成以後會播放進場動效或者設置控制器,並由控制器調用播放動效。但是該動效更新時讀取了值較大的Time.unscaledDeltaTime,於是這個時間內的動效都被”快進”了。Time.unscaledDeltaTime是兩幀之間實際消耗值而Time.deltaTime會被計算並控制在Time Manager設置的範圍內。

幾個解決方案:

  1. 直接修改FairyGUI源碼的默認ignoreEngineTimeScale爲false。讓FairyGUI就是默認使用Time.deltaTime來計算動畫時間。
    缺點:修改了庫代碼,會有後續版本更新的維護問題。修改有可能不符合作者設計思路,造成別的潛在bug出現的可能性。

  2. 設置特定動效的ignoreEngineTimeScale爲false。
    缺點:該值默認爲true,如果有大量動效受影響,修改不方便。而且如果該動效由控制器調用,則修改更爲麻煩。

  3. Start執行動效時Play函數傳入延遲時間,讓動效延遲1-2幀時間後執行。
    缺點:如果需要切換到控制器的非默認狀態則延遲太大容易造成界面以默認狀態描畫若干幀,造成畫面閃現。

  4. 使用協程或者其他程序邏輯,將動效播放或者控制器切換移出Start或者其他可能造成佔用時間長的函數。延遲播放。
    缺點:代碼邏輯複雜化,不方便維護。

  5. 美術在FairyGUI編輯器內由控制器播放動效處進行延遲播放。
    缺點:延遲的時間控制不準確。可能造成3的缺陷。

個人認爲使用Unity的TimeScale可對應大部分項目。期待谷主後續改進。


三、列表使用的一個優化案例

項目中需求要做一個類似下圖的Item列表:
請輸入圖片描述
兩排橫列,Item以上下上下的順序排布。並且要求Item斜向顯示。因爲需要斜着顯示,因此需要像素點擊測試,否則會發生點擊了4號Item右上角,響應了6號的情況。

按一般思路製作了第一版本的Item:
請輸入圖片描述

執行後:
請輸入圖片描述

當勾選UIPanel上的FairyBatching後:
請輸入圖片描述

基本省下一半DrawCall。繼續觀察OverDraw以及Frame Debug:
請輸入圖片描述
請輸入圖片描述

可以發現FairyGUI判斷左右title和背景圖有遮擋,但是上下沒有任何遮擋,因此上下進行合批渲染,使用了3個批次,分別渲染了底圖,icon和title。當前最後渲染的是title文字而下一列的第一個要渲染的是底圖,因此材質不同不能合批。因此總的DrawCall減少一半。

繼續尋求優化方法:
請輸入圖片描述
將原來一個list拆分成3個list。每個list對應Item的3個部分,title,bg,icon。Item也拆分成3個使用。只有list3接收觸摸響應,list1和list2設置爲不可觸摸,通過代碼進行滾動的同步。

Title部分:
請輸入圖片描述

Icon部分:
請輸入圖片描述

Bg部分:
請輸入圖片描述

修改腳本後執行:
請輸入圖片描述
分層以後,總共3個材質的DrawCall都可以合併了。

由於做了分層,代碼需要做一定修改:
請輸入圖片描述

其他實現細節根據大家項目實際需求做微調。還可以考慮將上述代碼封裝成自定義的List類來進行操作。


四、關於合批的點點滴滴

在此簡單跟一下FairyGUI的合批流程,本文不打算用太多篇幅展開討論細節,有興趣的同學可以自己看代碼。

當我們設置了UIPanel的fairyBatching成員變量後,在創建UI容器(CreateContrainer)和反映屬性修改(ApplyModifiedProperties,編輯器用)2個操作時,會賦值給容器的fairyBatching屬性,該屬性的set操作內會設置“_fBatchingRequested”並遍歷逐級通知父節點有子節點需要合批。如果該父節點合批屬性也爲on時,也再次設置“_fBatchingRequested”標識。運行期直接設置容器組件的fairyBatching屬性也可以激活合批。

回頭看更新操作,逐步跟蹤代碼執行StageEngine.LateUpdate -> Stage.InternalUpdate ->Container.Update ,進入Container.Update後如果”_fBatching”爲true,則上下文對象的batchingDepth++,這個上下文對象會在子節點組件調用Update時往下傳,而不少組件是繼承於Container的,因此這兒只在第一級Container,即第一級父容器時才做SetRenderingOrder的操作,之後出Update調用前batchingDepth--。

在Container.SetRenderingOrder函數內如果之前設置的“_fBatchingRequested”標識爲true則進行合批處理DoFairyBatching()。該函數內首先將“_fBatchingRequested”標識設置爲false,因此可以判斷,各容器設置合批屬性後,合批的操作只做一次。跟蹤設置“_fBatchingRequested”爲true的2個函數(UpdateBatchingFlags,InvalidateBatchingState),可發現以下情況可能再次觸發調用合批操作:

設置某容器節點合批屬性爲true時:

  • 添加子節點,移除子節點,設置子節點順序,交換子節點,改變子節點Order等節點操作
  • 設置是否可見
  • 設置混合模式
  • Image組件更新Texture時
  • MovieClip組件設置動畫數據時
  • GList組件更新Bounds時
  • GProgressBar更新時
  • GSlider更新時
  • 動效的停止,播放,緩動更新
  • 容器設置裁剪Rect
  • 容器設置遮罩mask
  • 進入繪畫模式,將組件對象畫入RenderTexture時

讓我們回到DoFairyBatching函數,這兒會維護一個_descendants列表,遞歸調用CollectChildren函數,收集子節點。如果子節點容器也設置了“_fBatchingRequested”爲true,則在CollectChildren內調用子節點容器的DoFairyBatching。收集完子節點後,兩層循環遍歷_descendants列表,根據材質和bounds信息進行插入排序。排序會調用List的RemoveAt和Insert,內部會可能調用Array.Copy,要注意性能問題。

排完序後回到SetRenderingOrder函數,再次循環遍歷_descendants列表依次設置子節點(DisplayObject)的renderingOrder屬性。

DisplayObject.renderingOrder屬性實際對應到DisplayObject內部的graphics和paintingGraphics2個NGraphics的sortingOrder。

NGraphics是FairyGUI的渲染部分核心,內部維護了對應到Unity場景中的GameObject,MeshFilter,MeshRenderer,Shader等信息。

而設置NGraphics.sortingOrder則是設置MeshRenderer.sortingOrder,這也是Unity中手動調整mesh渲染順序的方法。

以上就是整個的FairyGUI合批的大致流程。綜合來看要注意動效,GProgress,GSlider,繪畫模式這幾個可能會每幀更新造成持續做合批操作的部分。


五、關於循環虛擬列表

循環虛擬列表是個很有用的東西,尤其是在做一些關卡選擇,武器選擇之類界面時。但是有一些細節值得注意。

首先我們創建一個Item數爲10的循環虛擬列表:
請輸入圖片描述
請輸入圖片描述
在畫面中分別打印出一個Item的Child Index,ItemIndex和Hash值。Child Index對應了實際描畫在畫面中的子節點索引,在Demo中是0-5,可以通過OverDraw看到實際描畫了6個Item,但是因爲List有裁剪,只顯示了5個。Item Index對應虛擬列表內Item的實際數量,這兒是0-9。因爲循環虛擬列表的Item對應的DisplayObject對象是複用的。因此通過Hash來跟蹤。

當少許向右移動一點點,Item的索引信息發生了變化:
請輸入圖片描述
對比之前的可以發現,系統在畫面外最左邊添加了一個Item,這時畫面中的第一個Item的Child索引變成了1,其他都沒有變。

當繼續向右拖動List,直到0x39EB990這個Item對象再次出現在畫面最左端時該對象的Child Index還是爲1,ItemIndex爲3。
請輸入圖片描述
觀察可以發現向右持續拖動過程中,一旦最左的Item完全出現在畫面,就會在左邊再增補一個Item,這時候會發生重置Child的操作,這個過程還會引發所有Child的重繪並調用ItemRender,教程中也講到了相關效率問題。

首先我們要了解循環列表首先是虛擬列表,Item信息實際是存放在List內部_virtualItems列表內的。該列表的長度虛擬列表時和numItems一致,循環列表則numItems * 6,並且只增不減。並且該列表保存的是ItemInfo,而非實際的GButton之類對象。假設一共有5個Item,則循環列表的情況下_virutalItem長度爲30。而實際描畫用的Item對象可能有10個,保存在_virtualItem哪個位置是不一定的,HandleScroll系列函數會對其進行重排。實際會從_firstIndex開始的_numItems個。

觀察selectedIndex屬性的get操作:
可以看到如果是循環虛擬列表,會從_virtualItems列表裏循環搜索出被選中的那個並根據item數量取模獲得。

而selectedIndex屬性的set操作:
調用了AddSelection函數,注意是直接把Value傳入,即傳入的是需要選中的ItemIndex。

而AddSelection函數內並沒有對循環列表的判斷,直接用傳入的ItemIndex 不經過換算從_virtualItems直接獲取ItemInfo並設置該info對應obj 按鈕對象的selected屬性。

事實上虛擬列表可能沒問題,但是循環列表中想選中的對象可能並不在該位置上,ii.obj可能是一個空值。造成想選中的那個按鈕無法設置selected屬性。另外如果Item數比Child數少的情況下,畫面中一樣ItemIndex的對象有多個。那麼selectedIndex究竟選中的是哪個呢?

總結來說就是循環列表不要使用selectedIndex。


六、Text影響DrawCall的例子

拿着上面的例子可以繼續就DrawCall做一個探討。
當我簡單設置按鈕的title爲”test’這樣短小的字符串時:
請輸入圖片描述
可以看到DrawCall是2。
當我在title內寫入更多信息的時候:
請輸入圖片描述
DrawCall變成了7。查看FrameDebug究其原因則是因爲爲了顯示Text而生成的Mesh頂點超過了300。
請輸入圖片描述


七、濾鏡影響DrawCall

繼續折騰,給背景圖添加一個濾鏡效果:

完全沒有合批了。
請輸入圖片描述
根據之前關於合批部分的分析並跟蹤代碼會發現所有背景的Material都不一樣,也就是說系統針對每個Image都生成了一個Material實例。

繼續調查發現系統給設置了濾鏡的材質都設置了“COLOR_FILTER”關鍵詞。而系統會根據設置的材質Keyword來生成材質,在Material.GetMaterialManager函數內發現:
請輸入圖片描述
只要是帶上關鍵詞的Material都會重新生成一個實例,因此就無法進行合批了。目前知道的關鍵詞只有“COLOR_FILTER”。因此慎用濾鏡!


文末,再次感謝黃程的分享,如果您有任何獨到的見解或者發現也歡迎聯繫我們,一起探討。(QQ羣:465082844)。
也歡迎大家來積極參與U Sparkle開發者計劃,簡稱"US",代表你和我,代表UWA和開發者在一起!

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