com.unity.netcode.gameobjects@1.0.0-pre.6

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

Additional documentation and release notes are available at [Multiplayer Documentation](https://docs-multiplayer.unity3d.com).

## [1.0.0-pre.6] - 2022-03-02

### Added
- NetworkAnimator now properly synchrhonizes all animation layers as well as runtime-adjusted weighting between them (#1765)
- Added first set of tests for NetworkAnimator - parameter syncing, trigger set / reset, override network animator (#1735)

### Changed

### Fixed
- Fixed an issue where sometimes the first client to connect to the server could see messages from the server as coming from itself. (#1683)
- Fixed an issue where clients seemed to be able to send messages to ClientId 1, but these messages would actually still go to the server (id 0) instead of that client. (#1683)
- Improved clarity of error messaging when a client attempts to send a message to a destination other than the server, which isn't allowed. (#1683)
- Disallowed async keyword in RPCs (#1681)
- Fixed an issue where Alpha release versions of Unity (version 2022.2.0a5 and later) will not compile due to the UNet Transport no longer existing (#1678)
- Fixed messages larger than 64k being written with incorrectly truncated message size in header (#1686) (credit: @kaen)
- Fixed overloading RPC methods causing collisions and failing on IL2CPP targets. (#1694)
- Fixed spawn flow to propagate `IsSceneObject` down to children NetworkObjects, decouple implicit relationship between object spawning & `IsSceneObject` flag (#1685)
- Fixed error when serializing ConnectionApprovalMessage with scene management disabled when one or more objects is hidden via the CheckObjectVisibility delegate (#1720)
- Fixed CheckObjectVisibility delegate not being properly invoked for connecting clients when Scene Management is enabled. (#1680)
- Fixed NetworkList to properly call INetworkSerializable's NetworkSerialize() method (#1682)
- Fixed NetworkVariables containing more than 1300 bytes of data (such as large NetworkLists) no longer cause an OverflowException (the limit on data size is now whatever limit the chosen transport imposes on fragmented NetworkDelivery mechanisms) (#1725)
- Fixed ServerRpcParams and ClientRpcParams must be the last parameter of an RPC in order to function properly. Added a compile-time check to ensure this is the case and trigger an error if they're placed elsewhere (#1721)
- Fixed FastBufferReader being created with a length of 1 if provided an input of length 0 (#1724)
- Fixed The NetworkConfig's checksum hash includes the NetworkTick so that clients with a different tickrate than the server are identified and not allowed to connect (#1728)
- Fixed OwnedObjects not being properly modified when using ChangeOwnership (#1731)
- Improved performance in NetworkAnimator (#1735)
- Removed the "always sync" network animator (aka "autosend") parameters (#1746)
This commit is contained in:
Unity Technologies
2022-03-02 00:00:00 +00:00
parent 4818405514
commit 5b4aaa8b59
205 changed files with 6971 additions and 2722 deletions

8
TestHelpers/Runtime.meta Normal file
View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: e9af0202c9057c944b67aad6e4cdac96
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,6 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("TestProject.EditorTests")]
[assembly: InternalsVisibleTo("TestProject.RuntimeTests")]
[assembly: InternalsVisibleTo("TestProject.ToolsIntegration.RuntimeTests")]
[assembly: InternalsVisibleTo("Unity.Netcode.RuntimeTests")]

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 10ca1ce26995e754599c9eedc2c228d8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 19fbc3f43e13a9144a9c66c68a1c43c1
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,93 @@
namespace Unity.Netcode.TestHelpers.Runtime
{
public class ObjectNameIdentifier : NetworkBehaviour
{
private ulong m_CurrentOwner;
private ulong m_CurrentNetworkObjectId;
private bool m_IsRegistered;
/// <summary>
/// Keep a reference to the assigned NetworkObject
/// <see cref="OnDestroy"/>
/// </summary>
private NetworkObject m_NetworkObject;
public override void OnNetworkSpawn()
{
RegisterAndLabelNetworkObject();
}
protected void RegisterAndLabelNetworkObject()
{
if (!m_IsRegistered)
{
// This is required otherwise it will try to continue to update the NetworkBehaviour even if
// it has been destroyed.
m_NetworkObject = NetworkObject;
m_CurrentOwner = OwnerClientId;
m_CurrentNetworkObjectId = NetworkObjectId;
var objectOriginalName = gameObject.name.Replace("(Clone)", "");
var serverOrClient = IsServer ? "Server" : "Client";
if (NetworkObject.IsPlayerObject)
{
gameObject.name = NetworkManager.LocalClientId == OwnerClientId ? $"{objectOriginalName}({OwnerClientId})-Local{objectOriginalName}" :
$"{objectOriginalName}({OwnerClientId})-On{serverOrClient}({NetworkManager.LocalClientId})";
}
else
{
gameObject.name = $"{objectOriginalName}({NetworkObjectId})-On{serverOrClient}({NetworkManager.LocalClientId})";
}
// Don't add the player objects to the global list of NetworkObjects
if (!NetworkObject.IsPlayerObject)
{
NetcodeIntegrationTest.RegisterNetworkObject(NetworkObject);
}
m_IsRegistered = true;
}
}
protected void DeRegisterNetworkObject()
{
if (m_IsRegistered)
{
NetcodeIntegrationTest.DeregisterNetworkObject(m_CurrentOwner, m_CurrentNetworkObjectId);
m_IsRegistered = false;
}
}
public override void OnLostOwnership()
{
DeRegisterNetworkObject();
RegisterAndLabelNetworkObject();
}
public override void OnGainedOwnership()
{
DeRegisterNetworkObject();
RegisterAndLabelNetworkObject();
}
public override void OnNetworkDespawn()
{
DeRegisterNetworkObject();
}
public override void OnDestroy()
{
if (m_NetworkObject != null)
{
DeRegisterNetworkObject();
// This is required otherwise it will try to continue to update the NetworkBehaviour even if
// it has been destroyed (most likely integration test specific)
if (m_NetworkObject.ChildNetworkBehaviours != null && m_NetworkObject.ChildNetworkBehaviours.Contains(this))
{
NetworkObject.ChildNetworkBehaviours.Remove(this);
}
m_NetworkObject = null;
}
base.OnDestroy();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a915cfb2e4f748e4f9526a8bf5ee84f2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,60 @@
namespace Unity.Netcode.TestHelpers.Runtime
{
/// <summary>
/// Derive from this class to create your own conditional handling for your <see cref="NetcodeIntegrationTest"/>
/// integration tests when dealing with more complicated scenarios where initializing values, storing state to be
/// used across several integration tests.
/// </summary>
public class ConditionalPredicateBase : IConditionalPredicate
{
private bool m_TimedOut;
public bool TimedOut { get { return m_TimedOut; } }
protected virtual bool OnHasConditionBeenReached()
{
return true;
}
public bool HasConditionBeenReached()
{
return OnHasConditionBeenReached();
}
protected virtual void OnStarted() { }
public void Started()
{
OnStarted();
}
protected virtual void OnFinished() { }
public void Finished(bool timedOut)
{
m_TimedOut = timedOut;
OnFinished();
}
}
public interface IConditionalPredicate
{
/// <summary>
/// Test the conditions of the test to be reached
/// </summary>
bool HasConditionBeenReached();
/// <summary>
/// Wait for condition has started
/// </summary>
void Started();
/// <summary>
/// Wait for condition has finished:
/// Condition(s) met or timed out
/// </summary>
void Finished(bool timedOut);
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: dada6cae693646a4095924917e5e707a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,115 @@
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
using Object = UnityEngine.Object;
namespace Unity.Netcode.TestHelpers.Runtime
{
/// <summary>
/// The default SceneManagerHandler used for all NetcodeIntegrationTest derived children.
/// </summary>
internal class IntegrationTestSceneHandler : ISceneManagerHandler, IDisposable
{
internal CoroutineRunner CoroutineRunner;
// Default client simulated delay time
protected const float k_ClientLoadingSimulatedDelay = 0.02f;
// Controls the client simulated delay time
protected float m_ClientLoadingSimulatedDelay = k_ClientLoadingSimulatedDelay;
public delegate bool CanClientsLoadUnloadDelegateHandler();
public event CanClientsLoadUnloadDelegateHandler CanClientsLoad;
public event CanClientsLoadUnloadDelegateHandler CanClientsUnload;
internal List<Coroutine> CoroutinesRunning = new List<Coroutine>();
/// <summary>
/// Used to control when clients should attempt to fake-load a scene
/// Note: Unit/Integration tests that only use <see cref="NetcodeIntegrationTestHelpers"/>
/// need to subscribe to the CanClientsLoad and CanClientsUnload events
/// in order to control when clients can fake-load.
/// Tests that derive from <see cref="NetcodeIntegrationTest"/> already have integrated
/// support and you can override <see cref="NetcodeIntegrationTest.CanClientsLoad"/> and
/// <see cref="NetcodeIntegrationTest.CanClientsUnload"/>.
/// </summary>
protected bool OnCanClientsLoad()
{
if (CanClientsLoad != null)
{
return CanClientsLoad.Invoke();
}
return true;
}
/// <summary>
/// Fake-Loads a scene for a client
/// </summary>
internal IEnumerator ClientLoadSceneCoroutine(string sceneName, ISceneManagerHandler.SceneEventAction sceneEventAction)
{
yield return new WaitForSeconds(m_ClientLoadingSimulatedDelay);
while (!OnCanClientsLoad())
{
yield return new WaitForSeconds(m_ClientLoadingSimulatedDelay);
}
sceneEventAction.Invoke();
}
protected bool OnCanClientsUnload()
{
if (CanClientsUnload != null)
{
return CanClientsUnload.Invoke();
}
return true;
}
/// <summary>
/// Fake-Unloads a scene for a client
/// </summary>
internal IEnumerator ClientUnloadSceneCoroutine(ISceneManagerHandler.SceneEventAction sceneEventAction)
{
yield return new WaitForSeconds(m_ClientLoadingSimulatedDelay);
while (!OnCanClientsUnload())
{
yield return new WaitForSeconds(m_ClientLoadingSimulatedDelay);
}
sceneEventAction.Invoke();
}
public AsyncOperation LoadSceneAsync(string sceneName, LoadSceneMode loadSceneMode, ISceneManagerHandler.SceneEventAction sceneEventAction)
{
CoroutinesRunning.Add(CoroutineRunner.StartCoroutine(ClientLoadSceneCoroutine(sceneName, sceneEventAction)));
// This is OK to return a "nothing" AsyncOperation since we are simulating client loading
return new AsyncOperation();
}
public AsyncOperation UnloadSceneAsync(Scene scene, ISceneManagerHandler.SceneEventAction sceneEventAction)
{
CoroutinesRunning.Add(CoroutineRunner.StartCoroutine(ClientUnloadSceneCoroutine(sceneEventAction)));
// This is OK to return a "nothing" AsyncOperation since we are simulating client loading
return new AsyncOperation();
}
public IntegrationTestSceneHandler()
{
if (CoroutineRunner == null)
{
CoroutineRunner = new GameObject("UnitTestSceneHandlerCoroutine").AddComponent<CoroutineRunner>();
}
}
public void Dispose()
{
foreach (var coroutine in CoroutinesRunning)
{
CoroutineRunner.StopCoroutine(coroutine);
}
CoroutineRunner.StopAllCoroutines();
Object.Destroy(CoroutineRunner.gameObject);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 384935cc0ae40d641910e4c3924038c6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: ebacdb7d8cb876a43b4a908dd6d83aa9
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,66 @@
#if MULTIPLAYER_TOOLS
using System.Collections;
namespace Unity.Netcode.TestHelpers.Runtime.Metrics
{
internal abstract class SingleClientMetricTestBase : NetcodeIntegrationTest
{
protected override int NumberOfClients => 1;
internal NetworkManager Server { get; private set; }
internal NetworkMetrics ServerMetrics { get; private set; }
internal NetworkManager Client { get; private set; }
internal NetworkMetrics ClientMetrics { get; private set; }
protected override void OnServerAndClientsCreated()
{
Server = m_ServerNetworkManager;
Client = m_ClientNetworkManagers[0];
base.OnServerAndClientsCreated();
}
protected override IEnumerator OnStartedServerAndClients()
{
ServerMetrics = Server.NetworkMetrics as NetworkMetrics;
ClientMetrics = Client.NetworkMetrics as NetworkMetrics;
yield return base.OnStartedServerAndClients();
}
}
public abstract class DualClientMetricTestBase : NetcodeIntegrationTest
{
protected override int NumberOfClients => 2;
internal NetworkManager Server { get; private set; }
internal NetworkMetrics ServerMetrics { get; private set; }
internal NetworkManager FirstClient { get; private set; }
internal NetworkMetrics FirstClientMetrics { get; private set; }
internal NetworkManager SecondClient { get; private set; }
internal NetworkMetrics SecondClientMetrics { get; private set; }
protected override void OnServerAndClientsCreated()
{
Server = m_ServerNetworkManager;
FirstClient = m_ClientNetworkManagers[0];
SecondClient = m_ClientNetworkManagers[1];
base.OnServerAndClientsCreated();
}
protected override IEnumerator OnStartedServerAndClients()
{
ServerMetrics = Server.NetworkMetrics as NetworkMetrics;
FirstClientMetrics = FirstClient.NetworkMetrics as NetworkMetrics;
SecondClientMetrics = SecondClient.NetworkMetrics as NetworkMetrics;
yield return base.OnStartedServerAndClients();
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c726f5bc421c3874d9c1a26bcac3f091
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,19 @@
#if MULTIPLAYER_TOOLS
using UnityEngine;
namespace Unity.Netcode.TestHelpers.Runtime.Metrics
{
public class NetworkVariableComponent : NetworkBehaviour
{
public NetworkVariable<int> MyNetworkVariable { get; } = new NetworkVariable<int>();
private void Update()
{
if (IsServer)
{
MyNetworkVariable.Value = Random.Range(100, 999);
}
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 124489f89ef59d449ab4bed1f5ef2f59
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,22 @@
using System;
namespace Unity.Netcode.TestHelpers.Runtime.Metrics
{
public class RpcTestComponent : NetworkBehaviour
{
public event Action OnServerRpcAction;
public event Action OnClientRpcAction;
[ServerRpc]
public void MyServerRpc()
{
OnServerRpcAction?.Invoke();
}
[ClientRpc]
public void MyClientRpc()
{
OnClientRpcAction?.Invoke();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: fdfa28da9866545428083671c445a9ef
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,50 @@
#if MULTIPLAYER_TOOLS
using Unity.Multiplayer.Tools.MetricTypes;
using Unity.Multiplayer.Tools.NetStats;
namespace Unity.Netcode.TestHelpers.Runtime.Metrics
{
internal class WaitForCounterMetricValue : WaitForMetricValues<Counter>
{
private long m_Value;
public delegate bool CounterFilter(long metric);
private CounterFilter m_CounterFilterDelegate;
public WaitForCounterMetricValue(IMetricDispatcher dispatcher, DirectionalMetricInfo directionalMetricName)
: base(dispatcher, directionalMetricName)
{
}
public WaitForCounterMetricValue(IMetricDispatcher dispatcher, DirectionalMetricInfo directionalMetricName, CounterFilter counterFilter)
: this(dispatcher, directionalMetricName)
{
m_CounterFilterDelegate = counterFilter;
}
public long AssertMetricValueHaveBeenFound()
{
AssertHasError();
AssertIsFound();
return m_Value;
}
public override void Observe(MetricCollection collection)
{
if (FindMetric(collection, out var metric))
{
var typedMetric = metric as Counter;
if (typedMetric == default)
{
SetError(metric);
return;
}
m_Value = typedMetric.Value;
m_Found = m_CounterFilterDelegate != null ? m_CounterFilterDelegate(m_Value) : true;
}
}
}
}
#endif

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: aa1d3026d48b43bfa4c76e253b08b3ae
timeCreated: 1644269156

View File

@@ -0,0 +1,60 @@
#if MULTIPLAYER_TOOLS
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using NUnit.Framework;
using Unity.Multiplayer.Tools.MetricTypes;
using Unity.Multiplayer.Tools.NetStats;
namespace Unity.Netcode.TestHelpers.Runtime.Metrics
{
internal class WaitForEventMetricValues<TMetric> : WaitForMetricValues<TMetric>
{
IReadOnlyCollection<TMetric> m_EventValues;
public delegate bool EventFilter(TMetric metric);
EventFilter m_EventFilterDelegate;
public WaitForEventMetricValues(IMetricDispatcher dispatcher, DirectionalMetricInfo directionalMetricName)
: base(dispatcher, directionalMetricName)
{
}
public WaitForEventMetricValues(IMetricDispatcher dispatcher, DirectionalMetricInfo directionalMetricName, EventFilter eventFilter)
: this(dispatcher, directionalMetricName)
{
m_EventFilterDelegate = eventFilter;
}
public IReadOnlyCollection<TMetric> AssertMetricValuesHaveBeenFound()
{
AssertHasError();
AssertIsFound();
return m_EventValues;
}
public override void Observe(MetricCollection collection)
{
if (FindMetric(collection, out var metric))
{
var typedMetric = metric as IEventMetric<TMetric>;
if (typedMetric == default)
{
SetError(metric);
return;
}
if (typedMetric.Values.Any())
{
// Apply filter if one was provided
m_EventValues = m_EventFilterDelegate != null ? typedMetric.Values.Where(x => m_EventFilterDelegate(x)).ToList() : typedMetric.Values.ToList();
m_Found = m_EventValues.Count > 0;
}
}
}
}
}
#endif

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 319c55f92728431283c9e888d8f9d70e
timeCreated: 1644269156

View File

@@ -0,0 +1,55 @@
#if MULTIPLAYER_TOOLS
using Unity.Multiplayer.Tools.MetricTypes;
using Unity.Multiplayer.Tools.NetStats;
namespace Unity.Netcode.TestHelpers.Runtime.Metrics
{
internal class WaitForGaugeMetricValues : WaitForMetricValues<Gauge>
{
private double m_Value;
public delegate bool GaugeFilter(double metric);
private GaugeFilter m_GaugeFilterDelegate;
public WaitForGaugeMetricValues(IMetricDispatcher dispatcher, DirectionalMetricInfo directionalMetricName)
: base(dispatcher, directionalMetricName)
{
}
public WaitForGaugeMetricValues(IMetricDispatcher dispatcher, DirectionalMetricInfo directionalMetricName, GaugeFilter counterFilter)
: this(dispatcher, directionalMetricName)
{
m_GaugeFilterDelegate = counterFilter;
}
public bool MetricFound()
{
return m_Found;
}
public double AssertMetricValueHaveBeenFound()
{
AssertHasError();
AssertIsFound();
return m_Value;
}
public override void Observe(MetricCollection collection)
{
if (FindMetric(collection, out var metric))
{
var typedMetric = metric as Gauge;
if (typedMetric == default)
{
SetError(metric);
return;
}
m_Value = typedMetric.Value;
m_Found = m_GaugeFilterDelegate != null ? m_GaugeFilterDelegate(m_Value) : true;
}
}
}
}
#endif

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 1d76c4e546c546a3b9d63b2c74fcbbca
timeCreated: 1644269156

View File

@@ -0,0 +1,100 @@
#if MULTIPLAYER_TOOLS
using System.Collections;
using System.Linq;
using NUnit.Framework;
using Unity.Multiplayer.Tools.MetricTypes;
using Unity.Multiplayer.Tools.NetStats;
namespace Unity.Netcode.TestHelpers.Runtime.Metrics
{
internal abstract class WaitForMetricValues<TMetric> : IMetricObserver
{
protected readonly string m_MetricName;
protected bool m_Found;
protected bool m_HasError;
protected string m_Error;
protected uint m_NbFrames = 0;
public WaitForMetricValues(IMetricDispatcher dispatcher, DirectionalMetricInfo directionalMetricName)
{
m_MetricName = directionalMetricName.Id;
dispatcher.RegisterObserver(this);
}
abstract public void Observe(MetricCollection collection);
public void AssertMetricValuesHaveNotBeenFound()
{
if (m_HasError)
{
Assert.Fail(m_Error);
}
if (!m_Found)
{
Assert.Pass();
}
else
{
Assert.Fail();
}
}
public IEnumerator WaitForMetricsReceived()
{
yield return WaitForFrames(60);
}
protected void AssertHasError()
{
if (m_HasError)
{
Assert.Fail(m_Error);
}
}
protected void AssertIsFound()
{
if (!m_Found)
{
Assert.Fail($"Found no matching values for metric of type '{typeof(TMetric).Name}', with name '{m_MetricName}' during '{m_NbFrames}' frames.");
}
}
protected bool FindMetric(MetricCollection collection, out IMetric metric)
{
if (m_Found || m_HasError)
{
metric = null;
return false;
}
metric = collection.Metrics.SingleOrDefault(x => x.Name == m_MetricName);
if (metric == default)
{
m_HasError = true;
m_Error = $"Metric collection does not contain metric named '{m_MetricName}'.";
return false;
}
return true;
}
protected void SetError(IMetric metric)
{
m_HasError = true;
m_Error = $"Metric collection contains a metric of type '{metric.GetType().Name}' for name '{m_MetricName}', but was expecting '{typeof(TMetric).Name}'.";
}
private IEnumerator WaitForFrames(uint maxNbFrames)
{
while (!m_Found && m_NbFrames < maxNbFrames)
{
m_NbFrames++;
yield return null;
}
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 176888f06e2c5e14db33783fd0299668
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,727 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;
using Object = UnityEngine.Object;
namespace Unity.Netcode.TestHelpers.Runtime
{
/// <summary>
/// The default Netcode for GameObjects integration test helper class
/// </summary>
public abstract class NetcodeIntegrationTest
{
protected static TimeoutHelper s_GlobalTimeoutHelper = new TimeoutHelper(4.0f);
protected static WaitForSeconds s_DefaultWaitForTick = new WaitForSeconds(1.0f / k_DefaultTickRate);
/// <summary>
/// Registered list of all NetworkObjects spawned.
/// Format is as follows:
/// [ClientId-side where this NetworkObject instance resides][NetworkObjectId][NetworkObject]
/// Where finding the NetworkObject with a NetworkObjectId of 10 on ClientId of 2 would be:
/// s_GlobalNetworkObjects[2][10]
/// To find the client or server player objects please see:
/// <see cref="m_PlayerNetworkObjects"/>
/// </summary>
protected static Dictionary<ulong, Dictionary<ulong, NetworkObject>> s_GlobalNetworkObjects = new Dictionary<ulong, Dictionary<ulong, NetworkObject>>();
public static void RegisterNetworkObject(NetworkObject networkObject)
{
if (!s_GlobalNetworkObjects.ContainsKey(networkObject.NetworkManager.LocalClientId))
{
s_GlobalNetworkObjects.Add(networkObject.NetworkManager.LocalClientId, new Dictionary<ulong, NetworkObject>());
}
if (s_GlobalNetworkObjects[networkObject.NetworkManager.LocalClientId].ContainsKey(networkObject.NetworkObjectId))
{
if (s_GlobalNetworkObjects[networkObject.NetworkManager.LocalClientId] == null)
{
Assert.False(s_GlobalNetworkObjects[networkObject.NetworkManager.LocalClientId][networkObject.NetworkObjectId] != null,
$"Duplicate NetworkObjectId {networkObject.NetworkObjectId} found in {nameof(s_GlobalNetworkObjects)} for client id {networkObject.NetworkManager.LocalClientId}!");
}
else
{
s_GlobalNetworkObjects[networkObject.NetworkManager.LocalClientId][networkObject.NetworkObjectId] = networkObject;
}
}
else
{
s_GlobalNetworkObjects[networkObject.NetworkManager.LocalClientId].Add(networkObject.NetworkObjectId, networkObject);
}
}
public static void DeregisterNetworkObject(NetworkObject networkObject)
{
if (networkObject.IsSpawned && networkObject.NetworkManager != null)
{
DeregisterNetworkObject(networkObject.NetworkManager.LocalClientId, networkObject.NetworkObjectId);
}
}
public static void DeregisterNetworkObject(ulong localClientId, ulong networkObjectId)
{
if (s_GlobalNetworkObjects.ContainsKey(localClientId) && s_GlobalNetworkObjects[localClientId].ContainsKey(networkObjectId))
{
s_GlobalNetworkObjects[localClientId].Remove(networkObjectId);
if (s_GlobalNetworkObjects[localClientId].Count == 0)
{
s_GlobalNetworkObjects.Remove(localClientId);
}
}
}
protected int TotalClients => m_UseHost ? NumberOfClients + 1 : NumberOfClients;
protected const uint k_DefaultTickRate = 30;
protected abstract int NumberOfClients { get; }
public enum NetworkManagerInstatiationMode
{
PerTest, // This will create and destroy new NetworkManagers for each test within a child derived class
AllTests, // This will create one set of NetworkManagers used for all tests within a child derived class (destroyed once all tests are finished)
DoNotCreate // This will not create any NetworkManagers, it is up to the derived class to manage.
}
public enum HostOrServer
{
Host,
Server
}
protected GameObject m_PlayerPrefab;
protected NetworkManager m_ServerNetworkManager;
protected NetworkManager[] m_ClientNetworkManagers;
/// <summary>
/// Contains each client relative set of player NetworkObject instances
/// [Client Relative set of player instances][The player instance ClientId][The player instance's NetworkObject]
/// Example:
/// To get the player instance with a ClientId of 3 that was instantiated (relative) on the player instance with a ClientId of 2
/// m_PlayerNetworkObjects[2][3]
/// </summary>
protected Dictionary<ulong, Dictionary<ulong, NetworkObject>> m_PlayerNetworkObjects = new Dictionary<ulong, Dictionary<ulong, NetworkObject>>();
protected bool m_UseHost = true;
protected int m_TargetFrameRate = 60;
protected NetcodeIntegrationTestHelpers.InstanceTransport m_NetworkTransport = NetcodeIntegrationTestHelpers.InstanceTransport.SIP;
private NetworkManagerInstatiationMode m_NetworkManagerInstatiationMode;
/// <summary>
/// The very first thing invoked during the <see cref="OneTimeSetup"/> that
/// determines how this integration test handles NetworkManager instantiation
/// and destruction. <see cref="NetworkManagerInstatiationMode"/>
/// Override this method to change the default mode:
/// <see cref="NetworkManagerInstatiationMode.AllTests"/>
/// </summary>
protected virtual NetworkManagerInstatiationMode OnSetIntegrationTestMode()
{
return NetworkManagerInstatiationMode.PerTest;
}
protected virtual void OnOneTimeSetup()
{
}
[OneTimeSetUp]
public void OneTimeSetup()
{
m_NetworkManagerInstatiationMode = OnSetIntegrationTestMode();
// Enable NetcodeIntegrationTest auto-label feature
NetcodeIntegrationTestHelpers.RegisterNetcodeIntegrationTest(true);
OnOneTimeSetup();
}
/// <summary>
/// Called before creating and starting the server and clients
/// Note: For <see cref="NetworkManagerInstatiationMode.AllTests"/> and
/// <see cref="NetworkManagerInstatiationMode.PerTest"/> mode integration tests.
/// For those two modes, if you want to have access to the server or client
/// <see cref="NetworkManager"/>s then override <see cref="OnServerAndClientsCreated"/>.
/// <see cref="m_ServerNetworkManager"/> and <see cref="m_ClientNetworkManagers"/>
/// </summary>
protected virtual IEnumerator OnSetup()
{
yield return null;
}
[UnitySetUp]
public IEnumerator SetUp()
{
yield return OnSetup();
if (m_NetworkManagerInstatiationMode == NetworkManagerInstatiationMode.AllTests && m_ServerNetworkManager == null ||
m_NetworkManagerInstatiationMode == NetworkManagerInstatiationMode.PerTest)
{
CreateServerAndClients();
yield return StartServerAndClients();
}
}
/// <summary>
/// Override this to add components or adjustments to the default player prefab
/// <see cref="m_PlayerPrefab"/>
/// </summary>
protected virtual void OnCreatePlayerPrefab()
{
}
private void CreatePlayerPrefab()
{
// Create playerPrefab
m_PlayerPrefab = new GameObject("Player");
NetworkObject networkObject = m_PlayerPrefab.AddComponent<NetworkObject>();
// Make it a prefab
NetcodeIntegrationTestHelpers.MakeNetworkObjectTestPrefab(networkObject);
OnCreatePlayerPrefab();
}
/// <summary>
/// This is invoked before the server and client(s) are started.
/// Override this method if you want to make any adjustments to their
/// NetworkManager instances.
/// </summary>
protected virtual void OnServerAndClientsCreated()
{
}
/// <summary>
/// Will create <see cref="NumberOfClients"/> number of clients.
/// To create a specific number of clients <see cref="CreateServerAndClients(int)"/>
/// </summary>
protected void CreateServerAndClients()
{
CreateServerAndClients(NumberOfClients);
}
/// <summary>
/// Creates the server and clients
/// </summary>
/// <param name="numberOfClients"></param>
protected void CreateServerAndClients(int numberOfClients)
{
CreatePlayerPrefab();
// Create multiple NetworkManager instances
if (!NetcodeIntegrationTestHelpers.Create(numberOfClients, out NetworkManager server, out NetworkManager[] clients, m_TargetFrameRate, m_NetworkTransport))
{
Debug.LogError("Failed to create instances");
Assert.Fail("Failed to create instances");
}
m_ClientNetworkManagers = clients;
m_ServerNetworkManager = server;
if (m_ServerNetworkManager != null)
{
s_DefaultWaitForTick = new WaitForSeconds(1.0f / m_ServerNetworkManager.NetworkConfig.TickRate);
}
// Set the player prefab for the server and clients
m_ServerNetworkManager.NetworkConfig.PlayerPrefab = m_PlayerPrefab;
foreach (var client in m_ClientNetworkManagers)
{
client.NetworkConfig.PlayerPrefab = m_PlayerPrefab;
}
// Provides opportunity to allow child derived classes to
// modify the NetworkManager's configuration before starting.
OnServerAndClientsCreated();
}
/// <summary>
/// Override this method and return false in order to be able
/// to manually control when the server and clients are started.
/// </summary>
protected virtual bool CanStartServerAndClients()
{
return true;
}
/// <summary>
/// Invoked after the server and clients have started.
/// Note: No connection verification has been done at this point
/// </summary>
protected virtual IEnumerator OnStartedServerAndClients()
{
yield return null;
}
/// <summary>
/// Invoked after the server and clients have started and verified
/// their connections with each other.
/// </summary>
protected virtual IEnumerator OnServerAndClientsConnected()
{
yield return null;
}
/// <summary>
/// This starts the server and clients as long as <see cref="CanStartServerAndClients"/>
/// returns true.
/// </summary>
protected IEnumerator StartServerAndClients()
{
if (CanStartServerAndClients())
{
// Start the instances and pass in our SceneManagerInitialization action that is invoked immediately after host-server
// is started and after each client is started.
if (!NetcodeIntegrationTestHelpers.Start(m_UseHost, m_ServerNetworkManager, m_ClientNetworkManagers))
{
Debug.LogError("Failed to start instances");
Assert.Fail("Failed to start instances");
}
RegisterSceneManagerHandler();
// Notification that the server and clients have been started
yield return OnStartedServerAndClients();
// Wait for all clients to connect
yield return WaitForClientsConnectedOrTimeOut();
Assert.False(s_GlobalTimeoutHelper.TimedOut, $"{nameof(StartServerAndClients)} timed out waiting for all clients to be connected!");
if (s_GlobalTimeoutHelper.TimedOut)
{
yield return null;
}
if (m_UseHost || m_ServerNetworkManager.IsHost)
{
// Add the server player instance to all m_ClientSidePlayerNetworkObjects entries
var serverPlayerClones = Object.FindObjectsOfType<NetworkObject>().Where((c) => c.IsPlayerObject && c.OwnerClientId == m_ServerNetworkManager.LocalClientId);
foreach (var playerNetworkObject in serverPlayerClones)
{
if (!m_PlayerNetworkObjects.ContainsKey(playerNetworkObject.NetworkManager.LocalClientId))
{
m_PlayerNetworkObjects.Add(playerNetworkObject.NetworkManager.LocalClientId, new Dictionary<ulong, NetworkObject>());
}
m_PlayerNetworkObjects[playerNetworkObject.NetworkManager.LocalClientId].Add(m_ServerNetworkManager.LocalClientId, playerNetworkObject);
}
}
// Creates a dictionary for all player instances client and server relative
// This provides a simpler way to get a specific player instance relative to a client instance
foreach (var networkManager in m_ClientNetworkManagers)
{
Assert.NotNull(networkManager.LocalClient.PlayerObject, $"{nameof(StartServerAndClients)} detected that client {networkManager.LocalClientId} does not have an assigned player NetworkObject!");
// Get all player instances for the current client NetworkManager instance
var clientPlayerClones = Object.FindObjectsOfType<NetworkObject>().Where((c) => c.IsPlayerObject && c.OwnerClientId == networkManager.LocalClientId);
// Add this player instance to each client player entry
foreach (var playerNetworkObject in clientPlayerClones)
{
// When the server is not the host this needs to be done
if (!m_PlayerNetworkObjects.ContainsKey(playerNetworkObject.NetworkManager.LocalClientId))
{
m_PlayerNetworkObjects.Add(playerNetworkObject.NetworkManager.LocalClientId, new Dictionary<ulong, NetworkObject>());
}
m_PlayerNetworkObjects[playerNetworkObject.NetworkManager.LocalClientId].Add(networkManager.LocalClientId, playerNetworkObject);
}
}
// Notification that at this time the server and client(s) are instantiated,
// started, and connected on both sides.
yield return OnServerAndClientsConnected();
}
}
/// <summary>
/// Override this method to control when clients
/// can fake-load a scene.
/// </summary>
protected virtual bool CanClientsLoad()
{
return true;
}
/// <summary>
/// Override this method to control when clients
/// can fake-unload a scene.
/// </summary>
protected virtual bool CanClientsUnload()
{
return true;
}
/// <summary>
/// De-Registers from the CanClientsLoad and CanClientsUnload events of the
/// ClientSceneHandler (default is IntegrationTestSceneHandler).
/// </summary>
protected void DeRegisterSceneManagerHandler()
{
if (NetcodeIntegrationTestHelpers.ClientSceneHandler != null)
{
NetcodeIntegrationTestHelpers.ClientSceneHandler.CanClientsLoad -= ClientSceneHandler_CanClientsLoad;
NetcodeIntegrationTestHelpers.ClientSceneHandler.CanClientsUnload -= ClientSceneHandler_CanClientsUnload;
}
}
/// <summary>
/// Registers the CanClientsLoad and CanClientsUnload events of the
/// ClientSceneHandler.
/// The default is: <see cref="IntegrationTestSceneHandler"/>.
/// </summary>
protected void RegisterSceneManagerHandler()
{
if (NetcodeIntegrationTestHelpers.ClientSceneHandler != null)
{
NetcodeIntegrationTestHelpers.ClientSceneHandler.CanClientsLoad += ClientSceneHandler_CanClientsLoad;
NetcodeIntegrationTestHelpers.ClientSceneHandler.CanClientsUnload += ClientSceneHandler_CanClientsUnload;
}
}
private bool ClientSceneHandler_CanClientsUnload()
{
return CanClientsUnload();
}
private bool ClientSceneHandler_CanClientsLoad()
{
return CanClientsLoad();
}
/// <summary>
/// This shuts down all NetworkManager instances registered via the
/// <see cref="NetcodeIntegrationTestHelpers"/> class and cleans up
/// the test runner scene of any left over NetworkObjects.
/// <see cref="DestroySceneNetworkObjects"/>
/// </summary>
protected void ShutdownAndCleanUp()
{
// Shutdown and clean up both of our NetworkManager instances
try
{
if (NetcodeIntegrationTestHelpers.ClientSceneHandler != null)
{
NetcodeIntegrationTestHelpers.ClientSceneHandler.CanClientsLoad -= ClientSceneHandler_CanClientsLoad;
NetcodeIntegrationTestHelpers.ClientSceneHandler.CanClientsUnload -= ClientSceneHandler_CanClientsUnload;
}
NetcodeIntegrationTestHelpers.Destroy();
m_PlayerNetworkObjects.Clear();
s_GlobalNetworkObjects.Clear();
}
catch (Exception e) { throw e; }
finally
{
if (m_PlayerPrefab != null)
{
Object.Destroy(m_PlayerPrefab);
m_PlayerPrefab = null;
}
}
// Cleanup any remaining NetworkObjects
DestroySceneNetworkObjects();
// reset the m_ServerWaitForTick for the next test to initialize
s_DefaultWaitForTick = new WaitForSeconds(1.0f / k_DefaultTickRate);
}
/// <summary>
/// Note: For <see cref="NetworkManagerInstatiationMode.PerTest"/> mode
/// this is called before ShutdownAndCleanUp.
/// </summary>
protected virtual IEnumerator OnTearDown()
{
yield return null;
}
[UnityTearDown]
public IEnumerator TearDown()
{
yield return OnTearDown();
if (m_NetworkManagerInstatiationMode == NetworkManagerInstatiationMode.PerTest)
{
ShutdownAndCleanUp();
}
}
/// <summary>
/// Override this method to do handle cleaning up once the test(s)
/// within the child derived class have completed
/// Note: For <see cref="NetworkManagerInstatiationMode.AllTests"/> mode
/// this is called before ShutdownAndCleanUp.
/// </summary>
protected virtual void OnOneTimeTearDown()
{
}
[OneTimeTearDown]
public void OneTimeTearDown()
{
OnOneTimeTearDown();
if (m_NetworkManagerInstatiationMode == NetworkManagerInstatiationMode.AllTests)
{
ShutdownAndCleanUp();
}
// Disable NetcodeIntegrationTest auto-label feature
NetcodeIntegrationTestHelpers.RegisterNetcodeIntegrationTest(false);
}
/// <summary>
/// Override this to filter out the <see cref="NetworkObject"/>s that you
/// want to allow to persist between integration tests.
/// <see cref="DestroySceneNetworkObjects"/>
/// <see cref="ShutdownAndCleanUp"/>
/// </summary>
/// <param name="networkObject">the network object in question to be destroyed</param>
protected virtual bool CanDestroyNetworkObject(NetworkObject networkObject)
{
return true;
}
/// <summary>
/// Destroys all NetworkObjects at the end of a test cycle.
/// </summary>
protected void DestroySceneNetworkObjects()
{
var networkObjects = Object.FindObjectsOfType<NetworkObject>();
foreach (var networkObject in networkObjects)
{
// This can sometimes be null depending upon order of operations
// when dealing with parented NetworkObjects. If NetworkObjectB
// is a child of NetworkObjectA and NetworkObjectA comes before
// NetworkObjectB in the list of NeworkObjects found, then when
// NetworkObjectA's GameObject is destroyed it will also destroy
// NetworkObjectB's GameObject which will destroy NetworkObjectB.
// If there is a null entry in the list, this is the most likely
// scenario and so we just skip over it.
if (networkObject == null)
{
continue;
}
if (CanDestroyNetworkObject(networkObject))
{
// Destroy the GameObject that holds the NetworkObject component
Object.DestroyImmediate(networkObject.gameObject);
}
}
}
/// <summary>
/// Waits for the function condition to return true or it will time out.
/// This will operate at the current m_ServerNetworkManager.NetworkConfig.TickRate
/// and allow for a unique TimeoutHelper handler (if none then it uses the default)
/// Notes: This provides more stability when running integration tests that could be
/// impacted by:
/// -how the integration test is being executed (i.e. in editor or in a stand alone build)
/// -potential platform performance issues (i.e. VM is throttled or maxed)
/// Note: For more complex tests, <see cref="ConditionalPredicateBase"/> and the overloaded
/// version of this method
/// </summary>
public static IEnumerator WaitForConditionOrTimeOut(Func<bool> checkForCondition, TimeoutHelper timeOutHelper = null)
{
if (checkForCondition == null)
{
throw new ArgumentNullException($"checkForCondition cannot be null!");
}
// If none is provided we use the default global time out helper
if (timeOutHelper == null)
{
timeOutHelper = s_GlobalTimeoutHelper;
}
// Start checking for a timeout
timeOutHelper.Start();
while (!timeOutHelper.HasTimedOut())
{
// Update and check to see if the condition has been met
if (checkForCondition.Invoke())
{
break;
}
// Otherwise wait for 1 tick interval
yield return s_DefaultWaitForTick;
}
// Stop checking for a timeout
timeOutHelper.Stop();
}
/// <summary>
/// This version accepts an IConditionalPredicate implementation to provide
/// more flexibility for checking complex conditional cases.
/// </summary>
public static IEnumerator WaitForConditionOrTimeOut(IConditionalPredicate conditionalPredicate, TimeoutHelper timeOutHelper = null)
{
if (conditionalPredicate == null)
{
throw new ArgumentNullException($"checkForCondition cannot be null!");
}
// If none is provided we use the default global time out helper
if (timeOutHelper == null)
{
timeOutHelper = s_GlobalTimeoutHelper;
}
conditionalPredicate.Started();
yield return WaitForConditionOrTimeOut(conditionalPredicate.HasConditionBeenReached, timeOutHelper);
conditionalPredicate.Finished(timeOutHelper.TimedOut);
}
/// <summary>
/// Validates that all remote clients (i.e. non-server) detect they are connected
/// to the server and that the server reflects the appropriate number of clients
/// have connected or it will time out.
/// </summary>
/// <param name="clientsToCheck">An array of clients to be checked</param>
protected IEnumerator WaitForClientsConnectedOrTimeOut(NetworkManager[] clientsToCheck)
{
var remoteClientCount = clientsToCheck.Length;
var serverClientCount = m_ServerNetworkManager.IsHost ? remoteClientCount + 1 : remoteClientCount;
yield return WaitForConditionOrTimeOut(() => clientsToCheck.Where((c) => c.IsConnectedClient).Count() == remoteClientCount &&
m_ServerNetworkManager.ConnectedClients.Count == serverClientCount);
}
/// <summary>
/// Overloaded method that just passes in all clients to
/// <see cref="WaitForClientsConnectedOrTimeOut(NetworkManager[])"/>
/// </summary>
protected IEnumerator WaitForClientsConnectedOrTimeOut()
{
yield return WaitForClientsConnectedOrTimeOut(m_ClientNetworkManagers);
}
/// <summary>
/// Creates a basic NetworkObject test prefab, assigns it to a new
/// NetworkPrefab entry, and then adds it to the server and client(s)
/// NetworkManagers' NetworkConfig.NetworkPrefab lists.
/// </summary>
/// <param name="baseName">the basic name to be used for each instance</param>
/// <returns>NetworkObject of the GameObject assigned to the new NetworkPrefab entry</returns>
protected GameObject CreateNetworkObjectPrefab(string baseName)
{
var prefabCreateAssertError = $"You can only invoke this method during {nameof(OnServerAndClientsCreated)} " +
$"but before {nameof(OnStartedServerAndClients)}!";
Assert.IsNotNull(m_ServerNetworkManager, prefabCreateAssertError);
Assert.IsFalse(m_ServerNetworkManager.IsListening, prefabCreateAssertError);
var gameObject = new GameObject();
gameObject.name = baseName;
var networkObject = gameObject.AddComponent<NetworkObject>();
NetcodeIntegrationTestHelpers.MakeNetworkObjectTestPrefab(networkObject);
var networkPrefab = new NetworkPrefab() { Prefab = gameObject };
m_ServerNetworkManager.NetworkConfig.NetworkPrefabs.Add(networkPrefab);
foreach (var clientNetworkManager in m_ClientNetworkManagers)
{
clientNetworkManager.NetworkConfig.NetworkPrefabs.Add(networkPrefab);
}
return gameObject;
}
/// <summary>
/// Overloaded method <see cref="SpawnObject(NetworkObject, NetworkManager, bool)"/>
/// </summary>
protected GameObject SpawnObject(GameObject prefabGameObject, NetworkManager owner, bool destroyWithScene = false)
{
var prefabNetworkObject = prefabGameObject.GetComponent<NetworkObject>();
Assert.IsNotNull(prefabNetworkObject, $"{nameof(GameObject)} {prefabGameObject.name} does not have a {nameof(NetworkObject)} component!");
return SpawnObject(prefabNetworkObject, owner, destroyWithScene);
}
/// <summary>
/// Spawn a NetworkObject prefab instance
/// </summary>
/// <param name="prefabNetworkObject">the prefab NetworkObject to spawn</param>
/// <param name="owner">the owner of the instance</param>
/// <param name="destroyWithScene">default is false</param>
/// <returns>GameObject instance spawned</returns>
private GameObject SpawnObject(NetworkObject prefabNetworkObject, NetworkManager owner, bool destroyWithScene = false)
{
Assert.IsTrue(prefabNetworkObject.GlobalObjectIdHash > 0, $"{nameof(GameObject)} {prefabNetworkObject.name} has a {nameof(NetworkObject.GlobalObjectIdHash)} value of 0! Make sure to make it a valid prefab before trying to spawn!");
var newInstance = Object.Instantiate(prefabNetworkObject.gameObject);
var networkObjectToSpawn = newInstance.GetComponent<NetworkObject>();
networkObjectToSpawn.NetworkManagerOwner = m_ServerNetworkManager; // Required to assure the server does the spawning
if (owner == m_ServerNetworkManager)
{
if (m_UseHost)
{
networkObjectToSpawn.SpawnWithOwnership(owner.LocalClientId, destroyWithScene);
}
else
{
networkObjectToSpawn.Spawn(destroyWithScene);
}
}
else
{
networkObjectToSpawn.SpawnWithOwnership(owner.LocalClientId, destroyWithScene);
}
return newInstance;
}
/// <summary>
/// Overloaded method <see cref="SpawnObjects(NetworkObject, NetworkManager, int, bool)"/>
/// </summary>
protected List<GameObject> SpawnObjects(GameObject prefabGameObject, NetworkManager owner, int count, bool destroyWithScene = false)
{
var prefabNetworkObject = prefabGameObject.GetComponent<NetworkObject>();
Assert.IsNotNull(prefabNetworkObject, $"{nameof(GameObject)} {prefabGameObject.name} does not have a {nameof(NetworkObject)} component!");
return SpawnObjects(prefabNetworkObject, owner, count, destroyWithScene);
}
/// <summary>
/// Will spawn (x) number of prefab NetworkObjects
/// <see cref="SpawnObject(NetworkObject, NetworkManager, bool)"/>
/// </summary>
/// <param name="prefabNetworkObject">the prefab NetworkObject to spawn</param>
/// <param name="owner">the owner of the instance</param>
/// <param name="count">number of instances to create and spawn</param>
/// <param name="destroyWithScene">default is false</param>
private List<GameObject> SpawnObjects(NetworkObject prefabNetworkObject, NetworkManager owner, int count, bool destroyWithScene = false)
{
var gameObjectsSpawned = new List<GameObject>();
for (int i = 0; i < count; i++)
{
gameObjectsSpawned.Add(SpawnObject(prefabNetworkObject, owner, destroyWithScene));
}
return gameObjectsSpawned;
}
/// <summary>
/// Default constructor
/// </summary>
public NetcodeIntegrationTest()
{
}
/// <summary>
/// Optional Host or Server integration tests
/// Constructor that allows you To break tests up as a host
/// and a server.
/// Example: Decorate your child derived class with TestFixture
/// and then create a constructor at the child level
/// [TestFixture(HostOrServer.Host)]
/// [TestFixture(HostOrServer.Server)]
/// public class MyChildClass : NetcodeIntegrationTest
/// {
/// MyChildClass(HostOrServer hostOrServer) : base(hostOrServer) { }
/// }
/// </summary>
/// <param name="hostOrServer"></param>
public NetcodeIntegrationTest(HostOrServer hostOrServer)
{
m_UseHost = hostOrServer == HostOrServer.Host ? true : false;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 789a3189410645aca48f11a51c823418
timeCreated: 1621620979

View File

@@ -0,0 +1,763 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.SceneManagement;
using Object = UnityEngine.Object;
namespace Unity.Netcode.TestHelpers.Runtime
{
/// <summary>
/// Provides helpers for running multi instance tests.
/// </summary>
public static class NetcodeIntegrationTestHelpers
{
public const int DefaultMinFrames = 1;
public const float DefaultTimeout = 1f;
private static List<NetworkManager> s_NetworkManagerInstances = new List<NetworkManager>();
private static Dictionary<NetworkManager, MultiInstanceHooks> s_Hooks = new Dictionary<NetworkManager, MultiInstanceHooks>();
private static bool s_IsStarted;
private static int s_ClientCount;
private static int s_OriginalTargetFrameRate = -1;
public delegate bool MessageHandleCheck(object receivedMessage);
internal class MessageHandleCheckWithResult
{
public MessageHandleCheck Check;
public bool Result;
}
private class MultiInstanceHooks : INetworkHooks
{
public Dictionary<Type, List<MessageHandleCheckWithResult>> HandleChecks = new Dictionary<Type, List<MessageHandleCheckWithResult>>();
public static bool CheckForMessageOfType<T>(object receivedMessage) where T : INetworkMessage
{
return receivedMessage.GetType() == typeof(T);
}
public void OnBeforeSendMessage<T>(ulong clientId, ref T message, NetworkDelivery delivery) where T : INetworkMessage
{
}
public void OnAfterSendMessage<T>(ulong clientId, ref T message, NetworkDelivery delivery, int messageSizeBytes) where T : INetworkMessage
{
}
public void OnBeforeReceiveMessage(ulong senderId, Type messageType, int messageSizeBytes)
{
}
public void OnAfterReceiveMessage(ulong senderId, Type messageType, int messageSizeBytes)
{
}
public void OnBeforeSendBatch(ulong clientId, int messageCount, int batchSizeInBytes, NetworkDelivery delivery)
{
}
public void OnAfterSendBatch(ulong clientId, int messageCount, int batchSizeInBytes, NetworkDelivery delivery)
{
}
public void OnBeforeReceiveBatch(ulong senderId, int messageCount, int batchSizeInBytes)
{
}
public void OnAfterReceiveBatch(ulong senderId, int messageCount, int batchSizeInBytes)
{
}
public bool OnVerifyCanSend(ulong destinationId, Type messageType, NetworkDelivery delivery)
{
return true;
}
public bool OnVerifyCanReceive(ulong senderId, Type messageType)
{
return true;
}
public void OnBeforeHandleMessage<T>(ref T message, ref NetworkContext context) where T : INetworkMessage
{
}
public void OnAfterHandleMessage<T>(ref T message, ref NetworkContext context) where T : INetworkMessage
{
if (HandleChecks.ContainsKey(typeof(T)))
{
foreach (var check in HandleChecks[typeof(T)])
{
if (check.Check(message))
{
check.Result = true;
HandleChecks[typeof(T)].Remove(check);
break;
}
}
}
}
}
private const string k_FirstPartOfTestRunnerSceneName = "InitTestScene";
public static List<NetworkManager> NetworkManagerInstances => s_NetworkManagerInstances;
public enum InstanceTransport
{
SIP,
#if UTP_ADAPTER
UTP
#endif
}
internal static IntegrationTestSceneHandler ClientSceneHandler = null;
/// <summary>
/// Registers the IntegrationTestSceneHandler for integration tests.
/// The default client behavior is to not load scenes on the client side.
/// </summary>
private static void RegisterSceneManagerHandler(NetworkManager networkManager)
{
if (!networkManager.IsServer)
{
if (ClientSceneHandler == null)
{
ClientSceneHandler = new IntegrationTestSceneHandler();
}
networkManager.SceneManager.SceneManagerHandler = ClientSceneHandler;
}
}
/// <summary>
/// Call this to clean up the IntegrationTestSceneHandler and destroy the s_CoroutineRunner.
/// Note:
/// If deriving from <see cref="NetcodeIntegrationTest"/> or using <see cref="Destroy"/> then you
/// typically won't need to do this.
/// </summary>
public static void CleanUpHandlers()
{
if (ClientSceneHandler != null)
{
ClientSceneHandler.Dispose();
ClientSceneHandler = null;
}
}
/// <summary>
/// Call this to register scene validation and the IntegrationTestSceneHandler
/// Note:
/// If deriving from <see cref="NetcodeIntegrationTest"/> or using <see cref="Destroy"/> then you
/// typically won't need to call this.
/// </summary>
public static void RegisterHandlers(NetworkManager networkManager, bool serverSideSceneManager = false)
{
SceneManagerValidationAndTestRunnerInitialization(networkManager);
if (!networkManager.IsServer || networkManager.IsServer && serverSideSceneManager)
{
RegisterSceneManagerHandler(networkManager);
}
}
/// <summary>
/// Create the correct NetworkTransport, attach it to the game object and return it.
/// Default value is SIPTransport.
/// </summary>
internal static NetworkTransport CreateInstanceTransport(InstanceTransport instanceTransport, GameObject go)
{
switch (instanceTransport)
{
case InstanceTransport.SIP:
default:
return go.AddComponent<SIPTransport>();
#if UTP_ADAPTER
case InstanceTransport.UTP:
return go.AddComponent<UnityTransport>();
#endif
}
}
/// <summary>
/// Creates NetworkingManagers and configures them for use in a multi instance setting.
/// </summary>
/// <param name="clientCount">The amount of clients</param>
/// <param name="server">The server NetworkManager</param>
/// <param name="clients">The clients NetworkManagers</param>
/// <param name="targetFrameRate">The targetFrameRate of the Unity engine to use while the multi instance helper is running. Will be reset on shutdown.</param>
public static bool Create(int clientCount, out NetworkManager server, out NetworkManager[] clients, int targetFrameRate = 60, InstanceTransport instanceTransport = InstanceTransport.SIP)
{
s_NetworkManagerInstances = new List<NetworkManager>();
CreateNewClients(clientCount, out clients, instanceTransport);
// Create gameObject
var go = new GameObject("NetworkManager - Server");
// Create networkManager component
server = go.AddComponent<NetworkManager>();
NetworkManagerInstances.Insert(0, server);
// Set the NetworkConfig
server.NetworkConfig = new NetworkConfig()
{
// Set transport
NetworkTransport = CreateInstanceTransport(instanceTransport, go)
};
s_OriginalTargetFrameRate = Application.targetFrameRate;
Application.targetFrameRate = targetFrameRate;
return true;
}
/// <summary>
/// Used to add a client to the already existing list of clients
/// </summary>
/// <param name="clientCount">The amount of clients</param>
/// <param name="clients"></param>
public static bool CreateNewClients(int clientCount, out NetworkManager[] clients, InstanceTransport instanceTransport = InstanceTransport.SIP)
{
clients = new NetworkManager[clientCount];
var activeSceneName = SceneManager.GetActiveScene().name;
for (int i = 0; i < clientCount; i++)
{
// Create gameObject
var go = new GameObject("NetworkManager - Client - " + i);
// Create networkManager component
clients[i] = go.AddComponent<NetworkManager>();
// Set the NetworkConfig
clients[i].NetworkConfig = new NetworkConfig()
{
// Set transport
NetworkTransport = CreateInstanceTransport(instanceTransport, go)
};
}
NetworkManagerInstances.AddRange(clients);
return true;
}
/// <summary>
/// Stops one single client and makes sure to cleanup any static variables in this helper
/// </summary>
/// <param name="clientToStop"></param>
public static void StopOneClient(NetworkManager clientToStop)
{
clientToStop.Shutdown();
s_Hooks.Remove(clientToStop);
Object.Destroy(clientToStop.gameObject);
NetworkManagerInstances.Remove(clientToStop);
}
/// <summary>
/// Should always be invoked when finished with a single unit test
/// (i.e. during TearDown)
/// </summary>
public static void Destroy()
{
if (s_IsStarted == false)
{
return;
}
s_IsStarted = false;
// Shutdown the server which forces clients to disconnect
foreach (var networkManager in NetworkManagerInstances)
{
networkManager.Shutdown();
s_Hooks.Remove(networkManager);
}
// Destroy the network manager instances
foreach (var networkManager in NetworkManagerInstances)
{
Object.DestroyImmediate(networkManager.gameObject);
}
NetworkManagerInstances.Clear();
CleanUpHandlers();
Application.targetFrameRate = s_OriginalTargetFrameRate;
}
/// <summary>
/// We want to exclude the TestRunner scene on the host-server side so it won't try to tell clients to
/// synchronize to this scene when they connect
/// </summary>
private static bool VerifySceneIsValidForClientsToLoad(int sceneIndex, string sceneName, LoadSceneMode loadSceneMode)
{
// exclude test runner scene
if (sceneName.StartsWith(k_FirstPartOfTestRunnerSceneName))
{
return false;
}
return true;
}
/// <summary>
/// This registers scene validation callback for the server to prevent it from telling connecting
/// clients to synchronize (i.e. load) the test runner scene. This will also register the test runner
/// scene and its handle for both client(s) and server-host.
/// </summary>
private static void SceneManagerValidationAndTestRunnerInitialization(NetworkManager networkManager)
{
// If VerifySceneBeforeLoading is not already set, then go ahead and set it so the host/server
// will not try to synchronize clients to the TestRunner scene. We only need to do this for the server.
if (networkManager.IsServer && networkManager.SceneManager.VerifySceneBeforeLoading == null)
{
networkManager.SceneManager.VerifySceneBeforeLoading = VerifySceneIsValidForClientsToLoad;
// If a unit/integration test does not handle this on their own, then Ignore the validation warning
networkManager.SceneManager.DisableValidationWarnings(true);
}
// Register the test runner scene so it will be able to synchronize NetworkObjects without logging a
// warning about using the currently active scene
var scene = SceneManager.GetActiveScene();
// As long as this is a test runner scene (or most likely a test runner scene)
if (scene.name.StartsWith(k_FirstPartOfTestRunnerSceneName))
{
// Register the test runner scene just so we avoid another warning about not being able to find the
// scene to synchronize NetworkObjects. Next, add the currently active test runner scene to the scenes
// loaded and register the server to client scene handle since host-server shares the test runner scene
// with the clients.
networkManager.SceneManager.GetAndAddNewlyLoadedSceneByName(scene.name);
networkManager.SceneManager.ServerSceneHandleToClientSceneHandle.Add(scene.handle, scene.handle);
}
}
public delegate void BeforeClientStartCallback();
/// <summary>
/// Starts NetworkManager instances created by the Create method.
/// </summary>
/// <param name="host">Whether or not to create a Host instead of Server</param>
/// <param name="server">The Server NetworkManager</param>
/// <param name="clients">The Clients NetworkManager</param>
/// <param name="callback">called immediately after server is started and before client(s) are started</param>
/// <returns></returns>
public static bool Start(bool host, NetworkManager server, NetworkManager[] clients, BeforeClientStartCallback callback = null)
{
if (s_IsStarted)
{
throw new InvalidOperationException($"{nameof(NetcodeIntegrationTestHelpers)} already thinks it is started. Did you forget to Destroy?");
}
s_IsStarted = true;
s_ClientCount = clients.Length;
if (host)
{
server.StartHost();
}
else
{
server.StartServer();
}
var hooks = new MultiInstanceHooks();
server.MessagingSystem.Hook(hooks);
s_Hooks[server] = hooks;
// if set, then invoke this for the server
RegisterHandlers(server);
callback?.Invoke();
for (int i = 0; i < clients.Length; i++)
{
clients[i].StartClient();
hooks = new MultiInstanceHooks();
clients[i].MessagingSystem.Hook(hooks);
s_Hooks[clients[i]] = hooks;
// if set, then invoke this for the client
RegisterHandlers(clients[i]);
}
return true;
}
/// <summary>
/// Used to return a value of type T from a wait condition
/// </summary>
public class ResultWrapper<T>
{
public T Result;
}
private static uint s_AutoIncrementGlobalObjectIdHashCounter = 111111;
public static uint GetNextGlobalIdHashValue()
{
return ++s_AutoIncrementGlobalObjectIdHashCounter;
}
public static bool IsNetcodeIntegrationTestRunning { get; internal set; }
public static void RegisterNetcodeIntegrationTest(bool registered)
{
IsNetcodeIntegrationTestRunning = registered;
}
/// <summary>
/// Normally we would only allow player prefabs to be set to a prefab. Not runtime created objects.
/// In order to prevent having a Resource folder full of a TON of prefabs that we have to maintain,
/// MultiInstanceHelper has a helper function that lets you mark a runtime created object to be
/// treated as a prefab by the Netcode. That's how we can get away with creating the player prefab
/// at runtime without it being treated as a SceneObject or causing other conflicts with the Netcode.
/// </summary>
/// <param name="networkObject">The networkObject to be treated as Prefab</param>
/// <param name="globalObjectIdHash">The GlobalObjectId to force</param>
public static void MakeNetworkObjectTestPrefab(NetworkObject networkObject, uint globalObjectIdHash = default)
{
// Override `GlobalObjectIdHash` if `globalObjectIdHash` param is set
if (globalObjectIdHash != default)
{
networkObject.GlobalObjectIdHash = globalObjectIdHash;
}
// Fallback to auto-increment if `GlobalObjectIdHash` was never set
if (networkObject.GlobalObjectIdHash == default)
{
networkObject.GlobalObjectIdHash = ++s_AutoIncrementGlobalObjectIdHashCounter;
}
// Prevent object from being snapped up as a scene object
networkObject.IsSceneObject = false;
// To avoid issues with integration tests that forget to clean up,
// this feature only works with NetcodeIntegrationTest derived classes
if (IsNetcodeIntegrationTestRunning)
{
// Add the object identifier component
networkObject.gameObject.AddComponent<ObjectNameIdentifier>();
}
}
// We use GameObject instead of SceneObject to be able to keep hierarchy
public static void MarkAsSceneObjectRoot(GameObject networkObjectRoot, NetworkManager server, NetworkManager[] clients)
{
networkObjectRoot.name += " - Server";
NetworkObject[] serverNetworkObjects = networkObjectRoot.GetComponentsInChildren<NetworkObject>();
for (int i = 0; i < serverNetworkObjects.Length; i++)
{
serverNetworkObjects[i].NetworkManagerOwner = server;
}
for (int i = 0; i < clients.Length; i++)
{
GameObject root = Object.Instantiate(networkObjectRoot);
root.name += " - Client - " + i;
NetworkObject[] clientNetworkObjects = root.GetComponentsInChildren<NetworkObject>();
for (int j = 0; j < clientNetworkObjects.Length; j++)
{
clientNetworkObjects[j].NetworkManagerOwner = clients[i];
}
}
}
/// <summary>
/// Waits (yields) until specified amount of network ticks has been passed.
/// </summary>
public static IEnumerator WaitForTicks(NetworkManager networkManager, int count)
{
var targetTick = networkManager.NetworkTickSystem.LocalTime.Tick + count;
yield return new WaitUntil(() => networkManager.NetworkTickSystem.LocalTime.Tick >= targetTick);
}
/// <summary>
/// Waits on the client side to be connected.
/// </summary>
/// <param name="client">The client</param>
/// <param name="result">The result. If null, it will automatically assert</param>
/// <param name="maxFrames">The max frames to wait for</param>
public static IEnumerator WaitForClientConnected(NetworkManager client, ResultWrapper<bool> result = null, float timeout = DefaultTimeout)
{
yield return WaitForClientsConnected(new NetworkManager[] { client }, result, timeout);
}
/// <summary>
/// Similar to WaitForClientConnected, this waits for multiple clients to be connected.
/// </summary>
/// <param name="clients">The clients to be connected</param>
/// <param name="result">The result. If null, it will automatically assert<</param>
/// <param name="maxFrames">The max frames to wait for</param>
/// <returns></returns>
public static IEnumerator WaitForClientsConnected(NetworkManager[] clients, ResultWrapper<bool> result = null, float timeout = DefaultTimeout)
{
// Make sure none are the host client
foreach (var client in clients)
{
if (client.IsServer)
{
throw new InvalidOperationException("Cannot wait for connected as server");
}
}
var allConnected = true;
var startTime = Time.realtimeSinceStartup;
while (Time.realtimeSinceStartup - startTime < timeout)
{
allConnected = true;
foreach (var client in clients)
{
if (!client.IsConnectedClient)
{
allConnected = false;
break;
}
}
if (allConnected)
{
break;
}
var nextFrameNumber = Time.frameCount + 1;
yield return new WaitUntil(() => Time.frameCount >= nextFrameNumber);
}
if (result != null)
{
result.Result = allConnected;
}
else
{
for (var i = 0; i < clients.Length; ++i)
{
var client = clients[i];
// Logging i+1 because that's the local client ID they'll get (0 is server)
// Can't use client.LocalClientId because that doesn't get assigned until IsConnectedClient == true,
Assert.True(client.IsConnectedClient, $"Client {i + 1} never connected");
}
}
}
/// <summary>
/// Waits on the server side for 1 client to be connected
/// </summary>
/// <param name="server">The server</param>
/// <param name="result">The result. If null, it will automatically assert</param>
/// <param name="maxFrames">The max frames to wait for</param>
public static IEnumerator WaitForClientConnectedToServer(NetworkManager server, ResultWrapper<bool> result = null, float timeout = DefaultTimeout)
{
yield return WaitForClientsConnectedToServer(server, server.IsHost ? s_ClientCount + 1 : s_ClientCount, result, timeout);
}
/// <summary>
/// Waits on the server side for 1 client to be connected
/// </summary>
/// <param name="server">The server</param>
/// <param name="result">The result. If null, it will automatically assert</param>
/// <param name="maxFrames">The max frames to wait for</param>
public static IEnumerator WaitForClientsConnectedToServer(NetworkManager server, int clientCount = 1, ResultWrapper<bool> result = null, float timeout = DefaultTimeout)
{
if (!server.IsServer)
{
throw new InvalidOperationException("Cannot wait for connected as client");
}
var startTime = Time.realtimeSinceStartup;
while (Time.realtimeSinceStartup - startTime < timeout && server.ConnectedClients.Count != clientCount)
{
var nextFrameNumber = Time.frameCount + 1;
yield return new WaitUntil(() => Time.frameCount >= nextFrameNumber);
}
var res = server.ConnectedClients.Count == clientCount;
if (result != null)
{
result.Result = res;
}
else
{
Assert.True(res, "A client never connected to server");
}
}
/// <summary>
/// Gets a NetworkObject instance as it's represented by a certain peer.
/// </summary>
/// <param name="networkObjectId">The networkObjectId to get</param>
/// <param name="representation">The representation to get the object from</param>
/// <param name="result">The result</param>
/// <param name="failIfNull">Whether or not to fail if no object is found and result is null</param>
/// <param name="maxFrames">The max frames to wait for</param>
public static IEnumerator GetNetworkObjectByRepresentation(ulong networkObjectId, NetworkManager representation, ResultWrapper<NetworkObject> result, bool failIfNull = true, float timeout = DefaultTimeout)
{
if (result == null)
{
throw new ArgumentNullException("Result cannot be null");
}
var startTime = Time.realtimeSinceStartup;
while (Time.realtimeSinceStartup - startTime < timeout && representation.SpawnManager.SpawnedObjects.All(x => x.Value.NetworkObjectId != networkObjectId))
{
var nextFrameNumber = Time.frameCount + 1;
yield return new WaitUntil(() => Time.frameCount >= nextFrameNumber);
}
result.Result = representation.SpawnManager.SpawnedObjects.First(x => x.Value.NetworkObjectId == networkObjectId).Value;
if (failIfNull && result.Result == null)
{
Assert.Fail("NetworkObject could not be found");
}
}
/// <summary>
/// Gets a NetworkObject instance as it's represented by a certain peer.
/// </summary>
/// <param name="predicate">The predicate used to filter for your target NetworkObject</param>
/// <param name="representation">The representation to get the object from</param>
/// <param name="result">The result</param>
/// <param name="failIfNull">Whether or not to fail if no object is found and result is null</param>
/// <param name="maxFrames">The max frames to wait for</param>
public static IEnumerator GetNetworkObjectByRepresentation(Func<NetworkObject, bool> predicate, NetworkManager representation, ResultWrapper<NetworkObject> result, bool failIfNull = true, float timeout = DefaultTimeout)
{
if (result == null)
{
throw new ArgumentNullException("Result cannot be null");
}
if (predicate == null)
{
throw new ArgumentNullException("Predicate cannot be null");
}
var startTime = Time.realtimeSinceStartup;
while (Time.realtimeSinceStartup - startTime < timeout && !representation.SpawnManager.SpawnedObjects.Any(x => predicate(x.Value)))
{
var nextFrameNumber = Time.frameCount + 1;
yield return new WaitUntil(() => Time.frameCount >= nextFrameNumber);
}
result.Result = representation.SpawnManager.SpawnedObjects.FirstOrDefault(x => predicate(x.Value)).Value;
if (failIfNull && result.Result == null)
{
Assert.Fail("NetworkObject could not be found");
}
}
/// <summary>
/// Waits for a predicate condition to be met
/// </summary>
/// <param name="predicate">The predicate to wait for</param>
/// <param name="result">The result. If null, it will fail if the predicate is not met</param>
/// <param name="minFrames">The min frames to wait for</param>
/// <param name="maxFrames">The max frames to wait for</param>
public static IEnumerator WaitForCondition(Func<bool> predicate, ResultWrapper<bool> result = null, float timeout = DefaultTimeout, int minFrames = DefaultMinFrames)
{
if (predicate == null)
{
throw new ArgumentNullException("Predicate cannot be null");
}
var startTime = Time.realtimeSinceStartup;
if (minFrames > 0)
{
yield return new WaitUntil(() => Time.frameCount >= minFrames);
}
while (Time.realtimeSinceStartup - startTime < timeout && !predicate())
{
// Changed to 2 frames to avoid the scenario where it would take 1+ frames to
// see a value change (i.e. discovered in the NetworkTransformTests)
var nextFrameNumber = Time.frameCount + 2;
yield return new WaitUntil(() => Time.frameCount >= nextFrameNumber);
}
var res = predicate();
if (result != null)
{
result.Result = res;
}
else
{
Assert.True(res, "PREDICATE CONDITION");
}
}
/// <summary>
/// Waits for a message of the given type to be received
/// </summary>
/// <param name="result">The result. If null, it will fail if the predicate is not met</param>
/// <param name="timeout">The max time in seconds to wait for</param>
internal static IEnumerator WaitForMessageOfType<T>(NetworkManager toBeReceivedBy, ResultWrapper<bool> result = null, float timeout = 0.5f) where T : INetworkMessage
{
var hooks = s_Hooks[toBeReceivedBy];
if (!hooks.HandleChecks.ContainsKey(typeof(T)))
{
hooks.HandleChecks.Add(typeof(T), new List<MessageHandleCheckWithResult>());
}
var check = new MessageHandleCheckWithResult { Check = MultiInstanceHooks.CheckForMessageOfType<T> };
hooks.HandleChecks[typeof(T)].Add(check);
if (result == null)
{
result = new ResultWrapper<bool>();
}
yield return ExecuteWaitForHook(check, result, timeout);
Assert.True(result.Result, $"Expected message {typeof(T).Name} was not received within {timeout}s.");
}
/// <summary>
/// Waits for a specific message, defined by a user callback, to be received
/// </summary>
/// <param name="requirement">Called for each received message to check if it's the right one</param>
/// <param name="result">The result. If null, it will fail if the predicate is not met</param>
/// <param name="timeout">The max time in seconds to wait for</param>
internal static IEnumerator WaitForMessageMeetingRequirement<T>(NetworkManager toBeReceivedBy, MessageHandleCheck requirement, ResultWrapper<bool> result = null, float timeout = DefaultTimeout)
{
var hooks = s_Hooks[toBeReceivedBy];
if (!hooks.HandleChecks.ContainsKey(typeof(T)))
{
hooks.HandleChecks.Add(typeof(T), new List<MessageHandleCheckWithResult>());
}
var check = new MessageHandleCheckWithResult { Check = requirement };
hooks.HandleChecks[typeof(T)].Add(check);
if (result == null)
{
result = new ResultWrapper<bool>();
}
yield return ExecuteWaitForHook(check, result, timeout);
Assert.True(result.Result, $"Expected message meeting user requirements was not received within {timeout}s.");
}
private static IEnumerator ExecuteWaitForHook(MessageHandleCheckWithResult check, ResultWrapper<bool> result, float timeout)
{
var startTime = Time.realtimeSinceStartup;
while (!check.Result && Time.realtimeSinceStartup - startTime < timeout)
{
yield return null;
}
var res = check.Result;
result.Result = res;
}
}
// Empty MonoBehaviour that is a holder of coroutine
internal class CoroutineRunner : MonoBehaviour
{
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a1c62160e3e5b4489b2143fc21b56e55
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,269 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using NUnit.Framework;
namespace Unity.Netcode.TestHelpers.Runtime
{
/// <summary>
/// Helper class to instantiate a NetworkManager
/// This also provides the ability to:
/// --- instantiate GameObjects with NetworkObject components that returns a Guid for accessing it later.
/// --- add NetworkBehaviour components to the instantiated GameObjects
/// --- spawn a NetworkObject using its parent GameObject's Guid
/// Call StartNetworkManager in the constructor of your runtime unit test class.
/// Call ShutdownNetworkManager in the destructor of your runtime unit test class.
///
/// Includes a useful "BuffersMatch" method that allows you to compare two buffers (returns true if they match false if not)
/// </summary>
public static class NetworkManagerHelper
{
public static NetworkManager NetworkManagerObject { get; internal set; }
public static GameObject NetworkManagerGameObject { get; internal set; }
public static Dictionary<Guid, GameObject> InstantiatedGameObjects = new Dictionary<Guid, GameObject>();
public static Dictionary<Guid, NetworkObject> InstantiatedNetworkObjects = new Dictionary<Guid, NetworkObject>();
public static NetworkManagerOperatingMode CurrentNetworkManagerMode;
/// <summary>
/// This provides the ability to start NetworkManager in various modes
/// </summary>
public enum NetworkManagerOperatingMode
{
None,
Host,
Server,
Client,
}
/// <summary>
/// Called upon the RpcQueueTests being instantiated.
/// This creates an instance of the NetworkManager to be used during unit tests.
/// Currently, the best method to run unit tests is by starting in host mode as you can
/// send messages to yourself (i.e. Host-Client to Host-Server and vice versa).
/// As such, the default setting is to start in Host mode.
/// </summary>
/// <param name="managerMode">parameter to specify which mode you want to start the NetworkManager</param>
/// <param name="networkConfig">parameter to specify custom NetworkConfig settings</param>
/// <returns>true if it was instantiated or is already instantiate otherwise false means it failed to instantiate</returns>
public static bool StartNetworkManager(out NetworkManager networkManager, NetworkManagerOperatingMode managerMode = NetworkManagerOperatingMode.Host, NetworkConfig networkConfig = null)
{
// If we are changing the current manager mode and the current manager mode is not "None", then stop the NetworkManager mode
if (CurrentNetworkManagerMode != managerMode && CurrentNetworkManagerMode != NetworkManagerOperatingMode.None)
{
StopNetworkManagerMode();
}
if (NetworkManagerGameObject == null)
{
NetworkManagerGameObject = new GameObject(nameof(NetworkManager));
NetworkManagerObject = NetworkManagerGameObject.AddComponent<NetworkManager>();
if (NetworkManagerObject == null)
{
networkManager = null;
return false;
}
Debug.Log($"{nameof(NetworkManager)} Instantiated.");
// NOTE: For now we only use SIPTransport for tests until UnityTransport
// has been verified working in nightly builds
// TODO-MTT-2486: Provide support for other transports once tested and verified
// working on consoles.
var sipTransport = NetworkManagerGameObject.AddComponent<SIPTransport>();
if (networkConfig == null)
{
networkConfig = new NetworkConfig
{
EnableSceneManagement = false,
};
}
NetworkManagerObject.NetworkConfig = networkConfig;
NetworkManagerObject.NetworkConfig.NetworkTransport = sipTransport;
// Starts the network manager in the mode specified
StartNetworkManagerMode(managerMode);
}
networkManager = NetworkManagerObject;
return true;
}
/// <summary>
/// Add a GameObject with a NetworkObject component
/// </summary>
/// <param name="nameOfGameObject">the name of the object</param>
/// <returns></returns>
public static Guid AddGameNetworkObject(string nameOfGameObject)
{
var gameObjectId = Guid.NewGuid();
// Create the player object that we will spawn as a host
var gameObject = new GameObject(nameOfGameObject);
Assert.IsNotNull(gameObject);
var networkObject = gameObject.AddComponent<NetworkObject>();
Assert.IsNotNull(networkObject);
Assert.IsFalse(InstantiatedGameObjects.ContainsKey(gameObjectId));
Assert.IsFalse(InstantiatedNetworkObjects.ContainsKey(gameObjectId));
InstantiatedGameObjects.Add(gameObjectId, gameObject);
InstantiatedNetworkObjects.Add(gameObjectId, networkObject);
return gameObjectId;
}
/// <summary>
/// Helper class to add a component to the GameObject with a NetoworkObject component
/// </summary>
/// <typeparam name="T">NetworkBehaviour component being added to the GameObject</typeparam>
/// <param name="gameObjectIdentifier">ID returned to reference the game object</param>
/// <returns></returns>
public static T AddComponentToObject<T>(Guid gameObjectIdentifier) where T : NetworkBehaviour
{
Assert.IsTrue(InstantiatedGameObjects.ContainsKey(gameObjectIdentifier));
return InstantiatedGameObjects[gameObjectIdentifier].AddComponent<T>();
}
/// <summary>
/// Spawn the NetworkObject, so Rpcs can flow
/// </summary>
/// <param name="gameObjectIdentifier">ID returned to reference the game object</param>
public static void SpawnNetworkObject(Guid gameObjectIdentifier)
{
Assert.IsTrue(InstantiatedNetworkObjects.ContainsKey(gameObjectIdentifier));
if (!InstantiatedNetworkObjects[gameObjectIdentifier].IsSpawned)
{
InstantiatedNetworkObjects[gameObjectIdentifier].Spawn();
}
}
/// <summary>
/// Starts the NetworkManager in the current mode specified by managerMode
/// </summary>
/// <param name="managerMode">the mode to start the NetworkManager as</param>
private static void StartNetworkManagerMode(NetworkManagerOperatingMode managerMode)
{
CurrentNetworkManagerMode = managerMode;
switch (CurrentNetworkManagerMode)
{
case NetworkManagerOperatingMode.Host:
{
// Starts the host
NetworkManagerObject.StartHost();
break;
}
case NetworkManagerOperatingMode.Server:
{
// Starts the server
NetworkManagerObject.StartServer();
break;
}
case NetworkManagerOperatingMode.Client:
{
// Starts the client
NetworkManagerObject.StartClient();
break;
}
}
// If we started an netcode session
if (CurrentNetworkManagerMode != NetworkManagerOperatingMode.None)
{
// With some unit tests the Singleton can still be from a previous unit test
// depending upon the order of operations that occurred.
if (NetworkManager.Singleton != NetworkManagerObject)
{
NetworkManagerObject.SetSingleton();
}
// Only log this if we started an netcode session
Debug.Log($"{CurrentNetworkManagerMode} started.");
}
}
/// <summary>
/// Stops the current mode of the NetworkManager
/// </summary>
private static void StopNetworkManagerMode()
{
NetworkManagerObject.Shutdown();
Debug.Log($"{CurrentNetworkManagerMode} stopped.");
CurrentNetworkManagerMode = NetworkManagerOperatingMode.None;
}
// This is called, even if we assert and exit early from a test
public static void ShutdownNetworkManager()
{
// clean up any game objects created with custom unit testing components
foreach (var entry in InstantiatedGameObjects)
{
UnityEngine.Object.DestroyImmediate(entry.Value);
}
InstantiatedGameObjects.Clear();
if (NetworkManagerGameObject != null)
{
Debug.Log($"{nameof(NetworkManager)} shutdown.");
StopNetworkManagerMode();
UnityEngine.Object.DestroyImmediate(NetworkManagerGameObject);
Debug.Log($"{nameof(NetworkManager)} destroyed.");
}
NetworkManagerGameObject = null;
NetworkManagerObject = null;
}
public static bool BuffersMatch(int indexOffset, long targetSize, byte[] sourceArray, byte[] originalArray)
{
long largeInt64Blocks = targetSize >> 3; // Divide by 8
int originalArrayOffset = 0;
// process by 8 byte blocks if we can
for (long i = 0; i < largeInt64Blocks; i++)
{
if (BitConverter.ToInt64(sourceArray, indexOffset) != BitConverter.ToInt64(originalArray, originalArrayOffset))
{
return false;
}
indexOffset += 8;
originalArrayOffset += 8;
}
long offset = largeInt64Blocks * 8;
long remainder = targetSize - offset;
// 4 byte block
if (remainder >= 4)
{
if (BitConverter.ToInt32(sourceArray, indexOffset) != BitConverter.ToInt32(originalArray, originalArrayOffset))
{
return false;
}
indexOffset += 4;
originalArrayOffset += 4;
offset += 4;
}
// Remainder of bytes < 4
if (targetSize - offset > 0)
{
for (long i = 0; i < (targetSize - offset); i++)
{
if (sourceArray[indexOffset + i] != originalArray[originalArrayOffset + i])
{
return false;
}
}
}
return true;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c920be150fd14ad4ca1936e1a259417c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,153 @@
using System;
using System.Collections.Generic;
namespace Unity.Netcode.TestHelpers.Runtime
{
/// <summary>
/// Will automatically register for the NetworkVariable OnValueChanged
/// delegate handler. It then will expose that single delegate invocation
/// to anything that registers for this NetworkVariableHelper's instance's OnValueChanged event.
/// This allows us to register any NetworkVariable type as well as there are basically two "types of types":
/// IEquatable<T>
/// ValueType
/// From both we can then at least determine if the value indeed changed
/// </summary>
/// <typeparam name="T"></typeparam>
public class NetworkVariableHelper<T> : NetworkVariableBaseHelper where T : unmanaged
{
private readonly NetworkVariable<T> m_NetworkVariable;
public delegate void OnMyValueChangedDelegateHandler(T previous, T next);
public event OnMyValueChangedDelegateHandler OnValueChanged;
/// <summary>
/// IEquatable<T> Equals Check
/// </summary>
private void CheckVariableChanged(IEquatable<T> previous, IEquatable<T> next)
{
if (!previous.Equals(next))
{
ValueChanged();
}
}
/// <summary>
/// ValueType Equals Check
/// </summary>
private void CheckVariableChanged(ValueType previous, ValueType next)
{
if (!previous.Equals(next))
{
ValueChanged();
}
}
/// <summary>
/// INetworkVariable's OnVariableChanged delegate callback
/// </summary>
/// <param name="previous"></param>
/// <param name="next"></param>
private void OnVariableChanged(T previous, T next)
{
if (previous is ValueType testValueType)
{
CheckVariableChanged(previous, next);
}
else
{
CheckVariableChanged(previous as IEquatable<T>, next as IEquatable<T>);
}
OnValueChanged?.Invoke(previous, next);
}
public NetworkVariableHelper(NetworkVariableBase networkVariable) : base(networkVariable)
{
m_NetworkVariable = networkVariable as NetworkVariable<T>;
m_NetworkVariable.OnValueChanged = OnVariableChanged;
}
}
/// <summary>
/// The BaseNetworkVariableHelper keeps track of:
/// The number of instances and associates the instance with the NetworkVariable
/// The number of times a specific NetworkVariable instance had its value changed (i.e. !Equal)
/// Note: This could be expanded for future tests focuses around NetworkVariables
/// </summary>
public class NetworkVariableBaseHelper
{
private static Dictionary<NetworkVariableBaseHelper, NetworkVariableBase> s_Instances;
private static Dictionary<NetworkVariableBase, int> s_InstanceChangedCount;
/// <summary>
/// Returns the total number of registered INetworkVariables
/// </summary>
public static int InstanceCount
{
get
{
if (s_Instances != null)
{
return s_Instances.Count;
}
return 0;
}
}
/// <summary>
/// Returns total number of changes that occurred for all registered INetworkVariables
/// </summary>
public static int VarChangedCount
{
get
{
if (s_InstanceChangedCount != null)
{
var changeCount = 0;
foreach (var keyPair in s_InstanceChangedCount)
{
changeCount += keyPair.Value;
}
return changeCount;
}
return 0;
}
}
/// <summary>
/// Called by the child class NetworkVariableHelper when a value changed
/// </summary>
protected void ValueChanged()
{
if (s_Instances.ContainsKey(this))
{
if (s_InstanceChangedCount.ContainsKey(s_Instances[this]))
{
s_InstanceChangedCount[s_Instances[this]]++;
}
}
}
public NetworkVariableBaseHelper(NetworkVariableBase networkVariable)
{
if (s_Instances == null)
{
s_Instances = new Dictionary<NetworkVariableBaseHelper, NetworkVariableBase>();
}
if (s_InstanceChangedCount == null)
{
s_InstanceChangedCount = new Dictionary<NetworkVariableBase, int>();
}
// Register new instance and associated INetworkVariable
if (!s_Instances.ContainsKey(this))
{
s_Instances.Add(this, networkVariable);
if (!s_InstanceChangedCount.ContainsKey(networkVariable))
{
s_InstanceChangedCount.Add(networkVariable, 0);
}
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 218bb185a48c6f9449e1c74f4855a774
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,42 @@
using UnityEngine;
namespace Unity.Netcode.TestHelpers.Runtime
{
/// <summary>
/// Can be used independently or assigned to <see cref="NetcodeIntegrationTest.WaitForConditionOrTimeOut"></see> in the
/// event the default timeout period needs to be adjusted
/// </summary>
public class TimeoutHelper
{
private const float k_DefaultTimeOutWaitPeriod = 2.0f;
private float m_MaximumTimeBeforeTimeOut;
private float m_TimeOutPeriod;
private bool m_IsStarted;
public bool TimedOut { get; internal set; }
public void Start()
{
m_MaximumTimeBeforeTimeOut = Time.realtimeSinceStartup + m_TimeOutPeriod;
m_IsStarted = true;
TimedOut = false;
}
public void Stop()
{
TimedOut = HasTimedOut();
m_IsStarted = false;
}
public bool HasTimedOut()
{
return m_IsStarted ? m_MaximumTimeBeforeTimeOut < Time.realtimeSinceStartup : TimedOut;
}
public TimeoutHelper(float timeOutPeriod = k_DefaultTimeOutWaitPeriod)
{
m_TimeOutPeriod = timeOutPeriod;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: dcb37435a7d719d4795b6916d4ad12e6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: d764f651f0e54e8281952933cc49be97
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,70 @@
using System;
namespace Unity.Netcode.TestHelpers.Runtime
{
internal class MessageHooks : INetworkHooks
{
public bool IsWaiting;
public delegate bool MessageReceiptCheck(object receivedMessage);
public MessageReceiptCheck ReceiptCheck;
public static bool CheckForMessageOfType<T>(object receivedMessage) where T : INetworkMessage
{
return receivedMessage is T;
}
public void OnBeforeSendMessage<T>(ulong clientId, ref T message, NetworkDelivery delivery) where T : INetworkMessage
{
}
public void OnAfterSendMessage<T>(ulong clientId, ref T message, NetworkDelivery delivery, int messageSizeBytes) where T : INetworkMessage
{
}
public void OnBeforeReceiveMessage(ulong senderId, Type messageType, int messageSizeBytes)
{
}
public void OnAfterReceiveMessage(ulong senderId, Type messageType, int messageSizeBytes)
{
}
public void OnBeforeSendBatch(ulong clientId, int messageCount, int batchSizeInBytes, NetworkDelivery delivery)
{
}
public void OnAfterSendBatch(ulong clientId, int messageCount, int batchSizeInBytes, NetworkDelivery delivery)
{
}
public void OnBeforeReceiveBatch(ulong senderId, int messageCount, int batchSizeInBytes)
{
}
public void OnAfterReceiveBatch(ulong senderId, int messageCount, int batchSizeInBytes)
{
}
public bool OnVerifyCanSend(ulong destinationId, Type messageType, NetworkDelivery delivery)
{
return true;
}
public bool OnVerifyCanReceive(ulong senderId, Type messageType)
{
return true;
}
public void OnBeforeHandleMessage<T>(ref T message, ref NetworkContext context) where T : INetworkMessage
{
}
public void OnAfterHandleMessage<T>(ref T message, ref NetworkContext context) where T : INetworkMessage
{
if (IsWaiting && (ReceiptCheck == null || ReceiptCheck.Invoke(message)))
{
IsWaiting = false;
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e860701e80ca3be4d8cb89d340faebd1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,95 @@
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
namespace Unity.Netcode.TestHelpers.Runtime
{
public class MessageHooksConditional : ConditionalPredicateBase
{
private List<MessageHookEntry> m_MessageHookEntries;
public bool AllMessagesReceived { get; internal set; }
public int NumberOfMessagesReceived
{
get
{
return m_MessageHookEntries.Where((c) => !c.MessageHooks.IsWaiting).Count();
}
}
public string GetHooksStillWaiting()
{
var retMessageTypes = string.Empty;
var waitingMessages = m_MessageHookEntries.Where((c) => c.MessageHooks.IsWaiting);
foreach (var waitingMessage in waitingMessages)
{
retMessageTypes += $":{waitingMessage.MessageType}:";
}
return retMessageTypes;
}
protected override bool OnHasConditionBeenReached()
{
AllMessagesReceived = NumberOfMessagesReceived == m_MessageHookEntries.Count;
if (AllMessagesReceived)
{
return AllMessagesReceived;
}
return AllMessagesReceived;
}
protected override void OnFinished()
{
base.OnFinished();
}
public void Reset()
{
foreach (var entry in m_MessageHookEntries)
{
entry.Initialize();
}
}
public MessageHooksConditional(List<MessageHookEntry> messageHookEntries)
{
m_MessageHookEntries = messageHookEntries;
}
}
public class MessageHookEntry
{
internal MessageHooks MessageHooks;
protected NetworkManager m_NetworkManager;
private MessageHooks.MessageReceiptCheck m_MessageReceiptCheck;
internal string MessageType;
public void Initialize()
{
Assert.IsNotNull(m_MessageReceiptCheck, $"{nameof(m_MessageReceiptCheck)} is null, did you forget to initialize?");
MessageHooks = new MessageHooks();
MessageHooks.ReceiptCheck = m_MessageReceiptCheck;
Assert.IsNotNull(m_NetworkManager.MessagingSystem, $"{nameof(NetworkManager.MessagingSystem)} is null! Did you forget to start first?");
m_NetworkManager.MessagingSystem.Hook(MessageHooks);
}
internal void AssignMessageType<T>() where T : INetworkMessage
{
MessageType = typeof(T).Name;
m_MessageReceiptCheck = MessageHooks.CheckForMessageOfType<T>;
Initialize();
}
public MessageHookEntry(NetworkManager networkManager)
{
m_NetworkManager = networkManager;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2a97b3c56f3b2ba42a0669716e81674f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,267 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Unity.Netcode.TestHelpers.Runtime
{
/// <summary>
/// SIPTransport (SIngleProcessTransport)
/// is a NetworkTransport designed to be used with multiple network instances in a single process
/// it's designed for the netcode in a way where no networking stack has to be available
/// it's designed for testing purposes and it's not designed with speed in mind
/// </summary>
public class SIPTransport : TestingNetworkTransport
{
private struct Event
{
public NetworkEvent Type;
public ulong ConnectionId;
public ArraySegment<byte> Data;
}
private class Peer
{
public ulong ConnectionId;
public SIPTransport Transport;
public Queue<Event> IncomingBuffer = new Queue<Event>();
}
private readonly Dictionary<ulong, Peer> m_Peers = new Dictionary<ulong, Peer>();
private ulong m_ClientsCounter = 1;
private static Peer s_Server;
private Peer m_LocalConnection;
public override ulong ServerClientId => 0;
public ulong LocalClientId;
public override void DisconnectLocalClient()
{
if (m_LocalConnection != null)
{
// Inject local disconnect
m_LocalConnection.IncomingBuffer.Enqueue(new Event
{
Type = NetworkEvent.Disconnect,
ConnectionId = m_LocalConnection.ConnectionId,
Data = new ArraySegment<byte>()
});
if (s_Server != null && m_LocalConnection != null)
{
// Remove the connection
s_Server.Transport.m_Peers.Remove(m_LocalConnection.ConnectionId);
}
if (m_LocalConnection.ConnectionId == ServerClientId)
{
StopServer();
}
// Remove the local connection
m_LocalConnection = null;
}
}
// Called by server
public override void DisconnectRemoteClient(ulong clientId)
{
if (m_Peers.ContainsKey(clientId))
{
// Inject disconnect into remote
m_Peers[clientId].IncomingBuffer.Enqueue(new Event
{
Type = NetworkEvent.Disconnect,
ConnectionId = clientId,
Data = new ArraySegment<byte>()
});
// Inject local disconnect
m_LocalConnection.IncomingBuffer.Enqueue(new Event
{
Type = NetworkEvent.Disconnect,
ConnectionId = clientId,
Data = new ArraySegment<byte>()
});
// Remove the local connection on remote
m_Peers[clientId].Transport.m_LocalConnection = null;
// Remove connection on server
m_Peers.Remove(clientId);
}
}
public override ulong GetCurrentRtt(ulong clientId)
{
// Always returns 50ms
return 50;
}
public override void Initialize(NetworkManager networkManager = null)
{
}
private void StopServer()
{
s_Server = null;
m_Peers.Remove(ServerClientId);
m_LocalConnection = null;
}
public override void Shutdown()
{
// Inject disconnects to all the remotes
foreach (KeyValuePair<ulong, Peer> onePeer in m_Peers)
{
onePeer.Value.IncomingBuffer.Enqueue(new Event
{
Type = NetworkEvent.Disconnect,
ConnectionId = LocalClientId,
Data = new ArraySegment<byte>()
});
}
if (m_LocalConnection != null && m_LocalConnection.ConnectionId == ServerClientId)
{
StopServer();
}
// TODO: Cleanup
}
public override bool StartClient()
{
if (s_Server == null)
{
// No server
Debug.LogError("No server");
return false;
}
if (m_LocalConnection != null)
{
// Already connected
Debug.LogError("Already connected");
return false;
}
// Generate an Id for the server that represents this client
ulong serverConnectionId = ++s_Server.Transport.m_ClientsCounter;
LocalClientId = serverConnectionId;
// Create local connection
m_LocalConnection = new Peer()
{
ConnectionId = serverConnectionId,
Transport = this,
IncomingBuffer = new Queue<Event>()
};
// Add the server as a local connection
m_Peers.Add(ServerClientId, s_Server);
// Add local connection as a connection on the server
s_Server.Transport.m_Peers.Add(serverConnectionId, m_LocalConnection);
// Sends a connect message to the server
s_Server.Transport.m_LocalConnection.IncomingBuffer.Enqueue(new Event()
{
Type = NetworkEvent.Connect,
ConnectionId = serverConnectionId,
Data = new ArraySegment<byte>()
});
// Send a local connect message
m_LocalConnection.IncomingBuffer.Enqueue(new Event
{
Type = NetworkEvent.Connect,
ConnectionId = ServerClientId,
Data = new ArraySegment<byte>()
});
return true;
}
public override bool StartServer()
{
if (s_Server != null)
{
// Can only have one server
Debug.LogError("Server already started");
return false;
}
if (m_LocalConnection != null)
{
// Already connected
Debug.LogError("Already connected");
return false;
}
// Create local connection
m_LocalConnection = new Peer()
{
ConnectionId = ServerClientId,
Transport = this,
IncomingBuffer = new Queue<Event>()
};
// Set the local connection as the server
s_Server = m_LocalConnection;
m_Peers.Add(ServerClientId, s_Server);
return true;
}
public override void Send(ulong clientId, ArraySegment<byte> payload, NetworkDelivery networkDelivery)
{
if (m_LocalConnection != null)
{
// Create copy since netcode wants the byte array back straight after the method call.
// Hard on GC.
byte[] copy = new byte[payload.Count];
Buffer.BlockCopy(payload.Array, payload.Offset, copy, 0, payload.Count);
if (m_Peers.ContainsKey(clientId))
{
m_Peers[clientId].IncomingBuffer.Enqueue(new Event
{
Type = NetworkEvent.Data,
ConnectionId = m_LocalConnection.ConnectionId,
Data = new ArraySegment<byte>(copy)
});
}
}
}
public override NetworkEvent PollEvent(out ulong clientId, out ArraySegment<byte> payload, out float receiveTime)
{
if (m_LocalConnection != null)
{
if (m_LocalConnection.IncomingBuffer.Count == 0)
{
clientId = 0;
payload = new ArraySegment<byte>();
receiveTime = 0;
return NetworkEvent.Nothing;
}
var peerEvent = m_LocalConnection.IncomingBuffer.Dequeue();
clientId = peerEvent.ConnectionId;
payload = peerEvent.Data;
receiveTime = 0;
return peerEvent.Type;
}
clientId = 0;
payload = new ArraySegment<byte>();
receiveTime = 0;
return NetworkEvent.Nothing;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 1fd1b14eba874a189f13f12d343c331c
timeCreated: 1620145176

View File

@@ -0,0 +1,32 @@
{
"name": "Unity.Netcode.TestHelpers.Runtime",
"rootNamespace": "Unity.Netcode.TestHelpers.Runtime",
"references": [
"Unity.Netcode.Runtime",
"Unity.Netcode.Adapter.UTP",
"Unity.Multiplayer.MetricTypes",
"Unity.Multiplayer.NetStats",
"Unity.Multiplayer.Tools.MetricTypes",
"Unity.Multiplayer.Tools.NetStats"
],
"optionalUnityReferences": [
"TestAssemblies"
],
"versionDefines": [
{
"name": "com.unity.multiplayer.tools",
"expression": "",
"define": "MULTIPLAYER_TOOLS"
},
{
"name": "com.unity.netcode.adapter.utp",
"expression": "",
"define": "UTP_ADAPTER"
},
{
"name": "com.unity.multiplayer.tools",
"expression": "1.0.0-pre.4",
"define": "MULTIPLAYER_TOOLS_1_0_0_PRE_4"
}
]
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 101012598ee9f064da897a34e72947f9
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant: