ope體育滾球_ope體育滾球APP|ope體育什么時候上

關于幀同步和網游游戲開發的一些心得

從今年下半年開始制作一款實時對戰游戲以來,我就在著手寫一個幀同步的游戲框架,其中包含了服務器框架和客戶端框架,該框架目前已經開源。期間踩過無數的坑,充分領略到了國內中文技術文檔十分稀少和零散的問題,所以在這里我想寫下我走過的路,以便于后來者參考。

文/kisence

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

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

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

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

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

下面是解決方法。

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

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

遇到的幾個坑是:

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

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

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

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

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

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

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

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

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

那就是預測回滾和解。

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

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

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

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

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

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

在ECS架構的幫助下,我實現了對組件進行快照式的存儲,對實體進行了增量式的存儲,實現了對數據的回滾。

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

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

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

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

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

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

所以一定要可以和解,不能和解創造條件也要和解。

下面是解決方案

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

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

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

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

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

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

再說一點其他的技術細節

1.實體的集中創建與銷毀

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

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

2.斷線重連

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

3.常見的不同步情況

1.MomentComponentBase 組件的DeepCopy方法沒有正確的拷貝全部數據

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

3.前后端代碼不統一

4.前后端數據表不統一

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

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

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

本文來自kisence,本文觀點不代表GameLook立場,轉載請聯系原作者。

關注微信
网球优等生