小男孩‘自慰网亚洲一区二区,亚洲一级在线播放毛片,亚洲中文字幕av每天更新,黄aⅴ永久免费无码,91成人午夜在线精品,色网站免费在线观看,亚洲欧洲wwwww在线观看

分享

《守望先鋒》架構(gòu)設(shè)計(jì)與網(wǎng)絡(luò)同步

 kiki的號(hào) 2017-06-14

《守望先鋒》架構(gòu)設(shè)計(jì)與網(wǎng)絡(luò)同步

Overwatch Gameplay Architecture and Netcode

Timothy Ford

Lead Gameplay Engineer

Blizzard Entertainment

翻譯:kevinan

 

GDC2017Overwatch Gameplay Architecture andNetcode 】的分享會(huì)上,來(lái)自暴雪的Tim Ford介紹了《守望先鋒》游戲架構(gòu)和網(wǎng)絡(luò)同步的設(shè)計(jì)。一起來(lái)看看吧。

《守望先鋒》架構(gòu)設(shè)計(jì)與網(wǎng)絡(luò)同步  -- GDC2017 精品分享實(shí)錄

《守望先鋒》架構(gòu)設(shè)計(jì)與網(wǎng)絡(luò)同步  -- GDC2017 精品分享實(shí)錄

《守望先鋒》架構(gòu)設(shè)計(jì)與網(wǎng)絡(luò)同步  -- GDC2017 精品分享實(shí)錄

          哈嘍,大家好,這次的分享是關(guān)于《守望先鋒》(譯注:下文統(tǒng)一簡(jiǎn)稱為Overwatch)游戲架構(gòu)設(shè)計(jì)和網(wǎng)絡(luò)部分。老規(guī)矩,手機(jī)調(diào)成靜音;離開時(shí)記得填寫調(diào)查問(wèn)卷;換下半藏,趕緊推車!(眾笑)

          我是Tim Ford,是暴雪公司Overwatch開發(fā)團(tuán)隊(duì)老大。自從2013年夏季項(xiàng)目啟動(dòng)以來(lái)就在這個(gè)團(tuán)隊(duì)了。在那之前,我在《Titan》項(xiàng)目組,不過(guò)這次分享跟Titan沒(méi)有半毛錢關(guān)系。(眾笑)

          這次分享的一些技術(shù),是用來(lái)降低不停增長(zhǎng)的代碼庫(kù)的復(fù)雜度(譯注,代碼復(fù)雜度的概念需要讀者自行查閱)。為了達(dá)到這個(gè)目的我們遵循了一套嚴(yán)謹(jǐn)?shù)募軜?gòu)。最后會(huì)通過(guò)討論網(wǎng)絡(luò)同步(netcode)這個(gè)本質(zhì)很復(fù)雜的問(wèn)題,來(lái)說(shuō)明具體如何管理復(fù)雜性。

 

          Overwatch是一個(gè)近未來(lái)世界觀的在線團(tuán)隊(duì)英雄射擊游戲,它的主要是特點(diǎn)是英雄的多樣性, 每個(gè)英雄都有自己的獨(dú)門絕技。

《守望先鋒》架構(gòu)設(shè)計(jì)與網(wǎng)絡(luò)同步  -- GDC2017 精品分享實(shí)錄

Overwatch使用了一個(gè)叫做實(shí)體組件系統(tǒng)的架構(gòu),接下來(lái)我會(huì)簡(jiǎn)稱它為ECS。

《守望先鋒》架構(gòu)設(shè)計(jì)與網(wǎng)絡(luò)同步  -- GDC2017 精品分享實(shí)錄

          ECS不同于一些現(xiàn)成引擎中很流行的那種組件模型,而且與90年代后期到21世紀(jì)早期的經(jīng)典Actor模式區(qū)別更大。我們團(tuán)隊(duì)對(duì)這些架構(gòu)都有多年的經(jīng)驗(yàn),所以我們選擇用ECS有點(diǎn)是這山望著那山高的意味。不過(guò)我們事先制作了一個(gè)原型,所以這個(gè)決定并不是一時(shí)沖動(dòng)。

開發(fā)了3年多以后,我們才發(fā)現(xiàn),原來(lái)ECS架構(gòu)可以管理快速增長(zhǎng)的代碼復(fù)雜性。雖然我很樂(lè)意分享ECS的優(yōu)點(diǎn),但是要知道,我今天所講的一切其實(shí)都是事后諸葛亮 。

 

ECS架構(gòu)概述

《守望先鋒》架構(gòu)設(shè)計(jì)與網(wǎng)絡(luò)同步  -- GDC2017 精品分享實(shí)錄

          ECS架構(gòu)看起來(lái)就是這樣子的。先有個(gè)World,它是系統(tǒng)(譯注,這里的系統(tǒng)指的是ECS中的S,不是一般意義上的系統(tǒng),為了方便閱讀,下文統(tǒng)稱System)和實(shí)體(Entity)的集合。而實(shí)體就是一個(gè)ID,這個(gè)ID對(duì)應(yīng)了組件(Component)的集合。組件用來(lái)存儲(chǔ)游戲狀態(tài)并且沒(méi)有任何的行為(Behavior)System有行為但是沒(méi)有狀態(tài)。

 

這聽起來(lái)可能挺讓人驚訝的,因?yàn)榻M件沒(méi)有函數(shù)而System沒(méi)有任何字段。

 

 


 

ECS引擎用到的System和組件

《守望先鋒》架構(gòu)設(shè)計(jì)與網(wǎng)絡(luò)同步  -- GDC2017 精品分享實(shí)錄

          圖的左手邊是以輪詢順序排列的System列表,右邊是不同實(shí)體擁有的組件。在左邊選擇不同的System以后,就像彈鋼琴一樣,所有對(duì)應(yīng)的組件會(huì)在右邊高亮顯示,我們管這叫組件元組(譯注,元組tuple,從后文來(lái)看,主要作用就是可以調(diào)用Sibling函數(shù)來(lái)獲取同一個(gè)元組內(nèi)的組件,有點(diǎn)虛擬分組的意思)。

System遍歷檢查所有元組,并在其狀態(tài)(State)上執(zhí)行一些操作(也就是行為Behavior)。記住組件不包含任何函數(shù),它的狀態(tài)都是裸存儲(chǔ)的。

          絕大多數(shù)的重要System都關(guān)注了不止一個(gè)組件,如你所見(jiàn),這里的Transform組件就被很多System用到。


 

來(lái)自原型引擎里的一個(gè)System輪詢(tick)的例子

《守望先鋒》架構(gòu)設(shè)計(jì)與網(wǎng)絡(luò)同步  -- GDC2017 精品分享實(shí)錄

          這個(gè)是物理System的輪詢函數(shù),非常直截了當(dāng),就是一個(gè)內(nèi)部物理引擎的定時(shí)更新。物理引擎可能是Box2d或者是Domino(暴雪自有物理引擎)。執(zhí)行完物理世界的模擬以后,就遍歷元組集合。用DynamicPhysicsComponent組件里保存的proxy來(lái)取到底層的物理表示,并把它復(fù)制給Transform組件和Contact組件(譯注:碰撞組件,后文會(huì)大量用到)。

《守望先鋒》架構(gòu)設(shè)計(jì)與網(wǎng)絡(luò)同步  -- GDC2017 精品分享實(shí)錄

System不知道實(shí)體到底是什么,它只關(guān)心組件集合的小切片(slice,譯注:可以理解為特定子集合),然后在這個(gè)切片上執(zhí)行一組行為。有些實(shí)體有多達(dá)30個(gè)組件,而有些只有2、3個(gè),System不關(guān)心數(shù)量,它只關(guān)心執(zhí)行操作行為的組件的子集。

像這個(gè)原型引擎里的例子,(指著上圖7中)這個(gè)是玩家角色實(shí)體,可以做出很多很酷的行為,右邊這些是玩家能夠發(fā)射的子彈實(shí)體。

每個(gè)System在運(yùn)行時(shí),不知道也不關(guān)心這些實(shí)體是什么,它們只是在實(shí)體相關(guān)組件的子集上執(zhí)行操作而已。

Overwatch里的(ECS架構(gòu)的)實(shí)現(xiàn),就是這樣子的。

《守望先鋒》架構(gòu)設(shè)計(jì)與網(wǎng)絡(luò)同步  -- GDC2017 精品分享實(shí)錄

EntityAdmin是個(gè)World,存儲(chǔ)了一個(gè)所有System的集合,和一個(gè)所有實(shí)體的哈希表。表鍵是實(shí)體的ID。ID是個(gè)32位無(wú)符號(hào)整形數(shù),用來(lái)在實(shí)體管理器(Entity Array)上唯一標(biāo)識(shí)這個(gè)實(shí)體。另一方面,每個(gè)實(shí)體也都存了這個(gè)實(shí)體ID和資源句柄(resource handle),后者是個(gè)可選字段,指向了實(shí)體對(duì)應(yīng)的Asset資源(譯注:這需要依賴暴雪的另一套專門的Asset管理系統(tǒng)),資源定義了實(shí)體。

組件Component是個(gè)基類,有幾百個(gè)子類。每個(gè)子類組件都含有在System上執(zhí)行Behavior時(shí)所需的成員變量。在這里多態(tài)唯一的用處就是重載Create和析構(gòu)(Destructor)之類的生命周期管理函數(shù)。而其他能被繼承組件類實(shí)例直接使用的,就只有一些用來(lái)方便地訪問(wèn)內(nèi)部狀態(tài)的helper函數(shù)了。但這些helper函數(shù)不是行為(譯注:這里強(qiáng)調(diào)是為了遵循前面提到的原則:組件沒(méi)有行為),只是簡(jiǎn)單的訪問(wèn)器。

《守望先鋒》架構(gòu)設(shè)計(jì)與網(wǎng)絡(luò)同步  -- GDC2017 精品分享實(shí)錄

EntityAdmin的結(jié)尾部分會(huì)調(diào)用所有SystemUpdate。每個(gè)System都會(huì)做一些工作。上圖9就是我們的使用方式,我們沒(méi)有在固定的元組組件集合上執(zhí)行操作,而是選擇了一些基礎(chǔ)組件來(lái)遍歷,然后再由相應(yīng)的行為去調(diào)用其他兄弟組件。所以你可以看到這里的操作只針對(duì)那些含有DerpHerp組件的實(shí)體的元組執(zhí)行。

Overwatch客戶端的System和組件列表

《守望先鋒》架構(gòu)設(shè)計(jì)與網(wǎng)絡(luò)同步  -- GDC2017 精品分享實(shí)錄

          這里有大概46不同的System103個(gè)組件。這一頁(yè)的炫酷動(dòng)畫是用來(lái)吸引你們看的(眾笑)。

