Skip to content
1 change: 1 addition & 0 deletions com.unity.netcode.gameobjects/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Additional documentation and release notes are available at [Multiplayer Documen

### Added

- Single player session section to provide users with information about `SinglePlayerTransport` and an example script of how to switch between single and multi player sessions. (#4062)

### Changed

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
* [Connection approval](basics/connection-approval.md)
* [Max players](basics/maxnumberplayers.md)
* [Transports](advanced-topics/transports.md)
* [Single player sessions](advanced-topics/singleplayer.md)
* [Unity Relay](relay/relay.md)
* [Command-line arguments](command-line-arguments.md)
* [Network components](network-components.md)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Single player sessions

Netcode for GameObjects provides a [SinglePlayerTransport](https://docs.unity3d.com/Packages/com.unity.netcode.gameobjects@2.13/api/Unity.Netcode.Transports.SinglePlayer.SinglePlayerTransport.html) which derives from [NetworkTransport](https://docs.unity3d.com/Packages/com.unity.netcode.gameobjects@2.13/api/Unity.Netcode.NetworkTransport.html).

This provides the ability to run a hosted session using the single player transport without having to modify your primary netcode script.

## Adding the single player transport

- Add the `SinglePlayerTransport` to your NetworkManager.
- You can create a custom `MonoBehaviour` component to handle your connection flow or you can derive from `NetworkManager` and add additional methods/logic to handle starting a single player session or multiplayer session.
- When starting a single player session, prior to starting the NetworkManager as a host (_required_), you will want to assign the `SinglePlayerTransport` to the `NetworkManager.NetworkConfig.NetworkTransport`.
- When starting a multiplayer session, prior to starting the NetworkManager, you will want to assign the `UnityTransport` (_or any other `NetworkTransport` derived class that you might use for multiplayer sessions_) to the `NetworkManager.NetworkConfig.NetworkTransport`.

## Example script

Below is an example component script that provides a single method to start a single or multi player session:

[!code-cs[](../../Tests/Runtime/DocumentationCodeSamples/Configuration/SinglePlayerSessions.cs#SinglePlayerTransportExample)]

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
using System.Collections;
using System.Text;
using NUnit.Framework;
using Unity.Netcode;
using Unity.Netcode.TestHelpers.Runtime;
using Unity.Netcode.Transports.SinglePlayer;
using Unity.Netcode.Transports.UTP;
using UnityEngine;
using UnityEngine.TestTools;

namespace DocumentationCodeSamples
{
#region SinglePlayerTransportExample
/// <summary>
/// Example of how to start a single player or multiplayer session.
/// Place this on your NetworkManager's GameObject.
/// </summary>
internal class SwitchingTransportTypesExample : MonoBehaviour
{
public enum StartType
{
SinglePlayer,
Client,
Host,
Server
}

private UnityTransport m_UnityTransport;
private SinglePlayerTransport m_SinglePlayerTransport;
private NetworkManager m_NetworkManager;

private void Awake()
{
m_UnityTransport = GetComponent<UnityTransport>();
m_SinglePlayerTransport = GetComponent<SinglePlayerTransport>();
m_NetworkManager = GetComponent<NetworkManager>();
}

public bool StartSession(StartType startType)
{
var startStatus = false;
// Set the transport to use before starting
m_NetworkManager.NetworkConfig.NetworkTransport = startType == StartType.SinglePlayer ? m_SinglePlayerTransport : m_UnityTransport;
switch (startType)
{
case StartType.Host:
case StartType.SinglePlayer:
{
// Starting a host or single player is the same
startStatus = m_NetworkManager.StartHost();
break;
}
case StartType.Server:
{
startStatus = m_NetworkManager.StartServer();
break;
}
case StartType.Client:
{
startStatus = m_NetworkManager.StartClient();
break;
}
}
return startStatus;
}
}
#endregion

internal class VerifyNetcodeSessionActive : NetworkBehaviour
{
public bool RpcReceived { get; private set; }

public bool TestNetworkVariableEvent { get; private set; }
public NetworkVariable<bool> TestNetworkVariable = new NetworkVariable<bool>(false, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner);

[Rpc(SendTo.Everyone, InvokePermission = RpcInvokePermission.Owner)]
private void VerifyRpc(RpcParams rpcParams = default)
{
RpcReceived = true;
}

public void TestConnectivity()
{
if (IsOwner)
{
VerifyRpc();
TestNetworkVariable.Value = true;
}
}

protected override void OnNetworkPostSpawn()
{
TestNetworkVariable.OnValueChanged += TestNetworkVariableChanged;
base.OnNetworkPostSpawn();
}

private void TestNetworkVariableChanged(bool previous, bool current)
{
TestNetworkVariableEvent = true;
}
}

[TestFixture(SessionType.SinglePlayer)]
[TestFixture(SessionType.MultiPlayer)]
internal class SwitchingTransportTypesTests : NetcodeIntegrationTest
{
public enum SessionType
{
SinglePlayer,
MultiPlayer
}

protected override int NumberOfClients => 0;

// We do not need to test this against CMB.
protected override bool UseCMBService()
{
return false;
}

private SessionType m_SessionType;
public SwitchingTransportTypesTests(SessionType sessionType)
{
m_SessionType = sessionType;
}

protected override IEnumerator OnSetup()
{
m_CanStart = false;
return base.OnSetup();
}

protected override void OnCreatePlayerPrefab()
{
m_PlayerPrefab.AddComponent<VerifyNetcodeSessionActive>();
base.OnCreatePlayerPrefab();
}

private bool m_CanStart = false;
protected override bool CanStartServerAndClients()
{
return m_CanStart;
}

private NetworkManager m_Client;
protected override void OnNewClientCreated(NetworkManager networkManager)
{
m_Client = networkManager;
networkManager.NetworkConfig.EnableSceneManagement = false;
base.OnNewClientCreated(networkManager);
}

[UnityTest]
public IEnumerator SwitchTransportTest()
{
var authority = GetAuthorityNetworkManager();
authority.NetworkConfig.EnableSceneManagement = false;
var startType = SwitchingTransportTypesExample.StartType.Host;
if (m_SessionType == SessionType.SinglePlayer)
{
startType = SwitchingTransportTypesExample.StartType.SinglePlayer;
authority.gameObject.AddComponent<SinglePlayerTransport>();
}
m_CanStart = true;
var example = authority.gameObject.AddComponent<SwitchingTransportTypesExample>();
Assert.IsTrue(example.StartSession(startType), "Failed to start single player session!");

if (m_SessionType != SessionType.SinglePlayer)
{
yield return CreateAndStartNewClient();
AssertOnTimeout("Timed out waiting for client to start and connect!");
}

var verifyNetcode = authority.LocalClient.PlayerObject.GetComponent<VerifyNetcodeSessionActive>();
verifyNetcode.TestConnectivity();
if (m_SessionType != SessionType.SinglePlayer)
{
m_Client.LocalClient.PlayerObject.GetComponent<VerifyNetcodeSessionActive>().TestConnectivity();
}

yield return WaitForConditionOrTimeOut(VerifyNetcodeSession);
AssertOnTimeout($"Single player session had netcode related errors:");
}

/// <summary>
/// Verifies that all spawned players received the RPC and NetworkVariable
/// changed event.
/// </summary>
private bool VerifyNetcodeSession(StringBuilder errorLog)
{
foreach (var networkManager in m_NetworkManagers)
{
foreach (var networkObject in networkManager.SpawnManager.SpawnedObjectsList)
{
if (!networkObject.IsPlayerObject)
{
continue;
}
var verify = networkObject.gameObject.GetComponent<VerifyNetcodeSessionActive>();

if (!verify.RpcReceived)
{
errorLog.AppendLine($"[Client-{networkManager.LocalClientId}][{verify.name}] Rpc was not recieved!");
}

if (!verify.TestNetworkVariableEvent)
{
errorLog.AppendLine($"[Client-{networkManager.LocalClientId}][{verify.name}] NetworkVariable.OnValueChanged did not get invoked!");
}

if (!verify.TestNetworkVariable.Value)
{
errorLog.AppendLine($"[Client-{networkManager.LocalClientId}][{verify.name}] {nameof(VerifyNetcodeSessionActive.TestNetworkVariable)} did not get set!");
}
}
}
return errorLog.Length == 0;
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading