繼續談網絡游戲的同步問題
前面一篇談了放置類游戲的網絡同步 ,我想把其方法推廣到其它類型的游戲,比如 MMORPG ,比如動作游戲。尤其是動作類游戲,非常需要客戶端可以即時處理玩家的操作,而不能等待服務器確認。
我們來看看這些類型的游戲和放置類游戲的不同點。
放置類游戲大部分是玩家個人和服務器在玩,不涉及第三方的干擾。所以,只要操作序列一致,那么結果就一致。
MMORPG MOBA 動作游戲這些,是多人在玩。如果我們能同步所有玩家的操作,讓所有玩家的操作序列在一條線上,那么也一定可以保證結果一致。這點,是上篇 blog 的結論。
用同步操作的方式,同時賦予玩家對自己發起的操作標注時刻的權利,就可以在發起操作后,客戶端立刻計算。只有在事后發生沖突時,服務器才會命令客戶端取消無效操作。這個是前面談及的同步框架可以解決好的問題。
但,MMORPG/MOBA 都可能有一個問題:并非所有玩家的操作以及操作修改的數據,都是針對戰場上的每個人的。
比如,在 MMORPG 里,你可以選擇隱身,別的玩家就不應該收到你隱身后的移動操作指令;你可以在戰場上埋個地雷、別的玩家不應該事先知道你埋在了哪里。而且你未必希望讓別人知道你背包里剩下什么道具,你還有多少 HP 。
而前面談及的同步模型中,卻必須要求全部初始狀態為所有客戶端及服務器都知曉,整個游戲過程中發生的任何事件都需要傳達給任何人,最終才能保證結果的嚴格一致。
該怎么辦呢?
我認為最簡單的方法是建立兩套模型,一個是針對個人的:personal model ;另一個是針對戰場的:shared model ,所有人都可以見到。
對于服務器,應該有多個 personal model ,為每個客戶端都建立有一套對應的同步 model ;而只有一份 shared model ,每個客戶端都和服務器同步這個 model 。
從客戶端的角度看,它有兩個數據 model ,一個負責自身的狀態,這些狀態不會為別的玩家所見;另一個負責環境的狀態,保持和其他玩家以及服務器一致。
每個操作,都應該注明是針對哪個 model 的。對于 personal model 的操作,服務器不會對其他玩家廣播;對于 shared model 則在接收到玩家發起的操作后,廣播給別的玩家。
這里處理的難點在于,操作本身是只能修改 model 內的狀態的,禁止調用任何對 model 之外有影響的方法,禁止讀取 model 之外的其它狀態。只有避免副作用,才能保證一致性。但如果單個操作修改一個 model 的時候需要依賴另一個 model 內的數據怎么辦呢?
一個通用原則是:在產生針對 A model 的操作指令時,讀取 B model 的狀態,把狀態以參數的形式,打包在操作指令中。當這個操作指令從客戶端傳給服務器后,服務器需要做同樣的計算,讀取外部狀態,這可能不僅僅是 B model ,可能還包括了其他玩家的 model 的影響,計算出參數。
然后比較兩個參數是否一致。如果不一致,有兩種處理方式:其一,通知客戶端取消該操作;其二,如果偏差不大,先通知客戶端取消該操作,然后在生成一個時間戳一樣的新操作,但是附上服務器認可的參數傳給客戶端。
比如:玩家消耗 MP 給自己加了個加速移動 buf 。但游戲規則規定,如果玩家附近有敵對玩家,buf 效果會減弱。這時,有個敵對玩家隱身在他旁邊,這個隱身玩家的位置信息并沒有同步給他。如果該玩家不等服務器回應就要表現這個增益 buf ,并利用加速移動 buf 向前奔跑;那么就涉及之后位置不一致的情況。
對于施加加速移動 buf 這個行為,其實是針對玩家的 personal model 的,但需要獲取環境(shared model )的影響。玩家可以在生成這個操作指令的時候,自行計算環境的影響,得到 buf 的等級(效果);而服務器收到玩家的操作命令時,也自行計算 buf 等級。當兩者不一致時,服務器先取消掉玩家發送過來的操作指令,利用同樣的時間戳生成一個帶正確參數的操作命令發回給他即可。
玩家的客戶端在自行處理這個 buf 后,隨后收到了服務器的糾錯,由同步模塊內部的機制自動回滾和重新計算,可以到一致的 buf 等級。
服務器在處理完這個針對 personal model 的增益 buf 操作后,還需要針對場景的 shared model 發起一個該玩家對象被加速的操作;讓場景中的所有人都知道這件事。接下來,玩家移動的操作,就是針對 shared model 的。場景中所有人都能通過同步到玩家的位置、速度、加速 buf 的等級,計算出后續時刻的位置了。
Shop Heroes 這種放置類游戲也有這類問題。
Shop Heroes 里有個公會的設定。玩家可以把自家的金幣投資到公會的建筑上,建筑升級會給公會里所有的玩家一個短期的 buf 。比如,如果我投資公會的礦山,那么我自家的礦的單位時間產量會增加。
公會是很多玩家共有的。比如一個玩家可能和你同時投資,他的投資先生效有可能導致你的投資失敗;也可能使你的投資獲得更大收益(礦山等級更高 buf 效果更好)。這一切在服務器確認投資完成前你是不知道的。
Shop Heroes 的做法是,等待服務器回應后,玩家才真正看到效果,這也是涉及多人交互的游戲常規的做法。但對于個人操作體驗來說不是特別好,尤其是網絡條件比較差的情況。
如果采用以上提到的方法,就可以避免這個操作卡頓(等待服務器確認)。
玩家在客戶端發起 “投資公會礦山” 這個操作時,生成一個針對 personal model 的操作指令,這個指令的參數里包括了給自己增加一個礦產量增加的 buf 級別(一級礦山對應一級增益 buf )。這個級別數是生成這個操作指令時,訪問 shared model 也就是公會數據得到的。
玩家在把這個操作通知服務器的同時,還應該帶上它針對服務器的 shared model 修改的版本基準。比如,它這個操作是針對公會 2 級礦山的。
之后客戶端如果接著有收礦的操作,那么該收取多少礦石,這個增益 buf 是有效的。
服務器在收到這個操作請求時,應該先檢驗礦山等級基準是否一致,不一致可以駁回玩家的操作,但也可以保留。
例如:玩家認為礦山等級是 2 級,他的投資是針對 2 級礦山的;而這個時候同時有別的玩家搶先投資的礦山,導致礦山先被升到了 3 級;3 級礦山繼續投資是需要更多金幣的,玩家發起操作的時候并沒有考慮投資更多的錢,這個時候就應該取消該玩家操作,讓玩家的客戶端接下來回滾。
但是,在玩家投資的同時,也可能有另一個同公會玩家退會,導致礦山降級(這是 Shop Heroes 的一個游戲規則),如果礦山被降為 1 級,其實玩家是可以用更少的金幣投資的,這時,讓操作繼續可能是更體貼的做法;只是 buf 的效果也降低了。
還有另一種情況:玩家的投資基準沒有變化,但投資結果不同。比如礦山原來是 2 級,玩家的投資不足以讓礦山升級。但是有一個玩家同時投資了一筆錢,加上這筆投資,礦山恰好可以升到 3 級。那么投資產生的 buf 效果就是 3 級而不是 2 級。
接下來,服務器對自己的本地 personal model 應用這個操作。成功后(通常是在這里做一次金幣數量檢查,防止金幣不夠),應該由服務器重新計算當前礦山等級帶來的增益 buf 等級,這可能和玩家自己認為的不一致。如果和玩家提交的參數一致,直接 pass ;不一致的話,應該通知客戶端取消掉操作,并生成一個同時刻的,帶有正確 buf 等級參數的指令發送給玩家。
最后,服務器再針對 shared model 生成一個投資礦山的操作指令,并廣播給所有人。這樣便完成了整個流程。
以上,討論了“投資礦山”這個行為,即影響了玩家個人數據,又影響了公會數據,同時公會數據會作用于玩家個人數據變化。我們該如何處理這類問題,可以讓客戶端不依賴服務器確認,先行表現。
針對特定 model 的操作指令,一定要遵循不得訪問該 model 之外的數據;而在個人數據和公會數據兩個獨立的 model 發生交互時,必須在構建操作指令前訪問 model ,并以參數的形式打包在操作指令中。若服務器和客戶端計算出不同的參數,服務器可通過取消客戶端已做過的操作,生成同時刻的新操作來糾正客戶端。
來自:http://blog.codingnow.com/2016/10/gamesync.html