在多人版的遊戲開發過程中,我們會經常碰到這樣一個問題:由於每個客戶端網絡環境差異導致接收服務器消息的時間不同,就會導致多個客戶端呈現的畫面不同(即畫面不同步),例如:以彩期開獎爲例,客戶端A已經收到開獎結果的推送了,但客戶端B沒有收到,如果不做任何處理,會導致後面畫面的差異越來越大。
因爲網絡環境的差異是一個客觀的問題,所以我們並不能保證每個客戶端能在同一時間收到服務器推送的消息。但我們可以在代碼邏輯上可以避免差異的擴大化,儘量保證之後的差異不受前面的影響。
另外一個大前提:既然是多人版,就應該用的是長連接。
短連接:是指通訊雙方有數據交互時,就建立一個連接,數據發送完成後,則斷開此連接,即每次連接只完成一項業務的發送。
長連接:多用於操作頻繁,點對點的通訊,而且連接數不能太多情況。每個TCP連接都需要三步握手,這需要時間,如果每個操作都是短連接,再操作的話那麼處理速度會降低很多,所以每個操作完後都不斷開,下次處理時直接發送數據包就OK了,不用建立TCP連接。
舉一個很容易理解的例子:長短連接可比作爲即時通話和語音。
本文以彩期下注開獎爲例,做簡單的同步,能基本保證畫面同步。(長連接)
上圖 gameStart爲遊戲開始(主動請求)、stopBet爲停止下注(服務器推送)、gameResult爲開獎結果(服務器推送)。
這裏gameResult和gameComplete之間的時間是提供給客戶端呈現開獎畫面的時間(例如:轉盤滾動到指定位置)
遇到的主要問題:
1.倒計時不同步(start和result結果之間會有一個開獎倒計時)
2.客戶端由於網絡延遲導致收到result消息晚了,導致畫面不同步。(這裏是收到結果之後纔開始播動畫)
3.兩局之間銜接得不是很好(之前的做法是等到了gameComplete,也就是開獎動畫結束後纔開始請求下一局遊戲,如果這個請求延遲了就會導致剛開始就畫面不同步的問題)
首先處理問題3:直接給請求下一局預留緩衝,在拿到當前Result時候就去請求下一局的信息並保存下來。假若gameResult和complete之間有20s的時間,那麼給這個請求和接收預留的緩衝時間就是20S。再假設20S都沒接收到、那麼建議是提示玩家重新連接遊戲。
問題1和2有一個共性的問題就是:網絡延遲。具體一點就是…算了,舉例來說吧:假設客戶端A給服務器發了一個gameStart的請求,發送這個請求的時間是T0。服務器在S0時收到這個請求,給客戶端A發送應答消息。而客戶端A收到這個請求的應答時間是T1,在這個gameStart的response數據中,服務端會告訴你它的當前服務器時間S1以及開獎時間S2。(用來做倒計時用)
不難看出,因爲服務器時間是一直在走動的,如果發送和接收的時間差比較大的話(例如發送和接收均消耗10S),可能造成你收到的服務器時間誤差較大。(你收到的是S1,其實當前服務器時間是S1+10)
有很多人的做法是取 S1+(T1 - T0)/2 作爲當前服務器時間,這樣做有一定的效果,但是當發送和接收消耗的時間差距較大時(例如發送到服務端時間爲1S,消息應答從服務器返回消耗了19S),按上述計算出當前服務器時間爲S1+10,但實際卻爲S1+19。
Service.gameStart(data,this._newGame,this);
this._sendTime = new Date().getTime();
_newGame:funciton(data){
this._receiveTime = new Date().getTime();
var currTime = data.currTime;//服務器當前時間
var curServerTime = 0;
var timeDiff = (this._receiveTime - this._sendTime)/2;
if(timeDiff < 500 || Math.abs(currTime - ServerTimeUtils.getServerTime()) > 1000){
curServerTime = ServerTimeUtils.updateServerTime(currTime+timeDiff);
}else{
curServerTime = ServerTimeUtils.getServerTime();
}
}
思路,即本地記錄保存服務器時間LS,設置個clock讓其一直走。
在上述公式基礎上,加上判斷:如果發送接收時間差小於1S,就按公式來即S1+(T1 - T0)/2。
如若大於1S,則服務器時間按LS來。
“Math.abs(currTime - ServerTimeUtils.getServerTime()) > 1000”這個判斷只是爲了第一次的LS初始化。
ServerTimeUtils(全局變量):
var ServerTimeUtils = {
_serverTime: new Date().getTime(),
serverTimeClock:function(){
var self = this;
global.timers.setInterval(function(){
self._serverTime = self._serverTime + 1000;
},1000)
},
getServerTime: function(){
return this._serverTime;
},
updateServerTime: function(newServerTime){
this._serverTime = newServerTime;
}
}
ServerTimeUtils.serverTimeClock();
問題2的遺留問題:即接收Result的快慢導致開獎轉盤不同步問題,這裏由於我的項目中轉盤做的變減速運動、很難保證每個時間點轉盤畫面都同步。這裏我是採取了一個折中的辦法
var turnTime = this._turnSeconds;
var timeDiff = resultTime - ServerTimeUtils.getServerTime();//resultTime是服務端返result時返的服務器當前時間
if(Math.abs(timeDiff) > 1000){
if(resultTime > ServerTimeUtils.getServerTime()){
turnTime = turnTime - Math.abs(timeDiff)/1000;
}
}else{
ServerTimeUtils.updateServerTime(resultTime);
}
思路:如果接收result時間慢了幾秒,那麼就讓轉盤轉到目的位置的時間少消耗幾秒。(即提高了速度)
這樣保證了轉盤指針到目的地的時間是一致的,即多個客戶端即使受到result時間不同,到達目的地的時間也相同。這樣做保證能同時進入下一局遊戲。
時間畫面同步個人感覺是一個比較複雜的問題、特別是前端邏輯以及動畫比較複雜的環境。上述只能處理較簡單的小遊戲問題,而且還有部分極端情況未做處理(用重新登錄去處理)。