Network Simulation Loop
簡介
無論使用何種網路模式,Fusion都以一致的時間步長執行基於tick的離散模擬。處理這個問題的整體過程被稱為網路化模擬循環
。所有的邏輯都寫在NetworkBehaviour
或SimulationBehaviour
的子類中,這意味著常規的Unity GameObjects是這個模擬循環的核心部分。
除了向前移動世界狀態的快照,Fusion還可以根據本地玩家的輸入或任何基於物理學的對象來預測未來的狀態(打開客戶端的物理學預測是可選的)。
可以根據當前的已知數據和自定義代碼為任何物體編寫客戶端預測邏輯(例如,推斷彈道軌跡)。
本文更詳細地解釋了與Fusion的模擬循環如何工作有關的最重要的概念。
Ticks
在聯網的機器之間存在著自然發生的時間差異,Fusion使用離散的抽象時間單位(稱為_ticks_)來處理模擬,而不是直接用時間來驅動它。Ticks與任何特定的客戶端和主機上的實際現實世界的時間是脫鉤的。使用刻度而不是硬件時鐘,可以讓一個網路會話中的所有客戶共享一個共同的”時間“概念的參考框架。嘀嗒聲不會漂移,也不會受到精度問題的影響,這對於在多個聯網機器上準確推斷未來和過去的事件至關重要。
每個刻度之間的時間間隔由NetworkProjectConfig
中的Simulation > Tick Rate
來定義。tick率是以赫茲(Hz)為單位的,所以60的時間步長等於1/60秒的延遲。這意味著系統將使用1/60秒的時間步長來推進遊戲狀態,而不考慮在前一次和當前的tick之間實際經過了多少時間。在代碼中,時間步長可以通過NetworkRunner.DeltaTime
屬性訪問。
IMPORTANT: 用於模擬循環的tick速率與渲染速率(Frame Per Seconds)是不同的!
每個tick都有一個相應的狀態快照,它明確地說明了世界在該時間點上的”真相”。為了給下一個tick生成新的快照,Fusion將在每個SimulationBehaviour
和NetworkBehaviour
上呼叫FixedUpdateNetwork()
,以更新它們所負責的GameObject的整體狀態的部分。Fusion將此稱為對給定的GameObject
擁有State Authority
。
對於任何給定的對象,只有一台機器擁有State Authority
。
- 在客戶/伺服器設置中,這_always_是伺服器。
- 在客戶權限設置中,每個客戶擁有自己的對象,每個客戶通常對他們創建的對象擁有權限。
輸入處理
Fusion使用輸入來驅動客戶端的預測。客戶端根據其本地(過時的)訊息和本地玩家的輸入,預測下一個伺服器狀態。當收到實際狀態時,客戶端將使用這個新的基本事實作為基礎,重新模擬一個新的未來狀態。
為了使預測成為可能,輸入處理被分成了兩個步驟:
- 輸入輪詢:Fusion從客戶端的本地硬件中收集對某一對象具有輸入權限的輸入。
- 輸入消耗:輸入由本地模擬應用,並與伺服器共享,以包括在授權的模擬中。
關於輸入定義、輪詢和消耗的更多訊息,請閱讀手冊中的Network Input
頁面。
預測
預測涉及到客戶_predicting_基於其_current_本地訊息的_future_伺服器狀態。由於網路設備之間固有的延遲,基於從伺服器收到的真實遊戲狀態快照的本地訊息總是過時的。客戶端使用預測,試圖讓本地遊戲狀態跟上伺服器的遊戲狀態。這對於減輕網路延遲對玩家體驗的影響是必要的。網路延遲總是等於從伺服器到客戶端再到客戶端的往返時間。它可以用離散的ticks來推理。
想象一個具有以下特征的場景:
- 當前伺服器tick:100
- Tick率:1/60秒
- 客戶端1的延遲等於4個ticks(從伺服器到客戶端的2個ticks+從客戶端到伺服器的2個ticks)
- 客戶端2的延遲等於6次(從伺服器到客戶端的3次+從客戶端到伺服器的3次)。
雖然客戶端知道他們各自的延遲,但伺服器會通知客戶端他們需要預測多少個tick才能跟上。預測使客戶端能夠及時發送他們的輸入,以滿足伺服器在特定時間的要求。客戶端有輸入權限的對象將使用本地輸入(如玩家角色)進行預測,從而給人以立即響應的錯覺;它被稱為輸入權限,因為雖然它不能明確地控制對象的狀態,但它可以控制驅動該狀態的輸入。
例如,當客戶端1移動他們的玩家角色時,它根據它從伺服器收到的最後一個有效tick,預測未來的狀態。基於這個預測,客戶端1告訴伺服器它計劃在伺服器的未來2個刻度(即第102個刻度)做什麼。這就為輸入留出了足夠的時間,使其能夠在模擬Tick 102的驗証狀態時及時被伺服器接收,從而被消耗。只要沒有任何東西干擾客戶端1的玩家運動,客戶端1上的_predicted_ Tick 102和伺服器上的_validated_ Tick 102將處於同一狀態。
為了確保客戶端1在未來預測出足夠多的tick來補償延遲,伺服器告訴它總是比伺服器狀態提前至少2個tick。另一方面,由於客戶端2的延遲較高,它需要在未來預測3個刻度才能跟上。
從圖中可以看出,客戶端1和客戶端2由於其與伺服器的相對延遲,並_NOT_處於完全相同的狀態。客戶端意識到他們的”真相”是錯誤的,因為其他客戶端的訊息還沒有到達他們那裡。這就是複製和調和發揮作用的地方。
複製
Fusion在緊湊的內存緩沖區中管理和模擬狀態進展。當Fusion從伺服器上收到一個狀態快照時,它會將快照解壓到內存緩沖區中。由於保存在緩沖區中的當前狀態是基於預測的狀態,用快照中的狀態替換它意味著客戶端有效地回到了與從伺服器收到的快照相關的時間點上。然後,Fusion對收到的狀態和客戶的本地預測狀態之間的每一個tick執行一次模擬循環;這將使狀態回到它在收到快照之前的位置,只是有一個更準確的預測狀態。
核對
當客戶端收到一個新的快照,其中有一個更近的-因此也是更準確的-世界狀態,客戶端將其本地狀態與它的狀態進行核對。核對是通過複製客戶端收到的最後一個伺服器快照,並重新模擬本地遊戲狀態,從與伺服器快照相關的時間點到當前時間點。
最終的結果是,伺服器始終處於穩固的核對狀態,而客戶端會在意外事件發生時逐步進行自我調整。這個過程在Fusion中被稱為_Re-simulation_,因為它根據新的驗証狀態重新模擬舊的輸入效果,使客戶端的狀態與伺服器相協調。
如果客戶端1和2在伺服器上發生碰撞,這一點就特別重要。他們的位置將由伺服器來解決,因為它專門決定任何對象在任何給定時間內的最終狀態;這就是為什麼說伺服器有狀態權限的原因。
例子
伺服器向客戶發送穩定的舊數據流。當客戶端1收到_validated_ Tick 100時,它將把Tick 100的_validated_狀態複製到它的內存緩沖區,從而覆蓋了它之前的_predicted_ Tick 100。
- 如果沒有發生任何意外,那麼本地_predicting_的Tick 100將已經是正確的狀態,這意味著客戶端1和伺服器是完全同步的,只是客戶端1比伺服器早2個Tick。
- 如果驗証的Tick與客戶1的預測不一致,那麼_驗証的_Tick 100將被用作糾正的狀態。
在這兩種情況下,客戶端將按順序重新應用100、101和102的輸入,得出新的Tick 103。
從客戶1的角度向前看,當它收到更正後的Tick 101時,它將處於Tick 104,應用其對102和103的輸入,並到達一個更正的狀態。
客戶端2在其模擬循環中經歷了完全相同的程序。唯一的區別是,由於它的延遲較高,在接收伺服器_validated_的Tick 1 Tick時有輕微的延遲,它通過進一步提前預測來彌補這一缺陷。
FixedUpdateNetwork()
為了將當前的tick狀態推進到下一個tick狀態,Fusion在場景中的所有SimulationBehaviour
和NetworkBehaviour
組件上呼叫FixedUpdateNetwork()
。由於FixedUpdateNetwork
在複製和預測過程中都被呼叫,本地狀態應該只從網路狀態中衍生出來;從FixedUpdateNetwork
對本地狀態應用漸進的增量更新,很可能導致應用太多的變化。
快照插值
通常情況下,每個客戶端只對單個或少數幾個GameObject
有輸入權限;其餘的GameObject
的狀態是通過快照更新提供給客戶端的。與預測的狀態不同,快照總是在伺服器後面,因為在伺服器發送的時間和客戶端接收和複製的時間之間存在一定的延遲。
Fusion將這些“遠程“控制的GameObject
稱為__Proxies__。
如果快照被原封不動地應用於代理的當前(視覺)狀態,它就會以模擬的tick rate而不是客戶端的render rate來做動畫和更新。這是要避免的,有幾個原因:
- 節約頻寬。通常情況下,以比渲染低得多的頻率執行模擬是可取的。例如,一個30Hz的網路模擬頻率和一個120Hz的渲染頻率。
- 網路數據包的到達率並不一致。將遊戲的渲染頻率與一個具有很大隨機性的事件聯繫起來,會導致運動的抖動。
為了將網路數據包頻率與渲染的刷新率解耦,同時提供平滑的狀態轉換,Fusion在兩張快照之間對"渲染狀態"進行插值。插值的目的是為了執行實時的平滑,而不是tick;理想情況下,插值是基於兩個最新的快照。然而,快照可能不會以恆定的速度到達。因此,除非有一個小的餘量(緩沖區)防止這種情況發生,否則恆定速率的插值有可能趕上不恆定的網路快照。
Fusions的許多優勢之一是插值算法,該算法根據當前的網路條件適應所需的偏移和緩沖。這給客戶端提供了盡可能少的延遲,同時仍然提供流暢的視覺效果。
Back to top