遊戲中的網絡同步機制(二) 王者榮耀對幀同步的應用

轉載自:https://www.jianshu.com/p/81050871cce7

參考
解密:騰訊如何打造一款實時對戰手遊
從《王者榮耀》來聊聊遊戲的幀同步
《王者榮耀》技術總監覆盤回爐歷程:沒跨過這三座大山,就是另一款MOBA霸佔市場了

縱觀AppStore暢銷榜前十的遊戲,過半都支持玩家實時的PK或者合作攻關。由於實時對戰有玩家之間自發進行強互動的特點,活躍度和社交強度都是比較高,爲遊戲的用戶活躍和流水的提高奠定了堅實的基礎。騰訊的遊戲開發團隊,很早就觀察到實時對戰這一核心玩法對遊戲生命週期影響的重要性,因此在自研產品方面,加大力度開發圍繞實時對戰這一核心玩法的遊戲,從而誕生了《王者榮耀》、《穿越火線·槍戰王者》、《全民超神》、《全民突擊》、《天天炫鬥》等一大批優秀的作品,其中不乏日活躍過千萬的大作。而早期的休閒類遊戲如《全民飛機大戰》等,也加入了實時雙打等遊戲特性,所以現在依然可以經常在AppStore暢銷榜前十看到《全民飛機大戰》這款遊戲的身影。既然實時對戰是一個非常重要的遊戲玩法,爲什麼我們現在看到的許多遊戲,都不具備這一的玩法,或者並不是遊戲的主要玩法?其中一個重要的原因,就是開發實時對戰的功能,在技術上需要有一定的門檻。本文希望能向大家分享騰訊是如何跨過這些門檻,解決實時對戰遊戲開發的一系列核心技術難題。

首先我們介紹實時對戰手遊中最難解決的技術問題——弱網絡下的同步問題。

通過對玩家的遊戲數據進行觀察,發現玩家的遊戲環境存在很大差異,不同玩家會使用不同的2G/3G/4G/Wifi網絡,不同網絡之間的延遲相差很大。另外移動網絡質量不穩定,且都是按流量收費,這些都是需要考慮的問題。手機在網絡間的切換,又會造成底層網絡斷線、地址變化等問題,都是常見的情況。這些問題的統一解決手段,最重要的是通盤考慮各種需求,選擇一個合理的遊戲狀態同步模型。

 

不同網絡間的延時

 

騰訊在大量遊戲開發的實踐中,總結出三種遊戲的同步模型:

一、MMOG模式

這種同步模型,在端遊時代就使用的非常廣泛,特別是MMORPG裏面。

它的主要實現要點是:服務器負責計算全部的遊戲邏輯,並且廣播這些計算的結果,客戶端僅僅負責發送玩家的操作,以及表現收到的遊戲結果。一般來說,玩家發送一個操作到服務器上,服務器根據玩家操作去修改內存中的遊戲世界模型,同時運算遊戲世界對這個操作的反應,然後把這些反應都廣播給相關的多個客戶端,每個客戶端負責把這些數據表現出來給玩家看。

這種做法的優點是非常安全,由於整個遊戲邏輯都在服務器上,服務器只接受合法的玩家操作,一切都經過既定邏輯的運算。另外一個優點是遊戲的邏輯更新很方便,因爲主要邏輯都在服務器端。一般的遊戲玩法需要更新,遊戲開發團隊自己更新重啓服務器就可以了,無需讓千萬個手機去下載更新包。

但是這種做法的缺點也很明顯,首先就是用戶的體驗非常依賴網絡質量,如果一個用戶的網速慢,其他玩家都會發現他在遊戲中明顯的變卡。

另外一個缺點就是服務器負責了太多的遊戲邏輯運算。在動作遊戲裏,服務器往往需要針對二維或者三維空間進行運算。

最後,使用這種同步方案,由於每個遊戲表現都要以數據包發往客戶端,所以當一起玩的用戶數量較多,這種廣播的數據包量就會非常大。

因此根據以上的特點,騰訊一般會在那些同局遊戲人數不太多,但講求玩法變化快和安全性高的遊戲中採用這種同步方案。騰訊自研手遊中比較著名的《穿越火線·槍戰王者》、《全民超神》、《炫鬥之王》都是使用這種方案。

 

image.png

二、主機模式

這種同步方案的做法是:以參與對戰的一個客戶端爲“主機”,其他的客戶端爲“副機”。

遊戲邏輯的主要運算由“主機”完成,所有的“副機”把操作指令,通過服務器中轉,集中發送給“主機”;“主機”完成遊戲運算後,把結果指令再通過服務器中轉,廣播給所有的“副機”。

這個方案看起來有點奇怪,但是卻有很明顯的優點:首先是大量的實時動作遊戲,其遊戲過程的邏輯代碼,都是在客戶端上開發和運行的。客戶端的遊戲引擎對於二維、三維空間中的位置運算、碰撞檢測等功能,都有很好的支持。