然后是服務(wù)器

《守望先鋒》架構(gòu)設(shè)計(jì)與網(wǎng)絡(luò)同步  -- GDC2017 精品分享實(shí)錄

你可以看到有些System執(zhí)行需要很多組件,而有些System僅僅需要幾個(gè)。理想情況下,我們盡量確保每個(gè)System都依賴很多組件去運(yùn)行。把他們當(dāng)成純函數(shù)(譯注,pure function,無(wú)副作用的函數(shù)),而不改變(mutating)它們的狀態(tài),就可以做到這一點(diǎn)。我們的確有少量的System需要改變組件狀態(tài),這種情況下它們必須自己管理復(fù)雜性。

下面是個(gè)真實(shí)的System代碼

《守望先鋒》架構(gòu)設(shè)計(jì)與網(wǎng)絡(luò)同步  -- GDC2017 精品分享實(shí)錄

          這個(gè)System是用來(lái)管理玩家連接的,它負(fù)責(zé)我們所有游戲服務(wù)器上的強(qiáng)制下線(譯注,AFK, Away From Keyboard,表示長(zhǎng)時(shí)間沒(méi)操作而被認(rèn)為離線)功能。

這個(gè)System遍歷所有的Connection組件(譯注:這里不太合適直接翻譯成連接),Connection組件用來(lái)管理服務(wù)器上的玩家網(wǎng)絡(luò)連接,是掛在代表玩家的實(shí)體上的。它可以是正在進(jìn)行比賽的玩家、觀戰(zhàn)者或者其他玩家控制的角色。System不知道也不關(guān)心這些細(xì)節(jié),它的職責(zé)就是強(qiáng)制下線。

          每一個(gè)Connection組件的元組包含了輸入流(InputStream)Stats組件(譯注:看起來(lái)是用來(lái)統(tǒng)計(jì)戰(zhàn)斗信息的)。我們從輸入流組件讀入你的操作,來(lái)確保你必須做點(diǎn)什么事情,例如鍵盤按鍵;并從Stats組件讀取你在某種程度上對(duì)游戲的貢獻(xiàn)。

你只要做這些操作就會(huì)不停重置AFK定時(shí)器,否則的話,我們就會(huì)通過(guò)存儲(chǔ)在Connection組件上的網(wǎng)絡(luò)連接句柄發(fā)消息給你的客戶端,踢你下線。

          System上運(yùn)行的實(shí)體必須擁有完整的元組才能使得這些行為能夠正常工作。像我們游戲里的機(jī)器人實(shí)體就沒(méi)有Connection組件和輸入流組件,只有一個(gè)Stats組件,所以它就不會(huì)受到強(qiáng)制下線功能的影響。System的行為依賴于完整集合的切片。坦率來(lái)說(shuō),我們也確實(shí)沒(méi)必要浪費(fèi)資源去讓強(qiáng)制機(jī)器人下線。

 

為什么不能直接用傳統(tǒng)面向?qū)ο缶幊棠P停?/span>

《守望先鋒》架構(gòu)設(shè)計(jì)與網(wǎng)絡(luò)同步  -- GDC2017 精品分享實(shí)錄

上面System的更新行為會(huì)帶來(lái)了一個(gè)疑問(wèn):為什么不能使用傳統(tǒng)的面向?qū)ο缶幊?span>(OOP)的組件模型呢?例如在Connection組件里重載Update函數(shù),不停地跟蹤檢測(cè)AFK?

答案是,因?yàn)?span>Connection組件會(huì)同時(shí)被多個(gè)行為所使用,包括:AFK檢查;能接收網(wǎng)絡(luò)廣播消息的已連接玩家列表;存儲(chǔ)包括玩家名稱在內(nèi)的狀態(tài);存儲(chǔ)玩家已解鎖成就之類的狀態(tài)。所以(如果用傳統(tǒng)OOP方式的話)具體哪個(gè)行為應(yīng)該放在組件的Update中調(diào)用?其余部分又應(yīng)該放在哪里?

傳統(tǒng)OOP中,一個(gè)類既是行為又是數(shù)據(jù),但是Connection組件不是行為,它就只是狀態(tài)。Connection完全不符合OOP中的對(duì)象的概念,它在不同的System中、不同的時(shí)機(jī)下,意味著完全不同的事情。

 

那么把行為和狀態(tài)區(qū)分開,又有什么理論上的優(yōu)勢(shì)(conceptual advantages)呢?

《守望先鋒》架構(gòu)設(shè)計(jì)與網(wǎng)絡(luò)同步  -- GDC2017 精品分享實(shí)錄

          想象一下你家前院盛開的櫻桃樹吧,從主觀上講,這些樹對(duì)于你、你們小區(qū)業(yè)委會(huì)主席、園丁、一只鳥、房產(chǎn)稅官員和白蟻而言都是完全不同的。從描述這些樹的狀態(tài)上,不同的觀察者會(huì)看見(jiàn)不同的行為。樹是一個(gè)被不同的觀察者區(qū)別對(duì)待的主體(subject)。

《守望先鋒》架構(gòu)設(shè)計(jì)與網(wǎng)絡(luò)同步  -- GDC2017 精品分享實(shí)錄

類比來(lái)說(shuō),玩家實(shí)體,或者更準(zhǔn)確地說(shuō),Connection組件,就是一個(gè)被不同System區(qū)別對(duì)待的主體。我們之前討論過(guò)的管理玩家連接的System,把Connection組件視為AFK踢下線的主體;連接實(shí)用程序(ConnectUtility)則把Connection組件看作是廣播玩家網(wǎng)絡(luò)消息的主體;在客戶端上,用戶界面System則把Connection組件當(dāng)做記分板上帶有玩家名字的彈出式UI元素主體。

Behavior為什么要這么搞?結(jié)果看來(lái),根據(jù)主體視角區(qū)分所有Behavior,這樣來(lái)描述一棵的全部行為會(huì)更容易,這個(gè)道理同樣也適用于游戲?qū)ο?/b>(game objects)

 

然而隨著這個(gè)工業(yè)級(jí)強(qiáng)度的ECS架構(gòu)的實(shí)現(xiàn),我們遇到了新的問(wèn)題。

首先我們糾結(jié)于之前定下的規(guī)矩:組件不能有函數(shù);System不能有狀態(tài)。顯而易見(jiàn)地,System應(yīng)該可以有一些狀態(tài)的,對(duì)吧?一些從其他非ECS架構(gòu)導(dǎo)入的遺留System都有成員變量,這有什么問(wèn)題嗎?舉個(gè)例子,InputSystem, 你可以把玩家輸入信息保存在InputSystem里,而其他System如果也需要感知按鍵是否被按下,只需要一個(gè)指向InputSystem的指針就能實(shí)現(xiàn)。

在單個(gè)組件里存儲(chǔ)一個(gè)全局變量看起來(lái)很很愚蠢,因?yàn)槟汩_發(fā)一個(gè)新的組件類型,不可能只實(shí)例化一次(譯注:這里的意思是,如果實(shí)例化了多次,就會(huì)有多份全局變量的拷貝,明顯不合理),這一點(diǎn)無(wú)需證明。組件通常都是按照我們之前看見(jiàn)過(guò)的那種方式(譯注:指的是通過(guò)ComponentItr<>函數(shù)模板那種方式)來(lái)迭代訪問(wèn),如果某個(gè)組件在整個(gè)游戲里只有一個(gè)實(shí)例,那這樣訪問(wèn)就會(huì)看起來(lái)比較怪異了。

《守望先鋒》架構(gòu)設(shè)計(jì)與網(wǎng)絡(luò)同步  -- GDC2017 精品分享實(shí)錄

無(wú)論如何,這種方式撐了一陣子。我們?cè)?span>System里存儲(chǔ)了一次性(one-off)的狀態(tài)數(shù)據(jù),然后提供了一個(gè)全局訪問(wèn)方式。從圖16可以看到整個(gè)訪問(wèn)過(guò)程(譯注:重點(diǎn)是g_game->m_inputSystem這一行)。

如果一個(gè)System可以調(diào)用另外一個(gè)System的話,對(duì)于編譯時(shí)間來(lái)說(shuō)就不太友好了,因?yàn)?span>System需要互相包含(include)。假定我現(xiàn)在正在重構(gòu)InputSystem,想移動(dòng)一些函數(shù),修改頭文件(譯注:Client/System/Input/InputSystem.h),那么所有依賴這個(gè)頭文件去獲取輸入狀態(tài)的System都需要被重新編譯,這很煩人,還會(huì)有大量的耦合,因?yàn)?span>System之間互相暴露了內(nèi)部行為的實(shí)現(xiàn)。(譯注:轉(zhuǎn)載不注明出處,真的大丈夫嗎?還把譯者的名字都刪除!聲明:這篇文章是本人kevinan應(yīng)GAD要求而翻譯?。?/span>

從圖16最下面可以看見(jiàn)我們有個(gè)PostBuildPlayerCommand函數(shù),這個(gè)函數(shù)是InputSystem在這里的主要價(jià)值。如果我想在這個(gè)函數(shù)里增加一些新功能,那么CommandSystem就需要根據(jù)玩家的輸入,填充一些額外的結(jié)構(gòu)體信息發(fā)給服務(wù)器。那么我這個(gè)新功能應(yīng)該加到CommandSystem里還是PostBuildPlayerCommand函數(shù)里呢?我正在System之間互相暴露內(nèi)部實(shí)現(xiàn)嗎?

隨著系統(tǒng)的增長(zhǎng),選擇在何處添加新的行為代碼變得模棱兩可。上面CommandSystem的行為填充了一些結(jié)構(gòu)體,為什么要混在一起?又為什么要放到這里而不是別處?


 

無(wú)論如何,我們就這樣湊合了好一陣子,直到死亡回放(Killcam)需求的出現(xiàn)。

《守望先鋒》架構(gòu)設(shè)計(jì)與網(wǎng)絡(luò)同步  -- GDC2017 精品分享實(shí)錄 

《守望先鋒》架構(gòu)設(shè)計(jì)與網(wǎng)絡(luò)同步  -- GDC2017 精品分享實(shí)錄

 

《守望先鋒》架構(gòu)設(shè)計(jì)與網(wǎng)絡(luò)同步  -- GDC2017 精品分享實(shí)錄

          為了實(shí)現(xiàn)Killcam,我們會(huì)有兩個(gè)不同的、并行的游戲環(huán)境,一個(gè)用來(lái)進(jìn)行實(shí)時(shí)游戲過(guò)程渲染,一個(gè)用來(lái)專門做Killcam。我接下來(lái)會(huì)展示它們是如何實(shí)現(xiàn)的。

 

首先,也很直接,我會(huì)添加第二個(gè)全新的ECS World,現(xiàn)在就有兩個(gè)World了,一個(gè)是liveGame(正常游戲),一個(gè)是replayGame用來(lái)實(shí)現(xiàn)回放(Replay)。

《守望先鋒》架構(gòu)設(shè)計(jì)與網(wǎng)絡(luò)同步  -- GDC2017 精品分享實(shí)錄

         

回放(Replay)的工作方式是這樣的,服務(wù)器會(huì)下發(fā)大概812秒左右的網(wǎng)絡(luò)游戲數(shù)據(jù),接著客戶端翻轉(zhuǎn)World,開始渲染replayAdmin這個(gè)World的信息到玩家屏幕上。然后轉(zhuǎn)發(fā)網(wǎng)絡(luò)游戲數(shù)據(jù)給replayAdmin,假裝這些數(shù)據(jù)真的是來(lái)自網(wǎng)絡(luò)的。此時(shí),所有的System,所有的組件,所有的行為都不知道它們并沒(méi)有被預(yù)測(cè)(predict,譯注:后面才講到的同步技術(shù)),它們以為客戶端就是實(shí)時(shí)運(yùn)行在網(wǎng)絡(luò)上的,像正常游戲過(guò)程一樣。

聽起來(lái)很酷吧?如果有人想要了解更多關(guān)于回放的技術(shù),我建議你們明天去聽一下Phil Orwig的分享,也是在這個(gè)房間,上午11點(diǎn)整。

 

《守望先鋒》架構(gòu)設(shè)計(jì)與網(wǎng)絡(luò)同步  -- GDC2017 精品分享實(shí)錄

無(wú)論如何,到現(xiàn)在我們已經(jīng)知道的是:首先,所有需要全局訪問(wèn)System的調(diào)用點(diǎn)(call sites)會(huì)突然出錯(cuò)(譯注:Tim思維太跳躍了,突然話鋒一轉(zhuǎn),完全跟不上);另外,不再只有唯一一個(gè)全局EntityAdmin了,現(xiàn)在有兩個(gè);System A無(wú)法直接訪問(wèn)全局System B,不知怎地,只能通過(guò)共享的EntityAdmin來(lái)訪問(wèn)了,這樣很繞。

Killcam之后,我們花了很長(zhǎng)時(shí)間來(lái)回顧我們的編程模式的缺陷,包括:怪異的訪問(wèn)模式;編譯周期太長(zhǎng);最危險(xiǎn)的是內(nèi)部系統(tǒng)的耦合??雌饋?lái)我們有大麻煩了。

