This document is about: FUSION 2
SWITCH TO

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()中移動或更改狀態。這些固定間隔的已連網值由其他客戶端上的內插補點使用,而不考慮值發生變化時的實時性,這可能會導致內插補點結果出現掛接和/或傾斜。

Aliasing caused by Simulating in Update() vs FixedUpdateNetwork()
Update()與FixedUpdateNetwork()中的模擬引起的混疊。

處理混疊的策略

如果這種混疊是一個問題或值得注意,有一些解決方案。

注: 此混疊可能不足以引起關注,具體取決於所模擬值的性質以及相對幀率和刷新率值。在非常高的刷新率和幀率下,玩家可能不會注意到混疊。如果幀率遠高於刷新率,也可能不那麼令人擔憂。

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