Networking Controller Code
概述
Fusion有自己的模擬時刻段,稱為FixedUpdateNetwork()
,它與Unity的FixedUpdate()
非常相似,因為它是一個固定間隔的時刻段,一旦經過足夠的實際時間,它就會向前執行 - 並且它代表模擬。
然而FixedUpdateNetwork()
由Fusion調用和管理。FixedUpdateNetwork()
和FixedUpdate()
之間最顯著的區別是,在伺服器/客戶端拓撲中(不在共享模式下),當伺服器更新到達時,Fusion會定期重置客戶端上的狀態(已連網屬性),然後從遠端伺服器刷新狀態重新模擬到當前的本機刷新。
此外,在所有拓撲中,Fusion在刷新的所有FixedUpdateNetwork()
回調執行完畢後立即捕獲狀態。這使得FixedUpdateNetwork()
成為所有模擬程式碼的正確位置 - 因為FixedUpdateNetwork()
和模擬本質上是同義詞。
相比之下,FixedUpdate()
與FixedUpdateNetwork()
完全分離,通常應該避免使用。
在伺服器客戶端模式中的控制器程式碼
使用任何伺服器/用客戶端拓撲(專用伺服器/主機/單一),所有控制器程式碼在FixedUpdateNetwork()
內部進行模擬至關重要。從Asset Store匯入程式碼,甚至重用您自己的舊程式碼時,記住這一點很重要,因為這些程式碼通常會使用Update()
和FixedUpdate()
時刻段。
FixedUpdateNetwork()
與Unity的FixedUpdate()
非常相似,因為它是一個固定間隔的時刻段,當經過足夠的遊戲時間(累積)時,它會向前遞增。不過,主要區別在於伺服器模式下的FixedUpdateNetwork()
將再次定期運行刷新。
除此之外的任何模擬程式碼都不會意識到重新模擬,也不會為它們執行,從而導致客戶端不斷取消同步。
FixedUpdateNetwork需求的例外
如果滿足以下條件,Update()
或FixedUpdate()
中的模擬程式碼可以給出可接受的結果:
- 如果您沒有對物件採用任何類型的客戶端預測(它完全存在於客戶端的遠端時間幀中),並且只有伺服器正在對物件狀態應用更改;以及
- 客戶端不需要對該物件所做的更改進行平滑內插補點。
共享模式中的控制器程式碼
共享模式模擬固定的刷新,與伺服器客戶端模式(伺服器/主機/單一)相同。然而,開發人員可能希望在Unity中常見的Update()
中應用輸入,而不是在FixedUpdateNetwork()
,原因如下;
- 最快速的用戶輸入響應結果
- 可以使用
Update()
對現有資產進行編碼 - 其他元件系統和資產可能會在
Update()
中進行更改
這使得受控物件甚至領先於本機渲染時間幀(即最後兩個模擬刷新之間的線性插值),並導致物件在真實的本機玩家時間進行模擬和渲染。
然而,在Update()
中移動受控物件與Fusion的固定模擬並不完全相容。轉換值是在FixedUpdateNetwork()
之後捕獲的,而不是在Update()
後捕獲的,因此在Update()
中所做的任何更改都不會被捕獲,直到下一次模擬(這將在未來的Unity更新中發生)。由於捕獲與Update()
中基於deltaTime的任意移動之間的混疊,這可能會產生微掛接和/或傾斜。
共享模式中的混疊
因為共享模式並不嚴格要求控制器程式碼在FixedUpdateNetwork()
中進行模擬,所以如果用戶選擇只在Update()
進行模擬,它確實會引入混疊錯誤的可能性。下圖說明了這個問題。Fusion在FixedUpdateNetwork()
之後捕獲狀態,即使您的控制器程式碼在Update()
中移動或更改狀態。這些固定間隔的已連網值由其他客戶端上的內插補點使用,而不考慮值發生變化時的實時性,這可能會導致內插補點結果出現掛接和/或傾斜。

