1. 程式人生 > >動作手遊實時PVP幀同步方案

動作手遊實時PVP幀同步方案

1、概述

1.1、基於UDP的幀同步方案

  在技術選型方面,之所以選擇幀同步方案,在Kevin的一篇介紹PVP幀同步後臺實現的文章中已經做了詳細敘述,這裡簡單摘要如下:

  高一致性。如果每一幀的輸入都同步了,在同樣的上下文中,計算得出的結果應該也是同步的。

  低流量消耗。除了幀同步,其它方案(比如狀態同步)想做到高一致性,需要同步非常大量的資料。無論是對於行動網路,還是固絡都是不合適的。

  伺服器邏輯簡化。採用幀同步方案,伺服器只需要做簡單的幀同步,不需要關心太多的業務細節。有利於客戶端功能的擴充套件和伺服器的穩定和效能。

  反作弊。客戶端只需要在適當機時上報校驗資料給伺服器,伺服器對

2個客戶端上報的資料進行對比,就可以快速識別是否有人作弊。然後通過無收益的方式間接防止作弊。

  那麼,為什麼選擇UDP而不是TCP呢?主要有2點原因:

  弱網路環境。

  實時性要求。

  我們通過一個測試APP,在WIFI4G環境下,採用TCPUDP兩種方式連線同一個伺服器,分別獲得對應的RTT進行對比。

 

  我們可以發現,在弱網路環境下,UDPRTT幾乎不受影響。而TCPRTT波動比較大,特別是受丟包率影響比較明顯。

1.2、基於UDPFSP協議棧

  由於UDP具有不可靠性,所以在UDP的基礎上實現一個自定義的協議棧:FSP,即FrameSyncProtocol

FSP

的基本原理就是防照TCPACK/SEQ重傳機制,實現了傳輸的可靠性,同時還採用冗餘換速度的方式,又保證了傳輸的**速率。在幀同步方案中一舉兩得。

2、技術原理

2.1、幀同步技術原理

  如下圖所示,客戶端A的操作A1與客戶端B的操作B1封裝成OperateCmd資料傳送給PVP伺服器。PVP伺服器每66MS產生一個邏輯幀,在該幀所在時間段內收到A1B1後,生成一個Frame資料塊,在該幀時間結束時,將Frame傳送給客戶端ABFrame資料塊內有該幀的幀號。客戶端AB收到Frame資料後,便知道該幀內,客戶端AB都做了什麼操作。然後根據收到的操作A1B1進行遊戲表現,最終呈現給玩家

AB的結果是一致的。從而實現客戶端AB的資料同步。

 

1 幀同步技術原理

2.2FSP協議棧原理

  如下圖所示,傳送者維持一個傳送佇列,對每一次傳送進行編號。每一次傳送時,會將待發送的資料寫入佇列。然後將佇列裡的資料+編號傳送給接收者。

  接收者收到資料後,會將該編號回送給傳送者以確認。傳送者收到確認編號後,會將該編號對應的資料包從佇列中刪除,否則該資料仍儲存在傳送佇列中。

  下次傳送時,會有新的資料進入佇列。然後將佇列中的資料+最新的編號傳送給接收者。以此迴圈反覆。

 

2 FSP協議棧原理

上圖解析:

  第1次傳送,在傳送佇列裡只有Data1,於是將Data1和編號1Seq=1)傳送給接收者。收到確認編號1Ack=1)後,將Data1從佇列中刪除。

  第47次傳送,由於從第4次傳送開始就沒有收到確認編號,於是佇列中包含了Data4Data7。第7次傳送後,收到確認編號6,於是將Data4Data6從佇列中刪除。

  第8次傳送,佇列中包含Data7Data8。傳送後收到確認編號8,從而將Data7Data8從佇列中刪除。

  以上的關鍵點是,傳送者未收到確認編號,並不一直等待,而是會繼續下一次傳送。結合圖1

  如果傳送者是伺服器,則會每隔66MS會將一個Frame資料寫入傳送佇列,然後將該佇列裡的所有Frame資料一起傳送給客戶端

  如果傳送者是客戶端,則會在玩家有操作時,將玩家的每一個OperateCmd資料寫入傳送佇列,然後將該佇列裡的所有OperateCmd資料一起傳送給伺服器。如果傳送佇列不為空,則每隔99MS重複傳送。如果傳送佇列為空,則不再發送。直到玩家下一次操作。

  由於伺服器和客戶端即是傳送者,又是接收者。則伺服器和客戶端的每一次傳送,除了會帶上該次傳送的編號,還會帶上對對方傳送編號的確認

3、技術實現

3.1、整體框架

 

3 PVP通訊模組整體框架

  這是一個典型的手遊PVP通訊模組的整體框架。這裡主要分享一下FSP模組和幀同步模組的技術實現。

3.2FSP模組

FSP模組主要用來實現FSP協議棧。其協議格式定義如下。

FSP上行協議定義:

Seq

Ack

SomeData

OperateCmd List

CheckSum

FSP下行協議定義:

Seq

Ack

Frame List

CheckSum

  如下圖所示,是FSP模組的接收邏輯流程。


4 FSP模組接收邏輯流程

  其中關鍵點是:

  對Recv New Ack判斷,對曾經發送過的Operate進行確認刪除。

  對Recv New Seq判斷,過濾掉因為網路問題造成亂序的包。

  上圖中,接收到的Frame最終都儲存在RecvQueue中。我們將接收邏輯放在子執行緒中。所以只需要在主執行緒中需要Recv的時刻從RecvQueue中讀取FremeList即可。

  如下圖所示,是FSP模組的傳送邏輯流程。傳送邏輯同樣放在子執行緒中。傳送邏輯有2種觸發方式:

  業務層主動呼叫傳送

  每隔指定時間觸發一次(在WIFI4G下使用不同的時間,可以減少伺服器收到的純確認包比例,有利於提高通訊效能)


5 FSP模組主動傳送邏輯流程

 

6 FSP模組定時傳送邏輯流程

3.3、幀同步模組

  下圖是幀同步模組的實現框架。

 

