This document is about: FUSION 2
SWITCH TO

3 - Prediction

概述

Fusion 103將說明預測,以及如何使用預測來在伺服器授權網路遊戲中,提供關於客戶端的快速回饋。

在本章節的最後,本專案將允許玩家生成一個預測的運動學的球。

運動學的物件

為了能夠生成任何物件,必須首先有一個預製件。

  1. 在Unity編輯器中建立一個新的空的遊戲物件
  2. 將它重新命名為Ball
  3. 新增一個新的NetworkTransform元件到它。
  4. Fusion將顯示一個警告,內容是關於遺失NetworkObject元件,所以請繼續並按下Add Network Object
  5. 新增一個球體下層到Ball
  6. 在所有方向將它縮放小到0.2
  7. 從下層球體移除碰撞器
  8. 取而代之地在上層物件上建立一個新的球體碰撞器,並為其指定0.1的半徑,這樣它完全覆蓋下層物件的視覺效果代表。
  9. 新增一個新的指令碼到遊戲物件,並且稱其為Ball.cs
  10. 最後拖曳整個Ball物件到專案資料夾來建立一個預製件
  11. 儲存該場景以內嵌網路物件,並且從場景刪除預製件執行個體。
ball prefab
球預製件

已預測移動

目標是使Ball的執行個體在所有同儕節點上同時行為相同。

在此文中,「同時」的意思是「在相同的模擬刷新上」,而不是相同的實際世界時間。實現這一目標的方式如下:

  1. 伺服器在特定的、均勻間隔的刷新上運行模擬,並在各個刷新上調用FixedUpdateNetwork()。伺服器只是並且總是從一個刷新向前移動到下一個刷新——這與本機物理模擬中的常規Unity行為的FixedUpdate()完全相同。在各個模擬刷新之後,伺服器計算、壓縮並廣播網路狀態中相對於前一刷新的變化。
  2. 客戶端以定期間隔接收這些快照,但顯然總是延遲於伺服器之後。當接收到快照時,客戶端將其內部狀態設定回該快照的刷新,但隨後透過運行自己的模擬,來立即重新模擬在接收到的快照和客戶端當前刷新之間的所有刷新。
  3. 客戶端的當前刷新總是領先於伺服器足夠寬的範圍,因此它從使用者那裡收集的輸入可以在伺服器到達給定刷新之前發送到伺服器,並且需要該輸入來運行其模擬。

這樣許多含義:

  1. 客戶端每幀運行FixedUpdateNetwork()多次,並在接收更新的快照時多次模擬同一個刷新。這適用於已連網狀態,因為Fusion在調用FixedUpdateNetwork()之前將其重置為正確的刷新,但對於非連網狀態則不是如此,因此在FixedUpdateNetwork()中使用本機狀態時要非常小心。
  2. 各個同儕節點可以基於已知的先前位置、速度、加速度和其他確定性屬性,來模擬任何物件的一個已預測未來狀態。它無法預測的事是來自其他玩家的輸入,因此預測 失敗。
  3. 雖然本機輸入立即應用於客戶端以獲得即時回饋,但它們不是授權性的。仍然是伺服器生成的快照最終定義了該刷新——輸入的本機應用只是一個預測。

考慮到這一點,打開球指令碼,將基礎類別更改為NetworkBehaviour,以將其包含在Fusion的模擬迴圈中,並用Fusion的FixedUpdateNetwork()覆寫來替換預先生成的範本程式碼。

在這個簡單的例子中,我們將使Ball在取消生成自己之前,以恒定的速度向前移動5秒。請繼續並且向物件轉換新增一個簡單的線性運動,如下所示:

C#

using Fusion;

public class Ball : NetworkBehaviour
{
  public override void FixedUpdateNetwork()
  {
    transform.position += 5 * transform.forward * Runner.DeltaTime;
  }
}

