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