7 幀同步模組實現框架

  按照上圖箭頭編號描述如下:

  (1)負責接收來自FSP模組的FrameList

  (2)將FrameList裡的每1幀都存入FrameQueue

  (3)同時將FrameList的每1幀的幀號進行變換後,得到客戶端幀號。同時,在等下1個伺服器幀到來之前,需要將客戶端的幀鎖定在下1個伺服器幀的前一幀(LockFrameIndex)。然後 將FrameIndexLockFrameIndex傳入FrameBuffer

  (4)客戶端每1幀從FrameBuffer中取出當前可能需要跳幀加速的倍數(SpeedUpTimes)。

  (5)如果SpeedUpTimes0,則表示正在緩衝中,沒有需要處理的幀。如果SpeedUpTimes1,則表示緩衝結束,但是不需要加速,只需要處理最新的1幀。如果SpeedUpTimes大於1,則從FrameQueue裡取出這SpeedUpTimes個幀,將裡面的SyncCmd取出來。

  (6)將SyncCmd傳入OperationExecutor

  (7OperationExecutor與具體遊戲的業務邏輯相關聯,負責將SyncCmd傳入給業務邏輯和預表現模組進行具體的處理。

  其流程圖如下:

 

8 幀同步邏輯流程1

 

9 幀同步邏輯流程2

4、最新優化

4.1、斷線重連優化

  在傳統網路模組開發思想中,當傳送超時達到閥值,或者底層判定斷開連線時,需要重新建立連線。之前這部分工作是交給一個偏上層的模組來執行,該模組需要等Apollo通訊模組連線成功之後,才進行PVP通訊模組的連線。這樣使邏輯變得複雜。

  由於UDP本身的不可靠性,可以認為網路斷線也是其不可靠性的一部分。

  而FSP協議棧就是為了解決UDP的不可靠性而設計的,所以也附帶解決了斷線重連問題。

  去除了原來的斷線重連邏輯之後,用FSP模組本身的特性來處理斷線重連,實測能夠提高網路恢復的響應速度。由於PVP伺服器設定的超時閥值是15秒,有些時候,其實網路已經恢復,但是由於Apollo通訊模組對網路的恢復響應過於遲鈍,造成不必要的判輸。

4.2、接入GSDK

  從目前接入GSDK後的資料來看,能夠減少一定的網路延時,但是並不明顯。

4.3AckOnly優化

AckOnly優化是指減少伺服器收到的純確認包資料。這樣做的目的是:

  減少包量,有助於在WIFI下節省路由器效能。GSDK有個統計表明,有大概20%多的網路延時是因為路由器效能造成。

  節省流量,一定程度上也可以節省網路裝置效能,同時在4G下為使用者省錢。

  該優化分2部分實現:

  (1)空幀免確認

  (2WIFI延遲確認

  在優化前的AckOnly比例為:57%

  空幀免確認優化後降到:38%

WIFI延遲確認優化後降到:25%

5、一些嘗試

  將FSP模組抽象得與業務無關,使之可快速完成一個使用幀同步方案通訊的Demo成為可能。

  實驗了本地區域網PVP對局,只要在同一網段下,可以成功對局。(如果有需求,可以實現該功能)

  實驗了本地藍芽PVP對局,發現藍芽是帶連線態的,並且其通訊是用類似TCP的資料流進行的。同時它與WIFI訊號有干擾,如果開啟WIFI,其延時非常高。在非WIFI下,其單條資料的延時很低,但是如果以66MS的頻率傳送資料,則延時又非常高。

  建立了一套用於FSP線上診斷和斷線診斷的工具。

前言

  我們的遊戲是一款以忍者格鬥為題材的ACT遊戲,其主打的玩法是PVE推圖及PVP 競技。在劇情模式中,高度還原劇情再次使不少玩家淚目。而競技場的樂趣,伴隨著賽季和各種賽事相繼而來,也深受玩家喜愛,從各直播平臺幾萬到幾十萬的觀眾可見一斑。然而,在移動端推出實時PK並不是一蹴而就的,本文將向大家介紹遊戲的實時PVP相關技術。

技術選型

  實時PK的表現方式,是將N個玩家的行為快速同步給其它玩家展示並保持一致性的過程。這裡面涉及到幾個要思考的要點:

·        同步什麼?可以是玩傢俱體操作(如移動操作),也可以是某按鍵操作(如方向鍵),這兩者是有些微區別的。

·        怎麼同步?可以選擇方式多種,傳統的C/S模式,或者是P2P形式,或者是幀同步等。

·        同步方式?載體可以是TCP/UDP。使用哪個比較靠譜?

  基於以上的考量,在遊戲中,使用的是基於可靠UDP的幀同步模型作為實時PVP的技術方案。你問為什麼不採用TCP,為什麼不用C/S,為什麼不上P2P,下文分曉。

實現細則

  這裡講述一些重要細節,以解決眾多的Whynot問題。

使用幀同步模型

  為什麼選擇幀同步,直接原因是繼承之前AI(機甲旋風)經驗,對於ACT型別遊戲,我們認為幀同步是不錯的方案,主要是能夠獲得以下好處:

·        高一致性。對於格鬥中的技能連招,如果不精確到幀,會出現一些詭異現象。試想某個浮空下落的角色,可能一方客戶端看到已經倒地,另一方在未倒地時接上其它技能,會出現兩個結果,即使將其拉扯回,同樣奇怪。而幀同步的機制,保證了雙方客戶端的一致性。

·        伺服器邏輯簡化。傳統的C/S 架構下,在伺服器計算及校驗,大量的核心邏輯需要客戶端及伺服器都實現一遍,使用幀同步可以大大簡化及保證伺服器的穩定性。

·        低流量消耗。在行動網路中,使用者的流量即金錢,如果我們遊戲的核心部分耗流量嚴重,那讓45%1左右的非wifi使用者情何以堪呢?

·        反作弊。講道理來說,幀同步對於反作弊並不友好,但是有一個簡單的做法可以快速反作弊,就是雙方資料不一致時(上報的校驗資料或者戰鬥結果),即檢測到有人作弊。

  那麼,幀同步的過程是如何進行的呢?下圖演示了兩個客戶端PlayerA/PlayerBServer互動的過程。

  對於客戶端而言,不斷的上報其行為(action)至伺服器,並且等待伺服器下發的幀包驅動其邏輯。這種方式是幀鎖定同步(lockstep)的一種演化2對於伺服器來說,固定幀間隔(66ms)將佇列中的PlayerA/PlayerBactions放在一個Frame中,並同步給兩個客戶端,這似乎和BucketSync類似。

  我常被問到一個問題,對方的卡頓會不會影響我的遊戲體驗,從以上我們的同步原理中,或許你可以找到答案。

使用UDP代替TCP

  幀同步並對協議層是TCP還是UDP並無要求,但我們打一開始就沒考慮TCP直接擁抱UDP,究其原因,若是對TCP的特性稍有了解,大概會背那三字經:慢啟動快重傳擁塞避免(三個字!)。我概括它以下幾點不太適合對實時性要求高,對延遲敏感的移動網路遊戲:

·        慢啟動演算法不適合行動網路的情景,在行動網路環境下訊號時好時壞是常態,慢啟動會使資料不能及時快速達到對端。

·        擁塞避免演算法不適合行動網路主要原因是其考慮到網路的公平性及收斂性,並且AIMD 演算法會使實時性大受影響,延遲明顯提升。

  還有TCP協議用於重傳的RTO的指數變化及擁塞演算法的實現Nagle的快取等,都是TCP並不太適合高實時性要求的遊戲玩法的原因,不再一一列舉。

  那麼為什麼UDP對比TCP更合適呢?多說無益,看一組資料:

  顯而易見,在各種網路情形下,UDP的表現(延遲分佈)基本上都優於TCP。測試程式在相同的網路環境下,通過設定一定的延遲,丟包率,抖動等,獲得TCP/UDPRTT3

  對於P2P的選擇,我們放棄的原因是本身UDP打洞並非100%成功,而若打洞失敗則仍要走伺服器轉發,故簡化設計考慮,未選擇P2P方式去同步。

自己DIY可靠UDP

  上面講了用什麼(UDP)同步什麼(幀資料)的問題,有同學要問了,UDP不可靠傳輸,丟包怎麼辦?當然,為了資料一致,我們不允許(或許可以允許少量)丟包,TCP的可靠性是由ack/seq+重傳去保證的,世面上大多數的可靠UDP實現,也都是類似原理。

  在考察了幾個可靠UDP的實現(UDTENet等)覺得略顯複雜,並且在我們開發時,公司內部的可靠UDP實現未達到可使用階段,鑑於自己重新造個輪子並不複雜,於是挽起袖子寫了起來。

  用於可靠UDP的實現,其UDP協議自定義包頭是這樣的:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

//客戶端上行包

typedef struct UdpPkgRecvHead

{

    uint16_t seq; //start from 1

    uint16_t ack; //ack server seq

    uint16_t sid; //player uid

    uint8_t  action_len; //pkg actions contain

}UdpPkgRecvHead;

//...

//伺服器下行包

typedef struct UdpPkgSendHead

{

    uint16_t seq; //inc when frame id ++

    uint16_t ack; //ack client pkg's seq

    uint8_t frame_len; //pkg frames contain

}UdpPkgSendHead;

  基於以上的定義,可靠性保證的過程大概如圖如示:

  客戶端在滿足以下條件之一後,發包給伺服器:

·        玩家有操作時

·        發包後間隔(99ms)未收到回包時

·        收到包一定間隔(99ms)後

  若玩家有操作,則確認資訊隨著玩家的操作上行包捎帶至伺服器 ; 如果無操作,則固定時間上報確認資訊給伺服器。客戶端的seq每一個操作行為(action)時加1,伺服器seq在每一幀資料下發時加1 ,並且雙方的RTO 取值不同4

  對於可靠性的保證,可以採用請求重傳,而我們使用的是冗餘重傳。使用冗餘重傳的一個好處是,簡化了麻煩的時序問題,並且收到的每個包都是完整的順序的。對於網路擁塞情況下的頻寬利用優於TCP,它不足之處是流量略微增加了些。下圖是冗餘重傳的過程:

  圖解如下:

ClientAction1過來,記seq=1,伺服器未收到。Client又新增了Action2,此時新包將同時包含Action1,Action2,並且seq=2Server確認了上一步驟的包,發給Client的包Ack=2表示確認。Client由於某些原因(可能延遲等)尚未收到ServerAck=2的確認,此時新增Action3,併發包seq=3Client再次發Action4時,發現之前已經Ack=2了,故新包將只帶Action3Action4並且seq=4

  這裡演示了冗餘傳輸的過程,伺服器對於收到的包,可以根據seq/ack情況動態去除冗餘或者丟棄過期包。可能你會覺得全冗餘是否不太合適並且有明顯優化空間?在實際現網長期執行中,全冗餘的冗餘率是100%左右,相比於一些可靠傳輸的重發最近三幀等方式,這種為可靠性付出的代價是合適的並且也提高了更多實時性。

  小結:在刨除一些優化及細節外,這就是可靠UDP的機制,簡單有效,開銷極小5。經測試及實際線上執行來看,在弱網路環境下的表現也是不錯的。

反作弊策略

  實現的技術細節告一段落,接下來談談我們的反作弊策略。有些經驗是實踐下的真知:)我們知道幀同步的一切都在客戶端運算了,伺服器能做的顯得很有限。我們不知道玩家當前的位置,不知道玩家的技能使用情況,不知道玩家當前血量,拿什麼來反作弊?

實時的檢測,做了兩點:

  客戶端固定間隔上報雙方血量及技能使用情況,伺服器進行記錄  單局結束,上報勝負關係

  基於這兩點,我們可知道某一幀玩家的血量是多少,每個人都上報自己的及對方(們)的,雙向校驗可看出有有無作弊行為。困擾而不得其解的是,當只有兩人時,判斷誰是作弊者比較麻煩。當兩人以上時,可以仲裁。

  我們做了一點容錯,當勝負結果異常時,才去進一步檢查上報的記錄以判斷作弊者,判輸。而對於上報資料並不一致但是勝負關係一致的情況,記錄日誌來診斷(容易發生在版本變更時)問題。

  通過實時的檢測,基本可以檢測到單局中作弊行為(加速,無限CD,鎖血等),因為他們最終都導致雙方資料不一致而結算不一致或上報血量不一致。

非實時的統計學反作弊方案

  當有一些漏洞可被利用但是一時無法定位的時候,統計學上的反作弊會比較有用。這裡說的漏洞是指通過某種行為使對方掉線或者不發包等未知原因導致遊戲異常結束的行為。

  我們在遊戲結算時,非正常獲勝的(掉線等)都會記錄下來,並且作用於一個懲罰機制。

  每天通過非正常獲勝的次數有上限,達到上限後,其非正常獲勝都將不計。  非正常獲勝的次數作用於實時檢測邏輯,如果雙方資料不一致,非正常獲勝次數多的玩家失敗。  非正常獲勝次數影響玩家進入匹配,次數越高需要等越久才能開始匹配。

  這個方案在線上發揮過作用,有效阻擋了少部分非正常玩家利用漏洞獲益,減少了其影響面。

後話

  上文介紹了遊戲的實時PVP的技術實現,這裡配一個架構圖,看看其外圍。

  有兩點需要說明:

·        TGW的多通接入支援UDP四層,UDP 服務需要監聽所有tunelip/port

·        幀同步的原理,要求我們必須精細化匹配。遊戲中是二進位制版本+資源版本一致才相互匹配,可以做到更精細化的根據出戰忍者雙方客戶端資料hash值是否一致進行匹配。