因此把整個遊戲邏輯由客戶端負責,就能讓服務器端無需再開發這部分功能。服務器只負責做轉發、廣播的操作,所以能承載的人數和第一種方案有數量級上的差別。由於“主機”客戶端運行遊戲邏輯,所以其體驗是最好的,就算“副機”由於網絡不佳造成體驗下降,對於“主機”來說,只是發現“副機”動作有點遲緩而已。

在以PVE玩法爲主的遊戲中,用戶關注的是自己的體驗,不會太在意同伴的準確動作,這種情況下,主機模式就是一種不錯的同步方案。騰訊的《全民飛機大戰》的雙打模式就是採用這種方式,效果相當不錯。

 

image.png

三、幀同步模式

又叫“鎖步模式”。這種模式用形象的比喻來說,就是把所有參與對戰的客戶端,看成是排成一列的囚犯。這些囚犯們的左腳都被鏈子所在一起,因此他們如果要往前走,就只能同時邁步,如果其中某個人走快了,或者走慢了,都會讓整隊人停下來。

在實現上,一般是以服務器按固定的幀率,來蒐集每個客戶端的輸入,然後把這些輸入廣播給所有的客戶端;由於每個操作指令到達所有客戶端的時間(幀)都是一樣的,所以每個客戶端運算的結果也是一樣的,同樣的輸入就會得到同樣的結果。

這就好像:其他玩家通過網絡,把操作手柄接到你的手機。這種同步方案,是傳統單機-局域網遊戲中最常用的。

這種同步模型的最大優點是:強一致性。每個客戶端的表現是完全一樣的,非常適合高度要求操作技巧的遊戲。由於廣播的僅是玩家的操作,所以數據量很少。不管遊戲中的角色數、狀態量有多大、多複雜,都不會影響廣播的數據量。

但是這個方案也有缺點:對所有玩家的延遲都有要求,一般來說要求在50毫秒以內。如果有一個客戶端網絡卡了,所有的客戶端都要停下來等,大家在玩《星際爭霸》就見識過:一個玩家斷線,全部玩家的遊戲都暫停。騰訊遊戲中的《王者榮耀》、《全民突擊》由於競技性非常強,所以採用了這種方案。

另外在幀同步模式中,數據同步的頻率較高,網絡延遲越小越好。由於TCP的滑動窗口機制和重傳機制,導致延時無法控制,因此幀同步一般採用udp進行網絡傳輸,但udp又會衍生出可靠性問題,對於客戶端,如果某些udp包沒有收到,就會出現丟幀的情況,所以這裏我們自己研發了一套《可靠UDP傳輸》的協議,應用在《王者榮耀》項目。關於《可靠UDP傳輸》的相關技術介紹,後續會作爲專題繼續分享給大家。大體上是如此來解決:

  1. 爲每個數據包增加序列號,每發一次包,增加本地序號。

  2. 每個數據包增加一段位域,用來容納多個確認符。確認字符多少個,跟進應用的發包速率來覺得,速率越高,確認字符的數量也相應越多。

  3. 每次收到包,把收到的包上序列號變爲確認字符,發送包的時候帶上這些確認字符。

  4. 如果從確認字符裏面發現某個數據包有丟失,把它留給應用程序來編寫一個包含丟失數據的新的數據包,必要的話,這個包還會用一個新的序列號發送。

  5. 針對多次收到同一包的時候可以放棄它

image.png

樂觀鎖&斷線重連
囚徒模式的幀同步,有一個致命的缺陷就是,若聯網的玩家有一個網速慢了,勢必會影響其他玩家的體驗,因爲服務器要等待所有輸入達到之後再同步到所有的c端。

另外如果中途有人掉線了,遊戲就會無法繼續或者掉線玩家無法重連,因爲在嚴格的幀同步的情況下,中途加入遊戲是從技術上來講是非常困難的。因爲你重新進來之後,你的初始狀態和大家不一致,而且你的狀態信息都是丟失狀態的,比如,你的等級,隨機種子,角色的屬性信息等。

比如玩過早期的冰封王座都知道,一旦掉線基本這局就廢了,需要重開,至於爲何沒有卡頓的現象,因爲那時都是解決方案都是採用局域網的方式,所以基本是沒有延遲問題的。

後期爲了解決這個問題,如今包括王者榮耀,服務器會保存玩家當場遊戲的遊戲指令以及狀態信息,在玩家斷線重連的時候,能夠恢復到斷線前的狀態。

不過這個還是無法解決幀同步的問題,因爲嚴格的幀同步,是要等到所有玩家都輸入之後,再去通知廣播client更新,如果A服務器一直沒有輸入同步過來,大家是要等着的,那麼如何解決這個問題?

