Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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

- Added `SinglePlayerTransport` that provides the ability to start as a host for a single player network session. (#3473)
- When using UnityTransport >=2.4 and Unity >= 6000.1.0a1, SetConnectionData will accept a fully qualified hostname instead of an IP as a connect address on the client side. (#3441)

### Fixed
Expand Down

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,147 @@
using System;
using System.Collections.Generic;

namespace Unity.Netcode.Transports.SinglePlayer
{
/// <summary>
/// A transport that can be used to run a Netcode for GameObjects session in "single player" mode
/// by assigning this transport to the <see cref="NetworkConfig.NetworkTransport"/> property before
/// starting as a host.
/// </summary>
/// <remarks>
/// You can only start as a host when using this transport.
/// </remarks>
public class SinglePlayerTransport : NetworkTransport
{
/// <inheritdoc/>
public override ulong ServerClientId { get; } = 0;

internal static string NotStartingAsHostErrorMessage = $"When using {nameof(SinglePlayerTransport)}, you must start a hosted session so both client and server are available locally.";

private struct MessageData
{
public ulong FromClientId;
public ArraySegment<byte> Payload;
public NetworkEvent Event;
public float AvailableTime;
}

private static Dictionary<ulong, Queue<MessageData>> s_MessageQueue = new Dictionary<ulong, Queue<MessageData>>();

private bool m_Initialized;
private ulong m_TransportId = 0;
private NetworkManager m_NetworkManager;


/// <inheritdoc/>
public override void Send(ulong clientId, ArraySegment<byte> payload, NetworkDelivery networkDelivery)
{
var copy = new byte[payload.Array.Length];
Array.Copy(payload.Array, copy, payload.Array.Length);
s_MessageQueue[clientId].Enqueue(new MessageData
{
FromClientId = m_TransportId,
Payload = new ArraySegment<byte>(copy, payload.Offset, payload.Count),
Event = NetworkEvent.Data,
AvailableTime = (float)m_NetworkManager.LocalTime.FixedTime,
});
}

/// <inheritdoc/>
public override NetworkEvent PollEvent(out ulong clientId, out ArraySegment<byte> payload, out float receiveTime)
{
if (s_MessageQueue[m_TransportId].Count > 0)
{
var data = s_MessageQueue[m_TransportId].Peek();
if (data.AvailableTime > m_NetworkManager.LocalTime.FixedTime)
{
clientId = 0;
payload = new ArraySegment<byte>();
receiveTime = 0;
return NetworkEvent.Nothing;
}

s_MessageQueue[m_TransportId].Dequeue();
clientId = data.FromClientId;
payload = data.Payload;
receiveTime = m_NetworkManager.LocalTime.TimeAsFloat;
if (m_NetworkManager.IsServer && data.Event == NetworkEvent.Connect)
{
s_MessageQueue[data.FromClientId].Enqueue(new MessageData { Event = NetworkEvent.Connect, FromClientId = ServerClientId, Payload = new ArraySegment<byte>() });
}
return data.Event;
}
clientId = 0;
payload = new ArraySegment<byte>();
receiveTime = 0;
return NetworkEvent.Nothing;
}

/// <inheritdoc/>
/// <remarks>
/// This will always return false for <see cref="SinglePlayerTransport"/>.
/// Always use <see cref="StartServer"/>.
/// </remarks>
public override bool StartClient()
{
NetworkLog.LogError(NotStartingAsHostErrorMessage);
return false;
}

/// <inheritdoc/>
/// <remarks>
/// Always use <see cref="NetworkManager.StartHost"/> when hosting a local single player session.
/// </remarks>
public override bool StartServer()
{
s_MessageQueue[ServerClientId] = new Queue<MessageData>();
if (!m_NetworkManager.LocalClient.IsHost && m_NetworkManager.LocalClient.IsServer)
{
NetworkLog.LogError(NotStartingAsHostErrorMessage);
return false;
}
return true;
}

/// <inheritdoc/>
public override void DisconnectRemoteClient(ulong clientId)
{
s_MessageQueue[clientId].Enqueue(new MessageData { Event = NetworkEvent.Disconnect, FromClientId = m_TransportId, Payload = new ArraySegment<byte>() });
}

/// <inheritdoc/>
public override void DisconnectLocalClient()
{
s_MessageQueue[ServerClientId].Enqueue(new MessageData { Event = NetworkEvent.Disconnect, FromClientId = m_TransportId, Payload = new ArraySegment<byte>() });
}

/// <inheritdoc/>
/// <remarks>
/// Will always return 0 since this transport is for a local single player session.
/// </remarks>
public override ulong GetCurrentRtt(ulong clientId)
{
return 0;
}

/// <inheritdoc/>
public override void Shutdown()
{
s_MessageQueue.Clear();
m_TransportId = 0;
}

/// <inheritdoc/>
protected override NetworkTopologyTypes OnCurrentTopology()
{
return m_NetworkManager != null ? m_NetworkManager.NetworkConfig.NetworkTopology : NetworkTopologyTypes.ClientServer;
}

/// <inheritdoc/>
public override void Initialize(NetworkManager networkManager = null)
{
s_MessageQueue.Clear();
m_NetworkManager = networkManager;
}
}
}

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,211 @@
using System;
using System.Collections;
using System.Collections.Generic;
using NUnit.Framework;
using Unity.Netcode.TestHelpers.Runtime;
using Unity.Netcode.Transports.SinglePlayer;
using UnityEngine;
using UnityEngine.TestTools;
using Random = UnityEngine.Random;

namespace Unity.Netcode.RuntimeTests
{
internal class SinglePlayerTransportTests : NetcodeIntegrationTest
{
protected override int NumberOfClients => 0;

public struct SerializableStruct : INetworkSerializable, IEquatable<SerializableStruct>
{
public bool BoolValue;
public ulong ULongValue;

public bool Equals(SerializableStruct other)
{
return other.BoolValue == BoolValue && other.ULongValue == ULongValue;
}

public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
{
serializer.SerializeValue(ref BoolValue);
serializer.SerializeValue(ref ULongValue);
}
}

public class SinglePlayerTestComponent : NetworkBehaviour
{
private enum SpawnStates
{
PreSpawn,
Spawn,
PostSpawn,
}

private enum RpcInvocations
{
SendToServerRpc,
SendToEveryoneRpc,
SendToOwnerRpc,
}

private Dictionary<SpawnStates, int> m_SpawnStateInvoked = new Dictionary<SpawnStates, int>();
private Dictionary<RpcInvocations, int> m_RpcInvocations = new Dictionary<RpcInvocations, int>();
private NetworkVariable<int> m_IntValue = new NetworkVariable<int>();
private NetworkVariable<SerializableStruct> m_SerializableValue = new NetworkVariable<SerializableStruct>();


private void SpawnStateInvoked(SpawnStates spawnState)
{
if (!m_SpawnStateInvoked.ContainsKey(spawnState))
{
m_SpawnStateInvoked.Add(spawnState, 1);
}
else
{
m_SpawnStateInvoked[spawnState]++;
}
}

private void RpcInvoked(RpcInvocations rpcInvocation)
{
if (!m_RpcInvocations.ContainsKey(rpcInvocation))
{
m_RpcInvocations.Add(rpcInvocation, 1);
}
else
{
m_RpcInvocations[rpcInvocation]++;
}
}

private void ValidateValues(int someIntValue, SerializableStruct someValues)
{
Assert.IsTrue(m_IntValue.Value == someIntValue);
Assert.IsTrue(someValues.BoolValue == m_SerializableValue.Value.BoolValue);
Assert.IsTrue(someValues.ULongValue == m_SerializableValue.Value.ULongValue);
}

[Rpc(SendTo.Server)]
private void SendToServerRpc(int someIntValue, SerializableStruct someValues, RpcParams rpcParams = default)
{
ValidateValues(someIntValue, someValues);
RpcInvoked(RpcInvocations.SendToServerRpc);
}

[Rpc(SendTo.Everyone)]
private void SendToEveryoneRpc(int someIntValue, SerializableStruct someValues, RpcParams rpcParams = default)
{
ValidateValues(someIntValue, someValues);
RpcInvoked(RpcInvocations.SendToEveryoneRpc);
}

[Rpc(SendTo.Owner)]
private void SendToOwnerRpc(int someIntValue, SerializableStruct someValues, RpcParams rpcParams = default)
{
ValidateValues(someIntValue, someValues);
RpcInvoked(RpcInvocations.SendToOwnerRpc);
}


protected override void OnNetworkPreSpawn(ref NetworkManager networkManager)
{
SpawnStateInvoked(SpawnStates.PreSpawn);
base.OnNetworkPreSpawn(ref networkManager);
}

public override void OnNetworkSpawn()
{
SpawnStateInvoked(SpawnStates.Spawn);
m_IntValue.Value = Random.Range(0, 100);
m_SerializableValue.Value = new SerializableStruct()
{
BoolValue = Random.Range(0, 100) >= 50.0 ? true : false,
ULongValue = (ulong)Random.Range(0, 100000),
};
base.OnNetworkSpawn();
}

protected override void OnNetworkPostSpawn()
{
SpawnStateInvoked(SpawnStates.PostSpawn);
SendToServerRpc(m_IntValue.Value, m_SerializableValue.Value);
SendToEveryoneRpc(m_IntValue.Value, m_SerializableValue.Value);
SendToOwnerRpc(m_IntValue.Value, m_SerializableValue.Value);
base.OnNetworkPostSpawn();
}

public void ValidateStatesAndRpcInvocations()
{
foreach (var entry in m_SpawnStateInvoked)
{
Assert.True(entry.Value == 1, $"{entry.Key} failed with {entry.Value} invocations!");
}
foreach (var entry in m_RpcInvocations)
{
Assert.True(entry.Value == 1, $"{entry.Key} failed with {entry.Value} invocations!");
}
}
}

private GameObject m_PrefabToSpawn;
private bool m_CanStartHost;

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

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

protected override void OnServerAndClientsCreated()
{
var singlePlayerTransport = m_ServerNetworkManager.gameObject.AddComponent<SinglePlayerTransport>();
m_ServerNetworkManager.NetworkConfig.NetworkTransport = singlePlayerTransport;
m_PrefabToSpawn = CreateNetworkObjectPrefab("TestObject");
m_PrefabToSpawn.AddComponent<SinglePlayerTestComponent>();
base.OnServerAndClientsCreated();
}

protected override bool CanStartServerAndClients()
{
return m_CanStartHost;
}

[UnityTest]
public IEnumerator StartSinglePlayerAndSpawn()
{
m_CanStartHost = true;

yield return StartServerAndClients();

var spawnedInstance = SpawnObject(m_PrefabToSpawn, m_ServerNetworkManager).GetComponent<NetworkObject>();
var testComponent = spawnedInstance.GetComponent<SinglePlayerTestComponent>();
yield return s_DefaultWaitForTick;
var playerTestComponent = m_ServerNetworkManager.LocalClient.PlayerObject.GetComponent<SinglePlayerTestComponent>();
testComponent.ValidateStatesAndRpcInvocations();
playerTestComponent.ValidateStatesAndRpcInvocations();
}

[UnityTest]
public IEnumerator StartSinglePlayerAsClientError()
{
LogAssert.Expect(LogType.Error, $"[Netcode] {SinglePlayerTransport.NotStartingAsHostErrorMessage}");
LogAssert.Expect(LogType.Error, $"[Netcode] Client is shutting down due to network transport start failure of {nameof(SinglePlayerTransport)}!");
Assert.IsFalse(m_ServerNetworkManager.StartClient());
yield return null;
}

[UnityTest]
public IEnumerator StartSinglePlayerAsServerError()
{
LogAssert.Expect(LogType.Error, $"[Netcode] {SinglePlayerTransport.NotStartingAsHostErrorMessage}");
LogAssert.Expect(LogType.Error, $"[Netcode] Server is shutting down due to network transport start failure of {nameof(SinglePlayerTransport)}!");
Assert.IsFalse(m_ServerNetworkManager.StartServer());
yield return null;
}
}
}

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