diff --git a/com.unity.netcode.gameobjects/CHANGELOG.md b/com.unity.netcode.gameobjects/CHANGELOG.md
index 2c846d9d8a..c1f9a3fb74 100644
--- a/com.unity.netcode.gameobjects/CHANGELOG.md
+++ b/com.unity.netcode.gameobjects/CHANGELOG.md
@@ -5,11 +5,27 @@ All notable changes to this project will be documented in this file.
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).
-
## [Unreleased]
### Added
+- Added `NetworkSceneManager.ActiveSceneSynchronizationEnabled` property, disabled by default, that enables client synchronization of server-side active scene changes. (#2383)
+- Added `NetworkObject.ActiveSceneSynchronization`, disabled by default, that will automatically migrate a `NetworkObject` to a newly assigned active scene. (#2383)
+- Added `NetworkObject.SceneMigrationSynchronization`, enabled by default, that will synchronize client(s) when a `NetworkObject` is migrated into a new scene on the server side via `SceneManager.MoveGameObjectToScene`. (#2383)
+
+### Changed
+
+- Updated `NetworkSceneManager` to migrate dynamically spawned `NetworkObject`s with `DestroyWithScene` set to false into the active scene if their current scene is unloaded. (#2383)
+- Updated the server to synchronize its local `NetworkSceneManager.ClientSynchronizationMode` during the initial client synchronization. (#2383)
+
+### Fixed
+
+- Fixed registry of public `NetworkVariable`s in derived `NetworkBehaviour`s (#2423)
+- Fixed issue when the `NetworkSceneManager.ClientSynchronizationMode` is `LoadSceneMode.Additive` and the server changes the currently active scene prior to a client connecting then upon a client connecting and being synchronized the NetworkSceneManager would clear its internal ScenePlacedObjects list that could already be populated. (#2383)
+- Fixed issue where a client would load duplicate scenes of already preloaded scenes during the initial client synchronization and `NetworkSceneManager.ClientSynchronizationMode` was set to `LoadSceneMode.Additive`. (#2383)
+
+## [1.3.0]
+
### Changed
### Fixed
diff --git a/com.unity.netcode.gameobjects/Runtime/Configuration/NetworkPrefabs.cs b/com.unity.netcode.gameobjects/Runtime/Configuration/NetworkPrefabs.cs
index 6c2d526ba5..1dae3ce33f 100644
--- a/com.unity.netcode.gameobjects/Runtime/Configuration/NetworkPrefabs.cs
+++ b/com.unity.netcode.gameobjects/Runtime/Configuration/NetworkPrefabs.cs
@@ -52,12 +52,23 @@ private void RemoveTriggeredByNetworkPrefabList(NetworkPrefab networkPrefab)
}
~NetworkPrefabs()
+ {
+ Shutdown();
+ }
+
+ ///
+ /// Deregister from add and remove events
+ /// Clear the list
+ ///
+ internal void Shutdown()
{
foreach (var list in NetworkPrefabsLists)
{
list.OnAdd -= AddTriggeredByNetworkPrefabList;
list.OnRemove -= RemoveTriggeredByNetworkPrefabList;
}
+
+ NetworkPrefabsLists.Clear();
}
///
diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs
index 8f518d84f7..cefa410fee 100644
--- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs
+++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs
@@ -1278,6 +1278,8 @@ internal void ShutdownInternal()
m_StopProcessingMessages = false;
ClearClients();
+ // This cleans up the internal prefabs list
+ NetworkConfig?.Prefabs.Shutdown();
}
///
@@ -1395,6 +1397,10 @@ private void OnNetworkPostLateUpdate()
if (!m_ShuttingDown || !m_StopProcessingMessages)
{
+ // This should be invoked just prior to the MessagingSystem
+ // processes its outbound queue.
+ SceneManager.CheckForAndSendNetworkObjectSceneChanged();
+
MessagingSystem.ProcessSendQueues();
NetworkMetrics.UpdateNetworkObjectsCount(SpawnManager.SpawnedObjects.Count);
NetworkMetrics.UpdateConnectionsCount((IsServer) ? ConnectedClients.Count : 1);
@@ -1974,6 +1980,9 @@ internal void HandleConnectionApproval(ulong ownerClientId, ConnectionApprovalRe
var playerPrefabHash = response.PlayerPrefabHash ?? NetworkConfig.PlayerPrefab.GetComponent().GlobalObjectIdHash;
// Generate a SceneObject for the player object to spawn
+ // Note: This is only to create the local NetworkObject,
+ // many of the serialized properties of the player prefab
+ // will be set when instantiated.
var sceneObject = new NetworkObject.SceneObject
{
OwnerClientId = ownerClientId,
diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs
index 1e52e52cb3..4ac41c7de0 100644
--- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs
+++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs
@@ -105,6 +105,55 @@ internal void GenerateGlobalObjectIdHash()
///
public bool DestroyWithScene { get; set; }
+ ///
+ /// When set to true and the active scene is changed, this will automatically migrate the
+ /// into the new active scene on both the server and client instances.
+ ///
+ ///
+ /// - This only applies to dynamically spawned s.
+ /// - This only works when using integrated scene management ().
+ ///
+ /// If there are more than one scenes loaded and the currently active scene is unloaded, then typically
+ /// the will automatically assign a new active scene. Similar to
+ /// being set to , this prevents any from being destroyed
+ /// with the unloaded active scene by migrating it into the automatically assigned active scene.
+ /// Additionally, this is can be useful in some seamless scene streaming implementations.
+ /// Note:
+ /// Only having set to true will *not* synchronize clients when
+ /// changing a 's scene via .
+ /// To synchronize clients of a 's scene being changed via ,
+ /// make sure is enabled (it is by default).
+ ///
+ public bool ActiveSceneSynchronization;
+
+ ///
+ /// When enabled (the default), if a is migrated to a different scene (active or not)
+ /// via on the server side all client
+ /// instances will be synchronized and the migrated into the newly assigned scene.
+ /// The updated scene migration will get synchronized with late joining clients as well.
+ ///
+ ///
+ /// - This only applies to dynamically spawned s.
+ /// - This only works when using integrated scene management ().
+ /// Note:
+ /// You can have both and enabled.
+ /// The primary difference between the two is that only synchronizes clients
+ /// when the server migrates a to a new scene. If the scene is unloaded and
+ /// is and is and the scene is not the currently
+ /// active scene, then the will be destroyed.
+ ///
+ public bool SceneMigrationSynchronization = true;
+
+ ///
+ /// Notifies when the NetworkObject is migrated into a new scene
+ ///
+ ///
+ /// - or (or both) need to be enabled
+ /// - This only applies to dynamically spawned s.
+ /// - This only works when using integrated scene management ().
+ ///
+ public Action OnMigratedToNewScene;
+
///
/// Delegate type for checking visibility
///
@@ -188,6 +237,11 @@ public bool IsNetworkVisibleTo(ulong clientId)
///
internal int SceneOriginHandle = 0;
+ ///
+ /// The server-side scene origin handle
+ ///
+ internal int NetworkSceneHandle = 0;
+
private Scene m_SceneOrigin;
///
/// The scene where the NetworkObject was first instantiated
@@ -1118,6 +1172,18 @@ public bool WorldPositionStays
set => ByteUtility.SetBit(ref m_BitField, 5, value);
}
+ ///
+ /// Even though the server sends notifications for NetworkObjects that get
+ /// destroyed when a scene is unloaded, we want to synchronize this so
+ /// the client side can use it as part of a filter for automatically migrating
+ /// to the current active scene when its scene is unloaded. (only for dynamically spawned)
+ ///
+ public bool DestroyWithScene
+ {
+ get => ByteUtility.GetBit(m_BitField, 6);
+ set => ByteUtility.SetBit(ref m_BitField, 6, value);
+ }
+
//If(Metadata.HasParent)
public ulong ParentObjectId;
@@ -1160,7 +1226,7 @@ public void Serialize(FastBufferWriter writer)
var writeSize = 0;
writeSize += HasTransform ? FastBufferWriter.GetWriteSize() : 0;
- writeSize += IsSceneObject ? FastBufferWriter.GetWriteSize() : 0;
+ writeSize += FastBufferWriter.GetWriteSize();
if (!writer.TryBeginWrite(writeSize))
{
@@ -1172,14 +1238,9 @@ public void Serialize(FastBufferWriter writer)
writer.WriteValue(Transform);
}
- // In-Scene NetworkObjects are uniquely identified NetworkPrefabs defined by their
- // NetworkSceneHandle and GlobalObjectIdHash. Client-side NetworkSceneManagers use
- // this to locate their local instance of the in-scene placed NetworkObject instance.
- // Only written for in-scene placed NetworkObjects.
- if (IsSceneObject)
- {
- writer.WriteValue(OwnerObject.GetSceneOriginHandle());
- }
+ // The NetworkSceneHandle is the server-side relative
+ // scene handle that the NetworkObject resides in.
+ writer.WriteValue(OwnerObject.GetSceneOriginHandle());
// Synchronize NetworkVariables and NetworkBehaviours
var bufferSerializer = new BufferSerializer(new BufferSerializerWriter(writer));
@@ -1205,7 +1266,7 @@ public void Deserialize(FastBufferReader reader)
var readSize = 0;
readSize += HasTransform ? FastBufferWriter.GetWriteSize() : 0;
- readSize += IsSceneObject ? FastBufferWriter.GetWriteSize() : 0;
+ readSize += FastBufferWriter.GetWriteSize();
// Try to begin reading the remaining bytes
if (!reader.TryBeginRead(readSize))
@@ -1218,14 +1279,9 @@ public void Deserialize(FastBufferReader reader)
reader.ReadValue(out Transform);
}
- // In-Scene NetworkObjects are uniquely identified NetworkPrefabs defined by their
- // NetworkSceneHandle and GlobalObjectIdHash. Client-side NetworkSceneManagers use
- // this to locate their local instance of the in-scene placed NetworkObject instance.
- // Only read for in-scene placed NetworkObjects
- if (IsSceneObject)
- {
- reader.ReadValue(out NetworkSceneHandle);
- }
+ // The NetworkSceneHandle is the server-side relative
+ // scene handle that the NetworkObject resides in.
+ reader.ReadValue(out NetworkSceneHandle);
}
}
@@ -1317,6 +1373,7 @@ internal SceneObject GetMessageSceneObject(ulong targetClientId)
OwnerClientId = OwnerClientId,
IsPlayerObject = IsPlayerObject,
IsSceneObject = IsSceneObject ?? true,
+ DestroyWithScene = DestroyWithScene,
Hash = HostCheckForGlobalObjectIdHashOverride(),
OwnerObject = this,
TargetClientId = targetClientId
@@ -1435,11 +1492,126 @@ internal static NetworkObject AddSceneObject(in SceneObject sceneObject, FastBuf
networkObject.SynchronizeNetworkBehaviours(ref bufferSerializer, networkManager.LocalClientId);
// Spawn the NetworkObject
- networkManager.SpawnManager.SpawnNetworkObjectLocally(networkObject, sceneObject, false);
+ networkManager.SpawnManager.SpawnNetworkObjectLocally(networkObject, sceneObject, sceneObject.DestroyWithScene);
return networkObject;
}
+ ///
+ /// Subscribes to changes in the currently active scene
+ ///
+ ///
+ /// Only for dynamically spawned NetworkObjects
+ ///
+ internal void SubscribeToActiveSceneForSynch()
+ {
+ if (ActiveSceneSynchronization)
+ {
+ if (IsSceneObject.HasValue && !IsSceneObject.Value)
+ {
+ // Just in case it is a recycled NetworkObject, unsubscribe first
+ SceneManager.activeSceneChanged -= CurrentlyActiveSceneChanged;
+ SceneManager.activeSceneChanged += CurrentlyActiveSceneChanged;
+ }
+ }
+ }
+
+ ///
+ /// If AutoSynchActiveScene is enabled, then this is the callback that handles updating
+ /// a NetworkObject's scene information.
+ ///
+ private void CurrentlyActiveSceneChanged(Scene current, Scene next)
+ {
+ // Early exit if there is no NetworkManager assigned, the NetworkManager is shutting down, the NetworkObject
+ // is not spawned, or an in-scene placed NetworkObject
+ if (NetworkManager == null || NetworkManager.ShutdownInProgress || !IsSpawned || IsSceneObject != false)
+ {
+ return;
+ }
+ // This check is here in the event a user wants to disable this for some reason but also wants
+ // the NetworkObject to synchronize to changes in the currently active scene at some later time.
+ if (ActiveSceneSynchronization)
+ {
+ // Only dynamically spawned NetworkObjects that are not already in the newly assigned active scene will migrate
+ // and update their scene handles
+ if (IsSceneObject.HasValue && !IsSceneObject.Value && gameObject.scene != next && gameObject.transform.parent == null)
+ {
+ SceneManager.MoveGameObjectToScene(gameObject, next);
+ SceneChangedUpdate(next);
+ }
+ }
+ }
+
+ ///
+ /// Handles updating the NetworkObject's tracked scene handles
+ ///
+ internal void SceneChangedUpdate(Scene scene, bool notify = false)
+ {
+ // Avoiding edge case scenarios, if no NetworkSceneManager exit early
+ if (NetworkManager.SceneManager == null)
+ {
+ return;
+ }
+
+ SceneOriginHandle = scene.handle;
+ // Clients need to update the NetworkSceneHandle
+ if (!NetworkManager.IsServer && NetworkManager.SceneManager.ClientSceneHandleToServerSceneHandle.ContainsKey(SceneOriginHandle))
+ {
+ NetworkSceneHandle = NetworkManager.SceneManager.ClientSceneHandleToServerSceneHandle[SceneOriginHandle];
+ }
+ else if (NetworkManager.IsServer)
+ {
+ // Since the server is the source of truth for the NetworkSceneHandle,
+ // the NetworkSceneHandle is the same as the SceneOriginHandle.
+ NetworkSceneHandle = SceneOriginHandle;
+ }
+ else // Otherwise, the client did not find the client to server scene handle
+ if (NetworkManager.LogLevel == LogLevel.Developer)
+ {
+ // There could be a scenario where a user has some client-local scene loaded that they migrate the NetworkObject
+ // into, but that scenario seemed very edge case and under most instances a user should be notified that this
+ // server - client scene handle mismatch has occurred. It also seemed pertinent to make the message replicate to
+ // the server-side too.
+ NetworkLog.LogWarningServer($"[Client-{NetworkManager.LocalClientId}][{gameObject.name}] Server - " +
+ $"client scene mismatch detected! Client-side scene handle ({SceneOriginHandle}) for scene ({gameObject.scene.name})" +
+ $"has no associated server side (network) scene handle!");
+ }
+ OnMigratedToNewScene?.Invoke();
+
+ // Only the server side will notify clients of non-parented NetworkObject scene changes
+ if (NetworkManager.IsServer && notify && transform.parent == null)
+ {
+ NetworkManager.SceneManager.NotifyNetworkObjectSceneChanged(this);
+ }
+ }
+
+ ///
+ /// Update
+ /// Detects if a NetworkObject's scene has changed for both server and client instances
+ ///
+ ///
+ /// About In-Scene Placed NetworkObjects:
+ /// Since the same scene can be loaded more than once and in-scene placed NetworkObjects GlobalObjectIdHash
+ /// values are only unique to the scene asset itself (and not per scene instance loaded), we will not be able
+ /// to add this same functionality to in-scene placed NetworkObjects until we have a way to generate
+ /// per-NetworkObject-instance unique GlobalObjectIdHash values for in-scene placed NetworkObjects.
+ ///
+ private void Update()
+ {
+ // Early exit if SceneMigrationSynchronization is disabled, there is no NetworkManager assigned,
+ // the NetworkManager is shutting down, the NetworkObject is not spawned, it is an in-scene placed
+ // NetworkObject, or the GameObject's current scene handle is the same as the SceneOriginHandle
+ if (!SceneMigrationSynchronization || NetworkManager == null || NetworkManager.ShutdownInProgress || !IsSpawned
+ || IsSceneObject != false || gameObject.scene.handle == SceneOriginHandle)
+ {
+ return;
+ }
+
+ // Otherwise, this has to be a dynamically spawned NetworkObject that has been
+ // migrated to a new scene.
+ SceneChangedUpdate(gameObject.scene, true);
+ }
+
///
/// Only applies to Host mode.
/// Will return the registered source NetworkPrefab's GlobalObjectIdHash if one exists.
diff --git a/com.unity.netcode.gameobjects/Runtime/SceneManagement/DefaultSceneManagerHandler.cs b/com.unity.netcode.gameobjects/Runtime/SceneManagement/DefaultSceneManagerHandler.cs
new file mode 100644
index 0000000000..c63fc11270
--- /dev/null
+++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/DefaultSceneManagerHandler.cs
@@ -0,0 +1,358 @@
+using System;
+using System.Collections.Generic;
+using UnityEngine;
+using UnityEngine.SceneManagement;
+
+
+namespace Unity.Netcode
+{
+ ///
+ /// The default SceneManagerHandler that interfaces between the SceneManager and NetworkSceneManager
+ ///
+ internal class DefaultSceneManagerHandler : ISceneManagerHandler
+ {
+ private Scene m_InvalidScene = new Scene();
+
+ internal struct SceneEntry
+ {
+ public bool IsAssigned;
+ public Scene Scene;
+ }
+
+ internal Dictionary> SceneNameToSceneHandles = new Dictionary>();
+
+ public AsyncOperation LoadSceneAsync(string sceneName, LoadSceneMode loadSceneMode, SceneEventProgress sceneEventProgress)
+ {
+ var operation = SceneManager.LoadSceneAsync(sceneName, loadSceneMode);
+ sceneEventProgress.SetAsyncOperation(operation);
+ return operation;
+ }
+
+ public AsyncOperation UnloadSceneAsync(Scene scene, SceneEventProgress sceneEventProgress)
+ {
+ var operation = SceneManager.UnloadSceneAsync(scene);
+ sceneEventProgress.SetAsyncOperation(operation);
+ return operation;
+ }
+
+ ///
+ /// Resets scene tracking
+ ///
+ public void ClearSceneTracking(NetworkManager networkManager)
+ {
+ SceneNameToSceneHandles.Clear();
+ }
+
+ ///
+ /// Stops tracking a specific scene
+ ///
+ public void StopTrackingScene(int handle, string name, NetworkManager networkManager)
+ {
+ if (SceneNameToSceneHandles.ContainsKey(name))
+ {
+ if (SceneNameToSceneHandles[name].ContainsKey(handle))
+ {
+ SceneNameToSceneHandles[name].Remove(handle);
+ if (SceneNameToSceneHandles[name].Count == 0)
+ {
+ SceneNameToSceneHandles.Remove(name);
+ }
+ }
+ }
+ }
+
+ ///
+ /// Starts tracking a specific scene
+ ///
+ public void StartTrackingScene(Scene scene, bool assigned, NetworkManager networkManager)
+ {
+ if (!SceneNameToSceneHandles.ContainsKey(scene.name))
+ {
+ SceneNameToSceneHandles.Add(scene.name, new Dictionary());
+ }
+
+ if (!SceneNameToSceneHandles[scene.name].ContainsKey(scene.handle))
+ {
+ var sceneEntry = new SceneEntry()
+ {
+ IsAssigned = true,
+ Scene = scene
+ };
+ SceneNameToSceneHandles[scene.name].Add(scene.handle, sceneEntry);
+ }
+ else
+ {
+ throw new Exception($"[Duplicate Handle] Scene {scene.name} already has scene handle {scene.handle} registered!");
+ }
+ }
+
+ ///
+ /// Determines if there is an existing scene loaded that matches the scene name but has not been assigned
+ ///
+ public bool DoesSceneHaveUnassignedEntry(string sceneName, NetworkManager networkManager)
+ {
+ var scenesWithSceneName = new List();
+
+ // Get all loaded scenes with the same name
+ for (int i = 0; i < SceneManager.sceneCount; i++)
+ {
+ var scene = SceneManager.GetSceneAt(i);
+ if (scene.name == sceneName)
+ {
+ scenesWithSceneName.Add(scene);
+ }
+ }
+
+ // If there are no scenes of this name loaded then we have no loaded scenes
+ // to use
+ if (scenesWithSceneName.Count == 0)
+ {
+ return false;
+ }
+
+ // If we have 1 or more scenes with the name and we have no entries, then we do have
+ // a scene to use
+ if (scenesWithSceneName.Count > 0 && !SceneNameToSceneHandles.ContainsKey(sceneName))
+ {
+ return true;
+ }
+
+ // Determine if any of the loaded scenes has been used for synchronizing
+ foreach (var scene in scenesWithSceneName)
+ {
+ // If we don't have the handle, then we can use that scene
+ if (!SceneNameToSceneHandles[scene.name].ContainsKey(scene.handle))
+ {
+ return true;
+ }
+
+ // If we have an entry, but it is not yet assigned (i.e. preloaded)
+ // then we can use that.
+ if (!SceneNameToSceneHandles[scene.name][scene.handle].IsAssigned)
+ {
+ return true;
+ }
+ }
+ // If none were found, then we have no available scene (which most likely means one will get loaded)
+ return false;
+ }
+
+ ///
+ /// This will find any scene entry that hasn't been used/assigned, set the entry to assigned, and
+ /// return the associated scene. If none are found it returns an invalid scene.
+ ///
+ public Scene GetSceneFromLoadedScenes(string sceneName, NetworkManager networkManager)
+ {
+ if (SceneNameToSceneHandles.ContainsKey(sceneName))
+ {
+ foreach (var sceneHandleEntry in SceneNameToSceneHandles[sceneName])
+ {
+ if (!sceneHandleEntry.Value.IsAssigned)
+ {
+ var sceneEntry = sceneHandleEntry.Value;
+ sceneEntry.IsAssigned = true;
+ SceneNameToSceneHandles[sceneName][sceneHandleEntry.Key] = sceneEntry;
+ return sceneEntry.Scene;
+ }
+ }
+ }
+ // If we found nothing return an invalid scene
+ return m_InvalidScene;
+ }
+
+ ///
+ /// Only invoked is client synchronization is additive, this will generate the scene tracking table
+ /// in order to re-use the same scenes the server is synchronizing instead of having to unload the
+ /// scenes and reload them when synchronizing (i.e. client disconnects due to external reason, the
+ /// same application instance is still running, the same scenes are still loaded on the client, and
+ /// upon reconnecting the client doesn't have to unload the scenes and then reload them)
+ ///
+ public void PopulateLoadedScenes(ref Dictionary scenesLoaded, NetworkManager networkManager)
+ {
+ SceneNameToSceneHandles.Clear();
+ var sceneCount = SceneManager.sceneCount;
+ for (int i = 0; i < sceneCount; i++)
+ {
+ var scene = SceneManager.GetSceneAt(i);
+ if (!SceneNameToSceneHandles.ContainsKey(scene.name))
+ {
+ SceneNameToSceneHandles.Add(scene.name, new Dictionary());
+ }
+
+ if (!SceneNameToSceneHandles[scene.name].ContainsKey(scene.handle))
+ {
+ var sceneEntry = new SceneEntry()
+ {
+ IsAssigned = false,
+ Scene = scene
+ };
+ SceneNameToSceneHandles[scene.name].Add(scene.handle, sceneEntry);
+ if (!scenesLoaded.ContainsKey(scene.handle))
+ {
+ scenesLoaded.Add(scene.handle, scene);
+ }
+ }
+ else
+ {
+ throw new Exception($"[Duplicate Handle] Scene {scene.name} already has scene handle {scene.handle} registered!");
+ }
+ }
+ }
+
+ private List m_ScenesToUnload = new List();
+
+ ///
+ /// Unloads any scenes that have not been assigned.
+ /// TODO: There needs to be a way to validate if a scene should be unloaded
+ /// or not (i.e. local client-side UI loaded additively)
+ ///
+ ///
+ public void UnloadUnassignedScenes(NetworkManager networkManager = null)
+ {
+ SceneManager.sceneUnloaded += SceneManager_SceneUnloaded;
+ foreach (var sceneEntry in SceneNameToSceneHandles)
+ {
+ var scenHandleEntries = SceneNameToSceneHandles[sceneEntry.Key];
+ foreach (var sceneHandleEntry in scenHandleEntries)
+ {
+ if (!sceneHandleEntry.Value.IsAssigned)
+ {
+ m_ScenesToUnload.Add(sceneHandleEntry.Value.Scene);
+ }
+ }
+ }
+ foreach (var sceneToUnload in m_ScenesToUnload)
+ {
+ SceneManager.UnloadSceneAsync(sceneToUnload);
+ }
+ }
+
+ private void SceneManager_SceneUnloaded(Scene scene)
+ {
+ if (SceneNameToSceneHandles.ContainsKey(scene.name))
+ {
+ if (SceneNameToSceneHandles[scene.name].ContainsKey(scene.handle))
+ {
+ SceneNameToSceneHandles[scene.name].Remove(scene.handle);
+ }
+ if (SceneNameToSceneHandles[scene.name].Count == 0)
+ {
+ SceneNameToSceneHandles.Remove(scene.name);
+ }
+ m_ScenesToUnload.Remove(scene);
+ if (m_ScenesToUnload.Count == 0)
+ {
+ SceneManager.sceneUnloaded -= SceneManager_SceneUnloaded;
+ }
+ }
+ }
+
+ ///
+ /// Handles determining if a client should attempt to load a scene during synchronization.
+ ///
+ /// name of the scene to be loaded
+ /// when in client synchronization mode single, this determines if the scene is the primary active scene
+ /// the current client synchronization mode
+ /// instance
+ ///
+ 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 mode we always pass through, but in LoadSceneMode.Single we only pass through if the currently active scene
+ // is the primary scene to be loaded
+ if (clientSynchronizationMode == LoadSceneMode.Additive || (isPrimaryScene && clientSynchronizationMode == LoadSceneMode.Single))
+ {
+ // don't try to reload this scene and pass through to post load processing.
+ shouldPassThrough = true;
+ }
+ }
+ return shouldPassThrough;
+ }
+
+ ///
+ /// Handles migrating dynamically spawned NetworkObjects to the DDOL when a scene is unloaded
+ ///
+ /// relative instance
+ /// scene being unloaded
+ public void MoveObjectsFromSceneToDontDestroyOnLoad(ref NetworkManager networkManager, Scene scene)
+ {
+ bool isActiveScene = scene == SceneManager.GetActiveScene();
+ // Create a local copy of the spawned objects list since the spawn manager will adjust the list as objects
+ // are despawned.
+ var localSpawnedObjectsHashSet = new HashSet(networkManager.SpawnManager.SpawnedObjectsList);
+ foreach (var networkObject in localSpawnedObjectsHashSet)
+ {
+ if (networkObject == null || (networkObject != null && networkObject.gameObject.scene.handle != scene.handle))
+ {
+ 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)
+ {
+ UnityEngine.Object.DontDestroyOnLoad(networkObject.gameObject);
+ }
+ }
+ else if (networkManager.IsServer)
+ {
+ networkObject.Despawn();
+ }
+ else // We are a client, migrate the object into the DDOL temporarily until it receives the destroy command from the server
+ {
+ UnityEngine.Object.DontDestroyOnLoad(networkObject.gameObject);
+ }
+ }
+ }
+
+ ///
+ /// Sets the client synchronization mode which impacts whether both the server or client take into consideration scenes loaded before
+ /// starting the .
+ ///
+ ///
+ /// : Does not take preloaded scenes into consideration
+ /// : Does take preloaded scenes into consideration
+ ///
+ /// relative instance
+ /// or
+ public void SetClientSynchronizationMode(ref NetworkManager networkManager, LoadSceneMode mode)
+ {
+ var sceneManager = networkManager.SceneManager;
+
+ // For additive client synchronization, we take into consideration scenes
+ // already loaded.
+ if (mode == LoadSceneMode.Additive)
+ {
+ for (int i = 0; i < SceneManager.sceneCount; i++)
+ {
+ var scene = SceneManager.GetSceneAt(i);
+
+ // 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))
+ {
+ sceneManager.ScenesLoaded.Add(scene.handle, scene);
+ }
+ }
+ }
+ // Set the client synchronization mode
+ sceneManager.ClientSynchronizationMode = mode;
+ }
+ }
+}
diff --git a/com.unity.netcode.gameobjects/Runtime/SceneManagement/DefaultSceneManagerHandler.cs.meta b/com.unity.netcode.gameobjects/Runtime/SceneManagement/DefaultSceneManagerHandler.cs.meta
new file mode 100644
index 0000000000..a89ce83b79
--- /dev/null
+++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/DefaultSceneManagerHandler.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 8c18076bb9734cf4ea7297f85b7729be
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/com.unity.netcode.gameobjects/Runtime/SceneManagement/ISceneManagerHandler.cs b/com.unity.netcode.gameobjects/Runtime/SceneManagement/ISceneManagerHandler.cs
index 8b5b7e7fda..240a5c94f6 100644
--- a/com.unity.netcode.gameobjects/Runtime/SceneManagement/ISceneManagerHandler.cs
+++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/ISceneManagerHandler.cs
@@ -1,3 +1,4 @@
+using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
@@ -12,5 +13,24 @@ internal interface ISceneManagerHandler
AsyncOperation LoadSceneAsync(string sceneName, LoadSceneMode loadSceneMode, SceneEventProgress sceneEventProgress);
AsyncOperation UnloadSceneAsync(Scene scene, SceneEventProgress sceneEventProgress);
+
+ void PopulateLoadedScenes(ref Dictionary scenesLoaded, NetworkManager networkManager = null);
+ Scene GetSceneFromLoadedScenes(string sceneName, NetworkManager networkManager = null);
+
+ bool DoesSceneHaveUnassignedEntry(string sceneName, NetworkManager networkManager = null);
+
+ void StopTrackingScene(int handle, string name, NetworkManager networkManager = null);
+
+ void StartTrackingScene(Scene scene, bool assigned, NetworkManager networkManager = null);
+
+ void ClearSceneTracking(NetworkManager networkManager = null);
+
+ void UnloadUnassignedScenes(NetworkManager networkManager = null);
+
+ void MoveObjectsFromSceneToDontDestroyOnLoad(ref NetworkManager networkManager, Scene scene);
+
+ void SetClientSynchronizationMode(ref NetworkManager networkManager, LoadSceneMode mode);
+
+ bool ClientShouldPassThrough(string sceneName, bool isPrimaryScene, LoadSceneMode clientSynchronizationMode, NetworkManager networkManager);
}
}
diff --git a/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs b/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs
index b53890396f..71833eab14 100644
--- a/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs
+++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs
@@ -328,31 +328,38 @@ public class NetworkSceneManager : IDisposable
///
public VerifySceneBeforeLoadingDelegateHandler VerifySceneBeforeLoading;
+ private bool m_ActiveSceneSynchronizationEnabled;
///
- /// The SceneManagerHandler implementation
+ /// When enabled, the server or host will synchronize clients with changes to the currently active scene
///
- internal ISceneManagerHandler SceneManagerHandler = new DefaultSceneManagerHandler();
-
- ///
- /// The default SceneManagerHandler that interfaces between the SceneManager and NetworkSceneManager
- ///
- private class DefaultSceneManagerHandler : ISceneManagerHandler
+ public bool ActiveSceneSynchronizationEnabled
{
- public AsyncOperation LoadSceneAsync(string sceneName, LoadSceneMode loadSceneMode, SceneEventProgress sceneEventProgress)
+ get
{
- var operation = SceneManager.LoadSceneAsync(sceneName, loadSceneMode);
- sceneEventProgress.SetAsyncOperation(operation);
- return operation;
+ return m_ActiveSceneSynchronizationEnabled;
}
-
- public AsyncOperation UnloadSceneAsync(Scene scene, SceneEventProgress sceneEventProgress)
+ set
{
- var operation = SceneManager.UnloadSceneAsync(scene);
- sceneEventProgress.SetAsyncOperation(operation);
- return operation;
+ if (m_ActiveSceneSynchronizationEnabled != value)
+ {
+ m_ActiveSceneSynchronizationEnabled = value;
+ if (m_ActiveSceneSynchronizationEnabled)
+ {
+ SceneManager.activeSceneChanged += SceneManager_ActiveSceneChanged;
+ }
+ else
+ {
+ SceneManager.activeSceneChanged -= SceneManager_ActiveSceneChanged;
+ }
+ }
}
}
+ ///
+ /// The SceneManagerHandler implementation
+ ///
+ internal ISceneManagerHandler SceneManagerHandler = new DefaultSceneManagerHandler();
+
internal readonly Dictionary SceneEventProgressTracking = new Dictionary();
///
@@ -385,6 +392,77 @@ public AsyncOperation UnloadSceneAsync(Scene scene, SceneEventProgress sceneEven
/// instances with client unique scene instances
///
internal Dictionary ServerSceneHandleToClientSceneHandle = new Dictionary();
+ internal Dictionary ClientSceneHandleToServerSceneHandle = new Dictionary();
+
+ ///
+ /// Add the client to server (and vice versa) scene handle lookup.
+ /// Add the client-side handle to scene entry in the HandleToScene table.
+ /// If it fails (i.e. already added) it returns false.
+ ///
+ internal bool UpdateServerClientSceneHandle(int serverHandle, int clientHandle, Scene localScene)
+ {
+ if (!ServerSceneHandleToClientSceneHandle.ContainsKey(serverHandle))
+ {
+ ServerSceneHandleToClientSceneHandle.Add(serverHandle, clientHandle);
+ }
+ else
+ {
+ return false;
+ }
+
+ if (!ClientSceneHandleToServerSceneHandle.ContainsKey(clientHandle))
+ {
+ ClientSceneHandleToServerSceneHandle.Add(clientHandle, serverHandle);
+ }
+ else
+ {
+ return false;
+ }
+
+ // It is "Ok" if this already has an entry
+ if (!ScenesLoaded.ContainsKey(clientHandle))
+ {
+ ScenesLoaded.Add(clientHandle, localScene);
+ }
+
+ return true;
+ }
+
+ ///
+ /// Removes the client to server (and vice versa) scene handles.
+ /// If it fails (i.e. already removed) it returns false.
+ ///
+ internal bool RemoveServerClientSceneHandle(int serverHandle, int clientHandle)
+ {
+ if (ServerSceneHandleToClientSceneHandle.ContainsKey(serverHandle))
+ {
+ ServerSceneHandleToClientSceneHandle.Remove(serverHandle);
+ }
+ else
+ {
+ return false;
+ }
+
+ if (ClientSceneHandleToServerSceneHandle.ContainsKey(clientHandle))
+ {
+ ClientSceneHandleToServerSceneHandle.Remove(clientHandle);
+ }
+ else
+ {
+ return false;
+ }
+
+ if (ScenesLoaded.ContainsKey(clientHandle))
+ {
+ ScenesLoaded.Remove(clientHandle);
+ }
+ else
+ {
+ return false;
+ }
+
+ return true;
+ }
///
/// Hash to build index lookup table
@@ -413,6 +491,7 @@ public AsyncOperation UnloadSceneAsync(Scene scene, SceneEventProgress sceneEven
private NetworkManager m_NetworkManager { get; }
+ // Keep track of this scene until the NetworkSceneManager is destroyed.
internal Scene DontDestroyOnLoadScene;
///
@@ -435,11 +514,12 @@ public AsyncOperation UnloadSceneAsync(Scene scene, SceneEventProgress sceneEven
///
public void Dispose()
{
+ // Always assure we no longer listen to scene changes when disposed.
+ SceneManager.activeSceneChanged -= SceneManager_ActiveSceneChanged;
SceneUnloadEventHandler.Shutdown();
-
foreach (var keypair in SceneEventDataStore)
{
- if (NetworkLog.CurrentLogLevel <= LogLevel.Normal)
+ if (NetworkLog.CurrentLogLevel == LogLevel.Developer)
{
NetworkLog.LogInfo($"{nameof(SceneEventDataStore)} is disposing {nameof(SceneEventData.SceneEventId)} '{keypair.Key}'.");
}
@@ -493,6 +573,11 @@ internal string GetSceneNameFromPath(string scenePath)
///
internal void GenerateScenesInBuild()
{
+ // TODO 2023: We could support addressable or asset bundle scenes by
+ // adding a method that would allow users to add scenes to this.
+ // The method would be server-side only and require an additional SceneEventType
+ // that would be used to notify clients of the added scene. This might need
+ // to include information about the addressable or asset bundle (i.e. address to load assets)
HashToBuildIndex.Clear();
BuildIndexToHash.Clear();
for (int i = 0; i < SceneManager.sceneCountInBuildSettings; i++)
@@ -592,7 +677,8 @@ public void DisableValidationWarnings(bool disabled)
/// for initial client synchronization
public void SetClientSynchronizationMode(LoadSceneMode mode)
{
- ClientSynchronizationMode = mode;
+ var networkManager = m_NetworkManager;
+ SceneManagerHandler.SetClientSynchronizationMode(ref networkManager, mode);
}
///
@@ -605,13 +691,46 @@ internal NetworkSceneManager(NetworkManager networkManager)
m_NetworkManager = networkManager;
SceneEventDataStore = new Dictionary();
+ // Generates the scene name to hash value
GenerateScenesInBuild();
// Since NetworkManager is now always migrated to the DDOL we will use this to get the DDOL scene
DontDestroyOnLoadScene = networkManager.gameObject.scene;
- ServerSceneHandleToClientSceneHandle.Add(DontDestroyOnLoadScene.handle, DontDestroyOnLoadScene.handle);
- ScenesLoaded.Add(DontDestroyOnLoadScene.handle, DontDestroyOnLoadScene);
+ // Add to the server to client scene handle table
+ UpdateServerClientSceneHandle(DontDestroyOnLoadScene.handle, DontDestroyOnLoadScene.handle, DontDestroyOnLoadScene);
+ }
+
+ ///
+ /// Synchronizes clients when the currently active scene is changed
+ ///
+ private void SceneManager_ActiveSceneChanged(Scene current, Scene next)
+ {
+ // If no clients are connected, then don't worry about notifications
+ if (!(m_NetworkManager.ConnectedClientsIds.Count > (m_NetworkManager.IsHost ? 1 : 0)))
+ {
+ return;
+ }
+
+ // Don't notify if a scene event is in progress
+ foreach (var sceneEventEntry in SceneEventProgressTracking)
+ {
+ if (!sceneEventEntry.Value.HasTimedOut() && sceneEventEntry.Value.Status == SceneEventProgressStatus.Started)
+ {
+ return;
+ }
+ }
+
+ // If the scene's build index is in the hash table
+ if (BuildIndexToHash.ContainsKey(next.buildIndex))
+ {
+ // Notify clients of the change in active scene
+ var sceneEvent = BeginSceneEvent();
+ sceneEvent.SceneEventType = SceneEventType.ActiveSceneChanged;
+ sceneEvent.ActiveSceneHash = BuildIndexToHash[next.buildIndex];
+ SendSceneEventData(sceneEvent.SceneEventId, m_NetworkManager.ConnectedClientsIds.Where(c => c != NetworkManager.ServerClientId).ToArray());
+ EndSceneEvent(sceneEvent.SceneEventId);
+ }
}
///
@@ -675,11 +794,11 @@ internal Scene GetAndAddNewlyLoadedSceneByName(string sceneName)
if (!ScenesLoaded.ContainsKey(sceneLoaded.handle))
{
ScenesLoaded.Add(sceneLoaded.handle, sceneLoaded);
+ SceneManagerHandler.StartTrackingScene(sceneLoaded, true, m_NetworkManager);
return sceneLoaded;
}
}
}
-
throw new Exception($"Failed to find any loaded scene named {sceneName}!");
}
}
@@ -788,7 +907,7 @@ private void SendSceneEventData(uint sceneEventId, ulong[] targetClientIds)
///
/// the scene to be unloaded
///
- private SceneEventProgress ValidateSceneEventUnLoading(Scene scene)
+ private SceneEventProgress ValidateSceneEventUnloading(Scene scene)
{
if (!m_NetworkManager.IsServer)
{
@@ -939,7 +1058,7 @@ public SceneEventProgressStatus UnloadScene(Scene scene)
return SceneEventProgressStatus.SceneNotLoaded;
}
- var sceneEventProgress = ValidateSceneEventUnLoading(scene);
+ var sceneEventProgress = ValidateSceneEventUnloading(scene);
if (sceneEventProgress.Status != SceneEventProgressStatus.Started)
{
return sceneEventProgress.Status;
@@ -950,6 +1069,13 @@ public SceneEventProgressStatus UnloadScene(Scene scene)
Debug.LogError($"{nameof(UnloadScene)} internal error! {sceneName} with handle {scene.handle} is not within the internal scenes loaded dictionary!");
return SceneEventProgressStatus.InternalNetcodeError;
}
+
+ // Any NetworkObjects marked to not be destroyed with a scene and reside within the scene about to be unloaded
+ // should be migrated temporarily into the DDOL, once the scene is unloaded they will be migrated into the
+ // currently active scene.
+ var networkManager = m_NetworkManager;
+ SceneManagerHandler.MoveObjectsFromSceneToDontDestroyOnLoad(ref networkManager, scene);
+
var sceneEventData = BeginSceneEvent();
sceneEventData.SceneEventProgressId = sceneEventProgress.Guid;
sceneEventData.SceneEventType = SceneEventType.Unload;
@@ -964,7 +1090,6 @@ public SceneEventProgressStatus UnloadScene(Scene scene)
sceneEventProgress.SceneEventId = sceneEventData.SceneEventId;
sceneEventProgress.OnSceneEventCompleted = OnSceneUnloaded;
var sceneUnload = SceneManagerHandler.UnloadSceneAsync(scene, sceneEventProgress);
-
// Notify local server that a scene is going to be unloaded
OnSceneEvent?.Invoke(new SceneEvent()
{
@@ -1006,16 +1131,28 @@ private void OnClientUnloadScene(uint sceneEventId)
throw new Exception($"Client failed to unload scene {sceneName} " +
$"because the client scene handle {sceneHandle} was not found in ScenesLoaded!");
}
+
+ var scene = ScenesLoaded[sceneHandle];
+ // Any NetworkObjects marked to not be destroyed with a scene and reside within the scene about to be unloaded
+ // should be migrated temporarily into the DDOL, once the scene is unloaded they will be migrated into the
+ // currently active scene.
+ var networkManager = m_NetworkManager;
+ SceneManagerHandler.MoveObjectsFromSceneToDontDestroyOnLoad(ref networkManager, scene);
+
m_IsSceneEventActive = true;
var sceneEventProgress = new SceneEventProgress(m_NetworkManager);
sceneEventProgress.SceneEventId = sceneEventData.SceneEventId;
sceneEventProgress.OnSceneEventCompleted = OnSceneUnloaded;
- var sceneUnload = SceneManagerHandler.UnloadSceneAsync(ScenesLoaded[sceneHandle], sceneEventProgress);
+ var sceneUnload = SceneManagerHandler.UnloadSceneAsync(scene, sceneEventProgress);
- ScenesLoaded.Remove(sceneHandle);
+ SceneManagerHandler.StopTrackingScene(sceneHandle, sceneName, m_NetworkManager);
// Remove our server to scene handle lookup
- ServerSceneHandleToClientSceneHandle.Remove(sceneEventData.SceneHandle);
+ if (!RemoveServerClientSceneHandle(sceneEventData.SceneHandle, sceneHandle))
+ {
+ // If the exact same handle exists then there are problems with using handles
+ throw new Exception($"Failed to remove server scene handle ({sceneEventData.SceneHandle}) or client scene handle({sceneHandle})! Happened during scene unload for {sceneName}.");
+ }
// Notify the local client that a scene is going to be unloaded
OnSceneEvent?.Invoke(new SceneEvent()
@@ -1042,6 +1179,9 @@ private void OnSceneUnloaded(uint sceneEventId)
return;
}
+ // Migrate the NetworkObjects marked to not be destroyed with the scene into the currently active scene
+ MoveObjectsFromDontDestroyOnLoadToScene(SceneManager.GetActiveScene());
+
var sceneEventData = SceneEventDataStore[sceneEventId];
// First thing we do, if we are a server, is to send the unload scene event.
if (m_NetworkManager.IsServer)
@@ -1112,6 +1252,7 @@ internal void UnloadAdditivelyLoadedScenes(uint sceneEventId)
}
// clear out our scenes loaded list
ScenesLoaded.Clear();
+ SceneManagerHandler.ClearSceneTracking(m_NetworkManager);
}
///
@@ -1403,11 +1544,7 @@ private void OnSceneLoaded(uint sceneEventId)
else
{
// For the client, we make a server scene handle to client scene handle look up table
- if (!ServerSceneHandleToClientSceneHandle.ContainsKey(sceneEventData.SceneHandle))
- {
- ServerSceneHandleToClientSceneHandle.Add(sceneEventData.SceneHandle, nextScene.handle);
- }
- else
+ if (!UpdateServerClientSceneHandle(sceneEventData.SceneHandle, nextScene.handle, nextScene))
{
// If the exact same handle exists then there are problems with using handles
throw new Exception($"Server Scene Handle ({sceneEventData.SceneHandle}) already exist! Happened during scene load of {nextScene.name} with Client Handle ({nextScene.handle})");
@@ -1530,12 +1667,16 @@ internal void SynchronizeNetworkObjects(ulong clientId)
m_NetworkManager.SpawnManager.UpdateObservedNetworkObjects(clientId);
var sceneEventData = BeginSceneEvent();
-
+ sceneEventData.ClientSynchronizationMode = ClientSynchronizationMode;
sceneEventData.InitializeForSynch();
sceneEventData.TargetClientId = clientId;
sceneEventData.LoadSceneMode = ClientSynchronizationMode;
var activeScene = SceneManager.GetActiveScene();
sceneEventData.SceneEventType = SceneEventType.Synchronize;
+ if (BuildIndexToHash.ContainsKey(activeScene.buildIndex))
+ {
+ sceneEventData.ActiveSceneHash = BuildIndexToHash[activeScene.buildIndex];
+ }
// Organize how (and when) we serialize our NetworkObjects
for (int i = 0; i < SceneManager.sceneCount; i++)
@@ -1549,6 +1690,11 @@ internal void SynchronizeNetworkObjects(ulong clientId)
continue;
}
+ if (scene == DontDestroyOnLoadScene)
+ {
+ continue;
+ }
+
var sceneHash = SceneHashFromNameOrPath(scene.path);
// This would depend upon whether we are additive or not
@@ -1620,9 +1766,6 @@ private void OnClientBeginSync(uint sceneEventId)
});
OnSynchronize?.Invoke(m_NetworkManager.LocalClientId);
-
- // Clear the in-scene placed NetworkObjects when we load the first scene in our synchronization process
- ScenePlacedObjects.Clear();
}
// Always check to see if the scene needs to be validated
@@ -1636,16 +1779,14 @@ private void OnClientBeginSync(uint sceneEventId)
return;
}
- var shouldPassThrough = false;
var sceneLoad = (AsyncOperation)null;
- // Check to see if the client already has loaded the scene to be loaded
- if (sceneName == activeScene.name)
- {
- // If the client is already in the same scene, then pass through and
- // don't try to reload it.
- shouldPassThrough = true;
- }
+ // Determines if the client has the scene to be loaded already loaded, if so will return true and the client will skip loading this scene
+ // For ClientSynchronizationMode LoadSceneMode.Single, we pass in whether the scene being loaded is the first/primary active scene and if it is already loaded
+ // it should pass through to post load processing (ClientLoadedSynchronization).
+ // For ClientSynchronizationMode LoadSceneMode.Additive, if the scene is already loaded or the active scene is the scene to be loaded (does not require it to
+ // be the initial primary scene) then go ahead and pass through to post load processing (ClientLoadedSynchronization).
+ var shouldPassThrough = SceneManagerHandler.ClientShouldPassThrough(sceneName, sceneHash == sceneEventData.SceneHash, ClientSynchronizationMode, m_NetworkManager);
if (!shouldPassThrough)
{
@@ -1683,7 +1824,11 @@ private void ClientLoadedSynchronization(uint sceneEventId)
{
var sceneEventData = SceneEventDataStore[sceneEventId];
var sceneName = SceneNameFromHash(sceneEventData.ClientSceneHash);
- var nextScene = GetAndAddNewlyLoadedSceneByName(sceneName);
+ var nextScene = SceneManagerHandler.GetSceneFromLoadedScenes(sceneName, m_NetworkManager);
+ if (!nextScene.IsValid())
+ {
+ nextScene = GetAndAddNewlyLoadedSceneByName(sceneName);
+ }
if (!nextScene.isLoaded || !nextScene.IsValid())
{
@@ -1698,11 +1843,8 @@ private void ClientLoadedSynchronization(uint sceneEventId)
SceneManager.SetActiveScene(nextScene);
}
- if (!ServerSceneHandleToClientSceneHandle.ContainsKey(sceneEventData.NetworkSceneHandle))
- {
- ServerSceneHandleToClientSceneHandle.Add(sceneEventData.NetworkSceneHandle, nextScene.handle);
- }
- else
+ // For the client, we make a server scene handle to client scene handle look up table
+ if (!UpdateServerClientSceneHandle(sceneEventData.NetworkSceneHandle, nextScene.handle, nextScene))
{
// If the exact same handle exists then there are problems with using handles
throw new Exception($"Server Scene Handle ({sceneEventData.SceneHandle}) already exist! Happened during scene load of {nextScene.name} with Client Handle ({nextScene.handle})");
@@ -1744,6 +1886,48 @@ private void ClientLoadedSynchronization(uint sceneEventId)
HandleClientSceneEvent(sceneEventId);
}
+ ///
+ /// Makes sure that client-side instantiated dynamically spawned NetworkObjects are migrated
+ /// into the same scene (if not already) as they are on the server-side during the initial
+ /// client connection synchronization process.
+ ///
+ private void SynchronizeNetworkObjectScene()
+ {
+ foreach (var networkObject in m_NetworkManager.SpawnManager.SpawnedObjectsList)
+ {
+ // This is only done for dynamically spawned NetworkObjects
+ // Theoretically, a server could have NetworkObjects in a server-side only scene, if the client doesn't have that scene loaded
+ // then skip it (it will reside in the currently active scene in this scenario on the client-side)
+ if (networkObject.IsSceneObject.Value == false && ServerSceneHandleToClientSceneHandle.ContainsKey(networkObject.NetworkSceneHandle))
+ {
+ networkObject.SceneOriginHandle = ServerSceneHandleToClientSceneHandle[networkObject.NetworkSceneHandle];
+
+
+
+ // If the NetworkObject does not have a parent and is not in the same scene as it is on the server side, then find the right scene
+ // and move it to that scene.
+ if (networkObject.gameObject.scene.handle != networkObject.SceneOriginHandle && networkObject.transform.parent == null)
+ {
+ if (ScenesLoaded.ContainsKey(networkObject.SceneOriginHandle))
+ {
+ var scene = ScenesLoaded[networkObject.SceneOriginHandle];
+ if (scene == DontDestroyOnLoadScene)
+ {
+ Debug.Log($"{networkObject.gameObject.name} migrating into DDOL!");
+ }
+
+ SceneManager.MoveGameObjectToScene(networkObject.gameObject, scene);
+ }
+ else if (m_NetworkManager.LogLevel <= LogLevel.Normal)
+ {
+ NetworkLog.LogWarningServer($"[Client-{m_NetworkManager.LocalClientId}][{networkObject.gameObject.name}] Server - " +
+ $"client scene mismatch detected! Client-side has no scene loaded with handle ({networkObject.SceneOriginHandle})!");
+ }
+ }
+ }
+ }
+ }
+
///
/// Client Side:
/// Handles incoming Scene_Event messages for clients
@@ -1754,6 +1938,23 @@ private void HandleClientSceneEvent(uint sceneEventId)
var sceneEventData = SceneEventDataStore[sceneEventId];
switch (sceneEventData.SceneEventType)
{
+ case SceneEventType.ActiveSceneChanged:
+ {
+ if (HashToBuildIndex.ContainsKey(sceneEventData.ActiveSceneHash))
+ {
+ var scene = SceneManager.GetSceneByBuildIndex(HashToBuildIndex[sceneEventData.ActiveSceneHash]);
+ if (scene.isLoaded)
+ {
+ SceneManager.SetActiveScene(scene);
+ }
+ }
+ break;
+ }
+ case SceneEventType.ObjectSceneChanged:
+ {
+ MigrateNetworkObjectsIntoScenes();
+ break;
+ }
case SceneEventType.Load:
{
OnClientSceneLoadingEvent(sceneEventId);
@@ -1777,6 +1978,19 @@ private void HandleClientSceneEvent(uint sceneEventId)
// Synchronize the NetworkObjects for this scene
sceneEventData.SynchronizeSceneNetworkObjects(m_NetworkManager);
+ // If needed, set the currently active scene
+ if (HashToBuildIndex.ContainsKey(sceneEventData.ActiveSceneHash))
+ {
+ var targetActiveScene = SceneManager.GetSceneByBuildIndex(HashToBuildIndex[sceneEventData.ActiveSceneHash]);
+ if (targetActiveScene.isLoaded && targetActiveScene.handle != SceneManager.GetActiveScene().handle)
+ {
+ SceneManager.SetActiveScene(targetActiveScene);
+ }
+ }
+
+ // If needed, migrate dynamically spawned NetworkObjects to the same scene as on the server side
+ SynchronizeNetworkObjectScene();
+
sceneEventData.SceneEventType = SceneEventType.SynchronizeComplete;
SendSceneEventData(sceneEventId, new ulong[] { NetworkManager.ServerClientId });
@@ -1795,6 +2009,8 @@ private void HandleClientSceneEvent(uint sceneEventId)
OnSynchronizeComplete?.Invoke(m_NetworkManager.LocalClientId);
+ SceneManagerHandler.UnloadUnassignedScenes(m_NetworkManager);
+
EndSceneEvent(sceneEventId);
}
break;
@@ -1907,9 +2123,12 @@ private void HandleServerSceneEvent(uint sceneEventId, ulong clientId)
OnSynchronizeComplete?.Invoke(clientId);
- // We now can call the client connected callback on the server at this time
- // This assures the client is fully synchronized with all loaded scenes and
- // NetworkObjects
+ // At this time the client is fully synchronized with all loaded scenes and
+ // NetworkObjects and should be considered "fully connected". Send the
+ // notification that the client is connected.
+ // TODO 2023: We should have a better name for this or have multiple states the
+ // client progresses through (the name and associated legacy behavior/expected state
+ // of the client was persisted since MLAPI)
m_NetworkManager.InvokeOnClientConnectedCallback(clientId);
// Check to see if the client needs to resynchronize and before sending the message make sure the client is still connected to avoid
@@ -1955,6 +2174,23 @@ internal void HandleSceneEvent(ulong clientId, FastBufferReader reader)
if (sceneEventData.IsSceneEventClientSide())
{
+ // If the client is being synchronized for the first time do some initialization
+ if (sceneEventData.SceneEventType == SceneEventType.Synchronize)
+ {
+ ScenePlacedObjects.Clear();
+ // Set the server's configured client synchronization mode on the client side
+ ClientSynchronizationMode = sceneEventData.ClientSynchronizationMode;
+
+ // Only if ClientSynchronizationMode is Additive and the client receives a synchronize scene event
+ if (ClientSynchronizationMode == LoadSceneMode.Additive)
+ {
+ // Check for scenes already loaded and create a table of scenes already loaded (SceneEntries) that will be
+ // used if the server is synchronizing the same scenes (i.e. if a matching scene is already loaded on the
+ // client side, then that scene will be used as opposed to loading another scene). This allows for clients
+ // to reconnect to a network session without having to unload all of the scenes and reload all of the scenes.
+ SceneManagerHandler.PopulateLoadedScenes(ref ScenesLoaded, m_NetworkManager);
+ }
+ }
HandleClientSceneEvent(sceneEventData.SceneEventId);
}
else
@@ -1974,26 +2210,28 @@ internal void HandleSceneEvent(ulong clientId, FastBufferReader reader)
///
internal void MoveObjectsToDontDestroyOnLoad()
{
- // Move ALL NetworkObjects marked to persist scene transitions into the DDOL scene
- var objectsToKeep = new HashSet(m_NetworkManager.SpawnManager.SpawnedObjectsList);
- foreach (var sobj in objectsToKeep)
+ // Create a local copy of the spawned objects list since the spawn manager will adjust the list as objects
+ // are despawned.
+ var localSpawnedObjectsHashSet = new HashSet(m_NetworkManager.SpawnManager.SpawnedObjectsList);
+ foreach (var networkObject in localSpawnedObjectsHashSet)
{
- if (sobj == null)
+ if (networkObject == null || (networkObject != null && networkObject.gameObject.scene == DontDestroyOnLoadScene))
{
continue;
}
- if (!sobj.DestroyWithScene || sobj.gameObject.scene == DontDestroyOnLoadScene)
+ // Only NetworkObjects marked to not be destroyed with the scene
+ if (!networkObject.DestroyWithScene)
{
- // Only move dynamically spawned network objects with no parent as child objects will follow
- if (sobj.gameObject.transform.parent == null && sobj.IsSceneObject != null && !sobj.IsSceneObject.Value)
+ // 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)
{
- UnityEngine.Object.DontDestroyOnLoad(sobj.gameObject);
+ UnityEngine.Object.DontDestroyOnLoad(networkObject.gameObject);
}
}
else if (m_NetworkManager.IsServer)
{
- sobj.Despawn();
+ networkObject.Despawn();
}
}
}
@@ -2028,9 +2266,10 @@ internal void PopulateScenePlacedObjects(Scene sceneToFilterBy, bool clearSceneP
foreach (var networkObjectInstance in networkObjects)
{
var globalObjectIdHash = networkObjectInstance.GlobalObjectIdHash;
- var sceneHandle = networkObjectInstance.GetSceneOriginHandle();
+ var sceneHandle = networkObjectInstance.gameObject.scene.handle;
// We check to make sure the NetworkManager instance is the same one to be "NetcodeIntegrationTestHelpers" compatible and filter the list on a per scene basis (for additive scenes)
- if (networkObjectInstance.IsSceneObject != false && networkObjectInstance.NetworkManager == m_NetworkManager && sceneHandle == sceneToFilterBy.handle)
+ if (networkObjectInstance.IsSceneObject != false && (networkObjectInstance.NetworkManager == m_NetworkManager ||
+ networkObjectInstance.NetworkManagerOwner == null) && sceneHandle == sceneToFilterBy.handle)
{
if (!ScenePlacedObjects.ContainsKey(globalObjectIdHash))
{
@@ -2057,26 +2296,128 @@ internal void PopulateScenePlacedObjects(Scene sceneToFilterBy, bool clearSceneP
/// scene to move the NetworkObjects to
internal void MoveObjectsFromDontDestroyOnLoadToScene(Scene scene)
{
- // Move ALL NetworkObjects to the temp scene
- var objectsToKeep = m_NetworkManager.SpawnManager.SpawnedObjectsList;
-
- foreach (var sobj in objectsToKeep)
+ foreach (var networkObject in m_NetworkManager.SpawnManager.SpawnedObjectsList)
{
- if (sobj == null)
+ if (networkObject == null)
{
continue;
}
// If it is in the DDOL then
- if (sobj.gameObject.scene == DontDestroyOnLoadScene)
+ if (networkObject.gameObject.scene == DontDestroyOnLoadScene && !networkObject.DestroyWithScene)
{
// only move dynamically spawned network objects, with no parent as child objects will follow,
// back into the currently active scene
- if (sobj.gameObject.transform.parent == null && sobj.IsSceneObject != null && !sobj.IsSceneObject.Value)
+ if (networkObject.gameObject.transform.parent == null && networkObject.IsSceneObject != null && !networkObject.IsSceneObject.Value)
{
- SceneManager.MoveGameObjectToScene(sobj.gameObject, scene);
+ SceneManager.MoveGameObjectToScene(networkObject.gameObject, scene);
}
}
}
}
+
+ ///
+ /// Holds a list of scene handles (server-side relative) and NetworkObjects migrated into it
+ /// during the current frame.
+ ///
+ internal Dictionary> ObjectsMigratedIntoNewScene = new Dictionary>();
+
+ ///
+ /// Handles notifying clients when a NetworkObject has been migrated into a new scene
+ ///
+ internal void NotifyNetworkObjectSceneChanged(NetworkObject networkObject)
+ {
+ // Really, this should never happen but in case it does
+ if (!m_NetworkManager.IsServer)
+ {
+ if (m_NetworkManager.LogLevel == LogLevel.Developer)
+ {
+ NetworkLog.LogErrorServer("[Please Report This Error][NotifyNetworkObjectSceneChanged] A client is trying to notify of an object's scene change!");
+ }
+ return;
+ }
+
+ // Ignore in-scene placed NetworkObjects
+ if (networkObject.IsSceneObject != false)
+ {
+ // Really, this should ever happen but in case it does
+ if (m_NetworkManager.LogLevel == LogLevel.Developer)
+ {
+ NetworkLog.LogErrorServer("[Please Report This Error][NotifyNetworkObjectSceneChanged] Trying to notify in-scene placed object scene change!");
+ }
+ return;
+ }
+
+ // Ignore if the scene is the currently active scene and the NetworkObject is auto synchronizing/migrating
+ // to the currently active scene.
+ if (networkObject.gameObject.scene == SceneManager.GetActiveScene() && networkObject.ActiveSceneSynchronization)
+ {
+ return;
+ }
+
+ // Don't notify if a scene event is in progress
+ foreach (var sceneEventEntry in SceneEventProgressTracking)
+ {
+ if (!sceneEventEntry.Value.HasTimedOut() && sceneEventEntry.Value.Status == SceneEventProgressStatus.Started)
+ {
+ return;
+ }
+ }
+
+ // Otherwise, add the NetworkObject into the list of NetworkObjects who's scene has changed
+ if (!ObjectsMigratedIntoNewScene.ContainsKey(networkObject.gameObject.scene.handle))
+ {
+ ObjectsMigratedIntoNewScene.Add(networkObject.gameObject.scene.handle, new List());
+ }
+ ObjectsMigratedIntoNewScene[networkObject.gameObject.scene.handle].Add(networkObject);
+ }
+
+ ///
+ /// Invoked by clients when processing a event
+ ///
+ private void MigrateNetworkObjectsIntoScenes()
+ {
+ try
+ {
+ foreach (var sceneEntry in ObjectsMigratedIntoNewScene)
+ {
+ if (ServerSceneHandleToClientSceneHandle.ContainsKey(sceneEntry.Key))
+ {
+ var clientSceneHandle = ServerSceneHandleToClientSceneHandle[sceneEntry.Key];
+ if (ScenesLoaded.ContainsKey(ServerSceneHandleToClientSceneHandle[sceneEntry.Key]))
+ {
+ var scene = ScenesLoaded[clientSceneHandle];
+ foreach (var networkObject in sceneEntry.Value)
+ {
+ SceneManager.MoveGameObjectToScene(networkObject.gameObject, scene);
+ }
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ NetworkLog.LogErrorServer($"{ex.Message}\n Stack Trace:\n {ex.StackTrace}");
+ }
+
+ // Clear out the list once complete
+ ObjectsMigratedIntoNewScene.Clear();
+ }
+
+ ///
+ /// Should be invoked during PostLateUpdate just prior to the
+ /// MessagingSystem processes its outbound message queue.
+ ///
+ internal void CheckForAndSendNetworkObjectSceneChanged()
+ {
+ // Early exit if not the server or there is nothing pending
+ if (!m_NetworkManager.IsServer || ObjectsMigratedIntoNewScene.Count == 0)
+ {
+ return;
+ }
+ var sceneEvent = BeginSceneEvent();
+ sceneEvent.SceneEventType = SceneEventType.ObjectSceneChanged;
+ SendSceneEventData(sceneEvent.SceneEventId, m_NetworkManager.ConnectedClientsIds.Where(c => c != NetworkManager.ServerClientId).ToArray());
+ EndSceneEvent(sceneEvent.SceneEventId);
+ }
}
}
diff --git a/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs b/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs
index f4c9927180..d679d2ff26 100644
--- a/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs
+++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs
@@ -80,6 +80,16 @@ public enum SceneEventType : byte
/// Event Notification: Both server and client receive a local notification.
///
SynchronizeComplete,
+ ///
+ /// Synchronizes clients when the active scene has changed
+ /// See:
+ ///
+ ActiveSceneChanged,
+ ///
+ /// Synchronizes clients when one or more NetworkObjects are migrated into a new scene
+ /// See:
+ ///
+ ObjectSceneChanged,
}
///
@@ -94,7 +104,7 @@ internal class SceneEventData : IDisposable
internal ForceNetworkSerializeByMemcpy SceneEventProgressId;
internal uint SceneEventId;
-
+ internal uint ActiveSceneHash;
internal uint SceneHash;
internal int SceneHandle;
@@ -139,6 +149,8 @@ internal class SceneEventData : IDisposable
internal Queue ScenesToSynchronize;
internal Queue SceneHandlesToSynchronize;
+ internal LoadSceneMode ClientSynchronizationMode;
+
///
/// Server Side:
@@ -315,6 +327,8 @@ internal bool IsSceneEventClientSide()
case SceneEventType.ReSynchronize:
case SceneEventType.LoadEventCompleted:
case SceneEventType.UnloadEventCompleted:
+ case SceneEventType.ActiveSceneChanged:
+ case SceneEventType.ObjectSceneChanged:
{
return true;
}
@@ -384,6 +398,18 @@ internal void Serialize(FastBufferWriter writer)
// Write the scene event type
writer.WriteValueSafe(SceneEventType);
+ if (SceneEventType == SceneEventType.ActiveSceneChanged)
+ {
+ writer.WriteValueSafe(ActiveSceneHash);
+ return;
+ }
+
+ if (SceneEventType == SceneEventType.ObjectSceneChanged)
+ {
+ SerializeObjectsMovedIntoNewScene(writer);
+ return;
+ }
+
// Write the scene loading mode
writer.WriteValueSafe((byte)LoadSceneMode);
@@ -392,6 +418,10 @@ internal void Serialize(FastBufferWriter writer)
{
writer.WriteValueSafe(SceneEventProgressId);
}
+ else
+ {
+ writer.WriteValueSafe(ClientSynchronizationMode);
+ }
// Write the scene index and handle
writer.WriteValueSafe(SceneHash);
@@ -401,6 +431,7 @@ internal void Serialize(FastBufferWriter writer)
{
case SceneEventType.Synchronize:
{
+ writer.WriteValueSafe(ActiveSceneHash);
WriteSceneSynchronizationData(writer);
break;
}
@@ -536,6 +567,18 @@ internal void SerializeScenePlacedObjects(FastBufferWriter writer)
internal void Deserialize(FastBufferReader reader)
{
reader.ReadValueSafe(out SceneEventType);
+ if (SceneEventType == SceneEventType.ActiveSceneChanged)
+ {
+ reader.ReadValueSafe(out ActiveSceneHash);
+ return;
+ }
+
+ if (SceneEventType == SceneEventType.ObjectSceneChanged)
+ {
+ DeserializeObjectsMovedIntoNewScene(reader);
+ return;
+ }
+
reader.ReadValueSafe(out byte loadSceneMode);
LoadSceneMode = (LoadSceneMode)loadSceneMode;
@@ -543,6 +586,10 @@ internal void Deserialize(FastBufferReader reader)
{
reader.ReadValueSafe(out SceneEventProgressId);
}
+ else
+ {
+ reader.ReadValueSafe(out ClientSynchronizationMode);
+ }
reader.ReadValueSafe(out SceneHash);
reader.ReadValueSafe(out SceneHandle);
@@ -551,6 +598,7 @@ internal void Deserialize(FastBufferReader reader)
{
case SceneEventType.Synchronize:
{
+ reader.ReadValueSafe(out ActiveSceneHash);
CopySceneSynchronizationData(reader);
break;
}
@@ -939,6 +987,63 @@ internal void ReadSceneEventProgressDone(FastBufferReader reader)
}
}
+ ///
+ /// Serialize scene handles and associated NetworkObjects that were migrated
+ /// into a new scene.
+ ///
+ private void SerializeObjectsMovedIntoNewScene(FastBufferWriter writer)
+ {
+ var sceneManager = m_NetworkManager.SceneManager;
+ // Write the number of scene handles
+ writer.WriteValueSafe(sceneManager.ObjectsMigratedIntoNewScene.Count);
+ foreach (var sceneHandleObjects in sceneManager.ObjectsMigratedIntoNewScene)
+ {
+ // Write the scene handle
+ writer.WriteValueSafe(sceneHandleObjects.Key);
+ // Write the number of NetworkObjectIds to expect
+ writer.WriteValueSafe(sceneHandleObjects.Value.Count);
+ foreach (var networkObject in sceneHandleObjects.Value)
+ {
+ writer.WriteValueSafe(networkObject.NetworkObjectId);
+ }
+ }
+ // Once we are done, clear the table
+ sceneManager.ObjectsMigratedIntoNewScene.Clear();
+ }
+
+ ///
+ /// Deserialize scene handles and associated NetworkObjects that need to
+ /// be migrated into a new scene.
+ ///
+ private void DeserializeObjectsMovedIntoNewScene(FastBufferReader reader)
+ {
+ var sceneManager = m_NetworkManager.SceneManager;
+ var spawnManager = m_NetworkManager.SpawnManager;
+ // Just always assure this has no entries
+ sceneManager.ObjectsMigratedIntoNewScene.Clear();
+ var numberOfScenes = 0;
+ var sceneHandle = 0;
+ var objectCount = 0;
+ var networkObjectId = (ulong)0;
+ reader.ReadValueSafe(out numberOfScenes);
+ for (int i = 0; i < numberOfScenes; i++)
+ {
+ reader.ReadValueSafe(out sceneHandle);
+ sceneManager.ObjectsMigratedIntoNewScene.Add(sceneHandle, new List());
+ reader.ReadValueSafe(out objectCount);
+ for (int j = 0; j < objectCount; j++)
+ {
+ reader.ReadValueSafe(out networkObjectId);
+ if (!spawnManager.SpawnedObjects.ContainsKey(networkObjectId))
+ {
+ throw new Exception($"[Object Scene Migration] Trying to synchronize NetworkObjectId ({networkObjectId}) but it no longer exists!");
+ }
+ sceneManager.ObjectsMigratedIntoNewScene[sceneHandle].Add(spawnManager.SpawnedObjects[networkObjectId]);
+ }
+ }
+ }
+
+
///
/// Used to release the pooled network buffer
///
diff --git a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs
index 131489a9ac..315bf4b868 100644
--- a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs
+++ b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs
@@ -405,6 +405,9 @@ internal NetworkObject CreateLocalNetworkObject(NetworkObject.SceneObject sceneO
if (networkObject != null)
{
+ networkObject.DestroyWithScene = sceneObject.DestroyWithScene;
+ networkObject.NetworkSceneHandle = sceneObject.NetworkSceneHandle;
+
// SPECIAL CASE FOR IN-SCENE PLACED: (only when the parent has a NetworkObject)
// This is a special case scenario where a late joining client has joined and loaded one or
// more scenes that contain nested in-scene placed NetworkObject children yet the server's
@@ -610,6 +613,12 @@ private void SpawnNetworkObjectLocallyCommon(NetworkObject networkObject, ulong
}
childObject.IsSceneObject = sceneObject;
}
+
+ // Only dynamically spawned NetworkObjects are allowed
+ if (!sceneObject)
+ {
+ networkObject.SubscribeToActiveSceneForSynch();
+ }
}
internal void SendSpawnCallForObject(ulong clientId, NetworkObject networkObject)
diff --git a/com.unity.netcode.gameobjects/TestHelpers/Runtime/IntegrationTestSceneHandler.cs b/com.unity.netcode.gameobjects/TestHelpers/Runtime/IntegrationTestSceneHandler.cs
index 80e09c88db..fdba24e61d 100644
--- a/com.unity.netcode.gameobjects/TestHelpers/Runtime/IntegrationTestSceneHandler.cs
+++ b/com.unity.netcode.gameobjects/TestHelpers/Runtime/IntegrationTestSceneHandler.cs
@@ -15,6 +15,16 @@ namespace Unity.Netcode.TestHelpers.Runtime
///
internal class IntegrationTestSceneHandler : ISceneManagerHandler, IDisposable
{
+ private Scene m_InvalidScene = new Scene();
+
+ internal struct SceneEntry
+ {
+ public bool IsAssigned;
+ public Scene Scene;
+ }
+
+ internal static Dictionary>> SceneNameToSceneHandles = new Dictionary>>();
+
// All IntegrationTestSceneHandler instances register their associated NetworkManager
internal static List NetworkManagers = new List();
@@ -267,8 +277,8 @@ private void Sever_SceneLoaded(Scene scene, LoadSceneMode arg1)
{
if (m_ServerSceneBeingLoaded == scene.name)
{
- ProcessInSceneObjects(scene, NetworkManager);
SceneManager.sceneLoaded -= Sever_SceneLoaded;
+ ProcessInSceneObjects(scene, NetworkManager);
}
}
@@ -330,6 +340,7 @@ internal Scene GetAndAddNewlyLoadedSceneByName(string sceneName)
{
continue;
}
+
if (networkManager.SceneManager.ScenesLoaded.ContainsKey(sceneLoaded.handle))
{
if (NetworkManager.LogLevel == LogLevel.Developer)
@@ -347,7 +358,12 @@ internal Scene GetAndAddNewlyLoadedSceneByName(string sceneName)
{
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;
}
}
@@ -365,6 +381,500 @@ private bool ExcludeSceneFromSynchronizationCheck(Scene scene)
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>());
+ }
+
+ if (!SceneNameToSceneHandles[networkManager].ContainsKey(scene.name))
+ {
+ SceneNameToSceneHandles[networkManager].Add(scene.name, new Dictionary());
+ }
+
+ 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();
+ var scenesAssigned = new List();
+ 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 scenesLoaded, NetworkManager networkManager)
+ {
+ if (!SceneNameToSceneHandles.ContainsKey(networkManager))
+ {
+ SceneNameToSceneHandles.Add(networkManager, new Dictionary>());
+ }
+
+ 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());
+ }
+
+ 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 m_ScenesToUnload = new Dictionary();
+
+ // When true, any remaining scenes loaded will be unloaded
+ // TODO: There needs to be a way to validate if a scene should be unloaded
+ // or not (i.e. local client-side UI loaded additively)
+ public bool AllowUnassignedScenesToBeUnloaded = false;
+
+ ///
+ /// Handles unloading any scenes that might remain on a client that
+ /// need to be unloaded.
+ ///
+ ///
+ public void UnloadUnassignedScenes(NetworkManager networkManager = null)
+ {
+ // Only if we are specifically testing this functionality
+ if (!AllowUnassignedScenesToBeUnloaded)
+ {
+ return;
+ }
+
+ SceneManager.sceneUnloaded += SceneManager_SceneUnloaded;
+ var relativeSceneNameToSceneHandles = SceneNameToSceneHandles[networkManager];
+
+ foreach (var sceneEntry in relativeSceneNameToSceneHandles)
+ {
+ var scenHandleEntries = relativeSceneNameToSceneHandles[sceneEntry.Key];
+ foreach (var sceneHandleEntry in scenHandleEntries)
+ {
+ if (!sceneHandleEntry.Value.IsAssigned)
+ {
+ m_ScenesToUnload.Add(sceneHandleEntry.Value.Scene, networkManager);
+ }
+ }
+ }
+ foreach (var sceneToUnload in m_ScenesToUnload)
+ {
+ SceneManager.UnloadSceneAsync(sceneToUnload.Key);
+ }
+ }
+
+ ///
+ /// Removes the scene entry from the scene name to scene handle table
+ ///
+ 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;
+ }
+ }
+ }
+ }
+ }
+
+ ///
+ /// Integration test version that handles migrating dynamically spawned NetworkObjects to
+ /// the DDOL when a scene is unloaded
+ ///
+ /// relative instance
+ /// scene being unloaded
+ 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.
+ var networkObjects = Object.FindObjectsOfType().Where((c) => c.IsSpawned);
+ 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())
+ {
+ 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);
+ }
+ }
+ }
+ }
+
+ ///
+ /// Sets the client synchronization mode which impacts whether both the server or client take into consideration scenes loaded before
+ /// starting the .
+ ///
+ ///
+ /// : Does not take preloaded scenes into consideration
+ /// : Does take preloaded scenes into consideration
+ ///
+ /// relative instance
+ /// or
+ public void SetClientSynchronizationMode(ref NetworkManager networkManager, LoadSceneMode mode)
+ {
+ var sceneManager = networkManager.SceneManager;
+
+
+ // 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>());
+ }
+
+ 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;
+ }
+
+ ///
+ /// During integration testing, if the server loads a scene then
+ /// we want to start tracking it.
+ ///
+ ///
+ 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;
+ }
+ }
+ }
+
+ ///
+ /// Handles determining if a client should attempt to load a scene during synchronization.
+ ///
+ /// name of the scene to be loaded
+ /// when in client synchronization mode single, this determines if the scene is the primary active scene
+ /// the current client synchronization mode
+ /// relative instance
+ ///
+ 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;
+ }
+
///
/// Constructor now must take NetworkManager
///
diff --git a/com.unity.netcode.gameobjects/TestHelpers/Runtime/NetcodeIntegrationTest.cs b/com.unity.netcode.gameobjects/TestHelpers/Runtime/NetcodeIntegrationTest.cs
index cce5d2f19b..f7c9d91325 100644
--- a/com.unity.netcode.gameobjects/TestHelpers/Runtime/NetcodeIntegrationTest.cs
+++ b/com.unity.netcode.gameobjects/TestHelpers/Runtime/NetcodeIntegrationTest.cs
@@ -692,6 +692,7 @@ protected virtual IEnumerator OnTearDown()
[UnityTearDown]
public IEnumerator TearDown()
{
+ IntegrationTestSceneHandler.SceneNameToSceneHandles.Clear();
VerboseDebug($"Entering {nameof(TearDown)}");
yield return OnTearDown();
diff --git a/testproject/Assets/Scripts/ConnectionModeScript.cs b/testproject/Assets/Scripts/ConnectionModeScript.cs
index ace7d3092d..6fc2750a1d 100644
--- a/testproject/Assets/Scripts/ConnectionModeScript.cs
+++ b/testproject/Assets/Scripts/ConnectionModeScript.cs
@@ -2,6 +2,7 @@
using UnityEngine;
using Unity.Netcode;
using Unity.Netcode.Transports.UTP;
+using UnityEngine.SceneManagement;
#if ENABLE_RELAY_SERVICE
using System;
using Unity.Services.Core;
@@ -25,6 +26,13 @@ public class ConnectionModeScript : MonoBehaviour
[SerializeField]
private int m_MaxConnections = 10;
+ [SerializeField]
+ private LoadSceneMode m_ClientSynchronizationMode;
+
+ [SerializeField]
+ private GameObject m_DisconnectClientButton;
+
+
private CommandLineProcessor m_CommandLineProcessor;
[HideInInspector]
@@ -95,11 +103,21 @@ private void Start()
}
else
{
-
m_JoinCodeInput.SetActive(false);
m_AuthenticationButtons.SetActive(false);
m_ConnectionModeButtons.SetActive(NetworkManager.Singleton && !NetworkManager.Singleton.IsListening);
}
+ if (m_DisconnectClientButton != null)
+ {
+ if (!NetworkManager.Singleton.IsListening)
+ {
+ m_DisconnectClientButton.SetActive(false);
+ }
+ else
+ {
+ m_DisconnectClientButton.SetActive(!NetworkManager.Singleton.IsServer);
+ }
+ }
}
}
@@ -134,8 +152,9 @@ public void OnStartServerButton()
private void StartServer()
{
NetworkManager.Singleton.StartServer();
+ NetworkManager.Singleton.SceneManager.SetClientSynchronizationMode(m_ClientSynchronizationMode);
OnNotifyConnectionEventServer?.Invoke();
- m_ConnectionModeButtons.SetActive(false);
+ m_ConnectionModeButtons?.SetActive(false);
}
@@ -193,6 +212,7 @@ public void OnStartHostButton()
private void StartHost()
{
NetworkManager.Singleton.StartHost();
+ NetworkManager.Singleton.SceneManager.SetClientSynchronizationMode(m_ClientSynchronizationMode);
OnNotifyConnectionEventHost?.Invoke();
m_ConnectionModeButtons.SetActive(false);
}
@@ -220,6 +240,19 @@ private void StartClient()
NetworkManager.Singleton.StartClient();
OnNotifyConnectionEventClient?.Invoke();
m_ConnectionModeButtons.SetActive(false);
+ if (m_DisconnectClientButton != null)
+ {
+ m_DisconnectClientButton.SetActive(true);
+ }
+ }
+
+ public void DisconnectClient()
+ {
+ if (NetworkManager.Singleton.IsListening && !NetworkManager.Singleton.IsServer)
+ {
+ NetworkManager.Singleton.Shutdown();
+ m_ConnectionModeButtons.SetActive(true);
+ }
}
diff --git a/testproject/Assets/Tests/Manual/IntegrationTestScenes/EmptyScene1.unity b/testproject/Assets/Tests/Manual/IntegrationTestScenes/EmptyScene1.unity
new file mode 100644
index 0000000000..fe1ff710a2
--- /dev/null
+++ b/testproject/Assets/Tests/Manual/IntegrationTestScenes/EmptyScene1.unity
@@ -0,0 +1,125 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!29 &1
+OcclusionCullingSettings:
+ m_ObjectHideFlags: 0
+ serializedVersion: 2
+ m_OcclusionBakeSettings:
+ smallestOccluder: 5
+ smallestHole: 0.25
+ backfaceThreshold: 100
+ m_SceneGUID: 00000000000000000000000000000000
+ m_OcclusionCullingData: {fileID: 0}
+--- !u!104 &2
+RenderSettings:
+ m_ObjectHideFlags: 0
+ serializedVersion: 9
+ m_Fog: 0
+ m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1}
+ m_FogMode: 3
+ m_FogDensity: 0.01
+ m_LinearFogStart: 0
+ m_LinearFogEnd: 300
+ m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1}
+ m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1}
+ m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1}
+ m_AmbientIntensity: 1
+ m_AmbientMode: 0
+ m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1}
+ m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0}
+ m_HaloStrength: 0.5
+ m_FlareStrength: 1
+ m_FlareFadeSpeed: 3
+ m_HaloTexture: {fileID: 0}
+ m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0}
+ m_DefaultReflectionMode: 0
+ m_DefaultReflectionResolution: 128
+ m_ReflectionBounces: 1
+ m_ReflectionIntensity: 1
+ m_CustomReflection: {fileID: 0}
+ m_Sun: {fileID: 0}
+ m_IndirectSpecularColor: {r: 0.37311953, g: 0.38074014, b: 0.3587274, a: 1}
+ m_UseRadianceAmbientProbe: 0
+--- !u!157 &3
+LightmapSettings:
+ m_ObjectHideFlags: 0
+ serializedVersion: 12
+ m_GIWorkflowMode: 1
+ m_GISettings:
+ serializedVersion: 2
+ m_BounceScale: 1
+ m_IndirectOutputScale: 1
+ m_AlbedoBoost: 1
+ m_EnvironmentLightingMode: 0
+ m_EnableBakedLightmaps: 1
+ m_EnableRealtimeLightmaps: 0
+ m_LightmapEditorSettings:
+ serializedVersion: 12
+ m_Resolution: 2
+ m_BakeResolution: 40
+ m_AtlasSize: 1024
+ m_AO: 0
+ m_AOMaxDistance: 1
+ m_CompAOExponent: 1
+ m_CompAOExponentDirect: 0
+ m_ExtractAmbientOcclusion: 0
+ m_Padding: 2
+ m_LightmapParameters: {fileID: 0}
+ m_LightmapsBakeMode: 1
+ m_TextureCompression: 1
+ m_FinalGather: 0
+ m_FinalGatherFiltering: 1
+ m_FinalGatherRayCount: 256
+ m_ReflectionCompression: 2
+ m_MixedBakeMode: 2
+ m_BakeBackend: 1
+ m_PVRSampling: 1
+ m_PVRDirectSampleCount: 32
+ m_PVRSampleCount: 512
+ m_PVRBounces: 2
+ m_PVREnvironmentSampleCount: 256
+ m_PVREnvironmentReferencePointCount: 2048
+ m_PVRFilteringMode: 1
+ m_PVRDenoiserTypeDirect: 1
+ m_PVRDenoiserTypeIndirect: 1
+ m_PVRDenoiserTypeAO: 1
+ m_PVRFilterTypeDirect: 0
+ m_PVRFilterTypeIndirect: 0
+ m_PVRFilterTypeAO: 0
+ m_PVREnvironmentMIS: 1
+ m_PVRCulling: 1
+ m_PVRFilteringGaussRadiusDirect: 1
+ m_PVRFilteringGaussRadiusIndirect: 5
+ m_PVRFilteringGaussRadiusAO: 2
+ m_PVRFilteringAtrousPositionSigmaDirect: 0.5
+ m_PVRFilteringAtrousPositionSigmaIndirect: 2
+ m_PVRFilteringAtrousPositionSigmaAO: 1
+ m_ExportTrainingData: 0
+ m_TrainingDataDestination: TrainingData
+ m_LightProbeSampleCountMultiplier: 4
+ m_LightingDataAsset: {fileID: 0}
+ m_LightingSettings: {fileID: 0}
+--- !u!196 &4
+NavMeshSettings:
+ serializedVersion: 2
+ m_ObjectHideFlags: 0
+ m_BuildSettings:
+ serializedVersion: 2
+ agentTypeID: 0
+ agentRadius: 0.5
+ agentHeight: 2
+ agentSlope: 45
+ agentClimb: 0.4
+ ledgeDropHeight: 0
+ maxJumpAcrossDistance: 0
+ minRegionArea: 2
+ manualCellSize: 0
+ cellSize: 0.16666667
+ manualTileSize: 0
+ tileSize: 256
+ accuratePlacement: 0
+ maxJobWorkers: 0
+ preserveTilesOutsideBounds: 0
+ debug:
+ m_Flags: 0
+ m_NavMeshData: {fileID: 0}
diff --git a/testproject/Assets/Tests/Manual/IntegrationTestScenes/EmptyScene1.unity.meta b/testproject/Assets/Tests/Manual/IntegrationTestScenes/EmptyScene1.unity.meta
new file mode 100644
index 0000000000..696fa23b0f
--- /dev/null
+++ b/testproject/Assets/Tests/Manual/IntegrationTestScenes/EmptyScene1.unity.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 057ba2cc37faa0b43aa7051d9f555caa
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/testproject/Assets/Tests/Manual/IntegrationTestScenes/EmptyScene2.unity b/testproject/Assets/Tests/Manual/IntegrationTestScenes/EmptyScene2.unity
new file mode 100644
index 0000000000..fe1ff710a2
--- /dev/null
+++ b/testproject/Assets/Tests/Manual/IntegrationTestScenes/EmptyScene2.unity
@@ -0,0 +1,125 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!29 &1
+OcclusionCullingSettings:
+ m_ObjectHideFlags: 0
+ serializedVersion: 2
+ m_OcclusionBakeSettings:
+ smallestOccluder: 5
+ smallestHole: 0.25
+ backfaceThreshold: 100
+ m_SceneGUID: 00000000000000000000000000000000
+ m_OcclusionCullingData: {fileID: 0}
+--- !u!104 &2
+RenderSettings:
+ m_ObjectHideFlags: 0
+ serializedVersion: 9
+ m_Fog: 0
+ m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1}
+ m_FogMode: 3
+ m_FogDensity: 0.01
+ m_LinearFogStart: 0
+ m_LinearFogEnd: 300
+ m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1}
+ m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1}
+ m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1}
+ m_AmbientIntensity: 1
+ m_AmbientMode: 0
+ m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1}
+ m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0}
+ m_HaloStrength: 0.5
+ m_FlareStrength: 1
+ m_FlareFadeSpeed: 3
+ m_HaloTexture: {fileID: 0}
+ m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0}
+ m_DefaultReflectionMode: 0
+ m_DefaultReflectionResolution: 128
+ m_ReflectionBounces: 1
+ m_ReflectionIntensity: 1
+ m_CustomReflection: {fileID: 0}
+ m_Sun: {fileID: 0}
+ m_IndirectSpecularColor: {r: 0.37311953, g: 0.38074014, b: 0.3587274, a: 1}
+ m_UseRadianceAmbientProbe: 0
+--- !u!157 &3
+LightmapSettings:
+ m_ObjectHideFlags: 0
+ serializedVersion: 12
+ m_GIWorkflowMode: 1
+ m_GISettings:
+ serializedVersion: 2
+ m_BounceScale: 1
+ m_IndirectOutputScale: 1
+ m_AlbedoBoost: 1
+ m_EnvironmentLightingMode: 0
+ m_EnableBakedLightmaps: 1
+ m_EnableRealtimeLightmaps: 0
+ m_LightmapEditorSettings:
+ serializedVersion: 12
+ m_Resolution: 2
+ m_BakeResolution: 40
+ m_AtlasSize: 1024
+ m_AO: 0
+ m_AOMaxDistance: 1
+ m_CompAOExponent: 1
+ m_CompAOExponentDirect: 0
+ m_ExtractAmbientOcclusion: 0
+ m_Padding: 2
+ m_LightmapParameters: {fileID: 0}
+ m_LightmapsBakeMode: 1
+ m_TextureCompression: 1
+ m_FinalGather: 0
+ m_FinalGatherFiltering: 1
+ m_FinalGatherRayCount: 256
+ m_ReflectionCompression: 2
+ m_MixedBakeMode: 2
+ m_BakeBackend: 1
+ m_PVRSampling: 1
+ m_PVRDirectSampleCount: 32
+ m_PVRSampleCount: 512
+ m_PVRBounces: 2
+ m_PVREnvironmentSampleCount: 256
+ m_PVREnvironmentReferencePointCount: 2048
+ m_PVRFilteringMode: 1
+ m_PVRDenoiserTypeDirect: 1
+ m_PVRDenoiserTypeIndirect: 1
+ m_PVRDenoiserTypeAO: 1
+ m_PVRFilterTypeDirect: 0
+ m_PVRFilterTypeIndirect: 0
+ m_PVRFilterTypeAO: 0
+ m_PVREnvironmentMIS: 1
+ m_PVRCulling: 1
+ m_PVRFilteringGaussRadiusDirect: 1
+ m_PVRFilteringGaussRadiusIndirect: 5
+ m_PVRFilteringGaussRadiusAO: 2
+ m_PVRFilteringAtrousPositionSigmaDirect: 0.5
+ m_PVRFilteringAtrousPositionSigmaIndirect: 2
+ m_PVRFilteringAtrousPositionSigmaAO: 1
+ m_ExportTrainingData: 0
+ m_TrainingDataDestination: TrainingData
+ m_LightProbeSampleCountMultiplier: 4
+ m_LightingDataAsset: {fileID: 0}
+ m_LightingSettings: {fileID: 0}
+--- !u!196 &4
+NavMeshSettings:
+ serializedVersion: 2
+ m_ObjectHideFlags: 0
+ m_BuildSettings:
+ serializedVersion: 2
+ agentTypeID: 0
+ agentRadius: 0.5
+ agentHeight: 2
+ agentSlope: 45
+ agentClimb: 0.4
+ ledgeDropHeight: 0
+ maxJumpAcrossDistance: 0
+ minRegionArea: 2
+ manualCellSize: 0
+ cellSize: 0.16666667
+ manualTileSize: 0
+ tileSize: 256
+ accuratePlacement: 0
+ maxJobWorkers: 0
+ preserveTilesOutsideBounds: 0
+ debug:
+ m_Flags: 0
+ m_NavMeshData: {fileID: 0}
diff --git a/testproject/Assets/Tests/Manual/IntegrationTestScenes/EmptyScene2.unity.meta b/testproject/Assets/Tests/Manual/IntegrationTestScenes/EmptyScene2.unity.meta
new file mode 100644
index 0000000000..19ea576e01
--- /dev/null
+++ b/testproject/Assets/Tests/Manual/IntegrationTestScenes/EmptyScene2.unity.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 17b92153f7381d34fa48c4d5c0393d13
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/testproject/Assets/Tests/Manual/IntegrationTestScenes/EmptyScene3.unity b/testproject/Assets/Tests/Manual/IntegrationTestScenes/EmptyScene3.unity
new file mode 100644
index 0000000000..fe1ff710a2
--- /dev/null
+++ b/testproject/Assets/Tests/Manual/IntegrationTestScenes/EmptyScene3.unity
@@ -0,0 +1,125 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!29 &1
+OcclusionCullingSettings:
+ m_ObjectHideFlags: 0
+ serializedVersion: 2
+ m_OcclusionBakeSettings:
+ smallestOccluder: 5
+ smallestHole: 0.25
+ backfaceThreshold: 100
+ m_SceneGUID: 00000000000000000000000000000000
+ m_OcclusionCullingData: {fileID: 0}
+--- !u!104 &2
+RenderSettings:
+ m_ObjectHideFlags: 0
+ serializedVersion: 9
+ m_Fog: 0
+ m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1}
+ m_FogMode: 3
+ m_FogDensity: 0.01
+ m_LinearFogStart: 0
+ m_LinearFogEnd: 300
+ m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1}
+ m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1}
+ m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1}
+ m_AmbientIntensity: 1
+ m_AmbientMode: 0
+ m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1}
+ m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0}
+ m_HaloStrength: 0.5
+ m_FlareStrength: 1
+ m_FlareFadeSpeed: 3
+ m_HaloTexture: {fileID: 0}
+ m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0}
+ m_DefaultReflectionMode: 0
+ m_DefaultReflectionResolution: 128
+ m_ReflectionBounces: 1
+ m_ReflectionIntensity: 1
+ m_CustomReflection: {fileID: 0}
+ m_Sun: {fileID: 0}
+ m_IndirectSpecularColor: {r: 0.37311953, g: 0.38074014, b: 0.3587274, a: 1}
+ m_UseRadianceAmbientProbe: 0
+--- !u!157 &3
+LightmapSettings:
+ m_ObjectHideFlags: 0
+ serializedVersion: 12
+ m_GIWorkflowMode: 1
+ m_GISettings:
+ serializedVersion: 2
+ m_BounceScale: 1
+ m_IndirectOutputScale: 1
+ m_AlbedoBoost: 1
+ m_EnvironmentLightingMode: 0
+ m_EnableBakedLightmaps: 1
+ m_EnableRealtimeLightmaps: 0
+ m_LightmapEditorSettings:
+ serializedVersion: 12
+ m_Resolution: 2
+ m_BakeResolution: 40
+ m_AtlasSize: 1024
+ m_AO: 0
+ m_AOMaxDistance: 1
+ m_CompAOExponent: 1
+ m_CompAOExponentDirect: 0
+ m_ExtractAmbientOcclusion: 0
+ m_Padding: 2
+ m_LightmapParameters: {fileID: 0}
+ m_LightmapsBakeMode: 1
+ m_TextureCompression: 1
+ m_FinalGather: 0
+ m_FinalGatherFiltering: 1
+ m_FinalGatherRayCount: 256
+ m_ReflectionCompression: 2
+ m_MixedBakeMode: 2
+ m_BakeBackend: 1
+ m_PVRSampling: 1
+ m_PVRDirectSampleCount: 32
+ m_PVRSampleCount: 512
+ m_PVRBounces: 2
+ m_PVREnvironmentSampleCount: 256
+ m_PVREnvironmentReferencePointCount: 2048
+ m_PVRFilteringMode: 1
+ m_PVRDenoiserTypeDirect: 1
+ m_PVRDenoiserTypeIndirect: 1
+ m_PVRDenoiserTypeAO: 1
+ m_PVRFilterTypeDirect: 0
+ m_PVRFilterTypeIndirect: 0
+ m_PVRFilterTypeAO: 0
+ m_PVREnvironmentMIS: 1
+ m_PVRCulling: 1
+ m_PVRFilteringGaussRadiusDirect: 1
+ m_PVRFilteringGaussRadiusIndirect: 5
+ m_PVRFilteringGaussRadiusAO: 2
+ m_PVRFilteringAtrousPositionSigmaDirect: 0.5
+ m_PVRFilteringAtrousPositionSigmaIndirect: 2
+ m_PVRFilteringAtrousPositionSigmaAO: 1
+ m_ExportTrainingData: 0
+ m_TrainingDataDestination: TrainingData
+ m_LightProbeSampleCountMultiplier: 4
+ m_LightingDataAsset: {fileID: 0}
+ m_LightingSettings: {fileID: 0}
+--- !u!196 &4
+NavMeshSettings:
+ serializedVersion: 2
+ m_ObjectHideFlags: 0
+ m_BuildSettings:
+ serializedVersion: 2
+ agentTypeID: 0
+ agentRadius: 0.5
+ agentHeight: 2
+ agentSlope: 45
+ agentClimb: 0.4
+ ledgeDropHeight: 0
+ maxJumpAcrossDistance: 0
+ minRegionArea: 2
+ manualCellSize: 0
+ cellSize: 0.16666667
+ manualTileSize: 0
+ tileSize: 256
+ accuratePlacement: 0
+ maxJobWorkers: 0
+ preserveTilesOutsideBounds: 0
+ debug:
+ m_Flags: 0
+ m_NavMeshData: {fileID: 0}
diff --git a/testproject/Assets/Tests/Manual/IntegrationTestScenes/EmptyScene3.unity.meta b/testproject/Assets/Tests/Manual/IntegrationTestScenes/EmptyScene3.unity.meta
new file mode 100644
index 0000000000..46669dd13d
--- /dev/null
+++ b/testproject/Assets/Tests/Manual/IntegrationTestScenes/EmptyScene3.unity.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: abd4c8b51c445d54faa16c67ac973f1b
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/testproject/Assets/Tests/Manual/SceneTransitioning/SceneTransitioningTest.unity b/testproject/Assets/Tests/Manual/SceneTransitioning/SceneTransitioningTest.unity
index b253134e4e..eca5859239 100644
--- a/testproject/Assets/Tests/Manual/SceneTransitioning/SceneTransitioningTest.unity
+++ b/testproject/Assets/Tests/Manual/SceneTransitioning/SceneTransitioningTest.unity
@@ -178,7 +178,7 @@ MonoBehaviour:
m_SceneToSwitchTo: SecondSceneToLoad
m_EnableAutoSwitch: 1
m_AutoSwitchTimeOut: 300
- DisconnectClientUponLoadScene: 1
+ DisconnectClientUponLoadScene: 0
--- !u!114 &34066668
MonoBehaviour:
m_ObjectHideFlags: 0
@@ -193,6 +193,8 @@ MonoBehaviour:
m_EditorClassIdentifier:
GlobalObjectIdHash: 3270232563
AlwaysReplicateAsRoot: 0
+ ActiveSceneSynchronization: 0
+ SceneMigrationSynchronization: 1
DontDestroyWithOwner: 0
AutoObjectParentSync: 1
--- !u!1 &37242881
@@ -984,6 +986,8 @@ MonoBehaviour:
m_EditorClassIdentifier:
GlobalObjectIdHash: 2831848344
AlwaysReplicateAsRoot: 0
+ ActiveSceneSynchronization: 0
+ SceneMigrationSynchronization: 1
DontDestroyWithOwner: 0
AutoObjectParentSync: 1
--- !u!4 &797949416
@@ -1102,7 +1106,24 @@ MonoBehaviour:
NetworkTransport: {fileID: 1024114719}
PlayerPrefab: {fileID: 4079352819444256614, guid: c16f03336b6104576a565ef79ad643c0,
type: 3}
- NetworkPrefabs:
+ Prefabs:
+ NetworkPrefabsLists: []
+ TickRate: 30
+ ClientConnectionBufferTimeout: 10
+ ConnectionApproval: 0
+ ConnectionData:
+ EnableTimeResync: 0
+ TimeResyncInterval: 30
+ EnsureNetworkVariableLengthSafety: 0
+ EnableSceneManagement: 1
+ ForceSamePrefabs: 1
+ RecycleNetworkIds: 1
+ NetworkIdRecycleDelay: 120
+ RpcHashSize: 0
+ LoadSceneTimeOut: 120
+ SpawnTimeout: 1
+ EnableNetworkLogs: 1
+ OldPrefabList:
- Override: 1
Prefab: {fileID: 771575417923360811, guid: c0a45bdb516f341498d933b7a7ed4fc1,
type: 3}
@@ -1129,21 +1150,6 @@ MonoBehaviour:
SourcePrefabToOverride: {fileID: 0}
SourceHashToOverride: 0
OverridingTargetPrefab: {fileID: 0}
- TickRate: 30
- ClientConnectionBufferTimeout: 10
- ConnectionApproval: 0
- ConnectionData:
- EnableTimeResync: 0
- TimeResyncInterval: 30
- EnsureNetworkVariableLengthSafety: 0
- EnableSceneManagement: 1
- ForceSamePrefabs: 1
- RecycleNetworkIds: 1
- NetworkIdRecycleDelay: 120
- RpcHashSize: 0
- LoadSceneTimeOut: 120
- SpawnTimeout: 1
- EnableNetworkLogs: 1
--- !u!114 &1024114719
MonoBehaviour:
m_ObjectHideFlags: 0
@@ -1277,6 +1283,8 @@ MonoBehaviour:
m_EditorClassIdentifier:
GlobalObjectIdHash: 2782806529
AlwaysReplicateAsRoot: 0
+ ActiveSceneSynchronization: 0
+ SceneMigrationSynchronization: 1
DontDestroyWithOwner: 0
AutoObjectParentSync: 1
--- !u!1 &1332123091
@@ -1979,6 +1987,7 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 4f231c4fb786f3946a6b90b886c48677, type: 3}
m_Name:
m_EditorClassIdentifier:
+ m_SendPointerHoverToParent: 1
m_HorizontalAxis: Horizontal
m_VerticalAxis: Vertical
m_SubmitButton: Submit
@@ -2679,7 +2688,6 @@ MonoBehaviour:
m_EditorClassIdentifier:
m_ClientServerToggle: {fileID: 1588117327}
m_TrackSceneEvents: 1
- m_LogSceneEventsToConsole: 1
--- !u!114 &2107482023
MonoBehaviour:
m_ObjectHideFlags: 0
@@ -2694,6 +2702,8 @@ MonoBehaviour:
m_EditorClassIdentifier:
GlobalObjectIdHash: 2267100048
AlwaysReplicateAsRoot: 0
+ ActiveSceneSynchronization: 0
+ SceneMigrationSynchronization: 1
DontDestroyWithOwner: 0
AutoObjectParentSync: 1
--- !u!1001 &2848221156282925290
diff --git a/testproject/Assets/Tests/Manual/SceneTransitioning/SecondSceneToLoad.unity b/testproject/Assets/Tests/Manual/SceneTransitioning/SecondSceneToLoad.unity
index a3f679d703..cc072534b8 100644
--- a/testproject/Assets/Tests/Manual/SceneTransitioning/SecondSceneToLoad.unity
+++ b/testproject/Assets/Tests/Manual/SceneTransitioning/SecondSceneToLoad.unity
@@ -1031,6 +1031,8 @@ MonoBehaviour:
m_EditorClassIdentifier:
GlobalObjectIdHash: 2807568818
AlwaysReplicateAsRoot: 0
+ ActiveSceneSynchronization: 0
+ SceneMigrationSynchronization: 1
DontDestroyWithOwner: 0
AutoObjectParentSync: 1
--- !u!1 &1332123091
@@ -1465,6 +1467,7 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 4f231c4fb786f3946a6b90b886c48677, type: 3}
m_Name:
m_EditorClassIdentifier:
+ m_SendPointerHoverToParent: 1
m_HorizontalAxis: Horizontal
m_VerticalAxis: Vertical
m_SubmitButton: Submit
@@ -1950,6 +1953,7 @@ GameObject:
m_Component:
- component: {fileID: 2051885573}
- component: {fileID: 2051885574}
+ - component: {fileID: 2051885575}
m_Layer: 5
m_Name: SwitchSceneParent
m_TagString: Untagged
@@ -1995,6 +1999,24 @@ MonoBehaviour:
m_EnableAutoSwitch: 1
m_AutoSwitchTimeOut: 300
DisconnectClientUponLoadScene: 0
+--- !u!114 &2051885575
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 2051885572}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: d5a57f767e5e46a458fc5d3c628d0cbb, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
+ GlobalObjectIdHash: 3071296511
+ AlwaysReplicateAsRoot: 0
+ ActiveSceneSynchronization: 0
+ SceneMigrationSynchronization: 1
+ DontDestroyWithOwner: 0
+ AutoObjectParentSync: 1
--- !u!1 &2058276875
GameObject:
m_ObjectHideFlags: 0
@@ -2146,7 +2168,6 @@ MonoBehaviour:
m_EditorClassIdentifier:
m_ClientServerToggle: {fileID: 2126410740}
m_TrackSceneEvents: 1
- m_LogSceneEventsToConsole: 0
--- !u!114 &2107482023
MonoBehaviour:
m_ObjectHideFlags: 0
@@ -2161,6 +2182,8 @@ MonoBehaviour:
m_EditorClassIdentifier:
GlobalObjectIdHash: 4086995543
AlwaysReplicateAsRoot: 0
+ ActiveSceneSynchronization: 0
+ SceneMigrationSynchronization: 1
DontDestroyWithOwner: 0
AutoObjectParentSync: 1
--- !u!1 &2126410740
diff --git a/testproject/Assets/Tests/Manual/SceneTransitioningAdditive/SceneTransitioningBase1.unity b/testproject/Assets/Tests/Manual/SceneTransitioningAdditive/SceneTransitioningBase1.unity
index de7c662a05..39f2245def 100644
--- a/testproject/Assets/Tests/Manual/SceneTransitioningAdditive/SceneTransitioningBase1.unity
+++ b/testproject/Assets/Tests/Manual/SceneTransitioningAdditive/SceneTransitioningBase1.unity
@@ -186,6 +186,7 @@ GameObject:
m_Component:
- component: {fileID: 34066665}
- component: {fileID: 34066667}
+ - component: {fileID: 34066668}
m_Layer: 5
m_Name: SwitchSceneParent
m_TagString: Untagged
@@ -231,6 +232,22 @@ MonoBehaviour:
m_EnableAutoSwitch: 0
m_AutoSwitchTimeOut: 60
DisconnectClientUponLoadScene: 0
+--- !u!114 &34066668
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 34066664}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: d5a57f767e5e46a458fc5d3c628d0cbb, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
+ GlobalObjectIdHash: 412081913
+ AlwaysReplicateAsRoot: 0
+ DontDestroyWithOwner: 0
+ AutoObjectParentSync: 1
--- !u!1 &37242881
GameObject:
m_ObjectHideFlags: 0
@@ -638,6 +655,43 @@ MonoBehaviour:
m_ActivateOnLoad: 0
m_SceneToLoad: AdditiveSceneMultiInstance
m_SceneAsset: {fileID: 102900000, guid: 0ae94f636016d3b40bfbecad57d99553, type: 3}
+--- !u!1 &97319464
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 97319465}
+ m_Layer: 5
+ m_Name: DisconnectClientRoot
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!224 &97319465
+RectTransform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 97319464}
+ m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children:
+ - {fileID: 1259474592}
+ m_Father: {fileID: 167044834}
+ m_RootOrder: 1
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+ m_AnchorMin: {x: 0, y: 0}
+ m_AnchorMax: {x: 0, y: 0}
+ m_AnchoredPosition: {x: 300, y: 51}
+ m_SizeDelta: {x: 100, y: 100}
+ m_Pivot: {x: 0.5, y: 0.5}
--- !u!1 &125866602
GameObject:
m_ObjectHideFlags: 0
@@ -843,12 +897,12 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 0cd44c1031e13a943bb63640046fad76, type: 3}
m_Name:
m_EditorClassIdentifier:
- m_UiScaleMode: 0
+ m_UiScaleMode: 1
m_ReferencePixelsPerUnit: 100
m_ScaleFactor: 1
- m_ReferenceResolution: {x: 800, y: 600}
+ m_ReferenceResolution: {x: 1024, y: 768}
m_ScreenMatchMode: 0
- m_MatchWidthOrHeight: 0
+ m_MatchWidthOrHeight: 0.5
m_PhysicalUnit: 3
m_FallbackScreenDPI: 96
m_DefaultSpriteDPI: 96
@@ -888,6 +942,7 @@ RectTransform:
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 1865409449}
+ - {fileID: 97319465}
m_Father: {fileID: 0}
m_RootOrder: 3
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
@@ -1511,6 +1566,86 @@ CanvasRenderer:
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 432733928}
m_CullTransparentMesh: 1
+--- !u!1 &462643752
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 462643753}
+ - component: {fileID: 462643755}
+ - component: {fileID: 462643754}
+ m_Layer: 5
+ m_Name: Text
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!224 &462643753
+RectTransform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 462643752}
+ m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 1259474592}
+ m_RootOrder: 0
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+ m_AnchorMin: {x: 0, y: 0}
+ m_AnchorMax: {x: 1, y: 1}
+ m_AnchoredPosition: {x: 0, y: 0}
+ m_SizeDelta: {x: 0, y: 0}
+ m_Pivot: {x: 0.5, y: 0.5}
+--- !u!114 &462643754
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 462643752}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
+ m_Material: {fileID: 0}
+ m_Color: {r: 1, g: 0.5058824, b: 0.003921569, a: 1}
+ m_RaycastTarget: 1
+ m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
+ m_Maskable: 1
+ m_OnCullStateChanged:
+ m_PersistentCalls:
+ m_Calls: []
+ m_FontData:
+ m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0}
+ m_FontSize: 14
+ m_FontStyle: 0
+ m_BestFit: 0
+ m_MinSize: 10
+ m_MaxSize: 40
+ m_Alignment: 4
+ m_AlignByGeometry: 0
+ m_RichText: 1
+ m_HorizontalOverflow: 0
+ m_VerticalOverflow: 0
+ m_LineSpacing: 1
+ m_Text: Disconnect Client
+--- !u!222 &462643755
+CanvasRenderer:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 462643752}
+ m_CullTransparentMesh: 1
--- !u!1 &562991978
GameObject:
m_ObjectHideFlags: 0
@@ -2349,7 +2484,24 @@ MonoBehaviour:
NetworkTransport: {fileID: 1024114723}
PlayerPrefab: {fileID: 4079352819444256614, guid: c16f03336b6104576a565ef79ad643c0,
type: 3}
- NetworkPrefabs:
+ Prefabs:
+ NetworkPrefabsLists: []
+ TickRate: 32
+ ClientConnectionBufferTimeout: 10
+ ConnectionApproval: 0
+ ConnectionData:
+ EnableTimeResync: 0
+ TimeResyncInterval: 30
+ EnsureNetworkVariableLengthSafety: 0
+ EnableSceneManagement: 1
+ ForceSamePrefabs: 1
+ RecycleNetworkIds: 0
+ NetworkIdRecycleDelay: 120
+ RpcHashSize: 0
+ LoadSceneTimeOut: 120
+ SpawnTimeout: 1
+ EnableNetworkLogs: 1
+ OldPrefabList:
- Override: 0
Prefab: {fileID: 771575417923360811, guid: c0a45bdb516f341498d933b7a7ed4fc1,
type: 3}
@@ -2400,21 +2552,6 @@ MonoBehaviour:
SourcePrefabToOverride: {fileID: 0}
SourceHashToOverride: 0
OverridingTargetPrefab: {fileID: 0}
- TickRate: 32
- ClientConnectionBufferTimeout: 10
- ConnectionApproval: 0
- ConnectionData:
- EnableTimeResync: 0
- TimeResyncInterval: 30
- EnsureNetworkVariableLengthSafety: 0
- EnableSceneManagement: 1
- ForceSamePrefabs: 1
- RecycleNetworkIds: 0
- NetworkIdRecycleDelay: 120
- RpcHashSize: 0
- LoadSceneTimeOut: 120
- SpawnTimeout: 1
- EnableNetworkLogs: 1
--- !u!4 &1024114720
Transform:
m_ObjectHideFlags: 0
@@ -2691,6 +2828,140 @@ CanvasRenderer:
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1210784441}
m_CullTransparentMesh: 1
+--- !u!1 &1259474591
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 1259474592}
+ - component: {fileID: 1259474595}
+ - component: {fileID: 1259474594}
+ - component: {fileID: 1259474593}
+ m_Layer: 5
+ m_Name: DisconnectClient
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!224 &1259474592
+RectTransform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1259474591}
+ m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children:
+ - {fileID: 462643753}
+ m_Father: {fileID: 97319465}
+ m_RootOrder: 0
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+ m_AnchorMin: {x: 0.5, y: 0.5}
+ m_AnchorMax: {x: 0.5, y: 0.5}
+ m_AnchoredPosition: {x: 0, y: 0}
+ m_SizeDelta: {x: 160, y: 30}
+ m_Pivot: {x: 0.5, y: 0.5}
+--- !u!114 &1259474593
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1259474591}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
+ m_Navigation:
+ m_Mode: 3
+ m_WrapAround: 0
+ m_SelectOnUp: {fileID: 0}
+ m_SelectOnDown: {fileID: 0}
+ m_SelectOnLeft: {fileID: 0}
+ m_SelectOnRight: {fileID: 0}
+ m_Transition: 1
+ m_Colors:
+ m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
+ m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
+ m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1}
+ m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
+ m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608}
+ m_ColorMultiplier: 1
+ m_FadeDuration: 0.1
+ m_SpriteState:
+ m_HighlightedSprite: {fileID: 0}
+ m_PressedSprite: {fileID: 0}
+ m_SelectedSprite: {fileID: 0}
+ m_DisabledSprite: {fileID: 0}
+ m_AnimationTriggers:
+ m_NormalTrigger: Normal
+ m_HighlightedTrigger: Highlighted
+ m_PressedTrigger: Pressed
+ m_SelectedTrigger: Selected
+ m_DisabledTrigger: Disabled
+ m_Interactable: 1
+ m_TargetGraphic: {fileID: 1259474594}
+ m_OnClick:
+ m_PersistentCalls:
+ m_Calls:
+ - m_Target: {fileID: 1865409450}
+ m_TargetAssemblyTypeName: ConnectionModeScript, TestProject
+ m_MethodName: DisconnectClient
+ m_Mode: 1
+ m_Arguments:
+ m_ObjectArgument: {fileID: 0}
+ m_ObjectArgumentAssemblyTypeName: UnityEngine.Object, UnityEngine
+ m_IntArgument: 0
+ m_FloatArgument: 0
+ m_StringArgument:
+ m_BoolArgument: 0
+ m_CallState: 2
+--- !u!114 &1259474594
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1259474591}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
+ m_Material: {fileID: 0}
+ m_Color: {r: 0.1981132, g: 0.1981132, b: 0.1981132, a: 1}
+ m_RaycastTarget: 1
+ m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
+ m_Maskable: 1
+ m_OnCullStateChanged:
+ m_PersistentCalls:
+ m_Calls: []
+ m_Sprite: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0}
+ m_Type: 1
+ m_PreserveAspect: 0
+ m_FillCenter: 1
+ m_FillMethod: 4
+ m_FillAmount: 1
+ m_FillClockwise: 1
+ m_FillOrigin: 0
+ m_UseSpriteMesh: 0
+ m_PixelsPerUnitMultiplier: 1
+--- !u!222 &1259474595
+CanvasRenderer:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1259474591}
+ m_CullTransparentMesh: 1
--- !u!1 &1290928582
GameObject:
m_ObjectHideFlags: 0
@@ -4234,6 +4505,7 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 4f231c4fb786f3946a6b90b886c48677, type: 3}
m_Name:
m_EditorClassIdentifier:
+ m_SendPointerHoverToParent: 1
m_HorizontalAxis: Horizontal
m_VerticalAxis: Vertical
m_SubmitButton: Submit
@@ -4455,6 +4727,16 @@ PrefabInstance:
m_Modification:
m_TransformParent: {fileID: 167044834}
m_Modifications:
+ - target: {fileID: 4850072633501053442, guid: d725b5588e1b956458798319e6541d84,
+ type: 3}
+ propertyPath: m_DisconnectClientButton
+ value:
+ objectReference: {fileID: 97319464}
+ - target: {fileID: 4850072633501053442, guid: d725b5588e1b956458798319e6541d84,
+ type: 3}
+ propertyPath: m_ClientSynchronizationMode
+ value: 0
+ objectReference: {fileID: 0}
- target: {fileID: 6633621479308595792, guid: d725b5588e1b956458798319e6541d84,
type: 3}
propertyPath: m_Pivot.x
@@ -4573,6 +4855,18 @@ RectTransform:
type: 3}
m_PrefabInstance: {fileID: 1865409448}
m_PrefabAsset: {fileID: 0}
+--- !u!114 &1865409450 stripped
+MonoBehaviour:
+ m_CorrespondingSourceObject: {fileID: 4850072633501053442, guid: d725b5588e1b956458798319e6541d84,
+ type: 3}
+ m_PrefabInstance: {fileID: 1865409448}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 0}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 50623966c8d88ab40982cc2b0e4c2d2e, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
--- !u!1 &1889006546
GameObject:
m_ObjectHideFlags: 0
diff --git a/testproject/Assets/Tests/Manual/Scripts/NetworkPrefabPool.cs b/testproject/Assets/Tests/Manual/Scripts/NetworkPrefabPool.cs
index bacf986921..bfedee4698 100644
--- a/testproject/Assets/Tests/Manual/Scripts/NetworkPrefabPool.cs
+++ b/testproject/Assets/Tests/Manual/Scripts/NetworkPrefabPool.cs
@@ -56,9 +56,12 @@ private void OnEnable()
if (s_Instance != null && s_Instance != this)
{
var instancePool = s_Instance.GetComponent();
- instancePool.MoveBackToCurrentlyActiveScene();
- m_ObjectPool = new List(instancePool.m_ObjectPool);
- instancePool.m_ObjectPool.Clear();
+ if (instancePool.m_ObjectPool != null)
+ {
+ instancePool.MoveBackToCurrentlyActiveScene();
+ instancePool.m_ObjectPool.Clear();
+ m_ObjectPool = new List(instancePool.m_ObjectPool);
+ }
Destroy(s_Instance);
s_Instance = null;
}
diff --git a/testproject/Assets/Tests/Runtime/NetworkSceneManager/ClientSynchronizationModeTests.cs b/testproject/Assets/Tests/Runtime/NetworkSceneManager/ClientSynchronizationModeTests.cs
new file mode 100644
index 0000000000..9d15c892b1
--- /dev/null
+++ b/testproject/Assets/Tests/Runtime/NetworkSceneManager/ClientSynchronizationModeTests.cs
@@ -0,0 +1,283 @@
+using System.Collections;
+using System.Collections.Generic;
+using NUnit.Framework;
+using UnityEngine.SceneManagement;
+using UnityEngine.TestTools;
+using Unity.Netcode;
+using Unity.Netcode.TestHelpers.Runtime;
+
+namespace TestProject.RuntimeTests
+{
+ ///
+ /// Validates that set to
+ /// Will synchronize clients properly when scenes are preloaded and when the server changes the currently active scene
+ /// prior to a client joining.
+ ///
+ ///
+ /// Note: If a client does not preload a scene prior to the server setting it as the active scene then the client(s) and the
+ /// server will end up sharing the same scene. More info:
+ ///
+ [TestFixture(ServerPreloadStates.NoPreloadOnServer)]
+ [TestFixture(ServerPreloadStates.PreloadOnServer)]
+ public class ClientSynchronizationModeTests : NetcodeIntegrationTest
+ {
+ // Two scenes with different in-scene placed NetworkObjects and one empty scene used to test active scene switching and client synchronization.
+ private List m_TestScenes = new List() { "InSceneNetworkObject", "GenericInScenePlacedObject", "EmptyScene1" };
+
+ protected override int NumberOfClients => 0;
+
+ public enum ServerPreloadStates
+ {
+ PreloadOnServer,
+ NoPreloadOnServer
+ }
+
+ public enum ClientPreloadStates
+ {
+ PreloadOnClient,
+ NoPreloadOnClient
+ }
+
+ public enum ActiveSceneStates
+ {
+ DefaultActiveScene,
+ SwitchActiveScene,
+ }
+
+ private ServerPreloadStates m_ServerPreloadState;
+ private List m_ServerLoadedScenes = new List();
+ private List m_TempClientPreLoadedScenes = new List();
+
+ private Dictionary> m_ClientScenesLoaded = new Dictionary>();
+
+
+ public ClientSynchronizationModeTests(ServerPreloadStates serverPreloadStates)
+ {
+ m_ServerPreloadState = serverPreloadStates;
+ }
+
+ protected override IEnumerator OnSetup()
+ {
+ m_TempClientPreLoadedScenes.Clear();
+ m_ServerLoadedScenes.Clear();
+ if (m_ServerPreloadState == ServerPreloadStates.PreloadOnServer)
+ {
+ SceneManager.sceneLoaded += SceneManager_sceneLoaded;
+ yield return LoadScenesOnServer();
+ }
+ yield return base.OnSetup();
+ }
+
+ private IEnumerator LoadScenesOnServer()
+ {
+ if (m_ServerPreloadState == ServerPreloadStates.PreloadOnServer)
+ {
+ foreach (var sceneToLoad in m_TestScenes)
+ {
+ SceneManager.LoadSceneAsync(sceneToLoad, LoadSceneMode.Additive);
+ }
+ yield return WaitForConditionOrTimeOut(AllScenesLoadedOnServer);
+ SceneManager.sceneLoaded -= SceneManager_sceneLoaded;
+ AssertOnTimeout($"[{m_ServerPreloadState}] Timed out waiting for all server-side scenes to be loaded!");
+ }
+ else
+ {
+ foreach (var sceneToLoad in m_TestScenes)
+ {
+ m_ServerNetworkManager.SceneManager.LoadScene(sceneToLoad, LoadSceneMode.Additive);
+ yield return WaitForConditionOrTimeOut(() => SceneLoadedOnServer(sceneToLoad));
+ AssertOnTimeout($"[{m_ServerPreloadState}] Timed out waiting for scene {sceneToLoad} to be loaded!");
+ }
+ }
+ }
+
+ private bool SceneLoadedOnServer(string sceneName)
+ {
+ foreach (var scene in m_ServerLoadedScenes)
+ {
+ if (scene.name == sceneName)
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private bool AllScenesLoadedOnServer()
+ {
+ if (m_ServerLoadedScenes.Count == m_TestScenes.Count)
+ {
+ foreach (var loadedScene in m_ServerLoadedScenes)
+ {
+ if (!m_TestScenes.Contains(loadedScene.name))
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+
+ private bool AllScenesPreloadedForClient()
+ {
+ if (m_TempClientPreLoadedScenes.Count == m_TestScenes.Count)
+ {
+ foreach (var loadedScene in m_TempClientPreLoadedScenes)
+ {
+ if (!m_TestScenes.Contains(loadedScene.name))
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+
+ private bool AllScenesLoadedOnClients()
+ {
+ foreach (var clientNetworkManager in m_ClientNetworkManagers)
+ {
+ if (!m_ClientScenesLoaded.ContainsKey(clientNetworkManager.LocalClientId))
+ {
+ return false;
+ }
+
+ if (m_ClientScenesLoaded[clientNetworkManager.LocalClientId].Count != m_TestScenes.Count)
+ {
+ return false;
+ }
+ foreach (var loadedScene in m_ClientScenesLoaded[clientNetworkManager.LocalClientId])
+ {
+ if (!m_TestScenes.Contains(loadedScene.name))
+ {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ private void SceneManager_sceneLoaded(Scene scene, LoadSceneMode loadSceneMode)
+ {
+ m_ServerLoadedScenes.Add(scene);
+ }
+
+
+ protected override IEnumerator OnStartedServerAndClients()
+ {
+ m_ServerNetworkManager.SceneManager.SetClientSynchronizationMode(LoadSceneMode.Additive);
+ return base.OnStartedServerAndClients();
+ }
+
+ protected override void OnNewClientStarted(NetworkManager networkManager)
+ {
+ networkManager.SceneManager.OnSceneEvent += ClientSide_OnSceneEvent;
+ base.OnNewClientStarted(networkManager);
+ }
+
+ ///
+ /// Verifies that both clients and the server will utilize preloaded scenes and that
+ /// in-scene placed NetworkObjects synchronize properly if the active scene changes
+ /// prior to a client connecting.
+ ///
+ ///
+ /// Notes:
+ /// ClientPreloadStates.NoPreloadOnClient: verifies that if no scene that needs to
+ /// be synchronized is preloaded the client will load the scene to be synchronized.
+ ///
+ /// ClientPreloadStates.PreloadOnClient: verifies that if a client has scenes that
+ /// will be synchronized preloaded the client will use those scenes as opposed to
+ /// loading duplicates.
+ ///
+ [UnityTest]
+ public IEnumerator PreloadedScenesTest([Values] ClientPreloadStates clientPreloadStates, [Values] ActiveSceneStates activeSceneState)
+ {
+ // If we didn't preload the scenes, then load the scenes via NetworkSceneManager
+ if (m_ServerPreloadState == ServerPreloadStates.NoPreloadOnServer)
+ {
+ m_ServerNetworkManager.SceneManager.OnSceneEvent += ServerSide_OnSceneEvent;
+ yield return LoadScenesOnServer();
+ }
+
+ // This tests that a change in the active scene will not impact in-scene placed
+ // NetworkObject synchronization (if it does then the clients would get a soft
+ // synchronization error).
+ if (activeSceneState == ActiveSceneStates.SwitchActiveScene)
+ {
+ SceneManager.SetActiveScene(m_ServerLoadedScenes[2]);
+ }
+
+ // Late join some clients
+ for (int i = 0; i < 1; i++)
+ {
+ // This tests that clients can have scenes preloaded prior to
+ // connecting and will use those scenes for synchronization
+ if (clientPreloadStates == ClientPreloadStates.PreloadOnClient)
+ {
+ m_TempClientPreLoadedScenes.Clear();
+ SceneManager.sceneLoaded += PreLoadClient_SceneLoaded;
+ foreach (var sceneToLoad in m_TestScenes)
+ {
+ SceneManager.LoadSceneAsync(sceneToLoad, LoadSceneMode.Additive);
+ }
+ yield return WaitForConditionOrTimeOut(AllScenesPreloadedForClient);
+ SceneManager.sceneLoaded -= PreLoadClient_SceneLoaded;
+ AssertOnTimeout($"[{clientPreloadStates}] Timed out waiting for client-side scenes to be preloaded!");
+ }
+ yield return CreateAndStartNewClient();
+ AssertOnTimeout($"[Client Instance {i + 1}] Timed out waiting for client to start and connect!");
+
+ yield return WaitForConditionOrTimeOut(AllScenesLoadedOnClients);
+ AssertOnTimeout($"[Client-{m_ClientNetworkManagers[i].LocalClientId}] Timed out waiting for all scenes to be synchronized for new client!");
+ }
+ }
+
+ private void PreLoadClient_SceneLoaded(Scene scene, LoadSceneMode loadSceneMode)
+ {
+ m_TempClientPreLoadedScenes.Add(scene);
+ }
+
+ private void ClientSide_OnSceneEvent(SceneEvent sceneEvent)
+ {
+ switch (sceneEvent.SceneEventType)
+ {
+ case SceneEventType.LoadComplete:
+ {
+ if (!m_ClientScenesLoaded.ContainsKey(sceneEvent.ClientId))
+ {
+ m_ClientScenesLoaded.Add(sceneEvent.ClientId, new List());
+ }
+ m_ClientScenesLoaded[sceneEvent.ClientId].Add(sceneEvent.Scene);
+ break;
+ }
+ }
+ }
+ private void ServerSide_OnSceneEvent(SceneEvent sceneEvent)
+ {
+ // Filter for server-side only scene events
+ if (sceneEvent.ClientId != m_ServerNetworkManager.LocalClientId)
+ {
+ return;
+ }
+
+ switch (sceneEvent.SceneEventType)
+ {
+ case SceneEventType.LoadComplete:
+ {
+ m_ServerLoadedScenes.Add(sceneEvent.Scene);
+ break;
+ }
+ }
+ }
+
+ protected override IEnumerator OnTearDown()
+ {
+ SceneManager.sceneLoaded -= SceneManager_sceneLoaded;
+ m_TempClientPreLoadedScenes.Clear();
+ m_ClientScenesLoaded.Clear();
+ return base.OnTearDown();
+ }
+ }
+}
diff --git a/testproject/Assets/Tests/Runtime/NetworkSceneManager/ClientSynchronizationModeTests.cs.meta b/testproject/Assets/Tests/Runtime/NetworkSceneManager/ClientSynchronizationModeTests.cs.meta
new file mode 100644
index 0000000000..596123e1c6
--- /dev/null
+++ b/testproject/Assets/Tests/Runtime/NetworkSceneManager/ClientSynchronizationModeTests.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 26618a817fc595d4dbc1ad91cc89e53d
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkObjectSceneMigrationTests.cs b/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkObjectSceneMigrationTests.cs
new file mode 100644
index 0000000000..32e548b6c6
--- /dev/null
+++ b/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkObjectSceneMigrationTests.cs
@@ -0,0 +1,513 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using NUnit.Framework;
+using UnityEngine;
+using UnityEngine.SceneManagement;
+using UnityEngine.TestTools;
+using Unity.Netcode;
+using Unity.Netcode.TestHelpers.Runtime;
+using Object = UnityEngine.Object;
+
+namespace TestProject.RuntimeTests
+{
+ ///
+ /// NetworkObject Scene Migration Integration Tests
+ ///
+ ///
+ ///
+ public class NetworkObjectSceneMigrationTests : NetcodeIntegrationTest
+ {
+ private List m_TestScenes = new List() { "EmptyScene1", "EmptyScene2", "EmptyScene3" };
+ protected override int NumberOfClients => 2;
+ private GameObject m_TestPrefab;
+ private GameObject m_TestPrefabAutoSynchActiveScene;
+ private GameObject m_TestPrefabDestroyWithScene;
+ private Scene m_OriginalActiveScene;
+
+ private bool m_ClientsLoadedScene;
+ private bool m_ClientsUnloadedScene;
+ private Scene m_SceneLoaded;
+ private List m_ServerSpawnedPrefabInstances = new List();
+ private List m_ServerSpawnedDestroyWithSceneInstances = new List();
+ private List m_ScenesLoaded = new List();
+ private string m_CurrentSceneLoading;
+ private string m_CurrentSceneUnloading;
+
+
+ protected override IEnumerator OnSetup()
+ {
+ m_OriginalActiveScene = SceneManager.GetActiveScene();
+ return base.OnSetup();
+ }
+
+ protected override void OnServerAndClientsCreated()
+ {
+ // Synchronize Scene Changes (default) Test Network Prefab
+ m_TestPrefab = CreateNetworkObjectPrefab("TestObject");
+
+ // Auto Synchronize Active Scene Changes Test Network Prefab
+ m_TestPrefabAutoSynchActiveScene = CreateNetworkObjectPrefab("ASASObject");
+ m_TestPrefabAutoSynchActiveScene.GetComponent().ActiveSceneSynchronization = true;
+
+ // Destroy With Scene Test Network Prefab
+ m_TestPrefabDestroyWithScene = CreateNetworkObjectPrefab("DWSObject");
+ m_TestPrefabDestroyWithScene.AddComponent();
+
+ DestroyWithSceneInstancesTestHelper.ShouldNeverSpawn = m_TestPrefabDestroyWithScene;
+
+ base.OnServerAndClientsCreated();
+ }
+
+ protected override void OnNewClientCreated(NetworkManager networkManager)
+ {
+ foreach (var networkPrfab in m_ServerNetworkManager.NetworkConfig.Prefabs.Prefabs)
+ {
+ if (networkPrfab.Prefab == null)
+ {
+ continue;
+ }
+ networkManager.NetworkConfig.Prefabs.Add(networkPrfab);
+ }
+ base.OnNewClientCreated(networkManager);
+ }
+
+ private bool DidClientsSpawnInstance(NetworkObject serverObject, bool checkDestroyWithScene = false)
+ {
+ foreach (var networkManager in m_ClientNetworkManagers)
+ {
+ if (!s_GlobalNetworkObjects.ContainsKey(networkManager.LocalClientId))
+ {
+ return false;
+ }
+ var clientNetworkObjects = s_GlobalNetworkObjects[networkManager.LocalClientId];
+ if (!clientNetworkObjects.ContainsKey(serverObject.NetworkObjectId))
+ {
+ return false;
+ }
+
+ if (checkDestroyWithScene)
+ {
+ if (serverObject.DestroyWithScene != clientNetworkObjects[serverObject.NetworkObjectId])
+ {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ private bool VerifyAllClientsSpawnedInstances()
+ {
+ foreach (var serverObject in m_ServerSpawnedPrefabInstances)
+ {
+ if (!DidClientsSpawnInstance(serverObject))
+ {
+ return false;
+ }
+ }
+
+ foreach (var serverObject in m_ServerSpawnedDestroyWithSceneInstances)
+ {
+ if (!DidClientsSpawnInstance(serverObject, true))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private bool AreClientInstancesInTheRightScene(NetworkObject serverObject)
+ {
+ foreach (var networkManager in m_ClientNetworkManagers)
+ {
+ var clientNetworkObjects = s_GlobalNetworkObjects[networkManager.LocalClientId];
+ if (clientNetworkObjects == null)
+ {
+ continue;
+ }
+ // If a networkObject is null then it was destroyed
+ if (clientNetworkObjects[serverObject.NetworkObjectId].gameObject.scene.name != serverObject.gameObject.scene.name)
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private bool VerifySpawnedObjectsMigrated()
+ {
+ foreach (var serverObject in m_ServerSpawnedPrefabInstances)
+ {
+ if (!AreClientInstancesInTheRightScene(serverObject))
+ {
+ return false;
+ }
+ }
+
+ foreach (var serverObject in m_ServerSpawnedDestroyWithSceneInstances)
+ {
+ if (!AreClientInstancesInTheRightScene(serverObject))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ ///
+ /// Integration test to verify that migrating NetworkObjects
+ /// into different scenes (in the same frame) is synchronized
+ /// with connected clients and synchronizes with late joining
+ /// clients.
+ ///
+ [UnityTest]
+ public IEnumerator MigrateIntoNewSceneTest()
+ {
+ // Spawn 9 NetworkObject instances
+ for (int i = 0; i < 9; i++)
+ {
+ var serverInstance = Object.Instantiate(m_TestPrefab);
+ var serverNetworkObject = serverInstance.GetComponent();
+ serverNetworkObject.Spawn();
+ m_ServerSpawnedPrefabInstances.Add(serverNetworkObject);
+ }
+ yield return WaitForConditionOrTimeOut(VerifyAllClientsSpawnedInstances);
+ AssertOnTimeout($"Timed out waiting for all clients to spawn {nameof(NetworkObject)}s!");
+
+ // Now load three scenes to migrate the newly spawned NetworkObjects into
+ m_ServerNetworkManager.SceneManager.OnSceneEvent += SceneManager_OnSceneEvent;
+ for (int i = 0; i < 3; i++)
+ {
+ m_ClientsLoadedScene = false;
+ m_CurrentSceneLoading = m_TestScenes[i];
+ var status = m_ServerNetworkManager.SceneManager.LoadScene(m_TestScenes[i], LoadSceneMode.Additive);
+ Assert.True(status == SceneEventProgressStatus.Started, $"Failed to start loading scene {m_CurrentSceneLoading}! Return status: {status}");
+ yield return WaitForConditionOrTimeOut(() => m_ClientsLoadedScene);
+ AssertOnTimeout($"Timed out waiting for all clients to load scene {m_CurrentSceneLoading}!");
+ }
+
+ var objectCount = 0;
+ // Migrate each networkObject into one of the three scenes.
+ // There will be 3 networkObjects per newly loaded scenes when done.
+ foreach (var scene in m_ScenesLoaded)
+ {
+ // Now migrate the NetworkObject
+ SceneManager.MoveGameObjectToScene(m_ServerSpawnedPrefabInstances[objectCount].gameObject, scene);
+ SceneManager.MoveGameObjectToScene(m_ServerSpawnedPrefabInstances[objectCount + 1].gameObject, scene);
+ SceneManager.MoveGameObjectToScene(m_ServerSpawnedPrefabInstances[objectCount + 2].gameObject, scene);
+ objectCount += 3;
+ }
+
+ yield return WaitForConditionOrTimeOut(VerifySpawnedObjectsMigrated);
+ AssertOnTimeout($"Timed out waiting for all clients to migrate all NetworkObjects into the appropriate scenes!");
+
+ // Verify that a late joining client synchronizes properly
+ yield return CreateAndStartNewClient();
+ yield return WaitForConditionOrTimeOut(VerifySpawnedObjectsMigrated);
+ AssertOnTimeout($"[Late Joined Client] Timed out waiting for all clients to migrate all NetworkObjects into the appropriate scenes!");
+ }
+
+ ///
+ /// Integration test to verify changing the currently active scene
+ /// will migrate NetworkObjects with ActiveSceneSynchronization set
+ /// to true.
+ ///
+ [UnityTest]
+ public IEnumerator ActiveSceneSynchronizationTest()
+ {
+ // Disable resynchronization for this test to avoid issues with trying
+ // to synchronize them.
+ NetworkSceneManager.DisableReSynchronization = true;
+
+ // Spawn 3 NetworkObject instances that auto synchronize to active scene changes
+ for (int i = 0; i < 3; i++)
+ {
+ var serverInstance = Object.Instantiate(m_TestPrefabAutoSynchActiveScene);
+ var serverNetworkObject = serverInstance.GetComponent();
+ // We are also testing that objects marked to synchronize with changes to
+ // the active scene and marked to destroy with scene =are destroyed= if
+ // the scene being unloaded is currently the active scene and the scene that
+ // the NetworkObjects reside within.
+ serverNetworkObject.Spawn(true);
+ m_ServerSpawnedPrefabInstances.Add(serverNetworkObject);
+ }
+
+ // Spawn 3 NetworkObject instances that do not auto synchronize to active scene changes
+ // and ==should not be== destroyed with the scene (these should be the only remaining
+ // instances)
+ for (int i = 0; i < 3; i++)
+ {
+ var serverInstance = Object.Instantiate(m_TestPrefab);
+ var serverNetworkObject = serverInstance.GetComponent();
+ // This set of NetworkObjects will be used to verify that NetworkObjets
+ // spawned with DestroyWithScene set to false will migrate into the current
+ // active scene if the scene they currently reside within is destroyed and
+ // is not the currently active scene.
+ serverNetworkObject.Spawn();
+ m_ServerSpawnedPrefabInstances.Add(serverNetworkObject);
+ }
+
+ // Spawn 3 NetworkObject instances that do not auto synchronize to active scene changes
+ // and ==should be== destroyed with the scene when it is unloaded
+ for (int i = 0; i < 3; i++)
+ {
+ var serverInstance = Object.Instantiate(m_TestPrefabDestroyWithScene);
+ var serverNetworkObject = serverInstance.GetComponent();
+ // This set of NetworkObjects will be used to verify that NetworkObjets
+ // spawned with DestroyWithScene == true will get destroyed when the scene
+ // is unloaded
+ serverNetworkObject.Spawn(true);
+ m_ServerSpawnedDestroyWithSceneInstances.Add(serverNetworkObject);
+ }
+
+ yield return WaitForConditionOrTimeOut(VerifyAllClientsSpawnedInstances);
+ AssertOnTimeout($"Timed out waiting for all clients to spawn {nameof(NetworkObject)}s!");
+
+ // Now load three scenes
+ m_ServerNetworkManager.SceneManager.OnSceneEvent += SceneManager_OnSceneEvent;
+ for (int i = 0; i < 3; i++)
+ {
+ m_ClientsLoadedScene = false;
+ m_CurrentSceneLoading = m_TestScenes[i];
+ var loadStatus = m_ServerNetworkManager.SceneManager.LoadScene(m_TestScenes[i], LoadSceneMode.Additive);
+ Assert.True(loadStatus == SceneEventProgressStatus.Started, $"Failed to start loading scene {m_CurrentSceneLoading}! Return status: {loadStatus}");
+ yield return WaitForConditionOrTimeOut(() => m_ClientsLoadedScene);
+ AssertOnTimeout($"Timed out waiting for all clients to load scene {m_CurrentSceneLoading}!");
+ }
+
+ // Migrate the instances that don't synchronize with active scene changes into the 3rd loaded scene
+ // (We are making sure these stay in the same scene they are migrated into)
+ for (int i = 3; i < m_ServerSpawnedPrefabInstances.Count; i++)
+ {
+ SceneManager.MoveGameObjectToScene(m_ServerSpawnedPrefabInstances[i].gameObject, m_ScenesLoaded[2]);
+ }
+
+ // Migrate the instances that don't synchronize with active scene changes and are destroyed with the
+ // scene unloading into the 3rd loaded scene
+ // (We are making sure these get destroyed when the scene is unloaded)
+ for (int i = 0; i < m_ServerSpawnedDestroyWithSceneInstances.Count; i++)
+ {
+ SceneManager.MoveGameObjectToScene(m_ServerSpawnedDestroyWithSceneInstances[i].gameObject, m_ScenesLoaded[2]);
+ }
+
+ // Make sure they migrated to the proper scene
+ yield return WaitForConditionOrTimeOut(VerifySpawnedObjectsMigrated);
+ AssertOnTimeout($"Timed out waiting for all clients to migrate all NetworkObjects into the appropriate scenes!");
+
+ // Now change the active scene
+ SceneManager.SetActiveScene(m_ScenesLoaded[1]);
+ // We have to do this
+ Object.DontDestroyOnLoad(m_TestPrefabAutoSynchActiveScene);
+
+ // First, make sure server-side scenes and client side scenes match
+ yield return WaitForConditionOrTimeOut(VerifySpawnedObjectsMigrated);
+ AssertOnTimeout($"Timed out waiting for all clients to migrate all NetworkObjects into the appropriate scenes!");
+
+ // Verify that the auto-active-scene synchronization NetworkObjects migrated to the newly
+ // assigned active scene
+ for (int i = 0; i < 3; i++)
+ {
+ Assert.True(m_ServerSpawnedPrefabInstances[i].gameObject.scene == m_ScenesLoaded[1],
+ $"{m_ServerSpawnedPrefabInstances[i].gameObject.name} did not migrate into scene {m_ScenesLoaded[1].name}!");
+ }
+
+ // Verify that the other NetworkObjects that don't synchronize with active scene changes did
+ // not migrate into the active scene.
+ for (int i = 3; i < m_ServerSpawnedPrefabInstances.Count; i++)
+ {
+ Assert.False(m_ServerSpawnedPrefabInstances[i].gameObject.scene == m_ScenesLoaded[1],
+ $"{m_ServerSpawnedPrefabInstances[i].gameObject.name} migrated into scene {m_ScenesLoaded[1].name}!");
+ }
+
+ for (int i = 0; i < 3; i++)
+ {
+ Assert.False(m_ServerSpawnedDestroyWithSceneInstances[i].gameObject.scene == m_ScenesLoaded[1],
+ $"{m_ServerSpawnedDestroyWithSceneInstances[i].gameObject.name} migrated into scene {m_ScenesLoaded[1].name}!");
+ }
+
+ // Verify that a late joining client synchronizes properly and destroys the appropriate NetworkObjects
+ yield return CreateAndStartNewClient();
+ yield return WaitForConditionOrTimeOut(VerifySpawnedObjectsMigrated);
+ AssertOnTimeout($"[Late Joined Client #1] Timed out waiting for all clients to migrate all NetworkObjects into the appropriate scenes!");
+
+ // Now, unload the scene containing the NetworkObjects that don't synchronize with active scene changes
+ DestroyWithSceneInstancesTestHelper.NetworkObjectDestroyed += OnNonActiveSynchDestroyWithSceneNetworkObjectDestroyed;
+ m_ClientsUnloadedScene = false;
+ m_CurrentSceneUnloading = m_ScenesLoaded[2].name;
+ var status = m_ServerNetworkManager.SceneManager.UnloadScene(m_ScenesLoaded[2]);
+ Assert.True(status == SceneEventProgressStatus.Started, $"Failed to start unloading scene {m_ScenesLoaded[2].name} with status {status}!");
+ yield return WaitForConditionOrTimeOut(() => m_ClientsUnloadedScene);
+
+ // Clean up any destroyed NetworkObjects
+ for (int i = m_ServerSpawnedPrefabInstances.Count - 1; i >= 0; i--)
+ {
+ if (m_ServerSpawnedPrefabInstances[i] == null)
+ {
+ m_ServerSpawnedPrefabInstances.RemoveAt(i);
+ }
+ }
+
+ AssertOnTimeout($"Timed out waiting for all clients to unload scene {m_CurrentSceneUnloading}!");
+ yield return WaitForConditionOrTimeOut(VerifySpawnedObjectsMigrated);
+ AssertOnTimeout($"Timed out waiting for all clients to migrate all NetworkObjects into the appropriate scenes!");
+
+ // Verify that the NetworkObjects that don't synchronize with active scene changes but marked to not
+ // destroy with the scene are migrated into the current active scene
+ for (int i = 3; i < m_ServerSpawnedPrefabInstances.Count; i++)
+ {
+ Assert.True(m_ServerSpawnedPrefabInstances[i].gameObject.scene == m_ScenesLoaded[1],
+ $"{m_ServerSpawnedPrefabInstances[i].gameObject.name} did not migrate into scene {m_ScenesLoaded[1].name} but are in scene {m_ServerSpawnedPrefabInstances[i].gameObject.scene.name}!");
+ }
+
+ // Verify all NetworkObjects that should have been destroyed with the scene unloaded were destroyed
+ yield return WaitForConditionOrTimeOut(() => DestroyWithSceneInstancesTestHelper.ObjectRelativeInstances.Count == 0);
+ DestroyWithSceneInstancesTestHelper.NetworkObjectDestroyed -= OnNonActiveSynchDestroyWithSceneNetworkObjectDestroyed;
+ AssertOnTimeout($"Timed out waiting for all client instances marked to destroy when the scene unloaded to be despawned and destroyed.");
+
+ // Now unload the active scene to verify all remaining NetworkObjects are migrated into the SceneManager
+ // assigned active scene
+ m_ClientsUnloadedScene = false;
+ m_CurrentSceneUnloading = m_ScenesLoaded[1].name;
+ m_ServerNetworkManager.SceneManager.UnloadScene(m_ScenesLoaded[1]);
+ yield return WaitForConditionOrTimeOut(() => m_ClientsUnloadedScene);
+ AssertOnTimeout($"Timed out waiting for all clients to unload scene {m_CurrentSceneUnloading}!");
+
+ // Clean up any destroyed NetworkObjects
+ for (int i = m_ServerSpawnedPrefabInstances.Count - 1; i >= 0; i--)
+ {
+ if (m_ServerSpawnedPrefabInstances[i] == null)
+ {
+ m_ServerSpawnedPrefabInstances.RemoveAt(i);
+ }
+ }
+
+ // Verify a late joining client will synchronize properly with the end result
+ yield return CreateAndStartNewClient();
+
+ // Verify the late joining client spawns all instances
+ yield return WaitForConditionOrTimeOut(VerifyAllClientsSpawnedInstances);
+ AssertOnTimeout($"Timed out waiting for all clients to spawn {nameof(NetworkObject)}s!");
+
+ // Verify the instances are in the correct scenes
+ yield return WaitForConditionOrTimeOut(VerifySpawnedObjectsMigrated);
+ AssertOnTimeout($"[Late Joined Client #2] Timed out waiting for all clients to migrate all NetworkObjects into the appropriate scenes!");
+
+ // All but 3 instances should be destroyed
+ Assert.True(m_ServerSpawnedPrefabInstances.Count == 3, $"{nameof(m_ServerSpawnedPrefabInstances)} still has a count of {m_ServerSpawnedPrefabInstances.Count} " +
+ $"NetworkObject instances!");
+ Assert.True(m_ServerSpawnedDestroyWithSceneInstances.Count == 0, $"{nameof(m_ServerSpawnedDestroyWithSceneInstances)} still has a count of " +
+ $"{m_ServerSpawnedDestroyWithSceneInstances.Count} NetworkObject instances!");
+ for (int i = 0; i < 3; i++)
+ {
+ Assert.True(m_ServerSpawnedPrefabInstances[i].gameObject.name.Contains(m_TestPrefab.gameObject.name), $"Expected {m_ServerSpawnedPrefabInstances[i].gameObject.name} to contain {m_TestPrefab.gameObject.name}!");
+ }
+ }
+
+ ///
+ /// Callback invoked when a test prefab, with the
+ /// component attached, is destroyed.
+ ///
+ private void OnNonActiveSynchDestroyWithSceneNetworkObjectDestroyed(NetworkObject networkObject)
+ {
+ m_ServerSpawnedDestroyWithSceneInstances.Remove(networkObject);
+ }
+
+ private void SceneManager_OnSceneEvent(SceneEvent sceneEvent)
+ {
+ switch (sceneEvent.SceneEventType)
+ {
+ case SceneEventType.LoadComplete:
+ {
+ if (sceneEvent.ClientId == m_ServerNetworkManager.LocalClientId)
+ {
+ m_SceneLoaded = sceneEvent.Scene;
+ m_ScenesLoaded.Add(sceneEvent.Scene);
+ }
+ break;
+ }
+ case SceneEventType.LoadEventCompleted:
+ {
+ Assert.IsTrue(sceneEvent.ClientsThatTimedOut.Count == 0, $"{sceneEvent.ClientsThatTimedOut.Count} clients timed out while trying to load scene {m_CurrentSceneLoading}!");
+ m_ClientsLoadedScene = true;
+ break;
+ }
+ case SceneEventType.UnloadEventCompleted:
+ {
+ if (sceneEvent.SceneName == m_CurrentSceneUnloading)
+ {
+ m_ClientsUnloadedScene = true;
+ }
+ break;
+ }
+ }
+ }
+
+ protected override IEnumerator OnTearDown()
+ {
+ m_TestPrefab = null;
+ m_TestPrefabAutoSynchActiveScene = null;
+ m_TestPrefabDestroyWithScene = null;
+ SceneManager.SetActiveScene(m_OriginalActiveScene);
+ m_ServerSpawnedDestroyWithSceneInstances.Clear();
+ m_ServerSpawnedPrefabInstances.Clear();
+ m_ScenesLoaded.Clear();
+ yield return base.OnTearDown();
+ }
+ }
+
+ ///
+ /// Helper NetworkBehaviour Component
+ /// For test:
+ ///
+ internal class DestroyWithSceneInstancesTestHelper : NetworkBehaviour
+ {
+ public static GameObject ShouldNeverSpawn;
+
+ public static Dictionary> ObjectRelativeInstances = new Dictionary>();
+
+ public static Action NetworkObjectDestroyed;
+
+ ///
+ /// Called when destroyed
+ /// Passes the client ID and the NetworkObject instance
+ ///
+ public Action ObjectDestroyed;
+
+ public override void OnNetworkSpawn()
+ {
+ if (!ObjectRelativeInstances.ContainsKey(NetworkManager.LocalClientId))
+ {
+ ObjectRelativeInstances.Add(NetworkManager.LocalClientId, new Dictionary());
+ }
+
+ ObjectRelativeInstances[NetworkManager.LocalClientId].Add(NetworkObjectId, NetworkObject);
+ base.OnNetworkSpawn();
+ }
+
+ public override void OnNetworkDespawn()
+ {
+ ObjectRelativeInstances[NetworkManager.LocalClientId].Remove(NetworkObjectId);
+ if (ObjectRelativeInstances[NetworkManager.LocalClientId].Count == 0)
+ {
+ ObjectRelativeInstances.Remove(NetworkManager.LocalClientId);
+ }
+ base.OnNetworkDespawn();
+ }
+
+ public override void OnDestroy()
+ {
+ if (NetworkManager != null)
+ {
+ if (NetworkManager.LocalClientId == NetworkManager.ServerClientId)
+ {
+ NetworkObjectDestroyed?.Invoke(NetworkObject);
+ }
+ }
+ base.OnDestroy();
+ }
+ }
+
+}
diff --git a/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkObjectSceneMigrationTests.cs.meta b/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkObjectSceneMigrationTests.cs.meta
new file mode 100644
index 0000000000..7b8dfd6a1b
--- /dev/null
+++ b/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkObjectSceneMigrationTests.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 7915a6a8062bc414ab4ff730f3f778f5
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkSceneManagerEventNotifications.cs b/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkSceneManagerEventNotifications.cs
index 3d09e13690..2f8f7b927c 100644
--- a/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkSceneManagerEventNotifications.cs
+++ b/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkSceneManagerEventNotifications.cs
@@ -89,7 +89,7 @@ private void ClientSceneManager_OnSceneEvent(SceneEvent sceneEvent)
{
var matchedClient = m_ClientNetworkManagers.Where(c => c.LocalClientId == sceneEvent.ClientId);
Assert.True(matchedClient.Count() > 0, $"Found no client {nameof(NetworkManager)}s that had a {nameof(NetworkManager.LocalClientId)} of {sceneEvent.ClientId}");
- Assert.AreEqual(matchedClient.First().SceneManager.ClientSynchronizationMode, m_LoadSceneMode);
+ Assert.AreEqual(matchedClient.First().SceneManager.ClientSynchronizationMode, m_ServerNetworkManager.SceneManager.ClientSynchronizationMode);
break;
}
}
diff --git a/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkSceneManagerSeneVerification.cs b/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkSceneManagerSeneVerification.cs
index efeeb328da..185ec39360 100644
--- a/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkSceneManagerSeneVerification.cs
+++ b/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkSceneManagerSeneVerification.cs
@@ -161,10 +161,8 @@ private void InitializeSceneTestInfo(LoadSceneMode clientSynchronizationMode, bo
m_ScenesLoaded.Clear();
foreach (var manager in m_ClientNetworkManagers)
{
-
m_ShouldWaitList.Add(new SceneTestInfo() { ClientId = manager.LocalClientId, ShouldWait = false });
manager.SceneManager.VerifySceneBeforeLoading = m_ClientVerificationAction;
- manager.SceneManager.SetClientSynchronizationMode(clientSynchronizationMode);
}
}
@@ -255,11 +253,20 @@ private bool ContainsAllClients(List clients)
private bool ServerVerifySceneBeforeLoading(int sceneIndex, string sceneName, LoadSceneMode loadSceneMode)
{
- Assert.IsTrue(m_ExpectedSceneIndex == sceneIndex);
- Assert.IsTrue(m_ExpectedSceneName == sceneName);
- Assert.IsTrue(m_ExpectedLoadMode == loadSceneMode);
+ if (m_ExpectedSceneIndex != 0 && m_ExpectedSceneName != null)
+ {
+ // Ignore the test runner test scene.
+ if (sceneIndex != m_ExpectedSceneIndex && sceneName.Contains("InitTestScene"))
+ {
+ return false;
+ }
- return m_ServerVerifyScene;
+ Assert.IsTrue(m_ExpectedSceneIndex == sceneIndex);
+ Assert.IsTrue(m_ExpectedSceneName == sceneName);
+ Assert.IsTrue(m_ExpectedLoadMode == loadSceneMode);
+ return m_ServerVerifyScene;
+ }
+ return false;
}
private bool ClientVerifySceneBeforeLoading(int sceneIndex, string sceneName, LoadSceneMode loadSceneMode)
diff --git a/testproject/Assets/Tests/Runtime/ObjectParenting/NetworkObjectParentingTests.cs b/testproject/Assets/Tests/Runtime/ObjectParenting/NetworkObjectParentingTests.cs
index 9222db9140..f6b3925d55 100644
--- a/testproject/Assets/Tests/Runtime/ObjectParenting/NetworkObjectParentingTests.cs
+++ b/testproject/Assets/Tests/Runtime/ObjectParenting/NetworkObjectParentingTests.cs
@@ -38,6 +38,11 @@ private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
}
}
+ private void InvokeBeforeClientsStart()
+ {
+ m_ServerNetworkManager.SceneManager.ClientSynchronizationMode = LoadSceneMode.Additive;
+ }
+
[UnitySetUp]
public IEnumerator Setup()
{
@@ -83,7 +88,7 @@ public IEnumerator Setup()
}
// Start server and client NetworkManager instances
- Assert.That(NetcodeIntegrationTestHelpers.Start(true, m_ServerNetworkManager, m_ClientNetworkManagers));
+ Assert.That(NetcodeIntegrationTestHelpers.Start(true, m_ServerNetworkManager, m_ClientNetworkManagers, InvokeBeforeClientsStart));
// Wait for connection on client side
yield return NetcodeIntegrationTestHelpers.WaitForClientsConnected(m_ClientNetworkManagers);
diff --git a/testproject/Assets/Tests/Runtime/ObjectParenting/ParentingInSceneObjectsTests.cs b/testproject/Assets/Tests/Runtime/ObjectParenting/ParentingInSceneObjectsTests.cs
index c1853c0d0b..0f9faeb247 100644
--- a/testproject/Assets/Tests/Runtime/ObjectParenting/ParentingInSceneObjectsTests.cs
+++ b/testproject/Assets/Tests/Runtime/ObjectParenting/ParentingInSceneObjectsTests.cs
@@ -328,7 +328,7 @@ public IEnumerator DespawnParentTest([Values] ParentingSpace parentingSpace)
SceneManager.LoadScene(k_BaseSceneToLoad, LoadSceneMode.Additive);
m_InitialClientsLoadedScene = false;
m_ServerNetworkManager.SceneManager.OnSceneEvent += SceneManager_OnSceneEvent;
-
+ m_ServerNetworkManager.SceneManager.ClientSynchronizationMode = LoadSceneMode.Additive;
var sceneEventStartedStatus = m_ServerNetworkManager.SceneManager.LoadScene(k_TestSceneToLoad, LoadSceneMode.Additive);
Assert.True(sceneEventStartedStatus == SceneEventProgressStatus.Started, $"Failed to load scene {k_TestSceneToLoad} with a return status of {sceneEventStartedStatus}.");
yield return WaitForConditionOrTimeOut(() => m_InitialClientsLoadedScene);
diff --git a/testproject/ProjectSettings/EditorBuildSettings.asset b/testproject/ProjectSettings/EditorBuildSettings.asset
index 0e20bdb99d..c611036021 100644
--- a/testproject/ProjectSettings/EditorBuildSettings.asset
+++ b/testproject/ProjectSettings/EditorBuildSettings.asset
@@ -122,6 +122,15 @@ EditorBuildSettings:
- enabled: 1
path: Assets/Tests/Manual/IntegrationTestScenes/GenericInScenePlacedObject.unity
guid: 43c36dc1d38660e4d9879e84e580e22f
+ - enabled: 1
+ path: Assets/Tests/Manual/IntegrationTestScenes/EmptyScene1.unity
+ guid: 057ba2cc37faa0b43aa7051d9f555caa
+ - enabled: 1
+ path: Assets/Tests/Manual/IntegrationTestScenes/EmptyScene2.unity
+ guid: 17b92153f7381d34fa48c4d5c0393d13
+ - enabled: 1
+ path: Assets/Tests/Manual/IntegrationTestScenes/EmptyScene3.unity
+ guid: abd4c8b51c445d54faa16c67ac973f1b
m_configObjects:
com.unity.addressableassets: {fileID: 11400000, guid: 5a3d5c53c25349c48912726ae850f3b0,
type: 2}