採用“定時不等待”的樂觀方式在每次Interval時鐘發生時固定將操作廣播給所有用戶,不依賴具體每個玩家是否有操作更新。如此幀率的時鐘在由服務器控制,當客戶端有操作的時候及時的發送服務器,然後服務端每秒鐘20-50次向所有客戶端發送更新消息。如下圖:

image.png


上圖中,我們看到服務器不會再等到蒐集完所有用戶輸入再進行下一幀,而是按照固定頻率來同步玩家的輸入信息到每一個c端,如果有玩家網絡延遲,服務器的幀步進是不會等待的,比如上圖中,在第二幀的時候,玩家A的網速慢,那麼他這個時候,會被網速快的玩家給秒了(其他遊戲也差不多)。但是網速慢的玩家不會卡到快的玩家,只會感覺自己操作延遲而已。

 

四、幀同步的技術要點

參考幀同步遊戲開發基礎指南

在一般的幀同步系統中,會有一個Relay Server負責廣播(轉發)所有客戶端的數據。爲了讓各個客戶端能持續的運行,而不是卡住,所以需要定時的下發一個個“網絡幀”數據來驅動各個客戶端。因爲客戶端已經放棄了本地的時間,本地的循環驅動,所以這些“網絡幀”就必不可少了。這些網絡幀大部分實際上是“空”的,只有當玩家有輸入的時候,纔會把玩家的遊戲操作的數據,填入到網絡幀數據包中。對於客戶端來說,就好像有很多鍵盤、鼠標、遊戲手柄在通過網絡操作自己一樣。

一般來說,大多數的遊戲客戶端引擎,都會定時調用一個接口函數,這個函數由用戶填寫內容,用來修改和控制遊戲中各種需要顯示的內容。比如在Flash裏面叫OnEnterFrame(),在Unity裏面叫Update()。這類函數通常會在每幀畫面渲染前調用,當用戶修改了遊戲中的各個角色的位置、大小後,就在下一幀畫面中顯示出來。而在幀同步的遊戲中,這個Update()函數依然是存在,只不過裏面大部分的內容,需要挪到另外一個類似的函數中,我們可以稱之爲UpdateByNet()函數——由網絡層不斷的接收服務器發來的“網絡幀”數據包,每收到一個這樣的數據包,就調用一次這個UpdateByNet()函數,這樣遊戲就從通過本地CPU的Update()函數的驅動,改爲根據網絡來的UpdateByNet()函數驅動了。顯然,網絡發過來的同步幀速度會明顯比本地CPU要慢的多,這裏就對我們的遊戲邏輯開發提出了更高的要求——如何同步的同時,還能保證流暢?

1.幀數據要小
幀同步遊戲中,由於需要“每一幀”都要廣播數據,所以廣播的頻率非常高,這就要求每次廣播的數據要足夠的小。最好每一個網絡幀,能在一個MTU以下,這樣纔能有效降低底層網絡的延遲。同樣的理由,我們爲了提高實時性,一般也傾向於使用UDP而不是TCP協議,這樣底層的處理會更高效。但是,這樣也會帶來了丟包、亂序的可能性。因此我們常常會以冗餘的方式——比如每個幀數據包,實際上是包含了過去2幀的數據,也就是每次發3幀的數據,來對抗丟包。也就是說三個包裏面只要有一個包沒丟,就不影響遊戲。另外我們還會在RelayServer上保存大量的客戶端上傳的數據,如果客戶端發現丟了包(如果亂序了也認爲是丟包),那麼就發起一次“下載”請求,從服務器上重新下載丟失了的幀數據包(這個可能會使用TCP)。這一切,都依賴於每個幀數據要足夠的小。所以我們一般要求,每次客戶端發送的數據,應該小於128字節。你可以大概計算一下,如果我們的遊戲有4個玩家,我們的冗餘是3幀,那麼一個下行的網絡幀數據包大小會到128x4x3=1536字節,而每秒我們發15個網絡幀,那麼佔用的帶寬會到1536x15=23,040字節/秒,加上一些底層協議包頭也就是24kB/s,這個速度看起來已經要求手機是3G網絡才能支持了(實測中GPRS一般很難穩定到這個速度)。
我們使用的遊戲引擎,特別是3D遊戲引擎,裏面使用的位置數據,大多數是浮點數,大家知道,一個浮點數需要佔用8個字節,這可比簡單的整數4個字節大了足足一倍。而我們需要廣播的遊戲操作,往往不需要那麼高的精確度,所以我們應該把這些浮點數,想辦法變成整數來廣播。有時候我們甚至有可能只用1~2個字節(0-256-65535)來表達一個操作所需要的數字(比如按鍵值、鼠標座標)。這樣就能大大降低廣播的數據長度。最簡單的方法,就是把浮點數乘以1000或100然後取整。
另外一個降低廣播數據量的做法就是自己編寫序列化函數:一般現代編程語言,特別是面向對象的語言,都帶有把對象序列化和反序列化的功能。我們要廣播遊戲操作的時候,這些操作往往也是一個個的“對象”,因此最簡單的方法就是使用編程語言自帶的序列化庫來把對象轉換成字節數組去廣播。但是這些編程語言的默認序列化功能,爲了實現諸如反射等高級功能,會把很多遊戲邏輯所“不必要”的數據也序列化了,比如對象的類名、屬性名什麼的。如果我們自己去針對特定的數據對象來編寫序列化函數,就沒有這個問題了,我們可以僅僅提取我們想要的數據,甚至能合併和裁剪一些數據項,達到最小化數據長度的目的。

