This repository has been archived on 2025-04-22. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
com.unity.netcode.gameobjects/TestHelpers/Runtime/IntegrationTestSceneHandler.cs
Unity Technologies fe02ca682e com.unity.netcode.gameobjects@1.2.0
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.2.0] - 2022-11-21

### Added

- Added protected method `NetworkBehaviour.OnSynchronize` which is invoked during the initial `NetworkObject` synchronization process. This provides users the ability to include custom serialization information that will be applied to the `NetworkBehaviour` prior to the `NetworkObject` being spawned. (#2298)
- Added support for different versions of the SDK to talk to each other in circumstances where changes permit it. Starting with this version and into future versions, patch versions should be compatible as long as the minor version is the same. (#2290)
- Added `NetworkObject` auto-add helper and Multiplayer Tools install reminder settings to Project Settings. (#2285)
- Added `public string DisconnectReason` getter to `NetworkManager` and `string Reason` to `ConnectionApprovalResponse`. Allows connection approval to communicate back a reason. Also added `public void DisconnectClient(ulong clientId, string reason)` allowing setting a disconnection reason, when explicitly disconnecting a client. (#2280)

### Changed

- Changed 3rd-party `XXHash` (32 & 64) implementation with an in-house reimplementation (#2310)
- When `NetworkConfig.EnsureNetworkVariableLengthSafety` is disabled `NetworkVariable` fields do not write the additional `ushort` size value (_which helps to reduce the total synchronization message size_), but when enabled it still writes the additional `ushort` value. (#2298)
- Optimized bandwidth usage by encoding most integer fields using variable-length encoding. (#2276)

### Fixed

- Fixed issue where `NetworkTransform` components nested under a parent with a `NetworkObject` component  (i.e. network prefab) would not have their associated `GameObject`'s transform synchronized. (#2298)
- Fixed issue where `NetworkObject`s that failed to instantiate could cause the entire synchronization pipeline to be disrupted/halted for a connecting client. (#2298)
- Fixed issue where in-scene placed `NetworkObject`s nested under a `GameObject` would be added to the orphaned children list causing continual console warning log messages. (#2298)
- Custom messages are now properly received by the local client when they're sent while running in host mode. (#2296)
- Fixed issue where the host would receive more than one event completed notification when loading or unloading a scene only when no clients were connected. (#2292)
- Fixed an issue in `UnityTransport` where an error would be logged if the 'Use Encryption' flag was enabled with a Relay configuration that used a secure protocol. (#2289)
- Fixed issue where in-scene placed `NetworkObjects` were not honoring the `AutoObjectParentSync` property. (#2281)
- Fixed the issue where `NetworkManager.OnClientConnectedCallback` was being invoked before in-scene placed `NetworkObject`s had been spawned when starting `NetworkManager` as a host. (#2277)
- Creating a `FastBufferReader` with `Allocator.None` will not result in extra memory being allocated for the buffer (since it's owned externally in that scenario). (#2265)

### Removed

- Removed the `NetworkObject` auto-add and Multiplayer Tools install reminder settings from the Menu interface. (#2285)
2022-11-21 00:00:00 +00:00

417 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 SceneEventProgress SceneEventProgress;
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
var asyncOperation = SceneManager.LoadSceneAsync(queuedSceneJob.SceneName, LoadSceneMode.Additive);
queuedSceneJob.SceneEventProgress.SetAsyncOperation(asyncOperation);
// Wait for it to finish
while (queuedSceneJob.JobType != QueuedSceneJob.JobTypes.Completed)
{
yield return s_WaitForSeconds;
}
yield return s_WaitForSeconds;
}
/// <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
#if UNITY_2023_1_OR_NEWER
var inSceneNetworkObjects = Object.FindObjectsByType<NetworkObject>(FindObjectsSortMode.InstanceID).Where((c) => c.IsSceneObject != false && c.GetSceneOriginHandle() == scene.handle);
#else
var inSceneNetworkObjects = Object.FindObjectsOfType<NetworkObject>().Where((c) => c.IsSceneObject != false && c.GetSceneOriginHandle() == scene.handle);
#endif
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))
{
var asyncOperation = SceneManager.UnloadSceneAsync(queuedSceneJob.Scene);
queuedSceneJob.SceneEventProgress.SetAsyncOperation(asyncOperation);
}
else
{
CurrentQueuedSceneJob.JobType = QueuedSceneJob.JobTypes.Completed;
}
// Wait for it to finish
while (queuedSceneJob.JobType != QueuedSceneJob.JobTypes.Completed)
{
yield return s_WaitForSeconds;
}
}
/// <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, SceneEventProgress sceneEventProgress)
{
m_ServerSceneBeingLoaded = sceneName;
if (NetcodeIntegrationTest.IsRunning)
{
SceneManager.sceneLoaded += Sever_SceneLoaded;
}
var operation = SceneManager.LoadSceneAsync(sceneName, loadSceneMode);
sceneEventProgress.SetAsyncOperation(operation);
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, SceneEventProgress sceneEventProgress)
{
var operation = SceneManager.UnloadSceneAsync(scene);
sceneEventProgress.SetAsyncOperation(operation);
return operation;
}
public AsyncOperation LoadSceneAsync(string sceneName, LoadSceneMode loadSceneMode, SceneEventProgress sceneEventProgress)
{
// Server and non NetcodeIntegrationTest tests use the generic load scene method
if (!NetcodeIntegrationTest.IsRunning)
{
return GenericLoadSceneAsync(sceneName, loadSceneMode, sceneEventProgress);
}
else // NetcodeIntegrationTest Clients always get added to the jobs queue
{
AddJobToQueue(new QueuedSceneJob() { IntegrationTestSceneHandler = this, SceneName = sceneName, SceneEventProgress = sceneEventProgress, JobType = QueuedSceneJob.JobTypes.Loading });
}
return null;
}
public AsyncOperation UnloadSceneAsync(Scene scene, SceneEventProgress sceneEventProgress)
{
// Server and non NetcodeIntegrationTest tests use the generic unload scene method
if (!NetcodeIntegrationTest.IsRunning)
{
return GenericUnloadSceneAsync(scene, sceneEventProgress);
}
else // NetcodeIntegrationTest Clients always get added to the jobs queue
{
AddJobToQueue(new QueuedSceneJob() { IntegrationTestSceneHandler = this, Scene = scene, SceneEventProgress = sceneEventProgress, 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);
}
}
}