1. 程式人生 > >實時對戰網路遊戲--基於幀同步的最佳實踐

實時對戰網路遊戲--基於幀同步的最佳實踐

網路遊戲概述

  網路遊戲的發展始於90年代。歷經超過20年的發展,遊戲結構和內容發生了天翻地覆的變化。自2005年以後,網路遊戲的結構逐漸趨於穩定。網路遊戲從聯網特性上,可以大致分為弱聯網和強聯網兩大類。弱聯網,如大部分的頁遊,部分的手遊。除此之外的網路遊戲,如MMORPG,FPS/TPS,RTS等,都屬於強聯網。弱聯網遊戲,結構相對簡單,已經有大量前人的文章進行了分析,這裡就不再贅述。本文將對強聯網遊戲進行分析,並針對其中的幀同步方式,進行深入解說。

  強聯網遊戲,遊戲種類比較典型的有MMORPG,FPS/TPS,RTS這3大類。

  MMORPG遊戲

  一般使用權威伺服器結構,典型的CS架構。遊戲使用基於視野控制的區域資料同步,遊戲的模擬精度較低,遊戲整體延遲容忍度較大。一般單個場景,可以有2000個玩家,單位數量在10000以內,玩家可視同步數量在60-100,同步間隔在500ms。伺服器承擔遊戲核心資料的運算。伺服器上通常只有場景的可行走性,區域,觸發器等關鍵資料,無整體空間資料。客戶端作為遊戲世界的視窗,將遊戲內容呈現給玩家,並對玩家輸入,進行反饋。遊戲運算邏輯的分擔設計,主要考慮玩家體驗,伺服器負載及反外掛需求。如果核心邏輯全部都由伺服器計算,則安全性最佳,但客戶端玩家操作的模擬,及多客戶端之間由於同步延遲導致的畫面不一致,將比較巨大。典型如國內武俠類遊戲,刀光特效都在1米以上,有效攻擊檢測基本都在2米以上,這樣做是為了就算兩個玩家看起來位置很接近,但實際離很遠,也能保證命中。不同遊戲,主要是在位置移動同步,技能表現,動作表現這塊進行調優,但所使用的策略基本大同小異。

  FPS/TPS遊戲

  戰鬥部分一般使用HOST/CLIENT架構。遊戲使用全場景活動單位的同步(狀態,差值),遊戲模擬精度較高,遊戲對延遲的容忍度低。一般單個場景,活動單位數量在40個以內,同步間隔在50ms以內。主機承擔移動驗證及廣播同步,命中計算(驗證)及廣播同步等邏輯,主機有完整的遊戲場景資料。客戶端負責遊戲玩家的可視及操作反饋,移動邏輯一般在客戶端直接執行,主機只負責驗證。該類遊戲為了玩家的操作的體驗,甚至可以犧牲部分安全性,將射擊等部分關鍵邏輯,直接在客戶端運算執行,伺服器只進行後置有效性驗證。

  RTS遊戲

  一般使用基於幀同步的架構,包含幀同步伺服器及玩家端。幀同步伺服器,只負責控制命令的佇列化及幀資料的廣播。每個參與戰鬥的玩家端,都執行整個遊戲世界。遊戲使用幀同步技術,遊戲的精度高,資料同步量低,遊戲場景活動執行的單位數,不受網路同步限制,可以在1000以上。每個玩家,作為對等端參與遊戲的執行,玩家的操作將作為控制命令,傳送到幀同步伺服器,伺服器把命令插入某個遊戲幀,廣播給所有客戶端。

  以上大致介紹了3種類型遊戲的結構及同步方式。這裡對同步方式進行下總結,MMORPG同步60個單位,每秒同步2次。FPS/TPS同步40個單位,每秒同步30次。RTS遊戲,同步遊戲邏輯幀,不同步遊戲單位(遊戲單位數量對網路同步量無影響),每秒同步30次。

  3種方式,沒有優劣的區別,只有適應某種型別遊戲與否。從開發及維護難度上,各有自己所面臨的挑戰。但幀同步,因為部分特殊性,在3種類型的方式中,在維護難度上可能是最大的。以下我們開始詳述幀同步所面對的挑戰及解決方法。

  幀同步的挑戰

  大家可以想象一下,N個客戶端獨立執行整個遊戲,他們之間唯一的交集,就是有一致的驅動幀(控制命令)。每個客戶端的遊戲世界獨立模擬,在模擬過程中,如果發生了哪怕一個非常細微的差異,在最後的遊戲演變及結果上,都會造成巨大的不一致。這個就是幀同步開發及維護最大的挑戰。

  在開始說明我們要怎麼做之前,這裡對幀同步,這個比較模糊的概念,下一個更為明確的定義。我們這裡所要討論的,實際上可以定義為: 基於幀同步的對等計算。所有參與計算的對等端,有一致的開始,一致的變化過程,最終會有一致的結果。

  一致的開始: 遊戲有多個玩家,多個陣營。每個玩家,執行整個遊戲世界,遊戲邏輯相關的所有資料及程式邏輯模組完全一致。

  一致的變化過程:a. 遊戲中的邏輯驅動流程完全一致。b. 遊戲中邏輯資料變化,完全一致。

  每個參與遊戲的玩家,可以說是通過某個特定陣營視窗,對遊戲世界進行的觀察及操作。遊戲世界,所有人都一致,遊戲視窗可以有不同的表現。

  以上給出了明確的定義。在設計上,我們可以把遊戲拆分為邏輯部分及表現部分。遊戲邏輯部分,稱為服務端邏輯;遊戲表現相關,稱為客戶端表現邏輯。這裡強制加入服務端,客戶端概念,是希望讓開發者,在進行開發設計時,把這兩個作為獨立部分進行處理。在編碼上,甚至可以把它們放在兩個獨立工程下。服務端邏輯部分,表示完整的遊戲本身,在校驗伺服器進行邏輯後校驗時,也只會執行邏輯模組相關內容。客戶端表現部分,會只讀使用服務端的資料(本來就在同一個程序內),並通過一個特殊介面傳送控制命令給服務端。由此在相同的服務端邏輯基礎上,客戶端表現可以是2D,也可以是3D,在實際開發中我們做過類似的適配。

  現在可以開始我們的旅程了: 如何確保我們開發的邏輯在所有對等端都有一致的開始及一致的變化。如果發生了不一致,我們如何能檢測到。我們需要一個框架來幫助我們解決這些問題,我們先把這個框架命名為IGameKernel。

  遊戲一致的開始

  遊戲框架服務端邏輯部分,在所有對等端中,根據一致的初始化引數GameStartDocument,進行一致的初始化構造,如場景,物件,邏輯模組等。如此遊戲會有一致的開始。

  遊戲資料變化一致性

  遊戲資料型別,包括int,bool,float,string。其中int,bool,string運算不會有一致性問題。

  對於需要確定性的幀同步,浮點數是一個坑。在不同的CPU架構,不同編譯器版本,在乘法,除法,精度控制上,都可能具有一定程度的不一致。雖然理論上,只要編譯器及處理器都遵循IEEE754標準,就能夠確保浮點數的一致性。但在手遊環境下,晶片種類繁多,執行的標準,尤其是對浮點數運算器的優化,都有一定的差異。因此要確保浮點數運算結果的完全一致,就算強制呼叫類似_controlfp(_PC_24,_MCW_PC);_controlfp(_RC_NEAR, _MCW_RC);程式碼,也不一定有效果。因此,能不使用浮點數,就不使用浮點數。如果必須使用浮點數,則一定要使用float64。在我們專案中,大約萬次級別的測試中,遊戲單位數量200以上,平均戰鬥3分鐘,遊戲過程及結果都能保持一致。

  浮點數的完全確定性,在手遊環境下是難以做到的。但在實際執行環境中,float64精度引起的誤差,也許1億盤遊戲裡會有一盤直接影響遊戲結果。

  另外浮點數相關的函式庫,也是一個問題。數學庫,如cos,sin等,甚至浮點數fmod,都需要自己實現一套,與環境編譯器無關的例項。另外開方等高階數學計算,因為存在cpu及數學庫的依賴性,我們也需要自己手動實現相關函式,並保證結果的一致性。

  其他關於資料運算,如隨機函式等,也需要使用框架提供的介面,以保持一致性。

