Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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.Transports.SinglePlayer;
using Unity.Netcode.TestHelpers.Runtime;
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.