針對(duì)這些問(wèn)題的最終解決方案,依賴于這樣一個(gè)事實(shí):開發(fā)一個(gè)只有唯一實(shí)例的組件其實(shí)沒(méi)什么不對(duì)!根據(jù)這個(gè)原則,我們實(shí)現(xiàn)了一個(gè)單例(Singleton)組件。

《守望先鋒》架構(gòu)設(shè)計(jì)與網(wǎng)絡(luò)同步  -- GDC2017 精品分享實(shí)錄

          這些組件屬于單一的匿名實(shí)體,可以通過(guò)EntityAdmin直接訪問(wèn)。我們把System中的大部分狀態(tài)都移到了單例中。

          這里我要提一句,只需要被一個(gè)System訪問(wèn)的狀態(tài)其實(shí)是很罕見(jiàn)的。后來(lái)在開發(fā)一個(gè)新System的過(guò)程中我們保持了這個(gè)習(xí)慣,如果發(fā)現(xiàn)這個(gè)系統(tǒng)需要依賴一些狀態(tài)。就做一個(gè)單例來(lái)存儲(chǔ),幾乎每一次都會(huì)發(fā)現(xiàn)其他一些System也同樣需要這些狀態(tài),所以這里其實(shí)已經(jīng)提前解決了前面架構(gòu)里的耦合問(wèn)題。

下面是一個(gè)單例輸入的例子。

《守望先鋒》架構(gòu)設(shè)計(jì)與網(wǎng)絡(luò)同步  -- GDC2017 精品分享實(shí)錄

全部按鍵信息都存在一個(gè)單例里面,只是我們把它從InputSystem中移出來(lái)了。任何System如果想知道按鍵是否按下,只需要隨便拿一個(gè)組件來(lái)詢問(wèn)(那個(gè)單例)就行了。這樣做以后,一些很麻煩的耦合問(wèn)題消失了,我們也更加遵循ECS的架構(gòu)哲學(xué)了:System沒(méi)有狀態(tài);組件不帶行為。

按鍵并不是行為,掌管本地玩家移動(dòng)的Movement System里有一個(gè)行為,用這個(gè)單例來(lái)預(yù)測(cè)本地玩家的移動(dòng)。而MovementStateSystem里有個(gè)行為是把這些按鍵信息打包發(fā)到服務(wù)器(譯注:按鍵對(duì)于不同的System就不是不同的主體)。

結(jié)果發(fā)現(xiàn),單例模式的使用非常普遍,我們整個(gè)游戲里的40%組件都是單例的。

《守望先鋒》架構(gòu)設(shè)計(jì)與網(wǎng)絡(luò)同步  -- GDC2017 精品分享實(shí)錄

一旦我們把某些System狀態(tài)移到單例中,會(huì)把共享的System函數(shù)分解成Utility(實(shí)用)函數(shù),這些函數(shù)需要在那些單例上運(yùn)行,這又有點(diǎn)耦合了,我們接下來(lái)會(huì)詳細(xì)討論。

改造后如圖22,InputSystem依然存在(譯注:然而并沒(méi)有看到InputSystem在哪里),它負(fù)責(zé)從操作系統(tǒng)讀取輸入操作,填充SingletonInput的值,然后下游的其他System就可以得到同樣的Input去做它們想做的。

像按鍵映射之類的事情就可以在單例里實(shí)現(xiàn),就與CommandSystem解耦了。

我們把PostBuildPlayerCommand函數(shù)也挪到了CommandSysem里,本應(yīng)如此,現(xiàn)在可以保證所有對(duì)玩家輸入的命令(PlayerCommand)的修改都能且僅能在此處進(jìn)行了。這些玩家命令是很重要的數(shù)據(jù)結(jié)構(gòu),將來(lái)會(huì)在網(wǎng)絡(luò)上同步并用來(lái)模擬游戲過(guò)程。

在引入單例組件時(shí),我們還不知道,我們其實(shí)正在打造的是一個(gè)解耦合、降低復(fù)雜度的開發(fā)模式。在這個(gè)例子中,CommandSystem是唯一一處能夠產(chǎn)生與玩家輸入命令相關(guān)副作用的地方(譯注:sideeffect,指當(dāng)調(diào)用函數(shù)時(shí),除了返回函數(shù)值之外,還對(duì)主調(diào)用函數(shù)產(chǎn)生附加影響,例如修改全局變量了)。

每個(gè)程序員都能輕易地了解玩家命令的變化,因?yàn)樵谝淮?span>System更新的同一時(shí)刻,只有這一處代碼有可能產(chǎn)生變化。如果想添加針對(duì)玩家命令的修改代碼,那也很明朗,只能在這個(gè)源文件中改,所有的模棱兩可都消失了。

 

          現(xiàn)在討論另外一個(gè)問(wèn)題,與共享行為(sharedbehavior)有關(guān)。

《守望先鋒》架構(gòu)設(shè)計(jì)與網(wǎng)絡(luò)同步  -- GDC2017 精品分享實(shí)錄

          共享行為一般出現(xiàn)在同一行為被多個(gè)System用到的時(shí)候。

有時(shí),同一個(gè)主體的兩個(gè)觀察者,會(huì)對(duì)同一個(gè)行為感興趣?;氐角懊鏅鸦涞睦樱愕男^(qū)業(yè)委會(huì)主席和園丁,可能都想知道這棵樹會(huì)在春天到來(lái)的時(shí)候,掉落多少葉子。

根據(jù)這個(gè)輸出可以做不同的處理,至少主席可能會(huì)沖你大喊大叫,園丁會(huì)老老實(shí)實(shí)回去干活,但是這里的行為是相同的。

《守望先鋒》架構(gòu)設(shè)計(jì)與網(wǎng)絡(luò)同步  -- GDC2017 精品分享實(shí)錄

舉個(gè)例子,大量代碼都會(huì)關(guān)心敵對(duì)關(guān)系,例如,實(shí)體A與實(shí)體B互相敵對(duì)嗎?敵對(duì)關(guān)系是由3個(gè)可選組件共同決定的:filter bits,pet masterpet。filter bits存儲(chǔ)隊(duì)伍編號(hào)(team index);pet master存儲(chǔ)了它所擁有全部pet的唯一鍵;pet一般用于像托比昂的炮臺(tái)之類。

如果2個(gè)實(shí)體都沒(méi)有filter bits,那么它們就不是敵對(duì)的。所以對(duì)于兩扇門來(lái)說(shuō),它們就不是敵對(duì)的,因?yàn)樗鼈兊?span>filter bits組件沒(méi)有隊(duì)伍編號(hào)。

如果它們(譯注:2個(gè)實(shí)體)都在同一個(gè)隊(duì)伍,那自然就不是敵對(duì)的,這很容易理解。

如果它們分別屬于永遠(yuǎn)敵對(duì)的2個(gè)隊(duì)伍,它們會(huì)同時(shí)檢查自己身上和對(duì)方身上的pet master組件,確保每個(gè)pet都和對(duì)方是敵對(duì)關(guān)系。這也解決了一個(gè)問(wèn)題:如果你跟每個(gè)人都是敵對(duì)的,那么當(dāng)你建造一個(gè)炮臺(tái)時(shí),炮臺(tái)會(huì)立馬攻擊你(譯注:完全沒(méi)理解為什么會(huì)這樣)。確實(shí)會(huì)的,這是個(gè)bug,我們修復(fù)了。(眾笑)

如果你想檢查一枚飛行中的炮彈的敵對(duì)關(guān)系,只需要回溯檢查射出這枚炮彈的開火者就行了,很簡(jiǎn)單。

這個(gè)例子的實(shí)現(xiàn),其實(shí)就是個(gè)函數(shù)調(diào)用,函數(shù)名是CombatUtilityIsHostile,它接受2個(gè)實(shí)體作為參數(shù),并返回true或者false來(lái)代表它們是否敵對(duì)。無(wú)數(shù)System都調(diào)用了這個(gè)函數(shù)。

《守望先鋒》架構(gòu)設(shè)計(jì)與網(wǎng)絡(luò)同步  -- GDC2017 精品分享實(shí)錄

