2 - Setting Up A Scene
Overview
Fusion 102 will explain how to set up a basic networked scene.
At the end of this section, the project will contain:
- a basic Fusion Runner to start or join a match, and
- a character that you can move around
Consult the Manual for an in-depth description of Network Input
Launching Fusion
To launch Fusion, the StartGame
method needs to be called on the Fusion NetworkRunner
, so the application must either have this component in the scene or add it from code. This tutorial will be code centric because most networking logic requires some amount of code to be written regardless.
With the default scene open in Unity
- Create a new empty GameObject
- Add a new script component to it.
- Name the script
BasicSpawner
. - Open the script and add the
INetworkRunnerCallbacks
interface to theBasicSpawner
class, along with stubs for all the required methods:
C#
using Fusion;
using Fusion.Sockets;
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class BasicSpawner : MonoBehaviour, INetworkRunnerCallbacks
{
public void OnPlayerJoined(NetworkRunner runner, PlayerRef player) { }
public void OnPlayerLeft(NetworkRunner runner, PlayerRef player) { }
public void OnInput(NetworkRunner runner, NetworkInput input) { }
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, NetDisconnectReason reason) { }
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 OnHostMigration(NetworkRunner runner, HostMigrationToken hostMigrationToken) { }
public void OnSceneLoadDone(NetworkRunner runner) { }
public void OnSceneLoadStart(NetworkRunner runner) { }
public void OnObjectExitAOI(NetworkRunner runner, NetworkObject obj, PlayerRef player){ }
public void OnObjectEnterAOI(NetworkRunner runner, NetworkObject obj, PlayerRef player){ }
public void OnReliableDataReceived(NetworkRunner runner, PlayerRef player, ReliableKey key, ArraySegment<byte> data){ }
public void OnReliableDataProgress(NetworkRunner runner, PlayerRef player, ReliableKey key, float progress){ }
}
Implementing INetworkRunnerCallbacks
will allow Fusions NetworkRunner
to interact with the BasicSpawner
class. The NetworkRunner
is the heart and soul of Fusion and runs the actual network simulation.
The NetworkRunner
will automatically detect that the BasicSpawner
implements INetworkRunnerCallbacks
and call its methods because it will be added to the same game object in the StartGame
method.
Add the following to the BasicSpawner
script:
C#
private NetworkRunner _runner;
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;
// Create the NetworkSceneInfo from the current scene
var scene = SceneRef.FromIndex(SceneManager.GetActiveScene().buildIndex);
var sceneInfo = new NetworkSceneInfo();
if (scene.IsValid) {
sceneInfo.AddSceneRef(scene, LoadSceneMode.Additive);
}
// Start or join (depends on gamemode) a session with a specific name
await _runner.StartGame(new StartGameArgs()
{
GameMode = mode,
SessionName = "TestRoom",
Scene = scene,
SceneManager = gameObject.AddComponent<NetworkSceneManagerDefault>()
});
}
The StartGame
method first creates the Fusion NetworkRunner
and lets it know that this client will be providing input. It then starts a new session with a hardcoded name and the specified game mode (more on game modes in a second). The current scene index is passed in, but this is only relevant for the host as clients will be forced to use the scene specified by the host. Finally a default SceneManager is specified for good measure. The SceneManager handles instantiation of NetworkObjects that are placed directly in the scene and, strictly speaking, isn't needed in this example because there are no such objects.
Fusion supports several network topologies, but this introduction will focus on Hosted Mode
. In hosted mode, one network peer is both server and client and creates the network session, while the remaining peers are just clients joining the existing session.
To accommodate this, there needs to be a way for the user to choose if they are hosting the game or joining an existing session - for simplicity, the following example uses Unity IMGUI. Add the following method to BasicSpawner.cs
:
C#
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);
}
}
}
Running this application will allow a user to host a new session and allow additional instances to join that session, but since there is no interaction and no data being transferred it will look like a single player experience.
Creating a Player Avatar
In order for this to become a game, each player must be given a way to provide input and have a presence in the world such as a player avatar.
In the Unity editor,
- Create a new empty GameObject named
PlayerPrefab
- Add a
NetworkObject
component to it.
This will give it network identity so that all peers can reference it. Since the user will be controlling this avatar, it will also need a NetworkCharacterController
- it is not a requirement, but it is a good starting point for most player controlled objects, so go ahead and add that as well. The NetworkCharacterController
requires the default Unity Character Controller
component which will be automatically added.
It is generally advisable to separate network objects from their visual representations. To achieve this,
- Add a standard Unity
Cube
as a child object to thePlayerPrefab
- Rename it
Body
- Remove the
Collider
The avatar configuration should look like this:
Save
your project to have Fusion bake the new Network object, then drag it to the project folder to create the avatar prefab, and delete the object from the scene.
Spawning the Avatar
Because the game is running in hosted mode, only the host has authority to spawn new objects. This means that all player avatars must be spawned by the host when they join the session. Conveniently, the OnPlayerJoined
method of the INetworkRunnerCallbacks
interface is called on this exact occasion.
Similarly, when a player disconnects, the OnPlayerLeft
method is called.
Replace the empty OnPlayerJoined
and OnPlayerLeft
stubs with the following code:
C#
[SerializeField] private NetworkPrefabRef _playerPrefab;
private Dictionary<PlayerRef, NetworkObject> _spawnedCharacters = new Dictionary<PlayerRef, NetworkObject>();
public void OnPlayerJoined(NetworkRunner runner, PlayerRef player)
{
if (runner.IsServer)
{
// Create a unique position for the player
Vector3 spawnPosition = new Vector3((player.RawEncoded % runner.Config.Simulation.PlayerCount) * 3, 1, 0);
NetworkObject networkPlayerObject = runner.Spawn(_playerPrefab, spawnPosition, Quaternion.identity, player);
// Keep track of the player avatars for easy access
_spawnedCharacters.Add(player, networkPlayerObject);
}
}
public void OnPlayerLeft(NetworkRunner runner, PlayerRef player)
{
if (_spawnedCharacters.TryGetValue(player, out NetworkObject networkObject))
{
runner.Despawn(networkObject);
_spawnedCharacters.Remove(player);
}
}
This should all look quite familiar as it essentially replaces Unity's Instantiate()
method with runner.Spawn()
which takes a similar set of parameters, except for the last one. The last parameter is a reference to the player that will be allowed to provide input for the avatar - it is important to note that this is not the same as "owning" the object. More on this in a second.
Don't forget to go back to the Unity Editor and drag and drop the created prefab avatar into the Player Prefab
field of the BasicSpawner
.
Collecting Input
Having Input Authority does not allow a client to directly modify the network state of the object. Instead, it may provide an input structure that the host will then interpret in order to update the network state.
The client may also apply the input locally to provide instant feedback to the user, but this is just a local prediction which may be overruled by the host.
Before input can be collected from the user, a data structure must be defined to hold the input. Create a new file named NetworkInputData.cs
with the following struct in it:
C#
using Fusion;
using UnityEngine;
public struct NetworkInputData : INetworkInput
{
public Vector3 direction;
}
For simplicity this examples uses a vector to indicate desired movement direction, but know that there are less bandwidth expensive ways of doing this. For example a bitfield with one bit per direction. Do note that Fusion will compress the input and only send data that actually changes, so don't go too crazy with premature optimization.
The client needs to collect input from the user when polled by Fusion in the OnInput callback, so go back to the BasicSpawner.cs
and replace the OnInput()
stub with the following:
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);
}
Again, this should look quite familiar - the handler is using standard Unity input to collect and store input from the local client in the structure that was defined previously. The last line of this method passes the pre-filled input structure to Fusion which will then make it available to the host and any object this client have Input Authority over.
Applying Input
The final step is to apply the collected input data to the player avatar.
- Select the
PlayerPrefab
- Add a new script component called
Player
to it. - Open the new script and replace MonoBehaviour with NetworkBehaviour
- Implement
FixedUpdateNetwork()
so the behaviour can participate in the Fusion simulation loop:
C#
using Fusion;
public class Player : NetworkBehaviour
{
public override void FixedUpdateNetwork(){}
}
FixedUpdateNetwork gets called on every simulation tick. This can happen multiple times per rendering frame as Fusion applies an older confirmed network state and then re-simulates from that tick all the way up to the currently (predicted) local tick.
Input should be applied in FixedUpdateNetwork
to make sure that the correct input is applied for each tick. Fusion provides a simple method to acquire the input for the relevant tick, aptly named GetInput()
. Once the input has been acquired, the NetworkCharacterController
is called to apply the actual movement to the avatar transform.
The complete Player
class looks like this:
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);
}
}
}
Note the provided input is normalized to prevent cheating.
Testing
Now all that is left is to verify that the game does indeed have moveable player avatars, but first the scene needs a floor so the spawned objects don't just drop out of view.
Create a cube object, place it at (0,-1,0)
, and scale it by 100 in X and Z. Take care to also create and assign a new material so everything isn't the same color.
Build the application and start multiple instances (or run one of them directly from within Unity). Make sure one of the clients presses the Host
button and the rest press Join
.
One important note on running from within Unity: Frame rates can be highly erratic when running the game in the editor simply because the editor itself needs to do occasional rendering. This affects Fusions ability to correctly predict timing and may result in small amounts of jitter that would not happen in a built application. If in doubt, try running two standalone builds.
Next Host Mode Basics 3 - Prediction
Back to top