文/kisence

從今年下半年開始制作一款實時對戰(zhàn)游戲以來,我就在著手寫一個幀同步的游戲框架,其中包含了服務器框架和客戶端框架,該框架目前已經(jīng)開源。

期間踩過無數(shù)的坑,充分領略到了國內中文技術文檔十分稀少和零散的問題,所以在這里我想寫下我走過的路,以便于后來者參考。

首先,我希望寫一個前后端能統(tǒng)一語言的框架,以至于在前端寫好的游戲邏輯,拿到后端就可以直接使用。

其次,我的目標是寫一個同步框架,在框架層面解決同步問題,在此之上寫游戲邏輯的時候不需要再考慮游戲同步的問題。

目前看來,這兩個目標都得到了比較好的完成。

下面是解決方法。

首先要解決的是前后端語言一致的問題

這里我使用了一個c#服務器框架 SupperScoket

遇到的幾個坑是:

1.導出這個框架到在Mono上運行時報一個找不到window API的錯誤,解決方法是使用.Net 4.0以上版本的SupperSocket

2.框架在使用TCP模式時,有時會報出一個Send byte Time out 的異常,解決方法是使用TrySend方法,并在返回false時關閉連接。

第二個要解決的是同步框架的問題

這個問題比較復雜,如何在書寫游戲邏輯的時候感受不到同步問題的存在?如果每個事件都要等服務器的回包,還要體驗流暢,只能從預處理和追趕兩個角度入手。

預處理就是,這個事件還沒有發(fā)生,但是考慮到網(wǎng)絡延遲的存在,提前先把結果發(fā)送給每個客戶端,然后客戶端到了這個時刻再把這個事件表現(xiàn)出來,典型的例子就是皇室戰(zhàn)爭。

如果說沒有辦法做到預處理呢,比如說玩家的操作需要立即響應,那么其他玩家收到這個事件的時候必然已經(jīng)遲了,所以就要做追趕,比較典型的就是影子跟隨算法。

但是這兩種做法必然要在游戲邏輯中做對應的處理,開發(fā)者要時刻清醒此時是預測還是追趕,增加邏輯的復雜性,而且游戲的表現(xiàn)可能也參差不齊,有些地方也許同步的好,有些地方可能不好,要調優(yōu)需要在每個地方都下功夫,增加開發(fā)時間。

那么應該怎么辦呢,最理想的方法當然是全部當成本地計算,這樣就無需考慮是追趕還是預測的問題了,那么網(wǎng)絡游戲怎么全當成本地計算呢?當然就是幀同步了。

關于幀同步網(wǎng)上已經(jīng)有很多資料,在此不再贅述,但是關于幀同步有一個核心的問題,那就是它在網(wǎng)絡差的時候表現(xiàn)很差,這一點我們可以從星際爭霸和魔獸爭霸這些游戲中看出來,一旦有人卡頓,所有人都要停下等這個人的消息,但是我們知道手游《王者榮耀》這款游戲就是幀同步做的,他是怎么解決這一問題的呢?在《王者榮耀》負責人在unite 2017大會分享中我們沒有看到這一解決方案,這可能是他們的商業(yè)機密,但是在看了暴雪分享的守望先鋒同步機制之后,我得到了一個我自己的解決方案。

那就是預測回滾和解。

原理很簡單,游戲開始時,每個客戶端按照幀同步的方案推進著游戲,但是如果遇到服務器沒能及時返回其他玩家操作的時候,給對應的玩家預測一個操作(復制該玩家最后一次操作),并繼續(xù)推進游戲,如果在其后收到了服務器玩家關于這個人的操作,則把游戲回滾到預測開始的那一幀重新計算一遍,然后和現(xiàn)在游戲世界的表現(xiàn)和解。

