總結(jié) Overwatch Gameplay Architecture and Netcode 守望先鋒的游戲架構(gòu)與網(wǎng)絡(luò)代碼
原視頻
https://www.youtube.com/watch?v=W3aieHjyNvw&list=PLG9sbQS_QV1-6MnaNaN-uO5fUkaTocN58&index=4&t=2901s
翻譯鏈接
https://www.lfzxb.top/ow-gdc-gameplay-architecture-and-netcode/
還是看視頻和中文解析吧。我自己翻譯起來(lái)太麻煩了,主要是截圖浪費(fèi)時(shí)間, 上面的翻譯鏈接已經(jīng)足夠好了。
這里我截圖并且總結(jié)一下關(guān)鍵點(diǎn)。
選擇ECS
和傳統(tǒng)的Actor model和最近的 Component model不同,ECS是一個(gè)獨(dú)立的結(jié)構(gòu) Entity是一個(gè)ID,類似數(shù)據(jù)庫(kù)查詢的一個(gè)ID,可以找到這個(gè)ID對(duì)應(yīng)的所有Component數(shù)據(jù)。
ECS 架構(gòu)能夠在快速增長(zhǎng)的代碼庫(kù)上管理復(fù)雜性
ECS的結(jié)構(gòu)解釋
Component是一個(gè)純數(shù)據(jù)(State狀態(tài)),無(wú)任何邏輯(Behaviour行為)
System是一個(gè)純邏輯,無(wú)任何數(shù)據(jù)

在一個(gè)World中包含N個(gè)System和M個(gè)Entity

多個(gè)System的Tick有Order執(zhí)行
一個(gè)System可能需要多個(gè)Component,去組成一個(gè)ArchType
System并不關(guān)心Entity,System只關(guān)注Entity上的部分Component Tuple

下圖代碼舉例,去解釋上面的System和Component關(guān)系,System Behaviour如何與Component data關(guān)聯(lián)起來(lái)

下圖:World Entity System 三者之間的關(guān)系
EntityAdmin(World)里有
1.System數(shù)組
2.EntityId的映射,并且通過(guò)這個(gè)ID可以找到Entity包含的所有Component數(shù)。同時(shí)我們要管理不同Component的生命周期
3.System里會(huì)關(guān)聯(lián)多個(gè)Component,如下代碼圖


舉反例說(shuō)明使用System Behaviour執(zhí)行AFK判斷的優(yōu)點(diǎn):
下面代碼圖是ECS實(shí)現(xiàn)的Connection State的判斷

如果使用OOP或者ComponentModel,我們要把Connection State的更新邏輯放到哪個(gè)模塊呢?這么多和Connection有耦合的模塊。
ConnectionState不是Behaviour,只是State。
OOP的每個(gè)Component是Behaviour+State

制作ECS遇到了2個(gè)問(wèn)題,1.system里存儲(chǔ)數(shù)據(jù)了 2.system里耦合其他system,并且需要讀取system的數(shù)據(jù)
創(chuàng)建了一個(gè) global Entity admin,然后可以獲取system。
這造成了擴(kuò)展困難。
當(dāng)在實(shí)現(xiàn)回放的功能時(shí),這個(gè)問(wèn)題暴露出來(lái)了。

兩個(gè)world: liveGame和replayGame
replayGame: server發(fā)送一個(gè)8-12秒的網(wǎng)絡(luò)數(shù)據(jù)到client,然后像正常游戲一樣工作。細(xì)節(jié)需要看另外一個(gè)分享。這里沒(méi)有展開(kāi)

取消了之前的做法:創(chuàng)建一個(gè)Shared EntityAdmin
新的規(guī)則:只有一個(gè)Admin (world).
這個(gè)Admin里可以有 Singleion Components。這些Singleion Components存在一個(gè)匿名的single Entity上。然后Admin可以直接訪問(wèn)這個(gè)AnonymousSingleEntiy,去獲取上面的SignleComponent

舉例 Input的數(shù)據(jù)作為一個(gè)SingleComponent,其他System讀取這個(gè)InputComponent數(shù)據(jù),去同步給Server 去執(zhí)行本地的移動(dòng)等。
SingleComponent大改占有了40%

