This page is a work in progress and could be pending updates.

Fusion 102 - Setting Up A Scene

概述

Fusion 102將解釋如何設置一個基本的聯網場景。

在本節結束時,該項目將包含。

  • 一個基本的Fusion Runner,用來開始或加入一場比賽,以及
  • 一個您可以移動的角色

請查閱手冊中關於網路輸入的深入描述

Back To Top

啟動Fusion

為了啟動Fusion,需要在Fusion的NetworkRunner上呼叫StartGame方法,所以應用程式必須在場景中擁有這個組件或者從代碼中添加它。本教程將以代碼為中心,因為大多數網路邏輯都需要編寫一定量的代碼。

在Unity中打開默認場景

  1. 創建一個新的空的GameObject
  2. 為其添加一個新的腳本組件。
  3. 將該腳本命名為BasicSpawner
  4. 打開腳本,在BasicSpawner類中添加INetworkRunnerCallbacks接口,以及所有必要方法的存根:
public class BasicSpawner : MonoBehaviour, INetworkRunnerCallbacks 
{
  public void OnPlayerJoined(NetworkRunner runner, PlayerRef player)    {    }
  public void OnPlayerLeft(NetworkRunner runner, PlayerRef player)    {    }
  public void OnInputMissing(NetworkRunner runner, PlayerRef player, NetworkInput input) { }
  public void OnShutdown(NetworkRunner runner, ShutdownReason shutdownReason) { }
  public void OnConnectedToServer(NetworkRunner runner) { }
  public void OnDisconnectedFromServer(NetworkRunner runner) { }
  public void OnConnectRequest(NetworkRunner runner, NetworkRunnerCallbackArgs.ConnectRequest request, byte[] token) { }
  public void OnConnectFailed(NetworkRunner runner, NetAddress remoteAddress, NetConnectFailedReason reason) { }
  public void OnUserSimulationMessage(NetworkRunner runner, SimulationMessagePtr message) { }
  public void OnSessionListUpdated(NetworkRunner runner, List<SessionInfo> sessionList) { }
  public void OnCustomAuthenticationResponse(NetworkRunner runner, Dictionary<string, object> data) { }
  public void OnReliableDataReceived(NetworkRunner runner, PlayerRef player, ArraySegment<byte> data) { }
  public void OnSceneLoadDone(NetworkRunner runner) { }
  public void OnSceneLoadStart(NetworkRunner runner) { }
}

置入INetworkRunnerCallbacks將允許Fusions的NetworkRunnerBasicSpawner類進行交互。NetworkRunner是Fusion的核心和靈魂,執行實際的網路模擬。

NetworkRunner將自動檢測到BasicSpawner實現了INetworkRunnerCallbacks並呼叫其方法,因為它將被添加到StartGame方法中的同一個遊戲對象:

async void StartGame(GameMode mode)
{
  // Create the Fusion runner and let it know that we will be providing user input
  _runner = gameObject.AddComponent<NetworkRunner>();
  _runner.ProvideInput = true;

  // Start or join (depends on gamemode) a session with a specific name
  await _runner.StartGame(new StartGameArgs()
  {
    GameMode = mode, 
    SessionName = "TestRoom", 
    Scene = SceneManager.GetActiveScene().buildIndex,
    SceneObjectProvider = gameObject.AddComponent<NetworkSceneManagerDefault>()
  });}

StartGame方法首先創建了FusionNetworkRunner並讓它知道這個客戶端將提供輸入。然後,它用一個硬編碼的名字和指定的遊戲模式啟動一個新的會話(關於遊戲模式的更多內容將在第二部分)。當前的場景索引被傳遞進來,但這只與主機有關,因為客戶端將被強制使用主機指定的場景。最後指定了一個默認的SceneObjectProvider以備不時之需。SceneObjectProvider處理直接放在場景中的NetworkObjects的實例化,嚴格來說,在這個例子中不需要,因為沒有這樣的對象。

