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.9.1] - 2024-04-18 ### Added - Added AnticipatedNetworkVariable<T>, which adds support for client anticipation of NetworkVariable values, allowing for more responsive gameplay (#2820) - Added AnticipatedNetworkTransform, which adds support for client anticipation of NetworkTransforms (#2820) - Added NetworkVariableBase.ExceedsDirtinessThreshold to allow network variables to throttle updates by only sending updates when the difference between the current and previous values exceeds a threshold. (This is exposed in NetworkVariable<T> with the callback NetworkVariable<T>.CheckExceedsDirtinessThreshold) (#2820) - Added NetworkVariableUpdateTraits, which add additional throttling support: MinSecondsBetweenUpdates will prevent the NetworkVariable from sending updates more often than the specified time period (even if it exceeds the dirtiness threshold), while MaxSecondsBetweenUpdates will force a dirty NetworkVariable to send an update after the specified time period even if it has not yet exceeded the dirtiness threshold. (#2820) - Added virtual method NetworkVariableBase.OnInitialize() which can be used by NetworkVariable subclasses to add initialization code (#2820) - Added virtual method NetworkVariableBase.Update(), which is called once per frame to support behaviors such as interpolation between an anticipated value and an authoritative one. (#2820) - Added NetworkTime.TickWithPartial, which represents the current tick as a double that includes the fractional/partial tick value. (#2820) - Added NetworkTickSystem.AnticipationTick, which can be helpful with implementation of client anticipation. This value represents the tick the current local client was at at the beginning of the most recent network round trip, which enables it to correlate server update ticks with the client tick that may have triggered them. (#2820) - `NetworkVariable` now includes built-in support for `NativeHashSet`, `NativeHashMap`, `List`, `HashSet`, and `Dictionary` (#2813) - `NetworkVariable` now includes delta compression for collection values (`NativeList`, `NativeArray`, `NativeHashSet`, `NativeHashMap`, `List`, `HashSet`, `Dictionary`, and `FixedString` types) to save bandwidth by only sending the values that changed. (Note: For `NativeList`, `NativeArray`, and `List`, this algorithm works differently than that used in `NetworkList`. This algorithm will use less bandwidth for "set" and "add" operations, but `NetworkList` is more bandwidth-efficient if you are performing frequent "insert" operations.) (#2813) - `UserNetworkVariableSerialization` now has optional callbacks for `WriteDelta` and `ReadDelta`. If both are provided, they will be used for all serialization operations on NetworkVariables of that type except for the first one for each client. If either is missing, the existing `Write` and `Read` will always be used. (#2813) - Network variables wrapping `INetworkSerializable` types can perform delta serialization by setting `UserNetworkVariableSerialization<T>.WriteDelta` and `UserNetworkVariableSerialization<T>.ReadDelta` for those types. The built-in `INetworkSerializable` serializer will continue to be used for all other serialization operations, but if those callbacks are set, it will call into them on all but the initial serialization to perform delta serialization. (This could be useful if you have a large struct where most values do not change regularly and you want to send only the fields that did change.) (#2813) ### Fixed - Fixed issue where NetworkTransformEditor would throw and exception if you excluded the physics package. (#2871) - Fixed issue where `NetworkTransform` could not properly synchronize its base position when using half float precision. (#2845) - Fixed issue where the host was not invoking `OnClientDisconnectCallback` for its own local client when internally shutting down. (#2822) - Fixed issue where NetworkTransform could potentially attempt to "unregister" a named message prior to it being registered. (#2807) - Fixed issue where in-scene placed `NetworkObject`s with complex nested children `NetworkObject`s (more than one child in depth) would not synchronize properly if WorldPositionStays was set to true. (#2796) ### Changed - Changed `NetworkObjectReference` and `NetworkBehaviourReference` to allow null references when constructing and serializing. (#2874) - Changed `NetworkAnimator` no longer requires the `Animator` component to exist on the same `GameObject`. (#2872) - Changed `NetworkTransform` to now use `NetworkTransformMessage` as opposed to named messages for NetworkTransformState updates. (#2810) - Changed `CustomMessageManager` so it no longer attempts to register or "unregister" a null or empty string and will log an error if this condition occurs. (#2807)
972 lines
42 KiB
C#
972 lines
42 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
|
|
{
|
|
private Scene m_InvalidScene = new Scene();
|
|
|
|
internal struct SceneEntry
|
|
{
|
|
public bool IsAssigned;
|
|
public Scene Scene;
|
|
}
|
|
|
|
internal static Dictionary<NetworkManager, Dictionary<string, Dictionary<int, SceneEntry>>> SceneNameToSceneHandles = new Dictionary<NetworkManager, Dictionary<string, Dictionary<int, SceneEntry>>>();
|
|
|
|
// 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>
|
|
internal static 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)
|
|
{
|
|
ProcessInSceneObject(sobj, networkManager);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Assures to apply an ObjectNameIdentifier to all children
|
|
/// </summary>
|
|
private static void ProcessInSceneObject(NetworkObject networkObject, NetworkManager networkManager)
|
|
{
|
|
if (networkObject.NetworkManagerOwner != networkManager)
|
|
{
|
|
networkObject.NetworkManagerOwner = networkManager;
|
|
}
|
|
if (networkObject.GetComponent<ObjectNameIdentifier>() == null)
|
|
{
|
|
networkObject.gameObject.AddComponent<ObjectNameIdentifier>();
|
|
var networkObjects = networkObject.gameObject.GetComponentsInChildren<NetworkObject>();
|
|
foreach (var child in networkObjects)
|
|
{
|
|
if (child == networkObject)
|
|
{
|
|
continue;
|
|
}
|
|
ProcessInSceneObject(child, networkManager);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Processes scene unloading jobs
|
|
/// </summary>
|
|
/// <param name="queuedSceneJob">job to process</param>
|
|
internal static 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>
|
|
internal static 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)
|
|
{
|
|
SceneManager.sceneLoaded -= Sever_SceneLoaded;
|
|
ProcessInSceneObjects(scene, NetworkManager);
|
|
}
|
|
}
|
|
|
|
/// <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.");
|
|
}
|
|
if (DoesANetworkManagerHoldThisScene(sceneLoaded))
|
|
{
|
|
continue;
|
|
}
|
|
NetworkManager.SceneManager.ScenesLoaded.Add(sceneLoaded.handle, sceneLoaded);
|
|
StartTrackingScene(sceneLoaded, true, NetworkManager);
|
|
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;
|
|
}
|
|
|
|
public void ClearSceneTracking(NetworkManager networkManager)
|
|
{
|
|
SceneNameToSceneHandles.Clear();
|
|
}
|
|
|
|
public void StopTrackingScene(int handle, string name, NetworkManager networkManager)
|
|
{
|
|
if (!SceneNameToSceneHandles.ContainsKey(networkManager))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (SceneNameToSceneHandles[networkManager].ContainsKey(name))
|
|
{
|
|
if (SceneNameToSceneHandles[networkManager][name].ContainsKey(handle))
|
|
{
|
|
SceneNameToSceneHandles[networkManager][name].Remove(handle);
|
|
if (SceneNameToSceneHandles[networkManager][name].Count == 0)
|
|
{
|
|
SceneNameToSceneHandles[networkManager].Remove(name);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public void StartTrackingScene(Scene scene, bool assigned, NetworkManager networkManager)
|
|
{
|
|
if (!SceneNameToSceneHandles.ContainsKey(networkManager))
|
|
{
|
|
SceneNameToSceneHandles.Add(networkManager, new Dictionary<string, Dictionary<int, SceneEntry>>());
|
|
}
|
|
|
|
if (!SceneNameToSceneHandles[networkManager].ContainsKey(scene.name))
|
|
{
|
|
SceneNameToSceneHandles[networkManager].Add(scene.name, new Dictionary<int, SceneEntry>());
|
|
}
|
|
|
|
if (!SceneNameToSceneHandles[networkManager][scene.name].ContainsKey(scene.handle))
|
|
{
|
|
var sceneEntry = new SceneEntry()
|
|
{
|
|
IsAssigned = true,
|
|
Scene = scene
|
|
};
|
|
SceneNameToSceneHandles[networkManager][scene.name].Add(scene.handle, sceneEntry);
|
|
}
|
|
}
|
|
|
|
private bool DoesANetworkManagerHoldThisScene(Scene scene)
|
|
{
|
|
foreach (var netManEntry in SceneNameToSceneHandles)
|
|
{
|
|
if (!netManEntry.Value.ContainsKey(scene.name))
|
|
{
|
|
continue;
|
|
}
|
|
// The other NetworkManager only has to have an entry to
|
|
// disqualify this scene instance
|
|
if (netManEntry.Value[scene.name].ContainsKey(scene.handle))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public bool DoesSceneHaveUnassignedEntry(string sceneName, NetworkManager networkManager)
|
|
{
|
|
var scenesWithSceneName = new List<Scene>();
|
|
var scenesAssigned = new List<Scene>();
|
|
for (int i = 0; i < SceneManager.sceneCount; i++)
|
|
{
|
|
var scene = SceneManager.GetSceneAt(i);
|
|
if (scene.name == sceneName)
|
|
{
|
|
scenesWithSceneName.Add(scene);
|
|
}
|
|
}
|
|
|
|
// Check for other NetworkManager instances already having been assigned this scene
|
|
foreach (var netManEntry in SceneNameToSceneHandles)
|
|
{
|
|
// Ignore this NetworkManager instance at this stage
|
|
if (netManEntry.Key == networkManager)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
foreach (var scene in scenesWithSceneName)
|
|
{
|
|
if (!netManEntry.Value.ContainsKey(scene.name))
|
|
{
|
|
continue;
|
|
}
|
|
// The other NetworkManager only has to have an entry to
|
|
// disqualify this scene instance
|
|
if (netManEntry.Value[scene.name].ContainsKey(scene.handle))
|
|
{
|
|
scenesAssigned.Add(scene);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Remove all of the assigned scenes from the list of scenes with the
|
|
// passed in scene name.
|
|
foreach (var assignedScene in scenesAssigned)
|
|
{
|
|
if (scenesWithSceneName.Contains(assignedScene))
|
|
{
|
|
scenesWithSceneName.Remove(assignedScene);
|
|
}
|
|
}
|
|
|
|
// If all currently loaded scenes with the scene name are taken
|
|
// then we return false
|
|
if (scenesWithSceneName.Count == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// If we made it here, then no other NetworkManager is tracking this scene
|
|
// and if we don't have an entry for this NetworkManager then we can use any
|
|
// of the remaining scenes loaded with that name.
|
|
if (!SceneNameToSceneHandles.ContainsKey(networkManager))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// If we don't yet have a scene name in this NetworkManager's lookup table,
|
|
// then we can use any of the remaining availabel scenes with that scene name
|
|
if (!SceneNameToSceneHandles[networkManager].ContainsKey(sceneName))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
foreach (var scene in scenesWithSceneName)
|
|
{
|
|
// If we don't have an entry for this scene handle (with the scene name) then we
|
|
// can use that scene
|
|
if (!SceneNameToSceneHandles[networkManager][scene.name].ContainsKey(scene.handle))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// This entry is not assigned, then we can use the associated scene
|
|
if (!SceneNameToSceneHandles[networkManager][scene.name][scene.handle].IsAssigned)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// None of the scenes with the same scene name can be used
|
|
return false;
|
|
}
|
|
|
|
public Scene GetSceneFromLoadedScenes(string sceneName, NetworkManager networkManager)
|
|
{
|
|
|
|
if (!SceneNameToSceneHandles.ContainsKey(networkManager))
|
|
{
|
|
return m_InvalidScene;
|
|
}
|
|
if (SceneNameToSceneHandles[networkManager].ContainsKey(sceneName))
|
|
{
|
|
foreach (var sceneHandleEntry in SceneNameToSceneHandles[networkManager][sceneName])
|
|
{
|
|
if (!sceneHandleEntry.Value.IsAssigned)
|
|
{
|
|
var sceneEntry = sceneHandleEntry.Value;
|
|
sceneEntry.IsAssigned = true;
|
|
SceneNameToSceneHandles[networkManager][sceneName][sceneHandleEntry.Key] = sceneEntry;
|
|
return sceneEntry.Scene;
|
|
}
|
|
}
|
|
}
|
|
// This is tricky since NetworkManager instances share the same scene hierarchy during integration tests.
|
|
// TODO 2023: Determine if there is a better way to associate the active scene for client NetworkManager instances.
|
|
var activeScene = SceneManager.GetActiveScene();
|
|
|
|
if (sceneName == activeScene.name && networkManager.SceneManager.ClientSynchronizationMode == LoadSceneMode.Additive)
|
|
{
|
|
// For now, just return the current active scene
|
|
// Note: Clients will not be able to synchronize in-scene placed NetworkObjects in an integration test for
|
|
// scenes loaded that have in-scene placed NetworkObjects prior to the clients joining (i.e. there will only
|
|
// ever be one instance of the active scene). To test in-scene placed NetworkObjects and make an integration
|
|
// test loaded scene be the active scene, don't set scene as an active scene on the server side until all
|
|
// clients have connected and loaded the scene.
|
|
return activeScene;
|
|
}
|
|
// If we found nothing return an invalid scene
|
|
return m_InvalidScene;
|
|
}
|
|
|
|
public void PopulateLoadedScenes(ref Dictionary<int, Scene> scenesLoaded, NetworkManager networkManager)
|
|
{
|
|
if (!SceneNameToSceneHandles.ContainsKey(networkManager))
|
|
{
|
|
SceneNameToSceneHandles.Add(networkManager, new Dictionary<string, Dictionary<int, SceneEntry>>());
|
|
}
|
|
|
|
var sceneCount = SceneManager.sceneCount;
|
|
for (int i = 0; i < sceneCount; i++)
|
|
{
|
|
var scene = SceneManager.GetSceneAt(i);
|
|
// Ignore scenes that belong to other NetworkManager instances
|
|
|
|
if (DoesANetworkManagerHoldThisScene(scene))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!DoesSceneHaveUnassignedEntry(scene.name, networkManager))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!SceneNameToSceneHandles[networkManager].ContainsKey(scene.name))
|
|
{
|
|
SceneNameToSceneHandles[networkManager].Add(scene.name, new Dictionary<int, SceneEntry>());
|
|
}
|
|
|
|
if (!SceneNameToSceneHandles[networkManager][scene.name].ContainsKey(scene.handle))
|
|
{
|
|
var sceneEntry = new SceneEntry()
|
|
{
|
|
IsAssigned = false,
|
|
Scene = scene
|
|
};
|
|
SceneNameToSceneHandles[networkManager][scene.name].Add(scene.handle, sceneEntry);
|
|
if (!scenesLoaded.ContainsKey(scene.handle))
|
|
{
|
|
scenesLoaded.Add(scene.handle, scene);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
throw new Exception($"[{networkManager.LocalClient.PlayerObject.name}][Duplicate Handle] Scene {scene.name} already has scene handle {scene.handle} registered!");
|
|
}
|
|
}
|
|
}
|
|
|
|
private Dictionary<Scene, NetworkManager> m_ScenesToUnload = new Dictionary<Scene, NetworkManager>();
|
|
|
|
/// <summary>
|
|
/// Handles unloading any scenes that might remain on a client that
|
|
/// need to be unloaded.
|
|
/// </summary>
|
|
/// <param name="networkManager"></param>
|
|
public void UnloadUnassignedScenes(NetworkManager networkManager = null)
|
|
{
|
|
if (!SceneNameToSceneHandles.ContainsKey(networkManager))
|
|
{
|
|
return;
|
|
}
|
|
var relativeSceneNameToSceneHandles = SceneNameToSceneHandles[networkManager];
|
|
var sceneManager = networkManager.SceneManager;
|
|
SceneManager.sceneUnloaded += SceneManager_SceneUnloaded;
|
|
|
|
foreach (var sceneEntry in relativeSceneNameToSceneHandles)
|
|
{
|
|
var scenHandleEntries = relativeSceneNameToSceneHandles[sceneEntry.Key];
|
|
foreach (var sceneHandleEntry in scenHandleEntries)
|
|
{
|
|
if (!sceneHandleEntry.Value.IsAssigned)
|
|
{
|
|
if (sceneManager.VerifySceneBeforeUnloading == null || sceneManager.VerifySceneBeforeUnloading.Invoke(sceneHandleEntry.Value.Scene))
|
|
{
|
|
m_ScenesToUnload.Add(sceneHandleEntry.Value.Scene, networkManager);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach (var sceneToUnload in m_ScenesToUnload)
|
|
{
|
|
SceneManager.UnloadSceneAsync(sceneToUnload.Key);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes the scene entry from the scene name to scene handle table
|
|
/// </summary>
|
|
private void SceneManager_SceneUnloaded(Scene scene)
|
|
{
|
|
if (m_ScenesToUnload.ContainsKey(scene))
|
|
{
|
|
var networkManager = m_ScenesToUnload[scene];
|
|
var relativeSceneNameToSceneHandles = SceneNameToSceneHandles[networkManager];
|
|
if (relativeSceneNameToSceneHandles.ContainsKey(scene.name))
|
|
{
|
|
var scenHandleEntries = relativeSceneNameToSceneHandles[scene.name];
|
|
if (scenHandleEntries.ContainsKey(scene.handle))
|
|
{
|
|
scenHandleEntries.Remove(scene.handle);
|
|
if (scenHandleEntries.Count == 0)
|
|
{
|
|
relativeSceneNameToSceneHandles.Remove(scene.name);
|
|
}
|
|
m_ScenesToUnload.Remove(scene);
|
|
if (m_ScenesToUnload.Count == 0)
|
|
{
|
|
SceneManager.sceneUnloaded -= SceneManager_SceneUnloaded;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Integration test version that handles migrating dynamically spawned NetworkObjects to
|
|
/// the DDOL when a scene is unloaded
|
|
/// </summary>
|
|
/// <param name="networkManager"><see cref="NetworkManager"/> relative instance</param>
|
|
/// <param name="scene">scene being unloaded</param>
|
|
public void MoveObjectsFromSceneToDontDestroyOnLoad(ref NetworkManager networkManager, Scene scene)
|
|
{
|
|
// Create a local copy of the spawned objects list since the spawn manager will adjust the list as objects
|
|
// are despawned.
|
|
#if UNITY_2023_1_OR_NEWER
|
|
var networkObjects = Object.FindObjectsByType<NetworkObject>(FindObjectsSortMode.InstanceID).Where((c) => c.IsSpawned);
|
|
#else
|
|
var networkObjects = Object.FindObjectsOfType<NetworkObject>().Where((c) => c.IsSpawned);
|
|
#endif
|
|
foreach (var networkObject in networkObjects)
|
|
{
|
|
if (networkObject == null || (networkObject != null && networkObject.gameObject.scene.handle != scene.handle))
|
|
{
|
|
if (networkObject != null)
|
|
{
|
|
VerboseDebug($"[MoveObjects from {scene.name} | {scene.handle}] Ignoring {networkObject.gameObject.name} because it isn't in scene {networkObject.gameObject.scene.name} ");
|
|
}
|
|
continue;
|
|
}
|
|
|
|
bool skipPrefab = false;
|
|
|
|
foreach (var networkPrefab in networkManager.NetworkConfig.Prefabs.Prefabs)
|
|
{
|
|
if (networkPrefab.Prefab == null)
|
|
{
|
|
continue;
|
|
}
|
|
if (networkObject == networkPrefab.Prefab.GetComponent<NetworkObject>())
|
|
{
|
|
skipPrefab = true;
|
|
break;
|
|
}
|
|
}
|
|
if (skipPrefab)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Only NetworkObjects marked to not be destroyed with the scene and are not already in the DDOL are preserved
|
|
if (!networkObject.DestroyWithScene && networkObject.gameObject.scene != networkManager.SceneManager.DontDestroyOnLoadScene)
|
|
{
|
|
// Only move dynamically spawned NetworkObjects with no parent as the children will follow
|
|
if (networkObject.gameObject.transform.parent == null && networkObject.IsSceneObject != null && !networkObject.IsSceneObject.Value)
|
|
{
|
|
VerboseDebug($"[MoveObjects from {scene.name} | {scene.handle}] Moving {networkObject.gameObject.name} because it is in scene {networkObject.gameObject.scene.name} with DWS = {networkObject.DestroyWithScene}.");
|
|
Object.DontDestroyOnLoad(networkObject.gameObject);
|
|
}
|
|
}
|
|
else if (networkManager.IsServer)
|
|
{
|
|
if (networkObject.NetworkManager == networkManager)
|
|
{
|
|
VerboseDebug($"[MoveObjects from {scene.name} | {scene.handle}] Destroying {networkObject.gameObject.name} because it is in scene {networkObject.gameObject.scene.name} with DWS = {networkObject.DestroyWithScene}.");
|
|
networkObject.Despawn();
|
|
}
|
|
else //For integration testing purposes, migrate remaining into DDOL
|
|
{
|
|
VerboseDebug($"[MoveObjects from {scene.name} | {scene.handle}] Temporarily migrating {networkObject.gameObject.name} into DDOL to await server destroy message.");
|
|
Object.DontDestroyOnLoad(networkObject.gameObject);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the client synchronization mode which impacts whether both the server or client take into consideration scenes loaded before
|
|
/// starting the <see cref="NetworkManager"/>.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <see cref="LoadSceneMode.Single"/>: Does not take preloaded scenes into consideration
|
|
/// <see cref="LoadSceneMode.Single"/>: Does take preloaded scenes into consideration
|
|
/// </remarks>
|
|
/// <param name="networkManager">relative <see cref="NetworkManager"/> instance</param>
|
|
/// <param name="mode"><see cref="LoadSceneMode.Single"/> or <see cref="LoadSceneMode.Additive"/></param>
|
|
public void SetClientSynchronizationMode(ref NetworkManager networkManager, LoadSceneMode mode)
|
|
{
|
|
|
|
var sceneManager = networkManager.SceneManager;
|
|
|
|
// Don't let client's set this value
|
|
if (!networkManager.IsServer)
|
|
{
|
|
if (NetworkLog.CurrentLogLevel <= LogLevel.Normal)
|
|
{
|
|
NetworkLog.LogWarning("Clients should not set this value as it is automatically synchronized with the server's setting!");
|
|
}
|
|
return;
|
|
}
|
|
else if (networkManager.ConnectedClientsIds.Count > (networkManager.IsHost ? 1 : 0) && sceneManager.ClientSynchronizationMode != mode)
|
|
{
|
|
if (NetworkLog.CurrentLogLevel <= LogLevel.Normal)
|
|
{
|
|
NetworkLog.LogWarning("Server is changing client synchronization mode after clients have been synchronized! It is recommended to do this before clients are connected!");
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// For additive client synchronization, we take into consideration scenes
|
|
// already loaded.
|
|
if (mode == LoadSceneMode.Additive)
|
|
{
|
|
if (networkManager.IsServer)
|
|
{
|
|
sceneManager.OnSceneEvent -= SceneManager_OnSceneEvent;
|
|
sceneManager.OnSceneEvent += SceneManager_OnSceneEvent;
|
|
}
|
|
|
|
if (!SceneNameToSceneHandles.ContainsKey(networkManager))
|
|
{
|
|
SceneNameToSceneHandles.Add(networkManager, new Dictionary<string, Dictionary<int, SceneEntry>>());
|
|
}
|
|
|
|
var networkManagerScenes = SceneNameToSceneHandles[networkManager];
|
|
|
|
for (int i = 0; i < SceneManager.sceneCount; i++)
|
|
{
|
|
var scene = SceneManager.GetSceneAt(i);
|
|
|
|
// Ignore scenes that belong to other NetworkManager instances
|
|
if (!DoesSceneHaveUnassignedEntry(scene.name, networkManager))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// If using scene verification
|
|
if (sceneManager.VerifySceneBeforeLoading != null)
|
|
{
|
|
// Determine if we should take this scene into consideration
|
|
if (!sceneManager.VerifySceneBeforeLoading.Invoke(scene.buildIndex, scene.name, LoadSceneMode.Additive))
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// If the scene is not already in the ScenesLoaded list, then add it
|
|
if (!sceneManager.ScenesLoaded.ContainsKey(scene.handle))
|
|
{
|
|
StartTrackingScene(scene, true, networkManager);
|
|
sceneManager.ScenesLoaded.Add(scene.handle, scene);
|
|
}
|
|
}
|
|
}
|
|
// Set the client synchronization mode
|
|
sceneManager.ClientSynchronizationMode = mode;
|
|
}
|
|
|
|
/// <summary>
|
|
/// During integration testing, if the server loads a scene then
|
|
/// we want to start tracking it.
|
|
/// </summary>
|
|
/// <param name="sceneEvent"></param>
|
|
private void SceneManager_OnSceneEvent(SceneEvent sceneEvent)
|
|
{
|
|
// Filter for server only scene events
|
|
if (!NetworkManager.IsServer || sceneEvent.ClientId != NetworkManager.ServerClientId)
|
|
{
|
|
return;
|
|
}
|
|
|
|
switch (sceneEvent.SceneEventType)
|
|
{
|
|
case SceneEventType.LoadComplete:
|
|
{
|
|
StartTrackingScene(sceneEvent.Scene, true, NetworkManager);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles determining if a client should attempt to load a scene during synchronization.
|
|
/// </summary>
|
|
/// <param name="sceneName">name of the scene to be loaded</param>
|
|
/// <param name="isPrimaryScene">when in client synchronization mode single, this determines if the scene is the primary active scene</param>
|
|
/// <param name="clientSynchronizationMode">the current client synchronization mode</param>
|
|
/// <param name="networkManager"><see cref="NetworkManager"/>relative instance</param>
|
|
/// <returns></returns>
|
|
public bool ClientShouldPassThrough(string sceneName, bool isPrimaryScene, LoadSceneMode clientSynchronizationMode, NetworkManager networkManager)
|
|
{
|
|
var shouldPassThrough = clientSynchronizationMode == LoadSceneMode.Single ? false : DoesSceneHaveUnassignedEntry(sceneName, networkManager);
|
|
var activeScene = SceneManager.GetActiveScene();
|
|
|
|
// If shouldPassThrough is not yet true and the scene to be loaded is the currently active scene
|
|
if (!shouldPassThrough && sceneName == activeScene.name)
|
|
{
|
|
// In additive client synchronization mode we always pass through.
|
|
// Unlike the default behavior(i.e. DefaultSceneManagerHandler), for integration testing we always return false
|
|
// if it is the active scene and the client synchronization mode is LoadSceneMode.Single because the client should
|
|
// load the active scene additively for this NetworkManager instance (i.e. can't have multiple active scenes).
|
|
if (clientSynchronizationMode == LoadSceneMode.Additive)
|
|
{
|
|
// don't try to reload this scene and pass through to post load processing.
|
|
shouldPassThrough = true;
|
|
}
|
|
}
|
|
return shouldPassThrough;
|
|
}
|
|
|
|
/// <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>();
|
|
// Move the CoroutineRunner into the DDOL in case we unload the scene it was instantiated in.
|
|
// (which if that gets destroyed then it basically stops all integration test queue processing)
|
|
Object.DontDestroyOnLoad(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();
|
|
if (CoroutineRunner != null && CoroutineRunner.gameObject != null)
|
|
{
|
|
Object.Destroy(CoroutineRunner.gameObject);
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|