這幾乎與用於移動常規非連網Unity物件的程式碼完全相同,只是時間步不是Time.deltaTime,而是對應於刷新之間的時間的Runner.DeltaTime。這在像Unity transform這樣看似本機的屬性上可以跨網路工作的秘密,當然是先前新增的NetworkTransform元件。NetworkTransform是確保轉換屬性是網路狀態的一部分的一種方便方法。

在設定的時間到期後,程式碼仍然需要取消生成物件,這樣它就不會飛到無窮遠,最終迴圈並擊中玩家的頸部。 Fusion為計時器提供了一種方便的協助工具類型,恰當地命名為TickTimer。它不是儲存當前剩餘時間,而是以刷新儲存結束時間。這意味著計時器不需要在每個刷新同步,只需要在建立時同步一次。

為了將刷新計時器新增到遊戲已連網狀態,請向名為TickTimer類型的life的球新增幾個屬性,為取得器及設定器提供空的虛設常式,並用[Networked]屬性標記它。

C#

[Networked] private TickTimer life { get; set; }

標有[Networked]的欄位必須是屬性,並具有{get; set;}虛設常式,因為Fusion使用這些欄位來生成序列化程式碼。請確保始終遵循此模式。

應在生成對象之前設定計時器,並且由於只有在建立本機執行個體之後才調用Spawned(),因此不應將其用於初始化網路狀態。

取而代之地,建立一個可以從玩家調用的Init()方法,並使用它將生命屬性設定為未來5秒。最好使用TickTimer本身上的靜態協助工具方法CreateFromSeconds()來完成此操作。

C#

public void Init()
{
  life = TickTimer.CreateFromSeconds(Runner, 5.0f);
}

最後,必須更新FixedUpdateNetwork()以檢查計時器是否已過期,如果已過期,則取消生成球:

C#

if(life.Expired(Runner))
  Runner.Despawn(Object);

總而言之,Ball類別現在看起來應該是這樣的:

C#

using Fusion;

public class Ball : NetworkBehaviour
{
  [Networked] private TickTimer life { get; set; }

  public void Init()
  {
    life = TickTimer.CreateFromSeconds(Runner, 5.0f);
  }

  public override void FixedUpdateNetwork()
  {
    if(life.Expired(Runner))
      Runner.Despawn(Object);
    else
      transform.position += 5 * transform.forward * Runner.DeltaTime;
  }
}

生成預製件

生成任何預製件的工作原理與生成玩家虛擬人偶相同,但如果玩家生成是由網路事件(玩家加入遊戲階段)觸發的,則將根據使用者輸入來生成球。

為了實現這一點,需要使用額外的資料來增強輸入資料架構。這遵循與運動相同的模式,並且需要三個步驟:

  1. 新增資料到輸入架構
  2. 從Unity的輸入來收集資料
  3. 在玩家的FixedUpdateNetwork()實作中應用輸入

開啟NetworkInputData並且新增一個名為buttons的新位元組欄位,並為第一個滑鼠按鈕定義一個常數:

C#

using Fusion;
using UnityEngine;

public struct NetworkInputData : INetworkInput
{
    public const byte MOUSEBUTTON0 = 1;

    public NetworkButtons buttons;
    public Vector3 direction;
}

NetworkButtons類型是一種Fusion類型,有助於以最佳頻寬使用率,來追蹤多個已連網按鈕的輸入狀態。

開啟BasicSpawner,前往OnInput()方法,並且新增對主要滑鼠按鈕的檢查,並設定buttons欄位的第一個位元(如果它已關閉)。為了確保不會錯過快速點擊,在Update()中對滑鼠按鈕進行採樣,並在將其記錄在輸入架構後重新設定:

C#

private bool _mouseButton0;
private void Update()
{
  _mouseButton0 = _mouseButton0 | Input.GetMouseButton(0);
}