Fusion支持幾種不同的網路拓樸結構,但這個介紹將集中在Hosted Mode上。在托管模式中,一個網路對象既是伺服器又是客戶,並創建了網路會話,而其於對象只是加入現有會話的客戶。

為了適應這種情況,需要有一種方法讓用戶選擇是托管遊戲還是加入現有會話-為了簡單起見,下面的例子使用Unity IMGUI。在BasicSpawner.cs中添加以下方法:

private NetworkRunner _runner;

private void OnGUI()
{
  if (_runner == null)
  {
    if (GUI.Button(new Rect(0,0,200,40), "Host"))
    {
        StartGame(GameMode.Host);
    }
    if (GUI.Button(new Rect(0,40,200,40), "Join"))
    {
        StartGame(GameMode.Client);
    }
  }
}

執行這個應用程式將允許用戶主持一個新的會話,並允許額外的實例加入該會話,但由於沒有互動,也沒有數據被傳輸,所以看起來像一個單人遊戲的體驗。

Back To Top

創建玩家角色

為了使這個遊戲成為一個遊戲,必須給每個玩家提供一種方式來提供輸入,並在這個世界上有一個存在,如一個玩家角色。

在Unity編輯器中。

  1. 創建一個名為PlayerPrefab的新的空的遊戲對象。
  2. 給它添加一個NetworkObject組件。

這將給它一個網路身份,以便所有的對象可以引用它。由於用戶將控制這個化身,它也需要一個NetworkCharacterController-這不是一個要求,但它是大多數玩家控制對象的一個好的起點,所以也要繼續添加。

一般來說,最好將網路對象與它們的視覺表現分開,並允許網路對象捕捉到網路狀態,而視覺表現是平滑插值。

為了實現這一點,

  1. 添加一個標準的UnityCube作為PlayerPrefab的一個子對象。
  2. 將其重命名為Body
  3. 刪除Collider
  4. Body遊戲對象拖到父體上的NetworkCharacterControllerInterpolation Target屬性中。

最後,角色控制器需要一個碰撞器,所以

  1. 添加一個額外的Cube子對象。
  2. 將其命名為碰撞器
  3. 移除MeshRendererMeshFilter組件。
  4. 把它拖到NetworkCharacterControllerCollider屬性。

角色配置應該看起來像這樣:

Player Avatar Configuration
角色組件

Save您的項目,讓Fusion創建新的網路對象,然後把它拖到項目文件夾,創建角色預制件,並從場景中刪除該對象。

Back To Top

生成角色

因為遊戲是以托管模式執行的,所以只有主機有權力生成新的對象。這意味著所有玩家的角色都必須在他們加入會話時由主機生成。方便的是,INetworkRunnerCallbacks接口的OnPlayerJoined方法正是在這個場合被呼叫。

同樣,當玩家斷開連接時,以OnPlayerLeft方法呼叫。

用以下代碼替換空的OnPlayerJoinedOnPlayerLeft存根:

[SerializeField] private NetworkPrefabRef _playerPrefab;
private Dictionary<PlayerRef, NetworkObject> _spawnedCharacters = new Dictionary<PlayerRef, NetworkObject>();

public void OnPlayerJoined(NetworkRunner runner, PlayerRef player)
{
  // Create a unique position for the player
  Vector3 spawnPosition = new Vector3((player.RawEncoded%runner.Config.Simulation.DefaultPlayers)*3,1,0);
  NetworkObject networkPlayerObject = runner.Spawn(_playerPrefab, spawnPosition, Quaternion.identity, player);
  // Keep track of the player avatars so we can remove it when they disconnect
  _spawnedCharacters.Add(player, networkPlayerObject);
}

public void OnPlayerLeft(NetworkRunner runner, PlayerRef player)
{
  // Find and remove the players avatar
  if (_spawnedCharacters.TryGetValue(player, out NetworkObject networkObject))
  {
    runner.Despawn(networkObject);
    _spawnedCharacters.Remove(player);
  }
}

