<i id="bcuty"><sub id="bcuty"></sub></i>

<b id="bcuty"></b>

您的位置:首頁 >聚焦 >

28.從chrome消息隊列談unity3d的UI框架設計

2023-03-04 11:34:43    來源:程序員客棧
1 緣起

在UI架構圈,MVC是一個著名的小團伙,其中M是被偷窺重度成癮者,不過我們一般稱之為觀察者,畢竟讀書人的事,怎么能叫偷呢對吧。

M喜歡散播自己的花邊小新聞,屁大點兒事,都會翻譯成小道消息重大事件散播出去。VC則是倆狗仔,不管M出了啥事,總能第一時間聞到消息,搖旗吶喊。

這種玩法同時滿足了M和狗仔們的大多數需求,因此很快在UI架構圈流行開來。去年我用unity3d設計過一版UI框架,也是基于MVC模式。


(資料圖片僅供參考)

本來這樣也挺好,M出自己的名,狗仔追自己的星。但M太秀了,有時候一股腦爆出一大波新聞,讓狗仔們忙得應接不暇,有時候又波瀾不驚,讓狗仔們急得抓耳撓腮。

MVC模式中,M拋出的事件默認都是同步處理的。這帶來了一些問題:如果同一幀內收到并處理太多事件的話,可能由于處理時間太長,導致游戲卡頓。

2 取經

最近的學習研究chrome瀏覽器的一些架構設計,發現跟unity3d游戲設計有一些相似相溶的地方,隨手做了一番對比:

Unity3dChrome
消息隊列消息隊列
定時任務setTimeout(), 延遲隊列
觀察者MutationObserver
Task/CoroutinePromise
async/awaitasync/await
游戲渲染幀requestAnimationFrame()

早期的js對dom事件的支持并不友好,只能通過setTimeout()輪詢處理。這種方式簡單粗暴,但也有一些問題:如果輪詢間隔時間過短,則大部分情況下CPU在空轉;而如果輪詢間隔時間過長,則dom事件會得不到及時響應。

2000年的時候js引入了Mutation Event,采用觀察者模式,當dom發生變化時立刻觸發相應事件。Mutation Event屬于同步回調模式,解決了實時響應的問題,但也為頁面展示帶來了性能隱患。當我們使用js大量修改dom結構時,會頻繁拋出事件,如果所有的dom事件都同步處理,勢必會導致頁面卡頓。

也因此,Mutation Event被反對使用,并逐漸從web標準事件中被移除。dom4之后推薦使用MutationObserver,其作出的改進其實主要就一點:把所有的dom事件統一放入到微任務消息隊列中,在每一幀的結尾一次性觸發。通過異步回調解決性能問題,通過微任務解決實時性問題。

3 消息隊列

chrome中的異步任務體系都是基于消息隊列完成的,包括dom事件、fetch網絡請求、promise異步任務等等。

js中的異步任務還細分為了宏任務與微任務,其中只有MutationObserver 和promise是微任務,其它的像setTimeout()之類的都是宏任務。

宏任務由宿主(比如chrome中的window對象)維護,但js引擎需要有自己的異步任務體系,因此引入了微任務隊列。

后端也有消息隊列,比如Kafka、RocketMQ等,但與chrome把消息隊列主要用于異步任務處理不同,后端消息隊列最重要的作用主要有三:定序 (Ordering),分離式數據庫 (Unbundling Database)、重放 (Replay)。以首字母可簡記為:OUR。

我們在做unity3d框架設計的時候,其實也有大量使用消息隊列用于處理異步任務。

比如處理網絡協議:通常的做法是單獨起一個網絡線程,接收網絡協議,并把它們放到消息隊列中,然后由主線程按順序異步處理這些網絡協議。

再比如Coroutine庫:可以把所有的協程對象按順序放入到一個消息隊列中,每一幀按順序遍歷它們,并調用它們的next()方法。

這中間還誕生過一個很有趣的技巧:放置于消息隊列中的消息,往往對先后順序有要求,但對處理時間并不敏感。如果發現當前幀已經占用了太長的時間,可以直接跳出當前幀處理過程,把剩余的消息推遲到下一幀去處理。即無需每一幀都清空當前消息隊列,從而可以緩解游戲卡頓的問題。我把這個小技巧稱為分幀。

4 緣落

但UI框架遇到的問題又略有不同。游戲中有很多非常復雜的UI,比如背包,在加載結束之后,需要按需初始化大量的控件。初始化控件的邏輯是游戲業務邏輯,而不是框架底層邏輯,因此很難完全封裝在框架中。另外,初始化過程通常是在一幀內完成的,數量越多越有可能導致游戲卡頓。

針對這個問題,通用優化方案是將這些控件預先拖到一個MonoBehaviour腳本中,從而在加載的時候同步完成初始化過程。這個方案不是本文的重點,因此不再更多展開。另外,這個方案只能解決靜態控件的初始化問題,對于背包這種需要根據服務器協議初始化大量動態控件的復雜UI起不到優化效果。

基于前述消息隊列和分幀的思想,再參考MonoBehaviour的Start()方法可以配置成協程,我這里提一個新的優化思路:把UI框架中用于控件初始化的OnLoaded()方法設計為協程,這樣當遇到大量控件需要初始化的時候,可以支持分幀處理,從而緩解因為大量初始化動態控件導致的卡幀。

這個設計會帶來一些新的問題,其中一個是:很多UI事件會有先后順序要求,比如UI框架中的OnOpened()方法應該在OnLoaded()方法之后回調。但將OnLoaded()方法修改為協程后,就可能發生OnLoaded()方法未完全回調完成,就回調到了OnOpened()方法的情況。這需要進一步調整UI框架邏輯,以約束UI事件之間的先后順序關系。

5 緣盡

相逢是緣,相知是福,再不關注,就真的緣盡了老板。

5 References

本文提到的UI框架位于unicorn庫中,unicorn-examples示例代碼

18 | 宏任務和微任務:不是所有任務都是一個待遇

關鍵詞: 網絡協議 動態控件 輪詢間隔

相關閱讀

巨胸护士在线播放视频二区

<i id="bcuty"><sub id="bcuty"></sub></i>

<b id="bcuty"></b>