實時對戰網路遊戲–基於幀同步的最佳實踐 …
開方函式

  遊戲邏輯驅動一致性

  遊戲驅動,必須完全由IGameKernel框架完成。幀同步的細節由框架全部隱藏,在使用者介面層,完全不體現關於幀的概念,開發者只關注通用意義上的驅動點。在結構設計上,框架把遊戲邏輯實體分為GameObject,GameModule,GameObject包含所有資料,GameModule負責邏輯執行。框架提供的邏輯驅動點,只會包含command, heartbeat, event,critical, rechook這些。command:控制命令,heartbeat:心跳,event:框架內部事件,critical:遊戲物件屬性變更事件,rechook:遊戲物件表格資料變更事件。通常的執行流程如下,框架執行某一個邏輯幀,邏輯幀包含兩個部分,時間及玩家控制命令。框架會投遞玩家控制命令進行執行,然後內部心跳管理器進行心跳更新。心跳更新時,所有註冊的心跳被驅動,在心跳執行邏輯時,可以通過傳送command執行各種定製行為;當有GameObject屬性或表格資料發生變更時,也傳送變更事件,並執行相關邏輯。該框架提供的機制強度,足夠支援類似WAR3這種複雜度的遊戲開發了(實際使用過程中開發過類似複雜度遊戲)。

  為了保證執行過程的一致性,框架只是第一步。我們還需要確保遊戲邏輯呼叫是一致的。這裡關注幾個常見項: 排序演算法是穩定一致的,容器遍歷順序是一致的。

