Unity瑣碎(3) UGUI 圖文混排解決方案和優化
http://www.cnblogs.com/zsb517/p/6667050.html
感覺使用Unity之後總能看到各種各樣解決混排的方案,只能說明Unity不夠體恤下情啊。這篇文章主要講一下個人在使用過程中方案選擇和優化過程,已做記錄。順便提下,開源很多意味著坑,還是要開實際需求。
1. 方案選擇
1 TextMeshPro
Unity 最近公佈收購了TextMeshPro並且免費開源給大家使用,估計還需要幾個小版本才會完全融合到Unity中或者保持現在的狀態。TextMeshPro支援效果豐富,相容現在UI層級等,效能也可以滿足移動端,但是很糾結的是:
- 現在的版本生成的字型庫實在太大了,比較全的漢字字型檔生成TextMeshPro需要的字型檔之後已經接近17M,如果考慮到遊戲中存在2種字型,估計會超過20M的常駐記憶體在移動端,這個至少現在還很難接受。
- 另外一個文字是序列化,20M的序列化資料,移動端受IO限制,讀取時間會有點長。這個時間長度我沒仔細測試過
基於上面亮點,最後我還是沒有采用這種方案,如果Unity考慮融合進來,建議修改下字型檔的使用方式。
2 文字和圖片獨立渲染
- 文字和圖片採用layout的方式控制渲染位置,最後會生成大量的Text和Sprite,實時計算位置資訊,比如:RichText,這裡面最大的問題可能會再CPU端造成不必要的浪費。
- 文字中留空間,圖片再這個空間單獨渲染。Text支援富文字的時候會控制間隔,利用這個間隔提供圖片位置資訊,然後單獨渲染圖片位置。這種方案Text不需要實時更新,圖片(帶有動態)需要實時過更新。可以
- Shader中渲染圖片:Uwa UGUI表情系統解決方案 直接再shader中渲染圖片,這個方案對於outline、shadow時避免圖片也被處理的問題沒想到好的方案,就放棄了。
2. 基本原理
2.1 基本思想
- 利用Text富文字佔位符為圖片保留位置、圖片名字、長寬等資訊,通過字元解析獲取圖片相關資訊
- 監聽Text重繪以及位置更新等事件,並更新圖片位置
參考文章
Unity Text 插入圖片,這篇文章是基本的實現方式,後面CSDN“神碼程式設計”也就在這基礎上做了幾處擴充套件和一些文章分享
2.2 程式碼實現思路
-
提前生成sprite區域資訊,如果是一個系列的表情則根據sprite名字進行區分,當然後面也根據名字進行保留和查詢。如angry_0\angry_1\angry_2\angry_3 , die_0/die_1/die_2/die_4/die_5/die_6
-
表情管理器:記錄所有Text中圖片(有效的)位置、紋理、頂點資訊的索引關係,由資料變化時生成需要的Mesh資訊並提交
-
SpriteAsset 管理器:管理圖片中所有Text中使用的圖片資源載入以及sprite位置、名字資訊。
3. 爬坑記錄
最初的原始碼看似可用,但是在手機端ListView滾動情況下直接掉到20幀一下,即使在靜態100個表情同時更新的境況下效率也很難令人滿意。所以.................差不多用了一週時間爬各種坑,下面是一些主要的記錄:
3.1 優化內容
(1) GC
程式碼中在解析字元中基本每次都在new資料,包括解析字元、計算圖片位置、更新圖片Mesh等都存在很嚴重的GC,看上圖就可以看到滾動中如果頻繁建立的問題。
優化思路:
- 對於每個Text,限制最大圖片數量以及相關結構數量,只有在不夠的時候再進行分配(不超過最大數量則),後續使用中不再進行分配,當然增加了資料有效性判斷而不是是否為空。
- 對於圖片管理Mesh,則管理器中圖片總數量提前建立,只有再發生變化時才會重新進行記憶體分配。現在使用的策略還需優化。
(2) 圖片資訊查詢
啟動時讀取配置資訊,並簡歷sprite名字和資訊的對應Dictionary,加快查詢。當然也可以直接以Dictionary結構進行序列化,就可以節省這部分空間和時間,待優化。
(3) 有效圖片更新方式
原始版本中有效Sprite 列表時通過List的形式進行管理,每次任一個Text的變化(enabled,posotion等)都會將這個列表清除並重新將有效Text中的有效Sprite新增到列表中來。這種方式如果在類似ListView等一直會變化的元件中就會產生不必要的CPU開銷。
優化思路:
- 維護一個有效Text的Dictionary,儲存Text中對應Sprite的Key值,在Text OnEnable/OnDisable中進行註冊和登出操作
- 維護一個有效Sprite的Dictionary,儲存Sprite string以及實際資訊。
- 每次有Text改變時只修改Sprite 鍵值表中對應的部分,當然也考慮Text登出等情況。
這種方式避免在頻繁更新中不必要的列表清除操作以及對SpriteManager lateUpdate的影響
(4) 圖片Mesh資料更新過程時間
最初的版本採用對SpriteList遍歷的形式逐個將triangles、uv、vertices 賦值到新建立的快取中,再扔給iMesh去提交。在ListView快速移動時這部分的時間佔用就很誇張了。
優化思路:
- 儘量減少無效sprite進入列表,限制每個Text中sprite的最大數量
- 採用Array.Copy的形式替代逐個賦值
(5) 佔位符亂碼清除方式
原始版本可能時作者計算錯誤了,清除亂碼的UV位置其實只需要向後4個即可,但是也原始版本是按4 * Length(標籤長度)來計算,這項的CPU佔用率特別高。
(6) 動態表情更新方式
原始版本時在SpriteUpdate中每隔固定時間更新表情的索引(如果有)並重新更新Sprite Mesh內容。會產生一個問題:每種型別表情動畫圖片的數目不一樣,那就很難保證每個動態表情都很自然的播放。提高更新的間隔意味著有些表情像發飆一樣
優化思路:
每型別的表情中單獨存放其時間間隔以及已經執行的時間,在Update中根據各自的情況進行更新。
(7)圖片位置更新方式
原始程式碼中是在Text :SetVerticesDirty()中進行ParseText的操作並依賴SpriteManager中LaterUpdate更新圖片的Mesh資料,產生的問題:
- SetVerticesDirty 是Text 任何變化都會呼叫介面,意味著ParseText的操作在ListView滾動過程中一直在進行。
- SpriteManager中LaterUpdate更新與Text位置變化不同步,滾動時很明顯的可以看到sprite的位置偏移
優化思路:
- ParseText只在text文字內容變化時進行更新,可通過過載Text的text屬性實現
- 在ListView滾動過程中 sprite變化的只有位置資訊,所以只更新位置即可,並且直接更新MESH,不等待SpriteManager。
(8)其他
對應的還有編輯器、資料結構、貼圖資源管理等的優化
3.2 新增功能
(1)支援簡化標籤
支援 "[xxxxx]"來替代冗長的設定
(2)圖片層級管理
方便單個Canvas下多個層級,讓Text 可以直接設定SpriteManager或者找最近的一個。
(3)增加文字與圖片間隔設定
3.3 待優化內容
(1)下劃線解析和超連結解析都是基於字元位置對應實際字元頂點位置
(2)字串解析
(3)圖片Mesh
(4)多張sprite Asset
3.4 優化效果
測試方式,螢幕中160個動畫表情的情況,在ListView中快速滾動下進行測試的效能曲線(主要時CPU);
優化前
優化後
原生Text, 有佔位符,無表情
4 小結
採用這種方案各種原因都有,有好處也有弊端,就像層級問題,解決起來會有點頭痛。經過一段時間優化勉強可以在移動端滿足需求,不過還有很多可以繼續優化的空間。
歡迎繼續補充。