25中就是調(diào)用了這個(gè)函數(shù)的System,但是如你所見(jiàn),只用到了3個(gè)組件,少得可憐,而且這3個(gè)組件對(duì)它們都是只讀的。更重要的是,它們是純數(shù)據(jù),而且這些System絕不會(huì)修改里面的數(shù)據(jù),僅僅是讀。

再舉一個(gè)用到這個(gè)函數(shù)的例子。

《守望先鋒》架構(gòu)設(shè)計(jì)與網(wǎng)絡(luò)同步  -- GDC2017 精品分享實(shí)錄

作為一個(gè)例子,當(dāng)用到共享行為的Utility函數(shù)時(shí)我們采用了不同的規(guī)則。

如果你想在多處調(diào)用一個(gè)Utility函數(shù),那么這個(gè)函數(shù)就應(yīng)該依賴很少的組件,而且不應(yīng)該帶副作用或者很少的副作用。如果你的Utility函數(shù)依賴很多組件,那就試著限制調(diào)用點(diǎn)的數(shù)量。

我們這里的例子叫做CharacterMoveUtil,這個(gè)函數(shù)用來(lái)在游戲模擬過(guò)程中的每個(gè)tick里移動(dòng)玩家位置。有兩處調(diào)用點(diǎn),一處是在服務(wù)器上模擬執(zhí)行玩家的輸入命令,另一處是在客戶端上預(yù)測(cè)玩家的輸入。

 

《守望先鋒》架構(gòu)設(shè)計(jì)與網(wǎng)絡(luò)同步  -- GDC2017 精品分享實(shí)錄

          我們繼續(xù)用Utility函數(shù)替換 System間的函數(shù)調(diào)用,并把狀態(tài)從System移到單例組件中。

如果你打算用一個(gè)共享的Utility函數(shù)替換System間的函數(shù)調(diào)用,是不可能自動(dòng)地(magically)避免復(fù)雜性的,幾乎都得做語(yǔ)句級(jí)的調(diào)整。

正如你可以把副作用都隱藏在那些公開訪問(wèn)的System函數(shù)后面一樣,你也可以在Utility函數(shù)后面做同樣的事。

如果你需要從好幾處調(diào)用那些Utility函數(shù),就會(huì)在整個(gè)游戲循環(huán)中引入很多嚴(yán)重的副作用。雖然是在函數(shù)調(diào)用后面發(fā)生的,看起來(lái)沒(méi)那么明顯,但這也是相當(dāng)可怕的耦合。

如果本次分享只讓你學(xué)到一點(diǎn)的話,那最好是:如果只有一個(gè)調(diào)用點(diǎn),那么行為的復(fù)雜性就會(huì)很低,因?yàn)樗械母弊饔枚枷薅ǖ胶瘮?shù)調(diào)用發(fā)生的地方了

          下面瀏覽一下我們用來(lái)減少這類耦合的技術(shù)。

《守望先鋒》架構(gòu)設(shè)計(jì)與網(wǎng)絡(luò)同步  -- GDC2017 精品分享實(shí)錄

當(dāng)你發(fā)現(xiàn)有些行為可能產(chǎn)生嚴(yán)重的副作用,又必須執(zhí)行時(shí),先問(wèn)問(wèn)你自己:這些代碼,是必須現(xiàn)在就執(zhí)行嗎?

好的單例組件可以通過(guò)推遲Deferment)來(lái)解決System間耦合的問(wèn)題。推遲存儲(chǔ)了行為所需狀態(tài),然后把副作用延后到當(dāng)前幀里更好的時(shí)機(jī)再執(zhí)行。

 

例如,代碼里有好多調(diào)用點(diǎn)都要生成一個(gè)碰撞特效(impact effects)。

《守望先鋒》架構(gòu)設(shè)計(jì)與網(wǎng)絡(luò)同步  -- GDC2017 精品分享實(shí)錄

包括hitscan(譯注:直射,沒(méi)有飛行時(shí)間)子彈;帶飛行時(shí)間的可爆炸拋射物;查里婭的粒子光束,光束長(zhǎng)得就像墻壁裂縫,而且在開火時(shí)需要保持接觸目標(biāo);另外還有噴涂。

創(chuàng)建碰撞特效的副作用很大,因?yàn)槟阈枰谄聊簧蟿?chuàng)建一個(gè)新的實(shí)體,這個(gè)實(shí)體可能間接地影響到生命周期、線程、場(chǎng)景管理和資源管理。

碰撞特效的生命周期,需要在屏幕渲染之前就開始,這意味著它們不需要在游戲模擬的中途顯現(xiàn),在不同的調(diào)用點(diǎn)都是如此。

下圖30是用來(lái)創(chuàng)建碰撞特效的一小部分代碼。基于Transform(譯注:變形,包括位移旋轉(zhuǎn)和縮放)、碰撞類型、材質(zhì)結(jié)構(gòu)數(shù)據(jù)來(lái)做碰撞計(jì)算,而且還調(diào)用了LOD、場(chǎng)景管理、優(yōu)先級(jí)管理等,最終生成了所需的特效。

 

 《守望先鋒》架構(gòu)設(shè)計(jì)與網(wǎng)絡(luò)同步  -- GDC2017 精品分享實(shí)錄

      這些代碼確保了像彈孔、焦痕持久特效不會(huì)很奇怪的疊在一起。例如,你用獵空的槍去射擊一面墻,留下了一堆麻點(diǎn),然后法老之鷹發(fā)出一枚火箭彈,在麻點(diǎn)上面造成了一個(gè)大面積焦痕。你肯定想刪了那些麻點(diǎn),要不然看起來(lái)會(huì)很丑,像是那種深度沖突(Z-Fighting)引起的閃爍。我可不想在到處去執(zhí)行那個(gè)刪除操作,最好能在一處搞定。

          我得修改代碼了,但是看上去好多啊,調(diào)用點(diǎn)一大堆,改完了以后每一處都需要測(cè)試。而且以后英雄越來(lái)越多,每個(gè)人都需要新的特效。然后我就到處復(fù)制粘貼這個(gè)函數(shù)的調(diào)用,沒(méi)什么大不了的,不就是個(gè)函數(shù)調(diào)用嘛,又不是什么噩夢(mèng)。(眾笑)

          其實(shí)這樣做以后,會(huì)在每個(gè)調(diào)用點(diǎn)都產(chǎn)生副作用的。程序員就得花費(fèi)更多腦力來(lái)記住這段代碼是如何運(yùn)作的,這就是代碼復(fù)雜度所在,肯定是應(yīng)該避免的。

          于是我們有了Contact單例。

《守望先鋒》架構(gòu)設(shè)計(jì)與網(wǎng)絡(luò)同步  -- GDC2017 精品分享實(shí)錄

          它包含了一個(gè)未決的碰撞記錄的數(shù)組,每個(gè)記錄都有足夠的信息,來(lái)在本幀的晚些時(shí)候創(chuàng)建那個(gè)特效。如果你想要生成一個(gè)特效的時(shí)候,只需要添加一條新記錄并填充數(shù)據(jù)就可以了。等運(yùn)行到幀的后期,進(jìn)行場(chǎng)景更新和準(zhǔn)備渲染的時(shí)候,ResolveContactSystem會(huì)遍歷數(shù)組,根據(jù)LOD規(guī)則生成特效并互相疊加。這樣的話,即使有嚴(yán)重的副作用,在每一幀也只是發(fā)生在一個(gè)調(diào)用點(diǎn)而已。

          除了降低復(fù)雜度以外,推遲方案還有很多其他優(yōu)點(diǎn)。數(shù)據(jù)和指令都緩存在本地,可以帶來(lái)性能提升;你可以針對(duì)特效做性能預(yù)算了,例如你有12個(gè)D.VA同時(shí)在射墻,她們會(huì)帶來(lái)數(shù)百個(gè)特效,你不用立即創(chuàng)建全部這些特效,你可以僅僅創(chuàng)建自己操縱的D.VA的特效就可以了,其他特效可以在后面的運(yùn)算過(guò)程中分?jǐn)傞_來(lái),平滑性能毛刺。這樣做有很多好處,真的,你現(xiàn)在可以實(shí)現(xiàn)一些復(fù)雜的邏輯了。即使ResolveContactSystem需要執(zhí)行多線程協(xié)作,來(lái)確定單個(gè)粒子效果的朝向, 現(xiàn)在也很容易做。推遲技術(shù)真的很酷。

Utility函數(shù),單例,推遲,這些都只是我們過(guò)去3年時(shí)間建立ECS架構(gòu)的一小部分模式。除了限制System中不能有狀態(tài),組件里不能有行為以外,這些技術(shù)也規(guī)定了我們?cè)?span>Overwatch中如何解決問(wèn)題。

 

《守望先鋒》架構(gòu)設(shè)計(jì)與網(wǎng)絡(luò)同步  -- GDC2017 精品分享實(shí)錄

遵守這些限制意味著你要用很多奇技淫巧來(lái)解決問(wèn)題。不過(guò),這些技術(shù)最終造就了一個(gè)可持續(xù)維護(hù)的、解耦合的、簡(jiǎn)潔的代碼系統(tǒng)。它限制了你,它把你帶到坑里,但這是個(gè)成功之坑。

 

學(xué)習(xí)了這些之后呢,咱們來(lái)聊聊真正的難題之一,以及ECS是如何簡(jiǎn)化它的。

《守望先鋒》架構(gòu)設(shè)計(jì)與網(wǎng)絡(luò)同步  -- GDC2017 精品分享實(shí)錄

作為gameplay(游戲玩法,機(jī)制)工程師,我們解決過(guò)的最重要的問(wèn)題就是網(wǎng)絡(luò)同步(netcode)。

這里先說(shuō)下目標(biāo),是要開發(fā)一款快速響應(yīng)(responsive)的網(wǎng)絡(luò)對(duì)戰(zhàn)動(dòng)作游戲。為了實(shí)現(xiàn)快速響應(yīng),就必須針對(duì)玩家的操作做預(yù)測(cè)(predict,也可以說(shuō)是預(yù)表現(xiàn))。如果每個(gè)操作都要等服務(wù)器回包的話,就不可能有高響應(yīng)性了。盡管因?yàn)橐恍┗斓巴婕易鞅姿圆荒苄湃慰蛻舳耍且呀?jīng)20年了,這條FPS游戲真理沒(méi)變過(guò)。

《守望先鋒》架構(gòu)設(shè)計(jì)與網(wǎng)絡(luò)同步  -- GDC2017 精品分享實(shí)錄

          游戲中有快速響應(yīng)需求的操作包括:移動(dòng),技能,就我們而言還有帶技能的武器,以及命中判定(hit registration)。