public void OnInput(NetworkRunner runner, NetworkInput input)
{
  var data = new NetworkInputData();

  if (Input.GetKey(KeyCode.W))
    data.direction += Vector3.forward;

  if (Input.GetKey(KeyCode.S))
    data.direction += Vector3.back;

  if (Input.GetKey(KeyCode.A))
    data.direction += Vector3.left;

  if (Input.GetKey(KeyCode.D))
    data.direction += Vector3.right;

  data.buttons.Set( NetworkInputData.MOUSEBUTTON0, _mouseButton0);
  _mouseButton0 = false;

  input.Set(data);
}

開啟Player類別,在GetInput()檢查的內部,檢查按鈕是否被按下並生成一個預製件。可以提供與預製件一起的一個常規的Unity [SerializeField]成員,該成員可以由Unity檢查器指派。為了能夠在不同的方向上生成,也新增了一個成員變數來儲存最後的移動方向,並將其用作球的前進方向。

C#

[SerializeField] private Ball _prefabBall;

private Vector3 _forward = Vector3.forward;
...
if (GetInput(out NetworkInputData data))
{
  ...
  if (data.direction.sqrMagnitude > 0)
    _forward = data.direction;
  if (data.buttons.IsSet(NetworkInputData.MOUSEBUTTON0))
  {
      Runner.Spawn(_prefabBall,
      transform.position+_forward, Quaternion.LookRotation(_forward),
      Object.InputAuthority);
  }
  ...
}
...

為了限制生成頻率,請將對生成的調用封裝在已連網計時器中,該計時器必須在各個生成之間到期。只在偵測到按下按鈕時重新設定計時器:

C#

[Networked] private TickTimer delay { get; set; }
...
if (HasStateAuthority && delay.ExpiredOrNotRunning(Runner))
{
  if (data.buttons.IsSet(NetworkInputData.MOUSEBUTTON0))
  {
    delay = TickTimer.CreateFromSeconds(Runner, 0.5f);
    Runner.Spawn(_prefabBall,
    transform.position+_forward, Quaternion.LookRotation(_forward),
    Object.InputAuthority);
...

需要檢查StateAuthority,因為只有StateAuthority(主機端)才能生成NetworkObjects。因此,與移動不同的是,這些物件的生成只會在主機端上執行,而不會在客戶端上預測。

Spawn的實際調用需要稍微修改,因為球在同步之前需要額外的初始化。具體來說,必須調用先前新增的Init()方法,以確保刷新計時器設定正確。

為此,Fusion允許向Spawn()提供回調,該回調將在具現化預製件之後但在同步它之前叫用。

總之,類別應該是這樣的:

C#

using Fusion;
using UnityEngine;

public class Player : NetworkBehaviour
{
  [SerializeField] private Ball _prefabBall;

  [Networked] private TickTimer delay { get; set; }

  private NetworkCharacterController _cc;
  private Vector3 _forward;

  private void Awake()
  {
    _cc = GetComponent<NetworkCharacterController>();
    _forward = transform.forward;
  }

  public override void FixedUpdateNetwork()
  {
    if (GetInput(out NetworkInputData data))
    {
      data.direction.Normalize();
      _cc.Move(5*data.direction*Runner.DeltaTime);

      if (data.direction.sqrMagnitude > 0)
        _forward = data.direction;

      if (HasStateAuthority && delay.ExpiredOrNotRunning(Runner))
      {
        if (data.buttons.IsSet(NetworkInputData.MOUSEBUTTON0))
        {
          delay = TickTimer.CreateFromSeconds(Runner, 0.5f);
            Runner.Spawn(_prefabBall,
            transform.position+_forward, Quaternion.LookRotation(_forward),
            Object.InputAuthority, (runner, o) =>
            {
              // Initialize the Ball before synchronizing it
              o.GetComponent<Ball>().Init();
            });
        }
      }
    }
  }
}

測試前的最後一步是將預製件指派到Player預製件上的_prefabBall欄位。在專案中選擇PlayerPrefab,然後將Ball預製件預設拖到Prefab Ball欄位中。

下一章是 主機端模式基礎 4 - 物理

Back to top