image.png

 

2.加速播放
在網絡遊戲中,各個客戶端的運行條件和環境往往千差萬別,有的硬件好一些,有的差一些,各方的網絡情況也不一致;時不時玩家的網絡還會在遊戲過程中,發生臨時的擁堵,我們稱之爲“網絡抖動”。網絡遊戲有時候還會需要有中途加入遊戲的需求(亂入),有遊戲錄像和觀看、快進錄像的功能。這些功能,都可能導致客戶端收到“過去時間”裏的一堆網絡幀,因此,客戶端必須要有處理這些堆積起來的網絡數據的能力。最簡單的做法就是加速播放(快進)——如果收到網絡數據處理完遊戲邏輯後,然後在同一個渲染幀(同一次Update()函數裏)內,馬上繼續收下一個網絡數據,然後又立刻處理。這樣往往能在一個渲染幀的時間內,加速趕上服務器廣播的最新遊戲進度。但是這樣做也會有副作用,如果客戶端積累的包太多(比如遊戲已經開始玩了10分鐘,新的用戶中途加入),會導致這個用戶長時間卡住,因爲程序正在瘋狂的下載積累的幀同步包和運算快進。爲了解決這個問題,有些程序員會限制每一個渲染幀中所快進的操作次數,這樣用戶還是能看到畫面有活動。如果實在要快進的進度太多,就要採用“快照”技術,通過定時保存的遊戲狀態數據,來減少快進的進度了。這個快照功能這裏就不展開了。

 

image.png

 

image.png

3.發送玩家操作頻率
一般來說,我們的客戶端的渲染幀率都會大大高於網絡幀的接收頻率。如果我們每個渲染幀都去發送一次玩家操作(比如觸摸屏上的手指位置),那麼可能會導致發送的遊戲操作遠遠大於收到的操作,這樣做要麼會讓遊戲操作堆積在服務器上,導致操作的嚴重延遲,要麼導致下行的網絡包非常大(服務器每次都把收到的所有操作一次下發),這樣會讓網絡帶寬佔滿,同樣是會感覺延遲。不管怎麼處理,都是不太好的結果。正確的做法應該是控制發包頻率,最好是至少收到一個網絡下行幀,才發送一個上行的遊戲操作,避免堆積。另外,剛剛講到的“快進”,如果我們在快速播放遊戲邏輯的時候,每次播放同時也採集玩家輸入去發送,那麼同樣會導致短時間內發送一大堆上行數據給服務器,而這些數據很可能客戶端接收時產生大量的延遲。所以最好是在快進的時候不採集玩家的輸入,因爲玩家在看到快進過程中,實際上也很難有效的做出合理的反應,一個常見的做法,就是快進的時候,給遊戲覆蓋一個“等待”或“Loading”的蒙皮層,讓玩家不可以輸入操作。

 

image.png

 

image.png

4.容忍不一致
我們做幀同步的目標是各個客戶端都能看到一致的顯示。但是遊戲內容有很多,有一部分內容是可以容忍“不一致”的,比如我們做飛行射擊彈幕遊戲,滿屏幕有很多子彈,而每一顆子彈本身的存在的時間很短,如果我們不是做對打的遊戲(而是一起打電腦),那麼這些子彈是可以不一致的。又比如我們做一個橫版過關的配合遊戲,幾個玩家一起打電腦控制的怪物,大家關心的是怪物是怎麼被打死的,而玩法本身又比較容忍不一致(橫版動作遊戲的攻擊範圍往往比較大),所以就算有些不一致問題也不大。在以上的條件下,我們就可以嘗試,把更多的遊戲邏輯,從網絡幀的UpdateByNet()函數裏面拿出去,放回到單機遊戲中的Update()函數裏去。這樣就算網絡有點卡,起碼整個畫面裏還是有很多東西是不會被“卡住”的。但是必須注意的是,一般玩家控制的角色的動作,包括當前客戶端控制的角色,還是應該從網絡幀裏面獲得行爲數據,因爲如果玩家愛控制角色不一致的太多,整個遊戲場面就會差更多。很多遊戲中的怪物AI都是根據玩家角色來設定的,所以一旦玩家角色的行爲是同步的,那麼大多數的怪物的表現還是一致的。

 