實時對戰網路遊戲–基於幀同步的最佳實踐 …
kernel事件驅動回撥

  幀同步一致性檢測

  以上我們大致講解了如何保持幀同步遊戲的一致性。但是對於幀同步開發,哪怕開發團隊很有經驗,也很難避免出現因為人為疏忽,或者經驗原因,導致的遊戲不一致。此時我們需要一種手段能在最短時間,以最小代價,通過可重現的方式,自動化的幫助我們找到不一致點。

  騰訊王者榮耀團隊,有一個後臺自動戰鬥及日誌比對系統,該系統能解決部分問題。但這種定位還不夠精確。

  我們希望能直接鎖定發生不一致的幀,並且精確鎖定哪些遊戲物件的哪些屬性發生了不一致,該幀在哪個呼叫流程發生了不一致。而且最關鍵的,我們希望能通過程式,直接除錯該不一致發生的過程。滿足以上特性的框架,能把幀同步開發最大的挑戰,有效解決掉。

  這個是能做到的麼,當然可以。這裡給出我們的解決方案。

  遊戲物件,類體系結構由框架維護,即GameObject的所有型別,屬性,表格定義,由框架維護。框架給出類似Get/Set介面,對某一個物件的命名屬性進行操作。整個遊戲的執行,由框架驅動並檢測。框架在每一幀執行完成後,抓取遊戲內所有GameObject物件的資料,以及該幀遊戲執行驅動流程資料,打包上傳到幀同步伺服器。幀同步伺服器在收集到某幀所有玩家提交的遊戲資料後,對資料進行比對,分別比對所有GameObject的屬性及表格,並比對所有的事件驅動流。如果發生不一致,則通知玩家端,不一致發生的完整資料資訊(把該異常幀,所有玩家的資料都下發)。玩家端收集完異常資料後,遊戲結束,並輸出遊戲錄影,同時輸出幀同步異常比對文件。文件中打印出遊戲內所有遊戲物件的資料及遊戲執行流程資料,並標註清楚相同及不同標記。

  在專案測試階段,使用PC模擬端,IOS裝置,ANDROID裝置,執行自動匹配戰鬥指令碼。當有幀不一致發生後,自動輸出錄影及異常資訊文件。測試完成後,把相關的錄影及文件發給專案組,專案組可以安排人員進行問題調查及重現,對複雜問題,也能通過異常錄影直接除錯。