在使用了ECSingletonInput數(shù)據(jù)后,我們就不存在System之間的耦合了

多個(gè)system Behaviour去共享static function。
這里的 side effects,我覺(jué)得可以理解為“寫數(shù)據(jù)、發(fā)送網(wǎng)絡(luò)請(qǐng)求”之類的意思,可能會(huì)影響這個(gè)Component數(shù)據(jù)的值。
舉例:服務(wù)器和client都會(huì)調(diào)用相同的 static movement funciton,去修改entity的 position
兩種共享utility functions的情況

簡(jiǎn)化你的行為(SIMPLIFY YOUR BEHAVIORS)
-
在單一調(diào)用點(diǎn)表達(dá)行為(Express in a single call site) 把一個(gè)行為的邏輯集中在一個(gè)函數(shù)調(diào)用的地方表達(dá)出來(lái),而不是在很多地方分散處理。
-
將主要副作用局部化到該調(diào)用點(diǎn)(Localize major side effects to that call site) 副作用是指函數(shù)執(zhí)行時(shí)產(chǎn)生的額外影響,比如修改狀態(tài)、打印日志、發(fā)出網(wǎng)絡(luò)請(qǐng)求等。這里強(qiáng)調(diào)的是,**把這些副作用集中在一個(gè)地方(調(diào)用點(diǎn))**處理,而不是讓副作用“到處散落”

延遲處理 Deferment
如果多個(gè)system都要用到一個(gè)Component數(shù)據(jù)
我們可以使用singleComponent, pending后延遲處理這個(gè)數(shù)據(jù)帶來(lái)的效果
存儲(chǔ)并推遲調(diào)用:storing the state required to invoke major side

下圖是舉例說(shuō)明上面的延遲處理概念:
Singleton Contact 單例Contact效果
作者舉例:槍械和貼花在墻面上的特效,為了避免在同一片區(qū)域多種貼花效果z fighting然后和TA battle的問(wèn)題,使用了下圖的方式。
1.先有個(gè)PendingContact數(shù)組
2.有耦合的system點(diǎn),向pendingContact數(shù)組里添加數(shù)據(jù)
3.在最終的ResolveContactSystem里執(zhí)行(lod mix等)效果
有些數(shù)據(jù)流的感覺(jué),前面都是修改數(shù)據(jù),最后才歸納所有數(shù)據(jù),分析執(zhí)行,得到最后的效果
優(yōu)點(diǎn)除了解除耦合外,如果做異步加載也很容易(大量相同的effect延遲分段生成)。

Overwatch左上角的信息
-
FPS: 70
當(dāng)前的幀率(Frames Per Second)為 70,表示畫面每秒刷新 70 次。數(shù)值越高,游戲越流暢。 -
PNG: 248 ms
表示 Ping 值,即你客戶端到服務(wù)器之間發(fā)送一個(gè)數(shù)據(jù)包并接收到回復(fù)的時(shí)間。
248ms 有點(diǎn)偏高,可能會(huì)感到延遲。 -
RTT: 266 ms
Round Trip Time,也就是完整的來(lái)回耗時(shí),理論上 RTT ≈ Ping,但有時(shí)候 RTT 會(huì)包括更多網(wǎng)絡(luò)開(kāi)銷和排隊(duì)延遲。 -
IND: 24 ms
這是 Interpolation Delay,也稱為插值延遲,是客戶端用來(lái)平滑補(bǔ)償網(wǎng)絡(luò)抖動(dòng)的延遲。值越大說(shuō)明你看到的畫面越滯后于真實(shí)情況。24ms 屬于正常范圍。