如果服務器遲遲沒有收到某個玩家的消息,則會給這個玩家預測一個消息(復制該玩家的最后一次操作)然后推送給所有玩家,包括那個掉線的玩家。其他玩家會以這個預測操作為準計算接下來的游戲世界,而這個掉線玩家也會收到這個預測操作,并且替換掉玩家實際進行的操作,重新計算一遍游戲世界。保證每個客戶端的輸入一致。

原理說起來簡單,但是其實有幾個難點。

第一個難點就在于回滾,如何回滾到預測開始的那一幀呢,要記錄下每一幀的變化,然后逐幀退回嗎?還是把每一幀的數(shù)據(jù)做一個快照保存下來?

其實這個問題實現(xiàn)起來不難,關鍵是從性能考慮,如果把每一幀的數(shù)據(jù)都快照下來,內存可能會吃緊,如果做逐幀退回的方式,實現(xiàn)起來相對復雜,并且在性能上也可能有問題。

這里就引入了ECS架構幫助我簡化了這一問題,在ECS架構中,C 也就是component(組件),它是純數(shù)據(jù)的集合,并且 E 也就是 Entity(實體) 集中存放在一起,這方便了我對它們的集中操作,

在ECS架構的幫助下,我實現(xiàn)了對組件進行快照式的存儲,對實體進行了增量式的存儲,實現(xiàn)了對數(shù)據(jù)的回滾。

第二個難點在于和解,由于預測操作和玩家真實操作的不同,重計算出來的世界必然預測的世界有差異,那么怎樣以盡量不引人注意的形式,把預測世界過渡到真實世界呢,這一點守望先鋒的分享中提到了一部分,但是沒有完全解答這個問題。

實際上解決這個問題的思路是,先確定哪些是可以和解的,哪些是不可以和解的,然后分頭處理。怎么分頭處理呢,就是可以和解的在預測計算中就表現(xiàn),不可以和解的,要等到真正的數(shù)據(jù)來了才進行表現(xiàn)。

那么哪些是可以和解的呢?就是在玩家不知不覺間就可以過渡到的,比如說物體的位置,動畫。這里有很多的技術可以做這種和解,比如說影子跟隨算法。

不可以和解的比如說冒出的血條數(shù)字,你不能說傷害數(shù)字都冒出來了,然后又塞回去。

但其實有一個難點是,飛彈能不能和解?

顯然,飛彈的位置是可以和解的,但是飛彈的創(chuàng)建與銷毀呢?這里涉及到一個游戲表現(xiàn)的問題,如果飛彈的創(chuàng)建要等到服務器回包才出現(xiàn),那么這個表現(xiàn)在網(wǎng)絡差的時候就太糟糕了。

所以一定要可以和解,不能和解創(chuàng)造條件也要和解。

下面是解決方案

其實一部分解決方法在難點1已經(jīng)提到了,首先要建立一個對實體的回滾系統(tǒng),保證飛彈能回滾。

但是還有一個問題,在回滾的過程中要先把這個飛彈銷毀,但是如果重計算的結果是仍然創(chuàng)建這個飛彈呢?難道要再把這個飛彈再創(chuàng)建一次?雖然我們可以用池來避免頻繁的創(chuàng)建銷毀,但是粒子系統(tǒng)從池中取出仍然有重新構建的開銷。

很自然的想到可以延遲派發(fā)創(chuàng)建的事件,在數(shù)據(jù)層面這個實體已經(jīng)被重計算的很多次了,但只要這個實體仍然存在我就不再派發(fā)這個實體的創(chuàng)建事件。銷毀也是一樣。

但是我如何確定我兩次創(chuàng)建的實體的是一個呢?要知道我們框架的設計目標是開發(fā)時盡量避免對同步系統(tǒng)的感知,也就是我們游戲邏輯并不知道現(xiàn)在的數(shù)據(jù)是真實的數(shù)據(jù)還是預測的數(shù)據(jù),要在創(chuàng)建這個體的的時候判斷這個實體是否已經(jīng)在預測時創(chuàng)建過了顯然不應該是我們游戲邏輯應該做的,可我們的框架又如何確定兩個實體是否一致呢。