實時對戰網路遊戲–基於幀同步的最佳實踐 …
幀同步檢測文件

  幀同步的主要關鍵問題

  幀同步技術挑戰與解決方案,到這裡可以告一段落。在實際應用場景中,我們通常還會關注以下關鍵問題: a. 幀同步結構。 b. 遊戲重入性。 c. 網路優化。 d. 遊戲錄影。 e. 反外掛。 f. 第三方庫使用。

  幀同步結構

  傳統的幀同步,使用鎖幀技術。這個技術的要點,是在進行廣播某一幀時,在這之前必須收到所有對等端前置的通知應答。這麼做的理由只有1個: 保證公平性(大家程序一致,要卡一起卡)。在當前遊戲領域,我們更關注的是一個玩家卡了,不能把其他玩家也卡住。因此我們採用樂觀幀鎖定方式。玩家操作,會以command形式,傳送給幀同步伺服器,伺服器會把command插入某一個邏輯幀frame,然後在合適的時機把frame廣播給所有玩家,遊戲世界一致執行。在這個結構上,幀同步伺服器,是以固定間隔把frame進行廣播,某一個玩家卡了或者掛了,對於其他玩家是完全不可見,也是無影響的。

  遊戲重入性

  重入性,斷線重連及客戶端重啟後遊戲恢復,都是依靠幀同步伺服器來保證的。在伺服器上,儲存遊戲開始到當前的所有遊戲幀。這樣斷線重連就很簡單了,客戶端在網路斷開恢復後,請求伺服器重新發送從X幀開始的幀。伺服器收到請求後,傳送X到最新的邏輯幀到客戶端,客戶端快速執行完成這些幀,然後遊戲繼續執行。客戶端重啟也類似,客戶端重啟後,伺服器通知客戶端正在遊戲中,客戶端開始載入遊戲,並請求載入遊戲幀,伺服器傳送所有遊戲幀,客戶端快速執行到最新幀,遊戲就可以繼續執行了。遊戲旁觀,玩家中途加入,也採用類似方法。

  網路優化

  幀同步對於網路優化,是一個大問題。這裡涉及幾個點: 網路延遲及抖動處理,網路同步量及伺服器承載優化,玩家操作延遲優化。遊戲世界由邏輯幀,直接驅動,在不做優化的情況下,如果網路發生抖動,邏輯幀的到來時快時慢,對於玩家體驗而言是致命的。在這裡,先直接給出我們的做法: 幀聚合及幀延後執行技術。

  幀聚合可以簡單理解為把多個幀,集合起來傳送。幀延後執行,大致是本地緩衝部分邏輯幀,按照玩家本地速率執行,把網路抖動抹平。

  關於網路優化,幀同步使用UDP還是TCP, 這個問題是被很多人所討論的,這裡也給大家一個參考結論。幀同步,需要可靠傳輸,因此網路傳輸層一定要具有可靠性。另外,部分基於幀同步製作的遊戲,還希望要能有最短的使用者操作反饋延遲,比如100ms以內。綜上,我們簡單分析下兩個協議。TCP是可靠傳輸協議,但是TCP協議,對於網路丟包重傳機制,是基於網路公平性,總流量較少來設計的,其RTO機制,在發生丟包時,需要較長時間恢復。TCP底層實現過於複雜,無有效引數來直接設定RTT或者RTO超時性,對於減少丟包後的恢復時間,基本無解。UDP是不可靠傳輸協議,甚至可以認為只是在IP層上做了一個基於分包的簡單處理,不對連結管理,傳輸可靠性進行任何處理。這個也是UDP的優點,純淨無新增。在進行幀同步的底層開發時,也可以使用UDP協議,自己開發連結管理及可靠性傳輸(這裡不需要開發NAT穿透等功能,因此難度一般),並把RTO,RTT按照最快速度恢復策略,定製編碼即可。最終實現一個簡化的,基於最快速度恢復丟包的,TCP like協議。

  最終,幀同步網路底層,因為都是使用可靠傳輸協議,使用TCP還是UDP,在框架庫中,只是一個初始化引數的問題。介面層可以完全一致,因此對於最終開發使用者,TCP還是UDP,只是執行期配置引數的問題。至於到底選哪個,要根據專案情況而定。如果是希望玩家操作延遲最低,則可以配置為使用UDP。如果是希望幀同步伺服器負載能力最大化,則配置為使用TCP(用UDP封裝簡化的類TCP網路庫,系統執行期網路消耗及CPU消耗,都會大於原生的TCP介面)。