下圖是接下來(lái)要講的網(wǎng)絡(luò)方面的大綱:
BREADTH(廣度)
-
Novel techniques 使用了一些新穎的技術(shù)手段。
-
Reduced complexity through ECS 通過(guò)使用 ECS(實(shí)體-組件-系統(tǒng))架構(gòu),來(lái)減少?gòu)?fù)雜性。
ECS 是一種常用于游戲開(kāi)發(fā)的架構(gòu)模式,有助于提升性能和模塊化程度。在網(wǎng)絡(luò)同步方面,ECS 也能幫助分離數(shù)據(jù)與邏輯,簡(jiǎn)化狀態(tài)同步。 -
不會(huì)講的內(nèi)容(Not covering):
-
General replication of entities 不會(huì)講通用的實(shí)體同步機(jī)制(比如 transform 復(fù)制那種標(biāo)準(zhǔn)操作),因?yàn)檫@不是本次主題重點(diǎn)。
-
Remote entity interpolation 不會(huì)涉及遠(yuǎn)程實(shí)體插值的細(xì)節(jié)。這通常用于客戶端平滑遠(yuǎn)程玩家的位置以應(yīng)對(duì)網(wǎng)絡(luò)延遲。
-
Details of backwards reconciliation 不會(huì)講反向校正的細(xì)節(jié),即在客戶端預(yù)測(cè)錯(cuò)了之后,如何回滾并重放輸入來(lái)修正狀態(tài)。這屬于網(wǎng)絡(luò)同步中的高級(jí)話題。
-

Determinism
DETERMINISM(確定性),主要講的是在網(wǎng)絡(luò)游戲中如何實(shí)現(xiàn)“客戶端與服務(wù)器狀態(tài)一致性”的關(guān)鍵策略
Synchronized Clock(同步時(shí)鐘)
所有客戶端和服務(wù)器使用一個(gè)統(tǒng)一同步的時(shí)鐘基準(zhǔn)。
這樣才能保證大家在相同的邏輯時(shí)間點(diǎn)處理同樣的輸入,例如第 100 幀時(shí)所有人執(zhí)行的是同一批命令。
-
在 lockstep 或 deterministic replay(決定性回放)系統(tǒng)中尤為關(guān)鍵;
-
通常會(huì)通過(guò) NTP、Ping 反饋、幀號(hào)對(duì)齊等方式校準(zhǔn)時(shí)鐘。
Fixed Update(固定更新步長(zhǎng)) 使用固定時(shí)間步長(zhǎng)(如每 16ms 更新一次),而不是幀率驅(qū)動(dòng)邏輯。
Quantization(量化)將浮點(diǎn)值(比如位置、角度)進(jìn)行離散化處理,通常是為了:
-
降低浮點(diǎn)誤差對(duì)狀態(tài)同步的影響;
-
在重播/回放或狀態(tài)同步中保證一致性;
-
降低網(wǎng)絡(luò)帶寬(壓縮為 8bit、16bit 等)。
-
16ms command frames(16毫秒命令幀)意味著客戶端每 16ms 發(fā)送一次輸入命令,服務(wù)器也以同樣頻率接收和處理這些命令。這個(gè)節(jié)奏可以協(xié)調(diào)多個(gè)客戶端的輸入并保持同步。
Overwatch中,雖然它是一個(gè) 高動(dòng)作性、非 lockstep 的 FPS 游戲,但為了優(yōu)化同步效率、減少帶寬、提高一致性,他們?nèi)匀辉谀承┻壿媽又袘?yīng)用了“確定性設(shè)計(jì)思想”。
在游戲邏輯中,時(shí)間被量化成命令幀

下圖說(shuō)了是怎么把time量化成fixed frame id的
-
Loop clock => Fixed frames 循環(huán)時(shí)鐘(真實(shí)運(yùn)行時(shí)間)映射為固定幀(邏輯幀)
-
Add loop clock time to previous remainder 將當(dāng)前循環(huán)的時(shí)鐘時(shí)間加入上次未處理完的剩余時(shí)間
-
Increment command frame 如果累計(jì)時(shí)間足夠,推進(jìn)一個(gè)邏輯命令幀
-
Roll-over remainder to next frame 多出來(lái)的時(shí)間保留到下一個(gè)循環(huán)處理
-
-
System::UpdateFixed 所有固定步長(zhǎng)邏輯在
System::UpdateFixed()中執(zhí)行

下圖模擬了正常情況下Client發(fā)送數(shù)據(jù)給Server的方式,可以看出Client是領(lǐng)先Sever并且提前執(zhí)行預(yù)測(cè)。
下圖的Half RTT很好理解
下圖Buffer的話是Server Command Buffer。
server這里cache了一幀,server讀取buffer里的Command,去權(quán)威的執(zhí)行client的行為,并加入到要發(fā)送的快照隊(duì)列里去


