服务端构架干货:快节奏多人游戏的技术实现
stanleyluo編譯
一、簡介
序
本文是探索如何制作快節(jié)奏多人游戲相關技術和算法的系列文章中的第一章。如果你熟悉多人游戲背后的概念,可以放心跳過本章 - 接下來是一些介紹性的討論。
作弊問題
一切都始于作弊。
做為游戲開發(fā)者,通常不會關心是否有人在你的單人游戲中作弊,因為他的行為只會影響他自個兒。作弊的玩家可能不會按照你設計的過程來體驗游戲,但這已經(jīng)是他自己的游戲,他們有權利想怎么玩就怎么玩。
多人游戲則不同。在所有競技游戲中,作弊玩家不僅只提升自己的體驗,同時也破壞了其它玩家的體驗。做為開發(fā)者你得避免這種行為,因為這將導致玩家離開你的作品。
有許多辦法能防止作弊,但最重要的一點(也可能是唯一真正有意義的一點)很簡單:不信任玩家。做好最壞的打算 - 玩家會嘗試作弊。
權威服務器和啞客戶端 (Authoritative servers and dumb clients)
這是一個看似簡單的解決方案 - 所有的游戲邏輯在你的服務端實現(xiàn),客戶端僅做游戲表現(xiàn)。換句話說,就是游戲客戶端向服務器上送輸入(玩家的按鍵,指令),服務端運行游戲邏輯并下發(fā)運行結(jié)果給客戶端。這種做法可稱之為“權威服務器(authoritative server)”,因為游戲中發(fā)生的一切都由服務器控制。
當然,你的游戲服務端可能有能被利用的漏洞,那超出了我們的討論范圍。但使用權威服務器這種模式能防止大部分的攻擊。比如,玩家的血量以服務器的為準,被攻擊的客戶端能在本地將玩家的血量改大100倍,但服務器那還是只剩10%的血量 - 當玩家被攻擊時還是會死掉,客戶端怎么改也沒有用。
玩家游戲中的位置信息也不能相信客戶端。否則,被攻擊的客戶端通過上報位置給服務器說“我在(10,10)”并在一秒后上報“我在(20,10),達到穿墻或行動快于其它玩家的目的。相反,服務器知道玩家的位置在(10,10),客戶端上報指令說“向右移動一格”,服務器運行更新內(nèi)部狀態(tài)計算出玩家新的位置在(11,10),并返回客戶端“你現(xiàn)在在(11,10)”:
?
總而言之:游戲狀態(tài)完全由服務端管理。客戶端上送操作給服務器,服務器定期更新游戲狀態(tài),并下發(fā)新的游戲狀態(tài)給客戶端進行表現(xiàn)渲染。
網(wǎng)絡處理
上述啞客戶端(dumb client)的方案在慢節(jié)奏的回合制游戲上工作得很好,如策略或者是牌類游戲。在局域網(wǎng)、或是無延遲的通訊環(huán)境下也運行良好。但用于快節(jié)奏網(wǎng)絡游戲特別是在互聯(lián)網(wǎng)環(huán)境下就完蛋了。
先說物理環(huán)境。設想你在舊金山,連到位于紐約的服務器,大概4000公里或者2500英里(大概是兩倍北京到香港的距離)。字節(jié)在Internet上傳輸接近光速(達到低級別的光脈沖、電纜中的電子,或電磁波的速度),光速大概30萬公里/秒,所以這個距離大概需要13毫秒。
這聽起來非常快,而且是非常樂觀的設置 - 假設了數(shù)據(jù)能以光速直線傳輸,其實不然。在實際情況下,數(shù)據(jù)需要從路由器到路由器之間通過一系列的中轉(zhuǎn)(網(wǎng)絡術語稱之為hops),達不到光速;因為數(shù)據(jù)包必須復制、檢查、重新路由,所以路由器本身會造成一些延遲。
為求說法,我們假設數(shù)據(jù)在客戶端到服務器耗時50毫秒,這比較接近最佳場景。如果你從紐約接入位于東京的服務器呢?如果出于某種原因?qū)е戮W(wǎng)絡擁塞?延遲100,200甚至500甚至更大。
回到我們的例子,你的客戶端上送指令給服務器說“我按了右方向鍵”,服務器在50毫秒后收到 ,如果服務器立即處理完請求更新狀態(tài)并回應,客戶端也得在50毫秒后收到新的游戲狀態(tài)“你現(xiàn)在在(1,0)”。
這樣看來,你按下右方向鍵后有一小會沒有任何效果,然后游戲角色才會右移一格。這種處于你的輸入指令和響應之間的卡頓不多,但非常明顯。當然,半秒的卡頓已經(jīng)不是明顯,會直接導致游戲沒法玩。
總結(jié)
多人網(wǎng)絡游戲如此好玩,但面臨全新的挑戰(zhàn)。權威服務器的架構(gòu)能很好的防止作弊,但僅僅這樣簡單實現(xiàn)將造成游戲響應遲鈍。
后續(xù)內(nèi)容我們將嘗試基于權威服務器架構(gòu)來構(gòu)建一個系統(tǒng),讓玩家得到最小的延遲的體驗,達到幾乎和本地單機游戲一樣的效果。
二、快節(jié)奏多人游戲:客戶端預測+服務器比對
前言
在本系列的第一章中,我們探討過一種權威服務器與啞客戶端的C/S模型:僅上送輸入指令到服務器,然后在服務器更新游戲狀態(tài)并在響應之后由客戶端展現(xiàn)效果。
?
此原始實現(xiàn)方案會導致玩家操作到屏幕響應之間的延遲:玩家按下右方向鍵,游戲角色過一秒才開始移動。這是因為客戶端輸入必須先傳輸?shù)椒掌?#xff0c;服務器必須處理輸入并計算出新的游戲狀態(tài),然后新的游戲狀態(tài)才能回應給客戶端表現(xiàn)。
在Internet這樣的網(wǎng)絡環(huán)境中,如果延遲達到十分之一秒,游戲操作就會感覺反應遲鈍,最糟的情況則沒法玩兒。在這部分內(nèi)容中,我們得尋求改善并消除這個問題的方法。
客戶端預測
盡管會存在一些作弊玩家,但大多數(shù)時間里游戲服務器處理的是有效的請求。這意味著游戲狀態(tài)會按照預期更新;比如在你按了右方向鍵后角色就會走到(11,10)這個位置。
我們可以利用這一特點,在游戲世界是足夠的可預測的情況下(即,給予的游戲狀態(tài)和一系列輸入,運行結(jié)果是完全可預測的)。假設存在著100毫秒的滯后,并且游戲角色移動一個格子的動畫也需要100毫秒和話。使用之前的實現(xiàn),整個動作得花200毫秒。
?
由于游戲世界是確定性的,我們可以假設玩家上送到服務器的指命都能成功執(zhí)行。基于這個假設,客戶端可以預測指令在處理之后游戲世界的狀態(tài),而且預測幾乎能完全正確。
相比上送指令然后等待新的游戲狀態(tài)再開始表現(xiàn),手機靚號買賣現(xiàn)在客戶端可以在上送指令后就立即展現(xiàn)效果,如果上送的指令正確處理那么等待得到的新游戲狀態(tài)將與客戶端本地計算的狀態(tài)相一致。
這樣下來,使用權威服務器模式,玩家操作與屏幕表現(xiàn)的效果將完全沒有延遲(如果被攻擊的客戶端發(fā)出無效的指令,也能看到相應的效果,但不會影響服務器的狀態(tài)和其他玩家的表現(xiàn))。
同步問題
上面的例子里,我專門選的數(shù)值讓游戲運行得非常好。但是,如果稍微改動一下場景:服務器響應延遲達到250毫秒,移動一格動畫消耗100毫秒,玩家連續(xù)按了兩次右方向鍵,想向右移動兩格。
繼續(xù)使用上述方法后,會是這個樣子:
?
問題開始變得有趣。收到游戲新狀態(tài)的時間t=250ms,而客戶端此時預測產(chǎn)生的狀態(tài)是x=12,但服務器返回的狀態(tài)卻是x=11。因為服務器權威,這樣客戶端必須把角色退回到x=11的位置。但此時,在t=350時又收到了服務器更新狀態(tài),并通知說x=12,因為角色這時又得跳回去。
以玩家的角度看,他按了兩次右方向鍵,角色向右移動了兩格,站在那50毫秒后又跳回左邊一格,停了100毫秒后再次跳到右邊。這顯然是無法接受的。
服務器校對
解決此問題的關鍵在于理解客戶端看到的游戲世界是現(xiàn)在時,但由于滯后,從服務端取得的更新實際上是過去時的狀態(tài)。服務器下發(fā)的游戲更新狀態(tài),還不是處理完所有客戶端上送指令后的狀態(tài)。
解決這個問題不難。首先,客戶端在每個請求中增加序列號;上例中,第一個按鍵請求序列號為 #1,第二次按鍵為請求 #2。然后服務器的回應也帶上相應的序號:
?
這樣的話,當t=250,服務端說 “按請求#1,你的位置在x=11”,然后將服務器中角色位置設置為x=11。現(xiàn)在假設客戶端留有發(fā)往服務器請求包的所有備份,按照收到的回應,客戶端得知服務器處理完請求#1,所以扔掉本地對應的拷貝,客戶端知道后續(xù)還有#2的回應,所以本地預測會繼續(xù)。即使有些請求服務器還沒處理到,客戶端仍能根據(jù)服務端最后下發(fā)的狀態(tài)計算出游戲的“當前”狀態(tài)。
因此,當t=250,客戶端收到“x=11,已處理請求#1”,就扔掉請求包備份中的#1,但保留服務端尚未確認的#2的備份,然后將游戲內(nèi)部狀態(tài)按服務器的響應設置為x=11,并且會繼續(xù)表現(xiàn)服務器尚未收到的所有請求,即“向右移”的#2指令,得到的最終正確結(jié)果x=12。
之后,當在t=350時收到服務端的游戲新狀態(tài)時,服務端指示“x=12,已處理請求#2”,這時客戶端扔掉#2指令備份,并更新狀態(tài)為x=12。此時備份中已經(jīng)沒有未處理的指令,所以客戶端處理以正確的結(jié)果停在這兒。
其它
上面討論的是移動的處理,但同樣的原理適用于其它所有內(nèi)容。比如回合制戰(zhàn)斗游戲,當玩家攻擊一個其它角色時,可以展現(xiàn)掉血和傷害數(shù)值,但在服務器回應之前還不應該更新那個角色的生命值。
因為游戲狀態(tài)的復雜性,不是總能簡單的應對,可能你得在收到服務器確認前避免一個角色被殺掉,哪怕當前客戶端狀態(tài)中它的生命值低于0,比如這個角色正好在你的致命攻擊之前使用了回血包,但服務器還沒下發(fā)給你。
這又引入一個有趣的問題,即便游戲世界是完全確定的并且沒有任何客戶端作弊,還是有可能出現(xiàn)客戶端預測的狀態(tài)與服務端下發(fā)的狀態(tài)在比對后不一致。這個情形在單人游戲不可能出現(xiàn),但在服務器上同時連接了多個玩家時很常見。這將是下一篇文章的主題。
總結(jié)
使用權威服務器,需要在客戶端等待服務器實際處理上送指令的過程中,給予玩家即時響應的錯覺,客戶端按玩家的操作模擬出效果,當收到服務端的新狀態(tài)時,客戶端會使用收到的更新和其后上送服務器但還沒確認的輸入重算之前已預算的狀態(tài)。
三、實體插值
簡介
在本系列第一章,討論的是權威服務器的概念及其防作弊的作用,但過于簡單得使用這個方案會帶來可玩性和響應能力相關的問題;到第二章中則提到運用客戶端預測來克服這些問題。
這兩篇文章的最終結(jié)論是一系列關于讓一個玩家在互聯(lián)網(wǎng)有傳輸延遲的環(huán)境下連接權威服務器的情況下,操作游戲角色達到單機游戲體驗的概念和技巧。
在本章,我們討論在相同的服務器有多個玩家控制的角色接入的情況。
服務器時步(Server time step)
上一章,我們把服務器的行為描述的非常簡單:讀取客戶端指令,更新游戲狀態(tài),并回應給客戶端。但當多個客戶端接入時,服務器的主循環(huán)邏輯會略有不同。
此時,在快節(jié)奏游戲中的多個客戶端可能會同時上送指令(玩家飛快的操作按鍵,鼠標移動和點擊來發(fā)出指令)。每次從每個客戶端收到上送指令都處理游戲世界的狀更新和廣播的話,將消耗大量CPU和帶寬。
將收到的客戶端指令不做處理立即放到隊列中,然后以較低頻率定期處理游戲世界的更新(比如每秒更新10次)是個好辦法。這樣的話,每次更新的延遲是100毫秒,我們稱之為時步(time step)。服務器在每次循環(huán)更新時,處理掉所有未處理的客戶端指令(可能要以比時步更小的時間增量處理,方便預測物理行為),然后將新的游戲狀態(tài)廣播給客戶端。
總之,游戲世界的更新只以預期的頻率進行,而與客戶端指令的上送和數(shù)量無關。
處理低頻更新
從客戶端來看,這種方式和之前一樣能順暢的運行:客戶端預測處理與更新間隔的延遲無關,所以在相對較少的狀態(tài)更新時也能清晰的預測處理。但由于游戲狀態(tài)廣播的頻率低(如上每100毫秒一次),那么客戶端只有游戲世界中非常少量的可到處移動的那些其它實體的信息。
之前第一種實現(xiàn)會在收到新狀態(tài)時更新其它玩家角色的位置,那這些角色原本平滑的移動變成了每100毫秒分散的移動,成了非常斷斷續(xù)續(xù)的瞬移。如下圖:
?
根據(jù)游戲類型相應有各種針對此問題的解決方案;通常情況下,游戲中的實體越可預測就越好解決。
航位推算(Dead reckoning)
設想我們開發(fā)一款賽車游戲,車速非常快所以很好預測,比如賽車的速度每秒100米,一秒之后它將位于起點后大致100米的位置。為什么是“大致”?在一秒之間,車子可以加速或減速了一點兒,或者是左轉(zhuǎn)或右轉(zhuǎn)了一點兒,注意這個詞“一點兒”,汽車的機動性就是這樣,因為無論玩家如何實際操作,賽車在高速行駛期間其時間點所在的位置很大程度取決于其之前的位置、速度和方向。換句話說,賽車不會立即出現(xiàn)180度轉(zhuǎn)向。
那當服務器每100毫秒下發(fā)一次更新的話如何處理?客戶端收到下發(fā)的每臺競技賽車的速度和朝向,要下個100毫秒后才收到新的信息,仍得展現(xiàn)賽車繼續(xù)在行駛。最簡單的做法就是假設100毫秒內(nèi)車速和朝向不變,然后通過這些參數(shù)本地運行賽車的物理表現(xiàn)。然后在100毫秒收到服務器更新后再修正車的位置。
修正處理可大也可小,取決于很多因素。如果玩家保持賽車直線前進并且沒有改變車速,那么預測的位置將精確的對應服務器通知要修正的位置。另一方面,如果玩家被什么東西撞毀,那預測的位置也會錯得離譜。
注意航位推算法更適用于低速場景,比如戰(zhàn)艦。實際上,“航位推算”就是起源于海上航行。
實體插值
有不少場景無法應用航位推算法:特別是當玩家方向和速度會隨時改變的情景下。例如3D射擊游戲,玩家們通常會高速的跑動、停止,轉(zhuǎn)彎,這時航位法幾乎無效,因為位置和速度不再能通過之前的數(shù)據(jù)進行預測。
收到服務器為準的數(shù)據(jù)時不能立即更新玩家的位置,這會讓玩家每100毫秒閃跳一段距離而使得游戲沒法玩。
那在每100毫秒得到服務端位置數(shù)據(jù)時該怎么做呢?訣竅在于在這期間如何展現(xiàn)玩家角色的動畫。答案的關鍵是以相對于玩家過去的狀態(tài)來展現(xiàn)其它玩家。
比如在t=1000這個時間點收到位置數(shù)據(jù)時,已經(jīng)有了時間點t=900的數(shù)據(jù),所以可以知道玩家在t=900和t=1000時的位置,因此,從t=1000和t=1100,展現(xiàn)的是其它玩家在t=900到t=1000時的行為狀態(tài)。這樣的話總是以玩家真實的移動數(shù)據(jù)進行展現(xiàn),只是滯后了100毫秒。
?
從t=900到t=1000的用于插值的位置數(shù)據(jù)取決于游戲。插值處理通常效果良好,否則也可以讓服務器在每次更新時下發(fā)更多詳細的行動數(shù)據(jù)來改善插值效果,比如玩家路線的一系列直線段或者是每10毫秒的位置采樣(只需要發(fā)送小的行動的增量數(shù)據(jù)就不會放大下發(fā)的數(shù)據(jù)量,因為這種情況在數(shù)據(jù)傳輸時會被大量優(yōu)化)。
需要注意,使用這種方式時,每個玩家看到的游戲世界的表現(xiàn)會稍微不同,因為每個玩家看到的自己是現(xiàn)在時但看到的其它實體是過去時,但是就算在快節(jié)奏游戲中看其它實體有100毫秒的差異感覺也不明顯。
也有例外:當處于要求大量空間和時間精度的場景中,如當某玩家槍擊其他玩家時,因為看到的其他玩家是過去時,瞄準有100毫秒滯后,這表示你正在射擊的是100毫秒之前的目標!我們會在下一章處理這個問題。
總結(jié)
在有網(wǎng)絡延遲并使用低頻率更新的權威服務器的C/S架構(gòu)中,仍然必須為玩家提供游戲的連續(xù)性和平滑運行的錯覺。在本系列第二部分中討論過通過客戶端預測和服務器校對的方式實時展現(xiàn)玩家行動的方法,這樣做確保本地玩家的行為立即生效,并消除了影響游戲可玩性的延遲。但存在其它游戲?qū)嶓w時仍有問題,這一章則討論了解決這類問題的兩個方法。
第一個是航位推算(dead reckoning),適用于某些類型的模擬:即游戲中實體的位置可以通過如位置、速度和加速度這些已有數(shù)據(jù)估算出可用值。如果不符合這個條件這個方法就會失效。
第二個是實體插值(entity interpolation),不做預測而是使用服務器的真實數(shù)據(jù),來展現(xiàn)出稍微在時間上延遲的其它實體。
最終的效果是玩家的角色處于現(xiàn)在時但游戲中的其它實體處于過去時,這通常能創(chuàng)造出極其流暢的體驗。
但一切還沒結(jié)束,在需要高精度的空間和時間的情況時以上方法就失效了,比如射擊一個運動中的目標:客戶端2展現(xiàn)出來的客戶端1的位置,與服務器中和客戶端1中的位置不一致,那就別想爆頭了!當然得有爆頭,下一章來解決這個問題!
四、爆頭
概要
前三章解釋的C/S方案可以總結(jié)如下:
?
- 客戶端上送給服務器的所有請求都帶上時間戳
- 服務器處理輸入指令并更新游戲狀態(tài)
- 服務器定期下發(fā)游戲狀態(tài)給所有客戶端
- 客戶端上送指令并在立即本地展現(xiàn)效果
- 客戶端取得游戲狀態(tài)
- 同步預測服務端狀態(tài)
- 使用已有狀態(tài)為其它實體插值
從客戶端角度看,會產(chǎn)生兩個重要結(jié)果:
?
- 玩家看到的自己是現(xiàn)在時
- 玩家看到的其它實體是過去時
這個方法多數(shù)時有效,但在時間空間精度有要求的時候會很成問題,比如爆頭。
滯后補償(Lag Compensation)
當你端起狙擊槍完美的瞄準對手的腦袋時,你開槍了!必將一擊斃命!
但。。。沒中
怎么回事兒?!
因為在上述C/S架構(gòu)中,你瞄準的位置是100毫秒前對手的頭所在的位置,就像處于某個光速非常非常慢的宇宙中,你瞄準的是敵人過去的位置,當你扣下扳機時他已經(jīng)跑了。
幸運的是,這個問題也有一個簡單的應對方案,對于大部分玩家在大部分時間也是能被受的(有一個例外會討論)。
看看是怎么做到的:
?
- 開槍時,客戶端把完整的信息上送給服務器:開槍時精確的時間戳和武器瞄準的精確位置;
- 關鍵是這一步:因為服務器有所有帶有時間戳的客戶端指令,服務器能準確的重建游戲世界在過去任何一刻的狀態(tài)。事實上,服務器可以精確的重建任何客戶端在任何時間點的游戲世界的狀態(tài)。
- 這意味著服務器知道在你出手的瞬間你的槍口對準的是什么 ,那個位置那一刻是對手在過去時的腦袋,而且服務器也知道他腦袋的位置對于你是現(xiàn)在時。
- 服務器處理那個時間點的爆頭命中邏輯,并下發(fā)狀態(tài)給客戶端。
這個結(jié)果所有人都滿意!
服務器是老大,所以它總是滿意
你也滿意因為你瞄準對手的頭,開槍,并拿到了一個爆頭!
可能只有對手不是完全滿意,如果當他被擊中時他正站在那兒沒動,那就是他的錯,對吧?但當時他在跑。。Wow,那你真的是神狙。
但如果他正好從開放地形躲到了墻后邊,還正想著安全時就在不到一秒內(nèi)被擊中了呢?
是的,就這么發(fā)生了,這是權衡的結(jié)果。因為你擊中的是過去時的他,就算那幾毫秒后他躲起來還是被命中了。
這是有點不公平,但這是所有參與者最滿意的解決方案,這比完美一槍去沒命中要好太多!
結(jié)論
快節(jié)奏多人游戲系列到此結(jié)束。這類事情要做好的確很棘手,但如果清晰的理解了事情的原理,就不再那么困難。
雖然這些文章是為游戲開發(fā)者所寫,但另一組讀者也會感興趣,那就是玩家!做為玩家肯定也想知道為什么出現(xiàn)這樣的情況,原因是什么。
總結(jié)
以上是生活随笔為你收集整理的服务端构架干货:快节奏多人游戏的技术实现的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 图形渲染技术分享:《GTA V 》图形分
- 下一篇: unity实用技术:色盲玩家也能享受好的