游戏同步方案——帧同步
游戲同步方案——幀同步
- 幀同步(Lockstep)和狀態同步(State Synchronization)
- 狀態同步
- 幀同步
- 適用的游戲類型和代表作
- 幀同步的關鍵
- 運行環境一致
- 顯示與邏輯分離
- 舉個簡單的🌰
- 防作弊
- Q&A
幀同步(Lockstep)和狀態同步(State Synchronization)
所謂同步,就是要多個客戶端表現效果是一致的,例如我們玩王者榮耀的時候,需要十個玩家的屏幕顯示的英雄位置完全相同、技能釋放角度、釋放時間完全相同,這個就是同步。就好像很多個人一起跳街舞齊舞,每個人的動作都要保持一致。而對于大多數游戲,不僅客戶端的表現要一致,而且需要客戶端和服務端的數據是一致的。所以,同步是一個網絡游戲概念,當然只有網絡游戲才需要同步,而單機游戲是不需要同步的。
目前主流的網絡游戲同步方案中,主要分幀同步、狀態同步,但實際復雜、成熟的項目,介于其各自的優缺點,都會將兩者混合起來使用,尤其是幀同步,很多時候都要考慮到狀態的校驗,這里則不多說,先簡述兩者的基本概念(本文著重介紹幀同步)。
狀態同步
同步的是游戲中的各種狀態。一般的流程是客戶端上傳操作到服務器,服務器收到后計算游戲行為的結果,然后以廣播的方式下發游戲中各種狀態,客戶端收到狀態后再根據狀態顯示內容。
幀同步
同步的是客戶端的操作指令??蛻舳松蟼鞑僮鞯椒掌?#xff0c;并且服務器并不做過多的處理,然后將當前幀間隔內收集到的操作指令廣播給每一個客戶端,各個客戶端在一致環境下,處理同樣的操作輸入,則會得到同樣的結果,這樣服務器的計算壓力、傳輸數據量就相對小很多,能更好的滿足游戲對高實時性、高一致性的要求。其尤其適合RTS類這種有大量同質化單元的游戲,如星際爭霸等。其核心原理如下圖所示:
適用的游戲類型和代表作
| FGT、ACT(格斗、動作類) | 街霸 | 怪物獵人 |
| MOBA(多人在線戰術競技) | 王者榮耀、DOTA | 全面超神、LOL、DOTA2 |
| RTS(即時戰略類) | 星際爭霸、魔獸爭霸 | |
| MMO(大型多人在線) | 全民斗戰神 | 魔獸世界、軒轅傳奇、天涯明月刀 |
| FPS、TPS、STG(射擊類) | DOOM | CSGO,守望先鋒,逆戰,PUBG |
| 其它 | 劍與家園、街機類等等 | QQ飛車,各類游戲皆可 |
幀同步的關鍵
運行環境一致
- 隨機數的確定
以下RandomSeed供大家參考,在進入游戲邏輯前,各個客戶端統一設置相同的seed(算子),則各客戶端會產生相同的隨機數。注意,非幀同步相關邏輯,不要調用RandomSeed,調用系統的Random函數,注意區分邏輯,以免混淆。
/* * 根據隨機算子產生偽隨機數 */ export class RandomSeed{private static s_seed:number = 0;public static set seed(value:number){RandomSeed.s_seed = value;};public static get seed():number{return RandomSeed.s_seed;};public static randomCount = 0;public static Random(min?:number,max?:number){++RandomSeed.randomCount;min = min || 0; max = max || 1; RandomSeed.s_seed = (RandomSeed.seed * 9301 + 49297) % 233280; let rnd:number = RandomSeed.seed / 233280.0; return min + rnd * (max - min);} }- 浮點數精度一致
- 協議數據順序一致
- 客戶端,服務器的處理邏輯一致
- 具有確定性的物理引擎
顯示與邏輯分離
客戶端邏輯、渲染分離主要解決以下問題:
1,抗網絡抖動導致的渲染抖動(各種表現不平滑),不分離的話網絡抖動導致邏輯抖動進而影響渲染
2,新建線程處理邏輯層,避免邏輯、渲染共用線程導致拖累渲染,平攤cpu資源,保證渲染資源不被占用。即無論邏輯層如何延遲,渲染層始終平滑。這樣做還有好處是:邏輯和渲染可各自發揮最大效能,通常情況下邏輯層幀率在10到20間變化,可以輕松處理大幾百單位的邏輯運算(尋路,避障,ai等)
3,邏輯層獨立出來,可做服務端驗算
舉個簡單的🌰
首先根據個人的理解,再次闡述一些比較容易混淆的關鍵概念:
- 邏輯幀
邏輯幀一般是處理一些跟服務器需要交互的數據,是各個客戶端同步的關鍵數據,如玩家的移動指令,釋放技能操作等。邏輯幀的幀率視不同游戲而定,一般推薦1秒10幀,要考慮操作響應及時、數據吞吐能力、網絡穩定性等多個因素。 - 客戶端幀
有了邏輯幀,為什么還需要客戶端幀?考慮如下情況,一個高速飛行的子彈,假設速度為800m/s,我們定義的邏輯幀率是10,那實際子彈的軌跡,每個邏輯幀內是間距80m,如此大的間距,是很難去判斷碰撞關系的,就很容易造成子彈穿過物體的情況。所以,一般而言,邏輯幀率是滿足不了絕大部分游戲的判斷精度要求勒,判斷間隔過長。要注意,幀同步,所有的關鍵邏輯判斷,都要在對應幀里面去判斷,這樣大家得到的結果、記錄才是一致的。
客戶端幀是依賴于邏輯幀下,表示當前邏輯幀內,依據當前的數據環境中,客戶端依據客戶端幀去計算邏輯。舉個例子,邏輯幀率是10,客戶端幀率是60,則一個邏輯幀,在客戶端中,要對應6個客戶端幀,假設在第100個邏輯幀時候,收到的數據是玩家A以60m/s(嗯,就是這么快)的速度沖刺終點線,且此時,玩家距離終點線只有2米,那在收到第一個邏輯幀的時候,其實對應的客戶端幀就是第600幀,然后客戶端依據當前的信息,客戶端第601幀的時候,玩家還剩1米,第602幀的時候,玩家沖刺到終點線。我們很多的邏輯判斷都是在客戶端幀中去進行,邏輯幀更多是同步一些操作指令等,是處理數據上傳、分發??蛻舳藥壿嫀菄栏駥?#xff0c;且同樣是有序,客戶端幀率一般是邏輯幀的整數倍,客戶端幀是受邏輯幀驅動的,邏輯幀增加,才會觸發客戶端的觸發,從而推動游戲的邏輯計算。
客戶端幀率的設定,要考慮到游戲的計算精度、計算量大小、設備兼容性等。一般可以考慮為30,40,60幀率。 - 顯示與邏輯分離
為什么要顯示與邏輯分離?我們先思考一下這些問題,邏輯幀是服務器分發的,雖然約定是1秒10幀,那怎么去保證客戶端收到的數據就是均勻的 1秒10個數據呢?不能保證呀!客戶端幀對邏輯幀進行細化分割而已,所以,客戶端幀也不能保證是均勻的,及時的,所有不能直接用客戶端幀去控制顯示,不然你就試試看(千萬別,局域網測試的時候還真不能暴露出問題來),用客戶端幀去直接控制畫面顯示,畫面會刷新得非常不均勻!還有,如上面例子,客戶端幀是60幀,那我的電腦還是小時候爸爸獎勵我的層運行過window98的古董機,又或者我的主機是今年雙11剛配的2080ti呢,你讓我客戶端就60幀,多浪費呀。所有,實際我們游戲的FPS不應該受客戶端幀的過多影響,顯示歸顯示,邏輯處理歸邏輯處理。
還是上面的例子,在玩家正準備以60m/s的速度沖刺最后2米的時候,突然斷網勒,收不到之后的邏輯幀數據,對應客戶端幀也會停止分發,但是咱們游戲還在運行,我們的還可以利用當前的數據環境,繼續讓玩家去奔跑,只不過你發現,玩家會一直奔跑,毫無剎車的意思,哪怕過了終點(當然這是簡單的處理,實際顯示的畫面要跟實際的畫面要進行靠齊的,這里就不多說),所以顯示還是需要依靠每個客戶端自己的運行情況去顯示,這樣你的2080ti才有意義,邏輯歸邏輯,我的古董機也能結果跟2080ti保持運算結果一致。
下面結合個人實際項目經驗,摘略一個簡單的客戶端處理幀同步的流程,供大家參考。
1,構建DataManager.gameFrameData,接收服務器分發的幀數據,具體的幀數據定義,根據自己具體游戲而定,但至少都要有一個幀號來區分順序,對吧!!!
2,客戶端構建FrameManager,在其update(參考cocos引擎)中去持續監測幀數據,以推動客戶端邏輯按照幀數據執行,以下代碼供大家參考,clientFrame為客戶端幀,logicFrame為邏輯幀,兩者幀率不一定相同,但建議客戶端幀是邏輯幀的整數倍,方便運算。
export class FrameManager {//ControlDefine.CLIENT_FRAME_RATE客戶端的客戶端幀數FPS,如 30,60幀private static _clientFrameTime: number = 1.0 / ControlDefine.CLIENT_FRAME_RATE;//當前邏輯幀(第幾幀)public _curlogicFrame: number = 0;//邏輯幀數FPS,如10幀,能滿足大部分游戲的操作需求private _frameRate: number = 0;//當前客戶端幀(第幾幀)private _clientFrameNum: number = 0;private _startTime: number = 0;private _clientServerFrameRate: number;//客戶端幀數與邏輯幀數的倍率public get clientServerFrameRate() { return this._clientServerFrameRate;}public get frameRate(): number {return this._frameRate;}constructor(frameRate) {this._frameRate = frameRate;//_clientServerFrameRate應該要是整數this._clientServerFrameRate = ControlDefine.CLIENT_FRAME_RATE / this._frameRate;}public get clientFrameNum() { return this._clientFrameNum;}public restart() {this._clientFrameNum = 0;this._startTime = 0;this._curlogicFrame = 0;}//邏輯幀的更新,受服務器接收數據的驅動public logicUpdate() {//客戶單已接收到的最新的幀let lastSyncFrame: number = DataManager.gameFrameData.lastSyncFrame;//已接受到當前幀的輸入時可以進行當前幀的邏輯并繼續向下推進邏輯幀if (lastSyncFrame > this._curlogicFrame) {if (lastSyncFrame > this._curlogicFrame + 1) { //快進幀,發現客戶端正在運行的邏輯幀落后服務器接收到的邏輯幀,則客戶端快速跟進到最新this.stepToLogicFrame(lastSyncFrame); } else {//保持正常速率處理幀數據cc.director.getScheduler().setTimeScale(1);}}}stepToLogicFrame(frame) {while (this._clientFrameNum < this._clientServerFrameRate * frame) {this.updateClient(FrameManager._clientFrameTime, false);}this.updateClient(FrameManager._clientFrameTime, false);}public updateTime = 0;public updateCount = 0;public update(dt: number) {this.logicUpdate();this.updateClient(dt, true);}//更新客戶端幀,主要是當前邏輯幀下,客戶端幀的處理//isUpdateShowFrame,是否需要更新玩具的可視層public updateClient(dt: number, isUpdateShowFrame: boolean = true) {if (this._clientServerFrameRate * DataManager.gameFrameData.lastSyncFrame > 0 && isUpdateShowFrame) {GlobalEvent.Dispatch(GlobalEventType.CLIENT_SHOW_FRAME_UPDATE, dt);}//客戶端幀過快,服務器幀有延遲if (this._clientFrameNum > this._clientServerFrameRate * DataManager.gameFrameData.lastSyncFrame) {return;}while (this._startTime >= FrameManager._clientFrameTime && this._clientFrameNum <= this._clientServerFrameRate * DataManager.gameFrameData.lastSyncFrame) {if (DataManager.gameFrameData.lastSyncFrame >= this._curlogicFrame && this._clientFrameNum == this._curlogicFrame * this._clientServerFrameRate) {GlobalEvent.Dispatch(GlobalEventType.LOGIC_FRAME_UPDATE, this._curlogicFrame);this._curlogicFrame++;}if (this._clientFrameNum < this._clientServerFrameRate * DataManager.gameFrameData.lastSyncFrame) {++this._clientFrameNum;GlobalEvent.Dispatch(GlobalEventType.CLIENT_FRAME_UPDATE, this._clientFrameNum);this._startTime -= FrameManager._clientFrameTime;} else {break;}}} }注意觀察以上代碼,如何分發客戶端幀(GlobalEventType.CLIENT_FRAME_UPDATE)、邏輯幀(GlobalEventType.LOGIC_FRAME_UPDATE)和客戶端的顯示事件(GlobalEventType.CLIENT_SHOW_FRAME_UPDATE)。
當然一個完善的FrameManager還需要有很多細節需要去處理,如:
- 客戶端幀先跑,還是邏輯幀先跑
- 幀加鎖,方便實現“暫?!惫δ?/li>
- 緩沖幀的邏輯
- 快進幀的邏輯(stepToLogicFrame)
- 客戶端幀間隔如何均勻分發
- 客戶端幀、邏輯幀處理的邏輯拆分
- 客戶端幀和邏輯幀的誤差的彌補
- 客戶端幀的間隔即update(dt)的參數dt也是不穩定、不均勻的哦
3,對應事件的監聽處理
- GlobalEventType.LOGIC_FRAME_UPDATE
收到該事件的時候,我們應該去處理玩家的一些操作指令,更新到各玩家對應的數據層中 - GlobalEventType.CLIENT_FRAME_UPDATE
根據當前玩家的數據,執行響應的數據更新、邏輯處理。如玩家碰撞到勒什么、移動到哪里勒等等,這些多是在數據層的更新 - GlobalEventType.CLIENT_SHOW_FRAME_UPDATE
同樣根據當前玩家的數據,如玩家的位置坐標,去刷新畫面,顯示玩家的位置變化
以上一些流程,沒有展開具體細節,如顯示的畫面,跟實際的結果是及時匹配的。這些也是客戶端幀同步處理的核心、難點所在,而且不同游戲,不同團隊有自己不同的處理差異,重要的是,客戶端幀和邏輯幀一定要固定間隔,不可出現跳幀,這樣至少保證計算的一致性,至少同步了呀😸。
防作弊
幀同步的游戲,外掛橫行,典型的就是FPS類的游戲,其主要原因是每個客戶端具有完整的游戲數據,這就包括勒其它玩家的信息。根據不同游戲類型,復雜程度,作弊與反作弊的方式都會不一樣,這里僅簡述個人的一些經驗思考。
Q&A
-
Q:客戶端幀是否一定要有?
A:如果設定的邏輯幀能滿足自己游戲的邏輯運算需求,客戶端幀可以不需要。 -
Q:我的畫面明明跟別人畫面有一定延遲,而且也不太一樣,這還能叫同步?
A:每個人的網絡環境不一樣,對應畫面有一定延遲是難免的,而且不同客戶端顯示層的平滑處理是不會一樣的,FPS高的畫面會明顯游戲更流暢,畫面上給玩家的反饋也更及時,但是各個客戶端在實際數據上的計算是一致的。 -
Q:我的網絡很卡,難道不影響其它玩家嗎?
A:如果玩家的網絡卡,在幀同步的策略下,就經常發些自己的操作有延遲,或者畫面展示的操作是無效的。但是,別的網絡正常的玩家受到的影響不大,可能看到網絡卡的角色是不動的。
總結
以上是生活随笔為你收集整理的游戏同步方案——帧同步的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 雅思考试为您揭秘美国大学最新排名中的玄机
- 下一篇: 【原创】5.4青年有感