接下來(lái)三張圖:說(shuō)明服務(wù)器下發(fā)的快照和Client預(yù)測(cè)的運(yùn)動(dòng)沖突后,Client需要回滾執(zhí)行權(quán)威的服務(wù)器版本,從第17幀開(kāi)始
圖1:服務(wù)器計(jì)算第17幀的行為,玩家被冰凍了

當(dāng)?shù)?7幀的快照到達(dá)Client,Client的預(yù)測(cè)和服務(wù)器權(quán)威快照有沖突

Client從第17幀開(kāi)始回滾執(zhí)行

下圖:Server沒(méi)有正常的接收到Client的Command請(qǐng)求,耗盡了CommandBuffer里的數(shù)據(jù)。
Server簡(jiǎn)單的使用之前的輸入數(shù)據(jù)進(jìn)行Fake 模擬,并且回包中標(biāo)記為當(dāng)前是LostCommand的,接下來(lái)會(huì)告訴client

客戶端收到這個(gè)輸入丟失的標(biāo)記后,會(huì)提高send頻率,也許類似unity fixedupdate 修改fixedDeltaTime之類的
Server同時(shí)會(huì)增大Command buffer
如下面2張圖


經(jīng)典的發(fā)送冗余input, 大大降低輸入丟包。如下圖

大致提了Ability也是可以預(yù)測(cè)并且根據(jù)時(shí)間去回滾的,工作原理和上面的movement差不多,需要看另外一個(gè)GDC分享,這里沒(méi)有細(xì)說(shuō)。
Hit Registeration 命中檢測(cè)
首先傷害的結(jié)算使用延遲處理 Deferment在服務(wù)器進(jìn)行。就像上面提到的effect contact一樣。
命中的預(yù)測(cè)是在client進(jìn)行的。
服務(wù)器收到命中請(qǐng)求后,會(huì)在對(duì)應(yīng)的時(shí)刻(倒回rewound)效驗(yàn)

下圖舉例說(shuō)明服務(wù)器倒回Rewound去檢測(cè)命中和產(chǎn)生傷害。
首先在0.5s內(nèi)給每個(gè) 敵人目標(biāo)設(shè)置時(shí)間段的box,并且檢測(cè)shooting raycast是否和這個(gè)box相交。
如果相交,再rewound指定的 敵人。這也算一種優(yōu)化吧。并不是rewound所有的entity。只關(guān)注可能的部分。

下圖也是一個(gè)例子。在模擬60%丟包率情況下,
綠色和藍(lán)色是client下的 target和bullet
黃色和紫色是server下的模擬。

當(dāng)RTT超過(guò)220,客戶端預(yù)測(cè)命中開(kāi)始變得不太有效了。視頻的40分鐘左右。
不在進(jìn)行預(yù)測(cè)命中,直接交給Server去處理。
原因:客戶端上的Target外插值太久了,雖然看起來(lái)是及時(shí)響應(yīng)的(及時(shí)反饋了飆血Effect,但是真正的HP bar和命中點(diǎn)并沒(méi)有顯示),但客戶端預(yù)測(cè)命中實(shí)際上沒(méi)什么效果。
retrospective 回顧
主要是講了ecs的一些使用上的優(yōu)點(diǎn)
1.定義ComponentTuple,清晰的知道要做什么

2.明確的知道system需要哪些數(shù)據(jù)。進(jìn)行并行化。舉例:transform組件,明確的知道哪些System只是Read Transform,然后并行安全。
Entity lifeTime
延遲創(chuàng)建和延遲銷毀。
延遲創(chuàng)建帶來(lái)了麻煩的一幀的問(wèn)題。

規(guī)則:

ECS不是一種強(qiáng)制性設(shè)計(jì)原則。如果有些代碼不適合ECS,不要強(qiáng)制塞入ECS
update system在主線程上
有些獨(dú)立的功能是在work thread上的。
比如projectile的模擬,比如nav 路徑的重建(和ecs無(wú)關(guān))