image.png

5.非完全實時
一般來說,我們都希望遊戲中的角色控制是靈敏的,實時的。我們的遊戲角色往往在會玩家輸入操作後的幾十分之一秒內,就開始顯示變化。在幀同步遊戲中,我們可以讓玩家一輸入完操作,就立刻發包,然後儘快在下一個收到的網絡幀中收到這個操作,從而儘快的完成顯示。然而,網絡並不是那麼穩定,我們常常會發現一會快一會慢,這樣玩家的操作體驗就非常奇怪,無法預測輸入動作後,角色會在什麼時候起反應。這對於一些講求操作實時性的遊戲是很麻煩的。比如球類遊戲,控制的角色跑的一會兒快一會兒慢,很難玩好“微操”。要解決這個問題,我們一般可以學習傳輸語音業務的做法,就是接收網絡數據時,不立刻處理,而是給所有的操作增加一個固定的延遲,後在延遲的時間內,蒐集多幾個網絡包,然後按固定的時間去播放(運算)。這樣相當於做了一個網絡幀的緩衝區,用來平滑那些一會兒快一會兒慢的數據包,改成勻速的運算。這種做法會讓玩家感覺到一個固定延遲:輸入操作後,最少要隔一段時間,纔會起反應。但是起碼這個延遲是固定的,可預計的,這對於遊戲操作就便捷很多了,只要掌握了提前量,這個操作的感覺就好像角色有一定的“慣性”一樣:按下跑並不立刻跑,鬆開跑不會立刻停,但這個慣性的時間是固定的。

 

image.png

6.不鎖步
我們和其他玩家一起遊戲的時候,有時候不希望對方因爲電腦速度比較快,網絡比較好,而能比我們更早的看到遊戲的運行結果,從而提早作出操作。這一點在格鬥對打遊戲(如《街霸》)裏面非常關鍵,在一些RTS(《星際爭霸》)裏面,提早看到遊戲運行結果也是很有競爭優勢的。因此我們爲了讓網絡、硬件不一樣的玩家能公平遊戲,往往會使用一種叫“鎖步”的策略:就好像一串綁着腳鐐的囚犯,他們只能一起擡起左腳,然後再一起擡起右腳的走路,誰也不能走的更快。技術上的實現,就是每個客戶端都定時(每N個渲染幀)發送一個網絡幀到服務器上,就算玩家沒操作,也類似心跳的這樣發送空數據幀,所有客戶端都要完整的收到所有的其他客戶端的“心跳幀”才能開始運算一次遊戲邏輯。這就是讓所有的客戶端,都互相等待,如果任何一個客戶端卡了,其他的客戶端都立刻就能知道,然後彈出界面讓玩家停止輸入來等待。因此在很多場合,幀同步的技術也被成爲“鎖步”技術,事實上,在沒有統一的Relay Server服務器的時代(IPX局域網連機對戰的時代),幀同步的網絡幀其實就是上面所說的某個客戶端的“心跳幀”,是由某個客戶端產生並廣播的(比如以前的局域網遊戲,都會由一個客戶端充當Host主機)。在《星際爭霸》連機遊戲中,如果有一個玩家掉線了,所有其他玩家就會發現有一個界面彈出來擋住畫面,表示在等某某某。這種做法實際上是犧牲了流暢度的,因爲你會發現一旦有網絡、硬件卡的玩家加入遊戲,所有其他玩家都受他的影響。爲了減少這種對流暢度的影響,我們可以在需要“鎖步”的時候,儘量少鎖一點,比如不是發現缺了一幀就停下來,而是缺了若干幀,還是可以以“不公平”的方式繼續玩一會兒(比如幾秒),如果這段時間內還是沒有補齊所缺的幀,才宣佈鎖住遊戲等待。當然這個“容忍”的幀數我們可以調節到“最大”——就是沒有。那麼一個完全不鎖步的遊戲,肯定不是一個公平的遊戲,但是也會在流暢性產生最大的好處,就是完全不受其他玩家影響。在那些不是PVP(玩家對戰)的幀同步遊戲中,不公平這個往往問題不大。我們完全可以在遊戲的不同玩法裏,打開、調整、甚至關閉這個“鎖步”的機制,從而讓遊戲最大程度的平衡公平性和流暢性。

 

image.png

五、王者榮耀技術總監分享歷程

參考《王者榮耀》技術總監覆盤回爐歷程:沒跨過這三座大山,就是另一款MOBA霸佔市場了

1.狀態同步

先看一下狀態同步的優點。

第一,它的安全性非常高,外掛基本上沒有什麼能力從中收益。

第二,狀態同步對於網絡的帶寬和抖動包有更強的適應能力,即便出現了200、300的輸入延遲再恢復正常,玩家其實也感受不到不太舒服的地方。

