From c8e74c826cac6c359e1305132000aba38647d484 Mon Sep 17 00:00:00 2001 From: NoelStephensUnity <73188597+NoelStephensUnity@users.noreply.github.com> Date: Wed, 5 Oct 2022 16:51:07 -0500 Subject: [PATCH 1/8] fix This resolves the issue with in-scene placed NetworkObjects that are disabled when despawned not being able to re-spawn again. --- .../Runtime/SceneManagement/SceneEventData.cs | 12 +++++++++--- .../Runtime/Spawning/NetworkSpawnManager.cs | 7 +++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs b/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs index ae8d5a31a9..55136ca792 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(true).Where((c) => c.NetworkManager == m_NetworkManager); foreach (var sobj in inSceneNetworkObjects) { if (sobj.IsSceneObject.HasValue && sobj.IsSceneObject.Value && !sobj.IsSpawned) @@ -798,12 +799,17 @@ internal void SynchronizeSceneNetworkObjects(NetworkManager networkManager) if (m_NetworkManager.SceneManager.ScenesLoaded.ContainsKey(localSceneHandle)) { var objectRelativeScene = m_NetworkManager.SceneManager.ScenesLoaded[localSceneHandle]; - var inSceneNetworkObjects = UnityEngine.Object.FindObjectsOfType().Where((c) => + + // Find all active and non-active in-scene placed NetworkObjects + var inSceneNetworkObjects = UnityEngine.Object.FindObjectsOfType(true).Where((c) => c.GetSceneOriginHandle() == localSceneHandle && (c.IsSceneObject != false)).ToList(); foreach (var inSceneObject in inSceneNetworkObjects) { - sceneRelativeNetworkObjects.Add(inSceneObject.GlobalObjectIdHash, inSceneObject); + 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); diff --git a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs index 175e21a451..aaf3b5c476 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.gameObject.activeInHierarchy) + { + networkObject.gameObject.SetActive(true); + } } if (networkObject != null) From d022eaf9145f4e953097927ef1ded3ae72cc4eab Mon Sep 17 00:00:00 2001 From: NoelStephensUnity <73188597+NoelStephensUnity@users.noreply.github.com> Date: Wed, 5 Oct 2022 17:25:52 -0500 Subject: [PATCH 2/8] test - manual Minor adjustments to manually validate the fixes. Integration test will follow. --- .../DespawnInSceneNetworkObject.cs | 65 +++++++++---------- .../SceneTransitioningBase1.unity | 6 +- 2 files changed, 37 insertions(+), 34 deletions(-) diff --git a/testproject/Assets/Tests/Manual/SceneTransitioningAdditive/DespawnInSceneNetworkObject.cs b/testproject/Assets/Tests/Manual/SceneTransitioningAdditive/DespawnInSceneNetworkObject.cs index 825bfb86a3..9c02a4d780 100644 --- a/testproject/Assets/Tests/Manual/SceneTransitioningAdditive/DespawnInSceneNetworkObject.cs +++ b/testproject/Assets/Tests/Manual/SceneTransitioningAdditive/DespawnInSceneNetworkObject.cs @@ -8,53 +8,52 @@ 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; public override void OnNetworkSpawn() { Debug.Log($"{name} spawned!"); - m_MeshRenderer = GetComponent(); - if (m_MeshRenderer != null) - { - m_MeshRenderer.enabled = true; - } + if (!IsServer) { return; } + 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(); } @@ -63,24 +62,24 @@ public override void OnDestroy() { if (m_ScanInputHandle != null) { - StopCoroutine(m_ScanInputHandle); + NetworkManager.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 +87,8 @@ private IEnumerator ScanInput() if (Input.GetKeyDown(KeyCode.Backspace)) { Debug.Log($"{name} should spawn."); - NetworkObject.Spawn(); + networkObject.gameObject.SetActive(true); + networkObject.Spawn(); } } } @@ -99,7 +99,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 From 69078115d23c9f6c6f8dd38e8f8dc1f349e6e7e3 Mon Sep 17 00:00:00 2001 From: NoelStephensUnity <73188597+NoelStephensUnity@users.noreply.github.com> Date: Wed, 5 Oct 2022 18:35:15 -0500 Subject: [PATCH 3/8] fix This update handles synchronizing despawned in-scene placed NetworkObjects during a scene switch (LoadSceneMode.Single). Also fixes an exception when no in-scene placed NetworkObject is found. --- .../SceneManagement/NetworkSceneManager.cs | 3 + .../Runtime/SceneManagement/SceneEventData.cs | 162 ++++++++++-------- .../Runtime/Spawning/NetworkSpawnManager.cs | 2 +- 3 files changed, 95 insertions(+), 72 deletions(-) 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 55136ca792..eb438700d5 100644 --- a/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs +++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs @@ -462,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; @@ -508,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); @@ -625,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 { @@ -747,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(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 @@ -780,77 +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]; - - // Find all active and non-active in-scene placed NetworkObjects - var inSceneNetworkObjects = UnityEngine.Object.FindObjectsOfType(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()); - } + // Now deserialize the despawned in-scene placed NetworkObjects list (if any) + DeserializeDespawnedInScenePlacedNetworkObjects(); - 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 aaf3b5c476..5279496443 100644 --- a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs @@ -391,7 +391,7 @@ internal NetworkObject CreateLocalNetworkObject(NetworkObject.SceneObject sceneO // 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.gameObject.activeInHierarchy) + if (networkObject != null && !networkObject.gameObject.activeInHierarchy) { networkObject.gameObject.SetActive(true); } From f04669494cb3cfa4b289fa69f7d9139b544df20f Mon Sep 17 00:00:00 2001 From: NoelStephensUnity <73188597+NoelStephensUnity@users.noreply.github.com> Date: Wed, 5 Oct 2022 18:37:48 -0500 Subject: [PATCH 4/8] test - manual Cache the NetworkManager to avoid exception during OnDestroy. --- .../DespawnInSceneNetworkObject.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/testproject/Assets/Tests/Manual/SceneTransitioningAdditive/DespawnInSceneNetworkObject.cs b/testproject/Assets/Tests/Manual/SceneTransitioningAdditive/DespawnInSceneNetworkObject.cs index 9c02a4d780..e14af520f5 100644 --- a/testproject/Assets/Tests/Manual/SceneTransitioningAdditive/DespawnInSceneNetworkObject.cs +++ b/testproject/Assets/Tests/Manual/SceneTransitioningAdditive/DespawnInSceneNetworkObject.cs @@ -21,6 +21,8 @@ public class DespawnInSceneNetworkObject : NetworkBehaviour // first spawn (only if StartDespawned is true) private bool m_ServerDespawnedOnFirstSpawn; + private NetworkManager m_CachedNetworkManager; + public override void OnNetworkSpawn() { Debug.Log($"{name} spawned!"); @@ -30,6 +32,8 @@ public override void OnNetworkSpawn() return; } + m_CachedNetworkManager = NetworkManager; + if (m_ScanInputHandle == null) { // Using the NetworkManager to create the coroutine so it is not deactivated @@ -60,9 +64,9 @@ public override void OnNetworkDespawn() public override void OnDestroy() { - if (m_ScanInputHandle != null) + if (m_ScanInputHandle != null && m_CachedNetworkManager != null) { - NetworkManager.StopCoroutine(m_ScanInputHandle); + m_CachedNetworkManager.StopCoroutine(m_ScanInputHandle); } m_ScanInputHandle = null; base.OnDestroy(); From 8728c0daf4c75890fcb0d9f05c5a9d3679f8ae31 Mon Sep 17 00:00:00 2001 From: NoelStephensUnity <73188597+NoelStephensUnity@users.noreply.github.com> Date: Thu, 6 Oct 2022 12:53:22 -0500 Subject: [PATCH 5/8] test Added integration test that validates disabling NetworkObjects when despawned works with currently connected clients, late joining clients, and when scene switching (LoadSceneMode.Single) while also having the server despawn the in-scene placed NetworkObject upon its first spawn (i.e. so it starts off not visible/active to the clients when they finish the scene switch). --- .../InScenePlacedNetworkObjectTests.cs | 168 +++++++++++++++++- .../NetworkObjectTestComponent.cs | 26 ++- 2 files changed, 192 insertions(+), 2 deletions(-) diff --git a/testproject/Assets/Tests/Runtime/NetworkSceneManager/InScenePlacedNetworkObjectTests.cs b/testproject/Assets/Tests/Runtime/NetworkSceneManager/InScenePlacedNetworkObjectTests.cs index 3d02ff0689..1651da7b96 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; @@ -181,7 +182,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 +228,172 @@ 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(); + + 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!"); + + // 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..015c29fcb1 100644 --- a/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkObjectTestComponent.cs +++ b/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkObjectTestComponent.cs @@ -12,16 +12,35 @@ 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(); + // 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 +52,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(); } From c5ca7e84281dbe89a91ccbfa0d1d4e318e5f10b5 Mon Sep 17 00:00:00 2001 From: NoelStephensUnity <73188597+NoelStephensUnity@users.noreply.github.com> Date: Thu, 6 Oct 2022 12:56:12 -0500 Subject: [PATCH 6/8] update MTT-4832 --- com.unity.netcode.gameobjects/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) 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) From 1743080b8dd6bae443b212176e58b0fe20cb520f Mon Sep 17 00:00:00 2001 From: NoelStephensUnity <73188597+NoelStephensUnity@users.noreply.github.com> Date: Thu, 6 Oct 2022 14:35:02 -0500 Subject: [PATCH 7/8] fix This fixes the issue with the late joining client not being destroyed and removed as well as the issue with not resetting the new properties added to NetworkObjectTestComponent. --- .../InScenePlacedNetworkObjectTests.cs | 6 ++++-- .../NetworkSceneManager/NetworkObjectTestComponent.cs | 9 +++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/testproject/Assets/Tests/Runtime/NetworkSceneManager/InScenePlacedNetworkObjectTests.cs b/testproject/Assets/Tests/Runtime/NetworkSceneManager/InScenePlacedNetworkObjectTests.cs index 1651da7b96..2425576d7c 100644 --- a/testproject/Assets/Tests/Runtime/NetworkSceneManager/InScenePlacedNetworkObjectTests.cs +++ b/testproject/Assets/Tests/Runtime/NetworkSceneManager/InScenePlacedNetworkObjectTests.cs @@ -22,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(); } @@ -317,6 +316,8 @@ public IEnumerator EnableDisableInSceneObjectTests() // 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!"); @@ -369,6 +370,7 @@ public IEnumerator EnableDisableInSceneObjectTests() // 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! } diff --git a/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkObjectTestComponent.cs b/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkObjectTestComponent.cs index 015c29fcb1..89c6b3728b 100644 --- a/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkObjectTestComponent.cs +++ b/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkObjectTestComponent.cs @@ -18,6 +18,15 @@ public class NetworkObjectTestComponent : NetworkBehaviour 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; From 177242962c57327458c66e5e7589bcaad3a21b9c Mon Sep 17 00:00:00 2001 From: NoelStephensUnity <73188597+NoelStephensUnity@users.noreply.github.com> Date: Thu, 6 Oct 2022 16:09:36 -0500 Subject: [PATCH 8/8] update adding includeInactive: to FindObjectsOfType where this was changed to true in order to make it easier for reviewers to spot the change. --- .../Runtime/SceneManagement/SceneEventData.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs b/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs index eb438700d5..222e437ca2 100644 --- a/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs +++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs @@ -269,7 +269,7 @@ internal void AddDespawnedInSceneNetworkObjects() { m_DespawnedInSceneObjectsSync.Clear(); // Find all active and non-active in-scene placed NetworkObjects - var inSceneNetworkObjects = UnityEngine.Object.FindObjectsOfType(true).Where((c) => c.NetworkManager == m_NetworkManager); + 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) @@ -784,7 +784,7 @@ private void DeserializeDespawnedInScenePlacedNetworkObjects() var objectRelativeScene = m_NetworkManager.SceneManager.ScenesLoaded[localSceneHandle]; // Find all active and non-active in-scene placed NetworkObjects - var inSceneNetworkObjects = UnityEngine.Object.FindObjectsOfType(true).Where((c) => + var inSceneNetworkObjects = UnityEngine.Object.FindObjectsOfType(includeInactive: true).Where((c) => c.GetSceneOriginHandle() == localSceneHandle && (c.IsSceneObject != false)).ToList(); foreach (var inSceneObject in inSceneNetworkObjects)