這里所有的操作都有統(tǒng)一的原則:玩家按下按鍵后必須立即能夠看到響應(yīng)。即使網(wǎng)絡(luò)延遲很高時(shí)也必須是如此。

像我這頁(yè)PPT中演示的那樣,ping值已經(jīng)250ms了,我所有的操作也都是立即得到反饋的,看上去很完美,一點(diǎn)延遲都沒(méi)有。

         

然而呢,帶預(yù)測(cè)的客戶端,服務(wù)器的驗(yàn)證和網(wǎng)絡(luò)延遲就會(huì)帶來(lái)副作用:預(yù)測(cè)錯(cuò)誤(misprediction,或者說(shuō)預(yù)測(cè)失?。┝恕nA(yù)測(cè)錯(cuò)誤的主要癥狀就一點(diǎn),會(huì)使得你沒(méi)能成功執(zhí)行你認(rèn)為你已經(jīng)做出的操作。

雖然服務(wù)器需要糾正你的操作,但代價(jià)并不會(huì)是操作延遲。我們會(huì)用確定性Determinism)來(lái)減少預(yù)測(cè)錯(cuò)誤發(fā)生的概率,下面是具體的做法。

《守望先鋒》架構(gòu)設(shè)計(jì)與網(wǎng)絡(luò)同步  -- GDC2017 精品分享實(shí)錄

          前提條件不變,PING值還是250毫秒。我認(rèn)為我跳起來(lái)了,但是服務(wù)器不這么看,我被猛拉回原地,而且被凍住了(冰凍是英雄Mei的技能之一)。這里(PPT中視頻演示)你甚至可以看到整個(gè)預(yù)測(cè)的工作過(guò)程。預(yù)測(cè)過(guò)程開始時(shí),試圖把我們移到空中,甚至大猩猩跳躍技能的CD都已經(jīng)進(jìn)入冷卻了,這是對(duì)的,我們不希望預(yù)測(cè)準(zhǔn)確率僅僅是十之八九。所以我們希望盡可能的快速響應(yīng),

如果你碰巧在斯里蘭卡玩這個(gè)游戲,而且又被Mei凍住了,那么就有可能會(huì)預(yù)測(cè)錯(cuò)誤。

《守望先鋒》架構(gòu)設(shè)計(jì)與網(wǎng)絡(luò)同步  -- GDC2017 精品分享實(shí)錄

下面我會(huì)首先給出一些準(zhǔn)則,然后討論一下這個(gè)嶄新的技術(shù)是如何利用ECS來(lái)減少?gòu)?fù)雜度的。

          這里不會(huì)涉及到通用的數(shù)據(jù)復(fù)制技術(shù)、遠(yuǎn)端實(shí)體插值(remote entity interpolation)或者是向后緩和(backwardsreconciliation)技術(shù)細(xì)節(jié)。

我們完全是站在巨人的肩膀上,使用了一些其他文獻(xiàn)中提過(guò)的技術(shù)而已。后面的幻燈片會(huì)假定大家對(duì)那些技術(shù)都已經(jīng)很熟悉了。

 

確定性(Determinism)

《守望先鋒》架構(gòu)設(shè)計(jì)與網(wǎng)絡(luò)同步  -- GDC2017 精品分享實(shí)錄

          確定性模擬技術(shù)依賴于時(shí)鐘的同步,固定的更新周期和量化。服務(wù)器和客戶端都運(yùn)行在這個(gè)保持同步的時(shí)鐘和量化值之上。時(shí)間被量化成command frame,我們稱之為命令幀。每個(gè)命令幀都是固定的16毫秒,不過(guò)在電競(jìng)比賽時(shí)是7毫秒。

《守望先鋒》架構(gòu)設(shè)計(jì)與網(wǎng)絡(luò)同步  -- GDC2017 精品分享實(shí)錄

          模擬過(guò)程的頻率是固定的,所以需要把計(jì)算機(jī)時(shí)鐘循環(huán)轉(zhuǎn)換為固定的命令幀序號(hào)。我們使用了一個(gè)循環(huán)累加器來(lái)處理幀號(hào)的增長(zhǎng)。

在我們的ECS框架內(nèi),任何需要進(jìn)行預(yù)表現(xiàn)、或者基于玩家的輸入模擬結(jié)果的System,都不會(huì)使用Update,而是用UpdateFixed。UpdateFixed會(huì)在每個(gè)固定的命令幀調(diào)用。

《守望先鋒》架構(gòu)設(shè)計(jì)與網(wǎng)絡(luò)同步  -- GDC2017 精品分享實(shí)錄

          假定輸出流是穩(wěn)定的,那么客戶端的始終總是會(huì)超前于服務(wù)器的,超前了大概半個(gè)RTT加上一個(gè)緩存幀的時(shí)長(zhǎng)。這里的RTTPING值加上邏輯處理的時(shí)間。上圖39的例子中,我們的RTT160毫秒,一半就是80毫秒,再加上1幀,我們每幀是16毫秒,全加起來(lái)就是客戶端相對(duì)于服務(wù)器的提前量。

          圖中的垂直線代表每一個(gè)處理中的幀??蛻舳碎_始模擬并把第19幀的輸入上報(bào)給服務(wù)器,過(guò)一段時(shí)間(基本上是半個(gè)RTT加上緩沖時(shí)間)以后,服務(wù)器才開始模擬這一幀。這就是我為什么要說(shuō)客戶端永遠(yuǎn)是領(lǐng)先于服務(wù)器的。

          正因?yàn)榭蛻舳耸且还赡X的盡快接受玩家輸入,盡可能地貼近現(xiàn)在時(shí)刻,如果還需要等待服務(wù)器回包才能響應(yīng)的話,那看起來(lái)就太慢了,會(huì)讓游戲變得卡頓。圖39中的緩沖區(qū),你肯定希望盡可能的?。ㄗg注:緩沖越小,模擬時(shí)就越接近當(dāng)前時(shí)刻),順便說(shuō)一句,游戲運(yùn)行的頻率是60赫茲,我這里播放動(dòng)畫的速度是正常速度的百分之一(譯注:這也是為了讓觀眾看得更清晰、明白)。

《守望先鋒》架構(gòu)設(shè)計(jì)與網(wǎng)絡(luò)同步  -- GDC2017 精品分享實(shí)錄

客戶端的預(yù)測(cè)System讀取當(dāng)前輸入,然后模擬獵空的移動(dòng)過(guò)程。我這里是用游戲搖桿來(lái)表示獵空的輸入操作并上報(bào)的。這里的(第14幀)獵空是我當(dāng)前時(shí)刻模擬出來(lái)的運(yùn)動(dòng)狀態(tài),經(jīng)過(guò)完整的RTT加上緩沖事件,最終獵空會(huì)從服務(wù)器上回到客戶端(譯注:這里最好結(jié)合演講視頻,靜態(tài)的文章無(wú)法表達(dá)到位)。這里回來(lái)的是經(jīng)過(guò)服務(wù)器驗(yàn)證的運(yùn)動(dòng)狀態(tài)快照。服務(wù)器模擬權(quán)威帶來(lái)的副作用就是驗(yàn)證需要額外的半個(gè)RTT時(shí)間才能回到客戶端。

那么這里客戶端為什么要用一個(gè)環(huán)形緩沖(ring buffer)來(lái)記錄歷史運(yùn)動(dòng)軌跡呢?這是為了方便與服務(wù)器返回的結(jié)果進(jìn)行對(duì)比。經(jīng)過(guò)比較,如果與服務(wù)器模擬結(jié)果相同,那么客戶端會(huì)開開心心地繼續(xù)處理下一個(gè)輸入。如果結(jié)果不一致,那就是一個(gè)“預(yù)測(cè)錯(cuò)誤”,這時(shí)就需要“和解”(reconcile)了。

如果想簡(jiǎn)單,那就直接用服務(wù)器下發(fā)的結(jié)果覆蓋客戶端就行了,但是這個(gè)結(jié)果已經(jīng)是“舊”(相對(duì)于當(dāng)前時(shí)刻的輸入來(lái)講)的了,因?yàn)榉?wù)器的回包一般都是幾百毫秒之前的了。

除了上面那個(gè)環(huán)形緩沖以外,我們還有另一個(gè)環(huán)形緩沖用來(lái)存儲(chǔ)玩家的輸入操作。因?yàn)樘幚硪苿?dòng)的代碼是確定性的,一旦玩家開始進(jìn)入他想要進(jìn)入到移動(dòng)狀態(tài),想要重現(xiàn)這個(gè)過(guò)程也是很容易的。所以這里我們的處理方式就是,一旦從服務(wù)器回包發(fā)現(xiàn)預(yù)測(cè)失敗,我們把你的全部輸入都重播一遍直至追上當(dāng)前時(shí)刻。如下圖41中的第17幀所示,客戶端認(rèn)為獵空正在跑路,而服務(wù)器指出,你已經(jīng)被暈住了,有可能是受到了麥克雷的閃光彈的攻擊。

《守望先鋒》架構(gòu)設(shè)計(jì)與網(wǎng)絡(luò)同步  -- GDC2017 精品分享實(shí)錄

接下來(lái)的流程是,當(dāng)客戶端收到描述角色狀態(tài)的數(shù)據(jù)包時(shí),我們基本上就得把移動(dòng)狀態(tài)及時(shí)恢復(fù)到最近一次經(jīng)過(guò)服務(wù)器驗(yàn)證過(guò)狀態(tài)上去,而且必須重新計(jì)算之后所有的輸入操作,直至追上當(dāng)前時(shí)刻(第25幀)。

《守望先鋒》架構(gòu)設(shè)計(jì)與網(wǎng)絡(luò)同步  -- GDC2017 精品分享實(shí)錄

          現(xiàn)在客戶端進(jìn)行到第27幀(上圖)了,這時(shí)我們收到了服務(wù)器上第17幀的回包。一旦重新同步(譯注:注意下圖41中客戶端獵空的狀態(tài)全都更正為“暈”了)以后,就相當(dāng)于回退到了“幀同步”(lockstep)算法了。

我們肯定知道我們到底被暈了多久。

《守望先鋒》架構(gòu)設(shè)計(jì)與網(wǎng)絡(luò)同步  -- GDC2017 精品分享實(shí)錄

      到了下圖第33幀以后,客戶端就知道已經(jīng)不再是暈住的狀態(tài)了,而服務(wù)器上也正在模擬相同的情況。不再有奇怪的同步追趕問(wèn)題了。一旦進(jìn)入這個(gè)移動(dòng)狀態(tài),就可以重發(fā)玩家當(dāng)前時(shí)刻的操作輸入了。

