3 - Prediction
概述
Fusion 103將解釋預測,以及如何在伺服器授權的網路遊戲中使用它來為客戶端提供迅捷的反饋。
在本節結束時,該項目將允許玩家生成一個預測的運動球。
運動學對象
為了能夠催生任何物體,它必須首先有一個預制件。
- 在Unity編輯器中創建一個新的空的GameObject
- 將其重命名為
Ball
。 - 添加一個新的
NetworkTransform
組件到它。 - Fusion將顯示一個關於缺少
NetworkObject
組件的警告,所以繼續按Add Network Object
。 - 將
Interpolation Data Source
改為Predicted
,並將其設置為World Space
。 - 在
Ball
上添加一個球體的子對象。 - 在所有方向上將其縮小到0.2
- 將子球拖到父對象上的
NetworkTransform
組件的InterpolationTarget
處。這允許NetworkTransform
將平滑的插值視覺(子對象)與主要的網路對象本身(將扣到網路狀態)分離。 - 從子球體中移除碰撞器
- 在父對象上創建一個新的球體碰撞器,並賦予它0.1的半徑,使它完全覆蓋子對象的視覺表現。
- 在遊戲對象上添加一個新的腳本,並將其稱為
Ball.cs
。 - 最後將整個
Ball
對象拖入項目文件夾以創建一個預制件。 - 保存場景以創建網路對象,並從場景中刪除預制事件。
運動預測
我們的目標是讓Ball
的事件在所有對象上同時表現出相同的行為。
這裡的”同時”是指”在同一模擬時間”,而不是在同一實際世界時間。實現這一目標的方法如下:
- 伺服器在特定的、均勻間隔的時間點上執行模擬,並在每個時間點上呼叫
FixedUpdateNetwork()
。伺服器只從一個刻度點向前移動到下一個刻度點-這與本地物理模擬中常規Unity行為的FixedUpdate()
完全一樣。在每次模擬結束後,伺服器會計算、壓縮和廣播相對於前一次的網路狀態的變化。 - 客戶端定期收到這些快照,但顯然總是延遲於伺服器。當收到快照時,客戶端將其內部狀態設置為該快照的刻度,但隨後立即通過執行自己的模擬,重新模擬收到的快照和客戶端當前刻度之間的所有刻度。
- 客戶端當前的時間點總是領先於伺服器足夠大的幅度,它從用戶那裡收集的輸入可以在伺服器到達給定的時間點之前發送到伺服器,並需要輸入來執行其模擬。
這有一些影響:
- 客戶端每幀多次執行
FixedUpdateNetwork()
,並在收到更新的快照時多次模擬同一個tick。這對聯網狀態是有效的,因為Fusion在呼叫FixedUpdateNetwork()
之前將其重置為適當的tick,但這對非聯網狀態是不正確的,所以在FixedUpdateNetwork()
中使用本地狀態要真正小心。 - 每個對象可以根據已知的先前位置、速度、加速度和其他確定的屬性,模擬預測任何物體的未來狀態。它不能預測的是來自其他玩家的輸入,所以預測會失敗。
- 雖然本地輸入即時應用於客戶端以獲得即時反饋,但它們並不具有權威性。最終定義該tick的仍然是伺服器生成的快照-本地應用的輸入只是一種預測。
考慮到這一點,打開Ball腳本,將基類改為NetworkBehaviour
,以便將其納入Fusions的模擬循環中,並將預先生成的模板代碼替換為FusionsFixedUpdateNetwork()
的覆蓋。
在這個簡單的例子中,Ball
將以恆定的速度在其前進方向上移動5秒鐘,然後就會自動退出。繼續添加一個簡單的線性運動到對象的變換中,例如以下:
C#
using Fusion;
public class Ball : NetworkBehaviour
{
public override void FixedUpdateNetwork()
{
transform.position += 5 * transform.forward * Runner.DeltaTime;
}
}
這幾乎和用於移動普通的非網絡Unity對象的代碼完全一樣,只是時間步長不是Time.deltaTime
,而是對應於ticks之間的時間的Runner.DeltaTime
。為什麼在Unity transform
這樣一個看似局部的屬性上能跨網絡工作,其秘密就是之前添加的NetworkTransform
組件。NetworkTransform
是一種方便的方式,確保變換屬性是網絡狀態的一部分。
代碼仍然需要在設定的時間過後取消對象的生成,這樣它就不會飛到無限遠的地方,最終循環往復,打到玩家的脖子上。Fusion為定時器提供了一個方便的輔助類型,恰當地命名為 "TickTimer"。它不是存儲當前的剩餘時間,而是以ticks為單位存儲結束時間。這意味著定時器不需要在每一個刻度上同步,而只需要在它被創建時同步一次。
要在遊戲的網絡狀態中添加一個TickTimer,需要在Ball中添加一個名為life
的TickTimer
類型的屬性,為getter和setter提供空的存根,並在其上標注[Networked]
屬性。
C#
[Networked] private TickTimer life { get; set; }
計時器應該在對象被生成之前設置,由於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;
}
}
催生預制構件
生成任何預制件的工作原理與生成玩家角色相同,但如果玩家的生成是由網路事件(玩家加入遊戲會話)觸發的,那麼球將根據用戶的輸入來生成。
要做到這一點,輸入數據結構需要用額外的數據來增強。這與運動的模式相同,需要三個步驟:
- 向輸入結構添加數據
- 從Unity的輸入中收集數據
- 在玩家的
FixedUpdateNetwork()
置入中應用這些輸入數據
打開NetworkInputData
,添加一個名為buttons
的新字節字段,並為第一個鼠標按鈕定義一個常量:
C#
using Fusion;
using UnityEngine;
public struct NetworkInputData : INetworkInput
{
public const byte MOUSEBUTTON1 = 0x01;
public byte buttons;
public Vector3 direction;
}
打開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;
if (_mouseButton0)
data.buttons |= NetworkInputData.MOUSEBUTTON1;
_mouseButton0 = false;
input.Set(data);
}
打開Player
類,在GetInput()
的檢查裡面,獲得按鈕位,如果第一個位被設置,則生成一個預計件。預制件可以提供一個常規的Unity[SerializeField]
成員,可以從Unity檢查器中分配。為了能夠在不同的方向上生成,還可以添加一個成員變量來存儲最後的移動方向,並將其作為球的前進方向。
C#
[SerializeField] private Ball _prefabBall;
private Vector3 _forward;
...
if (GetInput(out NetworkInputData data))
{
...
if (data.direction.sqrMagnitude > 0)
_forward = data.direction;
if ((data.buttons & NetworkInputData.MOUSEBUTTON1) != 0)
{
Runner.Spawn(_prefabBall,
transform.position+_forward, Quaternion.LookRotation(_forward),
Object.InputAuthority);
}
...
}
...
為了限制生成的頻率,將生成的呼叫包裹在一個聯網的定時器中,每次生成之間必須過期。只有在檢測到有按鈕按下時才會重置計時器:
C#
[Networked] private TickTimer delay { get; set; }
...
if (delay.ExpiredOrNotRunning(Runner))
{
if ((data.buttons & NetworkInputData.MOUSEBUTTON1) != 0)
{
delay = TickTimer.CreateFromSeconds(Runner, 0.5f);
Runner.Spawn(_prefabBall,
transform.position+_forward, Quaternion.LookRotation(_forward),
Object.InputAuthority);
...
對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 (delay.ExpiredOrNotRunning(Runner))
{
if ((data.buttons & NetworkInputData.MOUSEBUTTON1) != 0)
{
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
字段。