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}