還是先說下追幀的問題吧。飛機項目采用的是幀同步的方案,渲染層與邏輯層分離,由定時器一秒20幀來驅動邏輯層做update,而對於渲染層則是以一秒40幀的速度來驅動。渲染層輪循邏輯層做插值。在網絡抖動的情況下,本地演算的幀LocalFrameId可能會落后或領先服務器下發的ServerFrameId。順便提一句,ServerFrameId在這里主要是為了給各個客戶端一個統一的參照系,並不會在Frame中下發其它客戶端的命令。也可以不通過ServerFrameId,而是通過對比其它客戶端的幀命令來校正各客戶端的時鍾盡可能地保持一致,我個人不推薦這種方案,這里也就不展開討論了。那么當LocalFrameId與ServerFrameId差距較大時,我之前的方案會調整本地邏輯層演算的速率,即LocalFrameId落后於ServerFrameId時加快(也就是調小了定時器驅動邏輯層的間隔),而領先於ServerFrameId時則減慢。比如在領先5幀(也就是100MS)左右可能會調整邏輯層驅動間隔由50MS=>120MS,落后5幀時則50MS=>20MS。但是實際測試時效果反而變差了。網絡不太好,但也不是那么差時會出現飛機忽快忽慢的現象。本來是希望快速調整差距的,沒想到手感反而變差了。
我仔細想了下,覺得之前的認識還是有一些問題的。首先為了排除干擾因素,我們假設客戶端開始游戲后,其計時時鍾與服務器計時時鍾完全一致。對客戶端而言,到底什么是網絡抖動呢?事實上就是網絡幀在某處大量累積,在某刻又突然大量涌入。這樣導致在一段時間前LocalFrameId遠遠領先ServerFrameId,此時邏輯層的邏輯幀被調得很慢,而在之后后網絡幀又突然涌入,導致LocalFrameId遠遠落后於ServerFrameId,邏輯幀又被調得很快。如果只考慮當前玩家的操作體驗的話,在一定的網絡抖動范圍內(假設為500MS),不調節邏輯層幀率,對玩家而言手感上自然是更好的。只是在預測其它玩家或AI時,可能會過度預測一些,或者某些極少數情況下玩家的操作在本地演算是成功的,但是實際上並不能成功,會對玩家體驗造成一些影響。但是一方面邏輯層回滾加上渲染層平滑插值會大大減輕這種不適感,二則很多時候的預測是正確或者偏差較小的,總體上來看是完全可以接受的。
我接着做了一些修改,在-250MS~250MS的浮動范圍內是50MS一幀,250MS~750MS范圍內為60MS,-750MS~-250MS范圍為40MS。簡單來說就是邏輯幀的速率調整變得非常溫和。游戲開始后服務器與客戶端時鍾可能有偏差,但是這個偏差在平均程度上也會在幾秒內被追平。如果LocalFrameId落后或者領先ServerFrameId超過1S,則采用暴力的手段直接拉平即可。
OK,追幀的算是說完了。還有個快照發送的問題,之前做得不徹底,今天回來的路上又整理了下,還是記下來吧。現在時間已經10點33分了。
之前的一個目標是希望有一個在時間上無限制的競技場,玩家可以不斷地加入進來或者隨時退出。如果是狀態同步的方案那么就很好處理,客戶端直接拉取服務器的戰斗狀態,重建戰場即可。但是幀同步方案下,服務端是不保存狀態的。有兩種方案。一種是服務器起運算結點跑同一套戰斗邏輯,這樣服務器就有了狀態,但這樣也就喪失了幀同步節省服務器資源的優點了;二是由客戶端定期向服務器上傳狀態快照,服務器保存快照以及自快照之后的所有客戶端命令,客戶端進來后拉取快照和命令演算至當前幀。粗粗一看,似乎狀態快照的上傳頗為簡單,不過,很快你就會看到事實並非如此。
快照上傳的一個重點就是要保證這個快照是不會再改變的有效快照。ServerFrameId同步到客戶端后,在一定的窗口范圍內,[ServerFrameId-WindowSize,ServerFrameId+WindowSize],其中的幀可能會發生回滾。比如在收到ServerFrameId為100時又收到另一個客戶端在第90幀的命令,此時客戶端要回滾到第90幀重新演算。因此我們只需要發送ServerFrameId-WindowSize幀的快照給服務器即可。OK,思路上沒有問題了,不過事情還沒有完。
直接的想法當然是在邏輯幀中直接發送ServerFrameId-WindowSize幀。但是這樣可能出現幾個問題:
一是LocalFrameId<ServerFrameId-WindowSize,有效快照尚未演算出來,無法發送。
二是一次計時器回調中可能會連續更新多個邏輯幀,此時ServerFrameId是不變的,每個邏輯幀內部都不加思考地發送ServerFrameId-WindowSize幀會導致重復發送。
三是可能會發生漏幀,比如在LocalFrameId1時因為有效快照ServerFrameId1-WindowSize尚未演算出來,因此無法發送。但是在下一幀之前,ServerFrameId可能會改變為ServerFrameId2,那么在LocalFrameId2時即使ServerFrameId1-WindowSize已經演算出來,但是此時計算要發送的快照為幀ServerFrameId2-WindowSize,出現了漏幀的情況。
其實仔細考慮下,這個問題是有簡明的方案的。說來說去其實就兩種情況。
1)因為網絡幀可能會一次性大量涌入,也就是說在網絡的回調函數中可能會一次性扔出連續多個ServerFrameId。ServerFrameId-WindowSize可能會大於LocalFrameId;
2)可能會出現在一次計時器回調中連續更新多個邏輯幀的情況,此時ServerFrameId是不變的。LocalFrameId+WindowSize可能會>ServerFrameId。
也就是說,在網絡回調和邏輯幀中都要考慮是否發送狀態快照的問題。雖然也可以解決,不過這樣的做法又繁瑣又丑陋。可以將ServerFrameId與LocalFrameId的變化扔到一個單獨的模塊X中,那么問題變轉變成在不超過兩個數組的各自上限的條件下,盡可能計算可發送的元素范圍。而X更是可以由自己來定制快照發送的策略。
我記得之前還有更細微的一些地方要注意,不過我懶得再回想或深入考慮了,對這個問題而言,上面的這些討論也已經差不多了。命令窗口的問題還要記錄下,不過等下次吧。本想休息下的,誰知道竟23點23分了。煩哪。