結(jié)論
先說(shuō)一下我們?cè)谘芯亢褪褂昧藥街螅贸龅慕Y(jié)論:
如果項(xiàng)目沒(méi)有錄像、觀戰(zhàn)功能,請(qǐng)先放棄使用幀同步的念頭,嘗試使用狀態(tài)同步。因?yàn)樵O(shè)計(jì)得好的狀態(tài)同步,可以在很少流量基礎(chǔ)上,完成類幀同步的效果。除非在通信上沒(méi)有壓縮空間,再考慮幀同步。
如果項(xiàng)目需求有中途加入,且不能容忍從頭Replay帶來(lái)的等待,游戲互動(dòng)玩法(游戲內(nèi)個(gè)體之間的相互作用)還非常復(fù)雜,則請(qǐng)慎重考慮是否使用幀同步。
要點(diǎn)
確定性運(yùn)算
- 浮點(diǎn):目前除了主機(jī)平臺(tái)上由于其平臺(tái)的統(tǒng)一性使用的技術(shù)不同外,其他平臺(tái)基本上都是使用的定點(diǎn)數(shù)做的確定性運(yùn)算。這需要將所有涉及同步部分的邏輯運(yùn)算,都要使用定點(diǎn)運(yùn)算。這個(gè)工作,對(duì)于不同規(guī)模的項(xiàng)目而言,工作量可大可小。如果項(xiàng)目中引用了第三方庫(kù)到同步邏輯部分的話,那么這個(gè)第三方庫(kù)也要被定點(diǎn)數(shù)重寫。關(guān)于定點(diǎn)數(shù),如果沒(méi)有太多的精力自寫的話,可以嘗試在網(wǎng)上查找定點(diǎn)庫(kù),網(wǎng)上已經(jīng)有一些開(kāi)源的定點(diǎn)庫(kù),可以拿來(lái)直接用,但注意在使用這些庫(kù)時(shí)的精度問(wèn)題。因?yàn)檫@些定點(diǎn)數(shù)庫(kù)的設(shè)計(jì),在使用時(shí),依然使用浮點(diǎn)數(shù)作為定點(diǎn)數(shù)的聲明和定義,面對(duì)不同的編譯器和運(yùn)算器,浮點(diǎn)轉(zhuǎn)成定點(diǎn)數(shù)的精度處理上,可能是不一致的。所以,在使用前,請(qǐng)驗(yàn)證精度有沒(méi)有問(wèn)題。或者采用其他的方式,比如不再使用浮點(diǎn)數(shù)進(jìn)行聲明和定義。
- 隨機(jī):如果游戲中同步部分的邏輯,使用了隨機(jī),則需要自己實(shí)現(xiàn)一套跨平臺(tái)的隨機(jī)算法,保證所有平臺(tái)隨機(jī)的一致。當(dāng)然,隨機(jī)還會(huì)帶來(lái)另一個(gè)問(wèn)題,就是中途加入時(shí)隨機(jī)數(shù)的一致性問(wèn)題,要保證中途加入的客戶端,執(zhí)行與其他客戶端一致的隨機(jī)序列。這些可能需要我們?cè)趯?shí)現(xiàn)隨機(jī)算法時(shí),兼顧到需要同步到另一端的需求。
- 物理:基本上,大部分的游戲都會(huì)用到物理,拋開(kāi)物理,也會(huì)用到碰撞。因?yàn)榇_定性運(yùn)算的問(wèn)題,我們需要重寫一套確定性物理引擎。
- 動(dòng)畫:如果游戲部分同步邏輯,比如AnimationEvent,坐標(biāo)等是由動(dòng)畫驅(qū)動(dòng)的,那這部分也需要重新設(shè)計(jì),不能使用內(nèi)置的驅(qū)動(dòng)邏輯。
- 其他:這里就包括所有涉及浮點(diǎn)的非運(yùn)算邏輯了,比如invoke、yield等,這些接口要避免使用。
時(shí)序
- 時(shí)序:要保證不同的客戶端,數(shù)據(jù)的存儲(chǔ)、邏輯的執(zhí)行保持時(shí)序一致。所以,一些常用的數(shù)據(jù)結(jié)構(gòu)就不能勝任了,比如常用的Dictionary、hashset等,需要我們使用其他的數(shù)據(jù)結(jié)構(gòu),比如類SortedDictionary這個(gè)效率偏差,或者自寫一套保證時(shí)序的數(shù)據(jù)結(jié)構(gòu)和算法。
難點(diǎn)-中途加入
幀同步的難點(diǎn)在于中途加入,當(dāng)然這里的中途加入,不討論從頭Replay一遍的方案,如果你的項(xiàng)目,能夠容忍從頭Replay,那就可以跳過(guò)了。這里討論的是,將其他客戶端的現(xiàn)場(chǎng)正確地同步給中途加入的客戶端這種方案。在最開(kāi)始的結(jié)論部分已經(jīng)提過(guò),如果游戲玩法比較簡(jiǎn)單,中途加入還是很好實(shí)現(xiàn)的。但如果包含復(fù)雜的互動(dòng)玩法,那對(duì)于游戲開(kāi)發(fā)來(lái)說(shuō),將是類似兩萬(wàn)五千里長(zhǎng)征似的漫長(zhǎng)負(fù)擔(dān)了。中途加入要處理的問(wèn)題很多,一個(gè)很小的功能需求改動(dòng),就可能導(dǎo)致整個(gè)同步機(jī)制掛掉。而且要注意,這個(gè)改動(dòng)帶來(lái)的同步不一致還不一定是必現(xiàn)的。這就對(duì)開(kāi)發(fā)和測(cè)試人員提出了極高的要求,必須一點(diǎn)問(wèn)題都沒(méi)有,不能有一丁點(diǎn)的bug,一旦出現(xiàn)bug,就是致命的——不同步。不同步不像其他的bug,可以忍受,不同步一旦發(fā)生,玩家的所有付出就都白費(fèi)了,結(jié)果不能上傳,得不到服務(wù)器的認(rèn)可,還可能會(huì)被認(rèn)為作弊。
剛開(kāi)始接觸幀同步的開(kāi)發(fā)人員,可能覺(jué)得,中途加入,就把所有對(duì)象的狀態(tài)(比如:位置)同步給另一端不就行了嗎?那這里舉幾個(gè)比較簡(jiǎn)單的例子,說(shuō)明一下中途加入的復(fù)雜:
碰撞反彈:這就涉及到時(shí)序問(wèn)題,先碰撞的就要先反彈。那么,某一時(shí)刻,3個(gè)物體碰撞在一起。此刻有玩家B中途加入戰(zhàn)局,就需要將玩家A現(xiàn)場(chǎng)同步給玩家B,那如何同步才能保證,在B端玩家下一刻執(zhí)行反彈邏輯順序與A一致呢?
碰撞過(guò)程:在使用碰撞過(guò)程中,經(jīng)常會(huì)依賴某個(gè)指定的碰撞過(guò)程,比如Enter、Stay、Exit等。以Enter為例,某一時(shí)刻,玩家A現(xiàn)場(chǎng)兩個(gè)物體碰撞觸發(fā)了Enter過(guò)程,并且A端將Enter的回調(diào)邏輯處理完畢,則此時(shí)A現(xiàn)場(chǎng)的狀態(tài)就是處理完之后的狀態(tài)。此時(shí),B加入戰(zhàn)局,需要將A現(xiàn)場(chǎng)同步給B,那如果將A當(dāng)前狀態(tài)同步給B的話,B端檢測(cè)到這兩個(gè)物體碰撞了,又會(huì)觸發(fā)一次Enter,則再次調(diào)用Enter的回調(diào)邏輯,而A端不會(huì)再觸發(fā)回調(diào)。這就導(dǎo)致A與B端不一致。
上面只是舉了幾個(gè)物理相關(guān)的例子而已,還有很多其他的會(huì)導(dǎo)致不同步的問(wèn)題,比如跟時(shí)間相關(guān)的狀態(tài)等等,這里沒(méi)有列舉。
總之,幀同步的終極問(wèn)題是中途加入。其他的問(wèn)題都還好,中途加入會(huì)導(dǎo)致后期的每一個(gè)功能改動(dòng),都可能會(huì)帶來(lái)整個(gè)版本的復(fù)查,其維護(hù)成本之高,可能會(huì)令很多團(tuán)隊(duì)承擔(dān)不起。因?yàn)槲覀儾荒茉试S中途加入出現(xiàn)bug,一旦出現(xiàn)bug,就是致命的。所以,還是開(kāi)篇的那句話:如果項(xiàng)目不需求中途加入,幀同步向你敞開(kāi)大門,如果需求中途加入,請(qǐng)仔細(xì)評(píng)估玩法和開(kāi)發(fā)之間的矛盾,選擇一條更適合的道路。