《守望先鋒》架構(gòu)設(shè)計(jì)與網(wǎng)絡(luò)同步  -- GDC2017 精品分享實(shí)錄

          然而,客戶端網(wǎng)絡(luò)并不保證如此穩(wěn)定,時(shí)有丟包發(fā)生。我們游戲里的輸入都是通過(guò)定制化的可靠UDP實(shí)現(xiàn)。所以客戶端的輸入包常常無(wú)法到達(dá)服務(wù)器,也就是丟包。服務(wù)器又試圖保持了一個(gè)小小的、保存未模擬輸入的緩沖區(qū),但是讓它盡量的小,以保證游戲操作的流暢。

《守望先鋒》架構(gòu)設(shè)計(jì)與網(wǎng)絡(luò)同步  -- GDC2017 精品分享實(shí)錄

          一旦這個(gè)緩沖區(qū)是空的,服務(wù)器只能根據(jù)你最后一次輸入去“猜測(cè)”。等到真正的輸入到達(dá)時(shí),它會(huì)試著“緩和”,確保不會(huì)弄丟你的任何操作,但是也會(huì)有預(yù)測(cè)錯(cuò)誤。

          下面是見(jiàn)證奇跡的時(shí)刻。

《守望先鋒》架構(gòu)設(shè)計(jì)與網(wǎng)絡(luò)同步  -- GDC2017 精品分享實(shí)錄

          上圖可以看到,已經(jīng)丟了一些來(lái)自客戶端的包,服務(wù)器意識(shí)到以后,就會(huì)復(fù)制先前的輸入操作來(lái)就行預(yù)測(cè),一邊祈禱希望預(yù)測(cè)正確,一邊發(fā)包告訴客戶端:嘿哥們,丟包了,不太對(duì)勁哦。接下來(lái)發(fā)生的就更奇怪的了,客戶端會(huì)進(jìn)行時(shí)間膨脹,比約定的幀率更快地進(jìn)行模擬。

 

《守望先鋒》架構(gòu)設(shè)計(jì)與網(wǎng)絡(luò)同步  -- GDC2017 精品分享實(shí)錄

這個(gè)例子里,約定好的幀速是16毫秒,客戶端就會(huì)假裝現(xiàn)在幀速是15.2毫秒,它想要更加提前。結(jié)果就是,這些輸入來(lái)的越來(lái)越快。服務(wù)器上緩沖區(qū)也會(huì)跟著變大,這就是為了在盡量不浪費(fèi)的情況下,度過(guò)(丟包的)難關(guān)。

 

這種技術(shù)運(yùn)轉(zhuǎn)良好,尤其是在經(jīng)常抖動(dòng)的互聯(lián)網(wǎng)環(huán)境下,丟包和PING都不穩(wěn)定。即使你是在國(guó)際空間站里玩這個(gè)游戲,也是可以的。所以我想這個(gè)方案真的很NB。

 

現(xiàn)在,各位都記個(gè)筆記吧,這里收到消息,現(xiàn)在開始放大時(shí)間刻度,注意我們是真的加速輪詢了,你可以看見(jiàn)圖中右邊的坡越來(lái)越平坦了。它比以前更加快速地上報(bào)輸入。同時(shí)服務(wù)器上的緩沖也越來(lái)越大了,可以容忍更多地丟包,如果真的發(fā)生丟包也有可能在緩沖期間補(bǔ)上。

《守望先鋒》架構(gòu)設(shè)計(jì)與網(wǎng)絡(luò)同步  -- GDC2017 精品分享實(shí)錄

          一旦服務(wù)器發(fā)現(xiàn),你現(xiàn)在的網(wǎng)絡(luò)恢復(fù)健康了,它就會(huì)發(fā)消息給你說(shuō):嘿哥們,現(xiàn)在沒(méi)事了。而客戶端會(huì)做相反的事情:它會(huì)縮小時(shí)間刻度,以更慢的速度發(fā)包。同時(shí)服務(wù)器會(huì)減小緩沖區(qū)的尺寸。

         

《守望先鋒》架構(gòu)設(shè)計(jì)與網(wǎng)絡(luò)同步  -- GDC2017 精品分享實(shí)錄

如果這個(gè)過(guò)程持續(xù)發(fā)生,那目標(biāo)就會(huì)是是不要超過(guò)承受極限,并通過(guò)輸入冗余來(lái)使得預(yù)測(cè)錯(cuò)誤最小化。

早些時(shí)候我有提到過(guò),服務(wù)器一旦饑餓,就會(huì)復(fù)制最后一次輸入操作,對(duì)吧?一旦客戶端趕上來(lái)了,就不會(huì)再?gòu)?fù)制輸入了,這樣會(huì)有因?yàn)閬G包而被忽略的風(fēng)險(xiǎn)。解決方法是,客戶端維持一個(gè)輸入操作的滑動(dòng)窗口。這項(xiàng)技術(shù)從《雷神世界》開始就有了。

我們不是僅僅發(fā)送當(dāng)前第19幀的輸入,而是把從最后一次被服務(wù)器確認(rèn)的運(yùn)動(dòng)狀態(tài)到現(xiàn)在的全部輸入都發(fā)送過(guò)去。上面的例子可以看出,最后一次從服務(wù)器來(lái)的確認(rèn)是第4幀。而我們剛剛模擬到了第19幀。我們會(huì)把每一幀的每一個(gè)輸入都打包成為一個(gè)數(shù)據(jù)包。玩家一般頂多每1/60秒才會(huì)有一次操作,所以壓縮后數(shù)據(jù)量其實(shí)不大。一般你按住“向前”按鈕之前,很可能是已經(jīng)在“前進(jìn)”了。

結(jié)果就是,即使發(fā)生丟包,下一個(gè)數(shù)據(jù)包到達(dá)時(shí)依然會(huì)有全部的輸入操作,這會(huì)在你真正模擬以前,就填充上所有因?yàn)閬G包而出現(xiàn)的空洞。所以這個(gè)反饋循環(huán)的過(guò)程和可增長(zhǎng)的緩沖區(qū)大小,以及滑動(dòng)窗口,使得你不會(huì)因?yàn)閬G包而損失什么。所以即使丟包也不會(huì)出現(xiàn)預(yù)測(cè)錯(cuò)誤。

接下來(lái)會(huì)再次給你展示動(dòng)畫過(guò)程,這一次是雙倍速,是正常速度的1/50了。

 

          這里有全部不穩(wěn)定因素:網(wǎng)絡(luò)PING值抖動(dòng),有丟包,客戶端時(shí)間刻度放大,輸入窗口填充了全部漏洞,有預(yù)測(cè)失敗,有服務(wù)器糾正。我們它們都合在一起播放給你看。

 

          接下來(lái)的議題,我不想講太多細(xì)節(jié),因?yàn)檫@是Dan Reid的分享的主題(譯注,已經(jīng)翻譯),因?yàn)檫@是開幕式的一部分,所以強(qiáng)烈推薦各位聽一下,真的很棒。還是在這個(gè)房間,我講完了就開始。

《守望先鋒》架構(gòu)設(shè)計(jì)與網(wǎng)絡(luò)同步  -- GDC2017 精品分享實(shí)錄

          所有的技能都是用暴雪自有指令式腳本語(yǔ)言Statescript開發(fā)的。腳本系統(tǒng)的一大優(yōu)點(diǎn)就是它可以在前后穿越時(shí)空。在客戶端預(yù)測(cè),然后服務(wù)器驗(yàn)證,就像之前的例子里面的移動(dòng)操作,我們可以把你回滾然后重播所有輸入。技能也使用了與移動(dòng)相同的前后滾原則,先回退到最后一次經(jīng)過(guò)驗(yàn)證的快照的狀態(tài),然后重播輸入直到當(dāng)前時(shí)刻。

大家肯定還記得這個(gè)例子,就是獵空被暈導(dǎo)致的服務(wù)器糾正過(guò)程,技能的處理過(guò)程是相同的??蛻舳撕头?wù)器都會(huì)模擬技能執(zhí)行的確定性過(guò)程,客戶端領(lǐng)先于服務(wù)器,所以一般是客戶端先模擬,服務(wù)器稍后跟進(jìn)。客戶端處理預(yù)測(cè)錯(cuò)誤的方式是,先根據(jù)服務(wù)器快照回滾,然后再前滾(roll forth),就像這樣幻燈演示的動(dòng)畫過(guò)程那樣。這里演示的是死神的幽靈形態(tài)。圖45中的這些方塊(譯注:Statescript中的State)代表了幽靈形態(tài),有了這些方塊我就可以很自信的播放很酷的特效和動(dòng)畫了。

 

幽靈形態(tài)結(jié)束后就會(huì)關(guān)閉這些方塊。在同一幀中這些小動(dòng)畫會(huì)展示出State的關(guān)閉過(guò)程。緊接著就是幽靈形態(tài)的出現(xiàn),不久以后我們就會(huì)得到來(lái)自服務(wù)器的消息:嗨,我預(yù)測(cè)的幽靈形態(tài)的過(guò)程已經(jīng)告訴你了,所以你趕緊倒退回去,把這些State都打開,然后咱們?cè)僦匦履M全部輸入,把這些State都關(guān)了。這基本上就是每次服務(wù)器下發(fā)更新時(shí)回滾和前滾的過(guò)程了。

 

能預(yù)測(cè)移動(dòng)很酷,這意味著可以預(yù)測(cè)每個(gè)技能,我們也確實(shí)這樣做了,同樣,對(duì)于武器或者其他的模塊,我們也可以這么做。

 

 

現(xiàn)在討論一下命中判定的預(yù)測(cè)和確認(rèn)。

《守望先鋒》架構(gòu)設(shè)計(jì)與網(wǎng)絡(luò)同步  -- GDC2017 精品分享實(shí)錄

          ECS處理這個(gè)其實(shí)很方便,還記得嗎,實(shí)體如果擁有行為所需的組件元組,它就會(huì)是這個(gè)行為的主體。如果你的實(shí)體是敵對(duì)的(還記得我們之前講的敵對(duì)性檢查吧)而且你有一個(gè)ModifyHealthQueue組件,你就可以被別的玩家擊中,這都受制于命中判定。

 

          這兩個(gè)組件,一個(gè)是用來(lái)檢查敵對(duì)性的,一個(gè)是ModifyHealthQueue。ModifyHealthQueue是服務(wù)器記錄的你身上的全部傷害和治療。與單例Contact類似,也是延遲計(jì)算的,而且有多個(gè)調(diào)用點(diǎn),這就是最大的副作用。延遲計(jì)算是因?yàn)椴幌朐趻伾湮锬M途中,立即生成一大堆特效,我們選擇延后。

 

順便說(shuō)一句,傷害,也完全不會(huì)在客戶端預(yù)測(cè),因?yàn)樗鼈內(nèi)际球_子。