實時對戰網路遊戲–基於幀同步的最佳實踐 …
網路服務層服務控制代碼

  遊戲錄影

  遊戲錄影功能,是幀同步天賦屬性(其他遊戲種類都不具備該屬性)。唯一需要關注的,就是錄影壓縮。在幀聚合中,把多個幀壓縮為了一個幀,但這還不夠,我們在錄影中,只記錄存在有效幀的資料。然後再把該錄影序列化,再通過類似zip等壓縮方式,做整體打包。通過以上方法,類似皇室戰爭複雜度的遊戲,一場戰鬥,處理過的錄影大小,基本能控制在1k位元組以內。

  反外掛

  反外掛是一個很大的議題。幀同步結構中,所有資料都在玩家本地,理論上玩家可以任意修改這些資料。這裡不討論傳統的加殼及反除錯技術。這裡討論在實際開發中,幀同步框架能夠通過什麼方法來解決該問題。框架能提供至少3種保護: a. 關鍵資料保護,b. 虛擬化, c. 伺服器後驗證。關鍵資料保護可以有很多技術,框架對核心資料,可以做記憶體加密,記憶體多拷貝冗餘保護等。框架提供虛擬化技術,也是一個不錯的選擇,部分程式碼可以在虛擬機器(lua)中直接執行,破解難度會增加(前提是資源保護足夠)。伺服器後驗證是殺手鐗,驗證伺服器能運行遊戲錄影,並直接得出遊戲戰鬥結果,任何作弊都無所遁形。

  因此對於幀同步,反外掛相對是一件比較容易的事情。遊戲過程中,玩家作弊只會影響到自己,不會影響到他人。遊戲結算時,當伺服器檢測到玩家之間遊戲結果不一致時,通過驗證伺服器,對遊戲錄影進行驗證計算,很容易就能發現是哪個玩家發生了作弊。

  第三方庫使用

  遊戲開發中,通常會用到很多第三方庫。比如物理引擎,尋路引擎等。在幀同步開發的專案中,邏輯模組所使用的任何庫,都需要滿足確定性原則,即對於某個輸入,必須要有一致的輸出。因此對於第三方庫的使用上,一定要慎重。而這也潛在的增加了專案開發及維護的成本。比如絕大部分物理庫,尋路庫,如havok,kynapse就無法直接使用。

實時對戰網路遊戲–基於幀同步的最佳實踐 …
笑臉-來源網路

  幀同步總結

  到這裡,關於幀同步的開發已經講解的差不多了。幀同步技術很容易用,但要用好,卻不是那麼簡單,而且如果用不好,對專案甚至可能是致命的(有做射擊遊戲的,使用幀同步,專案死在物理庫上的)。本文將實踐中,遇到的最常見的挑戰與解決方案,與大家進行了分享。如果在開發中碰到困難,可以聯絡我們進行諮詢。如果本文所闡述的內容有謬誤的,也歡迎指出。