CLOSING(總結(jié))
-
ECS 是“粘合劑” ECS(Entity Component System)作為一種架構(gòu),是用來(lái)連接游戲不同子系統(tǒng)(比如輸入、動(dòng)畫、物理、技能邏輯、網(wǎng)絡(luò)等)的一種“中介”結(jié)構(gòu)
-
ECS 可以最小化耦合度 ECS 的好處是能讓你寫出非常 解耦、模塊化、可組合 的代碼
-
系統(tǒng)之間互不依賴;
-
網(wǎng)絡(luò)代碼只處理組件的變化;
-
渲染代碼、輸入處理、模擬邏輯彼此隔離。
-
-
要對(duì)你的粘合代碼(glue code)加以約束(上面提到的規(guī)則)
“粘合代碼”指的是把多個(gè)子系統(tǒng)“連起來(lái)”的中間層代碼(例如:網(wǎng)絡(luò)→ECS、技能系統(tǒng)→特效、輸入→行為控制器)
這類代碼很容易變成“爛泥山”:
-
邏輯穿插、職責(zé)混亂、耦合過(guò)高;
-
很難調(diào)試或替換某一模塊;
-
網(wǎng)絡(luò)變化時(shí),影響波及全系統(tǒng)。
-
-
網(wǎng)絡(luò)代碼很復(fù)雜,所以一定要把它解耦
網(wǎng)絡(luò)代碼尤其難寫(tricky),原因包括:
-
要考慮丟包、延遲、亂序、帶寬限制;
-
還要兼容回滾、預(yù)測(cè)、補(bǔ)償;
-
而且所有這些不能污染游戲核心邏輯
所以最好設(shè)計(jì)成:
-
網(wǎng)絡(luò)模塊 → 只處理收發(fā)消息、解包、緩沖、回放;
-
游戲邏輯模塊 → 完全通過(guò)命令數(shù)據(jù)驅(qū)動(dòng)(例如 ApplyCommandFrame())
-