直接比較它們兩個是否相等肯定不行,把他們的數(shù)據(jù)取出來一一比對又太耗時。

我采用的方法是我稱之為特征碼的方法,在構建一個實體的時候,用一個字符串去描述這個實體,這個字符串要盡量簡略而又不能與其他特征碼重復,然后自己實現(xiàn)的hash方法(.NET自帶的GetHashCode有平臺差異)把這個字符串轉化為一個Int作為這個實體的唯一標識符,在創(chuàng)建實體的時候,只需要判斷這個實體ID和緩存中的ID是否一致就可以判斷這個實體是否已經(jīng)在預測中存在了,從而實現(xiàn)延遲派發(fā)。

再說一點其他的技術細節(jié)

1.實體的集中創(chuàng)建與銷毀

現(xiàn)在的架構中可以看到是一幀結束后把所有的實體集中的創(chuàng)建與銷毀,為什么要這樣做呢,實際上是為了重計算服務,當重計算進行時要先進行回滾,我根據(jù)回滾數(shù)據(jù)得知它是在某一幀里被創(chuàng)建的,但是不知道在哪個系統(tǒng)中,這就有可能造成,在實際計算中某個對象實際上在稍晚的時間被創(chuàng)建,不會被較早時間執(zhí)行的系統(tǒng)所影響,但是回滾后,這個對象被創(chuàng)建在了較早的時刻(這一幀的開始),導致較早執(zhí)行的系統(tǒng)也影響了他導致計算錯誤,為避免這一問題,我采用統(tǒng)一創(chuàng)建和銷毀時刻的方式解決。

這一方式有一個問題,就是創(chuàng)建飛彈等對象時至少要延遲一幀,在主觀感覺上就慢一點,守望先鋒也提到了這一問題,他們提到后來把創(chuàng)建的時刻重新拿回了游戲邏輯內,我估計是在保存回滾數(shù)據(jù)時把是在哪個系統(tǒng)創(chuàng)建的實體也保存了下來,這樣就可以避免計算錯誤的問題,目前在我的框架里還沒有優(yōu)化這一問題。

2.斷線重連

關于斷線重連這一點,使用ECS架構的優(yōu)勢就體現(xiàn)出來了,傳統(tǒng)的幀同步斷線重連只能把所有的玩家的數(shù)據(jù)從頭輸入一遍重計算,時間很長,而ECS可以很方便的把服務器的當前數(shù)據(jù)全部發(fā)送給重連的客戶端,讓客戶端直接從重連的那一幀開始游戲,避免了漫長的重連過程。

3.常見的不同步情況

1.MomentComponentBase 組件的DeepCopy方法沒有正確的拷貝全部數(shù)據(jù)

2.有些組件從本地創(chuàng)建和通過服務器同步消息創(chuàng)建的值有差異(比如有些組件有特殊的構造方法,而通過服務器同步的組件不執(zhí)行構造方法)

3.前后端代碼不統(tǒng)一

4.前后端數(shù)據(jù)表不統(tǒng)一

5.在進行整數(shù)計算的時候,數(shù)值溢出

6.浮點數(shù)計算誤差(讀表也會出這個錯誤)

7.同步邏輯之外的數(shù)值修改(例如付費復活)

原文鏈接https://www.kisence.com/2017/11/ ... g-bu-de-xie-xin-de/

銳亞教育

銳亞教育,游戲開發(fā)論壇|游戲制作人|游戲策劃|游戲開發(fā)|獨立游戲|游戲產業(yè)|游戲研發(fā)|游戲運營| unity|unity3d|unity3d官網(wǎng)|unity3d 教程|金融帝國3|8k8k8k|mcafee8.5i|游戲蠻牛|蠻牛 unity|蠻牛