處理混疊的策略
如果這種混疊是一個問題或值得注意,有一些解決方案。
注: 此混疊可能不足以引起關注,具體取決於所模擬值的性質以及相對幀率和刷新率值。在非常高的刷新率和幀率下,玩家可能不會注意到混疊。如果幀率遠高於刷新率,也可能不那麼令人擔憂。
1) 什麼都不做
如果混疊對於您的遊戲的特定用例來說足夠小,並且用戶不會注意到,您當然可以忽略它,只在Update()
中進行模擬。
2) 移入FixedUpdateNetwork()並在狀態授權上內插補點
一種選擇是將共享模式視為伺服器模式,使所有控制器程式碼都基於模擬,並僅在FixedUpdateNetwork()
中執行。然後,在Render()
中使用內插補點在最新的兩個模擬結果之間進行線性插值。缺點:這會導致少量的輸入延遲。
3) 同時移動FixedUpdateNetwork()和Update()
另一種選擇是使用FixedUpdateNetwork()
和Update()
的混合進行模擬。在FixedUpdateNetwork()
中移動將確保物件在基於刷新的捕獲和複製中均勻正確地移動,從而在遠端客戶端上產生平滑的結果。此外,在Update()
中移動將考慮自上次刷新以來經過的時間,您可以在本機將物件移動到本機時間的正確位置,而無需內插補點。
C#
using Fusion;
using UnityEngine;
/// <summary>
/// The objective of this basic controller code is to simulate
/// Object movement both in FixedUpdateNetwork() AND in Update().
/// It is important to move the Object in FixedUpdateNetwork()
/// to produce evenly spaced results, as these snapshots are used
/// to produce smooth interpolation on remote clients.
/// However, if it very common with Client Authority topologies
/// to apply inputs in Update(), as to apply player input
/// immediately to the rendered Object, rather than interpolating
/// between previous simulated states. So it becomes necessary
/// to simulate in both FUN() and Update() to satisfy both
/// Simulation and Interpolation requirements.
/// Simulation in Update() to produce smooth local movement,
// and Simulation in FUN() to capture even networked tick results.
/// </summary>
public class HybridSharedController : NetworkBehaviour, IBeforeUpdate
{
[SerializeField, Unit(Units.PerSecond)]
public float Speed = 5f;
float _lastSimulationTime;
Vector3 _currentInput;
public override void Spawned()
{
// Capture the starting time so our
// first simulation has a correct delta value.
_lastSimulationTime = Runner.SimulationTime;
}
public override void FixedUpdateNetwork()
{
// Only move the State Authority (there is no prediction in Shared Mode)
if (HasStateAuthority == false) return;
// Calculate the amount of time that has passed since the last
// FixedUpdateNetwork or Update - as this is our real delta
float timeSinceLastSim = Runner.SimulationTime - _lastSimulationTime;
_lastSimulationTime = Runner.SimulationTime;
transform.position += _currentInput * timeSinceLastSim;
}
// The BeforeUpdate() callback is the same as Update(),
// but it fires BEFORE all Fusion work is done.
// Update() would also work here, but input used in FUN()
// would always be from the previous Update().
void IBeforeUpdate.BeforeUpdate()
{
// Only move the State Authority (no prediction in Shared Mode)
if (HasStateAuthority == false) return;
// Collect input every Update(), and store it -
// as we will also use this input in FixedUpdateNetwork()
Vector3 input = default;
if (Input.GetKey(KeyCode.W)) { input += Vector3.forward * Speed; }
if (Input.GetKey(KeyCode.S)) { input += Vector3.back * Speed; }
if (Input.GetKey(KeyCode.A)) { input += Vector3.left * Speed; }
if (Input.GetKey(KeyCode.D)) { input += Vector3.right * Speed; }
_currentInput = input;
// Calculate the amount of time that has passed since last simulation
// (FixedUpdateNetwork or Update), as this is our real delta time
float realtime = Runner.LocalRenderTime + Runner.DeltaTime;
float timeSinceLastSim = realtime - _lastSimulationTime;
_lastSimulationTime = realtime;
transform.position += input * timeSinceLastSim;
}
}
Back to top