提問(wèn)時(shí)間:
在Component上 有沒(méi)有實(shí)double buffer?
答:沒(méi)有用到,movement和input是一個(gè)ring buffer.你說(shuō)的這個(gè)double buffer也好實(shí)現(xiàn)的,我們沒(méi)用到
你說(shuō)子彈的命中用的發(fā)射者去判斷。 如果子彈的生命周期中發(fā)射者在不停地變或者離開(kāi)了怎么辦?
答:這是個(gè)設(shè)計(jì)問(wèn)題,我們沒(méi)用到,具體問(wèn)題具體對(duì)待
游戲是一定60Hz幀的嗎?如果只有30hz的模擬怎么辦呢?
答:是的,邏輯幀一定是60的。
如果客戶端CPU性能很差,我們可以做一些優(yōu)化,去避免Death Spiral。
首先 ,本地預(yù)測(cè)玩家是重點(diǎn)(優(yōu)先處理),所以本地預(yù)測(cè)(local player prediction)是最精確、最及時(shí)的;這是高優(yōu)先級(jí)的模擬,占用 CPU 比較多是合理的。
### ② 遠(yuǎn)程角色(其他玩家)使用“預(yù)算化模擬”
關(guān)鍵詞:Budgeting(設(shè)定每幀上限)
“遠(yuǎn)程角色腳本模擬最多只能花 1.5ms”
解釋:
-
遠(yuǎn)程玩家的狀態(tài),客戶端通常是插值+預(yù)測(cè)出來(lái)的,不需要超高精度;
-
可以設(shè)置每幀最多處理幾個(gè)遠(yuǎn)程實(shí)體、或者限制腳本調(diào)用復(fù)雜度;
-
如果時(shí)間不夠,可以“跳過(guò)一些非關(guān)鍵計(jì)算”或“延后一幀處理”。
常見(jiàn)策略包括:
-
模擬遠(yuǎn)程玩家動(dòng)畫、姿態(tài)而不是精確動(dòng)作邏輯;
-
遠(yuǎn)程技能只表現(xiàn)結(jié)果,不模擬路徑;
-
Tick 頻率降低(例如遠(yuǎn)程實(shí)體每?jī)蓭?Tick 一次)。
### ③ Spike Smoothing(峰值平滑)技術(shù)
解釋:
-
有時(shí)幀突然很“重”(比如10個(gè)角色同時(shí)釋放技能),你不能一股腦處理完;
-
可以將部分模擬負(fù)載**“分幀處理”**,讓邏輯稍微延后,但保持幀率穩(wěn)定;
-
平滑掉 CPU usage 的 spike,避免掉幀和“death spiral”。
技術(shù)方式可能包括:
-
邏輯排隊(duì),按預(yù)算逐幀執(zhí)行;
-
異步/分批腳本執(zhí)行;
-
行為拆解,拆成多個(gè)幀去執(zhí)行(比如動(dòng)畫分段執(zhí)行、傷害延后一幀觸發(fā));
很慢的導(dǎo)彈也做預(yù)測(cè)了嗎?
答:目前沒(méi)考慮過(guò)不做預(yù)測(cè),有趣有趣。
量化空間有多精細(xì)?那物理引擎在這個(gè)精度下會(huì)遇到問(wèn)題嗎?有多少游戲邏輯會(huì)受到物理引擎的影響?
答:
-
空間量化的精度是大約 1 米 / 1024(≈1mm)
-
也就是一個(gè)浮點(diǎn)數(shù)表示的位置,會(huì)被壓縮為大概 10-bit 的離散格。
-
-
他們將“視覺(jué)物理引擎”和“游戲物理引擎”分離了:
-
視覺(jué)物理(client physics)主要是為了特效、擊中效果等視覺(jué)表現(xiàn);
-
游戲模擬物理(simulation physics)才是用于權(quán)威狀態(tài)判斷、邏輯碰撞。
-
-
兩個(gè)物理引擎用的是同一套底層邏輯代碼(同一個(gè)人寫的):
-
所以行為一致性高,且跨平臺(tái)行為一致(AMD/Intel/Linux等)。我們用了很明確的指令/手法控制浮點(diǎn)行為。
- 客戶端和服務(wù)器共享關(guān)鍵邏輯代碼(或者用統(tǒng)一庫(kù)編譯成 DLL、共享模塊)
-
非常穩(wěn)定,不會(huì)因?yàn)槠脚_(tái)浮點(diǎn)差異出問(wèn)題。
-
-
他們擔(dān)心浮點(diǎn)編譯器差異,比如 Clang 優(yōu)化 & 執(zhí)行亂序
-
但由于用了量化,很多浮點(diǎn)誤差問(wèn)題被規(guī)避了;
-
如果有更具體的疑問(wèn),可以去第二天某位講者的分享深聊 IEEE 浮點(diǎn)和一致性問(wèn)題。
-
你們客戶端預(yù)測(cè)依賴于確定性(determinism),動(dòng)態(tài)導(dǎo)航網(wǎng)格(navmesh)看起來(lái)是異步的,那怎么保持一致?
答:整個(gè)模擬系統(tǒng)是基于固定時(shí)間步長(zhǎng)(fixed timestep),所有系統(tǒng)都在統(tǒng)一節(jié)奏下運(yùn)行,是實(shí)現(xiàn)一致性的關(guān)鍵基礎(chǔ)。
他們不是 100% 的確定性(不像 StarCraft/Halo 那樣)
開(kāi)發(fā)者 Igor 做了一個(gè)“預(yù)測(cè)差異調(diào)試器”
-
可以看到哪一幀服務(wù)器和客戶端不一致;
-
可以精確地定位是哪個(gè)數(shù)學(xué)或邏輯步驟出現(xiàn)偏差;
-
通常是“尋找角色腳部的射線(ray)”導(dǎo)致的問(wèn)題。
關(guān)于 Navmesh:
-
Navmesh 的結(jié)果在客戶端和服務(wù)器給相同輸入時(shí)會(huì)一致;
-
但重建 Navmesh 的耗時(shí)可能不一致,這并不會(huì)導(dǎo)致嚴(yán)重問(wèn)題;
-
因?yàn)?span style="background-color: rgba(204, 153, 255, 1)">大多數(shù)玩家移動(dòng)技能不依賴 Navmesh;
-
即使錯(cuò)預(yù)測(cè),也會(huì)被服務(wù)器矯正回來(lái),不影響最終一致?tīng)顟B(tài);
-
不會(huì)為 Navmesh 做完整狀態(tài)回滾(太大,占 40MB),僅回滾必要內(nèi)容。

浙公網(wǎng)安備 33010602011771號(hào)