從今年下半年開始制作一款實(shí)時(shí)對(duì)戰(zhàn)游戲以來,,我就在著手寫一個(gè)幀同步的游戲框架,其中包含了服務(wù)器框架和客戶端框架,,該框架目前已經(jīng)開源,。 首先,,我希望寫一個(gè)前后端能統(tǒng)一語言的框架,,以至于在前端寫好的游戲邏輯,拿到后端就可以直接使用,。 目前看來,這兩個(gè)目標(biāo)都得到了比較好的完成,。 首先要解決的是前后端語言一致的問題 這里我使用了一個(gè)c#服務(wù)器框架 SupperScoket 1.導(dǎo)出這個(gè)框架到在Mono上運(yùn)行時(shí)報(bào)一個(gè)找不到window API的錯(cuò)誤,解決方法是使用.Net 4.0以上版本的SupperSocket 2.框架在使用TCP模式時(shí),,有時(shí)會(huì)報(bào)出一個(gè)Send byte Time out 的異常,,解決方法是使用TrySend方法,并在返回false時(shí)關(guān)閉連接,。 3.框架在解析消息時(shí),,遇到不完整的消息沒能正確解析,這里他的文檔不是很詳細(xì),,其實(shí)要把未解析的數(shù)據(jù)數(shù)量緩存起來,,詳情看這篇博客,源碼在此。 第二個(gè)要解決的是同步框架的問題 這個(gè)問題比較復(fù)雜,,如何在書寫游戲邏輯的時(shí)候感受不到同步問題的存在,?如果每個(gè)事件都要等服務(wù)器的回包,還要體驗(yàn)流暢,,只能從預(yù)處理和追趕兩個(gè)角度入手,。 預(yù)處理就是,,這個(gè)事件還沒有發(fā)生,,但是考慮到網(wǎng)絡(luò)延遲的存在,提前先把結(jié)果發(fā)送給每個(gè)客戶端,,然后客戶端到了這個(gè)時(shí)刻再把這個(gè)事件表現(xiàn)出來,,典型的例子就是皇室戰(zhàn)爭。 如果說沒有辦法做到預(yù)處理呢,,比如說玩家的操作需要立即響應(yīng),,那么其他玩家收到這個(gè)事件的時(shí)候必然已經(jīng)遲了,所以就要做追趕,,比較典型的就是影子跟隨算法,。 但是這兩種做法必然要在游戲邏輯中做對(duì)應(yīng)的處理,開發(fā)者要時(shí)刻清醒此時(shí)是預(yù)測還是追趕,,增加邏輯的復(fù)雜性,,而且游戲的表現(xiàn)可能也參差不齊,有些地方也許同步的好,,有些地方可能不好,,要調(diào)優(yōu)需要在每個(gè)地方都下功夫,增加開發(fā)時(shí)間,。 那么應(yīng)該怎么辦呢,,最理想的方法當(dāng)然是全部當(dāng)成本地計(jì)算,這樣就無需考慮是追趕還是預(yù)測的問題了,,那么網(wǎng)絡(luò)游戲怎么全當(dāng)成本地計(jì)算呢,?當(dāng)然就是幀同步了。 關(guān)于幀同步網(wǎng)上已經(jīng)有很多資料,,在此不再贅述,,但是關(guān)于幀同步有一個(gè)核心的問題,那就是它在網(wǎng)絡(luò)差的時(shí)候表現(xiàn)很差,,這一點(diǎn)我們可以從星際爭霸和魔獸爭霸這些游戲中看出來,,一旦有人卡頓,,所有人都要停下等這個(gè)人的消息,但是我們知道手游《王者榮耀》這款游戲就是幀同步做的,,他是怎么解決這一問題的呢,?在《王者榮耀》負(fù)責(zé)人在unite 2017大會(huì)分享中我們沒有看到這一解決方案,我感覺有可能是樂觀幀同步,,但是在看了暴雪分享的守望先鋒同步機(jī)制之后,,我得到了一個(gè)我自己的解決方案。 那就是預(yù)測回滾和解,。 原理很簡單,,游戲開始時(shí),每個(gè)客戶端按照幀同步的方案推進(jìn)著游戲,,但是如果遇到服務(wù)器沒能及時(shí)返回其他玩家操作的時(shí)候,,給對(duì)應(yīng)的玩家預(yù)測一個(gè)操作(復(fù)制該玩家最后一次操作),并繼續(xù)推進(jìn)游戲,如果在其后收到了服務(wù)器玩家關(guān)于這個(gè)人的操作,,則把游戲回滾到預(yù)測開始的那一幀重新計(jì)算一遍,,然后和現(xiàn)在游戲世界的表現(xiàn)和解。 如果服務(wù)器遲遲沒有收到某個(gè)玩家的消息,,則會(huì)給這個(gè)玩家預(yù)測一個(gè)消息(復(fù)制該玩家的最后一次操作)然后推送給所有玩家,,包括那個(gè)掉線的玩家。其他玩家會(huì)以這個(gè)預(yù)測操作為準(zhǔn)計(jì)算接下來的游戲世界,,而這個(gè)掉線玩家也會(huì)收到這個(gè)預(yù)測操作,,并且替換掉玩家實(shí)際進(jìn)行的操作,重新計(jì)算一遍游戲世界,。保證每個(gè)客戶端的輸入一致,。 原理說起來簡單,但是其實(shí)有幾個(gè)難點(diǎn),。 第一個(gè)難點(diǎn)就在于回滾,,如何回滾到預(yù)測開始的那一幀呢,要記錄下每一幀的變化,,然后逐幀退回嗎,?還是把每一幀的數(shù)據(jù)做一個(gè)快照保存下來? 其實(shí)這個(gè)問題實(shí)現(xiàn)起來不難,,關(guān)鍵是從性能考慮,,如果把每一幀的數(shù)據(jù)都快照下來,內(nèi)存可能會(huì)吃緊,,如果做逐幀退回的方式,,實(shí)現(xiàn)起來相對(duì)復(fù)雜,并且在性能上也可能有問題。 這里就引入了ECS架構(gòu)幫助我簡化了這一問題,,在ECS架構(gòu)中,,C 也就是component(組件),它是純數(shù)據(jù)的集合,并且 E 也就是 Entity(實(shí)體) 集中存放在一起,,這方便了我對(duì)它們的集中操作,, 在ECS架構(gòu)的幫助下,我實(shí)現(xiàn)了對(duì)組件進(jìn)行快照式的存儲(chǔ),,對(duì)實(shí)體進(jìn)行了增量式的存儲(chǔ),,實(shí)現(xiàn)了對(duì)數(shù)據(jù)的回滾。 第二個(gè)難點(diǎn)在于和解,,由于預(yù)測操作和玩家真實(shí)操作的不同,,重計(jì)算出來的世界必然預(yù)測的世界有差異,那么怎樣以盡量不引人注意的形式,,把預(yù)測世界過渡到真實(shí)世界呢,,這一點(diǎn)守望先鋒的分享中提到了一部分,但是沒有完全解答這個(gè)問題,。 實(shí)際上解決這個(gè)問題的思路是,,先確定哪些是可以和解的,哪些是不可以和解的,,然后分頭處理。怎么分頭處理呢,,就是可以和解的在預(yù)測計(jì)算中就表現(xiàn),,不可以和解的,要等到真正的數(shù)據(jù)來了才進(jìn)行表現(xiàn),。 那么哪些是可以和解的呢,?就是在玩家不知不覺間就可以過渡到的,比如說物體的位置,,動(dòng)畫,。這里有很多的技術(shù)可以做這種和解,比如說影子跟隨算法,。 不可以和解的比如說冒出的血條數(shù)字,,你不能說傷害數(shù)字都冒出來了,,然后又塞回去。 但其實(shí)有一個(gè)難點(diǎn)是,飛彈能不能和解,? 顯然,飛彈的位置是可以和解的,,但是飛彈的創(chuàng)建與銷毀呢,?這里涉及到一個(gè)游戲表現(xiàn)的問題,如果飛彈的創(chuàng)建要等到服務(wù)器回包才出現(xiàn),那么這個(gè)表現(xiàn)在網(wǎng)絡(luò)差的時(shí)候就太糟糕了,。 下面是解決方案 其實(shí)一部分解決方法在難點(diǎn)1已經(jīng)提到了,,首先要建立一個(gè)對(duì)實(shí)體的回滾系統(tǒng),,保證飛彈能回滾。 很自然的想到可以延遲派發(fā)創(chuàng)建的事件,在數(shù)據(jù)層面這個(gè)實(shí)體已經(jīng)被重計(jì)算的很多次了,,但只要這個(gè)實(shí)體仍然存在我就不再派發(fā)這個(gè)實(shí)體的創(chuàng)建事件,。銷毀也是一樣。 但是我如何確定我兩次創(chuàng)建的實(shí)體的是一個(gè)呢,?要知道我們框架的設(shè)計(jì)目標(biāo)是開發(fā)時(shí)盡量避免對(duì)同步系統(tǒng)的感知,,也就是我們游戲邏輯并不知道現(xiàn)在的數(shù)據(jù)是真實(shí)的數(shù)據(jù)還是預(yù)測的數(shù)據(jù),要在創(chuàng)建這個(gè)體的的時(shí)候判斷這個(gè)實(shí)體是否已經(jīng)在預(yù)測時(shí)創(chuàng)建過了顯然不應(yīng)該是我們游戲邏輯應(yīng)該做的,,可我們的框架又如何確定兩個(gè)實(shí)體是否一致呢,。 直接比較它們兩個(gè)是否相等肯定不行,把他們的數(shù)據(jù)取出來一一比對(duì)又太耗時(shí),。 第三個(gè)難點(diǎn)是重計(jì)算的性能,,在我開發(fā)的早期版本時(shí),,游戲邏輯執(zhí)行一幀要耗時(shí)5ms,如果此時(shí)客戶端預(yù)測了5幀,,那么收到服務(wù)器消息再重計(jì)算需要25ms才能計(jì)算的完,,在網(wǎng)絡(luò)延遲更大的時(shí)候,游戲性能是不可接受的,。 解決這個(gè)問題要從優(yōu)化游戲性能和限制預(yù)測幀數(shù)入手,,我優(yōu)化了ECS的幾個(gè)最基本API的執(zhí)行性能,再用四叉樹優(yōu)化了碰撞系統(tǒng),把游戲邏輯的執(zhí)行時(shí)間縮短到1ms左右,,然后又通過服務(wù)器控制客戶端的預(yù)測幀數(shù),,使其不至于過大導(dǎo)致沉重的重計(jì)算負(fù)擔(dān)。 再說一點(diǎn)其他的技術(shù)細(xì)節(jié) 1.實(shí)體的集中創(chuàng)建與銷毀 2.斷線重連 3.常見的不同步情況 參考資料: 云風(fēng):淺談《守望先鋒》中的 ECS 構(gòu)架 |
|