diff --git a/com.unity.netcode.gameobjects/CHANGELOG.md b/com.unity.netcode.gameobjects/CHANGELOG.md index d871452658..cbbdc26d95 100644 --- a/com.unity.netcode.gameobjects/CHANGELOG.md +++ b/com.unity.netcode.gameobjects/CHANGELOG.md @@ -28,6 +28,7 @@ Additional documentation and release notes are available at [Multiplayer Documen ### Fixed +- Fixed issue where an in-scene placed NetworkObject would not invoke NetworkBehaviour.OnNetworkSpawn if the GameObject was disabled when it was despawned. (#2239) - Fixed issue where clients were not rebuilding the `NetworkConfig` hash value for each unique connection request. (#2226) - Fixed the issue where player objects were not taking the `DontDestroyWithOwner` property into consideration when a client disconnected. (#2225) - Fixed issue where `SceneEventProgress` would not complete if a client late joins while it is still in progress. (#2222) diff --git a/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs b/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs index 531e582f55..c91cee28e9 100644 --- a/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs @@ -1441,6 +1441,9 @@ private void OnServerLoadedScene(uint sceneEventId, Scene scene) } } + // Add any despawned when spawned in-scene placed NetworkObjects to the scene event data + sceneEventData.AddDespawnedInSceneNetworkObjects(); + // Set the server's scene's handle so the client can build a look up table sceneEventData.SceneHandle = scene.handle; diff --git a/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs b/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs index ae8d5a31a9..222e437ca2 100644 --- a/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs +++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs @@ -268,7 +268,8 @@ internal void AddSpawnedNetworkObjects() internal void AddDespawnedInSceneNetworkObjects() { m_DespawnedInSceneObjectsSync.Clear(); - var inSceneNetworkObjects = UnityEngine.Object.FindObjectsOfType().Where((c) => c.NetworkManager == m_NetworkManager); + // Find all active and non-active in-scene placed NetworkObjects + var inSceneNetworkObjects = UnityEngine.Object.FindObjectsOfType(includeInactive: true).Where((c) => c.NetworkManager == m_NetworkManager); foreach (var sobj in inSceneNetworkObjects) { if (sobj.IsSceneObject.HasValue && sobj.IsSceneObject.Value && !sobj.IsSpawned) @@ -461,7 +462,6 @@ internal void WriteSceneSynchronizationData(FastBufferWriter writer) for (var i = 0; i < m_DespawnedInSceneObjectsSync.Count; ++i) { var noStart = writer.Position; - var sceneObject = m_DespawnedInSceneObjectsSync[i].GetMessageSceneObject(TargetClientId); BytePacker.WriteValuePacked(writer, m_DespawnedInSceneObjectsSync[i].GetSceneOriginHandle()); BytePacker.WriteValuePacked(writer, m_DespawnedInSceneObjectsSync[i].GlobalObjectIdHash); var noStop = writer.Position; @@ -507,6 +507,15 @@ internal void SerializeScenePlacedObjects(FastBufferWriter writer) } } + // Write the number of despawned in-scene placed NetworkObjects + writer.WriteValueSafe(m_DespawnedInSceneObjectsSync.Count); + // Write the scene handle and GlobalObjectIdHash value + for (var i = 0; i < m_DespawnedInSceneObjectsSync.Count; ++i) + { + BytePacker.WriteValuePacked(writer, m_DespawnedInSceneObjectsSync[i].GetSceneOriginHandle()); + BytePacker.WriteValuePacked(writer, m_DespawnedInSceneObjectsSync[i].GlobalObjectIdHash); + } + var tailPosition = writer.Position; // Reposition to our count position to the head before we wrote our object count writer.Seek(headPosition); @@ -624,6 +633,8 @@ internal void DeserializeScenePlacedObjects() sceneObject.Deserialize(InternalBuffer); NetworkObject.AddSceneObject(sceneObject, InternalBuffer, m_NetworkManager); } + // Now deserialize the despawned in-scene placed NetworkObjects list (if any) + DeserializeDespawnedInScenePlacedNetworkObjects(); } finally { @@ -746,6 +757,84 @@ internal void WriteClientSynchronizationResults(FastBufferWriter writer) } } + /// + /// For synchronizing any despawned in-scene placed NetworkObjects that were + /// despawned by the server during synchronization or scene loading + /// + private void DeserializeDespawnedInScenePlacedNetworkObjects() + { + // Process all de-spawned in-scene NetworkObjects for this network session + m_DespawnedInSceneObjects.Clear(); + InternalBuffer.ReadValueSafe(out int despawnedObjectsCount); + var sceneCache = new Dictionary>(); + + for (int i = 0; i < despawnedObjectsCount; i++) + { + // We just need to get the scene + ByteUnpacker.ReadValuePacked(InternalBuffer, out int networkSceneHandle); + ByteUnpacker.ReadValuePacked(InternalBuffer, out uint globalObjectIdHash); + var sceneRelativeNetworkObjects = new Dictionary(); + if (!sceneCache.ContainsKey(networkSceneHandle)) + { + if (m_NetworkManager.SceneManager.ServerSceneHandleToClientSceneHandle.ContainsKey(networkSceneHandle)) + { + var localSceneHandle = m_NetworkManager.SceneManager.ServerSceneHandleToClientSceneHandle[networkSceneHandle]; + if (m_NetworkManager.SceneManager.ScenesLoaded.ContainsKey(localSceneHandle)) + { + var objectRelativeScene = m_NetworkManager.SceneManager.ScenesLoaded[localSceneHandle]; + + // Find all active and non-active in-scene placed NetworkObjects + var inSceneNetworkObjects = UnityEngine.Object.FindObjectsOfType(includeInactive: true).Where((c) => + c.GetSceneOriginHandle() == localSceneHandle && (c.IsSceneObject != false)).ToList(); + + foreach (var inSceneObject in inSceneNetworkObjects) + { + if (!sceneRelativeNetworkObjects.ContainsKey(inSceneObject.GlobalObjectIdHash)) + { + sceneRelativeNetworkObjects.Add(inSceneObject.GlobalObjectIdHash, inSceneObject); + } + } + // Add this to a cache so we don't have to run this potentially multiple times (nothing will spawn or despawn during this time + sceneCache.Add(networkSceneHandle, sceneRelativeNetworkObjects); + } + else + { + UnityEngine.Debug.LogError($"In-Scene NetworkObject GlobalObjectIdHash ({globalObjectIdHash}) cannot find its relative local scene handle {localSceneHandle}!"); + } + } + else + { + UnityEngine.Debug.LogError($"In-Scene NetworkObject GlobalObjectIdHash ({globalObjectIdHash}) cannot find its relative NetworkSceneHandle {networkSceneHandle}!"); + } + } + else // Use the cached NetworkObjects if they exist + { + sceneRelativeNetworkObjects = sceneCache[networkSceneHandle]; + } + + // Now find the in-scene NetworkObject with the current GlobalObjectIdHash we are looking for + if (sceneRelativeNetworkObjects.ContainsKey(globalObjectIdHash)) + { + // Since this is a NetworkObject that was never spawned, we just need to send a notification + // out that it was despawned so users can make adjustments + sceneRelativeNetworkObjects[globalObjectIdHash].InvokeBehaviourNetworkDespawn(); + if (!m_NetworkManager.SceneManager.ScenePlacedObjects.ContainsKey(globalObjectIdHash)) + { + m_NetworkManager.SceneManager.ScenePlacedObjects.Add(globalObjectIdHash, new Dictionary()); + } + + if (!m_NetworkManager.SceneManager.ScenePlacedObjects[globalObjectIdHash].ContainsKey(sceneRelativeNetworkObjects[globalObjectIdHash].GetSceneOriginHandle())) + { + m_NetworkManager.SceneManager.ScenePlacedObjects[globalObjectIdHash].Add(sceneRelativeNetworkObjects[globalObjectIdHash].GetSceneOriginHandle(), sceneRelativeNetworkObjects[globalObjectIdHash]); + } + } + else + { + UnityEngine.Debug.LogError($"In-Scene NetworkObject GlobalObjectIdHash ({globalObjectIdHash}) could not be found!"); + } + } + } + /// /// Client Side: /// During the processing of a server sent Event_Sync, this method will be called for each scene once @@ -779,72 +868,9 @@ internal void SynchronizeSceneNetworkObjects(NetworkManager networkManager) } } - // Process all de-spawned in-scene NetworkObjects for this network session - m_DespawnedInSceneObjects.Clear(); - InternalBuffer.ReadValueSafe(out int despawnedObjectsCount); - var sceneCache = new Dictionary>(); - - for (int i = 0; i < despawnedObjectsCount; i++) - { - // We just need to get the scene - ByteUnpacker.ReadValuePacked(InternalBuffer, out int networkSceneHandle); - ByteUnpacker.ReadValuePacked(InternalBuffer, out uint globalObjectIdHash); - var sceneRelativeNetworkObjects = new Dictionary(); - if (!sceneCache.ContainsKey(networkSceneHandle)) - { - if (m_NetworkManager.SceneManager.ServerSceneHandleToClientSceneHandle.ContainsKey(networkSceneHandle)) - { - var localSceneHandle = m_NetworkManager.SceneManager.ServerSceneHandleToClientSceneHandle[networkSceneHandle]; - if (m_NetworkManager.SceneManager.ScenesLoaded.ContainsKey(localSceneHandle)) - { - var objectRelativeScene = m_NetworkManager.SceneManager.ScenesLoaded[localSceneHandle]; - var inSceneNetworkObjects = UnityEngine.Object.FindObjectsOfType().Where((c) => - c.GetSceneOriginHandle() == localSceneHandle && (c.IsSceneObject != false)).ToList(); + // Now deserialize the despawned in-scene placed NetworkObjects list (if any) + DeserializeDespawnedInScenePlacedNetworkObjects(); - foreach (var inSceneObject in inSceneNetworkObjects) - { - sceneRelativeNetworkObjects.Add(inSceneObject.GlobalObjectIdHash, inSceneObject); - } - // Add this to a cache so we don't have to run this potentially multiple times (nothing will spawn or despawn during this time - sceneCache.Add(networkSceneHandle, sceneRelativeNetworkObjects); - } - else - { - UnityEngine.Debug.LogError($"In-Scene NetworkObject GlobalObjectIdHash ({globalObjectIdHash}) cannot find its relative local scene handle {localSceneHandle}!"); - } - } - else - { - UnityEngine.Debug.LogError($"In-Scene NetworkObject GlobalObjectIdHash ({globalObjectIdHash}) cannot find its relative NetworkSceneHandle {networkSceneHandle}!"); - } - } - else // Use the cached NetworkObjects if they exist - { - sceneRelativeNetworkObjects = sceneCache[networkSceneHandle]; - } - - // Now find the in-scene NetworkObject with the current GlobalObjectIdHash we are looking for - if (sceneRelativeNetworkObjects.ContainsKey(globalObjectIdHash)) - { - // Since this is a NetworkObject that was never spawned, we just need to send a notification - // out that it was despawned so users can make adjustments - sceneRelativeNetworkObjects[globalObjectIdHash].InvokeBehaviourNetworkDespawn(); - if (!m_NetworkManager.SceneManager.ScenePlacedObjects.ContainsKey(globalObjectIdHash)) - { - m_NetworkManager.SceneManager.ScenePlacedObjects.Add(globalObjectIdHash, new Dictionary()); - } - - if (!m_NetworkManager.SceneManager.ScenePlacedObjects[globalObjectIdHash].ContainsKey(sceneRelativeNetworkObjects[globalObjectIdHash].GetSceneOriginHandle())) - { - m_NetworkManager.SceneManager.ScenePlacedObjects[globalObjectIdHash].Add(sceneRelativeNetworkObjects[globalObjectIdHash].GetSceneOriginHandle(), sceneRelativeNetworkObjects[globalObjectIdHash]); - } - - } - else - { - UnityEngine.Debug.LogError($"In-Scene NetworkObject GlobalObjectIdHash ({globalObjectIdHash}) could not be found!"); - } - } } finally { diff --git a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs index 175e21a451..5279496443 100644 --- a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs @@ -388,6 +388,13 @@ internal NetworkObject CreateLocalNetworkObject(NetworkObject.SceneObject sceneO NetworkLog.LogError($"{nameof(NetworkPrefab)} hash was not found! In-Scene placed {nameof(NetworkObject)} soft synchronization failure for Hash: {globalObjectIdHash}!"); } } + + // Since this NetworkObject is an in-scene placed NetworkObject, if it is disabled then enable it so + // NetworkBehaviours will have their OnNetworkSpawn method invoked + if (networkObject != null && !networkObject.gameObject.activeInHierarchy) + { + networkObject.gameObject.SetActive(true); + } } if (networkObject != null) diff --git a/testproject/Assets/Tests/Manual/SceneTransitioningAdditive/DespawnInSceneNetworkObject.cs b/testproject/Assets/Tests/Manual/SceneTransitioningAdditive/DespawnInSceneNetworkObject.cs index 825bfb86a3..e14af520f5 100644 --- a/testproject/Assets/Tests/Manual/SceneTransitioningAdditive/DespawnInSceneNetworkObject.cs +++ b/testproject/Assets/Tests/Manual/SceneTransitioningAdditive/DespawnInSceneNetworkObject.cs @@ -8,79 +8,82 @@ namespace TestProject.ManualTests /// /// Used for manually testing spawning and despawning in-scene /// placed NetworkObjects - /// - /// Note: We do not destroy in-scene placed NetworkObjects, but - /// users must handle visibility (rendering wise) when the in-scene - /// NetworkObject is spawned and despawned. This class just enables - /// or disabled the mesh renderer. /// public class DespawnInSceneNetworkObject : NetworkBehaviour { + [Tooltip("When set, the server will despawn the NetworkObject upon its first spawn.")] + public bool StartDespawned; + private Coroutine m_ScanInputHandle; - private MeshRenderer m_MeshRenderer; - private void Start() - { - if (!IsSpawned) - { - m_MeshRenderer = GetComponent(); - if (m_MeshRenderer != null) - { - m_MeshRenderer.enabled = false; - } - } - } + // Used to prevent the server from despawning + // the in-scene placed NetworkObject after the + // first spawn (only if StartDespawned is true) + private bool m_ServerDespawnedOnFirstSpawn; + + private NetworkManager m_CachedNetworkManager; public override void OnNetworkSpawn() { Debug.Log($"{name} spawned!"); - m_MeshRenderer = GetComponent(); - if (m_MeshRenderer != null) - { - m_MeshRenderer.enabled = true; - } + if (!IsServer) { return; } + + m_CachedNetworkManager = NetworkManager; + if (m_ScanInputHandle == null) { - m_ScanInputHandle = StartCoroutine(ScanInput()); + // Using the NetworkManager to create the coroutine so it is not deactivated + // when the GameObject this NetworkBehaviour is attached to is disabled. + m_ScanInputHandle = NetworkManager.StartCoroutine(ScanInput(NetworkObject)); + } + + // m_ServerDespawnedOnFirstSpawn prevents the server from always + // despawning on the server-side after the first spawn. + if (StartDespawned && !m_ServerDespawnedOnFirstSpawn) + { + m_ServerDespawnedOnFirstSpawn = true; + NetworkObject.Despawn(false); } } public override void OnNetworkDespawn() { - if (m_MeshRenderer != null) - { - m_MeshRenderer.enabled = false; - } + // It is OK to disable in-scene placed NetworkObjects upon + // despawning. When re-spawned the client-side will re-activate + // the GameObject, while the server-side must set the GameObject + // active itself. + gameObject.SetActive(false); + Debug.Log($"{name} despawned!"); base.OnNetworkDespawn(); } public override void OnDestroy() { - if (m_ScanInputHandle != null) + if (m_ScanInputHandle != null && m_CachedNetworkManager != null) { - StopCoroutine(m_ScanInputHandle); + m_CachedNetworkManager.StopCoroutine(m_ScanInputHandle); } m_ScanInputHandle = null; base.OnDestroy(); } - private IEnumerator ScanInput() + private IEnumerator ScanInput(NetworkObject networkObject) { while (true) { try { - if (IsSpawned) + if (networkObject.IsSpawned) { if (Input.GetKeyDown(KeyCode.Backspace)) { Debug.Log($"{name} should despawn."); - NetworkObject.Despawn(false); + networkObject.Despawn(false); } } else if (NetworkManager.Singleton && NetworkManager.Singleton.IsListening) @@ -88,7 +91,8 @@ private IEnumerator ScanInput() if (Input.GetKeyDown(KeyCode.Backspace)) { Debug.Log($"{name} should spawn."); - NetworkObject.Spawn(); + networkObject.gameObject.SetActive(true); + networkObject.Spawn(); } } } @@ -99,7 +103,6 @@ private IEnumerator ScanInput() yield return null; } - } } } diff --git a/testproject/Assets/Tests/Manual/SceneTransitioningAdditive/SceneTransitioningBase1.unity b/testproject/Assets/Tests/Manual/SceneTransitioningAdditive/SceneTransitioningBase1.unity index 3288c65aaf..de7c662a05 100644 --- a/testproject/Assets/Tests/Manual/SceneTransitioningAdditive/SceneTransitioningBase1.unity +++ b/testproject/Assets/Tests/Manual/SceneTransitioningAdditive/SceneTransitioningBase1.unity @@ -2250,6 +2250,11 @@ PrefabInstance: propertyPath: m_Name value: InSceneObjectToDespawn objectReference: {fileID: 0} + - target: {fileID: 4518755925279129999, guid: 3a854a190ab5b1b4fb00bec725fdda9e, + type: 3} + propertyPath: StartDespawned + value: 1 + objectReference: {fileID: 0} m_RemovedComponents: [] m_SourcePrefab: {fileID: 100100000, guid: 3a854a190ab5b1b4fb00bec725fdda9e, type: 3} --- !u!1 &1008611498 @@ -2466,7 +2471,6 @@ MonoBehaviour: m_ProtocolType: 0 m_MaxPacketQueueSize: 128 m_MaxPayloadSize: 512000 - m_MaxSendQueueSize: 4096000 m_HeartbeatTimeoutMS: 500 m_ConnectTimeoutMS: 1000 m_MaxConnectAttempts: 60 diff --git a/testproject/Assets/Tests/Runtime/NetworkSceneManager/InScenePlacedNetworkObjectTests.cs b/testproject/Assets/Tests/Runtime/NetworkSceneManager/InScenePlacedNetworkObjectTests.cs index 3d02ff0689..2425576d7c 100644 --- a/testproject/Assets/Tests/Runtime/NetworkSceneManager/InScenePlacedNetworkObjectTests.cs +++ b/testproject/Assets/Tests/Runtime/NetworkSceneManager/InScenePlacedNetworkObjectTests.cs @@ -1,4 +1,5 @@ using System.Collections; +using System.Collections.Generic; using System.Linq; using UnityEngine; using NUnit.Framework; @@ -21,8 +22,7 @@ public class InScenePlacedNetworkObjectTests : NetcodeIntegrationTest protected override IEnumerator OnSetup() { - NetworkObjectTestComponent.ServerNetworkObjectInstance = null; - NetworkObjectTestComponent.SpawnedInstances.Clear(); + NetworkObjectTestComponent.Reset(); m_CanStartServerAndClients = false; return base.OnSetup(); } @@ -181,7 +181,6 @@ public IEnumerator ParentedInSceneObjectLateJoiningClient() AssertOnTimeout($"Timed out waiting for the client-side id ({m_ClientNetworkManagers[0].LocalClientId}) server player transform to be set on the client-side in-scene object!"); } - private void OnSceneEvent(SceneEvent sceneEvent) { if (sceneEvent.SceneEventType == SceneEventType.LoadComplete && sceneEvent.SceneName == k_SceneToLoad && sceneEvent.ClientId == m_ClientNetworkManagers[0].LocalClientId) @@ -228,6 +227,175 @@ private void Unload_OnSceneEvent(SceneEvent sceneEvent) } } + + private bool m_AllClientsLoadedScene; + private bool m_AllClientsUnloadedScene; + + private int m_NumberOfInstancesCheck; + + private Scene m_SceneLoaded; + + private bool HaveAllClientsDespawnedInSceneObject() + { + // Make sure we despawned all instances + if (NetworkObjectTestComponent.DespawnedInstances.Count < m_NumberOfInstancesCheck) + { + return false; + } + + foreach (var despawnedInstance in NetworkObjectTestComponent.DespawnedInstances) + { + if (despawnedInstance.gameObject.activeInHierarchy) + { + return false; + } + } + + return true; + } + + private bool HaveAllClientsSpawnedInSceneObject() + { + // Make sure we despawned all instances + if (NetworkObjectTestComponent.SpawnedInstances.Count < m_NumberOfInstancesCheck) + { + return false; + } + + foreach (var despawnedInstance in NetworkObjectTestComponent.SpawnedInstances) + { + if (!despawnedInstance.gameObject.activeInHierarchy) + { + return false; + } + } + + return true; + } + + /// + /// This validates that users can despawn in-scene placed NetworkObjects and disable the + /// associated GameObject when OnNetworkDespawn is invoked while still being able to + /// re-spawn the same in-scene placed NetworkObject. + /// This test validates this for: + /// - Currently connected clients + /// - Late joining client + /// - Scene switching and having the server despawn the NetworkObject the first time it is spawned. + /// + [UnityTest] + public IEnumerator EnableDisableInSceneObjectTests() + { + NetworkObjectTestComponent.ServerNetworkObjectInstance = null; + // Enabled disabling the NetworkObject when it is despawned + NetworkObjectTestComponent.DisableOnDespawn = true; + // Set the number of instances to expect + m_NumberOfInstancesCheck = NumberOfClients + (m_UseHost ? 1 : 0); + + // Start the host and clients and load the in-scene object scene additively + m_CanStartServerAndClients = true; + yield return StartServerAndClients(); + m_ServerNetworkManager.SceneManager.OnLoadEventCompleted += SceneManager_OnLoadEventCompleted; + m_ServerNetworkManager.SceneManager.LoadScene(k_SceneToLoad, LoadSceneMode.Additive); + yield return WaitForConditionOrTimeOut(() => m_AllClientsLoadedScene); + AssertOnTimeout($"Timed out waiting for {k_SceneToLoad} scene to be loaded on all clients!"); + m_ServerNetworkManager.SceneManager.OnLoadEventCompleted -= SceneManager_OnLoadEventCompleted; + + // Verify all connected clients spawned the in-scene placed NetworkObject + yield return WaitForConditionOrTimeOut(HaveAllClientsSpawnedInSceneObject); + AssertOnTimeout($"Timed out waiting for all instances to be spawned and enabled!"); + + var serverInSceneObjectInstance = NetworkObjectTestComponent.ServerNetworkObjectInstance; + Assert.IsNotNull(serverInSceneObjectInstance, $"Could not get the server-side registration of {nameof(NetworkObjectTestComponent)}!"); + + // Test #1: Despawn the in-scene placed NetworkObject and verify it is despawned and disabled on the clients + serverInSceneObjectInstance.Despawn(false); + + yield return WaitForConditionOrTimeOut(HaveAllClientsDespawnedInSceneObject); + AssertOnTimeout($"[Test #1] Timed out waiting for all instances to be despawned and disabled!"); + + // Test #2: Late-join a client and re-verify that all in-scene placed object instances are still disabled + yield return CreateAndStartNewClient(); + + var newlyJoinedClient = m_ClientNetworkManagers[NumberOfClients]; + + m_NumberOfInstancesCheck++; + yield return WaitForConditionOrTimeOut(HaveAllClientsDespawnedInSceneObject); + AssertOnTimeout($"[Test #2] Timed out waiting for all instances to be despawned and disabled!"); + + // Test #3: Now spawn the same in-scene placed NetworkObject + serverInSceneObjectInstance.gameObject.SetActive(true); + serverInSceneObjectInstance.Spawn(); + yield return WaitForConditionOrTimeOut(HaveAllClientsSpawnedInSceneObject); + AssertOnTimeout($"[Test #2] Timed out waiting for all instances to be enabled and spawned!"); + + // Test #4: Now unload the in-scene object's scene and scene switch to the same scene while + // also having the server-side disable the in-scene placed NetworkObject and verify all + // connected clients completed the scene switch and that all in-scene placed NetworkObjects + // are despawned and disabled. + m_AllClientsLoadedScene = false; + m_AllClientsUnloadedScene = false; + + NetworkObjectTestComponent.ServerNetworkObjectInstance = null; + NetworkObjectTestComponent.DisableOnSpawn = true; + m_ServerNetworkManager.SceneManager.OnUnloadEventCompleted += SceneManager_OnUnloadEventCompleted; + m_ServerNetworkManager.SceneManager.UnloadScene(m_SceneLoaded); + yield return WaitForConditionOrTimeOut(() => m_AllClientsUnloadedScene); + AssertOnTimeout($"Timed out waiting for {k_SceneToLoad} scene to be unloaded on all clients!"); + m_ServerNetworkManager.SceneManager.OnUnloadEventCompleted -= SceneManager_OnUnloadEventCompleted; + + // Verify the spawned instances list is empty + Assert.True(NetworkObjectTestComponent.SpawnedInstances.Count == 0, $"There are {NetworkObjectTestComponent.SpawnedInstances.Count} that did not despawn when the scene was unloaded!"); + + // Go ahead and clear out the despawned instances list + NetworkObjectTestComponent.DespawnedInstances.Clear(); + + // Now scene switch (LoadSceneMode.Single) into the scene with the in-scene placed NetworkObject we have been testing + m_ServerNetworkManager.SceneManager.OnLoadEventCompleted += SceneManager_OnLoadEventCompleted; + m_ServerNetworkManager.SceneManager.LoadScene(k_SceneToLoad, LoadSceneMode.Single); + yield return WaitForConditionOrTimeOut(() => m_AllClientsLoadedScene); + AssertOnTimeout($"Timed out waiting for {k_SceneToLoad} scene to be loaded on all clients!"); + m_ServerNetworkManager.SceneManager.OnLoadEventCompleted -= SceneManager_OnLoadEventCompleted; + + // Verify all client instances are disabled and despawned when done scene switching + yield return WaitForConditionOrTimeOut(HaveAllClientsDespawnedInSceneObject); + AssertOnTimeout($"[Test #4] Timed out waiting for all instances to be despawned and disabled!"); + + serverInSceneObjectInstance = NetworkObjectTestComponent.ServerNetworkObjectInstance; + Assert.IsNotNull(serverInSceneObjectInstance, $"[Test #4] Could not get the server-side registration of {nameof(NetworkObjectTestComponent)}!"); + + // Test #5: Now spawn the in-scene placed NetworkObject + serverInSceneObjectInstance.gameObject.SetActive(true); + serverInSceneObjectInstance.Spawn(); + + // Verify all clients spawned their in-scene NetworkObject relative instance + yield return WaitForConditionOrTimeOut(HaveAllClientsSpawnedInSceneObject); + AssertOnTimeout($"[Test #2] Timed out waiting for all instances to be enabled and spawned!"); + yield return StopOneClient(newlyJoinedClient, true); + + // Tests complete! + } + + private void SceneManager_OnUnloadEventCompleted(string sceneName, LoadSceneMode loadSceneMode, List clientsCompleted, List clientsTimedOut) + { + foreach (var clientId in clientsCompleted) + { + Assert.True(m_ServerNetworkManager.ConnectedClientsIds.Contains(clientId)); + } + m_AllClientsUnloadedScene = true; + } + + private void SceneManager_OnLoadEventCompleted(string sceneName, LoadSceneMode loadSceneMode, List clientsCompleted, List clientsTimedOut) + { + foreach (var clientId in clientsCompleted) + { + Assert.True(m_ServerNetworkManager.ConnectedClientsIds.Contains(clientId)); + } + m_AllClientsLoadedScene = true; + m_SceneLoaded = SceneManager.GetSceneByName(sceneName); + } + + + /// /// Very important to always have a backup "unloading" catch /// in the event your test fails it could not potentially unload diff --git a/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkObjectTestComponent.cs b/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkObjectTestComponent.cs index 978e3093ed..89c6b3728b 100644 --- a/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkObjectTestComponent.cs +++ b/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkObjectTestComponent.cs @@ -12,16 +12,44 @@ namespace TestProject.RuntimeTests /// public class NetworkObjectTestComponent : NetworkBehaviour { + public static bool DisableOnDespawn; + public static bool DisableOnSpawn; public static NetworkObject ServerNetworkObjectInstance; public static List SpawnedInstances = new List(); + public static List DespawnedInstances = new List(); + public static void Reset() + { + DisableOnDespawn = false; + DisableOnSpawn = false; + ServerNetworkObjectInstance = null; + SpawnedInstances.Clear(); + DespawnedInstances.Clear(); + } + + // When disabling on spawning we only want this to happen on the initial spawn. + // This is used to track this so the server only does it once upon spawning. + public bool ObjectWasDisabledUponSpawn; public override void OnNetworkSpawn() { + SpawnedInstances.Add(this); + if (DisableOnDespawn) + { + if (DespawnedInstances.Contains(this)) + { + DespawnedInstances.Remove(this); + } + } + if (IsServer) { ServerNetworkObjectInstance = NetworkObject; + if (DisableOnSpawn && !ObjectWasDisabledUponSpawn) + { + NetworkObject.Despawn(false); + ObjectWasDisabledUponSpawn = true; + } } - SpawnedInstances.Add(this); base.OnNetworkSpawn(); } @@ -33,6 +61,11 @@ public override void OnNetworkDespawn() m_HasNotifiedSpawned = false; Debug.Log($"{NetworkManager.name} de-spawned {gameObject.name}."); SpawnedInstances.Remove(this); + if (DisableOnDespawn) + { + DespawnedInstances.Add(this); + gameObject.SetActive(false); + } base.OnNetworkDespawn(); }