2 - Setting Up A Scene
概述
Fusion 102將解釋如何設置一個基本的聯網場景。
在本節結束時,該項目將包含。
- 一個基本的Fusion Runner,用來開始或加入一場比賽,以及
- 一個您可以移動的角色
啟動Fusion
為了啟動Fusion,需要在Fusion的NetworkRunner
上呼叫StartGame
方法,所以應用程式必須在場景中擁有這個組件或者從代碼中添加它。本教程將以代碼為中心,因為大多數網路邏輯都需要編寫一定量的代碼。
在Unity中打開默認場景
- 創建一個新的空的GameObject
- 為其添加一個新的腳本組件。
- 將該腳本命名為
BasicSpawner
。 - 打開腳本,在
BasicSpawner
類中添加INetworkRunnerCallbacks
接口,以及所有必要方法的存根:
C#
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的NetworkRunner
與BasicSpawner
類進行交互。NetworkRunner
是Fusion的核心和靈魂,執行實際的網路模擬。
NetworkRunner
將自動檢測到BasicSpawner
實現了INetworkRunnerCallbacks
並呼叫其方法,因為它將被添加到StartGame
方法中的同一個遊戲對象:
C#
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
中添加以下方法:
C#
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);
}
}
}
執行這個應用程式將允許用戶主持一個新的會話,並允許額外的實例加入該會話,但由於沒有互動,也沒有數據被傳輸,所以看起來像一個單人遊戲的體驗。
創建玩家角色
為了使這個遊戲成為一個遊戲,必須給每個玩家提供一種方式來提供輸入,並在這個世界上有一個存在,如一個玩家角色。
在Unity編輯器中。
- 創建一個名為
PlayerPrefab
的新的空的遊戲對象。 - 給它添加一個
NetworkObject
組件。
這將給它一個網路身份,以便所有的對象可以引用它。由於用戶將控制這個化身,它也需要一個NetworkCharacterController
-這不是一個要求,但它是大多數玩家控制對象的一個好的起點,所以也要繼續添加。
一般來說,最好將網路對象與它們的視覺表現分開,並允許網路對象捕捉到網路狀態,而視覺表現是平滑插值。
為了實現這一點,
- 添加一個標準的Unity
Cube
作為PlayerPrefab
的一個子對象。 - 將其重命名為
Body
。 - 刪除
Collider
。 - 把
Body
遊戲對象拖到父體上的NetworkCharacterController
的Interpolation Target
屬性中。
最後,角色控制器需要一個碰撞器,所以
- 添加一個額外的
Cube
子對象。 - 將其命名為
碰撞器
。 - 移除
MeshRenderer
和MeshFilter
組件。 - 把它拖到
NetworkCharacterController
的Collider
屬性。
角色配置應該看起來像這樣:
Save
您的項目,讓Fusion創建新的網路對象,然後把它拖到項目文件夾,創建角色預制件,並從場景中刪除該對象。
生成角色
因為遊戲是以托管模式執行的,所以只有主機有權力生成新的對象。這意味著所有玩家的角色都必須在他們加入會話時由主機生成。方便的是,INetworkRunnerCallbacks
接口的OnPlayerJoined
方法正是在這個場合被呼叫。
同樣,當玩家斷開連接時,以OnPlayerLeft
方法呼叫。
用以下代碼替換空的OnPlayerJoined
和OnPlayerLeft
存根:
C#
[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編輯器,將創建的預制角色拖放到BasicSpawner
的Player Prefab
區域。
收集輸入
擁有輸入權限並不允許客戶端直接修改對象的網路狀態。相反,它可以提供一個輸入結構,然後由主機來解釋,以更新網路狀態。
客戶端也可以在本地應用輸入,向用戶提供即時反饋,但這只是一個本地預測,可能被主機推翻。
在收集用戶的輸入之前,必須定義一個數據結構來保存輸入。創建一個名為NetworkInputData.cs
的新文件,其中的結構如下:
C#
using Fusion;
using UnityEngine;
public struct NetworkInputData : INetworkInput
{
public Vector3 direction;
}
為了簡單起見,本例使用一個向量來表示所需的運動方向,但要知道,還有一些頻寬成本較低的方法可以做到這一點。例如,一個位元區域,每個方向有一個位元。請注意,Fusion將壓縮輸入,只發送實際變化的數據,所以不要過早地進行瘋狂的優化。
當Fusion在OnInput被拉至呼叫返回時,客戶端需要收集用戶的輸入,所以回到BasicSpawner.cs
中,用下面的內容替換OnInput()
存根:
C#
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會將其提供給主機和該客戶端有輸入權限的任何對象。
應用輸入
最後一步是將收集到的輸入數據應用於玩家的角色。
- 選擇
PlayerPrefab
- 添加一個新的腳本組件,叫做
Player
。 - 打開新的腳本,用NetworkBehaviour替換MonoBehaviour。
- 置入
FixedUpdateNetwork()
,這樣該行為就可以參與Fusion 模擬循環:
C#
using Fusion;
public class Player : NetworkBehaviour
{
public override void FixedUpdateNetwork(){}
}
FixedUpdateNetwork會在每個模擬時間段被呼叫。這可能會在每個渲染幀中發生多次,因為Fusion會應用一個較早的確認的網路狀態,然後從該tick一直到當前(預測的)本地tick進行重新模擬。
輸入應該在FixedUpdateNetwork
中應用,以確保每個tick應用正確的輸入。Fusion提供了一個簡單的方法來獲取相關tick的輸入,恰當地命名為GetInput()
。一旦獲得了輸入,就會呼叫NetworkCharacterController
來將實際的運動應用到角色轉換上。
完整的Player
類別看起來像以下:
C#
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);
}
}
}
注意所提供的輸入被規範化了,以防止作弊。
測試
現在剩下的就是驗証遊戲確實有可移動的玩家角色,但首先場景需要一個地板,這樣生成的物體才不會直接掉到視野之外。
創建一個立方體對象,把它放在(0,-1,0)
處,並在X和Z方向縮放100。
建立應用程式並啟動多個事件(或直接從Unity中執行其中一個)。確保其中一個客戶端按下Host
按鈕,其他客戶端按下Join
。
關於從Unity內部執行的一個重要說明。在編輯器中執行遊戲時,幀率可能非常不穩定,因為編輯器本身需要偶爾進行的渲染。這影響了Fusions正確預測時間的能力,並可能導致小量的抖動,而這些抖動在構建的應用程式中是不會發生的。如果有疑問,可以嘗試執行兩個獨立的構建。
Back to top