然而命中判定卻是在客戶端處理的。所以,如果你有一個(gè)MovementState組件,而且是一個(gè)不會(huì)被本地玩家操縱的remote對(duì)象,那你會(huì)被運(yùn)動(dòng) System經(jīng)過(guò)插值(interpolate)運(yùn)算來(lái)重新定位。標(biāo)準(zhǔn)插值是發(fā)生在最后一次收到的兩個(gè)MovementState之間的,這項(xiàng)技術(shù)自從《Quake》時(shí)代就有了。

 

System根本不在乎你是一個(gè)移動(dòng)平臺(tái)、炮臺(tái)、門還是法老之鷹,你只需要擁有一個(gè)MovementState組件就夠了,MovementState組件還要負(fù)責(zé)存儲(chǔ)環(huán)形緩沖區(qū),還記得環(huán)形緩沖嘛?之前用來(lái)保存那些獵空小人的位置的。

 

有了MovementState組件,服務(wù)器在計(jì)算命中以前,就會(huì)把你回滾到攻擊者上報(bào)時(shí)你所在的那一幀,這就是向后緩和(backwards reconcilation)。這一切都與ModifyHealthQueue組件正交, ModifyHealthQueue組件決定了是否接受傷害。我們還需要倒回門、平臺(tái)、車的狀態(tài),如果子彈被擋住了的話,就無(wú)所謂了。一般來(lái)說(shuō)如果你是敵對(duì)的,而且有MovementState組件,你就會(huì)被倒回,而且可能會(huì)受傷。

 

          被倒回(rewind)是被一組Utility函數(shù)操縱的行為;而受傷是MovementState組件被延遲處理時(shí)發(fā)生的另外一個(gè)行為。這兩種行為獨(dú)立開來(lái),各自發(fā)生在各自的組件切片上。

 

          射擊過(guò)程有點(diǎn)抽象,我這里會(huì)分解一下。

《守望先鋒》架構(gòu)設(shè)計(jì)與網(wǎng)絡(luò)同步  -- GDC2017 精品分享實(shí)錄

          47中的框是每一個(gè)實(shí)體的邏輯邊界(bounding volumes)。邏輯邊界基本上就是代表了這個(gè)源氏的實(shí)時(shí)快照的并集。所以源氏周圍的邏輯邊界就代表了過(guò)去半秒鐘這個(gè)角色的全部運(yùn)動(dòng)(的最大范圍)。如果我現(xiàn)在沿著準(zhǔn)星方向射擊,在倒回這個(gè)角色以前,會(huì)首先與這個(gè)邊界相交,因?yàn)榛谖业?span>PING值,它有可能在邊界內(nèi)的任意一處位置。

          這個(gè)例子里,如果我沿著這個(gè)方向射擊,那只需要單獨(dú)倒回安娜即可,因?yàn)樽訌椫缓退倪吔缦嘟涣?。不需要同時(shí)倒回大錘和他的能量盾或者車,以及后面的門。

 

          射擊如同移動(dòng)一樣,也可能會(huì)有預(yù)測(cè)失敗。

《守望先鋒》架構(gòu)設(shè)計(jì)與網(wǎng)絡(luò)同步  -- GDC2017 精品分享實(shí)錄

          這里的綠色人偶是死神的客戶端視角,黃色是服務(wù)器視角。這些綠色的小點(diǎn)點(diǎn)是客戶端認(rèn)為它的子彈擊中的位置。可以看見(jiàn)綠色的細(xì)線是子彈經(jīng)過(guò)的路徑,但服務(wù)器在校驗(yàn)的時(shí)候,這個(gè)藍(lán)紫色的半球才代表實(shí)際命中的位置。

          這完全是個(gè)人為制造的例子,確定型模擬過(guò)程是很可靠的,為了重現(xiàn)射擊過(guò)程中的預(yù)測(cè)失敗,我把我的丟包率設(shè)置為60%,然后足足射了這個(gè)混蛋20分鐘才成功重現(xiàn)(眾笑)

          這里我還得提一句,模擬過(guò)程如此精確,要?dú)w功于我們的QA團(tuán)隊(duì)的同事。他們從不接受“NO”作為答案,而且因?yàn)槭忻嫔掀渌螒蚨疾粫?huì)把命中判定的預(yù)測(cè)精確度做到這個(gè)水平,所以我們的QA小伙伴們根本不相信我,也不在乎我。只是不停地提bug單,而且是越來(lái)越多的bug單,而每一次當(dāng)我們?nèi)z查是否真的有bug時(shí),結(jié)果是每次都真的有。這里要對(duì)他們表示深深的感謝,有了他們的工作才使得我們能做出如此偉大的產(chǎn)品。

 


 

如果你的PING值特別高,命中判定就會(huì)失效。

《守望先鋒》架構(gòu)設(shè)計(jì)與網(wǎng)絡(luò)同步  -- GDC2017 精品分享實(shí)錄

          一旦PING值超過(guò)220毫秒,我們就會(huì)延后一些命中效果,也不會(huì)再去預(yù)測(cè)了,直接等服務(wù)器回包確認(rèn)。之所以這么做的原因是,客戶端上本來(lái)就做了外插值(extrapolate),不想把目標(biāo)倒回那么遠(yuǎn)。不想讓受害者覺(jué)得他們拼命跑到墻后面找掩護(hù),結(jié)果還是被回拉、受傷。所以加了一層保護(hù)。這倒回外插后一段時(shí)間內(nèi)的行為。下面的視頻會(huì)演示這個(gè)過(guò)程(譯注:強(qiáng)烈建議看視頻)。

 

          PING0的時(shí)候,對(duì)彈道碰撞做了預(yù)測(cè),而擊中點(diǎn)和血條沒(méi)有預(yù)測(cè),要等服務(wù)器回包才渲染。

          當(dāng)PING達(dá)到300毫秒的時(shí)候,碰撞都不預(yù)測(cè)了,因?yàn)樯鋼裟繕?biāo)正在做快讀的外插,他實(shí)際上根本沒(méi)在這里,這里我們用了DRDead Reckoning)導(dǎo)航推測(cè)算法,雖然很接近,但是他真沒(méi)在那里。死神左右來(lái)回晃動(dòng)時(shí)就會(huì)出現(xiàn)這種情況,外插時(shí)完全無(wú)法正確預(yù)測(cè)。這里我們不會(huì)照顧你的感受,你的網(wǎng)絡(luò)太差了。

          最后這個(gè)視頻,PING達(dá)到1秒的時(shí)候,尤為明顯。死神的移動(dòng)方式不變,還會(huì)有外插。順便提一句,甚至PING已經(jīng)是1秒鐘那么慢了,客戶端的所有操作都還是能夠立即預(yù)測(cè)、響應(yīng)的,只不過(guò)大部分都是錯(cuò)的而已。其實(shí)我應(yīng)該放大招的(午時(shí)已到),肯定能弄死他。

         

          下面講下其他預(yù)測(cè)失敗的例子,PING值還是不怎么好,150毫秒。這種條件下,無(wú)論何時(shí)遇到運(yùn)動(dòng)預(yù)測(cè)失敗,都會(huì)錯(cuò)誤的預(yù)測(cè)命中。下面用慢動(dòng)作展現(xiàn)一下。

《守望先鋒》架構(gòu)設(shè)計(jì)與網(wǎng)絡(luò)同步  -- GDC2017 精品分享實(shí)錄

 

      看,都已經(jīng)飆血了,但是卻沒(méi)看見(jiàn)血條,也沒(méi)看見(jiàn)彈坑,所以對(duì)于彈道碰撞的預(yù)測(cè)來(lái)講就是錯(cuò)誤的。服務(wù)器拒絕了,這不是一次合法的命中。碰撞效果預(yù)測(cè)失敗的原因就是冰墻立起來(lái)了。你以為自己開火時(shí)還站在地上,但是服務(wù)器模擬時(shí),你已經(jīng)被冰墻升到了空中,就是這個(gè)行為導(dǎo)致預(yù)測(cè)失敗的。

 

          當(dāng)我們修復(fù)這些微小的命中預(yù)測(cè)錯(cuò)誤時(shí),發(fā)現(xiàn)大部分情況都能通過(guò)與服務(wù)器就位置問(wèn)題達(dá)成一致來(lái)消除,所以我們花了很多時(shí)間來(lái)對(duì)齊位置。

         

          下面是與運(yùn)動(dòng)相關(guān)的預(yù)測(cè)失敗的例子,同時(shí)也與游戲玩法有關(guān)。

《守望先鋒》架構(gòu)設(shè)計(jì)與網(wǎng)絡(luò)同步  -- GDC2017 精品分享實(shí)錄

          PING值還是150毫秒,你想射中這個(gè)死神,但是他處于幽靈形態(tài),箭頭碰到他時(shí),客戶端會(huì)預(yù)測(cè)說(shuō)應(yīng)該有血飚出來(lái),沒(méi)有彈坑(hit pit),也沒(méi)有血條。我們根本沒(méi)擊中他,因?yàn)樗呀?jīng)先進(jìn)入幽靈狀態(tài)了。

 

這種例子里,雖然大部分時(shí)間都會(huì)優(yōu)先滿足進(jìn)攻者,但除非受害者做了什么事情緩和(mitigate)了這次進(jìn)攻。在這個(gè)例子里,死神的幽靈形態(tài)會(huì)給他3秒鐘的無(wú)敵時(shí)間。無(wú)論如何,我們沒(méi)有真的打到死神。

讓我從哲學(xué)角度想象一下,你就是那個(gè)死神,你進(jìn)入了幽靈狀態(tài),但事實(shí)上服務(wù)器很可能會(huì)讓你播放所有特效,讓后讓你死掉,因?yàn)槟悴豢赡苋绱丝焖龠M(jìn)入那個(gè)狀態(tài)。


 

ECS簡(jiǎn)化了網(wǎng)絡(luò)同步問(wèn)題。網(wǎng)絡(luò)同步代碼中用到的System,知道自己何時(shí)被用于玩家身上,很簡(jiǎn)單直接,基本上如果一個(gè)實(shí)體被一個(gè)帶有Connection組件的東西控制了,它就是一個(gè)玩家。

          System也知道哪些目標(biāo)需要被倒回到進(jìn)攻者時(shí)刻的那一幀上,任何包含MovementState組件的實(shí)體都會(huì)被倒回。

 

