Player Input
概述
Fusion提供一種機制,用於在每個刷新收集玩家輸入,將收集到的輸入資料存儲在歷史緩衝區中,並自動將這些資料複製到伺服器。
Fusion主要提供這種機制,使客戶端預測成為可能。刷新輸入用於刷新模擬(FixedUpdateNetwork()
),在正在預測的客戶端(HasInputAuthority == true
)和伺服器上,以便在它們之間產生一致的結果。客戶端上的歷史緩衝區用於重新模擬刷新。
輸入結構定義
輸入結構具有以下約束:
- 它必須繼承自
INetworkInput
; - 它只能包含基本類型和結構;
- 輸入結構及其包含的任何結構都是頂層結構(即不能巢狀在類別中);並且,
- 對於布林值,使用
NetworkBool
而不是bool
- C#不強制跨平台的布林值大小一致,因此使用NetworkBool
將其正確序列化為單個位元。
Fusion將智慧地映射結構的類型;這允許為不同的遊戲模式或遊戲的不同部分使用不同的結構。展開輸入時,Fusion將只傳回正確類型的可用輸入。
C#
public struct MyInput : INetworkedInput {
public Vector3 aimDirection;
}
按鈕
有一個特殊的NetworkButtons
類型,它為在INetworkInput
結構中保存按鈕按下提供了一個方便的包裝器。
要向輸入結構新增按鈕,只需:
- 為按鈕創建列舉(重要提示: 必須明確定義它並從0開始);並且,
- 將
NetworkButtons
變數新增到INetworkedInput
。
C#
enum MyButtons {
Forward = 0,
Backward = 1,
Left = 2,
Right = 3,
}
public struct MyInput : INetworkInput {
public NetworkButtons buttons;
public Vector3 aimDirection;
}
可用於直接從NetworkButtons
變數指派和讀取值的API為:
void Set(int button, bool state)
:獲取按鈕及其狀態的列舉值(按下=真,未按下=偽);bool IsSet(int button)
:獲取按鈕的列舉值並傳回其布林值狀態。
NetworkButtons
類型是無狀態的,因此不包含有關按鈕先前狀態的任何中繼資料。為了能夠使用NetworkButtons
提供的下一組方法,有必要追蹤按鈕的先前狀態;透過為每個玩家建立先前狀態的[Networked]
版本,可以很容易地完成這項工作。
C#
public class PlayerInputConsumerExample : NetworkBehaviour {
[Networked] public NetworkButtons ButtonsPrevious { get; set; }
// Full snippet in the GetInput() section further down.
}
透過這種方式,可以將按鈕的當前狀態與其先前狀態進行比較,以評估按鈕是剛剛被按下還是釋放。
NetworkButtons GetPressed(NetworkButtons previous)
:傳回剛剛按下的所有按鈕的一組值。NetworkButtons GetReleased(NetworkButtons previous)
:傳回剛剛釋放的所有按鈕的一組值。(NetworkButtons, NetworkButtons) GetPressedOrReleased(NetworkButtons previous)
:傳回剛剛按下並釋放的按鈕的值元組。
重要: 僅使用Input.GetKey()
指派按鈕值。請 不要 使用Input.GetKeyDown()
或Input.GetKeyUp()
,因為它們與Fusion刷新不同步,因此可能會被錯過。
輪詢輸入
Fusion透過輪詢本機客戶端並填入先前定義的輸入結構來收集輸入。Fusion運行器只追蹤單個輸入結構,因此強烈建議在單一地方實作輸入輪詢,以避免任何意外行為。
Fusion運行器透過調用INetworkRunnerCallbacks.OnInput()
方法來輪詢輸入。OnInput()
的實作可以用所選資料填入從INetworkInput
繼承的任何結構。透過在提供的NetworkInput
上調用Set()
,將填充的結構傳遞回Fusion。
重要:
- 擁有多個輪詢網站將導致除最後一個輸入結構版本之外的所有結構版本都被覆寫。
- 只 在本機輪詢輸入(所有模式)。
模擬行為 / 網路行為
為了在SimulationBehaviour
或NetworkBehaviour
元件中使用OnInput()
,請實作INetworkRunnerCallbacks
介面並調用NetworkRunner.AddCallbacks()
以向本機運行器註冊回調。
C#
public class InputProvider : SimulationBehaviour, INetworkRunnerCallbacks {
public void OnEnable(){
if(Runner != null){
Runner.AddCallbacks( this );
}
}
public void OnInput(NetworkRunner runner, NetworkInput input) {
var myInput = new MyInput();
myInput.Buttons.Set(MyButtons.Forward, Input.GetKey(KeyCode.W));
myInput.Buttons.Set(MyButtons.Backward, Input.GetKey(KeyCode.S));
myInput.Buttons.Set(MyButtons.Left, Input.GetKey(KeyCode.A));
myInput.Buttons.Set(MyButtons.Right, Input.GetKey(KeyCode.D));
myInput.Buttons.Set(MyButtons.Jump, Input.GetKey(KeyCode.Space));
input.Set(myInput);
}
public void OnDisable(){
if(Runner != null){
Runner.RemoveCallbacks( this );
}
}
}
單行為與純粹CSharp
為了輪詢來自常規CSharp指令碼或MonoBehaviour
的輸入,請執行以下步驟:
- 實作
INetworkRunnerCallbacks
和OnInput()
;並且, - 透過調用其上的
AddCallbacks()
,以NetworkRunner
註冊指令碼。
C#
public class InputProvider : Monobehaviour, INetworkRunnerCallbacks {
public void OnEnable(){
var myNetworkRunner = FindObjectOfType<NetworkRunner>();
myNetworkRunner.AddCallbacks( this );
}
public void OnInput(NetworkRunner runner, NetworkInput input) {
// Same as in the snippet for SimulationBehaviour and NetworkBehaviour.
}
public void OnDisable(){
var myNetworkRunner = FindObjectOfType<NetworkRunner>();
myNetworkRunner.RemoveCallbacks( this );
}
}
Unity新輸入系統
為了使用新的Unity輸入系統,過程是相同的,但有必要收集來自建立的輸入動作的輸入。
建立輸入操作並定義所需按鈕後,生成C#類別並在程式碼中建立它的執行個體。也可以使用PlayerInput
類別中的事件,只要輸入存儲在本機快取中以便在OnInput()
中使用。
目標是收集來自新輸入系統而不是舊輸入系統的OnInput()
中的按鈕狀態,因此除了系統的設定部分外,其餘部分基本相同。
C#
public class InputProvider : SimulationBehaviour, INetworkRunnerCallbacks {
// creating a instance of the Input Action created
private PlayerActionMap _playerActionMap = new PlayerActionMap();
public void OnEnable(){
if(Runner != null){
// enabling the input map
_playerActionMap.Player.Enable();
Runner.AddCallbacks(this);
}
}
public void OnInput(NetworkRunner runner, NetworkInput input)
{
var myInput = new MyInput();
var playerActions = _playerActionMap.Player;
myInput.buttons.Set(MyButtons.Jump, playerActions.Jump.IsPressed());
input.Set(myInput);
}
public void OnDisable(){
if(Runner != null){
// disabling the input map
_playerActionMap.Player.Disable();
Runner.RemoveCallbacks( this );
}
}
}
低刷新率中的輪詢輸入
為了以低刷新率收集輸入,有必要使用Unity的更新函數來累積結構中記錄的任何輸入,這些輸入稍後可能會被使用。
在OnInput
中,此結構將被讀取並透過input.Set()
調用正確傳輸到Fusion,然後它將被重新設定,開始為下一個刷新累積輸入。
C#
public class InputProvider : SimulationBehaviour, INetworkRunnerCallbacks {
// Local variable to store the input polled.
MyInput myInput = new MyInput();
public void OnEnable() {
if(Runner != null) {
Runner.AddCallbacks( this );
}
}
public void Update()
{
if (Input.GetMouseButtonDown(0)) {
myInput.Buttons.Set(MyButtons.Attack, true);
}
if (Input.GetKeyDown(KeyCode.Space)) {
myInput.Buttons.Set(MyButtons.Jump, true);
}
}
public void OnInput(NetworkRunner runner, NetworkInput input) {
input.Set(myInput);
// Reset the input struct to start with a clean slate
// when polling for the next tick
myInput = default;
}
}
以UI輪詢輸入
使用UI輪詢輸入遵循與上述相同的邏輯。從透過UI調用的方法中設定NetworkButton
,在OnInput
上讀取並重新設定它。
讀取輸入
模擬可以讀取輸入,以基於先前輪詢的輸入,將現有的已連網狀態從其當前狀態修改為新狀態。Fusion透過網路同步輸入結構,並在具有輸入授權的客戶端和具有狀態授權的客戶端(主機)上進行模擬時使其可用。
與輪詢輸入相反,可以根據需要在多個不同的地方進行讀取輸入。
注意: 玩家輸入僅適用於具有輸入授權和狀態授權的客戶端。在HostMode
和ServerMode
中,這意味著玩家客戶端和主機/伺服器,而在SharedMode
中這是同一個客戶端。
無法在一個客戶端上讀取另一個客戶端的輸入。因此,任何依賴於輸入的更改都需要保存為[Networked]
狀態,以便在其他客戶端上複製。
GetInput()
為了獲取輸入結構,請在任何對該等物件(例如控制玩家移動的元件)具有輸入授權的“NetworkBehaviour
的FixedUpdateNetwork()
中調用GetInput(out T input)
。對GetInput()
的調用提供了與之前在OnInput()
中填入的輸入結構相同的輸入結構。
在以下情況,對GetInput()
的調用將傳回偽:
- 客戶端沒有狀態授權或輸入授權
- 模擬中不存在請求的輸入類型
GameMode
的具體資訊:
- 在
HostMode
和ServerMode
下,給定刷新的輸入僅對玩家和主機/伺服器模擬可用。輸入 不 在玩家之間共享。 - 在
SharedMode
中,保持OnInput()
和GetInput()
模式是一種很好的做法,但缺乏中央授權意味著只有本機模擬才能存取本機玩家的輸入。輸入 不 在玩家之間共享。
C#
using Fusion;
using UnityEngine;
public class PlayerInputConsumerExample : NetworkBehaviour {
[Networked] public NetworkButtons ButtonsPrevious { get; set; }
public override void FixedUpdateNetwork() {
if (GetInput<MyInput>(out var input) == false) return;
// compute pressed/released state
var pressed = input.Buttons.GetPressed(ButtonsPrevious);
var released = input.Buttons.GetReleased(ButtonsPrevious);
// store latest input as 'previous' state we had
ButtonsPrevious = input.Buttons;
// movement (check for down)
var vector = default(Vector3);
if (input.Buttons.IsSet(MyButtons.Forward)) { vector.z += 1; }
if (input.Buttons.IsSet(MyButtons.Backward)) { vector.z -= 1; }
if (input.Buttons.IsSet(MyButtons.Left)) { vector.x -= 1; }
if (input.Buttons.IsSet(MyButtons.Right)) { vector.x += 1; }
DoMove(vector);
// jump (check for pressed)
if (pressed.IsSet(MyButtons.Jump)) {
DoJump();
}
}
void DoMove(Vector3 vector) {
// dummy method with no logic in it
}
void DoJump() {
// dummy method with no logic in it
}
}
Runner.TryGetInputForPlayer()
可以透過調用NetworkRunner.TryGetInputForPlayer<T>(PlayerRef playerRef, out var input)
來從NetworkBehaviour
外部讀取輸入。除了INetworkInput
類型外,它還需要指定應擷取輸入的玩家。注意: 適用與GetInput()
相同的限制;即,在具有輸入授權的客戶端上或伺服器/主機上可以獲得指定玩家的輸入。
C#
var myNetworkRunner = FindObjectOfType<NetworkRunner>();
// Example for local player if script runs only on the client
if(myNetworkRunner.TryGetInputForPlayer<MyInput>(myNetworkRunner.LocalPlayer, out var input)){
// do logic
}
關於授權的說明
為了保證完全的模擬授權,在填入輸入結構時,關鍵是只在OnInput()
中收集輸入值。基於輸入執行的邏輯應完全在GetInput()
中完成。
例如,以下分割將用於發射子彈:
OnInput()
:保存玩家射擊按鈕的值。GetInput()
:檢查發射按鈕是否被按下,如果被按下,則發射子彈。
每個同儕節點有多名玩家
通常被稱為「沙發」、「分割畫面」或「本機」多人遊戲。多個真人玩家可以向單個同儕節點(比如具有多個控制器的遊戲機)提供輸入,同時也可以參與線上多人遊戲。Fusion將同一同儕節點上的所有玩家視為單個PlayerRef
的一部分(PlayerRef
識別網路同儕節點,而不是單個真人玩家),並且對它們沒有區別。您可以決定什麼是「玩家」以及他們提供什麼樣的輸入。
如何處理此用例的一個例子是針對每個玩家,以一個巢狀的INetworkStruct
來定義您的INetworkInput
結構。
C#
public struct PlayerInputs : INetworkStruct
{
// All player specific inputs go here
public Vector2 dir;
}
public struct CombinedPlayerInputs : INetworkInput
{
// For this example we assume 4 players max on one peer
public PlayerInputs PlayerA;
public PlayerInputs PlayerB;
public PlayerInputs PlayerC;
public PlayerInputs PlayerD;
// Example indexer for easier access to nested player structs
public PlayerInputs this[int i]
{
get {
switch (i) {
case 0: return PlayerA;
case 1: return PlayerB;
case 2: return PlayerC;
case 3: return PlayerD;
default: return default;
}
}
set {
switch (i) {
case 0: PlayerA = value; return;
case 1: PlayerB = value; return;
case 2: PlayerC = value; return;
case 3: PlayerD = value; return;
default: return;
}
}
}
}
從多個玩家收集輸入:
C#
public class CouchCoopInput : MonoBehaviour, INetworkRunnerCallbacks
{
public void OnInput(NetworkRunner runner, NetworkInput input)
{
// For this example each player (4 total) has one Joystick.
var myInput = new CombinedPlayerInputs();
myInput[0] = new PlayerInputs() { dir = new Vector2( Input.GetAxis("Joy1_X"), Input.GetAxis("Joy1_Y")) };
myInput[1] = new PlayerInputs() { dir = new Vector2( Input.GetAxis("Joy2_X"), Input.GetAxis("Joy2_Y")) };
myInput[2] = new PlayerInputs() { dir = new Vector2( Input.GetAxis("Joy3_X"), Input.GetAxis("Joy3_Y")) };
myInput[3] = new PlayerInputs() { dir = new Vector2( Input.GetAxis("Joy4_X"), Input.GetAxis("Joy4_Y")) };
input.Set(myInput);
}
// (removed unused INetworkRunnerCallbacks)
}
為模擬取得輸入:
C#
public class CouchCoopController : NetworkBehaviour
{
// Player index 0-3, indicating which of the 4 players
// on the associated peer controls this object.
private int _playerIndex;
public override void FixedUpdateNetwork()
{
if (GetInput<CombinedPlayerInputs>(out var input))
{
var dir = input[_playerIndex].dir;
// Convert joystick direction into player heading
float heading = Mathf.Atan2(dir.y, dir.x) * Mathf.Rad2Deg;
transform.rotation = Quaternion.Euler(0f, heading - 90, 0f);
}
}
}
Back to top