關於場景服務的一些想法
最近由於遇到一些問題,老大們決定把場景顯示相關的代碼拆分出來用一個獨立的線程去做(大概是實現一個獨立的場景服務吧),感覺這樣挺好的,畢竟這部分功能本來就較為獨立。
我對這部分內容還挺感興趣的,思考了一下,心裏有一個感覺是比較好的解決方法,遂提筆記錄下來:)
先簡單說說背景:地圖場景是按格子劃分的,每個格子有若幹屬性(key-value對),這些屬性隨著場景事件的觸發而改變。由於相同的格子在不同的玩家看到可能會有不同的顯示,因此需要為每個玩家獨立計算,並不能簡單的把格子數據直接同步給client。值得一提的是,上一個版本就是這麽做的,結果遇到了幾個問題:
(1)有些時候client計算不太方便。比如這個計算依賴其它一些數據就不得不把這部分數據也同步過去,因此白白浪費了帶寬。
(2)客戶端在移動鏡頭的時候又要做復雜的邏輯計算會導致畫面卡頓,感受不好。
因此決定了在server就把顯示數據計算好再同步給client。
那麽,這個場景服務如何實現呢?
首先看看它需要哪些功能呢?我覺得最基本的,它只需要做到以下兩點:
(1)更新地圖上每個格子的屬性數據。
(2)提供一個類似 get_grid_show_data(role_id, grid_id) 的接口計算出玩家對某個格子的顯示數據。
我的設想是這樣的:邏輯線程將會導致地圖上格子屬性數據發生變化的事件,以消息的形式傳遞給場景線程,場景線程以此為驅動來更新格子的屬性數據。而對於同步玩家格子的顯示數據,以request-response模式工作,client定期(可以是1秒)請求server格子的顯示數據。
為什麽是request-response模式呢?
首先,這個模式server不用維護client的狀態,因此可以實現的十分簡單。
其次是,這種方式可以讓client更靈活的選擇更新格子顯示的策略:
(1)比如說,當我打開了一個全屏的ui面板,這個時候其實就可以不需要關心格子的變化了因此可以簡單的停止請求更新。
(2)又或者當client的網絡不是很好,而這個時候server一股腦的將更新數據同步給client並無益處,更好的做法是client再上一次請求返回之後才允許發起下一次更新請求。
(3)再比如,當幾個玩家正在某個格子正在進行一場激烈的戰鬥(或是其它行為),這個時候我們更應該優先關註這個格子的變化、而其他的一些格子倒是可以緩慢一些。此時client可以對這個關註的格子采用更為積極的更新策略(得到返回馬上發起下一次查詢),而其它不怎麽關心的格子可以相對保守一點,如2秒3秒請求更新一次。
(4)最後還有一點,當玩家調整鏡頭遠近導致可見格子數量變化後,client可以靈活的做出相應的調整,只請求更新當前可見的格子。
當然了,這種方式也是有缺點的。最為明顯的就是,比訂閱-發布這中模式浪費了更多帶寬。這是因為發布訂閱幾乎就是做到了狀態的精準同步,server只在數據發生改變的時候同步客戶端,當然也就可以只同步最少的數據!
不過好在,這個缺點是可以在一定程度上規避的!
為了不同步重復的冗余數據,可以在格子上記錄一個最近更新的時間戳。client請求格子顯示數據時帶上這個時間戳,如果這個時間戳大於格子上的說明client的已經是最新的了,無需再做同步。否則,server就根據格子數據計算顯示數據並同步client,註意這個時候應當將時間戳也一並同步。client將收到的時間戳保存起來,下一次查詢會再用到。
其實還可以再進一步優化。一般都是玩家鏡頭有一若幹個格子,正常情況下我需要定時請求所有這些玩家可見的格子的數據。倘若一般手機屏幕能顯示玩家周圍4*4這麽多格子,client以一秒為周期定時發起請求,那麽每秒最少就需要發16條消息。這樣顯然是不科學的!
一個解決方法是,引入一個塊(block)的概念。一個塊就是一個格子集合,一般是一個固定大小的矩形。玩家周圍的格子就可以看作是一個塊。服務器除了在格子上記錄更新時間戳之外,也在這個塊上記錄一個時間戳。塊的時間戳等於其中包含的格子時間戳的最大值。也就是說,每當更新塊中的格子時,都要更新塊的時間戳。平時client在查詢的時候時候可以一次查詢整個塊,參數就帶上上一次服務器同步過來的塊的時間戳。倘若這個時間戳沒有變化,說明這段時間裏這個塊都沒有數據變化,不做同步。若這個時間戳有變化了,則遍歷塊中的所有格子,查看其時間戳,如果比客戶端傳過來的時間戳大的,說明是最近更新的,計算其顯示數據並同步。最後再把整個塊的時間戳也同步給client。
參考:如何只基於請求回應模式實現 MMO 級別的場景服務
關於場景服務的一些想法