實(shí)體與組件之間的內(nèi)在關(guān)聯(lián)主要行為是MovementState可以在時(shí)間線上被取消。

 

          上圖52System和組件的全景圖,其中只有少數(shù)幾個(gè)與網(wǎng)絡(luò)同步行為有關(guān)。而這就是我們已知最復(fù)雜的問(wèn)題了。System中有兩個(gè)是NetworkEventNetworkMessage,是網(wǎng)絡(luò)同步模塊的核心組成部分,參與了接收輸入和發(fā)送輸出這樣的典型網(wǎng)絡(luò)行為。

 

          還有另外幾個(gè)System,一只手就數(shù)得過(guò)來(lái):InterpolateMovement,Weapons,Statescript,MovementState,我特別想刪了MovementState,因?yàn)槲也幌矚g它。所以呢,實(shí)際上網(wǎng)絡(luò)同步模塊中,只有3個(gè)System是與gameplay有關(guān)的,其中用到的組件就是右邊高亮列出的,也只有組件對(duì)于網(wǎng)絡(luò)同步模塊是只讀的。真正修改了數(shù)據(jù)的就是像ModifyHealthQueue,因?yàn)閷?duì)敵人造成的傷害是真實(shí)的。

《守望先鋒》架構(gòu)設(shè)計(jì)與網(wǎng)絡(luò)同步  -- GDC2017 精品分享實(shí)錄

現(xiàn)在回頭看一下,用了ECS這么多年后,都學(xué)到了哪些知識(shí)與心得。

我有點(diǎn)希望SystemUtility都能回到最早那個(gè)ECS操作元祖的權(quán)威例程的用法,做法有點(diǎn)特殊,我們只遍歷一個(gè)組件就夠了,再通過(guò)它訪問(wèn)所有兄弟組件。對(duì)于真正復(fù)雜的組件訪問(wèn)元組模型,你必須知道確切的訪問(wèn)對(duì)象才行。如果有個(gè)行為需要一個(gè)含有40個(gè)組件的元組,那可能是因?yàn)槟愕南到y(tǒng)設(shè)計(jì)過(guò)于復(fù)雜了,元組之間有沖突。

《守望先鋒》架構(gòu)設(shè)計(jì)與網(wǎng)絡(luò)同步  -- GDC2017 精品分享實(shí)錄

 

元組另一個(gè)很酷的副作用是,你掌握了關(guān)于什么System能訪問(wèn)什么狀態(tài)的先驗(yàn)知識(shí),那么回到我們用到元組的那個(gè)原型引擎當(dāng)中,就可以知道23個(gè)System可以操作不同的組件集合。因?yàn)楦鶕?jù)元組的定義就可以知道他們的用途。這里設(shè)計(jì)的非常容易擴(kuò)展。就像之前那個(gè)彈鋼琴的動(dòng)畫一樣,不過(guò)可以看到多個(gè)System同時(shí)點(diǎn)亮,只因?yàn)樗鼈儾倏v的組件集合是不同的。

《守望先鋒》架構(gòu)設(shè)計(jì)與網(wǎng)絡(luò)同步  -- GDC2017 精品分享實(shí)錄

由于已經(jīng)知道組件讀寫的優(yōu)先級(jí),System的輪詢可以做到多線程處理gameplay代碼。這里要提一句,Transform組件依然很受歡迎,但只有為數(shù)不多的幾個(gè)System會(huì)真正修改它,大部分System都是對(duì)它只讀。所以當(dāng)你定義元組時(shí),可以把組件標(biāo)記上只讀屬性,這就意味著,即使有多個(gè)System都操作對(duì)該組件,但都是只讀,可以并行處理。

實(shí)體生命周期管理需要一些技巧,尤其是在一幀的中間創(chuàng)建出來(lái)的那些。在早期,我們推遲了創(chuàng)建和銷毀行為,當(dāng)你說(shuō)嘿我想要?jiǎng)?chuàng)建一個(gè)實(shí)體時(shí),實(shí)際上是在那一幀結(jié)束時(shí)才完成的。事實(shí)證明,推遲銷毀一點(diǎn)問(wèn)題都沒(méi)有,而推遲創(chuàng)建卻有一大堆副作用。尤其是當(dāng)你在System A 中申請(qǐng)創(chuàng)建一個(gè)新的實(shí)體,然后在System B中使用,這時(shí)如果你推遲了創(chuàng)建過(guò)程,你就要隔一幀才能使用。

《守望先鋒》架構(gòu)設(shè)計(jì)與網(wǎng)絡(luò)同步  -- GDC2017 精品分享實(shí)錄

這有點(diǎn)不爽。這也增加了很多內(nèi)部復(fù)雜性(譯注:看到這里,復(fù)雜性都是一些潛規(guī)則,需要花腦力去記住的hardcode),我們想修改掉這部分代碼,使它可以在一幀的中途創(chuàng)建好,這樣就可以馬上使用了。

 

我們?cè)谟螒虬l(fā)布之后才做了這些改動(dòng),實(shí)在很恐怖。這個(gè)補(bǔ)丁打在了1.2或者1.3版本,上線那天晚上我都是通宵的。

《守望先鋒》架構(gòu)設(shè)計(jì)與網(wǎng)絡(luò)同步  -- GDC2017 精品分享實(shí)錄

我們大概花了1年半的時(shí)間來(lái)制定ECS的使用準(zhǔn)則,就像之前那個(gè)權(quán)威的例子,但是我們需要改造一些現(xiàn)有的代碼使之能夠適應(yīng)新的架構(gòu)。這些準(zhǔn)則包括:組件沒(méi)有函數(shù);System沒(méi)有狀態(tài);共享代碼要放到Utils里;組件里復(fù)雜的副作用要通過(guò)隊(duì)列的方式推遲處理,尤其是單例組件;System不能調(diào)用其他System的函數(shù),即使是我們自己的取名System也不行,這個(gè)System幾年之前暴雪分享過(guò)的。

 

仍然有大量代碼不符合這個(gè)規(guī)范,所以它們是復(fù)雜度和維護(hù)工作的主要來(lái)源,就一點(diǎn)也不奇怪了。通過(guò)檢視代碼變更數(shù)量或者說(shuō)bug數(shù)量,你就能發(fā)現(xiàn)這一點(diǎn)。

所以,如果你有什么遺留代碼而且無(wú)法融入ECS規(guī)范的話,就絕對(duì)不應(yīng)該使用。保持子系統(tǒng)整潔,不用創(chuàng)建任何代理組件去對(duì)它們進(jìn)行封裝。

不同的系統(tǒng)設(shè)計(jì)是用來(lái)解決問(wèn)題的不同方法。

ECS是一個(gè)集成大量System的工具,不合適的系統(tǒng)設(shè)計(jì)原則就不應(yīng)該被采用。

 

《守望先鋒》架構(gòu)設(shè)計(jì)與網(wǎng)絡(luò)同步  -- GDC2017 精品分享實(shí)錄

          ECS的設(shè)計(jì)目的是用來(lái)把大量的模塊進(jìn)行集成并解耦,很多 System及其依賴的組件都是冰山形狀的。

冰山型組件對(duì)其他ECSSystem暴露的表面很小,但它們內(nèi)部其實(shí)有大量的狀態(tài)、代理或者數(shù)據(jù)結(jié)構(gòu)是ECS層無(wú)法訪問(wèn)的。

 

在線程模型中這些冰山的體型相當(dāng)明顯,大部分ECS的工作,例如更新System,都是發(fā)生在主線程(58頂部)上的。我們也用到了大量的多線程技術(shù),像forkjoin。這個(gè)例子里,有角色發(fā)射了大量的拋射物,然后腳本System說(shuō)我們需要生成一些拋射物,就創(chuàng)建了幾個(gè)工作線程來(lái)干活。還有這里是ResolvedContactSystem想要?jiǎng)?chuàng)建一些碰撞特效,這里花費(fèi)了幾個(gè)工作線程去做這項(xiàng)工作。

 

拋射物模擬的幕后工作已經(jīng)被隔離,而且對(duì)上層ECS是不可見(jiàn)的,這樣很好。

另外一個(gè)很酷的例子就是AIPetDataSystem,很好的應(yīng)用了forkjoin模式,在ECS層面,只有一點(diǎn)點(diǎn)耦合,可能是說(shuō)嗨,這是一扇可破壞的門,你可能需要在這些區(qū)域重建路徑,但是幕后工作其實(shí)很多,像獲取所有三角形,渲染并裁減,這些都與ECS無(wú)關(guān),我們也不應(yīng)該把ECS置于那些問(wèn)題領(lǐng)域,應(yīng)該自己想辦法。

《守望先鋒》架構(gòu)設(shè)計(jì)與網(wǎng)絡(luò)同步  -- GDC2017 精品分享實(shí)錄

          這里的視頻演示的是PathValidationSystem,路徑(Path)就是全部這些藍(lán)色色塊,AI可以行走于其表面上。其實(shí)路徑并不只用于AI,也用在很多英雄的技能上。所以就需要在服務(wù)器和客戶端之間對(duì)這些路徑進(jìn)行數(shù)據(jù)同步。

 

          視頻里的禪亞塔將會(huì)破壞這里的這些物品,你會(huì)看見(jiàn)破壞后的物體掉落到表面下方。然后那里的門會(huì)打開我們會(huì)把那些表面粘在一起。PathValidationSystem只需要說(shuō):嗨,三角形有變化。然后冰山背后就會(huì)用全部數(shù)據(jù)重建路徑。

《守望先鋒》架構(gòu)設(shè)計(jì)與網(wǎng)絡(luò)同步  -- GDC2017 精品分享實(shí)錄

          現(xiàn)在準(zhǔn)備結(jié)束今天的分享了。

 

ECSOverwatch的粘合劑,它很酷,因?yàn)樗梢詭湍阌米钚〉鸟詈蟻?lái)集成大量分散的系統(tǒng)。如果你打算用ECS定義你的規(guī)范,實(shí)際上無(wú)論你想用什么架構(gòu)來(lái)快速定義你的規(guī)范,應(yīng)該都是只有少數(shù)程序員需要接觸物理系統(tǒng)代碼、腳本引擎或者音頻庫(kù)。但是每個(gè)人都應(yīng)該能夠用到膠水代碼,一起集成系統(tǒng)。

          實(shí)施這些限制,就能夠馬到成功。

 

          事實(shí)證明,網(wǎng)絡(luò)同步真的很復(fù)雜,所以必須盡可能的與引擎其余部分解耦,ECS是解決這個(gè)問(wèn)題的好辦法。

          最后在接受提問(wèn)以前,我想感謝我們團(tuán)隊(duì)成員,尤其是gameplay工程師,大家花了3年時(shí)間創(chuàng)造了如此美妙的藝術(shù)品。我們共同努力,創(chuàng)建原則,架構(gòu)不斷進(jìn)化,結(jié)果也是有目共睹的。

 

    本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購(gòu)買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶 評(píng)論公約

    類似文章 更多