第三,在開發遊戲過程中,它的斷線重連比較快,如果我的遊戲崩潰了,客戶端重啓之後只需要服務器把所有重要對象的狀態再同步一次過來,重新再創建出來就可以了。

第四,它的客戶端性能優化優勢也比較明顯,比如優化時可以做裁剪,玩家看不到的角色可以不用創建,不用對它進行運算,節省消耗。

再說一下我認爲的缺點。

第一,它的開發效率相對幀同步而言要差一些,很多時候你需要保證服務器與客戶端的每一個角色對象的狀態之間保持一致,但事實上你很難做到一致。

比如客戶端和服務器端更新的頻率,對優化的一些裁剪,網絡的抖動等等,你要讓每一個狀態在客戶端同步是比較難的,而你要想調試這些東西,來優化它帶來的漏洞、不一致的現象,花費的週期也會比較長,想要達到優化好的水平也比較難。

第二,它比較難做出動作類遊戲打擊感和精確性。比如說你要做一個射擊類角色,他的子彈每秒鐘要產生幾十顆,基於狀態同步來做是比較難的,因爲系統在很短時間內,會產生很多數據,要通過創建、銷燬、位置運算來同步。

第三,它的流量會隨着遊戲的複雜度,而逐漸增長,比如角色的多少。我們做《王者榮耀》時,希望在3G、4G的網絡條件下也能夠玩PvP,所以我們希望它對付費流量的消耗能控制在比較合理的水平,不希望打一局遊戲就消耗幾十兆的數據流量。

2.幀同步

另一種同步策略是幀同步。

這種技術應用的很廣泛,最早的《星際爭霸》《魔獸爭霸3》都採用了幀同步,他們都基於局域網運行,網絡的條件非常好,也不需要服務器就能搞定。幀同步的優點有幾個:

第一,它的開發效率比較高。如果你開發思路的整體框架是驗證可行的,如果你把它的缺點解決了,那麼你的開發思路完全就跟寫單機一樣,你只需要遵從這樣的思路,儘量保證性能,程序該怎麼寫就怎麼寫。

比如我們以前要在狀態同步下面做一個複雜的技能,有很多段位的技能,可能要開發好幾天,纔能有一個稍微過得去的結果,而在幀同步下面,英雄做多段位技能很可能半天就搞定了。

第二,它能實現更強的打擊感,打擊感強除了我們說的各種反饋、特效、音效外,還有它的準確性。利用幀同步,遊戲裏面看到這些揮舞的動作,就能做到在比較準確的時刻產生反饋,以及動作本身的密度也可以做到很高的頻率,這在狀態同步下是比較難做的。

第三,它的流量消耗是穩定的。大家應該看過《星級爭霸》的錄像,它只有幾百K的大小,這裏面只有驅動遊戲的輸入序列。幀同步只會隨着玩家數量的增多,流量纔會增長,如果玩家數量固定的話,不管你的遊戲有多複雜,你的角色有多少,流量消耗基本上都是穩定的。這點延伸開來還有一個好處,就是可以更方便地實現觀戰,錄像的存儲、回放,以及基於錄像文件的後續處理。

幀同步也有它的缺點。

第一,最致命的缺點是網絡要求比較高,幀同步是鎖幀的,如果有網絡的抖動,一段時間調用次數不穩定,網絡命令的延遲就會擠壓,引起卡頓。

第二,它的反外掛能力很弱,幀同步的邏輯都在客戶端裏面,你可以比較容易的修改它。但爲什麼《王者榮耀》敢用幀同步,一方面是因爲當時立項的時候開發週期很短,半年時間要做上線,要有幾十個英雄,存在時間的壓力,另一方面,MOBA類遊戲不像數值成長類的遊戲,它的玩法是基於單局的,單局的作弊修改,頂多影響這一局的勝負,不會存檔,不會出現刷多少錢刷多少好的裝備的問題,而且作弊之後我們也很容易監測到,並給予應有的懲罰,所以我們認爲這不是致命的缺點。

第三,它的斷線重回時間很長,相信臺下也有很多王者玩家,也曾碰到過閃退以後重回加載非常長的情況,甚至加載完以後遊戲也快結束了,這是幀同步比較致命的問題。

第四,它的邏輯性能優化有很大的壓力。大家應該沒有見到哪一款大型遊戲是用幀同步來做的,因爲這些遊戲的每一個邏輯對象都是需要在客戶端進行運算的。如果你做一個主城,主城裏面有上千人,上千人雖然玩家看不到它,但遊戲仍然需要對他們進行有效的邏輯運算,所以幀同步無法做非常多的對象都需要更新的遊戲場景。

3.選擇

那麼我們爲什麼選擇了幀同步而放棄了狀態同步呢?

我們前面提到它兩個優點缺點是相對的,這邊的優點對於那邊來說就是缺點。對於我們手遊立項的時候,最重要就是時間。當時市面上正在開發的MOBA手遊不止王者一款,大家都在爭上線的時間,所以我們要選擇一個開發週期最短的方案。

