diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs index 52e4dad4f1..f3d8e0e649 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs @@ -106,6 +106,12 @@ public uint PrefabIdHash private int m_SpawnCount; internal bool HasBeenSpawned => m_SpawnCount > 0; + /// + /// When true this object will be auto-spawned if it's instantiated before the NetworkManager starts. + /// Set to false to not spawn this object. + /// + internal bool AutoSpawnOnStart = true; + #if UNITY_EDITOR private const string k_GlobalIdTemplate = "GlobalObjectId_V1-{0}-{1}-{2}-{3}"; @@ -1449,10 +1455,16 @@ public bool IsNetworkVisibleTo(ulong clientId) /// internal NetworkSceneHandle SceneOriginHandle; + private NetworkSceneHandle m_TempHandle; /// /// The server-side scene origin handle /// - internal NetworkSceneHandle NetworkSceneHandle; + internal NetworkSceneHandle NetworkSceneHandle {get => m_TempHandle; + set + { + Debug.Log($"[{name}] setting NetworkSceneHandle! new value: {value}"); + m_TempHandle = value; + }} private Scene m_SceneOrigin; /// @@ -1877,28 +1889,7 @@ private void SpawnInternal(bool destroyWithScene, ulong ownerClientId, bool play } } - - // Calculate the legacy IsSceneObject value as the public field is obsolete with warning - // We can't break the public behavior of the field. -#pragma warning disable CS0618 // Type or member is obsolete - var legacyIsSceneObject = IsSceneObject.HasValue && IsSceneObject.Value; -#pragma warning restore CS0618 // Type or member is obsolete - - // If SpawnInternal is being called on an object that is marked as InScenePlaced, - // The scene object was never automatically spawned when the scene was loaded. - // Count this object as a dynamically spawned object. - // TODO-[MTT-15388]: Actually support disabled/not spawned InScenePlaced NetworkObjects - if (InScenePlaced && !HasBeenSpawned) - { - if (NetworkManagerOwner.NetworkConfig.EnableSceneManagement && NetworkManagerOwner.LogLevel <= LogLevel.Developer) - { - Debug.LogWarning($"[{name}][SceneOrigin={SceneOriginHandle}] Dynamically spawning InScenePlaced network object. This can cause issues!", this); - } - - InScenePlaced = false; - } - - if (!NetworkManagerOwner.SpawnManager.AuthorityLocalSpawn(this, NetworkManagerOwner.SpawnManager.GetNetworkObjectId(), legacyIsSceneObject, playerObject, ownerClientId, destroyWithScene)) + if (!NetworkManagerOwner.SpawnManager.AuthorityLocalSpawn(this, NetworkManagerOwner.SpawnManager.GetNetworkObjectId(), InScenePlaced, playerObject, ownerClientId, destroyWithScene)) { if (NetworkManagerOwner.LogLevel <= LogLevel.Normal) { @@ -3434,6 +3425,7 @@ internal void SynchronizeNetworkBehaviours(ref BufferSerializer serializer /// This function is the authority mirror of internal SerializedObject SerializeSpawnedObject(ulong targetClientId = NetworkManager.ServerClientId, bool syncObservers = false) { + Debug.Log($"[Client-{NetworkManager.LocalClientId}][{name}] Serializing spawned object. NetworkSceneHandle: {NetworkSceneHandle}, InScenePlaced: {InScenePlaced}"); var obj = new SerializedObject { HasParent = transform.parent != null, @@ -3517,7 +3509,7 @@ internal SerializedObject SerializeSpawnedObject(ulong targetClientId = NetworkM /// will be true if invoked by CreateObjectMessage /// The deserialized NetworkObject or null if deserialization failed [return: MaybeNull] - internal static NetworkObject DeserializeAndSpawnObject(in SerializedObject serializedObject, FastBufferReader reader, NetworkManager networkManager, bool invokedByMessage = false) + internal static NetworkObject DeserializeAndSpawnObject(in SerializedObject serializedObject, FastBufferReader reader, NetworkManager networkManager, bool invokedByMessage = false, NetworkObject existingObject = null) { var endOfSynchronizationData = reader.Position + serializedObject.SynchronizationDataSize; @@ -3528,8 +3520,12 @@ internal static NetworkObject DeserializeAndSpawnObject(in SerializedObject seri return null; } + if (existingObject != null) + { + Debug.Log($"Deserializing and spawning existing object: {existingObject.name}"); + } // Do the SpawnManager parts of the object spawn - var succeeded = networkManager.SpawnManager.NonAuthorityLocalSpawn(in serializedObject, out var networkObject, reader, serializedObject.DestroyWithScene); + var succeeded = networkManager.SpawnManager.NonAuthorityLocalSpawn(in serializedObject, out var networkObject, reader, serializedObject.DestroyWithScene, existingObject); // Process any deferred messages once the object is 100% finished spawning // Ensure this is done whether the spawn succeeds or fails diff --git a/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs b/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs index e3a5d2cfe4..b7ec847e6e 100644 --- a/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs @@ -411,6 +411,11 @@ public bool ActiveSceneSynchronizationEnabled /// internal ISceneManagerHandler SceneManagerHandler = new DefaultSceneManagerHandler(); + /// + /// The new tracker of what objects are in which scenes + /// + internal readonly SceneObjectStore SceneObjectStore = new(); + internal readonly Dictionary SceneEventProgressTracking = new Dictionary(); /// @@ -1005,12 +1010,11 @@ internal Scene GetAndAddNewlyLoadedSceneByName(string sceneName) /// value. Scene handles are used to distinguish between in-scene placed NetworkObjects under this situation. /// /// - internal void SetTheSceneBeingSynchronized(NetworkSceneHandle serverSceneHandle) + /// Used on initial synchronize because it's not bad to not + internal void SetTheSceneBeingSynchronized(NetworkSceneHandle serverSceneHandle, bool warnIfSceneMissing = true) { - var clientSceneHandle = serverSceneHandle; - if (ServerSceneHandleToClientSceneHandle.ContainsKey(serverSceneHandle)) + if (ServerSceneHandleToClientSceneHandle.TryGetValue(serverSceneHandle, out var clientSceneHandle)) { - clientSceneHandle = ServerSceneHandleToClientSceneHandle[serverSceneHandle]; // If we were already set, then ignore if (SceneBeingSynchronized.IsValid() && SceneBeingSynchronized.isLoaded && SceneBeingSynchronized.handle == clientSceneHandle) { @@ -1018,7 +1022,7 @@ internal void SetTheSceneBeingSynchronized(NetworkSceneHandle serverSceneHandle) } // Get the scene currently being synchronized - SceneBeingSynchronized = ScenesLoaded.ContainsKey(clientSceneHandle) ? ScenesLoaded[clientSceneHandle] : new Scene(); + SceneBeingSynchronized = ScenesLoaded.TryGetValue(clientSceneHandle, out var loadedScene) ? loadedScene : new Scene(); if (!SceneBeingSynchronized.IsValid() || !SceneBeingSynchronized.isLoaded) { @@ -1034,7 +1038,7 @@ internal void SetTheSceneBeingSynchronized(NetworkSceneHandle serverSceneHandle) // Most common scenario for DontDestroyOnLoad is when NetworkManager is set to not be destroyed if (serverSceneHandle == DontDestroyOnLoadScene.handle) { - SceneBeingSynchronized = DontDestroyOnLoadScene; + SceneBeingSynchronized = NetworkManager.gameObject.scene; return; } else @@ -1043,9 +1047,12 @@ internal void SetTheSceneBeingSynchronized(NetworkSceneHandle serverSceneHandle) // or the NetworkObject has yet to be moved to that specific scene (i.e. no DontDestroyOnLoad scene exists yet). SceneBeingSynchronized = SceneManager.GetActiveScene(); - // This could be the scenario where NetworkManager.DontDestroy is false and we are creating the first NetworkObject (client side) to be in the DontDestroyOnLoad scene - // Otherwise, this is some other specific scenario that we might not be handling currently. - Debug.LogWarning($"[{nameof(SceneEventData)}- Scene Handle Mismatch] {nameof(serverSceneHandle)} ({serverSceneHandle}) could not be found in {nameof(ServerSceneHandleToClientSceneHandle)}. Using the currently active scene."); + if (warnIfSceneMissing) + { + // This could be the scenario where NetworkManager.DontDestroy is false and we are creating the first NetworkObject (client side) to be in the DontDestroyOnLoad scene + // Otherwise, this is some other specific scenario that we might not be handling currently. + Debug.LogWarning($"[{nameof(SceneEventData)}- Scene Handle Mismatch] {nameof(serverSceneHandle)} ({serverSceneHandle}) could not be found in {nameof(ServerSceneHandleToClientSceneHandle)}. Using the currently active scene."); + } } } } @@ -1764,6 +1771,10 @@ private void OnSceneLoaded(uint sceneEventId) if (sceneEventData.LoadSceneMode == LoadSceneMode.Single) { SceneManager.SetActiveScene(nextScene); + + // The object store is now invalid because single scene loading will unload all other scenes + // Clear the store. It will be repopulated in PopulateScenePlacedObjects. + SceneObjectStore.ClearAllStoredObjects(); } if (NetworkManager.DistributedAuthorityMode) @@ -1951,21 +1962,6 @@ private void OnClientLoadedScene(uint sceneEventId, Scene scene) /// internal List ClientConnectionQueue = new List(); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void AddSceneToClientSynchronization(ref SceneEventData sceneEventData, ref Scene scene) - { - // If we are just a normal client and in distributed authority mode, then always use the known server scene handle - if (NetworkManager.DistributedAuthorityMode && NetworkManager.CMBServiceConnection) - { - sceneEventData.AddSceneToSynchronize(SceneHashFromNameOrPath(scene.path), ClientSceneHandleToServerSceneHandle[scene.handle]); - } - else - { - sceneEventData.AddSceneToSynchronize(SceneHashFromNameOrPath(scene.path), scene.handle); - } - } - /// /// Server Side: /// This is used for players that have just had their connection approved and will assure they are synchronized @@ -2016,53 +2012,61 @@ internal void SynchronizeNetworkObjects(ulong clientId, bool synchronizingServic // Organize how (and when) we serialize our NetworkObjects var hasSynchronizedActive = false; - - // It is possible a user might not want to synchronize the active scene, so we will check to see if it is valid before adding it to the synchronization list. - // !! Important !! - // The active scene MUST always be the first scene in the synchronization list. - if (ValidateSceneBeforeLoading(activeScene.buildIndex, activeScene.name, sceneEventData.LoadSceneMode)) + for (int i = 0; i < SceneManager.sceneCount; i++) { - sceneEventData.SceneHash = SceneHashFromNameOrPath(activeScene.path); - if (sceneEventData.SceneHash == sceneEventData.ActiveSceneHash) + var scene = SceneManager.GetSceneAt(i); + + // NetworkSceneManager does not synchronize scenes that are not loaded by NetworkSceneManager + // unless the scene in question is the currently active scene. + if (ExcludeSceneFromSychronization != null && !ExcludeSceneFromSychronization(scene)) { - hasSynchronizedActive = true; + continue; } - // If we are just a normal client, then always use the server scene handle - if (NetworkManager.DistributedAuthorityMode) + if (scene == DontDestroyOnLoadScene) { - sceneEventData.SenderClientId = NetworkManager.LocalClientId; - sceneEventData.SceneHandle = ClientSceneHandleToServerSceneHandle[activeScene.handle]; + continue; } - else + + // This would depend upon whether we are additive or not + // If we are the base scene, then we set the root scene index; + if (activeScene == scene) { - sceneEventData.SceneHandle = activeScene.handle; - } - AddSceneToClientSynchronization(ref sceneEventData, ref activeScene); - } + if (!ValidateSceneBeforeLoading(scene.buildIndex, scene.name, sceneEventData.LoadSceneMode)) + { + continue; + } + sceneEventData.SceneHash = SceneHashFromNameOrPath(scene.path); + if (sceneEventData.SceneHash == sceneEventData.ActiveSceneHash) + { + hasSynchronizedActive = true; + } - for (int i = 0; i < SceneManager.sceneCount; i++) - { - var scene = SceneManager.GetSceneAt(i); - // Skip adding the active scene at this point as we are just adding all other additively loaded scenes to the synchronization list. - // Skip adding the dont destroy on load scene as that is never synchronized. - if ((scene.handle == activeScene.handle) || (scene == DontDestroyOnLoadScene)) + // If we are just a normal client, then always use the server scene handle + if (NetworkManager.DistributedAuthorityMode) + { + sceneEventData.SenderClientId = NetworkManager.LocalClientId; + sceneEventData.SceneHandle = ClientSceneHandleToServerSceneHandle[scene.handle]; + } + else + { + sceneEventData.SceneHandle = scene.handle; + } + } + else if (!ValidateSceneBeforeLoading(scene.buildIndex, scene.name, LoadSceneMode.Additive)) { continue; } - // NetworkSceneManager does not synchronize scenes that are not loaded by NetworkSceneManager - // unless the scene in question is the currently active scene. - if (ExcludeSceneFromSychronization != null && !ExcludeSceneFromSychronization(scene)) + // If we are just a normal client and in distributed authority mode, then always use the known server scene handle + if (NetworkManager.DistributedAuthorityMode && NetworkManager.CMBServiceConnection) { - continue; + sceneEventData.AddSceneToSynchronize(SceneHashFromNameOrPath(scene.path), ClientSceneHandleToServerSceneHandle[scene.handle]); } - - if (!ValidateSceneBeforeLoading(scene.buildIndex, scene.name, LoadSceneMode.Additive)) + else { - continue; + sceneEventData.AddSceneToSynchronize(SceneHashFromNameOrPath(scene.path), scene.handle); } - AddSceneToClientSynchronization(ref sceneEventData, ref scene); } if (!hasSynchronizedActive && NetworkManager.CMBServiceConnection && synchronizingService) @@ -2115,12 +2119,9 @@ private void OnClientBeginSync(uint sceneEventId) var sceneHash = sceneEventData.GetNextSceneSynchronizationHash(); var sceneHandle = sceneEventData.GetNextSceneSynchronizationHandle(); var sceneName = SceneNameFromHash(sceneHash); - var activeSceneName = SceneNameFromHash(sceneEventData.ActiveSceneHash); var activeScene = SceneManager.GetActiveScene(); - var activeSceneLoaded = activeSceneName == activeScene.name; - - var loadSceneMode = sceneHash == sceneEventData.SceneHash && !activeSceneLoaded ? sceneEventData.LoadSceneMode : LoadSceneMode.Additive; + var loadSceneMode = sceneHash == sceneEventData.SceneHash ? sceneEventData.LoadSceneMode : LoadSceneMode.Additive; // Store the sceneHandle and hash sceneEventData.NetworkSceneHandle = sceneHandle; @@ -2260,6 +2261,8 @@ private void SynchronizeNetworkObjectScene() { 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) @@ -2267,6 +2270,11 @@ private void SynchronizeNetworkObjectScene() 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 (NetworkManager.LogLevel <= LogLevel.Normal) @@ -2753,50 +2761,31 @@ internal void MoveObjectsToDontDestroyOnLoad() /// Using the local scene relative Scene.handle as a sub-key to the root dictionary allows us to /// distinguish between duplicate in-scene placed NetworkObjects /// + /// List of NetworkObjects that need to be spawned on startup internal void PopulateScenePlacedObjects(Scene sceneToFilterBy, bool clearScenePlacedObjects = true) { if (clearScenePlacedObjects) { ScenePlacedObjects.Clear(); } - var sceneHandle = sceneToFilterBy.handle; - // Just add every NetworkObject found that isn't already in the list - // With additive scenes, we can have multiple in-scene placed NetworkObjects with the same GlobalObjectIdHash value - // During Client Side Synchronization: We add them on a FIFO basis, for each scene loaded without clearing, and then - // at the end of scene loading we use this list to soft synchronize all in-scene placed NetworkObjects - foreach (var networkObjectInstance in FindObjects.FromSceneByType(sceneToFilterBy, true)) - { - if (!networkObjectInstance.InScenePlaced) - { - continue; - } - - if (networkObjectInstance.NetworkManagerOwner == null) - { - networkObjectInstance.NetworkManagerOwner = NetworkManager; - } + SceneObjectStore.ProcessObjectsOnSceneLoad(sceneToFilterBy, NetworkManager, ScenePlacedObjects); + } - var globalObjectIdHash = networkObjectInstance.GlobalObjectIdHash; - // 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.NetworkManagerOwner == NetworkManager && networkObjectInstance.isActiveAndEnabled) - { - if (!ScenePlacedObjects.ContainsKey(globalObjectIdHash)) - { - ScenePlacedObjects.Add(globalObjectIdHash, new Dictionary()); - } + /// + /// Server/Authority side + /// Used to populate all the in-scene-placed objects after the SpawnManager has auto spawned all objects to auto spawn. + /// + internal void PopulateScenePlacedObjectsOnStartup() + { + // Since we are spawning in-scene placed NetworkObjects for already loaded scenes, + // we need to add any in-scene placed NetworkObject to our tracking table + SceneObjectStore.ClearAllStoredObjects(); + ScenePlacedObjects.Clear(); - if (!ScenePlacedObjects[globalObjectIdHash].ContainsKey(sceneHandle)) - { - ScenePlacedObjects[globalObjectIdHash].Add(sceneHandle, networkObjectInstance); - } - else - { - var exitingEntryName = ScenePlacedObjects[globalObjectIdHash][sceneHandle] != null ? ScenePlacedObjects[globalObjectIdHash][sceneHandle].name : "Null Entry"; - throw new Exception($"{networkObjectInstance.name} tried to registered with {nameof(ScenePlacedObjects)} which already contains " + - $"the same {nameof(NetworkObject.GlobalObjectIdHash)} value {globalObjectIdHash} for {exitingEntryName}!"); - } - } + foreach (var sceneLoaded in ScenesLoaded) + { + NetworkManager.SceneManager.PopulateScenePlacedObjects(sceneLoaded.Value, false); } } @@ -2890,12 +2879,6 @@ internal bool IsSceneUnloading(NetworkObject networkObject) /// internal void NotifyNetworkObjectSceneChanged(NetworkObject networkObject) { - if (networkObject.NetworkManagerOwner != NetworkManager) - { - Debug.Log($"!!!!!!!!!!!!! Integration test is registering for scene migration for instances outside of the bounds of this NetworkManager context !!!!!!!!!!!!!"); - return; - } - // Really, this should never happen but in case it does if (!networkObject.HasAuthority) { @@ -2919,7 +2902,7 @@ internal void NotifyNetworkObjectSceneChanged(NetworkObject networkObject) // 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.name == SceneManager.GetActiveScene().name && networkObject.ActiveSceneSynchronization) + if (networkObject.gameObject.scene == SceneManager.GetActiveScene() && networkObject.ActiveSceneSynchronization) { return; } @@ -2928,13 +2911,6 @@ internal void NotifyNetworkObjectSceneChanged(NetworkObject networkObject) // Note: This does not apply to SceneEventType.Synchronize since synchronization isn't a global connected client event. if (IsSceneEventInProgress()) { - Debug.Log($"{networkObject.name} scene event in progress -- ignoring!"); - return; - } - - if (IsSceneUnloading(networkObject)) - { - Debug.Log($"{networkObject.name} scene unloading in progress -- ignoring!"); return; } @@ -3059,15 +3035,7 @@ internal void CheckForAndSendNetworkObjectSceneChanged() // Some NetworkObjects still exist, send the message var sceneEvent = BeginSceneEvent(); sceneEvent.SceneEventType = SceneEventType.ObjectSceneChanged; - // SendSceneEventData can throw an exception. We need to wrap this and recover from the exception gracefully. - try - { - SendSceneEventData(sceneEvent.SceneEventId, NetworkManager.ConnectedClientsIds.Where(c => c != NetworkManager.LocalClientId).ToArray()); - } - catch (Exception ex) - { - Debug.LogException(ex); - } + SendSceneEventData(sceneEvent.SceneEventId, NetworkManager.ConnectedClientsIds.Where(c => c != NetworkManager.LocalClientId).ToArray()); ObjectsMigratedIntoNewScene.Clear(); EndSceneEvent(sceneEvent.SceneEventId); } diff --git a/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs b/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs index 6195fa2335..de8d919eb1 100644 --- a/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs +++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Text; using Unity.Collections; +using UnityEngine; using UnityEngine.SceneManagement; namespace Unity.Netcode @@ -1096,12 +1097,24 @@ internal void SynchronizeSceneNetworkObjects(NetworkManager networkManager) var serializedObject = new NetworkObject.SerializedObject(); serializedObject.Deserialize(m_InternalBuffer); - // If the sceneObject is in-scene placed, then set the scene being synchronized - if (serializedObject.IsSceneObject) + // On initial synchronization we want to match any objects already existing in the scene, + // Always set the scene being synchronized. + m_NetworkManager.SceneManager.SetTheSceneBeingSynchronized(serializedObject.NetworkSceneHandle, false); + // If the object is not in-scene-placed do an addition check for whether the object exists in the project + // We only want to do this on the initial client-side synchronize. + NetworkObject existingObject = null; + if (!serializedObject.IsSceneObject) { - m_NetworkManager.SceneManager.SetTheSceneBeingSynchronized(serializedObject.NetworkSceneHandle); + Debug.Log($"[Client-{m_NetworkManager.LocalClientId}] looking at dynamic synchronize object {serializedObject.Hash}."); + var sceneHandle = m_NetworkManager.SceneManager.SceneBeingSynchronized.handle; + var inSceneObject = m_NetworkManager.SceneManager.SceneObjectStore.GetSceneRelativeInSceneNetworkObject(serializedObject.Hash, sceneHandle); + if (inSceneObject != null && !inSceneObject.HasBeenSpawned) + { + Debug.Log($"[Client-{m_NetworkManager.LocalClientId}] Found existing object! {inSceneObject.name}"); + existingObject = inSceneObject; + } } - var spawnedNetworkObject = NetworkObject.DeserializeAndSpawnObject(serializedObject, m_InternalBuffer, networkManager); + var spawnedNetworkObject = NetworkObject.DeserializeAndSpawnObject(serializedObject, m_InternalBuffer, networkManager, existingObject: existingObject); if (spawnedNetworkObject == null) { continue; diff --git a/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneObjectStore.cs b/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneObjectStore.cs new file mode 100644 index 0000000000..58f8ec9571 --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneObjectStore.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.SceneManagement; + +namespace Unity.Netcode +{ + internal class SceneObjectStore + { + /// + /// We organize our m_ObjectsPerScene by: + /// [GlobalObjectIdHash][NetworkSceneHandle][NetworkObject] + /// Using the local scene relative Scene.handle as a sub-key to the root dictionary allows us to + /// distinguish between duplicate in-scene placed NetworkObjects + /// + private readonly Dictionary> m_ObjectsPerScene = new(); + + /// + /// Clean the store. + /// + internal void ClearAllStoredObjects() + { + m_ObjectsPerScene.Clear(); + } + + /// + /// Should be invoked on both the client and server side after: + /// -- A new scene has been loaded + /// -- Before any "DontDestroyOnLoad" NetworkObjects have been added back into the scene. + /// Added the ability to choose not to clear the scene placed objects for additive scene loading. + /// + internal void ProcessObjectsOnSceneLoad(Scene sceneToFilterBy, NetworkManager associatedNetworkManager, Dictionary> scenePlacedObjects) + { + var sceneHandle = sceneToFilterBy.handle; + // if (associatedNetworkManager.LocalClientId == 1) + // { + // Debug.Break(); + // } + + // Just add every NetworkObject found that isn't already in the list + // With additive scenes, we can have multiple in-scene placed NetworkObjects with the same GlobalObjectIdHash value + // During Client Side Synchronization: We add them on a FIFO basis, for each scene loaded without clearing, and then + // at the end of scene loading we use this list to soft synchronize all in-scene placed NetworkObjects + foreach (var obj in FindObjects.FromSceneByType(sceneToFilterBy, true)) + { + if (!obj.AutoSpawnOnStart) + { + continue; + } + + if (obj.NetworkManagerOwner == null) + { + obj.NetworkManagerOwner = associatedNetworkManager; + } + + // We check to make sure the NetworkManager instance is the same one to be "NetcodeIntegrationTestHelpers" compatible + if (obj.NetworkManagerOwner != associatedNetworkManager) + { + continue; + } + + var globalObjectIdHash = obj.GlobalObjectIdHash; + + // Add everything into m_ObjectsPerScene + if (!m_ObjectsPerScene.ContainsKey(globalObjectIdHash)) + { + m_ObjectsPerScene.Add(globalObjectIdHash, new Dictionary()); + } + + if (!m_ObjectsPerScene[globalObjectIdHash].ContainsKey(sceneHandle)) + { + Debug.Log($"[Client-{associatedNetworkManager.LocalClientId}] Saving object {obj.name} to sceneHandle {sceneHandle} with GlobalObjectIdHash: {globalObjectIdHash}"); + m_ObjectsPerScene[globalObjectIdHash].Add(sceneHandle, obj); + } + else if (!obj.HasBeenSpawned) + { + var existing = m_ObjectsPerScene[globalObjectIdHash][sceneHandle]; + var exitingEntryName = existing == null ? existing.name : "Null Entry"; + throw new Exception($"{obj.name} tried to registered with {nameof(m_ObjectsPerScene)} which already contains " + + $"the same {nameof(NetworkObject.GlobalObjectIdHash)} value {globalObjectIdHash} for {exitingEntryName}!"); + } + + // Legacy path: + // Only add active in-scene-placed objects into ScenePlacedObjects + if (obj.isActiveAndEnabled && obj.InScenePlaced) + { + if (!scenePlacedObjects.ContainsKey(globalObjectIdHash)) + { + scenePlacedObjects.Add(globalObjectIdHash, new Dictionary()); + } + + if (!scenePlacedObjects[globalObjectIdHash].ContainsKey(sceneHandle)) + { + scenePlacedObjects[globalObjectIdHash].Add(sceneHandle, obj); + } + else + { + var existing = scenePlacedObjects[globalObjectIdHash][sceneHandle]; + var exitingEntryName = existing == null ? existing.name : "Null Entry"; + throw new Exception($"{obj.name} tried to registered with {nameof(NetworkSceneManager.ScenePlacedObjects)} which already contains " + + $"the same {nameof(NetworkObject.GlobalObjectIdHash)} value {globalObjectIdHash} for {exitingEntryName}!"); + } + } + } + } + + + /// + /// During soft synchronization of in-scene placed NetworkObjects, this is now used by NetworkSpawnManager.CreateLocalNetworkObject + /// + internal NetworkObject GetSceneRelativeInSceneNetworkObject(uint globalObjectIdHash, NetworkSceneHandle sceneToCheck) + { + if (m_ObjectsPerScene.TryGetValue(globalObjectIdHash, out var scenePlacedObjectsForHash)) + { + if (scenePlacedObjectsForHash.TryGetValue(sceneToCheck, out var scenePlaceObject)) + { + return scenePlaceObject; + } + } + return null; + } + } +} diff --git a/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneObjectStore.cs.meta b/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneObjectStore.cs.meta new file mode 100644 index 0000000000..af2e89f7a2 --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneObjectStore.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: df806932e4614ac885108528353392e2 +timeCreated: 1784045611 \ No newline at end of file diff --git a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs index 74d0ee121e..ef013fbfc7 100644 --- a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs @@ -955,7 +955,7 @@ internal NetworkObject InstantiateNetworkPrefab([NotNull] GameObject networkPref /// For most cases this is client-side only, except when the server is spawning a player. /// [return: MaybeNull] - internal NetworkObject CreateLocalNetworkObject(NetworkObject.SerializedObject serializedObject, byte[] instantiationData = null) + internal NetworkObject CreateLocalNetworkObject(NetworkObject.SerializedObject serializedObject, byte[] instantiationData = null, NetworkObject existingObject = null) { NetworkObject networkObject = null; var globalObjectIdHash = serializedObject.Hash; @@ -965,18 +965,32 @@ internal NetworkObject CreateLocalNetworkObject(NetworkObject.SerializedObject s var parentNetworkId = serializedObject.HasParent ? serializedObject.ParentObjectId : default; var worldPositionStays = (!serializedObject.HasParent) || serializedObject.WorldPositionStays; + if (existingObject != null) + { + Debug.Log($"CreateLocalNetworkObject for existing object: {existingObject.name}"); + } + // If scene management is disabled or the NetworkObject was dynamically spawned - if (!NetworkManager.NetworkConfig.EnableSceneManagement || !serializedObject.IsSceneObject) + if (!NetworkManager.NetworkConfig.EnableSceneManagement || (!serializedObject.IsSceneObject && existingObject == null)) { networkObject = GetNetworkObjectToSpawn(serializedObject.Hash, serializedObject.OwnerClientId, position, rotation, serializedObject.IsSceneObject, instantiationData); } else // Get the in-scene placed NetworkObject { - networkObject = NetworkManager.SceneManager.GetSceneRelativeInSceneNetworkObject(globalObjectIdHash, serializedObject.NetworkSceneHandle); - if (networkObject == null) + if (existingObject != null) + { + Debug.LogWarning("Have an existing object"); + networkObject = existingObject; + } + else { - NetworkLog.LogErrorServer(new Context(LogLevel.Error, $"{nameof(NetworkPrefab)} hash was not found! In-Scene placed {nameof(NetworkObject)} soft synchronization failure!").AddInfo(nameof(NetworkObject.GlobalObjectIdHash), globalObjectIdHash)); - return null; + networkObject = NetworkManager.SceneManager.GetSceneRelativeInSceneNetworkObject(globalObjectIdHash, serializedObject.NetworkSceneHandle); + + if (networkObject == null) + { + NetworkLog.LogErrorServer(new Context(LogLevel.Error, $"{nameof(NetworkPrefab)} hash was not found! In-Scene placed {nameof(NetworkObject)} soft synchronization failure!").AddInfo(nameof(NetworkObject.GlobalObjectIdHash), globalObjectIdHash)); + return null; + } } // Since this NetworkObject is an in-scene placed NetworkObject, if it is disabled then enable it so @@ -1138,7 +1152,7 @@ internal bool AuthorityLocalSpawn([NotNull] NetworkObject networkObject, ulong n networkObject.NetworkManagerOwner = NetworkManager; networkObject.InvokeBehaviourNetworkPreSpawn(); - if (NetworkManager.DistributedAuthorityMode && NetworkManager.NetworkConfig.EnableSceneManagement && networkObject.InScenePlaced) + if (NetworkManager.DistributedAuthorityMode && NetworkManager.NetworkConfig.EnableSceneManagement) { networkObject.SceneOriginHandle = networkObject.gameObject.scene.handle; networkObject.NetworkSceneHandle = NetworkManager.SceneManager.ClientSceneHandleToServerSceneHandle[networkObject.gameObject.scene.handle]; @@ -1172,7 +1186,7 @@ internal bool AuthorityLocalSpawn([NotNull] NetworkObject networkObject, ulong n /// IMPORTANT: Pre spawn methods need to be invoked from within . /// /// boolean indicating whether the spawn succeeded - internal bool NonAuthorityLocalSpawn(in NetworkObject.SerializedObject serializedObject, [MaybeNullWhen(false)] out NetworkObject networkObject, FastBufferReader reader, bool destroyWithScene) + internal bool NonAuthorityLocalSpawn(in NetworkObject.SerializedObject serializedObject, [MaybeNullWhen(false)] out NetworkObject networkObject, FastBufferReader reader, bool destroyWithScene, NetworkObject existingObject = null) { if (SpawnedObjects.ContainsKey(serializedObject.NetworkObjectId)) { @@ -1186,9 +1200,13 @@ internal bool NonAuthorityLocalSpawn(in NetworkObject.SerializedObject serialize { reader.ReadValueSafe(out instantiationData); } + if (existingObject != null) + { + Debug.Log($"NonAuthorityLocalSpawn for existing object: {existingObject.name}"); + } // Attempt to create a local NetworkObject - networkObject = CreateLocalNetworkObject(serializedObject, instantiationData); + networkObject = CreateLocalNetworkObject(serializedObject, instantiationData, existingObject); // Log the error that the NetworkObject failed to construct if (networkObject == null) @@ -1582,8 +1600,8 @@ internal void ServerSpawnSceneObjectsOnStartSweep() // This used to be two loops. // The first added all NetworkObjects to a list and the second spawned all NetworkObjects in the list. - // Now, a parent will set its children's IsSceneObject value when spawned, so we check for null or for true. - if (networkObject.InScenePlaced) + // Now, a parent will spawn its children when spawned, so we check whether the object has already been spawned. + if (!networkObject.HasBeenSpawned && networkObject.AutoSpawnOnStart) { var ownerId = networkObject.OwnerClientId; if (NetworkManager.DistributedAuthorityMode) @@ -1591,25 +1609,36 @@ internal void ServerSpawnSceneObjectsOnStartSweep() ownerId = NetworkManager.LocalClientId; } - if (AuthorityLocalSpawn(networkObject, GetNetworkObjectId(), true, false, ownerId, true)) + if (networkObject.InScenePlaced) { - networkObjectsToSpawn.Add(networkObject); + if (AuthorityLocalSpawn(networkObject, GetNetworkObjectId(), networkObject.InScenePlaced, false, ownerId, true)) + { + networkObjectsToSpawn.Add(networkObject); + } + else + { + networkObject.ResetOnDespawn(); + } } else { - networkObject.ResetOnDespawn(); + Debug.Log($"Spawning {networkObject.name} on startup sweep"); + networkObject.Spawn(); + if (networkObject.IsSpawned) + { + networkObjectsToSpawn.Add(networkObject); + } + else + { + + Debug.LogError($"Failed to spawn {networkObject.name} on startup sweep"); + } } } } - // Since we are spawing in-scene placed NetworkObjects for already loaded scenes, - // we need to add any in-scene placed NetworkObject to our tracking table - var clearFirst = true; - foreach (var sceneLoaded in NetworkManager.SceneManager.ScenesLoaded) - { - NetworkManager.SceneManager.PopulateScenePlacedObjects(sceneLoaded.Value, clearFirst); - clearFirst = false; - } + // Populate all the newly spawned objects into the scene management tables. + NetworkManager.SceneManager.PopulateScenePlacedObjectsOnStartup(); // Notify all in-scene placed NetworkObjects have been spawned foreach (var networkObject in networkObjectsToSpawn) diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/TestHelpers/IntegrationTestSceneHandler.cs b/com.unity.netcode.gameobjects/Tests/Runtime/TestHelpers/IntegrationTestSceneHandler.cs index 209caf300b..4d84311a2c 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/TestHelpers/IntegrationTestSceneHandler.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/TestHelpers/IntegrationTestSceneHandler.cs @@ -409,6 +409,8 @@ public void StartTrackingScene(Scene scene, bool assigned, NetworkManager networ if (!SceneNameToSceneHandles[networkManager][scene.name].ContainsKey(scene.handle)) { + Debug.Log($"[Client-{NetworkManager.LocalClientId}] Populating {scene.name} ({scene.handle}) for Client-{NetworkManager.LocalClientId}!"); + var sceneEntry = new SceneEntry() { IsAssigned = true, @@ -594,6 +596,8 @@ public void PopulateLoadedScenes(ref Dictionary scene if (!SceneNameToSceneHandles[networkManager][scene.name].ContainsKey(scene.handle)) { + Debug.Log($"[Client-{NetworkManager.LocalClientId}] Populating {scene.name} ({scene.handle}) for Client-{NetworkManager.LocalClientId}!"); + var sceneEntry = new SceneEntry() { IsAssigned = false, diff --git a/testproject/Assets/AddressableAssetsData/AssetGroups/Default Local Group.asset b/testproject/Assets/AddressableAssetsData/AssetGroups/Default Local Group.asset index 36d70b4b1c..d3f0f5a0e5 100644 --- a/testproject/Assets/AddressableAssetsData/AssetGroups/Default Local Group.asset +++ b/testproject/Assets/AddressableAssetsData/AssetGroups/Default Local Group.asset @@ -15,6 +15,11 @@ MonoBehaviour: m_GroupName: Default Local Group m_GUID: 93aa504d1b753cb41a8a779ae63f5795 m_SerializeEntries: + - m_GUID: 68d21678646384e6291bb2b568b5d95c + m_Address: Assets/Scenes/AddressableInSceneObject.unity + m_ReadOnly: 0 + m_SerializedLabels: [] + FlaggedDuringContentUpdateRestriction: 0 - m_GUID: ffa1ab8ed58b72343ad93116ded1700a m_Address: AddressableTestObject.prefab m_ReadOnly: 0 diff --git a/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkObjectSceneMigrationTests.cs b/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkObjectSceneMigrationTests.cs index 736de24acf..1fe30d2051 100644 --- a/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkObjectSceneMigrationTests.cs +++ b/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkObjectSceneMigrationTests.cs @@ -110,8 +110,19 @@ private bool VerifyAllScenesMatch(StringBuilder errorLog, List au var originalSceneTracker = instance.GetComponent(); Assert.AreEqual(originalSceneTracker.SceneWhereAwakeHappened, (NetworkSceneHandle)instance.SceneOrigin.handle, "The SceneOrigin of an object should never change!"); } + if (!sceneLoaded && loadType == ExpectedLoadType.Loaded) + { + errorLog.AppendLine($"[Client-{networkManager.LocalClientId}] scene {sceneName} wasn't loaded on this client!"); + allValid = false; + } + else if (sceneLoaded && loadType == ExpectedLoadType.Unloaded) + { + errorLog.AppendLine($"[Client-{networkManager.LocalClientId}] scene {sceneName} was still loaded on this client!"); + allValid = false; + } } - return true; + + return allValid; } private const int k_MaxObjectsToSpawn = 9; diff --git a/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkSceneManagerStartupTests.cs.meta b/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkSceneManagerStartupTests.cs.meta index ddf1a7550e..705a6eda80 100644 --- a/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkSceneManagerStartupTests.cs.meta +++ b/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkSceneManagerStartupTests.cs.meta @@ -1,2 +1,2 @@ fileFormatVersion: 2 -guid: ca8e58d04b6fc6c48a6661c90d4c12dd \ No newline at end of file +guid: ca8e58d04b6fc6c48a6661c90d4c12dd