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.10] - 2022-06-21 ### Added - Added a new `OnTransportFailure` callback to `NetworkManager`. This callback is invoked when the manager's `NetworkTransport` encounters an unrecoverable error. Transport failures also cause the `NetworkManager` to shut down. Currently, this is only used by `UnityTransport` to signal a timeout of its connection to the Unity Relay servers. (#1994) - Added `NetworkEvent.TransportFailure`, which can be used by implementations of `NetworkTransport` to signal to `NetworkManager` that an unrecoverable error was encountered. (#1994) - Added test to ensure a warning occurs when nesting NetworkObjects in a NetworkPrefab (#1969) - Added `NetworkManager.RemoveNetworkPrefab(...)` to remove a prefab from the prefabs list (#1950) ### Changed - Updated `UnityTransport` dependency on `com.unity.transport` to 1.1.0. (#2025) - (API Breaking) `ConnectionApprovalCallback` is no longer an `event` and will not allow more than 1 handler registered at a time. Also, `ConnectionApprovalCallback` is now a `Func<>` taking `ConnectionApprovalRequest` in and returning `ConnectionApprovalResponse` back out (#1972) ### Removed ### Fixed - Fixed issue where dynamically spawned `NetworkObject`s could throw an exception if the scene of origin handle was zero (0) and the `NetworkObject` was already spawned. (#2017) - Fixed issue where `NetworkObject.Observers` was not being cleared when despawned. (#2009) - Fixed `NetworkAnimator` could not run in the server authoritative mode. (#2003) - Fixed issue where late joining clients would get a soft synchronization error if any in-scene placed NetworkObjects were parented under another `NetworkObject`. (#1985) - Fixed issue where `NetworkBehaviourReference` would throw a type cast exception if using `NetworkBehaviourReference.TryGet` and the component type was not found. (#1984) - Fixed `NetworkSceneManager` was not sending scene event notifications for the currently active scene and any additively loaded scenes when loading a new scene in `LoadSceneMode.Single` mode. (#1975) - Fixed issue where one or more clients disconnecting during a scene event would cause `LoadEventCompleted` or `UnloadEventCompleted` to wait until the `NetworkConfig.LoadSceneTimeOut` period before being triggered. (#1973) - Fixed issues when multiple `ConnectionApprovalCallback`s were registered (#1972) - Fixed a regression in serialization support: `FixedString`, `Vector2Int`, and `Vector3Int` types can now be used in NetworkVariables and RPCs again without requiring a `ForceNetworkSerializeByMemcpy<>` wrapper. (#1961) - Fixed generic types that inherit from NetworkBehaviour causing crashes at compile time. (#1976) - Fixed endless dialog boxes when adding a `NetworkBehaviour` to a `NetworkManager` or vice-versa. (#1947) - Fixed `NetworkAnimator` issue where it was only synchronizing parameters if the layer or state changed or was transitioning between states. (#1946) - Fixed `NetworkAnimator` issue where when it did detect a parameter had changed it would send all parameters as opposed to only the parameters that changed. (#1946) - Fixed `NetworkAnimator` issue where it was not always disposing the `NativeArray` that is allocated when spawned. (#1946) - Fixed `NetworkAnimator` issue where it was not taking the animation speed or state speed multiplier into consideration. (#1946) - Fixed `NetworkAnimator` issue where it was not properly synchronizing late joining clients if they joined while `Animator` was transitioning between states. (#1946) - Fixed `NetworkAnimator` issue where the server was not relaying changes to non-owner clients when a client was the owner. (#1946) - Fixed issue where the `PacketLoss` metric for tools would return the packet loss over a connection lifetime instead of a single frame. (#2004)
413 lines
17 KiB
C#
413 lines
17 KiB
C#
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using UnityEngine;
|
|
using UnityEngine.SceneManagement;
|
|
using Object = UnityEngine.Object;
|
|
|
|
namespace Unity.Netcode.TestHelpers.Runtime
|
|
{
|
|
/// <summary>
|
|
/// The default SceneManagerHandler used for all NetcodeIntegrationTest derived children.
|
|
/// This enables clients to load scenes within the same scene hierarchy during integration
|
|
/// testing.
|
|
/// </summary>
|
|
internal class IntegrationTestSceneHandler : ISceneManagerHandler, IDisposable
|
|
{
|
|
// All IntegrationTestSceneHandler instances register their associated NetworkManager
|
|
internal static List<NetworkManager> NetworkManagers = new List<NetworkManager>();
|
|
|
|
internal static CoroutineRunner CoroutineRunner;
|
|
|
|
internal static Queue<QueuedSceneJob> QueuedSceneJobs = new Queue<QueuedSceneJob>();
|
|
internal List<Coroutine> CoroutinesRunning = new List<Coroutine>();
|
|
internal static Coroutine SceneJobProcessor;
|
|
internal static QueuedSceneJob CurrentQueuedSceneJob;
|
|
protected static WaitForSeconds s_WaitForSeconds;
|
|
|
|
|
|
public delegate bool CanClientsLoadUnloadDelegateHandler();
|
|
public static event CanClientsLoadUnloadDelegateHandler CanClientsLoad;
|
|
public static event CanClientsLoadUnloadDelegateHandler CanClientsUnload;
|
|
|
|
|
|
public static bool VerboseDebugMode;
|
|
/// <summary>
|
|
/// Used for loading scenes on the client-side during
|
|
/// an integration test
|
|
/// </summary>
|
|
internal class QueuedSceneJob
|
|
{
|
|
public enum JobTypes
|
|
{
|
|
Loading,
|
|
Unloading,
|
|
Completed
|
|
}
|
|
public JobTypes JobType;
|
|
public string SceneName;
|
|
public Scene Scene;
|
|
public ISceneManagerHandler.SceneEventAction SceneAction;
|
|
public IntegrationTestSceneHandler IntegrationTestSceneHandler;
|
|
}
|
|
|
|
internal NetworkManager NetworkManager;
|
|
|
|
internal string NetworkManagerName;
|
|
|
|
/// <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;
|
|
}
|
|
|
|
protected bool OnCanClientsUnload()
|
|
{
|
|
if (CanClientsUnload != null)
|
|
{
|
|
return CanClientsUnload.Invoke();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
internal static void VerboseDebug(string message)
|
|
{
|
|
if (VerboseDebugMode)
|
|
{
|
|
Debug.Log(message);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Processes scene loading jobs
|
|
/// </summary>
|
|
/// <param name="queuedSceneJob">job to process</param>
|
|
static internal IEnumerator ProcessLoadingSceneJob(QueuedSceneJob queuedSceneJob)
|
|
{
|
|
var itegrationTestSceneHandler = queuedSceneJob.IntegrationTestSceneHandler;
|
|
while (!itegrationTestSceneHandler.OnCanClientsLoad())
|
|
{
|
|
yield return s_WaitForSeconds;
|
|
}
|
|
|
|
SceneManager.sceneLoaded += SceneManager_sceneLoaded;
|
|
// We always load additively for all scenes during integration tests
|
|
SceneManager.LoadSceneAsync(queuedSceneJob.SceneName, LoadSceneMode.Additive);
|
|
|
|
// Wait for it to finish
|
|
while (queuedSceneJob.JobType != QueuedSceneJob.JobTypes.Completed)
|
|
{
|
|
yield return s_WaitForSeconds;
|
|
}
|
|
yield return s_WaitForSeconds;
|
|
CurrentQueuedSceneJob.SceneAction.Invoke();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles scene loading and assists with making sure the right NetworkManagerOwner
|
|
/// is assigned to newly instantiated NetworkObjects.
|
|
///
|
|
/// Note: Static property usage is OK since jobs are processed one at a time
|
|
/// </summary>
|
|
private static void SceneManager_sceneLoaded(Scene scene, LoadSceneMode loadSceneMode)
|
|
{
|
|
if (CurrentQueuedSceneJob.JobType != QueuedSceneJob.JobTypes.Completed && CurrentQueuedSceneJob.SceneName == scene.name)
|
|
{
|
|
SceneManager.sceneLoaded -= SceneManager_sceneLoaded;
|
|
|
|
ProcessInSceneObjects(scene, CurrentQueuedSceneJob.IntegrationTestSceneHandler.NetworkManager);
|
|
|
|
CurrentQueuedSceneJob.JobType = QueuedSceneJob.JobTypes.Completed;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles some pre-spawn processing of in-scene placed NetworkObjects
|
|
/// to make sure the appropriate NetworkManagerOwner is assigned. It
|
|
/// also makes sure that each in-scene placed NetworkObject has an
|
|
/// ObjectIdentifier component if one is not assigned to it or its
|
|
/// children.
|
|
/// </summary>
|
|
/// <param name="scene">the scenes that was just loaded</param>
|
|
/// <param name="networkManager">the relative NetworkManager</param>
|
|
private static void ProcessInSceneObjects(Scene scene, NetworkManager networkManager)
|
|
{
|
|
// Get all in-scene placed NeworkObjects that were instantiated when this scene loaded
|
|
var inSceneNetworkObjects = Object.FindObjectsOfType<NetworkObject>().Where((c) => c.IsSceneObject != false && c.GetSceneOriginHandle() == scene.handle);
|
|
foreach (var sobj in inSceneNetworkObjects)
|
|
{
|
|
if (sobj.NetworkManagerOwner != networkManager)
|
|
{
|
|
sobj.NetworkManagerOwner = networkManager;
|
|
}
|
|
if (sobj.GetComponent<ObjectNameIdentifier>() == null && sobj.GetComponentInChildren<ObjectNameIdentifier>() == null)
|
|
{
|
|
sobj.gameObject.AddComponent<ObjectNameIdentifier>();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Processes scene unloading jobs
|
|
/// </summary>
|
|
/// <param name="queuedSceneJob">job to process</param>
|
|
static internal IEnumerator ProcessUnloadingSceneJob(QueuedSceneJob queuedSceneJob)
|
|
{
|
|
var itegrationTestSceneHandler = queuedSceneJob.IntegrationTestSceneHandler;
|
|
while (!itegrationTestSceneHandler.OnCanClientsUnload())
|
|
{
|
|
yield return s_WaitForSeconds;
|
|
}
|
|
|
|
SceneManager.sceneUnloaded += SceneManager_sceneUnloaded;
|
|
if (queuedSceneJob.Scene.IsValid() && queuedSceneJob.Scene.isLoaded && !queuedSceneJob.Scene.name.Contains(NetcodeIntegrationTestHelpers.FirstPartOfTestRunnerSceneName))
|
|
{
|
|
SceneManager.UnloadSceneAsync(queuedSceneJob.Scene);
|
|
}
|
|
else
|
|
{
|
|
CurrentQueuedSceneJob.JobType = QueuedSceneJob.JobTypes.Completed;
|
|
}
|
|
|
|
// Wait for it to finish
|
|
while (queuedSceneJob.JobType != QueuedSceneJob.JobTypes.Completed)
|
|
{
|
|
yield return s_WaitForSeconds;
|
|
}
|
|
CurrentQueuedSceneJob.SceneAction.Invoke();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles closing out scene unloading jobs
|
|
/// </summary>
|
|
private static void SceneManager_sceneUnloaded(Scene scene)
|
|
{
|
|
if (CurrentQueuedSceneJob.JobType != QueuedSceneJob.JobTypes.Completed && CurrentQueuedSceneJob.Scene.name == scene.name)
|
|
{
|
|
SceneManager.sceneUnloaded -= SceneManager_sceneUnloaded;
|
|
|
|
CurrentQueuedSceneJob.JobType = QueuedSceneJob.JobTypes.Completed;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Processes all jobs within the queue.
|
|
/// When all jobs are finished, the coroutine stops.
|
|
/// </summary>
|
|
static internal IEnumerator JobQueueProcessor()
|
|
{
|
|
while (QueuedSceneJobs.Count != 0)
|
|
{
|
|
CurrentQueuedSceneJob = QueuedSceneJobs.Dequeue();
|
|
VerboseDebug($"[ITSH-START] {CurrentQueuedSceneJob.IntegrationTestSceneHandler.NetworkManagerName} processing {CurrentQueuedSceneJob.JobType} for scene {CurrentQueuedSceneJob.SceneName}.");
|
|
if (CurrentQueuedSceneJob.JobType == QueuedSceneJob.JobTypes.Loading)
|
|
{
|
|
yield return ProcessLoadingSceneJob(CurrentQueuedSceneJob);
|
|
}
|
|
else if (CurrentQueuedSceneJob.JobType == QueuedSceneJob.JobTypes.Unloading)
|
|
{
|
|
yield return ProcessUnloadingSceneJob(CurrentQueuedSceneJob);
|
|
}
|
|
VerboseDebug($"[ITSH-STOP] {CurrentQueuedSceneJob.IntegrationTestSceneHandler.NetworkManagerName} processing {CurrentQueuedSceneJob.JobType} for scene {CurrentQueuedSceneJob.SceneName}.");
|
|
}
|
|
SceneJobProcessor = null;
|
|
yield break;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds a job to the job queue, and if the JobQueueProcessor coroutine
|
|
/// is not running then it will be started as well.
|
|
/// </summary>
|
|
/// <param name="queuedSceneJob">job to add to the queue</param>
|
|
private void AddJobToQueue(QueuedSceneJob queuedSceneJob)
|
|
{
|
|
QueuedSceneJobs.Enqueue(queuedSceneJob);
|
|
if (SceneJobProcessor == null)
|
|
{
|
|
SceneJobProcessor = CoroutineRunner.StartCoroutine(JobQueueProcessor());
|
|
}
|
|
}
|
|
|
|
private string m_ServerSceneBeingLoaded;
|
|
/// <summary>
|
|
/// Server always loads like it normally would
|
|
/// </summary>
|
|
public AsyncOperation GenericLoadSceneAsync(string sceneName, LoadSceneMode loadSceneMode, ISceneManagerHandler.SceneEventAction sceneEventAction)
|
|
{
|
|
m_ServerSceneBeingLoaded = sceneName;
|
|
if (NetcodeIntegrationTest.IsRunning)
|
|
{
|
|
SceneManager.sceneLoaded += Sever_SceneLoaded;
|
|
}
|
|
var operation = SceneManager.LoadSceneAsync(sceneName, loadSceneMode);
|
|
|
|
operation.completed += new Action<AsyncOperation>(asyncOp2 => { sceneEventAction.Invoke(); });
|
|
return operation;
|
|
}
|
|
|
|
private void Sever_SceneLoaded(Scene scene, LoadSceneMode arg1)
|
|
{
|
|
if (m_ServerSceneBeingLoaded == scene.name)
|
|
{
|
|
ProcessInSceneObjects(scene, NetworkManager);
|
|
SceneManager.sceneLoaded -= Sever_SceneLoaded;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Server always unloads like it normally would
|
|
/// </summary>
|
|
public AsyncOperation GenericUnloadSceneAsync(Scene scene, ISceneManagerHandler.SceneEventAction sceneEventAction)
|
|
{
|
|
var operation = SceneManager.UnloadSceneAsync(scene);
|
|
operation.completed += new Action<AsyncOperation>(asyncOp2 => { sceneEventAction.Invoke(); });
|
|
return operation;
|
|
}
|
|
|
|
|
|
public AsyncOperation LoadSceneAsync(string sceneName, LoadSceneMode loadSceneMode, ISceneManagerHandler.SceneEventAction sceneEventAction)
|
|
{
|
|
// Server and non NetcodeIntegrationTest tests use the generic load scene method
|
|
if (!NetcodeIntegrationTest.IsRunning)
|
|
{
|
|
return GenericLoadSceneAsync(sceneName, loadSceneMode, sceneEventAction);
|
|
}
|
|
else // NetcodeIntegrationTest Clients always get added to the jobs queue
|
|
{
|
|
AddJobToQueue(new QueuedSceneJob() { IntegrationTestSceneHandler = this, SceneName = sceneName, SceneAction = sceneEventAction, JobType = QueuedSceneJob.JobTypes.Loading });
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public AsyncOperation UnloadSceneAsync(Scene scene, ISceneManagerHandler.SceneEventAction sceneEventAction)
|
|
{
|
|
// Server and non NetcodeIntegrationTest tests use the generic unload scene method
|
|
if (!NetcodeIntegrationTest.IsRunning)
|
|
{
|
|
return GenericUnloadSceneAsync(scene, sceneEventAction);
|
|
}
|
|
else // NetcodeIntegrationTest Clients always get added to the jobs queue
|
|
{
|
|
AddJobToQueue(new QueuedSceneJob() { IntegrationTestSceneHandler = this, Scene = scene, SceneAction = sceneEventAction, JobType = QueuedSceneJob.JobTypes.Unloading });
|
|
}
|
|
// This is OK to return a "nothing" AsyncOperation since we are simulating client loading
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Replacement callback takes other NetworkManagers into consideration
|
|
/// </summary>
|
|
internal Scene GetAndAddNewlyLoadedSceneByName(string sceneName)
|
|
{
|
|
for (int i = 0; i < SceneManager.sceneCount; i++)
|
|
{
|
|
var sceneLoaded = SceneManager.GetSceneAt(i);
|
|
if (sceneLoaded.name == sceneName)
|
|
{
|
|
var skip = false;
|
|
foreach (var networkManager in NetworkManagers)
|
|
{
|
|
if (NetworkManager.LocalClientId == networkManager.LocalClientId || !networkManager.IsListening)
|
|
{
|
|
continue;
|
|
}
|
|
if (networkManager.SceneManager.ScenesLoaded.ContainsKey(sceneLoaded.handle))
|
|
{
|
|
if (NetworkManager.LogLevel == LogLevel.Developer)
|
|
{
|
|
NetworkLog.LogInfo($"{NetworkManager.name}'s ScenesLoaded contains {sceneLoaded.name} with a handle of {sceneLoaded.handle}. Skipping over scene.");
|
|
}
|
|
skip = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!skip && !NetworkManager.SceneManager.ScenesLoaded.ContainsKey(sceneLoaded.handle))
|
|
{
|
|
if (NetworkManager.LogLevel == LogLevel.Developer)
|
|
{
|
|
NetworkLog.LogInfo($"{NetworkManager.name} adding {sceneLoaded.name} with a handle of {sceneLoaded.handle} to its ScenesLoaded.");
|
|
}
|
|
NetworkManager.SceneManager.ScenesLoaded.Add(sceneLoaded.handle, sceneLoaded);
|
|
return sceneLoaded;
|
|
}
|
|
}
|
|
}
|
|
|
|
throw new Exception($"Failed to find any loaded scene named {sceneName}!");
|
|
}
|
|
|
|
private bool ExcludeSceneFromSynchronizationCheck(Scene scene)
|
|
{
|
|
if (!NetworkManager.SceneManager.ScenesLoaded.ContainsKey(scene.handle) && SceneManager.GetActiveScene().handle != scene.handle)
|
|
{
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Constructor now must take NetworkManager
|
|
/// </summary>
|
|
public IntegrationTestSceneHandler(NetworkManager networkManager)
|
|
{
|
|
networkManager.SceneManager.OverrideGetAndAddNewlyLoadedSceneByName = GetAndAddNewlyLoadedSceneByName;
|
|
networkManager.SceneManager.ExcludeSceneFromSychronization = ExcludeSceneFromSynchronizationCheck;
|
|
NetworkManagers.Add(networkManager);
|
|
NetworkManagerName = networkManager.name;
|
|
if (s_WaitForSeconds == null)
|
|
{
|
|
s_WaitForSeconds = new WaitForSeconds(1.0f / networkManager.NetworkConfig.TickRate);
|
|
}
|
|
NetworkManager = networkManager;
|
|
if (CoroutineRunner == null)
|
|
{
|
|
CoroutineRunner = new GameObject("UnitTestSceneHandlerCoroutine").AddComponent<CoroutineRunner>();
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
NetworkManagers.Clear();
|
|
if (SceneJobProcessor != null)
|
|
{
|
|
CoroutineRunner.StopCoroutine(SceneJobProcessor);
|
|
SceneJobProcessor = null;
|
|
}
|
|
|
|
foreach (var job in QueuedSceneJobs)
|
|
{
|
|
if (job.JobType != QueuedSceneJob.JobTypes.Completed)
|
|
{
|
|
if (job.JobType == QueuedSceneJob.JobTypes.Loading)
|
|
{
|
|
SceneManager.sceneLoaded -= SceneManager_sceneLoaded;
|
|
}
|
|
else
|
|
{
|
|
SceneManager.sceneUnloaded -= SceneManager_sceneUnloaded;
|
|
}
|
|
job.JobType = QueuedSceneJob.JobTypes.Completed;
|
|
}
|
|
}
|
|
QueuedSceneJobs.Clear();
|
|
Object.Destroy(CoroutineRunner.gameObject);
|
|
}
|
|
}
|
|
}
|