然後我們做端遊的時候也有一個深刻的體會,如果要做有趣的英雄,有趣的技能,它在狀態同步上面很難調出一個比較滿意的效果。所以最後我們依然選擇幀同步的方案。

現在來看,選擇幀同步方案之後,我們再把它的缺點進行優化或是規避,之後它帶來的好處是非常明顯的。《王者榮耀》重除了英雄的設計以及技能的感覺,還有很重要的一點,就是它確實在做一些非常有特色的英雄,它的技能、反饋、體驗上面都做得不錯,這些都是基於幀同步技術方案帶來的優勢。
我們選擇了方案之後,當時覺得很high,覺得這樣一個技術方案開發起來得心應手,效率如此之高,做出來的效果也很好。

但事實上,它也有好的一面,也有壞的一面,技術測試版本上線後質量不好,其中技術層面遇到的問題就是下面這三座大山。

第一是同步性,同步性這塊容易解決,其實也解決了;

第二也是最大一塊網絡問題,幀同步它的網絡問題導致我們對它技術方案的原理沒有喫透,碰到了一些問題,那時候遊戲的延遲很重,畫面卡頓,能明顯感覺走路抖動的現象;

第三是性能問題,這個問題始終存在,我們也一直在優化。

4.第一座大山:同步

第一座大山,最容易解決的同步問題。

幀同步的技術原理相當簡單,10、20年前在應用這種技術了,從一個相同初始的狀態開始,獲得一個相同的輸入,往下一幀一幀執行,執行時所有代碼的流程走得都是一樣的,這個結果調用完了以後,又有一個新狀態,完成循環。相同的狀態,相同的流程,不停的這樣循環下去。

這個原理雖然簡單,但是你要去實現它的時候,還是會有很多坑。
首先,我們所有的運算都是基於整數,沒有浮點數。浮點數是用分子分母表達的。

其次,我們還會用到第三方的組件,幀組件也要需要進行一個比較嚴格的甄別。我們本身用的公司裏面關於時間軸的編輯器裏面,最初也是是浮點數,我們都是進行重寫改造的。

再次,很多人初次接觸幀同步裏面的問題,就是在寫邏輯的時候和本地進行了關聯、和“我”相關,這樣就導致不同客戶端走到了不同的分支。實際上,真正客戶端跟邏輯的話,要跟我這樣一個概念無關。

接下來還有隨機數,這個要嚴格一致。這是實現的要點,嚴格按照這上面的規則寫代碼還是有可能不同步,本身就很難杜絕這樣的問題。

最後,真正重要的是開發者要提升自己發現不同步問題的能力,什麼時候不同步了,不同步你還要知道不同步在什麼點,這是最關鍵的。你需要通過你的經驗和總結提升這樣的能力。這個能力還是通過輸出來看不同客戶端不同輸出,找到發生在什麼點。

比如在《王者榮耀》裏,我們看到不同步的現象應該是這樣,有人對着牆跑,你看到的和別人玩的遊戲是不一樣的,就像進入平行世界。

最開始測試《王者榮耀》的,我們希望不同步率達到1%,就是100局裏面有1局出現不同步,我們就算遊戲合格,但實際上對於這麼大體量遊戲來說,這個比率是有問題的,經過我們不停的努力,現在已經控制在萬分之幾,一萬局遊戲裏面,可能有幾局是不同步的。

這個問題不一定是代碼原因或者沒有遵循這些要點纔出現的,有可能是你去修改內存,你去加載資源的時候,本地資源有損害或者缺失,或者是異常。說白了,你沒有辦法往下執行,大家走了不同分支,這都可能引起最終是不同步的。

如果你不同步概率比較低,到了這種萬分之幾概率的時候,很難通過測試來去還原,去找到這樣不同步的點。

最開始我們遊戲出現不同步的時候,就是在週末玩家開黑多的時候,隨着你的概率越來越低,基本上你就自己就還原不出這些問題了,只能依靠玩家幫你還原這樣的場景,來分析這樣的不同步問題。

同步性遵循這樣的要點,按照這樣的思路來寫,加上你不同步定位的能力,有了監控手段能夠去發現,這個問題其實就解決了。解決之後,你就可以好好享受幀同步的開發優勢。

5.第二座大山:網絡

第二座大山就是網絡,《王者榮耀》技術測試版本出臺的時候,延遲非常大,而且還是卡頓,現在看一下幀同步裏面比較特別的地方。幀同步有點像在看電影,它傳統的幀同步需要有buffer,每個玩家輸入會轉發給所有客戶端,互相會有編號,按順序輸入幀。

比如我現在已經收到第N幀,只有當我收到第N+1幀的時候,第N這一幀我纔可以執行。服務器會按照一定的頻率,不同的給大家同步幀編號,包括這一幀的輸入帶給客戶端,如果帶一幀給你的數據你拿到之後就執行,下一幀數據沒來就不能執行,它的結果就是卡頓。

網絡絕對理想的情況下還好,但現實的網絡環境不是這樣的。幀同步要解決問題就是調試buffer,以前有動態的buffer,它有1到n這樣的緩衝區,根據網絡抖動的情況,收入然後放到隊列裏面。

這個buffer的大小,會影響到延遲和卡頓。如果你的buffer越小,你的延遲就越低,你拿到以後你不需要緩衝等待,馬上就可以執行。但是如果下一幀沒來,buffer很小,你就不能執行,最終導致的結果你的延遲還好,但是卡頓很明顯。

如果調到幀同步的buffer,假如我們認爲網絡延遲是1秒,你抖動調到1秒,那得到的結果雖然你畫面不抖動了,但是你的延遲極其高。如果連最壞的網絡情況都考慮進去,buffer足夠大,那麼記過就跟看視頻是一樣的,平行的東西,看你調大條小。一些局部的措施我們都做過,都是一樣的問題。

具體我們怎麼優化卡頓的問題呢?

剛纔提到該幀同步與buffer,這個buffer可以是1也可以到n,我們要解決我們的延遲問題,我們就讓buffer足夠小。事實上《王者榮耀》最後做到的buffer是零,它不需要buffer,服務器給了我n,馬上知道是n,我收到n,我知道下一次肯定是n+1,所以我收到n之後馬上就把n這一幀的輸入執行了。

那麼爲什麼不卡頓了,畫面不抖動了?

最後一個關鍵點,是本地插值平滑加邏輯與表現分離。客戶端只負責一些模型、動畫、它的位置,它會根據綁定的邏輯對象狀態、速度、方向來進行一個插值,這樣可以做到我們的邏輯幀率和渲染幀率不一樣,但是做了插值平滑和邏輯表現分離,畫面不抖了,延遲感也是很好的。

做了這些後,我們還把TCP換成UDP,在手機環境下,弱網的情況下,TCP很難恢復重連,所以最後用了UDP來做。整體來說,在網絡好的情況下,它延遲也是很好的,在網絡比較差的情況下做插值,也是傳統CS的表現。

我們經常見到角色A和B,有些客戶端A在左B在右,有些是A在右B在左,幀同步邏輯上面AB之間的距離和座標都是完全一樣,但是畫面上看到他們可能會不重合,那就是你把它們分離之後的表現。網絡極其好的情況下,它應該是重合的,但是在網絡差的情況下,可能會有些偏差。這裏面是最重要的一塊優化。

5.第三座大山:性能優化

第三座大山,是我們對性能的優化。

本身幀同步邏輯上面在優化上面存在一些缺點,所有的角色都需要進行運算。這方面我們也是藉助Unity的特性,如果你想追求性能上的極致,有些東西你需要尋求好的方式。

第一點是熱點的處理。

我們是不用反射的,它都有GC性能開銷,我們的做法裏面,會把對象的顯示隱藏放在不同的渲染層裏面,儘量讓整個遊戲幀率是平滑的過程。還有我們本身有自己的系統,比如AI,在《王者榮耀》這樣的多角色遊戲中,你如果想要做出比較好的體驗,那麼AI就要做得比較複雜。

而要去優化熱點,我覺得就只有這三個步驟可以走。

首先,從程序的結構上面能找到更優的,它的優化效果就是最明顯的;其次,如果你的結構都是用的最好,就去挖掘局部的算法,調整你代碼的一些寫法。最後,如果局部的算法都已經調到最優還是沒有什麼辦法,那只有一條路,就是犧牲整個質量,就是分幀降頻。

第二點是GC,這塊剛纔說不用反射,還有裝箱和拆箱的行爲也是儘量少用。Unity指導過我們的優化,從GC上面的考慮,他們建議每一幀應該在200個字節以內是比較好的狀態,其實很難做到,王者也是每一幀在1k左右,很難做到200。

第三點是Drawcall,這些傳統的優化手段大家都用的很熟了。

第四點是裁剪,幀同步裏面是不能裁剪的,表現裏面我看不到的可以降低頻率或者不更新它,這在表現裏面可以做的。

第五點是3DUI的優化,比如《王者榮耀》的血條、小地圖上面疊的元素等等,這些UI都比較豐富,這塊我們用了31UI的方式來優化,沒有用UGUI裏面進行血條方面的處理。

我們也犧牲了一些東西,我們把所有東西都加載了,在遊戲過程當中,我們希望不要有任何IO行爲,包括輸出我們都是要佈局的。你處理的決策和複雜度,如果在一幀裏面放出100顆子彈,在放100顆子彈的時候一定要掉幀的,一定要在力所能及的時候把這些東西做到極致。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章