這看起來應該很熟悉,因為它基本上用runner.Spawn()替換了Unity的Instantiate()方法,後者需要一組相似的參數,除了最後一個。最後一個參數是對允許為角色提供輸入的玩家的引用-需要注意的是,這與”擁有”該對象不一樣。稍後會有更多關於這方面的內容。

不要忘記回到Unity編輯器,將創建的預制角色拖放到BasicSpawnerPlayer Prefab區域。

Back To Top

收集輸入

擁有輸入權限並不允許客戶端直接修改對象的網路狀態。相反,它可以提供一個輸入結構,然後由主機來解釋,以更新網路狀態。

客戶端也可以在本地應用輸入,向用戶提供即時反饋,但這只是一個本地預測,可能被主機推翻。

在收集用戶的輸入之前,必須定義一個數據結構來保存輸入。創建一個名為NetworkInputData.cs的新文件,其中的結構如下:

using Fusion;
using UnityEngine;

public struct NetworkInputData : INetworkInput
{
  public Vector3 direction;
}

為了簡單起見,本例使用一個向量來表示所需的運動方向,但要知道,還有一些頻寬成本較低的方法可以做到這一點。例如,一個位元區域,每個方向有一個位元。請注意,Fusion將壓縮輸入,只發送實際變化的數據,所以不要過早地進行瘋狂的優化。

當Fusion在OnInput被拉至呼叫返回時,客戶端需要收集用戶的輸入,所以回到BasicSpawner.cs中,用下面的內容替換OnInput()存根:

  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;

    input.Set(data);
  }

同樣,這看起來應該很熟悉-處理程序正在使用標準的Unity輸入來收集和存儲來自本地客戶端的輸入,這個結構是之前定義的。這個方法的最後一行將預填充的輸入結構傳遞給Fusion,Fusion會將其提供給主機和該客戶端有輸入權限的任何對象。

Back To Top

應用輸入

最後一步是將收集到的輸入數據應用於玩家的角色。

  1. 選擇PlayerPrefab
  2. 添加一個新的腳本組件,叫做Player
  3. 打開新的腳本,用NetworkBehaviour替換MonoBehaviour。
  4. 置入FixedUpdateNetwork(),這樣該行為就可以參與Fusion 模擬循環:
using Fusion;

public class Player : NetworkBehaviour
{
  public override void FixedUpdateNetwork(){}
}

FixedUpdateNetwork會在每個模擬時間段被呼叫。這可能會在每個渲染幀中發生多次,因為Fusion會應用一個較早的確認的網路狀態,然後從該tick一直到當前(預測的)本地tick進行重新模擬。

輸入應該在FixedUpdateNetwork中應用,以確保每個tick應用正確的輸入。Fusion提供了一個簡單的方法來獲取相關tick的輸入,恰當地命名為GetInput()。一旦獲得了輸入,就會呼叫NetworkCharacterController來將實際的運動應用到角色轉換上。

完整的Player類別看起來像以下:

using Fusion;

public class Player : NetworkBehaviour
{
  private NetworkCharacterController _cc;

  private void Awake()
  {
    _cc = GetComponent<NetworkCharacterController>();
  }

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

注意所提供的輸入被規範化了,以防止作弊。

Back To Top

測試

現在剩下的就是驗証遊戲確實有可移動的玩家角色,但首先場景需要一個地板,這樣生成的物體才不會直接掉到視野之外。

創建一個立方體對象,把它放在(0,-1,0)處,並在X和Z方向縮放100。

建立應用程式並啟動多個事件(或直接從Unity中執行其中一個)。確保其中一個客戶端按下Host按鈕,其他客戶端按下Join

關於從Unity內部執行的一個重要說明。在編輯器中執行遊戲時,幀率可能非常不穩定,因為編輯器本身需要偶爾進行的渲染。這影響了Fusions正確預測時間的能力,並可能導致小量的抖動,而這些抖動在構建的應用程式中是不會發生的。如果有疑問,可以嘗試執行兩個獨立的構建。

To Document Top