From 124c2b1e2eddc88713702878dca473c053ae7cc2 Mon Sep 17 00:00:00 2001
From: NoelStephensUnity <73188597+NoelStephensUnity@users.noreply.github.com>
Date: Sun, 15 Jan 2023 16:56:54 -0600
Subject: [PATCH 01/25] fix
When the ClientSynchronizationMode is additive and the client is being synchronized, we need to not clear the ScenePlacedObjects list as there might already be ScenePlacedObjects populated.
---
.../Runtime/SceneManagement/NetworkSceneManager.cs | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs b/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs
index b53890396f..8fa169bf8f 100644
--- a/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs
+++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs
@@ -1622,7 +1622,12 @@ private void OnClientBeginSync(uint sceneEventId)
OnSynchronize?.Invoke(m_NetworkManager.LocalClientId);
// Clear the in-scene placed NetworkObjects when we load the first scene in our synchronization process
- ScenePlacedObjects.Clear();
+ // But only clear the in-scene placed NetworkObjects if we are loading in Single mode, otherwise we could
+ // already have scene placed objects populated
+ if (loadSceneMode == LoadSceneMode.Single)
+ {
+ ScenePlacedObjects.Clear();
+ }
}
// Always check to see if the scene needs to be validated
From b5f845918098a82300d7289130e74c6ea8396313 Mon Sep 17 00:00:00 2001
From: NoelStephensUnity <73188597+NoelStephensUnity@users.noreply.github.com>
Date: Sun, 15 Jan 2023 18:36:27 -0600
Subject: [PATCH 02/25] update
Updating changelog entry with fix.
---
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 f87c08399c..9381a396e6 100644
--- a/com.unity.netcode.gameobjects/CHANGELOG.md
+++ b/com.unity.netcode.gameobjects/CHANGELOG.md
@@ -21,6 +21,7 @@ Additional documentation and release notes are available at [Multiplayer Documen
- Network prefabs are now stored in a ScriptableObject that can be shared between NetworkManagers, and have been exposed for public access. By default, a Default Prefabs List is created that contains all NetworkObject prefabs in the project, and new NetworkManagers will default to using that unless that option is turned off in the Netcode for GameObjects settings. Existing NetworkManagers will maintain their existing lists, which can be migrated to the new format via a button in their inspector. (#2322)
### Fixed
+- Fixed issue when the ClientSynchronizationMode is additive and the server changes the currently active scene prior to a client connecting then upon a client connecting and being synchronized the NetworkSceneManager would clear its internal ScenePlacedObjects list that could already be populated. (#2383)
- Fixed a UTP test that was failing when you install Unity Transport package 2.0.0 or newer. (#2347)
- Fixed issue where `NetcodeSettingsProvider` would throw an exception in Unity 2020.3.x versions. (#2345)
- Fixed server side issue where, depending upon component ordering, some NetworkBehaviour components might not have their OnNetworkDespawn method invoked if the client side disconnected. (#2323)
From 673d753c02e34495ca0b6ebfc9f67cc6e2068db8 Mon Sep 17 00:00:00 2001
From: NoelStephensUnity <73188597+NoelStephensUnity@users.noreply.github.com>
Date: Mon, 16 Jan 2023 16:32:46 -0600
Subject: [PATCH 03/25] fix
A more robust fix for running the server-side NetworkSceneManager in client synchronization mode additive.
---
.../Runtime/Configuration/NetworkPrefabs.cs | 11 ++
.../Runtime/Core/NetworkManager.cs | 2 +
.../DefaultSceneManagerHandler.cs | 169 ++++++++++++++++++
.../DefaultSceneManagerHandler.cs.meta | 11 ++
.../SceneManagement/ISceneManagerHandler.cs | 12 ++
.../SceneManagement/NetworkSceneManager.cs | 67 ++++---
.../Runtime/SceneManagement/SceneEventData.cs | 10 ++
7 files changed, 247 insertions(+), 35 deletions(-)
create mode 100644 com.unity.netcode.gameobjects/Runtime/SceneManagement/DefaultSceneManagerHandler.cs
create mode 100644 com.unity.netcode.gameobjects/Runtime/SceneManagement/DefaultSceneManagerHandler.cs.meta
diff --git a/com.unity.netcode.gameobjects/Runtime/Configuration/NetworkPrefabs.cs b/com.unity.netcode.gameobjects/Runtime/Configuration/NetworkPrefabs.cs
index 6c2d526ba5..1dae3ce33f 100644
--- a/com.unity.netcode.gameobjects/Runtime/Configuration/NetworkPrefabs.cs
+++ b/com.unity.netcode.gameobjects/Runtime/Configuration/NetworkPrefabs.cs
@@ -52,12 +52,23 @@ private void RemoveTriggeredByNetworkPrefabList(NetworkPrefab networkPrefab)
}
~NetworkPrefabs()
+ {
+ Shutdown();
+ }
+
+ ///
+ /// Deregister from add and remove events
+ /// Clear the list
+ ///
+ internal void Shutdown()
{
foreach (var list in NetworkPrefabsLists)
{
list.OnAdd -= AddTriggeredByNetworkPrefabList;
list.OnRemove -= RemoveTriggeredByNetworkPrefabList;
}
+
+ NetworkPrefabsLists.Clear();
}
///
diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs
index 6cf9ea0db4..f7fffccd08 100644
--- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs
+++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs
@@ -1277,6 +1277,8 @@ internal void ShutdownInternal()
m_StopProcessingMessages = false;
ClearClients();
+ // This cleans up the internal prefabs list
+ NetworkConfig?.Prefabs.Shutdown();
}
///
diff --git a/com.unity.netcode.gameobjects/Runtime/SceneManagement/DefaultSceneManagerHandler.cs b/com.unity.netcode.gameobjects/Runtime/SceneManagement/DefaultSceneManagerHandler.cs
new file mode 100644
index 0000000000..7e7b9ab949
--- /dev/null
+++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/DefaultSceneManagerHandler.cs
@@ -0,0 +1,169 @@
+using System;
+using System.Collections.Generic;
+using UnityEngine;
+using UnityEngine.SceneManagement;
+
+
+namespace Unity.Netcode
+{
+ ///
+ /// The default SceneManagerHandler that interfaces between the SceneManager and NetworkSceneManager
+ ///
+ internal class DefaultSceneManagerHandler : ISceneManagerHandler
+ {
+ private Scene m_InvalidScene = new Scene();
+
+ internal struct SceneEntry
+ {
+ public bool IsAssigned;
+ public Scene Scene;
+ }
+
+ internal Dictionary> SceneNameToSceneHandles = new Dictionary>();
+
+ public AsyncOperation LoadSceneAsync(string sceneName, LoadSceneMode loadSceneMode, SceneEventProgress sceneEventProgress)
+ {
+ var operation = SceneManager.LoadSceneAsync(sceneName, loadSceneMode);
+ sceneEventProgress.SetAsyncOperation(operation);
+ return operation;
+ }
+
+ public AsyncOperation UnloadSceneAsync(Scene scene, SceneEventProgress sceneEventProgress)
+ {
+ var operation = SceneManager.UnloadSceneAsync(scene);
+ sceneEventProgress.SetAsyncOperation(operation);
+ return operation;
+ }
+
+ ///
+ /// Resets scene tracking
+ ///
+ public void ClearSceneTracking(NetworkManager networkManager)
+ {
+ SceneNameToSceneHandles.Clear();
+ }
+
+ ///
+ /// Stops tracking a specific scene
+ ///
+ public void StopTrackingScene(int handle, string name, NetworkManager networkManager)
+ {
+ if (SceneNameToSceneHandles.ContainsKey(name))
+ {
+ if (SceneNameToSceneHandles[name].ContainsKey(handle))
+ {
+ SceneNameToSceneHandles[name].Remove(handle);
+ if (SceneNameToSceneHandles[name].Count == 0)
+ {
+ SceneNameToSceneHandles.Remove(name);
+ }
+ }
+ }
+ }
+
+ ///
+ /// Starts tracking a specific scene
+ ///
+ public void StartTrackingScene(Scene scene, bool assigned, NetworkManager networkManager)
+ {
+ if (!SceneNameToSceneHandles.ContainsKey(scene.name))
+ {
+ SceneNameToSceneHandles.Add(scene.name, new Dictionary());
+ }
+
+ if (!SceneNameToSceneHandles[scene.name].ContainsKey(scene.handle))
+ {
+ var sceneEntry = new SceneEntry()
+ {
+ IsAssigned = true,
+ Scene = scene
+ };
+ SceneNameToSceneHandles[scene.name].Add(scene.handle, sceneEntry);
+ }
+ else
+ {
+ throw new Exception($"[Duplicate Handle] Scene {scene.name} already has scene handle {scene.handle} registered!");
+ }
+ }
+
+ ///
+ /// Determines if there is an existing scene loaded that matches the scene name but has not been assigned
+ ///
+ public bool DoesSceneHaveUnassignedEntry(string sceneName, NetworkManager networkManager)
+ {
+ if (SceneNameToSceneHandles.ContainsKey(sceneName))
+ {
+ foreach (var sceneHandleEntry in SceneNameToSceneHandles[sceneName])
+ {
+ if (!sceneHandleEntry.Value.IsAssigned)
+ {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ ///
+ /// This will find any scene entry that hasn't been used/assigned, set the entry to assigned, and
+ /// return the associated scene. If none are found it returns an invalid scene.
+ ///
+ public Scene GetSceneFromLoadedScenes(string sceneName, NetworkManager networkManager)
+ {
+ if (SceneNameToSceneHandles.ContainsKey(sceneName))
+ {
+ foreach (var sceneHandleEntry in SceneNameToSceneHandles[sceneName])
+ {
+ if (!sceneHandleEntry.Value.IsAssigned)
+ {
+ var sceneEntry = sceneHandleEntry.Value;
+ sceneEntry.IsAssigned = true;
+ SceneNameToSceneHandles[sceneName][sceneHandleEntry.Key] = sceneEntry;
+ return sceneEntry.Scene;
+ }
+ }
+ }
+ // If we found nothing return an invalid scene
+ return m_InvalidScene;
+ }
+
+ ///
+ /// Only invoked is client synchronization is additive, this will generate the scene tracking table
+ /// in order to re-use the same scenes the server is synchronizing instead of having to unload the
+ /// scenes and reload them when synchronizing (i.e. client disconnects due to external reason, the
+ /// same application instance is still running, the same scenes are still loaded on the client, and
+ /// upon reconnecting the client doesn't have to unload the scenes and then reload them)
+ ///
+ public void PopulateLoadedScenes(ref Dictionary scenesLoaded, NetworkManager networkManager)
+ {
+ SceneNameToSceneHandles.Clear();
+ var sceneCount = SceneManager.sceneCount;
+ for (int i = 0; i < sceneCount; i++)
+ {
+ var scene = SceneManager.GetSceneAt(i);
+ if (!SceneNameToSceneHandles.ContainsKey(scene.name))
+ {
+ SceneNameToSceneHandles.Add(scene.name, new Dictionary());
+ }
+
+ if (!SceneNameToSceneHandles[scene.name].ContainsKey(scene.handle))
+ {
+ var sceneEntry = new SceneEntry()
+ {
+ IsAssigned = false,
+ Scene = scene
+ };
+ SceneNameToSceneHandles[scene.name].Add(scene.handle, sceneEntry);
+ if (!scenesLoaded.ContainsKey(scene.handle))
+ {
+ scenesLoaded.Add(scene.handle, scene);
+ }
+ }
+ else
+ {
+ throw new Exception($"[Duplicate Handle] Scene {scene.name} already has scene handle {scene.handle} registered!");
+ }
+ }
+ }
+ }
+}
diff --git a/com.unity.netcode.gameobjects/Runtime/SceneManagement/DefaultSceneManagerHandler.cs.meta b/com.unity.netcode.gameobjects/Runtime/SceneManagement/DefaultSceneManagerHandler.cs.meta
new file mode 100644
index 0000000000..a89ce83b79
--- /dev/null
+++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/DefaultSceneManagerHandler.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 8c18076bb9734cf4ea7297f85b7729be
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/com.unity.netcode.gameobjects/Runtime/SceneManagement/ISceneManagerHandler.cs b/com.unity.netcode.gameobjects/Runtime/SceneManagement/ISceneManagerHandler.cs
index 8b5b7e7fda..ccb7abcd8d 100644
--- a/com.unity.netcode.gameobjects/Runtime/SceneManagement/ISceneManagerHandler.cs
+++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/ISceneManagerHandler.cs
@@ -1,3 +1,4 @@
+using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
@@ -12,5 +13,16 @@ internal interface ISceneManagerHandler
AsyncOperation LoadSceneAsync(string sceneName, LoadSceneMode loadSceneMode, SceneEventProgress sceneEventProgress);
AsyncOperation UnloadSceneAsync(Scene scene, SceneEventProgress sceneEventProgress);
+
+ void PopulateLoadedScenes(ref Dictionary scenesLoaded, NetworkManager networkManager = null);
+ Scene GetSceneFromLoadedScenes(string sceneName, NetworkManager networkManager = null);
+
+ bool DoesSceneHaveUnassignedEntry(string sceneName, NetworkManager networkManager = null);
+
+ void StopTrackingScene(int handle, string name, NetworkManager networkManager = null);
+
+ void StartTrackingScene(Scene scene, bool assigned, NetworkManager networkManager = null);
+
+ void ClearSceneTracking(NetworkManager networkManager = null);
}
}
diff --git a/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs b/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs
index 8fa169bf8f..feb601c582 100644
--- a/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs
+++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs
@@ -333,26 +333,6 @@ public class NetworkSceneManager : IDisposable
///
internal ISceneManagerHandler SceneManagerHandler = new DefaultSceneManagerHandler();
- ///
- /// The default SceneManagerHandler that interfaces between the SceneManager and NetworkSceneManager
- ///
- private class DefaultSceneManagerHandler : ISceneManagerHandler
- {
- public AsyncOperation LoadSceneAsync(string sceneName, LoadSceneMode loadSceneMode, SceneEventProgress sceneEventProgress)
- {
- var operation = SceneManager.LoadSceneAsync(sceneName, loadSceneMode);
- sceneEventProgress.SetAsyncOperation(operation);
- return operation;
- }
-
- public AsyncOperation UnloadSceneAsync(Scene scene, SceneEventProgress sceneEventProgress)
- {
- var operation = SceneManager.UnloadSceneAsync(scene);
- sceneEventProgress.SetAsyncOperation(operation);
- return operation;
- }
- }
-
internal readonly Dictionary SceneEventProgressTracking = new Dictionary();
///
@@ -675,11 +655,11 @@ internal Scene GetAndAddNewlyLoadedSceneByName(string sceneName)
if (!ScenesLoaded.ContainsKey(sceneLoaded.handle))
{
ScenesLoaded.Add(sceneLoaded.handle, sceneLoaded);
+ SceneManagerHandler.StartTrackingScene(sceneLoaded, true, m_NetworkManager);
return sceneLoaded;
}
}
}
-
throw new Exception($"Failed to find any loaded scene named {sceneName}!");
}
}
@@ -1013,7 +993,7 @@ private void OnClientUnloadScene(uint sceneEventId)
var sceneUnload = SceneManagerHandler.UnloadSceneAsync(ScenesLoaded[sceneHandle], sceneEventProgress);
ScenesLoaded.Remove(sceneHandle);
-
+ SceneManagerHandler.StopTrackingScene(sceneHandle, sceneName, m_NetworkManager);
// Remove our server to scene handle lookup
ServerSceneHandleToClientSceneHandle.Remove(sceneEventData.SceneHandle);
@@ -1112,6 +1092,7 @@ internal void UnloadAdditivelyLoadedScenes(uint sceneEventId)
}
// clear out our scenes loaded list
ScenesLoaded.Clear();
+ SceneManagerHandler.ClearSceneTracking(m_NetworkManager);
}
///
@@ -1530,7 +1511,7 @@ internal void SynchronizeNetworkObjects(ulong clientId)
m_NetworkManager.SpawnManager.UpdateObservedNetworkObjects(clientId);
var sceneEventData = BeginSceneEvent();
-
+ sceneEventData.ClientSynchronizationMode = ClientSynchronizationMode;
sceneEventData.InitializeForSynch();
sceneEventData.TargetClientId = clientId;
sceneEventData.LoadSceneMode = ClientSynchronizationMode;
@@ -1620,14 +1601,6 @@ private void OnClientBeginSync(uint sceneEventId)
});
OnSynchronize?.Invoke(m_NetworkManager.LocalClientId);
-
- // Clear the in-scene placed NetworkObjects when we load the first scene in our synchronization process
- // But only clear the in-scene placed NetworkObjects if we are loading in Single mode, otherwise we could
- // already have scene placed objects populated
- if (loadSceneMode == LoadSceneMode.Single)
- {
- ScenePlacedObjects.Clear();
- }
}
// Always check to see if the scene needs to be validated
@@ -1641,11 +1614,12 @@ private void OnClientBeginSync(uint sceneEventId)
return;
}
- var shouldPassThrough = false;
+ var shouldPassThrough = ClientSynchronizationMode == LoadSceneMode.Single ? false : SceneManagerHandler.DoesSceneHaveUnassignedEntry(sceneName, m_NetworkManager);
var sceneLoad = (AsyncOperation)null;
- // Check to see if the client already has loaded the scene to be loaded
- if (sceneName == activeScene.name)
+ // Check to see if the client already has loaded the scene to be loaded and the client synchronization
+ // mode is set to additive
+ if (sceneName == activeScene.name && ClientSynchronizationMode == LoadSceneMode.Additive)
{
// If the client is already in the same scene, then pass through and
// don't try to reload it.
@@ -1688,7 +1662,11 @@ private void ClientLoadedSynchronization(uint sceneEventId)
{
var sceneEventData = SceneEventDataStore[sceneEventId];
var sceneName = SceneNameFromHash(sceneEventData.ClientSceneHash);
- var nextScene = GetAndAddNewlyLoadedSceneByName(sceneName);
+ var nextScene = SceneManagerHandler.GetSceneFromLoadedScenes(sceneName, m_NetworkManager);
+ if (!nextScene.IsValid())
+ {
+ nextScene = GetAndAddNewlyLoadedSceneByName(sceneName);
+ }
if (!nextScene.isLoaded || !nextScene.IsValid())
{
@@ -1942,6 +1920,8 @@ private void HandleServerSceneEvent(uint sceneEventId, ulong clientId)
}
}
+
+
///
/// Both Client and Server: Incoming scene event entry point
///
@@ -1960,6 +1940,23 @@ internal void HandleSceneEvent(ulong clientId, FastBufferReader reader)
if (sceneEventData.IsSceneEventClientSide())
{
+ // If the client is being synchronized for the first time do some initialization
+ if (sceneEventData.SceneEventType == SceneEventType.Synchronize)
+ {
+ ScenePlacedObjects.Clear();
+ // Set the server's configured client synchronization mode on the client side
+ ClientSynchronizationMode = sceneEventData.ClientSynchronizationMode;
+
+ // Only if ClientSynchronizationMode is Additive and the client receives a synchronize scene event
+ if (ClientSynchronizationMode == LoadSceneMode.Additive)
+ {
+ // Check for scenes already loaded and create a table of scenes already loaded (SceneEntries) that will be
+ // used if the server is synchronizing the same scenes (i.e. if a matching scene is already loaded on the
+ // client side, then that scene will be used as opposed to loading another scene). This allows for clients
+ // to reconnect to a network session without having to unload all of the scenes and reload all of the scenes.
+ SceneManagerHandler.PopulateLoadedScenes(ref ScenesLoaded, m_NetworkManager);
+ }
+ }
HandleClientSceneEvent(sceneEventData.SceneEventId);
}
else
diff --git a/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs b/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs
index f4c9927180..359a4fd790 100644
--- a/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs
+++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs
@@ -139,6 +139,8 @@ internal class SceneEventData : IDisposable
internal Queue ScenesToSynchronize;
internal Queue SceneHandlesToSynchronize;
+ internal LoadSceneMode ClientSynchronizationMode;
+
///
/// Server Side:
@@ -392,6 +394,10 @@ internal void Serialize(FastBufferWriter writer)
{
writer.WriteValueSafe(SceneEventProgressId);
}
+ else
+ {
+ writer.WriteValueSafe(ClientSynchronizationMode);
+ }
// Write the scene index and handle
writer.WriteValueSafe(SceneHash);
@@ -543,6 +549,10 @@ internal void Deserialize(FastBufferReader reader)
{
reader.ReadValueSafe(out SceneEventProgressId);
}
+ else
+ {
+ reader.ReadValueSafe(out ClientSynchronizationMode);
+ }
reader.ReadValueSafe(out SceneHash);
reader.ReadValueSafe(out SceneHandle);
From 553251503618165b6b4e11dcc0a00ca5dcc0bb82 Mon Sep 17 00:00:00 2001
From: NoelStephensUnity <73188597+NoelStephensUnity@users.noreply.github.com>
Date: Mon, 16 Jan 2023 16:33:58 -0600
Subject: [PATCH 04/25] test
Updating existing integration tests and the additive scene loading test to be able to manually test.
---
.../Runtime/IntegrationTestSceneHandler.cs | 142 ++++++++
.../Assets/Scripts/ConnectionModeScript.cs | 32 +-
.../SceneTransitioningBase1.unity | 332 +++++++++++++++++-
.../NetworkSceneManagerEventNotifications.cs | 2 +-
.../NetworkObjectParentingTests.cs | 7 +-
.../ParentingInSceneObjectsTests.cs | 2 +-
6 files changed, 494 insertions(+), 23 deletions(-)
diff --git a/com.unity.netcode.gameobjects/TestHelpers/Runtime/IntegrationTestSceneHandler.cs b/com.unity.netcode.gameobjects/TestHelpers/Runtime/IntegrationTestSceneHandler.cs
index 80e09c88db..02c96590f7 100644
--- a/com.unity.netcode.gameobjects/TestHelpers/Runtime/IntegrationTestSceneHandler.cs
+++ b/com.unity.netcode.gameobjects/TestHelpers/Runtime/IntegrationTestSceneHandler.cs
@@ -15,6 +15,16 @@ namespace Unity.Netcode.TestHelpers.Runtime
///
internal class IntegrationTestSceneHandler : ISceneManagerHandler, IDisposable
{
+ private Scene m_InvalidScene = new Scene();
+
+ internal struct SceneEntry
+ {
+ public bool IsAssigned;
+ public Scene Scene;
+ }
+
+ internal Dictionary>> SceneNameToSceneHandles = new Dictionary>>();
+
// All IntegrationTestSceneHandler instances register their associated NetworkManager
internal static List NetworkManagers = new List();
@@ -365,6 +375,138 @@ private bool ExcludeSceneFromSynchronizationCheck(Scene scene)
return true;
}
+ public void ClearSceneTracking(NetworkManager networkManager)
+ {
+ SceneNameToSceneHandles.Clear();
+ }
+
+ public void StopTrackingScene(int handle, string name, NetworkManager networkManager)
+ {
+ if (!SceneNameToSceneHandles.ContainsKey(networkManager))
+ {
+ return;
+ }
+
+ if (SceneNameToSceneHandles[networkManager].ContainsKey(name))
+ {
+ if (SceneNameToSceneHandles[networkManager][name].ContainsKey(handle))
+ {
+ SceneNameToSceneHandles[networkManager][name].Remove(handle);
+ if (SceneNameToSceneHandles[networkManager][name].Count == 0)
+ {
+ SceneNameToSceneHandles[networkManager].Remove(name);
+ }
+ }
+ }
+ }
+
+ public void StartTrackingScene(Scene scene, bool assigned, NetworkManager networkManager)
+ {
+ if (!SceneNameToSceneHandles.ContainsKey(networkManager))
+ {
+ SceneNameToSceneHandles.Add(networkManager, new Dictionary>());
+ }
+
+ if (!SceneNameToSceneHandles[networkManager].ContainsKey(scene.name))
+ {
+ SceneNameToSceneHandles[networkManager].Add(scene.name, new Dictionary());
+ }
+
+ if (!SceneNameToSceneHandles[networkManager][scene.name].ContainsKey(scene.handle))
+ {
+ var sceneEntry = new SceneEntry()
+ {
+ IsAssigned = true,
+ Scene = scene
+ };
+ SceneNameToSceneHandles[networkManager][scene.name].Add(scene.handle, sceneEntry);
+ }
+ else
+ {
+ throw new Exception($"[{networkManager.LocalClient.PlayerObject.name}][Duplicate Handle] Scene {scene.name} already has scene handle {scene.handle} registered!");
+ }
+ }
+
+ public bool DoesSceneHaveUnassignedEntry(string sceneName, NetworkManager networkManager)
+ {
+ if (!SceneNameToSceneHandles.ContainsKey(networkManager))
+ {
+ return false;
+ }
+ if (SceneNameToSceneHandles[networkManager].ContainsKey(sceneName))
+ {
+ foreach (var sceneHandleEntry in SceneNameToSceneHandles[networkManager][sceneName])
+ {
+ if (!sceneHandleEntry.Value.IsAssigned)
+ {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ public Scene GetSceneFromLoadedScenes(string sceneName, NetworkManager networkManager)
+ {
+ if (!SceneNameToSceneHandles.ContainsKey(networkManager))
+ {
+ return m_InvalidScene;
+ }
+ if (SceneNameToSceneHandles[networkManager].ContainsKey(sceneName))
+ {
+ foreach (var sceneHandleEntry in SceneNameToSceneHandles[networkManager][sceneName])
+ {
+ if (!sceneHandleEntry.Value.IsAssigned)
+ {
+ var sceneEntry = sceneHandleEntry.Value;
+ sceneEntry.IsAssigned = true;
+ SceneNameToSceneHandles[networkManager][sceneName][sceneHandleEntry.Key] = sceneEntry;
+ return sceneEntry.Scene;
+ }
+ }
+ }
+ // If we found nothing return an invalid scene
+ return m_InvalidScene;
+ }
+
+ public void PopulateLoadedScenes(ref Dictionary scenesLoaded, NetworkManager networkManager)
+ {
+ SceneNameToSceneHandles.Clear();
+ if (!SceneNameToSceneHandles.ContainsKey(networkManager))
+ {
+ SceneNameToSceneHandles.Add(networkManager, new Dictionary>());
+ }
+
+ var sceneCount = SceneManager.sceneCount;
+ for (int i = 0; i < sceneCount; i++)
+ {
+ var scene = SceneManager.GetSceneAt(i);
+ if (!SceneNameToSceneHandles[networkManager].ContainsKey(scene.name))
+ {
+ SceneNameToSceneHandles[networkManager].Add(scene.name, new Dictionary());
+ }
+
+ if (!SceneNameToSceneHandles[networkManager][scene.name].ContainsKey(scene.handle))
+ {
+ var sceneEntry = new SceneEntry()
+ {
+ IsAssigned = false,
+ Scene = scene
+ };
+ SceneNameToSceneHandles[networkManager][scene.name].Add(scene.handle, sceneEntry);
+ if (!scenesLoaded.ContainsKey(scene.handle))
+ {
+ scenesLoaded.Add(scene.handle, scene);
+ }
+ }
+ else
+ {
+ throw new Exception($"[{networkManager.LocalClient.PlayerObject.name}][Duplicate Handle] Scene {scene.name} already has scene handle {scene.handle} registered!");
+ }
+ }
+ }
+
+
///
/// Constructor now must take NetworkManager
///
diff --git a/testproject/Assets/Scripts/ConnectionModeScript.cs b/testproject/Assets/Scripts/ConnectionModeScript.cs
index ace7d3092d..1f80a95a19 100644
--- a/testproject/Assets/Scripts/ConnectionModeScript.cs
+++ b/testproject/Assets/Scripts/ConnectionModeScript.cs
@@ -2,6 +2,7 @@
using UnityEngine;
using Unity.Netcode;
using Unity.Netcode.Transports.UTP;
+using UnityEngine.SceneManagement;
#if ENABLE_RELAY_SERVICE
using System;
using Unity.Services.Core;
@@ -25,6 +26,13 @@ public class ConnectionModeScript : MonoBehaviour
[SerializeField]
private int m_MaxConnections = 10;
+ [SerializeField]
+ private LoadSceneMode m_ClientSynchronizationMode;
+
+ [SerializeField]
+ private GameObject m_DisconnectClientButton;
+
+
private CommandLineProcessor m_CommandLineProcessor;
[HideInInspector]
@@ -95,11 +103,21 @@ private void Start()
}
else
{
-
m_JoinCodeInput.SetActive(false);
m_AuthenticationButtons.SetActive(false);
m_ConnectionModeButtons.SetActive(NetworkManager.Singleton && !NetworkManager.Singleton.IsListening);
}
+ if (m_DisconnectClientButton != null)
+ {
+ if (!NetworkManager.Singleton.IsListening)
+ {
+ m_DisconnectClientButton.SetActive(false);
+ }
+ else
+ {
+ m_DisconnectClientButton.SetActive(!NetworkManager.Singleton.IsServer);
+ }
+ }
}
}
@@ -134,6 +152,7 @@ public void OnStartServerButton()
private void StartServer()
{
NetworkManager.Singleton.StartServer();
+ NetworkManager.Singleton.SceneManager.SetClientSynchronizationMode(m_ClientSynchronizationMode);
OnNotifyConnectionEventServer?.Invoke();
m_ConnectionModeButtons.SetActive(false);
}
@@ -193,6 +212,7 @@ public void OnStartHostButton()
private void StartHost()
{
NetworkManager.Singleton.StartHost();
+ NetworkManager.Singleton.SceneManager.SetClientSynchronizationMode(m_ClientSynchronizationMode);
OnNotifyConnectionEventHost?.Invoke();
m_ConnectionModeButtons.SetActive(false);
}
@@ -220,6 +240,16 @@ private void StartClient()
NetworkManager.Singleton.StartClient();
OnNotifyConnectionEventClient?.Invoke();
m_ConnectionModeButtons.SetActive(false);
+ m_DisconnectClientButton.SetActive(true);
+ }
+
+ public void DisconnectClient()
+ {
+ if (NetworkManager.Singleton.IsListening && !NetworkManager.Singleton.IsServer)
+ {
+ NetworkManager.Singleton.Shutdown();
+ m_ConnectionModeButtons.SetActive(true);
+ }
}
diff --git a/testproject/Assets/Tests/Manual/SceneTransitioningAdditive/SceneTransitioningBase1.unity b/testproject/Assets/Tests/Manual/SceneTransitioningAdditive/SceneTransitioningBase1.unity
index de7c662a05..39f2245def 100644
--- a/testproject/Assets/Tests/Manual/SceneTransitioningAdditive/SceneTransitioningBase1.unity
+++ b/testproject/Assets/Tests/Manual/SceneTransitioningAdditive/SceneTransitioningBase1.unity
@@ -186,6 +186,7 @@ GameObject:
m_Component:
- component: {fileID: 34066665}
- component: {fileID: 34066667}
+ - component: {fileID: 34066668}
m_Layer: 5
m_Name: SwitchSceneParent
m_TagString: Untagged
@@ -231,6 +232,22 @@ MonoBehaviour:
m_EnableAutoSwitch: 0
m_AutoSwitchTimeOut: 60
DisconnectClientUponLoadScene: 0
+--- !u!114 &34066668
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 34066664}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: d5a57f767e5e46a458fc5d3c628d0cbb, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
+ GlobalObjectIdHash: 412081913
+ AlwaysReplicateAsRoot: 0
+ DontDestroyWithOwner: 0
+ AutoObjectParentSync: 1
--- !u!1 &37242881
GameObject:
m_ObjectHideFlags: 0
@@ -638,6 +655,43 @@ MonoBehaviour:
m_ActivateOnLoad: 0
m_SceneToLoad: AdditiveSceneMultiInstance
m_SceneAsset: {fileID: 102900000, guid: 0ae94f636016d3b40bfbecad57d99553, type: 3}
+--- !u!1 &97319464
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 97319465}
+ m_Layer: 5
+ m_Name: DisconnectClientRoot
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!224 &97319465
+RectTransform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 97319464}
+ m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children:
+ - {fileID: 1259474592}
+ m_Father: {fileID: 167044834}
+ m_RootOrder: 1
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+ m_AnchorMin: {x: 0, y: 0}
+ m_AnchorMax: {x: 0, y: 0}
+ m_AnchoredPosition: {x: 300, y: 51}
+ m_SizeDelta: {x: 100, y: 100}
+ m_Pivot: {x: 0.5, y: 0.5}
--- !u!1 &125866602
GameObject:
m_ObjectHideFlags: 0
@@ -843,12 +897,12 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 0cd44c1031e13a943bb63640046fad76, type: 3}
m_Name:
m_EditorClassIdentifier:
- m_UiScaleMode: 0
+ m_UiScaleMode: 1
m_ReferencePixelsPerUnit: 100
m_ScaleFactor: 1
- m_ReferenceResolution: {x: 800, y: 600}
+ m_ReferenceResolution: {x: 1024, y: 768}
m_ScreenMatchMode: 0
- m_MatchWidthOrHeight: 0
+ m_MatchWidthOrHeight: 0.5
m_PhysicalUnit: 3
m_FallbackScreenDPI: 96
m_DefaultSpriteDPI: 96
@@ -888,6 +942,7 @@ RectTransform:
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 1865409449}
+ - {fileID: 97319465}
m_Father: {fileID: 0}
m_RootOrder: 3
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
@@ -1511,6 +1566,86 @@ CanvasRenderer:
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 432733928}
m_CullTransparentMesh: 1
+--- !u!1 &462643752
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 462643753}
+ - component: {fileID: 462643755}
+ - component: {fileID: 462643754}
+ m_Layer: 5
+ m_Name: Text
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!224 &462643753
+RectTransform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 462643752}
+ m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 1259474592}
+ m_RootOrder: 0
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+ m_AnchorMin: {x: 0, y: 0}
+ m_AnchorMax: {x: 1, y: 1}
+ m_AnchoredPosition: {x: 0, y: 0}
+ m_SizeDelta: {x: 0, y: 0}
+ m_Pivot: {x: 0.5, y: 0.5}
+--- !u!114 &462643754
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 462643752}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
+ m_Material: {fileID: 0}
+ m_Color: {r: 1, g: 0.5058824, b: 0.003921569, a: 1}
+ m_RaycastTarget: 1
+ m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
+ m_Maskable: 1
+ m_OnCullStateChanged:
+ m_PersistentCalls:
+ m_Calls: []
+ m_FontData:
+ m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0}
+ m_FontSize: 14
+ m_FontStyle: 0
+ m_BestFit: 0
+ m_MinSize: 10
+ m_MaxSize: 40
+ m_Alignment: 4
+ m_AlignByGeometry: 0
+ m_RichText: 1
+ m_HorizontalOverflow: 0
+ m_VerticalOverflow: 0
+ m_LineSpacing: 1
+ m_Text: Disconnect Client
+--- !u!222 &462643755
+CanvasRenderer:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 462643752}
+ m_CullTransparentMesh: 1
--- !u!1 &562991978
GameObject:
m_ObjectHideFlags: 0
@@ -2349,7 +2484,24 @@ MonoBehaviour:
NetworkTransport: {fileID: 1024114723}
PlayerPrefab: {fileID: 4079352819444256614, guid: c16f03336b6104576a565ef79ad643c0,
type: 3}
- NetworkPrefabs:
+ Prefabs:
+ NetworkPrefabsLists: []
+ TickRate: 32
+ ClientConnectionBufferTimeout: 10
+ ConnectionApproval: 0
+ ConnectionData:
+ EnableTimeResync: 0
+ TimeResyncInterval: 30
+ EnsureNetworkVariableLengthSafety: 0
+ EnableSceneManagement: 1
+ ForceSamePrefabs: 1
+ RecycleNetworkIds: 0
+ NetworkIdRecycleDelay: 120
+ RpcHashSize: 0
+ LoadSceneTimeOut: 120
+ SpawnTimeout: 1
+ EnableNetworkLogs: 1
+ OldPrefabList:
- Override: 0
Prefab: {fileID: 771575417923360811, guid: c0a45bdb516f341498d933b7a7ed4fc1,
type: 3}
@@ -2400,21 +2552,6 @@ MonoBehaviour:
SourcePrefabToOverride: {fileID: 0}
SourceHashToOverride: 0
OverridingTargetPrefab: {fileID: 0}
- TickRate: 32
- ClientConnectionBufferTimeout: 10
- ConnectionApproval: 0
- ConnectionData:
- EnableTimeResync: 0
- TimeResyncInterval: 30
- EnsureNetworkVariableLengthSafety: 0
- EnableSceneManagement: 1
- ForceSamePrefabs: 1
- RecycleNetworkIds: 0
- NetworkIdRecycleDelay: 120
- RpcHashSize: 0
- LoadSceneTimeOut: 120
- SpawnTimeout: 1
- EnableNetworkLogs: 1
--- !u!4 &1024114720
Transform:
m_ObjectHideFlags: 0
@@ -2691,6 +2828,140 @@ CanvasRenderer:
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1210784441}
m_CullTransparentMesh: 1
+--- !u!1 &1259474591
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 1259474592}
+ - component: {fileID: 1259474595}
+ - component: {fileID: 1259474594}
+ - component: {fileID: 1259474593}
+ m_Layer: 5
+ m_Name: DisconnectClient
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!224 &1259474592
+RectTransform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1259474591}
+ m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children:
+ - {fileID: 462643753}
+ m_Father: {fileID: 97319465}
+ m_RootOrder: 0
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+ m_AnchorMin: {x: 0.5, y: 0.5}
+ m_AnchorMax: {x: 0.5, y: 0.5}
+ m_AnchoredPosition: {x: 0, y: 0}
+ m_SizeDelta: {x: 160, y: 30}
+ m_Pivot: {x: 0.5, y: 0.5}
+--- !u!114 &1259474593
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1259474591}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
+ m_Navigation:
+ m_Mode: 3
+ m_WrapAround: 0
+ m_SelectOnUp: {fileID: 0}
+ m_SelectOnDown: {fileID: 0}
+ m_SelectOnLeft: {fileID: 0}
+ m_SelectOnRight: {fileID: 0}
+ m_Transition: 1
+ m_Colors:
+ m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
+ m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
+ m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1}
+ m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
+ m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608}
+ m_ColorMultiplier: 1
+ m_FadeDuration: 0.1
+ m_SpriteState:
+ m_HighlightedSprite: {fileID: 0}
+ m_PressedSprite: {fileID: 0}
+ m_SelectedSprite: {fileID: 0}
+ m_DisabledSprite: {fileID: 0}
+ m_AnimationTriggers:
+ m_NormalTrigger: Normal
+ m_HighlightedTrigger: Highlighted
+ m_PressedTrigger: Pressed
+ m_SelectedTrigger: Selected
+ m_DisabledTrigger: Disabled
+ m_Interactable: 1
+ m_TargetGraphic: {fileID: 1259474594}
+ m_OnClick:
+ m_PersistentCalls:
+ m_Calls:
+ - m_Target: {fileID: 1865409450}
+ m_TargetAssemblyTypeName: ConnectionModeScript, TestProject
+ m_MethodName: DisconnectClient
+ m_Mode: 1
+ m_Arguments:
+ m_ObjectArgument: {fileID: 0}
+ m_ObjectArgumentAssemblyTypeName: UnityEngine.Object, UnityEngine
+ m_IntArgument: 0
+ m_FloatArgument: 0
+ m_StringArgument:
+ m_BoolArgument: 0
+ m_CallState: 2
+--- !u!114 &1259474594
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1259474591}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
+ m_Material: {fileID: 0}
+ m_Color: {r: 0.1981132, g: 0.1981132, b: 0.1981132, a: 1}
+ m_RaycastTarget: 1
+ m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
+ m_Maskable: 1
+ m_OnCullStateChanged:
+ m_PersistentCalls:
+ m_Calls: []
+ m_Sprite: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0}
+ m_Type: 1
+ m_PreserveAspect: 0
+ m_FillCenter: 1
+ m_FillMethod: 4
+ m_FillAmount: 1
+ m_FillClockwise: 1
+ m_FillOrigin: 0
+ m_UseSpriteMesh: 0
+ m_PixelsPerUnitMultiplier: 1
+--- !u!222 &1259474595
+CanvasRenderer:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1259474591}
+ m_CullTransparentMesh: 1
--- !u!1 &1290928582
GameObject:
m_ObjectHideFlags: 0
@@ -4234,6 +4505,7 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 4f231c4fb786f3946a6b90b886c48677, type: 3}
m_Name:
m_EditorClassIdentifier:
+ m_SendPointerHoverToParent: 1
m_HorizontalAxis: Horizontal
m_VerticalAxis: Vertical
m_SubmitButton: Submit
@@ -4455,6 +4727,16 @@ PrefabInstance:
m_Modification:
m_TransformParent: {fileID: 167044834}
m_Modifications:
+ - target: {fileID: 4850072633501053442, guid: d725b5588e1b956458798319e6541d84,
+ type: 3}
+ propertyPath: m_DisconnectClientButton
+ value:
+ objectReference: {fileID: 97319464}
+ - target: {fileID: 4850072633501053442, guid: d725b5588e1b956458798319e6541d84,
+ type: 3}
+ propertyPath: m_ClientSynchronizationMode
+ value: 0
+ objectReference: {fileID: 0}
- target: {fileID: 6633621479308595792, guid: d725b5588e1b956458798319e6541d84,
type: 3}
propertyPath: m_Pivot.x
@@ -4573,6 +4855,18 @@ RectTransform:
type: 3}
m_PrefabInstance: {fileID: 1865409448}
m_PrefabAsset: {fileID: 0}
+--- !u!114 &1865409450 stripped
+MonoBehaviour:
+ m_CorrespondingSourceObject: {fileID: 4850072633501053442, guid: d725b5588e1b956458798319e6541d84,
+ type: 3}
+ m_PrefabInstance: {fileID: 1865409448}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 0}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 50623966c8d88ab40982cc2b0e4c2d2e, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
--- !u!1 &1889006546
GameObject:
m_ObjectHideFlags: 0
diff --git a/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkSceneManagerEventNotifications.cs b/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkSceneManagerEventNotifications.cs
index 3d09e13690..2f8f7b927c 100644
--- a/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkSceneManagerEventNotifications.cs
+++ b/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkSceneManagerEventNotifications.cs
@@ -89,7 +89,7 @@ private void ClientSceneManager_OnSceneEvent(SceneEvent sceneEvent)
{
var matchedClient = m_ClientNetworkManagers.Where(c => c.LocalClientId == sceneEvent.ClientId);
Assert.True(matchedClient.Count() > 0, $"Found no client {nameof(NetworkManager)}s that had a {nameof(NetworkManager.LocalClientId)} of {sceneEvent.ClientId}");
- Assert.AreEqual(matchedClient.First().SceneManager.ClientSynchronizationMode, m_LoadSceneMode);
+ Assert.AreEqual(matchedClient.First().SceneManager.ClientSynchronizationMode, m_ServerNetworkManager.SceneManager.ClientSynchronizationMode);
break;
}
}
diff --git a/testproject/Assets/Tests/Runtime/ObjectParenting/NetworkObjectParentingTests.cs b/testproject/Assets/Tests/Runtime/ObjectParenting/NetworkObjectParentingTests.cs
index 9222db9140..f6b3925d55 100644
--- a/testproject/Assets/Tests/Runtime/ObjectParenting/NetworkObjectParentingTests.cs
+++ b/testproject/Assets/Tests/Runtime/ObjectParenting/NetworkObjectParentingTests.cs
@@ -38,6 +38,11 @@ private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
}
}
+ private void InvokeBeforeClientsStart()
+ {
+ m_ServerNetworkManager.SceneManager.ClientSynchronizationMode = LoadSceneMode.Additive;
+ }
+
[UnitySetUp]
public IEnumerator Setup()
{
@@ -83,7 +88,7 @@ public IEnumerator Setup()
}
// Start server and client NetworkManager instances
- Assert.That(NetcodeIntegrationTestHelpers.Start(true, m_ServerNetworkManager, m_ClientNetworkManagers));
+ Assert.That(NetcodeIntegrationTestHelpers.Start(true, m_ServerNetworkManager, m_ClientNetworkManagers, InvokeBeforeClientsStart));
// Wait for connection on client side
yield return NetcodeIntegrationTestHelpers.WaitForClientsConnected(m_ClientNetworkManagers);
diff --git a/testproject/Assets/Tests/Runtime/ObjectParenting/ParentingInSceneObjectsTests.cs b/testproject/Assets/Tests/Runtime/ObjectParenting/ParentingInSceneObjectsTests.cs
index c1853c0d0b..0f9faeb247 100644
--- a/testproject/Assets/Tests/Runtime/ObjectParenting/ParentingInSceneObjectsTests.cs
+++ b/testproject/Assets/Tests/Runtime/ObjectParenting/ParentingInSceneObjectsTests.cs
@@ -328,7 +328,7 @@ public IEnumerator DespawnParentTest([Values] ParentingSpace parentingSpace)
SceneManager.LoadScene(k_BaseSceneToLoad, LoadSceneMode.Additive);
m_InitialClientsLoadedScene = false;
m_ServerNetworkManager.SceneManager.OnSceneEvent += SceneManager_OnSceneEvent;
-
+ m_ServerNetworkManager.SceneManager.ClientSynchronizationMode = LoadSceneMode.Additive;
var sceneEventStartedStatus = m_ServerNetworkManager.SceneManager.LoadScene(k_TestSceneToLoad, LoadSceneMode.Additive);
Assert.True(sceneEventStartedStatus == SceneEventProgressStatus.Started, $"Failed to load scene {k_TestSceneToLoad} with a return status of {sceneEventStartedStatus}.");
yield return WaitForConditionOrTimeOut(() => m_InitialClientsLoadedScene);
From 27c601a4a7651c9ede72e3711ccfafc6aaecbb0b Mon Sep 17 00:00:00 2001
From: Noel Stephens
Date: Thu, 19 Jan 2023 14:52:32 -0600
Subject: [PATCH 05/25] Update com.unity.netcode.gameobjects/CHANGELOG.md
Co-authored-by: Sam Bellomo <71790295+SamuelBellomo@users.noreply.github.com>
---
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 9381a396e6..4814d05cb9 100644
--- a/com.unity.netcode.gameobjects/CHANGELOG.md
+++ b/com.unity.netcode.gameobjects/CHANGELOG.md
@@ -22,6 +22,7 @@ Additional documentation and release notes are available at [Multiplayer Documen
### Fixed
- Fixed issue when the ClientSynchronizationMode is additive and the server changes the currently active scene prior to a client connecting then upon a client connecting and being synchronized the NetworkSceneManager would clear its internal ScenePlacedObjects list that could already be populated. (#2383)
+ - This removes the need to resync the whole scene when players disconnect and reconnect while still being in the same scene.
- Fixed a UTP test that was failing when you install Unity Transport package 2.0.0 or newer. (#2347)
- Fixed issue where `NetcodeSettingsProvider` would throw an exception in Unity 2020.3.x versions. (#2345)
- Fixed server side issue where, depending upon component ordering, some NetworkBehaviour components might not have their OnNetworkDespawn method invoked if the client side disconnected. (#2323)
From 22c5d6d3c53a9dce718afb4c59a3864b1250b245 Mon Sep 17 00:00:00 2001
From: NoelStephensUnity <73188597+NoelStephensUnity@users.noreply.github.com>
Date: Thu, 19 Jan 2023 17:49:42 -0600
Subject: [PATCH 06/25] update
Adding a piece to the puzzle where a client has scenes that don't need to be loaded or is disconnected, scenes are still loaded, and some short period later reconnects but certain scenes on the server-side are no longer loaded... under this scenario the client side will unload those scenes.
Still figuring out how to best distinguish between scenes you want to keep loaded but don't get synchronized and those that do get synchronized and you do want to unload.
---
.../DefaultSceneManagerHandler.cs | 48 +++++++++++++
.../SceneManagement/ISceneManagerHandler.cs | 2 +
.../SceneManagement/NetworkSceneManager.cs | 2 +
.../Runtime/IntegrationTestSceneHandler.cs | 69 +++++++++++++++++++
4 files changed, 121 insertions(+)
diff --git a/com.unity.netcode.gameobjects/Runtime/SceneManagement/DefaultSceneManagerHandler.cs b/com.unity.netcode.gameobjects/Runtime/SceneManagement/DefaultSceneManagerHandler.cs
index 7e7b9ab949..a85eb74577 100644
--- a/com.unity.netcode.gameobjects/Runtime/SceneManagement/DefaultSceneManagerHandler.cs
+++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/DefaultSceneManagerHandler.cs
@@ -165,5 +165,53 @@ public void PopulateLoadedScenes(ref Dictionary scenesLoaded, Networ
}
}
}
+
+ private List m_ScenesToUnload = new List();
+
+ ///
+ /// Unloads any scenes that have not been assigned.
+ /// TODO: There needs to be a way to validate if a scene should be unloaded
+ /// or not (i.e. local client-side UI loaded additively)
+ ///
+ ///
+ public void UnloadUnassignedScenes(NetworkManager networkManager = null)
+ {
+ SceneManager.sceneUnloaded += SceneManager_SceneUnloaded;
+ foreach (var sceneEntry in SceneNameToSceneHandles)
+ {
+ var scenHandleEntries = SceneNameToSceneHandles[sceneEntry.Key];
+ foreach (var sceneHandleEntry in scenHandleEntries)
+ {
+ if (!sceneHandleEntry.Value.IsAssigned)
+ {
+ m_ScenesToUnload.Add(sceneHandleEntry.Value.Scene);
+ }
+ }
+ }
+ foreach (var sceneToUnload in m_ScenesToUnload)
+ {
+ SceneManager.UnloadSceneAsync(sceneToUnload);
+ }
+ }
+
+ private void SceneManager_SceneUnloaded(Scene scene)
+ {
+ if (SceneNameToSceneHandles.ContainsKey(scene.name))
+ {
+ if (SceneNameToSceneHandles[scene.name].ContainsKey(scene.handle))
+ {
+ SceneNameToSceneHandles[scene.name].Remove(scene.handle);
+ }
+ if (SceneNameToSceneHandles[scene.name].Count == 0)
+ {
+ SceneNameToSceneHandles.Remove(scene.name);
+ }
+ m_ScenesToUnload.Remove(scene);
+ if (m_ScenesToUnload.Count == 0)
+ {
+ SceneManager.sceneUnloaded -= SceneManager_SceneUnloaded;
+ }
+ }
+ }
}
}
diff --git a/com.unity.netcode.gameobjects/Runtime/SceneManagement/ISceneManagerHandler.cs b/com.unity.netcode.gameobjects/Runtime/SceneManagement/ISceneManagerHandler.cs
index ccb7abcd8d..b9073d0da5 100644
--- a/com.unity.netcode.gameobjects/Runtime/SceneManagement/ISceneManagerHandler.cs
+++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/ISceneManagerHandler.cs
@@ -24,5 +24,7 @@ internal interface ISceneManagerHandler
void StartTrackingScene(Scene scene, bool assigned, NetworkManager networkManager = null);
void ClearSceneTracking(NetworkManager networkManager = null);
+
+ void UnloadUnassignedScenes(NetworkManager networkManager = null);
}
}
diff --git a/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs b/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs
index feb601c582..993a26e422 100644
--- a/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs
+++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs
@@ -1778,6 +1778,8 @@ private void HandleClientSceneEvent(uint sceneEventId)
OnSynchronizeComplete?.Invoke(m_NetworkManager.LocalClientId);
+ SceneManagerHandler.UnloadUnassignedScenes(m_NetworkManager);
+
EndSceneEvent(sceneEventId);
}
break;
diff --git a/com.unity.netcode.gameobjects/TestHelpers/Runtime/IntegrationTestSceneHandler.cs b/com.unity.netcode.gameobjects/TestHelpers/Runtime/IntegrationTestSceneHandler.cs
index 02c96590f7..f9f3b78d73 100644
--- a/com.unity.netcode.gameobjects/TestHelpers/Runtime/IntegrationTestSceneHandler.cs
+++ b/com.unity.netcode.gameobjects/TestHelpers/Runtime/IntegrationTestSceneHandler.cs
@@ -506,6 +506,75 @@ public void PopulateLoadedScenes(ref Dictionary scenesLoaded, Networ
}
}
+ private Dictionary m_ScenesToUnload = new Dictionary();
+
+ // When true, any remaining scenes loaded will be unloaded
+ // TODO: There needs to be a way to validate if a scene should be unloaded
+ // or not (i.e. local client-side UI loaded additively)
+ public bool AllowUnassignedScenesToBeUnloaded = false;
+
+ ///
+ /// Handles unloading any scenes that might remain on a client that
+ /// need to be unloaded.
+ ///
+ ///
+ public void UnloadUnassignedScenes(NetworkManager networkManager = null)
+ {
+ // Only if we are specifically testing this functionality
+ if (!AllowUnassignedScenesToBeUnloaded)
+ {
+ return;
+ }
+
+ SceneManager.sceneUnloaded += SceneManager_SceneUnloaded;
+ var relativeSceneNameToSceneHandles = SceneNameToSceneHandles[networkManager];
+
+ foreach (var sceneEntry in relativeSceneNameToSceneHandles)
+ {
+ var scenHandleEntries = relativeSceneNameToSceneHandles[sceneEntry.Key];
+ foreach (var sceneHandleEntry in scenHandleEntries)
+ {
+ if (!sceneHandleEntry.Value.IsAssigned)
+ {
+ m_ScenesToUnload.Add(sceneHandleEntry.Value.Scene, networkManager);
+ }
+ }
+ }
+ foreach (var sceneToUnload in m_ScenesToUnload)
+ {
+ SceneManager.UnloadSceneAsync(sceneToUnload.Key);
+ }
+ }
+
+ ///
+ /// Removes the scene entry from the scene name to scene handle table
+ ///
+ private void SceneManager_SceneUnloaded(Scene scene)
+ {
+ if (m_ScenesToUnload.ContainsKey(scene))
+ {
+ var networkManager = m_ScenesToUnload[scene];
+ var relativeSceneNameToSceneHandles = SceneNameToSceneHandles[networkManager];
+ if (relativeSceneNameToSceneHandles.ContainsKey(scene.name))
+ {
+ var scenHandleEntries = relativeSceneNameToSceneHandles[scene.name];
+ if (scenHandleEntries.ContainsKey(scene.handle))
+ {
+ scenHandleEntries.Remove(scene.handle);
+ if (scenHandleEntries.Count == 0)
+ {
+ relativeSceneNameToSceneHandles.Remove(scene.name);
+ }
+ m_ScenesToUnload.Remove(scene);
+ if (m_ScenesToUnload.Count == 0)
+ {
+ SceneManager.sceneUnloaded -= SceneManager_SceneUnloaded;
+ }
+ }
+ }
+ }
+ }
+
///
/// Constructor now must take NetworkManager
From f20cf0859d2ae9b5a8eb131f3c39187ed2c8b66d Mon Sep 17 00:00:00 2001
From: NoelStephensUnity <73188597+NoelStephensUnity@users.noreply.github.com>
Date: Thu, 9 Feb 2023 19:46:24 -0600
Subject: [PATCH 07/25] update
Auto scene migration for don't destroy with scene (migrates to the currently active scene when the scene is unloaded) and added auto active scene synchronization where, when set, they automatically migrate into the new active scene.
Includes auto-scene synchronization for late joining clients and dynamically spawned NetworkObjects.
---
.../Runtime/Core/NetworkManager.cs | 3 +
.../Runtime/Core/NetworkObject.cs | 86 ++++-
.../SceneManagement/NetworkSceneManager.cs | 293 +++++++++++++++---
.../Runtime/SceneManagement/SceneEventData.cs | 22 +-
.../Runtime/Spawning/NetworkSpawnManager.cs | 29 ++
5 files changed, 385 insertions(+), 48 deletions(-)
diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs
index 9fb0c1a7cb..894476a555 100644
--- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs
+++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs
@@ -1976,6 +1976,9 @@ internal void HandleConnectionApproval(ulong ownerClientId, ConnectionApprovalRe
var playerPrefabHash = response.PlayerPrefabHash ?? NetworkConfig.PlayerPrefab.GetComponent().GlobalObjectIdHash;
// Generate a SceneObject for the player object to spawn
+ // Note: This is only to create the local NetworkObject,
+ // many of the serialized properties of the player prefab
+ // will be set when instantiated.
var sceneObject = new NetworkObject.SceneObject
{
OwnerClientId = ownerClientId,
diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs
index 1e52e52cb3..5c46336350 100644
--- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs
+++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs
@@ -103,7 +103,14 @@ internal void GenerateGlobalObjectIdHash()
///
/// Gets whether or not the object should be automatically removed when the scene is unloaded.
///
- public bool DestroyWithScene { get; set; }
+ public bool DestroyWithScene = true;
+
+ ///
+ /// When set to true, this will automatically migrate the NetworkObject to a newly assigned active scene
+ ///
+ public bool AutoSynchActiveScene;
+
+ public Action MigratedToNewScene;
///
/// Delegate type for checking visibility
@@ -188,6 +195,11 @@ public bool IsNetworkVisibleTo(ulong clientId)
///
internal int SceneOriginHandle = 0;
+ ///
+ /// The server-side scene origin handle
+ ///
+ internal int NetworkSceneHandle = 0;
+
private Scene m_SceneOrigin;
///
/// The scene where the NetworkObject was first instantiated
@@ -1118,6 +1130,12 @@ public bool WorldPositionStays
set => ByteUtility.SetBit(ref m_BitField, 5, value);
}
+ public bool DestroyWithScene
+ {
+ get => ByteUtility.GetBit(m_BitField, 6);
+ set => ByteUtility.SetBit(ref m_BitField, 6, value);
+ }
+
//If(Metadata.HasParent)
public ulong ParentObjectId;
@@ -1160,7 +1178,8 @@ public void Serialize(FastBufferWriter writer)
var writeSize = 0;
writeSize += HasTransform ? FastBufferWriter.GetWriteSize() : 0;
- writeSize += IsSceneObject ? FastBufferWriter.GetWriteSize() : 0;
+ //writeSize += IsSceneObject ? FastBufferWriter.GetWriteSize() : 0;
+ writeSize += FastBufferWriter.GetWriteSize();
if (!writer.TryBeginWrite(writeSize))
{
@@ -1176,10 +1195,11 @@ public void Serialize(FastBufferWriter writer)
// NetworkSceneHandle and GlobalObjectIdHash. Client-side NetworkSceneManagers use
// this to locate their local instance of the in-scene placed NetworkObject instance.
// Only written for in-scene placed NetworkObjects.
- if (IsSceneObject)
- {
- writer.WriteValue(OwnerObject.GetSceneOriginHandle());
- }
+ //if (IsSceneObject)
+ //{
+ // writer.WriteValue(OwnerObject.GetSceneOriginHandle());
+ //}
+ writer.WriteValue(OwnerObject.GetSceneOriginHandle());
// Synchronize NetworkVariables and NetworkBehaviours
var bufferSerializer = new BufferSerializer(new BufferSerializerWriter(writer));
@@ -1205,7 +1225,8 @@ public void Deserialize(FastBufferReader reader)
var readSize = 0;
readSize += HasTransform ? FastBufferWriter.GetWriteSize() : 0;
- readSize += IsSceneObject ? FastBufferWriter.GetWriteSize() : 0;
+ //readSize += IsSceneObject ? FastBufferWriter.GetWriteSize() : 0;
+ readSize += FastBufferWriter.GetWriteSize();
// Try to begin reading the remaining bytes
if (!reader.TryBeginRead(readSize))
@@ -1222,10 +1243,11 @@ public void Deserialize(FastBufferReader reader)
// NetworkSceneHandle and GlobalObjectIdHash. Client-side NetworkSceneManagers use
// this to locate their local instance of the in-scene placed NetworkObject instance.
// Only read for in-scene placed NetworkObjects
- if (IsSceneObject)
- {
- reader.ReadValue(out NetworkSceneHandle);
- }
+ //if (IsSceneObject)
+ //{
+ // reader.ReadValue(out NetworkSceneHandle);
+ //}
+ reader.ReadValue(out NetworkSceneHandle);
}
}
@@ -1317,6 +1339,7 @@ internal SceneObject GetMessageSceneObject(ulong targetClientId)
OwnerClientId = OwnerClientId,
IsPlayerObject = IsPlayerObject,
IsSceneObject = IsSceneObject ?? true,
+ DestroyWithScene = DestroyWithScene,
Hash = HostCheckForGlobalObjectIdHashOverride(),
OwnerObject = this,
TargetClientId = targetClientId
@@ -1440,6 +1463,47 @@ internal static NetworkObject AddSceneObject(in SceneObject sceneObject, FastBuf
return networkObject;
}
+ ///
+ /// Subscribes to changes in the currently active scene
+ ///
+ ///
+ /// Only for dynamically spawned NetworkObjects
+ ///
+ internal void SubscribeToActiveSceneForSynch()
+ {
+ if (AutoSynchActiveScene)
+ {
+ if (IsSceneObject.HasValue && !IsSceneObject.Value)
+ {
+ // Just in case it is a recycled NetworkObject, unsubscribe first
+ SceneManager.activeSceneChanged -= CurrentlyActiveSceneChanged;
+ SceneManager.activeSceneChanged += CurrentlyActiveSceneChanged;
+ }
+ }
+ }
+
+ private void CurrentlyActiveSceneChanged(Scene current, Scene next)
+ {
+ // This check is here in the event a user wants to disable this for some reason but also wants
+ // the NetworkObject to synchronize to changes in the currently active scene at some later time.
+ if (AutoSynchActiveScene)
+ {
+ // Only dynamically spawned NetworkObjects that are not already in the newly assigned active scene will migrate
+ // and update their scene handles
+ if (IsSceneObject.HasValue && !IsSceneObject.Value && gameObject.scene != next && gameObject.transform.parent == null)
+ {
+ SceneManager.MoveGameObjectToScene(gameObject, next);
+ SceneOriginHandle = next.handle;
+ if (NetworkManager.SceneManager.ClientSceneHandleToServerSceneHandle.ContainsKey(SceneOriginHandle))
+ {
+ NetworkSceneHandle = NetworkManager.SceneManager.ClientSceneHandleToServerSceneHandle[SceneOriginHandle];
+ }
+
+ MigratedToNewScene?.Invoke();
+ }
+ }
+ }
+
///
/// Only applies to Host mode.
/// Will return the registered source NetworkPrefab's GlobalObjectIdHash if one exists.
diff --git a/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs b/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs
index 993a26e422..5dc854541c 100644
--- a/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs
+++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs
@@ -328,6 +328,33 @@ public class NetworkSceneManager : IDisposable
///
public VerifySceneBeforeLoadingDelegateHandler VerifySceneBeforeLoading;
+ private bool m_ActiveSceneSynchronizationEnabled;
+ ///
+ /// When enabled, the server or host will synchronize clients with changes to the currently active scene
+ ///
+ public bool ActiveSceneSynchronizationEnabled
+ {
+ get
+ {
+ return m_ActiveSceneSynchronizationEnabled;
+ }
+ set
+ {
+ if (m_ActiveSceneSynchronizationEnabled != value)
+ {
+ m_ActiveSceneSynchronizationEnabled = value;
+ if (m_ActiveSceneSynchronizationEnabled)
+ {
+ SceneManager.activeSceneChanged += SceneManager_activeSceneChanged;
+ }
+ else
+ {
+ SceneManager.activeSceneChanged -= SceneManager_activeSceneChanged;
+ }
+ }
+ }
+ }
+
///
/// The SceneManagerHandler implementation
///
@@ -365,6 +392,72 @@ public class NetworkSceneManager : IDisposable
/// instances with client unique scene instances
///
internal Dictionary ServerSceneHandleToClientSceneHandle = new Dictionary();
+ internal Dictionary ClientSceneHandleToServerSceneHandle = new Dictionary();
+ internal Dictionary HandleToScene = new Dictionary();
+
+ internal bool UpdateServerClientSceneHandle(int serverHandle, int clientHandle, Scene localScene)
+ {
+ if (!ServerSceneHandleToClientSceneHandle.ContainsKey(serverHandle))
+ {
+ ServerSceneHandleToClientSceneHandle.Add(serverHandle, clientHandle);
+ }
+ else
+ {
+ return false;
+ }
+
+ if (!ClientSceneHandleToServerSceneHandle.ContainsKey(clientHandle))
+ {
+ ClientSceneHandleToServerSceneHandle.Add(clientHandle, serverHandle);
+ }
+ else
+ {
+ return false;
+ }
+
+ if (!HandleToScene.ContainsKey(clientHandle))
+ {
+ HandleToScene.Add(clientHandle, localScene);
+ }
+ else
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ internal bool RemoveServerClientSceneHandle(int serverHandle, int clientHandle)
+ {
+ if (ServerSceneHandleToClientSceneHandle.ContainsKey(serverHandle))
+ {
+ ServerSceneHandleToClientSceneHandle.Remove(serverHandle);
+ }
+ else
+ {
+ return false;
+ }
+
+ if (ClientSceneHandleToServerSceneHandle.ContainsKey(clientHandle))
+ {
+ ClientSceneHandleToServerSceneHandle.Remove(clientHandle);
+ }
+ else
+ {
+ return false;
+ }
+
+ if (HandleToScene.ContainsKey(clientHandle))
+ {
+ HandleToScene.Remove(clientHandle);
+ }
+ else
+ {
+ return false;
+ }
+
+ return true;
+ }
///
/// Hash to build index lookup table
@@ -419,7 +512,7 @@ public void Dispose()
foreach (var keypair in SceneEventDataStore)
{
- if (NetworkLog.CurrentLogLevel <= LogLevel.Normal)
+ if (NetworkLog.CurrentLogLevel == LogLevel.Developer)
{
NetworkLog.LogInfo($"{nameof(SceneEventDataStore)} is disposing {nameof(SceneEventData.SceneEventId)} '{keypair.Key}'.");
}
@@ -590,10 +683,42 @@ internal NetworkSceneManager(NetworkManager networkManager)
// Since NetworkManager is now always migrated to the DDOL we will use this to get the DDOL scene
DontDestroyOnLoadScene = networkManager.gameObject.scene;
- ServerSceneHandleToClientSceneHandle.Add(DontDestroyOnLoadScene.handle, DontDestroyOnLoadScene.handle);
+ UpdateServerClientSceneHandle(DontDestroyOnLoadScene.handle, DontDestroyOnLoadScene.handle, DontDestroyOnLoadScene);
ScenesLoaded.Add(DontDestroyOnLoadScene.handle, DontDestroyOnLoadScene);
}
+ ///
+ /// Synchronizes clients when the currently active scene is changed
+ ///
+ private void SceneManager_activeSceneChanged(Scene current, Scene next)
+ {
+ // If no clients are connected, then don't worry about notifications
+ if (!(m_NetworkManager.ConnectedClientsIds.Count > (m_NetworkManager.IsHost ? 1 : 0)))
+ {
+ return;
+ }
+
+ // Don't notify if a scene event is in progress
+ foreach (var sceneEventEntry in SceneEventProgressTracking)
+ {
+ if (!sceneEventEntry.Value.HasTimedOut() && sceneEventEntry.Value.Status == SceneEventProgressStatus.Started)
+ {
+ return;
+ }
+ }
+
+ // If the scene's build index is in the hash table
+ if (BuildIndexToHash.ContainsKey(next.buildIndex))
+ {
+ // Notify clients of the change in active scene
+ var sceneEvent = BeginSceneEvent();
+ sceneEvent.SceneEventType = SceneEventType.ActiveSceneChanged;
+ sceneEvent.ActiveSceneHash = BuildIndexToHash[next.buildIndex];
+ SendSceneEventData(sceneEvent.SceneEventId, m_NetworkManager.ConnectedClientsIds.Where(c => c != NetworkManager.ServerClientId).ToArray());
+ EndSceneEvent(sceneEvent.SceneEventId);
+ }
+ }
+
///
/// If the VerifySceneBeforeLoading delegate handler has been set by the user, this will provide
/// an additional level of security and/or validation that the scene being loaded in the specified
@@ -768,7 +893,7 @@ private void SendSceneEventData(uint sceneEventId, ulong[] targetClientIds)
///
/// the scene to be unloaded
///
- private SceneEventProgress ValidateSceneEventUnLoading(Scene scene)
+ private SceneEventProgress ValidateSceneEventUnloading(Scene scene)
{
if (!m_NetworkManager.IsServer)
{
@@ -919,7 +1044,7 @@ public SceneEventProgressStatus UnloadScene(Scene scene)
return SceneEventProgressStatus.SceneNotLoaded;
}
- var sceneEventProgress = ValidateSceneEventUnLoading(scene);
+ var sceneEventProgress = ValidateSceneEventUnloading(scene);
if (sceneEventProgress.Status != SceneEventProgressStatus.Started)
{
return sceneEventProgress.Status;
@@ -930,6 +1055,10 @@ public SceneEventProgressStatus UnloadScene(Scene scene)
Debug.LogError($"{nameof(UnloadScene)} internal error! {sceneName} with handle {scene.handle} is not within the internal scenes loaded dictionary!");
return SceneEventProgressStatus.InternalNetcodeError;
}
+
+ // Persist NetworkObjects marked to not be destroyed in the scene to be unloaded
+ MoveObjectsFromSceneToDontDestroyOnLoad(scene);
+
var sceneEventData = BeginSceneEvent();
sceneEventData.SceneEventProgressId = sceneEventProgress.Guid;
sceneEventData.SceneEventType = SceneEventType.Unload;
@@ -944,7 +1073,6 @@ public SceneEventProgressStatus UnloadScene(Scene scene)
sceneEventProgress.SceneEventId = sceneEventData.SceneEventId;
sceneEventProgress.OnSceneEventCompleted = OnSceneUnloaded;
var sceneUnload = SceneManagerHandler.UnloadSceneAsync(scene, sceneEventProgress);
-
// Notify local server that a scene is going to be unloaded
OnSceneEvent?.Invoke(new SceneEvent()
{
@@ -986,16 +1114,26 @@ private void OnClientUnloadScene(uint sceneEventId)
throw new Exception($"Client failed to unload scene {sceneName} " +
$"because the client scene handle {sceneHandle} was not found in ScenesLoaded!");
}
+
+ var scene = ScenesLoaded[sceneHandle];
+ // Persist NetworkObjects marked to not be destroyed in the scene to be unloaded
+ MoveObjectsFromSceneToDontDestroyOnLoad(scene);
+
m_IsSceneEventActive = true;
var sceneEventProgress = new SceneEventProgress(m_NetworkManager);
sceneEventProgress.SceneEventId = sceneEventData.SceneEventId;
sceneEventProgress.OnSceneEventCompleted = OnSceneUnloaded;
- var sceneUnload = SceneManagerHandler.UnloadSceneAsync(ScenesLoaded[sceneHandle], sceneEventProgress);
+ var sceneUnload = SceneManagerHandler.UnloadSceneAsync(scene, sceneEventProgress);
ScenesLoaded.Remove(sceneHandle);
SceneManagerHandler.StopTrackingScene(sceneHandle, sceneName, m_NetworkManager);
+
// Remove our server to scene handle lookup
- ServerSceneHandleToClientSceneHandle.Remove(sceneEventData.SceneHandle);
+ if (!RemoveServerClientSceneHandle(sceneEventData.SceneHandle, sceneHandle))
+ {
+ // If the exact same handle exists then there are problems with using handles
+ throw new Exception($"Failed to remove server scene handle ({sceneEventData.SceneHandle}) or client scene handle({sceneHandle})! Happened during scene unload for {sceneName}.");
+ }
// Notify the local client that a scene is going to be unloaded
OnSceneEvent?.Invoke(new SceneEvent()
@@ -1022,6 +1160,9 @@ private void OnSceneUnloaded(uint sceneEventId)
return;
}
+ // Migrate the NetworkObjects marked to not be destroyed with the scene into the currently active scene
+ MoveObjectsFromDontDestroyOnLoadToScene(SceneManager.GetActiveScene());
+
var sceneEventData = SceneEventDataStore[sceneEventId];
// First thing we do, if we are a server, is to send the unload scene event.
if (m_NetworkManager.IsServer)
@@ -1384,11 +1525,7 @@ private void OnSceneLoaded(uint sceneEventId)
else
{
// For the client, we make a server scene handle to client scene handle look up table
- if (!ServerSceneHandleToClientSceneHandle.ContainsKey(sceneEventData.SceneHandle))
- {
- ServerSceneHandleToClientSceneHandle.Add(sceneEventData.SceneHandle, nextScene.handle);
- }
- else
+ if (!UpdateServerClientSceneHandle(sceneEventData.SceneHandle, nextScene.handle, nextScene))
{
// If the exact same handle exists then there are problems with using handles
throw new Exception($"Server Scene Handle ({sceneEventData.SceneHandle}) already exist! Happened during scene load of {nextScene.name} with Client Handle ({nextScene.handle})");
@@ -1517,6 +1654,10 @@ internal void SynchronizeNetworkObjects(ulong clientId)
sceneEventData.LoadSceneMode = ClientSynchronizationMode;
var activeScene = SceneManager.GetActiveScene();
sceneEventData.SceneEventType = SceneEventType.Synchronize;
+ if (BuildIndexToHash.ContainsKey(activeScene.buildIndex))
+ {
+ sceneEventData.ActiveSceneHash = BuildIndexToHash[activeScene.buildIndex];
+ }
// Organize how (and when) we serialize our NetworkObjects
for (int i = 0; i < SceneManager.sceneCount; i++)
@@ -1681,11 +1822,8 @@ private void ClientLoadedSynchronization(uint sceneEventId)
SceneManager.SetActiveScene(nextScene);
}
- if (!ServerSceneHandleToClientSceneHandle.ContainsKey(sceneEventData.NetworkSceneHandle))
- {
- ServerSceneHandleToClientSceneHandle.Add(sceneEventData.NetworkSceneHandle, nextScene.handle);
- }
- else
+ // For the client, we make a server scene handle to client scene handle look up table
+ if (!UpdateServerClientSceneHandle(sceneEventData.NetworkSceneHandle, nextScene.handle, nextScene))
{
// If the exact same handle exists then there are problems with using handles
throw new Exception($"Server Scene Handle ({sceneEventData.SceneHandle}) already exist! Happened during scene load of {nextScene.name} with Client Handle ({nextScene.handle})");
@@ -1727,6 +1865,38 @@ private void ClientLoadedSynchronization(uint sceneEventId)
HandleClientSceneEvent(sceneEventId);
}
+ private void MigrateNetworkObjectsToTheirProperScenes()
+ {
+ foreach (var networkObject in m_NetworkManager.SpawnManager.SpawnedObjectsList)
+ {
+ // This is only done for dynamically spawned NetworkObjects
+ // Theoretically, a server could have NetworkObjects in a server-side only scene, if the client doesn't have that scene loaded
+ // then skip it.
+ if (networkObject.IsSceneObject != false && ServerSceneHandleToClientSceneHandle.ContainsKey(networkObject.NetworkSceneHandle))
+ {
+ networkObject.SceneOriginHandle = ServerSceneHandleToClientSceneHandle[networkObject.NetworkSceneHandle];
+
+ // If the NetworkObject is not in the scene it should be in, then find the right scene and migrate it to that scene
+ // only if it does not have a parent.
+ if (networkObject.gameObject.scene.handle != networkObject.SceneOriginHandle && networkObject.transform.parent == null)
+ {
+ var scene = HandleToScene[networkObject.SceneOriginHandle];
+ SceneManager.MoveGameObjectToScene(networkObject.gameObject, scene);
+
+ //for (int i = 0; i < SceneManager.sceneCount; i++)
+ //{
+ // var scene = SceneManager.GetSceneAt(i);
+ // if (networkObject.SceneOriginHandle == scene.handle)
+ // {
+ // SceneManager.MoveGameObjectToScene(networkObject.gameObject, scene);
+ // break;
+ // }
+ //}
+ }
+ }
+ }
+ }
+
///
/// Client Side:
/// Handles incoming Scene_Event messages for clients
@@ -1737,6 +1907,18 @@ private void HandleClientSceneEvent(uint sceneEventId)
var sceneEventData = SceneEventDataStore[sceneEventId];
switch (sceneEventData.SceneEventType)
{
+ case SceneEventType.ActiveSceneChanged:
+ {
+ if (HashToBuildIndex.ContainsKey(sceneEventData.ActiveSceneHash))
+ {
+ var scene = SceneManager.GetSceneByBuildIndex(HashToBuildIndex[sceneEventData.ActiveSceneHash]);
+ if (scene.isLoaded)
+ {
+ SceneManager.SetActiveScene(scene);
+ }
+ }
+ break;
+ }
case SceneEventType.Load:
{
OnClientSceneLoadingEvent(sceneEventId);
@@ -1760,6 +1942,19 @@ private void HandleClientSceneEvent(uint sceneEventId)
// Synchronize the NetworkObjects for this scene
sceneEventData.SynchronizeSceneNetworkObjects(m_NetworkManager);
+ // Now set the active scene (if needed)
+ if (HashToBuildIndex.ContainsKey(sceneEventData.ActiveSceneHash))
+ {
+ var targetActiveScene = SceneManager.GetSceneByBuildIndex(HashToBuildIndex[sceneEventData.ActiveSceneHash]);
+ if (targetActiveScene.isLoaded && targetActiveScene.handle != SceneManager.GetActiveScene().handle)
+ {
+ SceneManager.SetActiveScene(targetActiveScene);
+ }
+ }
+
+ // Now migrate any dynamically spawned NetworkObjects that are not in the same scene as on the server side
+ MigrateNetworkObjectsToTheirProperScenes();
+
sceneEventData.SceneEventType = SceneEventType.SynchronizeComplete;
SendSceneEventData(sceneEventId, new ulong[] { NetworkManager.ServerClientId });
@@ -1922,8 +2117,6 @@ private void HandleServerSceneEvent(uint sceneEventId, ulong clientId)
}
}
-
-
///
/// Both Client and Server: Incoming scene event entry point
///
@@ -1972,32 +2165,63 @@ internal void HandleSceneEvent(ulong clientId, FastBufferReader reader)
}
}
+ internal void MoveObjectsFromSceneToDontDestroyOnLoad(Scene scene)
+ {
+ // Create a local copy of the spawned objects list since the spawn manager will adjust the list as objects
+ // are despawned.
+ var localSpawnedObjectsHashSet = new HashSet(m_NetworkManager.SpawnManager.SpawnedObjectsList);
+ foreach (var networkObject in localSpawnedObjectsHashSet)
+ {
+ if (networkObject == null || (networkObject != null && networkObject.gameObject.scene != scene))
+ {
+ continue;
+ }
+
+ // Only NetworkObjects marked to not be destroyed with the scene and are not already in the DDOL
+ if (!networkObject.DestroyWithScene && networkObject.gameObject.scene != DontDestroyOnLoadScene)
+ {
+ // Only move dynamically spawned NetworkObjects with no parent as the children will follow
+ if (networkObject.gameObject.transform.parent == null && networkObject.IsSceneObject != null && !networkObject.IsSceneObject.Value)
+ {
+ UnityEngine.Object.DontDestroyOnLoad(networkObject.gameObject);
+ }
+ }
+ else if (m_NetworkManager.IsServer)
+ {
+ networkObject.Despawn();
+ }
+ }
+ }
+
+
///
/// Moves all NetworkObjects that don't have the set to
/// the "Do not destroy on load" scene.
///
internal void MoveObjectsToDontDestroyOnLoad()
{
- // Move ALL NetworkObjects marked to persist scene transitions into the DDOL scene
- var objectsToKeep = new HashSet(m_NetworkManager.SpawnManager.SpawnedObjectsList);
- foreach (var sobj in objectsToKeep)
+ // Create a local copy of the spawned objects list since the spawn manager will adjust the list as objects
+ // are despawned.
+ var localSpawnedObjectsHashSet = new HashSet(m_NetworkManager.SpawnManager.SpawnedObjectsList);
+ foreach (var networkObject in localSpawnedObjectsHashSet)
{
- if (sobj == null)
+ if (networkObject == null)
{
continue;
}
- if (!sobj.DestroyWithScene || sobj.gameObject.scene == DontDestroyOnLoadScene)
+ // Only NetworkObjects marked to not be destroyed with the scene and are not already in the DDOL
+ if (!networkObject.DestroyWithScene && networkObject.gameObject.scene != DontDestroyOnLoadScene)
{
- // Only move dynamically spawned network objects with no parent as child objects will follow
- if (sobj.gameObject.transform.parent == null && sobj.IsSceneObject != null && !sobj.IsSceneObject.Value)
+ // Only move dynamically spawned NetworkObjects with no parent as the children will follow
+ if (networkObject.gameObject.transform.parent == null && networkObject.IsSceneObject != null && !networkObject.IsSceneObject.Value)
{
- UnityEngine.Object.DontDestroyOnLoad(sobj.gameObject);
+ UnityEngine.Object.DontDestroyOnLoad(networkObject.gameObject);
}
}
else if (m_NetworkManager.IsServer)
{
- sobj.Despawn();
+ networkObject.Despawn();
}
}
}
@@ -2061,23 +2285,20 @@ internal void PopulateScenePlacedObjects(Scene sceneToFilterBy, bool clearSceneP
/// scene to move the NetworkObjects to
internal void MoveObjectsFromDontDestroyOnLoadToScene(Scene scene)
{
- // Move ALL NetworkObjects to the temp scene
- var objectsToKeep = m_NetworkManager.SpawnManager.SpawnedObjectsList;
-
- foreach (var sobj in objectsToKeep)
+ foreach (var networkObject in m_NetworkManager.SpawnManager.SpawnedObjectsList)
{
- if (sobj == null)
+ if (networkObject == null)
{
continue;
}
// If it is in the DDOL then
- if (sobj.gameObject.scene == DontDestroyOnLoadScene)
+ if (networkObject.gameObject.scene == DontDestroyOnLoadScene && !networkObject.DestroyWithScene)
{
// only move dynamically spawned network objects, with no parent as child objects will follow,
// back into the currently active scene
- if (sobj.gameObject.transform.parent == null && sobj.IsSceneObject != null && !sobj.IsSceneObject.Value)
+ if (networkObject.gameObject.transform.parent == null && networkObject.IsSceneObject != null && !networkObject.IsSceneObject.Value)
{
- SceneManager.MoveGameObjectToScene(sobj.gameObject, scene);
+ SceneManager.MoveGameObjectToScene(networkObject.gameObject, scene);
}
}
}
diff --git a/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs b/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs
index 359a4fd790..13b12cb506 100644
--- a/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs
+++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs
@@ -80,6 +80,11 @@ public enum SceneEventType : byte
/// Event Notification: Both server and client receive a local notification.
///
SynchronizeComplete,
+
+ ///
+ /// Notifies clients that the active scene has changed
+ ///
+ ActiveSceneChanged,
}
///
@@ -94,7 +99,7 @@ internal class SceneEventData : IDisposable
internal ForceNetworkSerializeByMemcpy SceneEventProgressId;
internal uint SceneEventId;
-
+ internal uint ActiveSceneHash;
internal uint SceneHash;
internal int SceneHandle;
@@ -311,6 +316,7 @@ internal bool IsSceneEventClientSide()
{
switch (SceneEventType)
{
+ case SceneEventType.ActiveSceneChanged:
case SceneEventType.Load:
case SceneEventType.Unload:
case SceneEventType.Synchronize:
@@ -386,6 +392,12 @@ internal void Serialize(FastBufferWriter writer)
// Write the scene event type
writer.WriteValueSafe(SceneEventType);
+ if (SceneEventType == SceneEventType.ActiveSceneChanged)
+ {
+ writer.WriteValueSafe(ActiveSceneHash);
+ return;
+ }
+
// Write the scene loading mode
writer.WriteValueSafe((byte)LoadSceneMode);
@@ -407,6 +419,7 @@ internal void Serialize(FastBufferWriter writer)
{
case SceneEventType.Synchronize:
{
+ writer.WriteValueSafe(ActiveSceneHash);
WriteSceneSynchronizationData(writer);
break;
}
@@ -542,6 +555,12 @@ internal void SerializeScenePlacedObjects(FastBufferWriter writer)
internal void Deserialize(FastBufferReader reader)
{
reader.ReadValueSafe(out SceneEventType);
+ if (SceneEventType == SceneEventType.ActiveSceneChanged)
+ {
+ reader.ReadValueSafe(out ActiveSceneHash);
+ return;
+ }
+
reader.ReadValueSafe(out byte loadSceneMode);
LoadSceneMode = (LoadSceneMode)loadSceneMode;
@@ -561,6 +580,7 @@ internal void Deserialize(FastBufferReader reader)
{
case SceneEventType.Synchronize:
{
+ reader.ReadValueSafe(out ActiveSceneHash);
CopySceneSynchronizationData(reader);
break;
}
diff --git a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs
index cd87ee63ca..ca4f9429a6 100644
--- a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs
+++ b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs
@@ -405,6 +405,29 @@ internal NetworkObject CreateLocalNetworkObject(NetworkObject.SceneObject sceneO
if (networkObject != null)
{
+ networkObject.DestroyWithScene = sceneObject.DestroyWithScene;
+ networkObject.NetworkSceneHandle = sceneObject.NetworkSceneHandle;
+
+ // For dynamically spawned NetworkObjects, check to make sure it is in the right scene and if not migrate it to the right scene
+ if (!sceneObject.IsSceneObject && NetworkManager.SceneManager.ServerSceneHandleToClientSceneHandle.ContainsKey(networkObject.NetworkSceneHandle))
+ {
+ networkObject.SceneOriginHandle = NetworkManager.SceneManager.ServerSceneHandleToClientSceneHandle[networkObject.NetworkSceneHandle];
+
+ // If the NetworkObject is not in the scene it should be in, then find the right scene and migrate it to that scene
+ if (networkObject.gameObject.scene.handle != networkObject.SceneOriginHandle && networkObject.transform.parent == null)
+ {
+ for (int i = 0; i < UnityEngine.SceneManagement.SceneManager.sceneCount; i++)
+ {
+ var scene = UnityEngine.SceneManagement.SceneManager.GetSceneAt(i);
+ if (networkObject.SceneOriginHandle == scene.handle)
+ {
+ UnityEngine.SceneManagement.SceneManager.MoveGameObjectToScene(networkObject.gameObject, scene);
+ break;
+ }
+ }
+ }
+ }
+
// SPECIAL CASE FOR IN-SCENE PLACED: (only when the parent has a NetworkObject)
// This is a special case scenario where a late joining client has joined and loaded one or
// more scenes that contain nested in-scene placed NetworkObject children yet the server's
@@ -605,6 +628,12 @@ private void SpawnNetworkObjectLocallyCommon(NetworkObject networkObject, ulong
{
childObject.IsSceneObject = sceneObject;
}
+
+ // Only dynamically spawned NetworkObjects are allowed
+ if (!sceneObject)
+ {
+ networkObject.SubscribeToActiveSceneForSynch();
+ }
}
internal void SendSpawnCallForObject(ulong clientId, NetworkObject networkObject)
From fbed8722c5a3eda30d376a84040e551b3d972891 Mon Sep 17 00:00:00 2001
From: NoelStephensUnity <73188597+NoelStephensUnity@users.noreply.github.com>
Date: Fri, 10 Feb 2023 08:41:46 -0600
Subject: [PATCH 08/25] update
some minor adjustments due to changes.
Removed the scene migration in spawn as it ended up not really making sense to do it during spawn (i.e. synchronization already handles this and if active scene synchronization is enabled then connected clients will spawn in the proper scene already).
Added some missed asserts on time out to the parent dynamic under inscene placed test and removed a section of initialization code no longer needed.
---
.../SceneManagement/NetworkSceneManager.cs | 6 +++---
.../Runtime/Spawning/NetworkSpawnManager.cs | 20 -------------------
.../ParentDynamicUnderInScenePlaced.cs | 11 +++-------
3 files changed, 6 insertions(+), 31 deletions(-)
diff --git a/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs b/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs
index 5dc854541c..db1ca6fa67 100644
--- a/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs
+++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs
@@ -2205,13 +2205,13 @@ internal void MoveObjectsToDontDestroyOnLoad()
var localSpawnedObjectsHashSet = new HashSet(m_NetworkManager.SpawnManager.SpawnedObjectsList);
foreach (var networkObject in localSpawnedObjectsHashSet)
{
- if (networkObject == null)
+ if (networkObject == null || (networkObject != null && networkObject.gameObject.scene == DontDestroyOnLoadScene))
{
continue;
}
- // Only NetworkObjects marked to not be destroyed with the scene and are not already in the DDOL
- if (!networkObject.DestroyWithScene && networkObject.gameObject.scene != DontDestroyOnLoadScene)
+ // Only NetworkObjects marked to not be destroyed with the scene
+ if (!networkObject.DestroyWithScene)
{
// Only move dynamically spawned NetworkObjects with no parent as the children will follow
if (networkObject.gameObject.transform.parent == null && networkObject.IsSceneObject != null && !networkObject.IsSceneObject.Value)
diff --git a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs
index 2eca2f1763..315bf4b868 100644
--- a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs
+++ b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs
@@ -408,26 +408,6 @@ internal NetworkObject CreateLocalNetworkObject(NetworkObject.SceneObject sceneO
networkObject.DestroyWithScene = sceneObject.DestroyWithScene;
networkObject.NetworkSceneHandle = sceneObject.NetworkSceneHandle;
- // For dynamically spawned NetworkObjects, check to make sure it is in the right scene and if not migrate it to the right scene
- if (!sceneObject.IsSceneObject && NetworkManager.SceneManager.ServerSceneHandleToClientSceneHandle.ContainsKey(networkObject.NetworkSceneHandle))
- {
- networkObject.SceneOriginHandle = NetworkManager.SceneManager.ServerSceneHandleToClientSceneHandle[networkObject.NetworkSceneHandle];
-
- // If the NetworkObject is not in the scene it should be in, then find the right scene and migrate it to that scene
- if (networkObject.gameObject.scene.handle != networkObject.SceneOriginHandle && networkObject.transform.parent == null)
- {
- for (int i = 0; i < UnityEngine.SceneManagement.SceneManager.sceneCount; i++)
- {
- var scene = UnityEngine.SceneManagement.SceneManager.GetSceneAt(i);
- if (networkObject.SceneOriginHandle == scene.handle)
- {
- UnityEngine.SceneManagement.SceneManager.MoveGameObjectToScene(networkObject.gameObject, scene);
- break;
- }
- }
- }
- }
-
// SPECIAL CASE FOR IN-SCENE PLACED: (only when the parent has a NetworkObject)
// This is a special case scenario where a late joining client has joined and loaded one or
// more scenes that contain nested in-scene placed NetworkObject children yet the server's
diff --git a/testproject/Assets/Tests/Runtime/ObjectParenting/ParentDynamicUnderInScenePlaced.cs b/testproject/Assets/Tests/Runtime/ObjectParenting/ParentDynamicUnderInScenePlaced.cs
index afa031091d..7d0447ae84 100644
--- a/testproject/Assets/Tests/Runtime/ObjectParenting/ParentDynamicUnderInScenePlaced.cs
+++ b/testproject/Assets/Tests/Runtime/ObjectParenting/ParentDynamicUnderInScenePlaced.cs
@@ -40,13 +40,6 @@ protected override void OnServerAndClientsCreated()
base.OnServerAndClientsCreated();
}
- protected override IEnumerator OnStartedServerAndClients()
- {
- m_ServerNetworkManager.SceneManager.DisableValidationWarnings(true);
- m_ServerNetworkManager.SceneManager.ClientSynchronizationMode = LoadSceneMode.Additive;
- return base.OnStartedServerAndClients();
- }
-
protected override void OnNewClientCreated(NetworkManager networkManager)
{
foreach (var networkPrefab in m_ServerNetworkManager.NetworkConfig.Prefabs.Prefabs)
@@ -65,7 +58,7 @@ protected override void OnNewClientStarted(NetworkManager networkManager)
private NetworkObject m_FailedValidation;
private bool TestParentedAndNotInScenePlaced()
{
- var serverPlayer = m_ServerNetworkManager.LocalClient.PlayerObject;
+ var serverPlayer = m_FailedValidation = m_ServerNetworkManager.LocalClient.PlayerObject;
if (serverPlayer.transform.parent == null || serverPlayer.IsSceneObject.Value == true)
{
m_FailedValidation = serverPlayer;
@@ -111,9 +104,11 @@ public IEnumerator ParentUnderInSceneplaced()
m_ServerNetworkManager.SceneManager.LoadScene(k_SceneToLoad, LoadSceneMode.Additive);
// Wait for the scene with the in-scene placed NetworkObject to be loaded
yield return WaitForConditionOrTimeOut(() => m_SceneIsLoaded == true);
+ AssertOnTimeout($"Timed out waiting for the scene {k_SceneToLoad} to load!");
// Wait for the host-server's player to be parented under the in-scene placed NetworkObject
yield return WaitForConditionOrTimeOut(TestParentedAndNotInScenePlaced);
+ AssertOnTimeout($"[{m_FailedValidation.name}] Failed validation! InScenePlaced ({m_FailedValidation.IsSceneObject.Value}) | Was Parented ({m_FailedValidation.transform.position != null})");
// Now dynamically spawn a NetworkObject to also test dynamically spawned NetworkObjects being parented
// under in-scene placed NetworkObjects
From f709d766f10a71e8775a99bb4dfb33d09e96ee46 Mon Sep 17 00:00:00 2001
From: NoelStephensUnity <73188597+NoelStephensUnity@users.noreply.github.com>
Date: Fri, 10 Feb 2023 08:45:41 -0600
Subject: [PATCH 09/25] update
Removing commented out code.
---
.../Runtime/Core/NetworkObject.cs | 22 ++++---------------
1 file changed, 4 insertions(+), 18 deletions(-)
diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs
index 5c46336350..6bc0184b8f 100644
--- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs
+++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs
@@ -1178,7 +1178,6 @@ public void Serialize(FastBufferWriter writer)
var writeSize = 0;
writeSize += HasTransform ? FastBufferWriter.GetWriteSize() : 0;
- //writeSize += IsSceneObject ? FastBufferWriter.GetWriteSize() : 0;
writeSize += FastBufferWriter.GetWriteSize();
if (!writer.TryBeginWrite(writeSize))
@@ -1191,14 +1190,8 @@ public void Serialize(FastBufferWriter writer)
writer.WriteValue(Transform);
}
- // In-Scene NetworkObjects are uniquely identified NetworkPrefabs defined by their
- // NetworkSceneHandle and GlobalObjectIdHash. Client-side NetworkSceneManagers use
- // this to locate their local instance of the in-scene placed NetworkObject instance.
- // Only written for in-scene placed NetworkObjects.
- //if (IsSceneObject)
- //{
- // writer.WriteValue(OwnerObject.GetSceneOriginHandle());
- //}
+ // The NetworkSceneHandle is the server-side relative
+ // scene handle that the NetworkObject resides in.
writer.WriteValue(OwnerObject.GetSceneOriginHandle());
// Synchronize NetworkVariables and NetworkBehaviours
@@ -1225,7 +1218,6 @@ public void Deserialize(FastBufferReader reader)
var readSize = 0;
readSize += HasTransform ? FastBufferWriter.GetWriteSize() : 0;
- //readSize += IsSceneObject ? FastBufferWriter.GetWriteSize() : 0;
readSize += FastBufferWriter.GetWriteSize();
// Try to begin reading the remaining bytes
@@ -1239,14 +1231,8 @@ public void Deserialize(FastBufferReader reader)
reader.ReadValue(out Transform);
}
- // In-Scene NetworkObjects are uniquely identified NetworkPrefabs defined by their
- // NetworkSceneHandle and GlobalObjectIdHash. Client-side NetworkSceneManagers use
- // this to locate their local instance of the in-scene placed NetworkObject instance.
- // Only read for in-scene placed NetworkObjects
- //if (IsSceneObject)
- //{
- // reader.ReadValue(out NetworkSceneHandle);
- //}
+ // The NetworkSceneHandle is the server-side relative
+ // scene handle that the NetworkObject resides in.
reader.ReadValue(out NetworkSceneHandle);
}
}
From dcf19f59e2b3ea7e9522b01943e7d268d8de5a9d Mon Sep 17 00:00:00 2001
From: NoelStephensUnity <73188597+NoelStephensUnity@users.noreply.github.com>
Date: Fri, 10 Feb 2023 08:50:57 -0600
Subject: [PATCH 10/25] style
adding a comment about why we are now synchronizing DestroyWithScene
---
.../Runtime/Core/NetworkObject.cs | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs
index 6bc0184b8f..fec9255b4d 100644
--- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs
+++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs
@@ -110,6 +110,9 @@ internal void GenerateGlobalObjectIdHash()
///
public bool AutoSynchActiveScene;
+ ///
+ /// Notifies when a NetworkObject is migrated into a new scene
+ ///
public Action MigratedToNewScene;
///
@@ -1130,6 +1133,12 @@ public bool WorldPositionStays
set => ByteUtility.SetBit(ref m_BitField, 5, value);
}
+ ///
+ /// Even though the server sends notifications for NetworkObjects that get
+ /// destroyed when a scene is unloaded, we want to synchronize this so
+ /// the client side can use it as part of a filter for automatically migrating
+ /// to the current active scene when its scene is unloaded. (only for dynamically spawned)
+ ///
public bool DestroyWithScene
{
get => ByteUtility.GetBit(m_BitField, 6);
From 14287f84e88c0e455557a28eedfebcae5c956b4e Mon Sep 17 00:00:00 2001
From: NoelStephensUnity <73188597+NoelStephensUnity@users.noreply.github.com>
Date: Fri, 10 Feb 2023 09:45:49 -0600
Subject: [PATCH 11/25] revert
Reverting the change with DestroyWithScene (breaking API change).
Still will get synchronized, but users will have to set this themselves.
---
com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs
index fec9255b4d..dc6569636f 100644
--- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs
+++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs
@@ -103,7 +103,7 @@ internal void GenerateGlobalObjectIdHash()
///
/// Gets whether or not the object should be automatically removed when the scene is unloaded.
///
- public bool DestroyWithScene = true;
+ public bool DestroyWithScene { get; set; }
///
/// When set to true, this will automatically migrate the NetworkObject to a newly assigned active scene
From ce39f8d379952ae884f5281422666df0dc8c7dfc Mon Sep 17 00:00:00 2001
From: NoelStephensUnity <73188597+NoelStephensUnity@users.noreply.github.com>
Date: Fri, 10 Feb 2023 11:51:28 -0600
Subject: [PATCH 12/25] update
Removed commented code.
Fixed issue with migrating into the appropriate scene.
---
.../SceneManagement/NetworkSceneManager.cs | 16 +++-------------
1 file changed, 3 insertions(+), 13 deletions(-)
diff --git a/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs b/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs
index db1ca6fa67..eb4b4aee90 100644
--- a/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs
+++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs
@@ -1865,14 +1865,14 @@ private void ClientLoadedSynchronization(uint sceneEventId)
HandleClientSceneEvent(sceneEventId);
}
- private void MigrateNetworkObjectsToTheirProperScenes()
+ private void MigrateNetworkObjectsToAssignedScene()
{
foreach (var networkObject in m_NetworkManager.SpawnManager.SpawnedObjectsList)
{
// This is only done for dynamically spawned NetworkObjects
// Theoretically, a server could have NetworkObjects in a server-side only scene, if the client doesn't have that scene loaded
// then skip it.
- if (networkObject.IsSceneObject != false && ServerSceneHandleToClientSceneHandle.ContainsKey(networkObject.NetworkSceneHandle))
+ if (networkObject.IsSceneObject.Value == false && ServerSceneHandleToClientSceneHandle.ContainsKey(networkObject.NetworkSceneHandle))
{
networkObject.SceneOriginHandle = ServerSceneHandleToClientSceneHandle[networkObject.NetworkSceneHandle];
@@ -1882,16 +1882,6 @@ private void MigrateNetworkObjectsToTheirProperScenes()
{
var scene = HandleToScene[networkObject.SceneOriginHandle];
SceneManager.MoveGameObjectToScene(networkObject.gameObject, scene);
-
- //for (int i = 0; i < SceneManager.sceneCount; i++)
- //{
- // var scene = SceneManager.GetSceneAt(i);
- // if (networkObject.SceneOriginHandle == scene.handle)
- // {
- // SceneManager.MoveGameObjectToScene(networkObject.gameObject, scene);
- // break;
- // }
- //}
}
}
}
@@ -1953,7 +1943,7 @@ private void HandleClientSceneEvent(uint sceneEventId)
}
// Now migrate any dynamically spawned NetworkObjects that are not in the same scene as on the server side
- MigrateNetworkObjectsToTheirProperScenes();
+ MigrateNetworkObjectsToAssignedScene();
sceneEventData.SceneEventType = SceneEventType.SynchronizeComplete;
SendSceneEventData(sceneEventId, new ulong[] { NetworkManager.ServerClientId });
From cb391eb56a4bb0ec34004ad7b5f4af4125d68046 Mon Sep 17 00:00:00 2001
From: NoelStephensUnity <73188597+NoelStephensUnity@users.noreply.github.com>
Date: Fri, 10 Feb 2023 18:33:44 -0600
Subject: [PATCH 13/25] update
After some deliberation over whether to include this update, I decided it would make more sense to include full NetworkObject scene migration synchronization. This includes the additional changes required to assure that clients are synchronized when a dynamically spawned NetworkObject is moved into a new scene.
---
.../Runtime/Core/NetworkManager.cs | 4 +
.../Runtime/Core/NetworkObject.cs | 139 ++++++++++++++++--
.../SceneManagement/NetworkSceneManager.cs | 128 +++++++++++++++-
.../Runtime/SceneManagement/SceneEventData.cs | 81 +++++++++-
4 files changed, 331 insertions(+), 21 deletions(-)
diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs
index 894476a555..cefa410fee 100644
--- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs
+++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs
@@ -1397,6 +1397,10 @@ private void OnNetworkPostLateUpdate()
if (!m_ShuttingDown || !m_StopProcessingMessages)
{
+ // This should be invoked just prior to the MessagingSystem
+ // processes its outbound queue.
+ SceneManager.CheckForAndSendNetworkObjectSceneChanged();
+
MessagingSystem.ProcessSendQueues();
NetworkMetrics.UpdateNetworkObjectsCount(SpawnManager.SpawnedObjects.Count);
NetworkMetrics.UpdateConnectionsCount((IsServer) ? ConnectedClients.Count : 1);
diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs
index dc6569636f..0123f2dcab 100644
--- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs
+++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs
@@ -106,14 +106,53 @@ internal void GenerateGlobalObjectIdHash()
public bool DestroyWithScene { get; set; }
///
- /// When set to true, this will automatically migrate the NetworkObject to a newly assigned active scene
+ /// When set to true and the active scene is changed, this will automatically migrate the
+ /// into the new active scene on both the server and client instances.
///
- public bool AutoSynchActiveScene;
+ ///
+ /// - This only applies to dynamically spawned s.
+ /// - This only works when using integrated scene management ().
+ ///
+ /// If there are more than one scenes loaded and the currently active scene is unloaded, then typically
+ /// the will automatically assign a new active scene. Similar to
+ /// being set to , this prevents any from being destroyed
+ /// with the unloaded active scene by migrating it into the automatically assigned active scene.
+ /// Additionally, this is can be useful in some seamless scene streaming implementations.
+ /// Note:
+ /// Only having set to true will *not* synchronize clients when
+ /// changing a 's scene via .
+ /// To synchronize clients of a 's scene being changed via ,
+ /// make sure is enabled (it is by default).
+ ///
+ public bool ActiveSceneSynchronization;
+
+ ///
+ /// When enabled (the default), if a is migrated to a different scene (active or not)
+ /// via on the server side all client
+ /// instances will be synchronized and the migrated into the newly assigned scene.
+ /// The updated scene migration will get synchronized with late joining clients as well.
+ ///
+ ///
+ /// - This only applies to dynamically spawned s.
+ /// - This only works when using integrated scene management ().
+ /// Note:
+ /// You can have both and enabled.
+ /// The primary difference between the two is that only synchronizes clients
+ /// when the server migrates a to a new scene. If the scene is unloaded and
+ /// is and is and the scene is not the currently
+ /// active scene, then the will be destroyed.
+ ///
+ public bool SceneMigrationSynchronization = true;
///
- /// Notifies when a NetworkObject is migrated into a new scene
+ /// Notifies when the NetworkObject is migrated into a new scene
///
- public Action MigratedToNewScene;
+ ///
+ /// - or (or both) need to be enabled
+ /// - This only applies to dynamically spawned s.
+ /// - This only works when using integrated scene management ().
+ ///
+ public Action OnMigratedToNewScene;
///
/// Delegate type for checking visibility
@@ -1466,7 +1505,7 @@ internal static NetworkObject AddSceneObject(in SceneObject sceneObject, FastBuf
///
internal void SubscribeToActiveSceneForSynch()
{
- if (AutoSynchActiveScene)
+ if (ActiveSceneSynchronization)
{
if (IsSceneObject.HasValue && !IsSceneObject.Value)
{
@@ -1477,28 +1516,102 @@ internal void SubscribeToActiveSceneForSynch()
}
}
+ ///
+ /// If AutoSynchActiveScene is enabled, then this is the callback that handles updating
+ /// a NetworkObject's scene information.
+ ///
private void CurrentlyActiveSceneChanged(Scene current, Scene next)
{
+ // Early exit if there is no NetworkManager assigned, the NetworkManager is shutting down, the NetworkObject
+ // is not spawned, or an in-scene placed NetworkObject
+ if (NetworkManager == null || NetworkManager.ShutdownInProgress || !IsSpawned || IsSceneObject != false)
+ {
+ return;
+ }
// This check is here in the event a user wants to disable this for some reason but also wants
// the NetworkObject to synchronize to changes in the currently active scene at some later time.
- if (AutoSynchActiveScene)
+ if (ActiveSceneSynchronization)
{
// Only dynamically spawned NetworkObjects that are not already in the newly assigned active scene will migrate
// and update their scene handles
if (IsSceneObject.HasValue && !IsSceneObject.Value && gameObject.scene != next && gameObject.transform.parent == null)
{
SceneManager.MoveGameObjectToScene(gameObject, next);
- SceneOriginHandle = next.handle;
- if (NetworkManager.SceneManager.ClientSceneHandleToServerSceneHandle.ContainsKey(SceneOriginHandle))
- {
- NetworkSceneHandle = NetworkManager.SceneManager.ClientSceneHandleToServerSceneHandle[SceneOriginHandle];
- }
-
- MigratedToNewScene?.Invoke();
+ SceneChangedUpdate(next);
}
}
}
+ ///
+ /// Handles updating the NetworkObject's tracked scene handles
+ ///
+ internal void SceneChangedUpdate(Scene scene, bool notify = false)
+ {
+ // Avoiding edge case scenarios, if no NetworkSceneManager exit early
+ if (NetworkManager.SceneManager == null)
+ {
+ return;
+ }
+
+ SceneOriginHandle = scene.handle;
+ // Clients need to update the NetworkSceneHandle
+ if (!NetworkManager.IsServer && NetworkManager.SceneManager.ClientSceneHandleToServerSceneHandle.ContainsKey(SceneOriginHandle))
+ {
+ NetworkSceneHandle = NetworkManager.SceneManager.ClientSceneHandleToServerSceneHandle[SceneOriginHandle];
+ }
+ else if (NetworkManager.IsServer)
+ {
+ // Since the server is the source of truth for the NetworkSceneHandle,
+ // the NetworkSceneHandle is the same as the SceneOriginHandle.
+ NetworkSceneHandle = SceneOriginHandle;
+ }
+ else // Otherwise, the client did not find the client to server scene handle
+ if (NetworkManager.LogLevel == LogLevel.Developer)
+ {
+ // There could be a scenario where a user has some client-local scene loaded that they migrate the NetworkObject
+ // into, but that scenario seemed very edge case and under most instances a user should be notified that this
+ // server - client scene handle mismatch has occurred. It also seemed pertinent to make the message replicate to
+ // the server-side too.
+ NetworkLog.LogWarningServer($"[Client-{NetworkManager.LocalClientId}][{gameObject.name}] Server - " +
+ $"client scene mismatch detected! Client-side scene handle ({SceneOriginHandle}) for scene ({gameObject.scene.name})" +
+ $"has no associated server side (network) scene handle!");
+ }
+ OnMigratedToNewScene?.Invoke();
+
+ // Only the server side will notify clients of non-parented NetworkObject scene changes
+ if (NetworkManager.IsServer && notify && transform.parent == null)
+ {
+ NetworkManager.SceneManager.NotifyNetworkObjectSceneChanged(this);
+ }
+ }
+
+ ///
+ /// Update
+ /// Detects if a NetworkObject's scene has changed for both server and client instances
+ ///
+ ///
+ /// About In-Scene Placed NetworkObjects:
+ /// Since the same scene can be loaded more than once and in-scene placed NetworkObjects GlobalObjectIdHash
+ /// values are only unique to the scene asset itself (and not per scene instance loaded), we will not be able
+ /// to add this same functionality to in-scene placed NetworkObjects until we have a way to generate
+ /// per-NetworkObject-instance unique GlobalObjectIdHash values for in-scene placed NetworkObjects.
+ ///
+ private void Update()
+ {
+ // Early exit if SceneMigrationSynchronization is disabled, there is no NetworkManager assigned,
+ // the NetworkManager is shutting down, the NetworkObject is not spawned, it is an in-scene placed
+ // NetworkObject, or the GameObject's current scene handle is the same as the SceneOriginHandle
+ if (!SceneMigrationSynchronization || NetworkManager == null || NetworkManager.ShutdownInProgress || !IsSpawned
+ || IsSceneObject != false || gameObject.scene.handle == SceneOriginHandle)
+ {
+ return;
+ }
+
+ // Otherwise, this has to be a dynamically spawned NetworkObject that has been
+ // migrated to a new scene.
+ SceneChangedUpdate(gameObject.scene, true);
+ }
+
///
/// Only applies to Host mode.
/// Will return the registered source NetworkPrefab's GlobalObjectIdHash if one exists.
diff --git a/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs b/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs
index eb4b4aee90..d635986c75 100644
--- a/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs
+++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs
@@ -1865,7 +1865,7 @@ private void ClientLoadedSynchronization(uint sceneEventId)
HandleClientSceneEvent(sceneEventId);
}
- private void MigrateNetworkObjectsToAssignedScene()
+ private void SynchronizeNetworkObjectScene()
{
foreach (var networkObject in m_NetworkManager.SpawnManager.SpawnedObjectsList)
{
@@ -1880,8 +1880,16 @@ private void MigrateNetworkObjectsToAssignedScene()
// only if it does not have a parent.
if (networkObject.gameObject.scene.handle != networkObject.SceneOriginHandle && networkObject.transform.parent == null)
{
- var scene = HandleToScene[networkObject.SceneOriginHandle];
- SceneManager.MoveGameObjectToScene(networkObject.gameObject, scene);
+ if (HandleToScene.ContainsKey(networkObject.SceneOriginHandle))
+ {
+ var scene = HandleToScene[networkObject.SceneOriginHandle];
+ SceneManager.MoveGameObjectToScene(networkObject.gameObject, scene);
+ }
+ else if (m_NetworkManager.LogLevel <= LogLevel.Normal)
+ {
+ NetworkLog.LogWarningServer($"[Client-{m_NetworkManager.LocalClientId}][{networkObject.gameObject.name}] Server - " +
+ $"client scene mismatch detected! Client-side has no scene loaded with handle ({networkObject.SceneOriginHandle})!");
+ }
}
}
}
@@ -1909,6 +1917,11 @@ private void HandleClientSceneEvent(uint sceneEventId)
}
break;
}
+ case SceneEventType.ObjectSceneChanged:
+ {
+ MigrateNetworkObjectsIntoScenes();
+ break;
+ }
case SceneEventType.Load:
{
OnClientSceneLoadingEvent(sceneEventId);
@@ -1942,8 +1955,8 @@ private void HandleClientSceneEvent(uint sceneEventId)
}
}
- // Now migrate any dynamically spawned NetworkObjects that are not in the same scene as on the server side
- MigrateNetworkObjectsToAssignedScene();
+ // Now Synchronize dynamically spawned NetworkObjects to reside in the same scene as on the server side
+ SynchronizeNetworkObjectScene();
sceneEventData.SceneEventType = SceneEventType.SynchronizeComplete;
SendSceneEventData(sceneEventId, new ulong[] { NetworkManager.ServerClientId });
@@ -2293,5 +2306,110 @@ internal void MoveObjectsFromDontDestroyOnLoadToScene(Scene scene)
}
}
}
+
+ ///
+ /// Holds a list of scene handles (server-side relative) and NetworkObjects migrated into it
+ /// during the current frame.
+ ///
+ internal Dictionary> ObjectsMigratedIntoNewScene = new Dictionary>();
+
+ ///
+ /// Handles notifying clients when a NetworkObject has been migrated into a new scene
+ ///
+ internal void NotifyNetworkObjectSceneChanged(NetworkObject networkObject)
+ {
+ // Really, this should never happen but in case it does
+ if (!m_NetworkManager.IsServer)
+ {
+ if (m_NetworkManager.LogLevel == LogLevel.Developer)
+ {
+ NetworkLog.LogErrorServer("[Please Report This Error][NotifyNetworkObjectSceneChanged] A client is trying to notify of an object's scene change!");
+ }
+ return;
+ }
+
+ // Ignore in-scene placed NetworkObjects
+ if (networkObject.IsSceneObject != false)
+ {
+ // Really, this should ever happen but in case it does
+ if (m_NetworkManager.LogLevel == LogLevel.Developer)
+ {
+ NetworkLog.LogErrorServer("[Please Report This Error][NotifyNetworkObjectSceneChanged] Trying to notify in-scene placed object scene change!");
+ }
+ return;
+ }
+
+ // Ignore if the scene is the currently active scene and the NetworkObject is auto synchronizing/migrating
+ // to the currently active scene.
+ if (networkObject.gameObject.scene == SceneManager.GetActiveScene() && networkObject.ActiveSceneSynchronization)
+ {
+ return;
+ }
+
+ // Don't notify if a scene event is in progress
+ foreach (var sceneEventEntry in SceneEventProgressTracking)
+ {
+ if (!sceneEventEntry.Value.HasTimedOut() && sceneEventEntry.Value.Status == SceneEventProgressStatus.Started)
+ {
+ return;
+ }
+ }
+
+ // Otherwise, add the NetworkObject into the list of NetworkObjects who's scene has changed
+ if (!ObjectsMigratedIntoNewScene.ContainsKey(networkObject.gameObject.scene.handle))
+ {
+ ObjectsMigratedIntoNewScene.Add(networkObject.gameObject.scene.handle, new List());
+ }
+ ObjectsMigratedIntoNewScene[networkObject.gameObject.scene.handle].Add(networkObject);
+ }
+
+ ///
+ /// Invoked by clients when processing a event
+ ///
+ private void MigrateNetworkObjectsIntoScenes()
+ {
+ try
+ {
+ foreach (var sceneEntry in ObjectsMigratedIntoNewScene)
+ {
+ if (ServerSceneHandleToClientSceneHandle.ContainsKey(sceneEntry.Key))
+ {
+ var clientSceneHandle = ServerSceneHandleToClientSceneHandle[sceneEntry.Key];
+ if (HandleToScene.ContainsKey(ServerSceneHandleToClientSceneHandle[sceneEntry.Key]))
+ {
+ var scene = HandleToScene[clientSceneHandle];
+ foreach (var networkObject in sceneEntry.Value)
+ {
+ SceneManager.MoveGameObjectToScene(networkObject.gameObject, scene);
+ }
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ NetworkLog.LogErrorServer($"{ex.Message}\n Stack Trace:\n {ex.StackTrace}");
+ }
+
+ // Clear out the list once complete
+ ObjectsMigratedIntoNewScene.Clear();
+ }
+
+ ///
+ /// Should be invoked during PostLateUpdate just prior to the
+ /// MessagingSystem processes its outbound message queue.
+ ///
+ internal void CheckForAndSendNetworkObjectSceneChanged()
+ {
+ // Early exit if not the server or there is nothing pending
+ if (!m_NetworkManager.IsServer || ObjectsMigratedIntoNewScene.Count == 0)
+ {
+ return;
+ }
+ var sceneEvent = BeginSceneEvent();
+ sceneEvent.SceneEventType = SceneEventType.ObjectSceneChanged;
+ SendSceneEventData(sceneEvent.SceneEventId, m_NetworkManager.ConnectedClientsIds.Where(c => c != NetworkManager.ServerClientId).ToArray());
+ EndSceneEvent(sceneEvent.SceneEventId);
+ }
}
}
diff --git a/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs b/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs
index 13b12cb506..d679d2ff26 100644
--- a/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs
+++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs
@@ -80,11 +80,16 @@ public enum SceneEventType : byte
/// Event Notification: Both server and client receive a local notification.
///
SynchronizeComplete,
-
///
- /// Notifies clients that the active scene has changed
+ /// Synchronizes clients when the active scene has changed
+ /// See:
///
ActiveSceneChanged,
+ ///
+ /// Synchronizes clients when one or more NetworkObjects are migrated into a new scene
+ /// See:
+ ///
+ ObjectSceneChanged,
}
///
@@ -316,13 +321,14 @@ internal bool IsSceneEventClientSide()
{
switch (SceneEventType)
{
- case SceneEventType.ActiveSceneChanged:
case SceneEventType.Load:
case SceneEventType.Unload:
case SceneEventType.Synchronize:
case SceneEventType.ReSynchronize:
case SceneEventType.LoadEventCompleted:
case SceneEventType.UnloadEventCompleted:
+ case SceneEventType.ActiveSceneChanged:
+ case SceneEventType.ObjectSceneChanged:
{
return true;
}
@@ -398,6 +404,12 @@ internal void Serialize(FastBufferWriter writer)
return;
}
+ if (SceneEventType == SceneEventType.ObjectSceneChanged)
+ {
+ SerializeObjectsMovedIntoNewScene(writer);
+ return;
+ }
+
// Write the scene loading mode
writer.WriteValueSafe((byte)LoadSceneMode);
@@ -561,6 +573,12 @@ internal void Deserialize(FastBufferReader reader)
return;
}
+ if (SceneEventType == SceneEventType.ObjectSceneChanged)
+ {
+ DeserializeObjectsMovedIntoNewScene(reader);
+ return;
+ }
+
reader.ReadValueSafe(out byte loadSceneMode);
LoadSceneMode = (LoadSceneMode)loadSceneMode;
@@ -969,6 +987,63 @@ internal void ReadSceneEventProgressDone(FastBufferReader reader)
}
}
+ ///
+ /// Serialize scene handles and associated NetworkObjects that were migrated
+ /// into a new scene.
+ ///
+ private void SerializeObjectsMovedIntoNewScene(FastBufferWriter writer)
+ {
+ var sceneManager = m_NetworkManager.SceneManager;
+ // Write the number of scene handles
+ writer.WriteValueSafe(sceneManager.ObjectsMigratedIntoNewScene.Count);
+ foreach (var sceneHandleObjects in sceneManager.ObjectsMigratedIntoNewScene)
+ {
+ // Write the scene handle
+ writer.WriteValueSafe(sceneHandleObjects.Key);
+ // Write the number of NetworkObjectIds to expect
+ writer.WriteValueSafe(sceneHandleObjects.Value.Count);
+ foreach (var networkObject in sceneHandleObjects.Value)
+ {
+ writer.WriteValueSafe(networkObject.NetworkObjectId);
+ }
+ }
+ // Once we are done, clear the table
+ sceneManager.ObjectsMigratedIntoNewScene.Clear();
+ }
+
+ ///
+ /// Deserialize scene handles and associated NetworkObjects that need to
+ /// be migrated into a new scene.
+ ///
+ private void DeserializeObjectsMovedIntoNewScene(FastBufferReader reader)
+ {
+ var sceneManager = m_NetworkManager.SceneManager;
+ var spawnManager = m_NetworkManager.SpawnManager;
+ // Just always assure this has no entries
+ sceneManager.ObjectsMigratedIntoNewScene.Clear();
+ var numberOfScenes = 0;
+ var sceneHandle = 0;
+ var objectCount = 0;
+ var networkObjectId = (ulong)0;
+ reader.ReadValueSafe(out numberOfScenes);
+ for (int i = 0; i < numberOfScenes; i++)
+ {
+ reader.ReadValueSafe(out sceneHandle);
+ sceneManager.ObjectsMigratedIntoNewScene.Add(sceneHandle, new List());
+ reader.ReadValueSafe(out objectCount);
+ for (int j = 0; j < objectCount; j++)
+ {
+ reader.ReadValueSafe(out networkObjectId);
+ if (!spawnManager.SpawnedObjects.ContainsKey(networkObjectId))
+ {
+ throw new Exception($"[Object Scene Migration] Trying to synchronize NetworkObjectId ({networkObjectId}) but it no longer exists!");
+ }
+ sceneManager.ObjectsMigratedIntoNewScene[sceneHandle].Add(spawnManager.SpawnedObjects[networkObjectId]);
+ }
+ }
+ }
+
+
///
/// Used to release the pooled network buffer
///
From 364b09482695c36e16610e7ce56d1f89c7e2a2a6 Mon Sep 17 00:00:00 2001
From: NoelStephensUnity <73188597+NoelStephensUnity@users.noreply.github.com>
Date: Fri, 10 Feb 2023 20:02:21 -0600
Subject: [PATCH 14/25] test
The additional integration tests for this PR. (WIP)
Includes 3 additional empty scenes to verify object scene migration.
Also a minor style issue addressed in NetworkSceneManager.
---
.../SceneManagement/NetworkSceneManager.cs | 6 +-
.../IntegrationTestScenes/EmptyScene1.unity | 125 ++++++++++++
.../EmptyScene1.unity.meta | 7 +
.../IntegrationTestScenes/EmptyScene2.unity | 125 ++++++++++++
.../EmptyScene2.unity.meta | 7 +
.../IntegrationTestScenes/EmptyScene3.unity | 125 ++++++++++++
.../EmptyScene3.unity.meta | 7 +
.../NetworkObjectSceneMigrationTests.cs | 182 ++++++++++++++++++
.../NetworkObjectSceneMigrationTests.cs.meta | 11 ++
.../ProjectSettings/EditorBuildSettings.asset | 9 +
10 files changed, 601 insertions(+), 3 deletions(-)
create mode 100644 testproject/Assets/Tests/Manual/IntegrationTestScenes/EmptyScene1.unity
create mode 100644 testproject/Assets/Tests/Manual/IntegrationTestScenes/EmptyScene1.unity.meta
create mode 100644 testproject/Assets/Tests/Manual/IntegrationTestScenes/EmptyScene2.unity
create mode 100644 testproject/Assets/Tests/Manual/IntegrationTestScenes/EmptyScene2.unity.meta
create mode 100644 testproject/Assets/Tests/Manual/IntegrationTestScenes/EmptyScene3.unity
create mode 100644 testproject/Assets/Tests/Manual/IntegrationTestScenes/EmptyScene3.unity.meta
create mode 100644 testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkObjectSceneMigrationTests.cs
create mode 100644 testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkObjectSceneMigrationTests.cs.meta
diff --git a/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs b/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs
index d635986c75..881d308141 100644
--- a/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs
+++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs
@@ -345,11 +345,11 @@ public bool ActiveSceneSynchronizationEnabled
m_ActiveSceneSynchronizationEnabled = value;
if (m_ActiveSceneSynchronizationEnabled)
{
- SceneManager.activeSceneChanged += SceneManager_activeSceneChanged;
+ SceneManager.activeSceneChanged += SceneManager_ActiveSceneChanged;
}
else
{
- SceneManager.activeSceneChanged -= SceneManager_activeSceneChanged;
+ SceneManager.activeSceneChanged -= SceneManager_ActiveSceneChanged;
}
}
}
@@ -690,7 +690,7 @@ internal NetworkSceneManager(NetworkManager networkManager)
///
/// Synchronizes clients when the currently active scene is changed
///
- private void SceneManager_activeSceneChanged(Scene current, Scene next)
+ private void SceneManager_ActiveSceneChanged(Scene current, Scene next)
{
// If no clients are connected, then don't worry about notifications
if (!(m_NetworkManager.ConnectedClientsIds.Count > (m_NetworkManager.IsHost ? 1 : 0)))
diff --git a/testproject/Assets/Tests/Manual/IntegrationTestScenes/EmptyScene1.unity b/testproject/Assets/Tests/Manual/IntegrationTestScenes/EmptyScene1.unity
new file mode 100644
index 0000000000..fe1ff710a2
--- /dev/null
+++ b/testproject/Assets/Tests/Manual/IntegrationTestScenes/EmptyScene1.unity
@@ -0,0 +1,125 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!29 &1
+OcclusionCullingSettings:
+ m_ObjectHideFlags: 0
+ serializedVersion: 2
+ m_OcclusionBakeSettings:
+ smallestOccluder: 5
+ smallestHole: 0.25
+ backfaceThreshold: 100
+ m_SceneGUID: 00000000000000000000000000000000
+ m_OcclusionCullingData: {fileID: 0}
+--- !u!104 &2
+RenderSettings:
+ m_ObjectHideFlags: 0
+ serializedVersion: 9
+ m_Fog: 0
+ m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1}
+ m_FogMode: 3
+ m_FogDensity: 0.01
+ m_LinearFogStart: 0
+ m_LinearFogEnd: 300
+ m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1}
+ m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1}
+ m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1}
+ m_AmbientIntensity: 1
+ m_AmbientMode: 0
+ m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1}
+ m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0}
+ m_HaloStrength: 0.5
+ m_FlareStrength: 1
+ m_FlareFadeSpeed: 3
+ m_HaloTexture: {fileID: 0}
+ m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0}
+ m_DefaultReflectionMode: 0
+ m_DefaultReflectionResolution: 128
+ m_ReflectionBounces: 1
+ m_ReflectionIntensity: 1
+ m_CustomReflection: {fileID: 0}
+ m_Sun: {fileID: 0}
+ m_IndirectSpecularColor: {r: 0.37311953, g: 0.38074014, b: 0.3587274, a: 1}
+ m_UseRadianceAmbientProbe: 0
+--- !u!157 &3
+LightmapSettings:
+ m_ObjectHideFlags: 0
+ serializedVersion: 12
+ m_GIWorkflowMode: 1
+ m_GISettings:
+ serializedVersion: 2
+ m_BounceScale: 1
+ m_IndirectOutputScale: 1
+ m_AlbedoBoost: 1
+ m_EnvironmentLightingMode: 0
+ m_EnableBakedLightmaps: 1
+ m_EnableRealtimeLightmaps: 0
+ m_LightmapEditorSettings:
+ serializedVersion: 12
+ m_Resolution: 2
+ m_BakeResolution: 40
+ m_AtlasSize: 1024
+ m_AO: 0
+ m_AOMaxDistance: 1
+ m_CompAOExponent: 1
+ m_CompAOExponentDirect: 0
+ m_ExtractAmbientOcclusion: 0
+ m_Padding: 2
+ m_LightmapParameters: {fileID: 0}
+ m_LightmapsBakeMode: 1
+ m_TextureCompression: 1
+ m_FinalGather: 0
+ m_FinalGatherFiltering: 1
+ m_FinalGatherRayCount: 256
+ m_ReflectionCompression: 2
+ m_MixedBakeMode: 2
+ m_BakeBackend: 1
+ m_PVRSampling: 1
+ m_PVRDirectSampleCount: 32
+ m_PVRSampleCount: 512
+ m_PVRBounces: 2
+ m_PVREnvironmentSampleCount: 256
+ m_PVREnvironmentReferencePointCount: 2048
+ m_PVRFilteringMode: 1
+ m_PVRDenoiserTypeDirect: 1
+ m_PVRDenoiserTypeIndirect: 1
+ m_PVRDenoiserTypeAO: 1
+ m_PVRFilterTypeDirect: 0
+ m_PVRFilterTypeIndirect: 0
+ m_PVRFilterTypeAO: 0
+ m_PVREnvironmentMIS: 1
+ m_PVRCulling: 1
+ m_PVRFilteringGaussRadiusDirect: 1
+ m_PVRFilteringGaussRadiusIndirect: 5
+ m_PVRFilteringGaussRadiusAO: 2
+ m_PVRFilteringAtrousPositionSigmaDirect: 0.5
+ m_PVRFilteringAtrousPositionSigmaIndirect: 2
+ m_PVRFilteringAtrousPositionSigmaAO: 1
+ m_ExportTrainingData: 0
+ m_TrainingDataDestination: TrainingData
+ m_LightProbeSampleCountMultiplier: 4
+ m_LightingDataAsset: {fileID: 0}
+ m_LightingSettings: {fileID: 0}
+--- !u!196 &4
+NavMeshSettings:
+ serializedVersion: 2
+ m_ObjectHideFlags: 0
+ m_BuildSettings:
+ serializedVersion: 2
+ agentTypeID: 0
+ agentRadius: 0.5
+ agentHeight: 2
+ agentSlope: 45
+ agentClimb: 0.4
+ ledgeDropHeight: 0
+ maxJumpAcrossDistance: 0
+ minRegionArea: 2
+ manualCellSize: 0
+ cellSize: 0.16666667
+ manualTileSize: 0
+ tileSize: 256
+ accuratePlacement: 0
+ maxJobWorkers: 0
+ preserveTilesOutsideBounds: 0
+ debug:
+ m_Flags: 0
+ m_NavMeshData: {fileID: 0}
diff --git a/testproject/Assets/Tests/Manual/IntegrationTestScenes/EmptyScene1.unity.meta b/testproject/Assets/Tests/Manual/IntegrationTestScenes/EmptyScene1.unity.meta
new file mode 100644
index 0000000000..696fa23b0f
--- /dev/null
+++ b/testproject/Assets/Tests/Manual/IntegrationTestScenes/EmptyScene1.unity.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 057ba2cc37faa0b43aa7051d9f555caa
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/testproject/Assets/Tests/Manual/IntegrationTestScenes/EmptyScene2.unity b/testproject/Assets/Tests/Manual/IntegrationTestScenes/EmptyScene2.unity
new file mode 100644
index 0000000000..fe1ff710a2
--- /dev/null
+++ b/testproject/Assets/Tests/Manual/IntegrationTestScenes/EmptyScene2.unity
@@ -0,0 +1,125 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!29 &1
+OcclusionCullingSettings:
+ m_ObjectHideFlags: 0
+ serializedVersion: 2
+ m_OcclusionBakeSettings:
+ smallestOccluder: 5
+ smallestHole: 0.25
+ backfaceThreshold: 100
+ m_SceneGUID: 00000000000000000000000000000000
+ m_OcclusionCullingData: {fileID: 0}
+--- !u!104 &2
+RenderSettings:
+ m_ObjectHideFlags: 0
+ serializedVersion: 9
+ m_Fog: 0
+ m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1}
+ m_FogMode: 3
+ m_FogDensity: 0.01
+ m_LinearFogStart: 0
+ m_LinearFogEnd: 300
+ m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1}
+ m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1}
+ m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1}
+ m_AmbientIntensity: 1
+ m_AmbientMode: 0
+ m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1}
+ m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0}
+ m_HaloStrength: 0.5
+ m_FlareStrength: 1
+ m_FlareFadeSpeed: 3
+ m_HaloTexture: {fileID: 0}
+ m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0}
+ m_DefaultReflectionMode: 0
+ m_DefaultReflectionResolution: 128
+ m_ReflectionBounces: 1
+ m_ReflectionIntensity: 1
+ m_CustomReflection: {fileID: 0}
+ m_Sun: {fileID: 0}
+ m_IndirectSpecularColor: {r: 0.37311953, g: 0.38074014, b: 0.3587274, a: 1}
+ m_UseRadianceAmbientProbe: 0
+--- !u!157 &3
+LightmapSettings:
+ m_ObjectHideFlags: 0
+ serializedVersion: 12
+ m_GIWorkflowMode: 1
+ m_GISettings:
+ serializedVersion: 2
+ m_BounceScale: 1
+ m_IndirectOutputScale: 1
+ m_AlbedoBoost: 1
+ m_EnvironmentLightingMode: 0
+ m_EnableBakedLightmaps: 1
+ m_EnableRealtimeLightmaps: 0
+ m_LightmapEditorSettings:
+ serializedVersion: 12
+ m_Resolution: 2
+ m_BakeResolution: 40
+ m_AtlasSize: 1024
+ m_AO: 0
+ m_AOMaxDistance: 1
+ m_CompAOExponent: 1
+ m_CompAOExponentDirect: 0
+ m_ExtractAmbientOcclusion: 0
+ m_Padding: 2
+ m_LightmapParameters: {fileID: 0}
+ m_LightmapsBakeMode: 1
+ m_TextureCompression: 1
+ m_FinalGather: 0
+ m_FinalGatherFiltering: 1
+ m_FinalGatherRayCount: 256
+ m_ReflectionCompression: 2
+ m_MixedBakeMode: 2
+ m_BakeBackend: 1
+ m_PVRSampling: 1
+ m_PVRDirectSampleCount: 32
+ m_PVRSampleCount: 512
+ m_PVRBounces: 2
+ m_PVREnvironmentSampleCount: 256
+ m_PVREnvironmentReferencePointCount: 2048
+ m_PVRFilteringMode: 1
+ m_PVRDenoiserTypeDirect: 1
+ m_PVRDenoiserTypeIndirect: 1
+ m_PVRDenoiserTypeAO: 1
+ m_PVRFilterTypeDirect: 0
+ m_PVRFilterTypeIndirect: 0
+ m_PVRFilterTypeAO: 0
+ m_PVREnvironmentMIS: 1
+ m_PVRCulling: 1
+ m_PVRFilteringGaussRadiusDirect: 1
+ m_PVRFilteringGaussRadiusIndirect: 5
+ m_PVRFilteringGaussRadiusAO: 2
+ m_PVRFilteringAtrousPositionSigmaDirect: 0.5
+ m_PVRFilteringAtrousPositionSigmaIndirect: 2
+ m_PVRFilteringAtrousPositionSigmaAO: 1
+ m_ExportTrainingData: 0
+ m_TrainingDataDestination: TrainingData
+ m_LightProbeSampleCountMultiplier: 4
+ m_LightingDataAsset: {fileID: 0}
+ m_LightingSettings: {fileID: 0}
+--- !u!196 &4
+NavMeshSettings:
+ serializedVersion: 2
+ m_ObjectHideFlags: 0
+ m_BuildSettings:
+ serializedVersion: 2
+ agentTypeID: 0
+ agentRadius: 0.5
+ agentHeight: 2
+ agentSlope: 45
+ agentClimb: 0.4
+ ledgeDropHeight: 0
+ maxJumpAcrossDistance: 0
+ minRegionArea: 2
+ manualCellSize: 0
+ cellSize: 0.16666667
+ manualTileSize: 0
+ tileSize: 256
+ accuratePlacement: 0
+ maxJobWorkers: 0
+ preserveTilesOutsideBounds: 0
+ debug:
+ m_Flags: 0
+ m_NavMeshData: {fileID: 0}
diff --git a/testproject/Assets/Tests/Manual/IntegrationTestScenes/EmptyScene2.unity.meta b/testproject/Assets/Tests/Manual/IntegrationTestScenes/EmptyScene2.unity.meta
new file mode 100644
index 0000000000..19ea576e01
--- /dev/null
+++ b/testproject/Assets/Tests/Manual/IntegrationTestScenes/EmptyScene2.unity.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 17b92153f7381d34fa48c4d5c0393d13
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/testproject/Assets/Tests/Manual/IntegrationTestScenes/EmptyScene3.unity b/testproject/Assets/Tests/Manual/IntegrationTestScenes/EmptyScene3.unity
new file mode 100644
index 0000000000..fe1ff710a2
--- /dev/null
+++ b/testproject/Assets/Tests/Manual/IntegrationTestScenes/EmptyScene3.unity
@@ -0,0 +1,125 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!29 &1
+OcclusionCullingSettings:
+ m_ObjectHideFlags: 0
+ serializedVersion: 2
+ m_OcclusionBakeSettings:
+ smallestOccluder: 5
+ smallestHole: 0.25
+ backfaceThreshold: 100
+ m_SceneGUID: 00000000000000000000000000000000
+ m_OcclusionCullingData: {fileID: 0}
+--- !u!104 &2
+RenderSettings:
+ m_ObjectHideFlags: 0
+ serializedVersion: 9
+ m_Fog: 0
+ m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1}
+ m_FogMode: 3
+ m_FogDensity: 0.01
+ m_LinearFogStart: 0
+ m_LinearFogEnd: 300
+ m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1}
+ m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1}
+ m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1}
+ m_AmbientIntensity: 1
+ m_AmbientMode: 0
+ m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1}
+ m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0}
+ m_HaloStrength: 0.5
+ m_FlareStrength: 1
+ m_FlareFadeSpeed: 3
+ m_HaloTexture: {fileID: 0}
+ m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0}
+ m_DefaultReflectionMode: 0
+ m_DefaultReflectionResolution: 128
+ m_ReflectionBounces: 1
+ m_ReflectionIntensity: 1
+ m_CustomReflection: {fileID: 0}
+ m_Sun: {fileID: 0}
+ m_IndirectSpecularColor: {r: 0.37311953, g: 0.38074014, b: 0.3587274, a: 1}
+ m_UseRadianceAmbientProbe: 0
+--- !u!157 &3
+LightmapSettings:
+ m_ObjectHideFlags: 0
+ serializedVersion: 12
+ m_GIWorkflowMode: 1
+ m_GISettings:
+ serializedVersion: 2
+ m_BounceScale: 1
+ m_IndirectOutputScale: 1
+ m_AlbedoBoost: 1
+ m_EnvironmentLightingMode: 0
+ m_EnableBakedLightmaps: 1
+ m_EnableRealtimeLightmaps: 0
+ m_LightmapEditorSettings:
+ serializedVersion: 12
+ m_Resolution: 2
+ m_BakeResolution: 40
+ m_AtlasSize: 1024
+ m_AO: 0
+ m_AOMaxDistance: 1
+ m_CompAOExponent: 1
+ m_CompAOExponentDirect: 0
+ m_ExtractAmbientOcclusion: 0
+ m_Padding: 2
+ m_LightmapParameters: {fileID: 0}
+ m_LightmapsBakeMode: 1
+ m_TextureCompression: 1
+ m_FinalGather: 0
+ m_FinalGatherFiltering: 1
+ m_FinalGatherRayCount: 256
+ m_ReflectionCompression: 2
+ m_MixedBakeMode: 2
+ m_BakeBackend: 1
+ m_PVRSampling: 1
+ m_PVRDirectSampleCount: 32
+ m_PVRSampleCount: 512
+ m_PVRBounces: 2
+ m_PVREnvironmentSampleCount: 256
+ m_PVREnvironmentReferencePointCount: 2048
+ m_PVRFilteringMode: 1
+ m_PVRDenoiserTypeDirect: 1
+ m_PVRDenoiserTypeIndirect: 1
+ m_PVRDenoiserTypeAO: 1
+ m_PVRFilterTypeDirect: 0
+ m_PVRFilterTypeIndirect: 0
+ m_PVRFilterTypeAO: 0
+ m_PVREnvironmentMIS: 1
+ m_PVRCulling: 1
+ m_PVRFilteringGaussRadiusDirect: 1
+ m_PVRFilteringGaussRadiusIndirect: 5
+ m_PVRFilteringGaussRadiusAO: 2
+ m_PVRFilteringAtrousPositionSigmaDirect: 0.5
+ m_PVRFilteringAtrousPositionSigmaIndirect: 2
+ m_PVRFilteringAtrousPositionSigmaAO: 1
+ m_ExportTrainingData: 0
+ m_TrainingDataDestination: TrainingData
+ m_LightProbeSampleCountMultiplier: 4
+ m_LightingDataAsset: {fileID: 0}
+ m_LightingSettings: {fileID: 0}
+--- !u!196 &4
+NavMeshSettings:
+ serializedVersion: 2
+ m_ObjectHideFlags: 0
+ m_BuildSettings:
+ serializedVersion: 2
+ agentTypeID: 0
+ agentRadius: 0.5
+ agentHeight: 2
+ agentSlope: 45
+ agentClimb: 0.4
+ ledgeDropHeight: 0
+ maxJumpAcrossDistance: 0
+ minRegionArea: 2
+ manualCellSize: 0
+ cellSize: 0.16666667
+ manualTileSize: 0
+ tileSize: 256
+ accuratePlacement: 0
+ maxJobWorkers: 0
+ preserveTilesOutsideBounds: 0
+ debug:
+ m_Flags: 0
+ m_NavMeshData: {fileID: 0}
diff --git a/testproject/Assets/Tests/Manual/IntegrationTestScenes/EmptyScene3.unity.meta b/testproject/Assets/Tests/Manual/IntegrationTestScenes/EmptyScene3.unity.meta
new file mode 100644
index 0000000000..46669dd13d
--- /dev/null
+++ b/testproject/Assets/Tests/Manual/IntegrationTestScenes/EmptyScene3.unity.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: abd4c8b51c445d54faa16c67ac973f1b
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkObjectSceneMigrationTests.cs b/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkObjectSceneMigrationTests.cs
new file mode 100644
index 0000000000..c50d061e8e
--- /dev/null
+++ b/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkObjectSceneMigrationTests.cs
@@ -0,0 +1,182 @@
+using System.Collections;
+using System.Collections.Generic;
+using NUnit.Framework;
+using UnityEngine;
+using UnityEngine.SceneManagement;
+using UnityEngine.TestTools;
+using Unity.Netcode;
+using Unity.Netcode.TestHelpers.Runtime;
+
+namespace TestProject.RuntimeTests
+{
+ public class NetworkObjectSceneMigrationTests : NetcodeIntegrationTest
+ {
+ private List m_TestScenes = new List() { "EmptyScene1", "EmptyScene2", "EmptyScene3" };
+ protected override int NumberOfClients => 2;
+ private GameObject m_TestPrefab;
+
+ protected override void OnServerAndClientsCreated()
+ {
+ m_TestPrefab = CreateNetworkObjectPrefab("TestObject");
+ base.OnServerAndClientsCreated();
+ }
+
+ protected override void OnNewClientCreated(NetworkManager networkManager)
+ {
+ foreach (var networkPrfab in m_ServerNetworkManager.NetworkConfig.Prefabs.Prefabs)
+ {
+ networkManager.NetworkConfig.Prefabs.Add(networkPrfab);
+ }
+ base.OnNewClientCreated(networkManager);
+ }
+
+ private bool VerifyAllClientsSpawnedInstances()
+ {
+ foreach (var serverObject in m_ServerSpawnedPrefabInstances)
+ {
+ foreach (var networkManager in m_ClientNetworkManagers)
+ {
+ if (!s_GlobalNetworkObjects.ContainsKey(networkManager.LocalClientId))
+ {
+ return false;
+ }
+ var clientNetworkObjects = s_GlobalNetworkObjects[networkManager.LocalClientId];
+ if (!clientNetworkObjects.ContainsKey(serverObject.NetworkObjectId))
+ {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ private bool VerifySpawnedObjectsMigrated()
+ {
+ foreach (var serverObject in m_ServerSpawnedPrefabInstances)
+ {
+ foreach (var networkManager in m_ClientNetworkManagers)
+ {
+ var clientNetworkObjects = s_GlobalNetworkObjects[networkManager.LocalClientId];
+ if (clientNetworkObjects[serverObject.NetworkObjectId].gameObject.scene.name != serverObject.gameObject.scene.name)
+ {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ private bool m_ClientsLoadedScene;
+ private Scene m_SceneLoaded;
+ private List m_ServerSpawnedPrefabInstances = new List();
+ private List m_ScenesLoaded = new List();
+ private string m_CurrentSceneLoading;
+
+ ///
+ /// Integration test to verify that migrating NetworkObjects
+ /// into different scenes (in the same frame) is synchronized
+ /// with connected clients and synchronizes with late joining
+ /// clients.
+ ///
+ [UnityTest]
+ public IEnumerator MigrateIntoNewSceneTest()
+ {
+ // Spawn 10 NetworkObject instances
+ for (int i = 0; i < 9; i++)
+ {
+ var serverInstance = Object.Instantiate(m_TestPrefab);
+ var serverNetworkObject = serverInstance.GetComponent();
+ serverNetworkObject.Spawn();
+ m_ServerSpawnedPrefabInstances.Add(serverNetworkObject);
+ }
+ yield return WaitForConditionOrTimeOut(VerifyAllClientsSpawnedInstances);
+ AssertOnTimeout($"Timed out waiting for all clients to spawn {nameof(NetworkObject)}s!");
+
+ // Now load three scenes to migrate the newly spawned NetworkObjects into
+ m_ServerNetworkManager.SceneManager.OnSceneEvent += SceneManager_OnSceneEvent;
+ for (int i = 0; i < 3; i++)
+ {
+ m_ClientsLoadedScene = false;
+ m_CurrentSceneLoading = m_TestScenes[i];
+ var status = m_ServerNetworkManager.SceneManager.LoadScene(m_TestScenes[i], LoadSceneMode.Additive);
+ Assert.True(status == SceneEventProgressStatus.Started, $"Failed to start loading scene {m_CurrentSceneLoading}! Return status: {status}");
+ yield return WaitForConditionOrTimeOut(() => m_ClientsLoadedScene);
+ AssertOnTimeout($"Timed out waiting for all clients to load scene {m_CurrentSceneLoading}!");
+ }
+
+ var objectCount = 0;
+ // Migrate each networkObject into one of the three scenes.
+ // There will be 3 networkObjects per newly loaded scenes when done.
+ foreach (var scene in m_ScenesLoaded)
+ {
+ // Now migrate the NetworkObject
+ SceneManager.MoveGameObjectToScene(m_ServerSpawnedPrefabInstances[objectCount].gameObject, scene);
+ SceneManager.MoveGameObjectToScene(m_ServerSpawnedPrefabInstances[objectCount + 1].gameObject, scene);
+ SceneManager.MoveGameObjectToScene(m_ServerSpawnedPrefabInstances[objectCount + 2].gameObject, scene);
+ objectCount += 3;
+ }
+
+ yield return WaitForConditionOrTimeOut(VerifySpawnedObjectsMigrated);
+ AssertOnTimeout($"Timed out waiting for all clients to migrate all NetworkObjects into the appropriate scenes!");
+
+ // Verify that a late joining client synchronizes properly
+ yield return CreateAndStartNewClient();
+ yield return WaitForConditionOrTimeOut(VerifySpawnedObjectsMigrated);
+ AssertOnTimeout($"[Late Joined Client] Timed out waiting for all clients to migrate all NetworkObjects into the appropriate scenes!");
+ }
+
+ ///
+ /// Integration test to verify changing the currently active scene
+ /// will migrate NetworkObjects with ActiveSceneSynchronization set
+ /// to true.
+ ///
+ [UnityTest]
+ public IEnumerator ActiveSceneSynchronizationTest()
+ {
+ // WIP
+ yield return null;
+ }
+
+ public enum MigrateUnloadType
+ {
+ ActiveSynch,
+ DestroyWithScene
+ }
+
+ ///
+ /// Integration test to verify that unloading a scene that a NetworkObject
+ /// with ActiveSceneSynchronization (true) and DestroyWithScene (false) will
+ /// migrate the NetworkObject into the next active scene when their current
+ /// one is unloaded.
+ ///
+ [UnityTest]
+ public IEnumerator MigrateOnUnloadSceneTest([Values] MigrateUnloadType migrateUnloadType)
+ {
+ // WIP
+ yield return null;
+ }
+
+
+ private void SceneManager_OnSceneEvent(SceneEvent sceneEvent)
+ {
+ switch (sceneEvent.SceneEventType)
+ {
+ case SceneEventType.LoadComplete:
+ {
+ if (sceneEvent.ClientId == m_ServerNetworkManager.LocalClientId)
+ {
+ m_SceneLoaded = sceneEvent.Scene;
+ m_ScenesLoaded.Add(sceneEvent.Scene);
+ }
+ break;
+ }
+ case SceneEventType.LoadEventCompleted:
+ {
+ Assert.IsTrue(sceneEvent.ClientsThatTimedOut.Count == 0, $"{sceneEvent.ClientsThatTimedOut.Count} clients timed out while trying to load scene {m_CurrentSceneLoading}!");
+ m_ClientsLoadedScene = true;
+ break;
+ }
+ }
+ }
+ }
+}
diff --git a/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkObjectSceneMigrationTests.cs.meta b/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkObjectSceneMigrationTests.cs.meta
new file mode 100644
index 0000000000..7b8dfd6a1b
--- /dev/null
+++ b/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkObjectSceneMigrationTests.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 7915a6a8062bc414ab4ff730f3f778f5
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/testproject/ProjectSettings/EditorBuildSettings.asset b/testproject/ProjectSettings/EditorBuildSettings.asset
index 0e20bdb99d..c611036021 100644
--- a/testproject/ProjectSettings/EditorBuildSettings.asset
+++ b/testproject/ProjectSettings/EditorBuildSettings.asset
@@ -122,6 +122,15 @@ EditorBuildSettings:
- enabled: 1
path: Assets/Tests/Manual/IntegrationTestScenes/GenericInScenePlacedObject.unity
guid: 43c36dc1d38660e4d9879e84e580e22f
+ - enabled: 1
+ path: Assets/Tests/Manual/IntegrationTestScenes/EmptyScene1.unity
+ guid: 057ba2cc37faa0b43aa7051d9f555caa
+ - enabled: 1
+ path: Assets/Tests/Manual/IntegrationTestScenes/EmptyScene2.unity
+ guid: 17b92153f7381d34fa48c4d5c0393d13
+ - enabled: 1
+ path: Assets/Tests/Manual/IntegrationTestScenes/EmptyScene3.unity
+ guid: abd4c8b51c445d54faa16c67ac973f1b
m_configObjects:
com.unity.addressableassets: {fileID: 11400000, guid: 5a3d5c53c25349c48912726ae850f3b0,
type: 2}
From 189214de12365627c23e2e9a1887594b42b647c1 Mon Sep 17 00:00:00 2001
From: NoelStephensUnity <73188597+NoelStephensUnity@users.noreply.github.com>
Date: Fri, 10 Feb 2023 20:23:43 -0600
Subject: [PATCH 15/25] update
updating the changelog
---
com.unity.netcode.gameobjects/CHANGELOG.md | 17 +++++++++++++----
1 file changed, 13 insertions(+), 4 deletions(-)
diff --git a/com.unity.netcode.gameobjects/CHANGELOG.md b/com.unity.netcode.gameobjects/CHANGELOG.md
index 3b439dc962..4168748fe6 100644
--- a/com.unity.netcode.gameobjects/CHANGELOG.md
+++ b/com.unity.netcode.gameobjects/CHANGELOG.md
@@ -5,10 +5,21 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
Additional documentation and release notes are available at [Multiplayer Documentation](https://docs-multiplayer.unity3d.com).
+## [Unreleased - Post v1.3.0]
+### Added
+- Added `NetworkSceneManager.ActiveSceneSynchronizationEnabled` property, disabled by default, that will synchronize clients when the active scene is changed on the server side via `SceneManager.MoveGameObjectToScene`. (#2383)
+- Added `NetworkObject.ActiveSceneSynchronization`, disabled by default, that will automatically migrate a `NetworkObject` to a newly assigned active scene. (#2383)
+- Added `NetworkObject.SceneMigrationSynchronization`, enabled by default, that will synchronize client(s) when a `NetworkObject` is migrated into a new scene. (#2383)
-## [Unreleased]
+### Changed
+- Updated `NetworkSceneManager` to migrate dynamically spawned `NetworkObject`s with `DestroyWithScene` set to false into the active scene if their current scene is unloaded. (#2383)
+- Updated the server to synchronize its local `NetworkSceneManager.ClientSynchronizationMode` during the initial client synchronization. (#2383)
-### Added
+### Fixed:
+- Fixed issue when the `NetworkSceneManager.ClientSynchronizationMode` is `LoadSceneMode.Additive` and the server changes the currently active scene prior to a client connecting then upon a client connecting and being synchronized the NetworkSceneManager would clear its internal ScenePlacedObjects list that could already be populated. (#2383)
+- Fixed issue where a client would load duplicate scenes of already preloaded scenes during the initial client synchronization and `NetworkSceneManager.ClientSynchronizationMode` was set to `LoadSceneMode.Additive`. (#2383)
+
+## [Unreleased]
### Changed
@@ -25,8 +36,6 @@ Additional documentation and release notes are available at [Multiplayer Documen
- Fixed issue where changes to a layer's weight would not synchronize unless a state transition was occurring.(#2399)
- Fixed issue where `NetworkManager.LocalClientId` was returning the `NetworkTransport.ServerClientId` as opposed to the `NetworkManager.m_LocalClientId`. (#2398)
- Fixed issue where a dynamically spawned `NetworkObject` parented under an in-scene placed `NetworkObject` would have its `InScenePlaced` value changed to `true`. This would result in a soft synchronization error for late joining clients. (#2396)
-- Fixed issue when the ClientSynchronizationMode is additive and the server changes the currently active scene prior to a client connecting then upon a client connecting and being synchronized the NetworkSceneManager would clear its internal ScenePlacedObjects list that could already be populated. (#2383)
- - This removes the need to resync the whole scene when players disconnect and reconnect while still being in the same scene.
- Fixed a UTP test that was failing when you install Unity Transport package 2.0.0 or newer. (#2347)
- Fixed issue where `NetcodeSettingsProvider` would throw an exception in Unity 2020.3.x versions. (#2345)
- Fixed server side issue where, depending upon component ordering, some NetworkBehaviour components might not have their OnNetworkDespawn method invoked if the client side disconnected. (#2323)
From d793587c6e74123c5338c24f739f16623d3a4ab3 Mon Sep 17 00:00:00 2001
From: NoelStephensUnity <73188597+NoelStephensUnity@users.noreply.github.com>
Date: Fri, 10 Feb 2023 20:28:09 -0600
Subject: [PATCH 16/25] update
Adjusting a mix-up in the added section.
---
com.unity.netcode.gameobjects/CHANGELOG.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/com.unity.netcode.gameobjects/CHANGELOG.md b/com.unity.netcode.gameobjects/CHANGELOG.md
index 4168748fe6..e9c00352d0 100644
--- a/com.unity.netcode.gameobjects/CHANGELOG.md
+++ b/com.unity.netcode.gameobjects/CHANGELOG.md
@@ -7,9 +7,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
Additional documentation and release notes are available at [Multiplayer Documentation](https://docs-multiplayer.unity3d.com).
## [Unreleased - Post v1.3.0]
### Added
-- Added `NetworkSceneManager.ActiveSceneSynchronizationEnabled` property, disabled by default, that will synchronize clients when the active scene is changed on the server side via `SceneManager.MoveGameObjectToScene`. (#2383)
+- Added `NetworkSceneManager.ActiveSceneSynchronizationEnabled` property, disabled by default, that will synchronize clients when the active scene is changed on the server side. (#2383)
- Added `NetworkObject.ActiveSceneSynchronization`, disabled by default, that will automatically migrate a `NetworkObject` to a newly assigned active scene. (#2383)
-- Added `NetworkObject.SceneMigrationSynchronization`, enabled by default, that will synchronize client(s) when a `NetworkObject` is migrated into a new scene. (#2383)
+- Added `NetworkObject.SceneMigrationSynchronization`, enabled by default, that will synchronize client(s) when a `NetworkObject` is migrated into a new scene on the server side via `SceneManager.MoveGameObjectToScene`. (#2383)
### Changed
- Updated `NetworkSceneManager` to migrate dynamically spawned `NetworkObject`s with `DestroyWithScene` set to false into the active scene if their current scene is unloaded. (#2383)
From 79fbe81b15da83f5297b5b30e07fcfe409cc6b51 Mon Sep 17 00:00:00 2001
From: NoelStephensUnity <73188597+NoelStephensUnity@users.noreply.github.com>
Date: Fri, 10 Feb 2023 22:05:45 -0600
Subject: [PATCH 17/25] update
Fixing isolation test failure on change log.
---
com.unity.netcode.gameobjects/CHANGELOG.md | 12 ++++++++----
1 file changed, 8 insertions(+), 4 deletions(-)
diff --git a/com.unity.netcode.gameobjects/CHANGELOG.md b/com.unity.netcode.gameobjects/CHANGELOG.md
index e9c00352d0..a43f94c23d 100644
--- a/com.unity.netcode.gameobjects/CHANGELOG.md
+++ b/com.unity.netcode.gameobjects/CHANGELOG.md
@@ -5,21 +5,25 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
Additional documentation and release notes are available at [Multiplayer Documentation](https://docs-multiplayer.unity3d.com).
-## [Unreleased - Post v1.3.0]
+## [Unreleased]
+
### Added
-- Added `NetworkSceneManager.ActiveSceneSynchronizationEnabled` property, disabled by default, that will synchronize clients when the active scene is changed on the server side. (#2383)
+
+- Added `NetworkSceneManager.ActiveSceneSynchronizationEnabled` property, disabled by default, that enables client synchronization of server-side active scene changes. (#2383)
- Added `NetworkObject.ActiveSceneSynchronization`, disabled by default, that will automatically migrate a `NetworkObject` to a newly assigned active scene. (#2383)
- Added `NetworkObject.SceneMigrationSynchronization`, enabled by default, that will synchronize client(s) when a `NetworkObject` is migrated into a new scene on the server side via `SceneManager.MoveGameObjectToScene`. (#2383)
### Changed
+
- Updated `NetworkSceneManager` to migrate dynamically spawned `NetworkObject`s with `DestroyWithScene` set to false into the active scene if their current scene is unloaded. (#2383)
- Updated the server to synchronize its local `NetworkSceneManager.ClientSynchronizationMode` during the initial client synchronization. (#2383)
-### Fixed:
+### Fixed
+
- Fixed issue when the `NetworkSceneManager.ClientSynchronizationMode` is `LoadSceneMode.Additive` and the server changes the currently active scene prior to a client connecting then upon a client connecting and being synchronized the NetworkSceneManager would clear its internal ScenePlacedObjects list that could already be populated. (#2383)
- Fixed issue where a client would load duplicate scenes of already preloaded scenes during the initial client synchronization and `NetworkSceneManager.ClientSynchronizationMode` was set to `LoadSceneMode.Additive`. (#2383)
-## [Unreleased]
+## [1.3.0]
### Changed
From 1ff53804b264232acc42f572d71f0aedb44d463a Mon Sep 17 00:00:00 2001
From: NoelStephensUnity <73188597+NoelStephensUnity@users.noreply.github.com>
Date: Sat, 11 Feb 2023 18:36:33 -0600
Subject: [PATCH 18/25] test and fix
Adding tests for NetworkObject scene migration.
Updating scene manager handler to include moving objects from a scene to the DDOL (for unloading a scene only).
Fixing minor issue where AddSceneObject was always passing false for the destroy with scene parameter.
---
.../Runtime/Core/NetworkObject.cs | 2 +-
.../DefaultSceneManagerHandler.cs | 38 ++
.../SceneManagement/ISceneManagerHandler.cs | 2 +
.../SceneManagement/NetworkSceneManager.cs | 77 ++--
.../Runtime/IntegrationTestSceneHandler.cs | 66 +++
.../NetworkObjectSceneMigrationTests.cs | 404 ++++++++++++++++--
6 files changed, 509 insertions(+), 80 deletions(-)
diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs
index 0123f2dcab..4ac41c7de0 100644
--- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs
+++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs
@@ -1492,7 +1492,7 @@ internal static NetworkObject AddSceneObject(in SceneObject sceneObject, FastBuf
networkObject.SynchronizeNetworkBehaviours(ref bufferSerializer, networkManager.LocalClientId);
// Spawn the NetworkObject
- networkManager.SpawnManager.SpawnNetworkObjectLocally(networkObject, sceneObject, false);
+ networkManager.SpawnManager.SpawnNetworkObjectLocally(networkObject, sceneObject, sceneObject.DestroyWithScene);
return networkObject;
}
diff --git a/com.unity.netcode.gameobjects/Runtime/SceneManagement/DefaultSceneManagerHandler.cs b/com.unity.netcode.gameobjects/Runtime/SceneManagement/DefaultSceneManagerHandler.cs
index a85eb74577..c45cc48c33 100644
--- a/com.unity.netcode.gameobjects/Runtime/SceneManagement/DefaultSceneManagerHandler.cs
+++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/DefaultSceneManagerHandler.cs
@@ -213,5 +213,43 @@ private void SceneManager_SceneUnloaded(Scene scene)
}
}
}
+
+ ///
+ /// Handles migrating dynamically spawned NetworkObjects to the DDOL when a scene is unloaded
+ ///
+ /// relative instance
+ /// scene being unloaded
+ public void MoveObjectsFromSceneToDontDestroyOnLoad(ref NetworkManager networkManager, Scene scene)
+ {
+ bool isActiveScene = scene == SceneManager.GetActiveScene();
+ // Create a local copy of the spawned objects list since the spawn manager will adjust the list as objects
+ // are despawned.
+ var localSpawnedObjectsHashSet = new HashSet(networkManager.SpawnManager.SpawnedObjectsList);
+ foreach (var networkObject in localSpawnedObjectsHashSet)
+ {
+ if (networkObject == null || (networkObject != null && networkObject.gameObject.scene.handle != scene.handle))
+ {
+ continue;
+ }
+
+ // Only NetworkObjects marked to not be destroyed with the scene and are not already in the DDOL are preserved
+ if (!networkObject.DestroyWithScene && networkObject.gameObject.scene != networkManager.SceneManager.DontDestroyOnLoadScene)
+ {
+ // Only move dynamically spawned NetworkObjects with no parent as the children will follow
+ if (networkObject.gameObject.transform.parent == null && networkObject.IsSceneObject != null && !networkObject.IsSceneObject.Value)
+ {
+ UnityEngine.Object.DontDestroyOnLoad(networkObject.gameObject);
+ }
+ }
+ else if (networkManager.IsServer)
+ {
+ networkObject.Despawn();
+ }
+ else // We are a client, migrate the object into the DDOL temporarily until it receives the destroy command from the server
+ {
+ UnityEngine.Object.DontDestroyOnLoad(networkObject.gameObject);
+ }
+ }
+ }
}
}
diff --git a/com.unity.netcode.gameobjects/Runtime/SceneManagement/ISceneManagerHandler.cs b/com.unity.netcode.gameobjects/Runtime/SceneManagement/ISceneManagerHandler.cs
index b9073d0da5..4d15db4cff 100644
--- a/com.unity.netcode.gameobjects/Runtime/SceneManagement/ISceneManagerHandler.cs
+++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/ISceneManagerHandler.cs
@@ -26,5 +26,7 @@ internal interface ISceneManagerHandler
void ClearSceneTracking(NetworkManager networkManager = null);
void UnloadUnassignedScenes(NetworkManager networkManager = null);
+
+ void MoveObjectsFromSceneToDontDestroyOnLoad(ref NetworkManager networkManager, Scene scene);
}
}
diff --git a/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs b/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs
index 881d308141..f3a56a0743 100644
--- a/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs
+++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs
@@ -395,6 +395,11 @@ public bool ActiveSceneSynchronizationEnabled
internal Dictionary ClientSceneHandleToServerSceneHandle = new Dictionary();
internal Dictionary HandleToScene = new Dictionary();
+ ///
+ /// Add the client to server (and vice versa) scene handle lookup.
+ /// Add the client-side handle to scene entry in the HandleToScene table.
+ /// If it fails (i.e. already added) it returns false.
+ ///
internal bool UpdateServerClientSceneHandle(int serverHandle, int clientHandle, Scene localScene)
{
if (!ServerSceneHandleToClientSceneHandle.ContainsKey(serverHandle))
@@ -427,6 +432,10 @@ internal bool UpdateServerClientSceneHandle(int serverHandle, int clientHandle,
return true;
}
+ ///
+ /// Removes the client to server (and vice versa) scene handles.
+ /// If it fails (i.e. already removed) it returns false.
+ ///
internal bool RemoveServerClientSceneHandle(int serverHandle, int clientHandle)
{
if (ServerSceneHandleToClientSceneHandle.ContainsKey(serverHandle))
@@ -486,6 +495,7 @@ internal bool RemoveServerClientSceneHandle(int serverHandle, int clientHandle)
private NetworkManager m_NetworkManager { get; }
+ // Keep track of this scene until the NetworkSceneManager is destroyed.
internal Scene DontDestroyOnLoadScene;
///
@@ -1056,8 +1066,11 @@ public SceneEventProgressStatus UnloadScene(Scene scene)
return SceneEventProgressStatus.InternalNetcodeError;
}
- // Persist NetworkObjects marked to not be destroyed in the scene to be unloaded
- MoveObjectsFromSceneToDontDestroyOnLoad(scene);
+ // Any NetworkObjects marked to not be destroyed with a scene and reside within the scene about to be unloaded
+ // should be migrated temporarily into the DDOL, once the scene is unloaded they will be migrated into the
+ // currently active scene.
+ var networkManager = m_NetworkManager;
+ SceneManagerHandler.MoveObjectsFromSceneToDontDestroyOnLoad(ref networkManager, scene);
var sceneEventData = BeginSceneEvent();
sceneEventData.SceneEventProgressId = sceneEventProgress.Guid;
@@ -1116,8 +1129,11 @@ private void OnClientUnloadScene(uint sceneEventId)
}
var scene = ScenesLoaded[sceneHandle];
- // Persist NetworkObjects marked to not be destroyed in the scene to be unloaded
- MoveObjectsFromSceneToDontDestroyOnLoad(scene);
+ // Any NetworkObjects marked to not be destroyed with a scene and reside within the scene about to be unloaded
+ // should be migrated temporarily into the DDOL, once the scene is unloaded they will be migrated into the
+ // currently active scene.
+ var networkManager = m_NetworkManager;
+ SceneManagerHandler.MoveObjectsFromSceneToDontDestroyOnLoad(ref networkManager, scene);
m_IsSceneEventActive = true;
var sceneEventProgress = new SceneEventProgress(m_NetworkManager);
@@ -1865,19 +1881,24 @@ private void ClientLoadedSynchronization(uint sceneEventId)
HandleClientSceneEvent(sceneEventId);
}
+ ///
+ /// Makes sure that client-side instantiated dynamically spawned NetworkObjects are migrated
+ /// into the same scene (if not already) as they are on the server-side during the initial
+ /// client connection synchronization process.
+ ///
private void SynchronizeNetworkObjectScene()
{
foreach (var networkObject in m_NetworkManager.SpawnManager.SpawnedObjectsList)
{
// This is only done for dynamically spawned NetworkObjects
// Theoretically, a server could have NetworkObjects in a server-side only scene, if the client doesn't have that scene loaded
- // then skip it.
+ // then skip it (it will reside in the currently active scene in this scenario on the client-side)
if (networkObject.IsSceneObject.Value == false && ServerSceneHandleToClientSceneHandle.ContainsKey(networkObject.NetworkSceneHandle))
{
networkObject.SceneOriginHandle = ServerSceneHandleToClientSceneHandle[networkObject.NetworkSceneHandle];
- // If the NetworkObject is not in the scene it should be in, then find the right scene and migrate it to that scene
- // only if it does not have a parent.
+ // If the NetworkObject does not have a parent and is not in the same scene as it is on the server side, then find the right scene
+ // and move it to that scene.
if (networkObject.gameObject.scene.handle != networkObject.SceneOriginHandle && networkObject.transform.parent == null)
{
if (HandleToScene.ContainsKey(networkObject.SceneOriginHandle))
@@ -1945,7 +1966,7 @@ private void HandleClientSceneEvent(uint sceneEventId)
// Synchronize the NetworkObjects for this scene
sceneEventData.SynchronizeSceneNetworkObjects(m_NetworkManager);
- // Now set the active scene (if needed)
+ // If needed, set the currently active scene
if (HashToBuildIndex.ContainsKey(sceneEventData.ActiveSceneHash))
{
var targetActiveScene = SceneManager.GetSceneByBuildIndex(HashToBuildIndex[sceneEventData.ActiveSceneHash]);
@@ -1955,7 +1976,7 @@ private void HandleClientSceneEvent(uint sceneEventId)
}
}
- // Now Synchronize dynamically spawned NetworkObjects to reside in the same scene as on the server side
+ // If needed, migrate dynamically spawned NetworkObjects to the same scene as on the server side
SynchronizeNetworkObjectScene();
sceneEventData.SceneEventType = SceneEventType.SynchronizeComplete;
@@ -2090,9 +2111,12 @@ private void HandleServerSceneEvent(uint sceneEventId, ulong clientId)
OnSynchronizeComplete?.Invoke(clientId);
- // We now can call the client connected callback on the server at this time
- // This assures the client is fully synchronized with all loaded scenes and
- // NetworkObjects
+ // At this time the client is fully synchronized with all loaded scenes and
+ // NetworkObjects and should be considered "fully connected". Send the
+ // notification that the client is connected.
+ // TODO 2023: We should have a better name for this or have multiple states the
+ // client progresses through (the name and associated legacy behavior/expected state
+ // of the client was persisted since MLAPI)
m_NetworkManager.InvokeOnClientConnectedCallback(clientId);
// Check to see if the client needs to resynchronize and before sending the message make sure the client is still connected to avoid
@@ -2168,35 +2192,6 @@ internal void HandleSceneEvent(ulong clientId, FastBufferReader reader)
}
}
- internal void MoveObjectsFromSceneToDontDestroyOnLoad(Scene scene)
- {
- // Create a local copy of the spawned objects list since the spawn manager will adjust the list as objects
- // are despawned.
- var localSpawnedObjectsHashSet = new HashSet(m_NetworkManager.SpawnManager.SpawnedObjectsList);
- foreach (var networkObject in localSpawnedObjectsHashSet)
- {
- if (networkObject == null || (networkObject != null && networkObject.gameObject.scene != scene))
- {
- continue;
- }
-
- // Only NetworkObjects marked to not be destroyed with the scene and are not already in the DDOL
- if (!networkObject.DestroyWithScene && networkObject.gameObject.scene != DontDestroyOnLoadScene)
- {
- // Only move dynamically spawned NetworkObjects with no parent as the children will follow
- if (networkObject.gameObject.transform.parent == null && networkObject.IsSceneObject != null && !networkObject.IsSceneObject.Value)
- {
- UnityEngine.Object.DontDestroyOnLoad(networkObject.gameObject);
- }
- }
- else if (m_NetworkManager.IsServer)
- {
- networkObject.Despawn();
- }
- }
- }
-
-
///
/// Moves all NetworkObjects that don't have the set to
/// the "Do not destroy on load" scene.
diff --git a/com.unity.netcode.gameobjects/TestHelpers/Runtime/IntegrationTestSceneHandler.cs b/com.unity.netcode.gameobjects/TestHelpers/Runtime/IntegrationTestSceneHandler.cs
index f9f3b78d73..aa2db18a4e 100644
--- a/com.unity.netcode.gameobjects/TestHelpers/Runtime/IntegrationTestSceneHandler.cs
+++ b/com.unity.netcode.gameobjects/TestHelpers/Runtime/IntegrationTestSceneHandler.cs
@@ -575,6 +575,72 @@ private void SceneManager_SceneUnloaded(Scene scene)
}
}
+ ///
+ /// Integration test version that handles migrating dynamically spawned NetworkObjects to
+ /// the DDOL when a scene is unloaded
+ ///
+ /// relative instance
+ /// scene being unloaded
+ public void MoveObjectsFromSceneToDontDestroyOnLoad(ref NetworkManager networkManager, Scene scene)
+ {
+ // Create a local copy of the spawned objects list since the spawn manager will adjust the list as objects
+ // are despawned.
+ var networkObjects = Object.FindObjectsOfType().Where((c) => c.IsSpawned);
+ foreach (var networkObject in networkObjects)
+ {
+ if (networkObject == null || (networkObject != null && networkObject.gameObject.scene.handle != scene.handle))
+ {
+ if (networkObject != null)
+ {
+ VerboseDebug($"[MoveObjects from {scene.name} | {scene.handle}] Ignoring {networkObject.gameObject.name} because it isn't in scene {networkObject.gameObject.scene.name} ");
+ }
+ continue;
+ }
+
+ bool skipPrefab = false;
+
+ foreach (var networkPrefab in networkManager.NetworkConfig.Prefabs.Prefabs)
+ {
+ if (networkPrefab.Prefab == null)
+ {
+ continue;
+ }
+ if (networkObject == networkPrefab.Prefab.GetComponent())
+ {
+ skipPrefab = true;
+ break;
+ }
+ }
+ if (skipPrefab)
+ {
+ continue;
+ }
+
+ // Only NetworkObjects marked to not be destroyed with the scene and are not already in the DDOL are preserved
+ if (!networkObject.DestroyWithScene && networkObject.gameObject.scene != networkManager.SceneManager.DontDestroyOnLoadScene)
+ {
+ // Only move dynamically spawned NetworkObjects with no parent as the children will follow
+ if (networkObject.gameObject.transform.parent == null && networkObject.IsSceneObject != null && !networkObject.IsSceneObject.Value)
+ {
+ VerboseDebug($"[MoveObjects from {scene.name} | {scene.handle}] Moving {networkObject.gameObject.name} because it is in scene {networkObject.gameObject.scene.name} with DWS = {networkObject.DestroyWithScene}.");
+ Object.DontDestroyOnLoad(networkObject.gameObject);
+ }
+ }
+ else if (networkManager.IsServer)
+ {
+ if (networkObject.NetworkManager == networkManager)
+ {
+ VerboseDebug($"[MoveObjects from {scene.name} | {scene.handle}] Destroying {networkObject.gameObject.name} because it is in scene {networkObject.gameObject.scene.name} with DWS = {networkObject.DestroyWithScene}.");
+ networkObject.Despawn();
+ }
+ else //For integration testing purposes, migrate remaining into DDOL
+ {
+ VerboseDebug($"[MoveObjects from {scene.name} | {scene.handle}] Temporarily migrating {networkObject.gameObject.name} into DDOL to await server destroy message.");
+ Object.DontDestroyOnLoad(networkObject.gameObject);
+ }
+ }
+ }
+ }
///
/// Constructor now must take NetworkManager
diff --git a/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkObjectSceneMigrationTests.cs b/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkObjectSceneMigrationTests.cs
index c50d061e8e..5ef2e14cb6 100644
--- a/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkObjectSceneMigrationTests.cs
+++ b/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkObjectSceneMigrationTests.cs
@@ -1,3 +1,4 @@
+using System;
using System.Collections;
using System.Collections.Generic;
using NUnit.Framework;
@@ -6,18 +7,55 @@
using UnityEngine.TestTools;
using Unity.Netcode;
using Unity.Netcode.TestHelpers.Runtime;
+using Object = UnityEngine.Object;
namespace TestProject.RuntimeTests
{
+ ///
+ /// NetworkObject Scene Migration Integration Tests
+ ///
+ ///
+ ///
public class NetworkObjectSceneMigrationTests : NetcodeIntegrationTest
{
private List m_TestScenes = new List() { "EmptyScene1", "EmptyScene2", "EmptyScene3" };
protected override int NumberOfClients => 2;
private GameObject m_TestPrefab;
+ private GameObject m_TestPrefabAutoSynchActiveScene;
+ private GameObject m_TestPrefabDestroyWithScene;
+ private Scene m_OriginalActiveScene;
+
+ private bool m_ClientsLoadedScene;
+ private bool m_ClientsUnloadedScene;
+ private Scene m_SceneLoaded;
+ private List m_ServerSpawnedPrefabInstances = new List();
+ private List m_ServerSpawnedDestroyWithSceneInstances = new List();
+ private List m_ScenesLoaded = new List();
+ private string m_CurrentSceneLoading;
+ private string m_CurrentSceneUnloading;
+
+
+ protected override IEnumerator OnSetup()
+ {
+ m_OriginalActiveScene = SceneManager.GetActiveScene();
+ return base.OnSetup();
+ }
protected override void OnServerAndClientsCreated()
{
+ // Synchronize Scene Changes (default) Test Network Prefab
m_TestPrefab = CreateNetworkObjectPrefab("TestObject");
+
+ // Auto Synchronize Active Scene Changes Test Network Prefab
+ m_TestPrefabAutoSynchActiveScene = CreateNetworkObjectPrefab("ASASObject");
+ m_TestPrefabAutoSynchActiveScene.GetComponent().ActiveSceneSynchronization = true;
+
+ // Destroy With Scene Test Network Prefab
+ m_TestPrefabDestroyWithScene = CreateNetworkObjectPrefab("DWSObject");
+ m_TestPrefabDestroyWithScene.AddComponent();
+
+ DestroyWithSceneInstancesTestHelper.ShouldNeverSpawn = m_TestPrefabDestroyWithScene;
+
base.OnServerAndClientsCreated();
}
@@ -25,23 +63,32 @@ protected override void OnNewClientCreated(NetworkManager networkManager)
{
foreach (var networkPrfab in m_ServerNetworkManager.NetworkConfig.Prefabs.Prefabs)
{
+ if (networkPrfab.Prefab == null)
+ {
+ continue;
+ }
networkManager.NetworkConfig.Prefabs.Add(networkPrfab);
}
base.OnNewClientCreated(networkManager);
}
- private bool VerifyAllClientsSpawnedInstances()
+ private bool DidClientsSpawnInstance(NetworkObject serverObject, bool checkDestroyWithScene = false)
{
- foreach (var serverObject in m_ServerSpawnedPrefabInstances)
+ foreach (var networkManager in m_ClientNetworkManagers)
{
- foreach (var networkManager in m_ClientNetworkManagers)
+ if (!s_GlobalNetworkObjects.ContainsKey(networkManager.LocalClientId))
{
- if (!s_GlobalNetworkObjects.ContainsKey(networkManager.LocalClientId))
- {
- return false;
- }
- var clientNetworkObjects = s_GlobalNetworkObjects[networkManager.LocalClientId];
- if (!clientNetworkObjects.ContainsKey(serverObject.NetworkObjectId))
+ return false;
+ }
+ var clientNetworkObjects = s_GlobalNetworkObjects[networkManager.LocalClientId];
+ if (!clientNetworkObjects.ContainsKey(serverObject.NetworkObjectId))
+ {
+ return false;
+ }
+
+ if (checkDestroyWithScene)
+ {
+ if (serverObject.DestroyWithScene != clientNetworkObjects[serverObject.NetworkObjectId])
{
return false;
}
@@ -50,27 +97,65 @@ private bool VerifyAllClientsSpawnedInstances()
return true;
}
- private bool VerifySpawnedObjectsMigrated()
+ private bool VerifyAllClientsSpawnedInstances()
{
foreach (var serverObject in m_ServerSpawnedPrefabInstances)
{
- foreach (var networkManager in m_ClientNetworkManagers)
+ if (!DidClientsSpawnInstance(serverObject))
{
- var clientNetworkObjects = s_GlobalNetworkObjects[networkManager.LocalClientId];
- if (clientNetworkObjects[serverObject.NetworkObjectId].gameObject.scene.name != serverObject.gameObject.scene.name)
- {
- return false;
- }
+ return false;
+ }
+ }
+
+ foreach (var serverObject in m_ServerSpawnedDestroyWithSceneInstances)
+ {
+ if (!DidClientsSpawnInstance(serverObject, true))
+ {
+ return false;
}
}
+
return true;
}
- private bool m_ClientsLoadedScene;
- private Scene m_SceneLoaded;
- private List m_ServerSpawnedPrefabInstances = new List();
- private List m_ScenesLoaded = new List();
- private string m_CurrentSceneLoading;
+ private bool AreClientInstancesInTheRightScene(NetworkObject serverObject)
+ {
+ foreach (var networkManager in m_ClientNetworkManagers)
+ {
+ var clientNetworkObjects = s_GlobalNetworkObjects[networkManager.LocalClientId];
+ if (clientNetworkObjects == null)
+ {
+ continue;
+ }
+ // If a networkObject is null then it was destroyed
+ if (clientNetworkObjects[serverObject.NetworkObjectId].gameObject.scene.name != serverObject.gameObject.scene.name)
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private bool VerifySpawnedObjectsMigrated()
+ {
+ foreach (var serverObject in m_ServerSpawnedPrefabInstances)
+ {
+ if (!AreClientInstancesInTheRightScene(serverObject))
+ {
+ return false;
+ }
+ }
+
+ foreach (var serverObject in m_ServerSpawnedDestroyWithSceneInstances)
+ {
+ if (!AreClientInstancesInTheRightScene(serverObject))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
///
/// Integration test to verify that migrating NetworkObjects
@@ -81,7 +166,7 @@ private bool VerifySpawnedObjectsMigrated()
[UnityTest]
public IEnumerator MigrateIntoNewSceneTest()
{
- // Spawn 10 NetworkObject instances
+ // Spawn 9 NetworkObject instances
for (int i = 0; i < 9; i++)
{
var serverInstance = Object.Instantiate(m_TestPrefab);
@@ -133,30 +218,203 @@ public IEnumerator MigrateIntoNewSceneTest()
[UnityTest]
public IEnumerator ActiveSceneSynchronizationTest()
{
- // WIP
- yield return null;
- }
+ // Disable resynchronization for this test to avoid issues with trying
+ // to synchronize them.
+ NetworkSceneManager.DisableReSynchronization = true;
- public enum MigrateUnloadType
- {
- ActiveSynch,
- DestroyWithScene
+ // Spawn 3 NetworkObject instances that auto synchronize to active scene changes
+ for (int i = 0; i < 3; i++)
+ {
+ var serverInstance = Object.Instantiate(m_TestPrefabAutoSynchActiveScene);
+ var serverNetworkObject = serverInstance.GetComponent();
+ // We are also testing that objects marked to synchronize with changes to
+ // the active scene and marked to destroy with scene =are destroyed= if
+ // the scene being unloaded is currently the active scene and the scene that
+ // the NetworkObjects reside within.
+ serverNetworkObject.Spawn(true);
+ m_ServerSpawnedPrefabInstances.Add(serverNetworkObject);
+ }
+
+ // Spawn 3 NetworkObject instances that do not auto synchronize to active scene changes
+ // and ==should not be== destroyed with the scene (these should be the only remaining
+ // instances)
+ for (int i = 0; i < 3; i++)
+ {
+ var serverInstance = Object.Instantiate(m_TestPrefab);
+ var serverNetworkObject = serverInstance.GetComponent();
+ // This set of NetworkObjects will be used to verify that NetworkObjets
+ // spawned with DestroyWithScene set to false will migrate into the current
+ // active scene if the scene they currently reside within is destroyed and
+ // is not the currently active scene.
+ serverNetworkObject.Spawn();
+ m_ServerSpawnedPrefabInstances.Add(serverNetworkObject);
+ }
+
+ // Spawn 3 NetworkObject instances that do not auto synchronize to active scene changes
+ // and ==should be== destroyed with the scene when it is unloaded
+ for (int i = 0; i < 3; i++)
+ {
+ var serverInstance = Object.Instantiate(m_TestPrefabDestroyWithScene);
+ var serverNetworkObject = serverInstance.GetComponent();
+ // This set of NetworkObjects will be used to verify that NetworkObjets
+ // spawned with DestroyWithScene == true will get destroyed when the scene
+ // is unloaded
+ serverNetworkObject.Spawn(true);
+ m_ServerSpawnedDestroyWithSceneInstances.Add(serverNetworkObject);
+ }
+
+ yield return WaitForConditionOrTimeOut(VerifyAllClientsSpawnedInstances);
+ AssertOnTimeout($"Timed out waiting for all clients to spawn {nameof(NetworkObject)}s!");
+
+ // Now load three scenes
+ m_ServerNetworkManager.SceneManager.OnSceneEvent += SceneManager_OnSceneEvent;
+ for (int i = 0; i < 3; i++)
+ {
+ m_ClientsLoadedScene = false;
+ m_CurrentSceneLoading = m_TestScenes[i];
+ var loadStatus = m_ServerNetworkManager.SceneManager.LoadScene(m_TestScenes[i], LoadSceneMode.Additive);
+ Assert.True(loadStatus == SceneEventProgressStatus.Started, $"Failed to start loading scene {m_CurrentSceneLoading}! Return status: {loadStatus}");
+ yield return WaitForConditionOrTimeOut(() => m_ClientsLoadedScene);
+ AssertOnTimeout($"Timed out waiting for all clients to load scene {m_CurrentSceneLoading}!");
+ }
+
+ // Migrate the instances that don't synchronize with active scene changes into the 3rd loaded scene
+ // (We are making sure these stay in the same scene they are migrated into)
+ for (int i = 3; i < m_ServerSpawnedPrefabInstances.Count; i++)
+ {
+ SceneManager.MoveGameObjectToScene(m_ServerSpawnedPrefabInstances[i].gameObject, m_ScenesLoaded[2]);
+ }
+
+ // Migrate the instances that don't synchronize with active scene changes and are destroyed with the
+ // scene unloading into the 3rd loaded scene
+ // (We are making sure these get destroyed when the scene is unloaded)
+ for (int i = 0; i < m_ServerSpawnedDestroyWithSceneInstances.Count; i++)
+ {
+ SceneManager.MoveGameObjectToScene(m_ServerSpawnedDestroyWithSceneInstances[i].gameObject, m_ScenesLoaded[2]);
+ }
+
+ // Make sure they migrated to the proper scene
+ yield return WaitForConditionOrTimeOut(VerifySpawnedObjectsMigrated);
+ AssertOnTimeout($"Timed out waiting for all clients to migrate all NetworkObjects into the appropriate scenes!");
+
+ // Now change the active scene
+ SceneManager.SetActiveScene(m_ScenesLoaded[1]);
+ // We have to do this
+ Object.DontDestroyOnLoad(m_TestPrefabAutoSynchActiveScene);
+
+ // First, make sure server-side scenes and client side scenes match
+ yield return WaitForConditionOrTimeOut(VerifySpawnedObjectsMigrated);
+ AssertOnTimeout($"Timed out waiting for all clients to migrate all NetworkObjects into the appropriate scenes!");
+
+ // Verify that the auto-active-scene synchronization NetworkObjects migrated to the newly
+ // assigned active scene
+ for (int i = 0; i < 3; i++)
+ {
+ Assert.True(m_ServerSpawnedPrefabInstances[i].gameObject.scene == m_ScenesLoaded[1],
+ $"{m_ServerSpawnedPrefabInstances[i].gameObject.name} did not migrate into scene {m_ScenesLoaded[1].name}!");
+ }
+
+ // Verify that the other NetworkObjects that don't synchronize with active scene changes did
+ // not migrate into the active scene.
+ for (int i = 3; i < m_ServerSpawnedPrefabInstances.Count; i++)
+ {
+ Assert.False(m_ServerSpawnedPrefabInstances[i].gameObject.scene == m_ScenesLoaded[1],
+ $"{m_ServerSpawnedPrefabInstances[i].gameObject.name} migrated into scene {m_ScenesLoaded[1].name}!");
+ }
+
+ for (int i = 0; i < 3; i++)
+ {
+ Assert.False(m_ServerSpawnedDestroyWithSceneInstances[i].gameObject.scene == m_ScenesLoaded[1],
+ $"{m_ServerSpawnedDestroyWithSceneInstances[i].gameObject.name} migrated into scene {m_ScenesLoaded[1].name}!");
+ }
+
+ // Verify that a late joining client synchronizes properly and destroys the appropriate NetworkObjects
+ yield return CreateAndStartNewClient();
+ yield return WaitForConditionOrTimeOut(VerifySpawnedObjectsMigrated);
+ AssertOnTimeout($"[Late Joined Client #1] Timed out waiting for all clients to migrate all NetworkObjects into the appropriate scenes!");
+
+ // Now, unload the scene containing the NetworkObjects that don't synchronize with active scene changes
+ DestroyWithSceneInstancesTestHelper.NetworkObjectDestroyed += OnNonActiveSynchDestroyWithSceneNetworkObjectDestroyed;
+ m_ClientsUnloadedScene = false;
+ m_CurrentSceneUnloading = m_ScenesLoaded[2].name;
+ var status = m_ServerNetworkManager.SceneManager.UnloadScene(m_ScenesLoaded[2]);
+ Assert.True(status == SceneEventProgressStatus.Started, $"Failed to start unloading scene {m_ScenesLoaded[2].name} with status {status}!");
+ yield return WaitForConditionOrTimeOut(() => m_ClientsUnloadedScene);
+
+ // Clean up any destroyed NetworkObjects
+ for (int i = m_ServerSpawnedPrefabInstances.Count - 1; i >= 0; i--)
+ {
+ if (m_ServerSpawnedPrefabInstances[i] == null)
+ {
+ m_ServerSpawnedPrefabInstances.RemoveAt(i);
+ }
+ }
+
+ AssertOnTimeout($"Timed out waiting for all clients to unload scene {m_CurrentSceneUnloading}!");
+ yield return WaitForConditionOrTimeOut(VerifySpawnedObjectsMigrated);
+ AssertOnTimeout($"Timed out waiting for all clients to migrate all NetworkObjects into the appropriate scenes!");
+
+ // Verify that the NetworkObjects that don't synchronize with active scene changes but marked to not
+ // destroy with the scene are migrated into the current active scene
+ for (int i = 3; i < m_ServerSpawnedPrefabInstances.Count; i++)
+ {
+ Assert.True(m_ServerSpawnedPrefabInstances[i].gameObject.scene == m_ScenesLoaded[1],
+ $"{m_ServerSpawnedPrefabInstances[i].gameObject.name} did not migrate into scene {m_ScenesLoaded[1].name} but are in scene {m_ServerSpawnedPrefabInstances[i].gameObject.scene.name}!");
+ }
+
+ // Verify all NetworkObjects that should have been destroyed with the scene unloaded were destroyed
+ yield return WaitForConditionOrTimeOut(() => DestroyWithSceneInstancesTestHelper.ObjectRelativeInstances.Count == 0);
+ DestroyWithSceneInstancesTestHelper.NetworkObjectDestroyed -= OnNonActiveSynchDestroyWithSceneNetworkObjectDestroyed;
+ AssertOnTimeout($"Timed out waiting for all client instances marked to destroy when the scene unloaded to be despawned and destroyed.");
+
+ // Now unload the active scene to verify all remaining NetworkObjects are migrated into the SceneManager
+ // assigned active scene
+ m_ClientsUnloadedScene = false;
+ m_CurrentSceneUnloading = m_ScenesLoaded[1].name;
+ m_ServerNetworkManager.SceneManager.UnloadScene(m_ScenesLoaded[1]);
+ yield return WaitForConditionOrTimeOut(() => m_ClientsUnloadedScene);
+ AssertOnTimeout($"Timed out waiting for all clients to unload scene {m_CurrentSceneUnloading}!");
+
+ // Clean up any destroyed NetworkObjects
+ for (int i = m_ServerSpawnedPrefabInstances.Count - 1; i >= 0; i--)
+ {
+ if (m_ServerSpawnedPrefabInstances[i] == null)
+ {
+ m_ServerSpawnedPrefabInstances.RemoveAt(i);
+ }
+ }
+
+ // Verify a late joining client will synchronize properly with the end result
+ yield return CreateAndStartNewClient();
+
+ // Verify the late joining client spawns all instances
+ yield return WaitForConditionOrTimeOut(VerifyAllClientsSpawnedInstances);
+ AssertOnTimeout($"Timed out waiting for all clients to spawn {nameof(NetworkObject)}s!");
+
+ // Verify the instances are in the correct scenes
+ yield return WaitForConditionOrTimeOut(VerifySpawnedObjectsMigrated);
+ AssertOnTimeout($"[Late Joined Client #2] Timed out waiting for all clients to migrate all NetworkObjects into the appropriate scenes!");
+
+ // All but 3 instances should be destroyed
+ Assert.True(m_ServerSpawnedPrefabInstances.Count == 3, $"{nameof(m_ServerSpawnedPrefabInstances)} still has a count of {m_ServerSpawnedPrefabInstances.Count} " +
+ $"NetworkObject instances!");
+ Assert.True(m_ServerSpawnedDestroyWithSceneInstances.Count == 0, $"{nameof(m_ServerSpawnedDestroyWithSceneInstances)} still has a count of " +
+ $"{m_ServerSpawnedDestroyWithSceneInstances.Count} NetworkObject instances!");
+ for (int i = 0; i < 3; i++)
+ {
+ Assert.True(m_ServerSpawnedPrefabInstances[i].gameObject.name.Contains(m_TestPrefab.gameObject.name), $"Expected {m_ServerSpawnedPrefabInstances[i].gameObject.name} to contain {m_TestPrefab.gameObject.name}!");
+ }
}
///
- /// Integration test to verify that unloading a scene that a NetworkObject
- /// with ActiveSceneSynchronization (true) and DestroyWithScene (false) will
- /// migrate the NetworkObject into the next active scene when their current
- /// one is unloaded.
+ /// Callback invoked when a test prefab, with the
+ /// component attached, is destroyed.
///
- [UnityTest]
- public IEnumerator MigrateOnUnloadSceneTest([Values] MigrateUnloadType migrateUnloadType)
+ private void OnNonActiveSynchDestroyWithSceneNetworkObjectDestroyed(NetworkObject networkObject)
{
- // WIP
- yield return null;
+ m_ServerSpawnedDestroyWithSceneInstances.Remove(networkObject);
}
-
private void SceneManager_OnSceneEvent(SceneEvent sceneEvent)
{
switch (sceneEvent.SceneEventType)
@@ -176,7 +434,77 @@ private void SceneManager_OnSceneEvent(SceneEvent sceneEvent)
m_ClientsLoadedScene = true;
break;
}
+ case SceneEventType.UnloadEventCompleted:
+ {
+ if (sceneEvent.SceneName == m_CurrentSceneUnloading)
+ {
+ m_ClientsUnloadedScene = true;
+ }
+ break;
+ }
}
}
+
+ protected override IEnumerator OnTearDown()
+ {
+ m_TestPrefab = null;
+ m_TestPrefabAutoSynchActiveScene = null;
+ m_TestPrefabDestroyWithScene = null;
+ SceneManager.SetActiveScene(m_OriginalActiveScene);
+ m_ServerSpawnedDestroyWithSceneInstances.Clear();
+ m_ServerSpawnedPrefabInstances.Clear();
+ m_ScenesLoaded.Clear();
+ yield return base.OnTearDown();
+ }
}
+
+ ///
+ /// Helper NetworkBehaviour Component
+ /// For test:
+ ///
+ internal class DestroyWithSceneInstancesTestHelper : NetworkBehaviour
+ {
+ public static GameObject ShouldNeverSpawn;
+
+ public static Dictionary> ObjectRelativeInstances = new Dictionary>();
+
+ public static Action NetworkObjectDestroyed;
+
+ ///
+ /// Called when destroyed
+ /// Passes the client ID and the NetworkObject instance
+ ///
+ public Action ObjectDestroyed;
+
+ public override void OnNetworkSpawn()
+ {
+ if (!ObjectRelativeInstances.ContainsKey(NetworkManager.LocalClientId))
+ {
+ ObjectRelativeInstances.Add(NetworkManager.LocalClientId, new Dictionary());
+ }
+
+ ObjectRelativeInstances[NetworkManager.LocalClientId].Add(NetworkObjectId, NetworkObject);
+ base.OnNetworkSpawn();
+ }
+
+ public override void OnNetworkDespawn()
+ {
+ ObjectRelativeInstances[NetworkManager.LocalClientId].Remove(NetworkObjectId);
+ if (ObjectRelativeInstances[NetworkManager.LocalClientId].Count == 0)
+ {
+ ObjectRelativeInstances.Remove(NetworkManager.LocalClientId);
+ }
+ base.OnNetworkDespawn();
+ }
+
+ public override void OnDestroy()
+ {
+ if (NetworkManager.LocalClientId == NetworkManager.ServerClientId)
+ {
+ NetworkObjectDestroyed?.Invoke(NetworkObject);
+ }
+ base.OnDestroy();
+ }
+ }
+
}
From ca15b806711553b62fcafc8f756282b66a56376a Mon Sep 17 00:00:00 2001
From: NoelStephensUnity <73188597+NoelStephensUnity@users.noreply.github.com>
Date: Sun, 12 Feb 2023 17:07:46 -0600
Subject: [PATCH 19/25] update and test
Added last test, ClientSynchronizationModeTests, for this PR that validates preloaded scenes will be used and in-scene placed NetworkObjects will be synchronized even when the active scene changes which could change the order in which scenes are loaded and synchronized.
This includes minor adjustments to one of the existing test.
This includes the additional INetworkSceneManager.SetClientSynchronizationMode method that now builds a list of preloaded scenes if the client synchronization mode is additive.
Making the parent dynamic under inscene placed run in client synchronization additive mode again.
---
.../DefaultSceneManagerHandler.cs | 88 +++++-
.../SceneManagement/ISceneManagerHandler.cs | 2 +
.../SceneManagement/NetworkSceneManager.cs | 13 +-
.../Runtime/IntegrationTestSceneHandler.cs | 233 +++++++++++++-
.../Runtime/NetcodeIntegrationTest.cs | 1 +
.../ClientSynchronizationModeTests.cs | 283 ++++++++++++++++++
.../ClientSynchronizationModeTests.cs.meta | 11 +
.../NetworkSceneManagerSeneVerification.cs | 19 +-
.../ParentDynamicUnderInScenePlaced.cs | 6 +
9 files changed, 631 insertions(+), 25 deletions(-)
create mode 100644 testproject/Assets/Tests/Runtime/NetworkSceneManager/ClientSynchronizationModeTests.cs
create mode 100644 testproject/Assets/Tests/Runtime/NetworkSceneManager/ClientSynchronizationModeTests.cs.meta
diff --git a/com.unity.netcode.gameobjects/Runtime/SceneManagement/DefaultSceneManagerHandler.cs b/com.unity.netcode.gameobjects/Runtime/SceneManagement/DefaultSceneManagerHandler.cs
index c45cc48c33..fd03064c2d 100644
--- a/com.unity.netcode.gameobjects/Runtime/SceneManagement/DefaultSceneManagerHandler.cs
+++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/DefaultSceneManagerHandler.cs
@@ -91,16 +91,49 @@ public void StartTrackingScene(Scene scene, bool assigned, NetworkManager networ
///
public bool DoesSceneHaveUnassignedEntry(string sceneName, NetworkManager networkManager)
{
- if (SceneNameToSceneHandles.ContainsKey(sceneName))
+ var scenesWithSceneName = new List();
+
+ // Get all loaded scenes with the same name
+ for (int i = 0; i < SceneManager.sceneCount; i++)
{
- foreach (var sceneHandleEntry in SceneNameToSceneHandles[sceneName])
+ var scene = SceneManager.GetSceneAt(i);
+ if (scene.name == sceneName)
{
- if (!sceneHandleEntry.Value.IsAssigned)
- {
- return true;
- }
+ scenesWithSceneName.Add(scene);
+ }
+ }
+
+ // If there are no scenes of this name loaded then we have no loaded scenes
+ // to use
+ if (scenesWithSceneName.Count == 0)
+ {
+ return false;
+ }
+
+ // If we have 1 or more scenes with the name and we have no entries, then we do have
+ // a scene to use
+ if (scenesWithSceneName.Count > 0 && !SceneNameToSceneHandles.ContainsKey(sceneName))
+ {
+ return true;
+ }
+
+ // Determine if any of the loaded scenes has been used for synchronizing
+ foreach (var scene in scenesWithSceneName)
+ {
+ // If we don't have the handle, then we can use that scene
+ if (!SceneNameToSceneHandles[scene.name].ContainsKey(scene.handle))
+ {
+ return true;
+ }
+
+ // If we have an entry, but it is not yet assigned (i.e. preloaded)
+ // then we can use that.
+ if (!SceneNameToSceneHandles[scene.name][scene.handle].IsAssigned)
+ {
+ return true;
}
}
+ // If none were found, then we have no available scene (which most likely means one will get loaded)
return false;
}
@@ -251,5 +284,48 @@ public void MoveObjectsFromSceneToDontDestroyOnLoad(ref NetworkManager networkMa
}
}
}
+
+ ///
+ /// Sets the client synchronization mode which impacts whether both the server or client take into consideration scenes loaded before
+ /// starting the .
+ ///
+ ///
+ /// : Does not take preloaded scenes into consideration
+ /// : Does take preloaded scenes into consideration
+ ///
+ /// relative instance
+ /// or
+ public void SetClientSynchronizationMode(ref NetworkManager networkManager, LoadSceneMode mode)
+ {
+ var sceneManager = networkManager.SceneManager;
+
+ // For additive client synchronization, we take into consideration scenes
+ // already loaded.
+ if (mode == LoadSceneMode.Additive)
+ {
+ for (int i = 0; i < SceneManager.sceneCount; i++)
+ {
+ var scene = SceneManager.GetSceneAt(i);
+
+ // If using scene verification
+ if (sceneManager.VerifySceneBeforeLoading != null)
+ {
+ // Determine if we should take this scene into consideration
+ if (!sceneManager.VerifySceneBeforeLoading.Invoke(scene.buildIndex, scene.name, LoadSceneMode.Additive))
+ {
+ continue;
+ }
+ }
+
+ // If the scene is not already in the ScenesLoaded list, then add it
+ if (!sceneManager.ScenesLoaded.ContainsKey(scene.handle))
+ {
+ sceneManager.ScenesLoaded.Add(scene.handle, scene);
+ }
+ }
+ }
+ // Set the client synchronization mode
+ sceneManager.ClientSynchronizationMode = mode;
+ }
}
}
diff --git a/com.unity.netcode.gameobjects/Runtime/SceneManagement/ISceneManagerHandler.cs b/com.unity.netcode.gameobjects/Runtime/SceneManagement/ISceneManagerHandler.cs
index 4d15db4cff..e761232440 100644
--- a/com.unity.netcode.gameobjects/Runtime/SceneManagement/ISceneManagerHandler.cs
+++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/ISceneManagerHandler.cs
@@ -28,5 +28,7 @@ internal interface ISceneManagerHandler
void UnloadUnassignedScenes(NetworkManager networkManager = null);
void MoveObjectsFromSceneToDontDestroyOnLoad(ref NetworkManager networkManager, Scene scene);
+
+ void SetClientSynchronizationMode(ref NetworkManager networkManager, LoadSceneMode mode);
}
}
diff --git a/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs b/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs
index f3a56a0743..4ebfce0bff 100644
--- a/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs
+++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs
@@ -576,6 +576,11 @@ internal string GetSceneNameFromPath(string scenePath)
///
internal void GenerateScenesInBuild()
{
+ // TODO 2023: We could support addressable or asset bundle scenes by
+ // adding a method that would allow users to add scenes to this.
+ // The method would be server-side only and require an additional SceneEventType
+ // that would be used to notify clients of the added scene. This might need
+ // to include information about the addressable or asset bundle (i.e. address to load assets)
HashToBuildIndex.Clear();
BuildIndexToHash.Clear();
for (int i = 0; i < SceneManager.sceneCountInBuildSettings; i++)
@@ -675,7 +680,8 @@ public void DisableValidationWarnings(bool disabled)
/// for initial client synchronization
public void SetClientSynchronizationMode(LoadSceneMode mode)
{
- ClientSynchronizationMode = mode;
+ var networkManager = m_NetworkManager;
+ SceneManagerHandler.SetClientSynchronizationMode(ref networkManager, mode);
}
///
@@ -688,11 +694,13 @@ internal NetworkSceneManager(NetworkManager networkManager)
m_NetworkManager = networkManager;
SceneEventDataStore = new Dictionary();
+ // Generates the scene name to hash value
GenerateScenesInBuild();
// Since NetworkManager is now always migrated to the DDOL we will use this to get the DDOL scene
DontDestroyOnLoadScene = networkManager.gameObject.scene;
+ // Add to the server to client scene handle table
UpdateServerClientSceneHandle(DontDestroyOnLoadScene.handle, DontDestroyOnLoadScene.handle, DontDestroyOnLoadScene);
ScenesLoaded.Add(DontDestroyOnLoadScene.handle, DontDestroyOnLoadScene);
}
@@ -2256,7 +2264,8 @@ internal void PopulateScenePlacedObjects(Scene sceneToFilterBy, bool clearSceneP
var globalObjectIdHash = networkObjectInstance.GlobalObjectIdHash;
var sceneHandle = networkObjectInstance.GetSceneOriginHandle();
// We check to make sure the NetworkManager instance is the same one to be "NetcodeIntegrationTestHelpers" compatible and filter the list on a per scene basis (for additive scenes)
- if (networkObjectInstance.IsSceneObject != false && networkObjectInstance.NetworkManager == m_NetworkManager && sceneHandle == sceneToFilterBy.handle)
+ if (networkObjectInstance.IsSceneObject != false && (networkObjectInstance.NetworkManager == m_NetworkManager ||
+ networkObjectInstance.NetworkManagerOwner == null) && sceneHandle == sceneToFilterBy.handle)
{
if (!ScenePlacedObjects.ContainsKey(globalObjectIdHash))
{
diff --git a/com.unity.netcode.gameobjects/TestHelpers/Runtime/IntegrationTestSceneHandler.cs b/com.unity.netcode.gameobjects/TestHelpers/Runtime/IntegrationTestSceneHandler.cs
index aa2db18a4e..dc75646526 100644
--- a/com.unity.netcode.gameobjects/TestHelpers/Runtime/IntegrationTestSceneHandler.cs
+++ b/com.unity.netcode.gameobjects/TestHelpers/Runtime/IntegrationTestSceneHandler.cs
@@ -23,7 +23,7 @@ internal struct SceneEntry
public Scene Scene;
}
- internal Dictionary>> SceneNameToSceneHandles = new Dictionary>>();
+ internal static Dictionary>> SceneNameToSceneHandles = new Dictionary>>();
// All IntegrationTestSceneHandler instances register their associated NetworkManager
internal static List NetworkManagers = new List();
@@ -277,8 +277,8 @@ private void Sever_SceneLoaded(Scene scene, LoadSceneMode arg1)
{
if (m_ServerSceneBeingLoaded == scene.name)
{
- ProcessInSceneObjects(scene, NetworkManager);
SceneManager.sceneLoaded -= Sever_SceneLoaded;
+ ProcessInSceneObjects(scene, NetworkManager);
}
}
@@ -340,6 +340,7 @@ internal Scene GetAndAddNewlyLoadedSceneByName(string sceneName)
{
continue;
}
+
if (networkManager.SceneManager.ScenesLoaded.ContainsKey(sceneLoaded.handle))
{
if (NetworkManager.LogLevel == LogLevel.Developer)
@@ -357,7 +358,12 @@ internal Scene GetAndAddNewlyLoadedSceneByName(string sceneName)
{
NetworkLog.LogInfo($"{NetworkManager.name} adding {sceneLoaded.name} with a handle of {sceneLoaded.handle} to its ScenesLoaded.");
}
+ if (DoesANetworkManagerHoldThisScene(sceneLoaded))
+ {
+ continue;
+ }
NetworkManager.SceneManager.ScenesLoaded.Add(sceneLoaded.handle, sceneLoaded);
+ StartTrackingScene(sceneLoaded, true, NetworkManager);
return sceneLoaded;
}
}
@@ -421,33 +427,126 @@ public void StartTrackingScene(Scene scene, bool assigned, NetworkManager networ
};
SceneNameToSceneHandles[networkManager][scene.name].Add(scene.handle, sceneEntry);
}
- else
+ //else
+ //{
+ // if (exceptionOnExisting)
+ // {
+ // throw new Exception($"[{networkManager.LocalClient.PlayerObject.name}][Duplicate Handle] Scene {scene.name} already has scene handle {scene.handle} registered!");
+ // }
+ //}
+ }
+
+ private bool DoesANetworkManagerHoldThisScene(Scene scene)
+ {
+ foreach (var netManEntry in SceneNameToSceneHandles)
{
- throw new Exception($"[{networkManager.LocalClient.PlayerObject.name}][Duplicate Handle] Scene {scene.name} already has scene handle {scene.handle} registered!");
+ if (!netManEntry.Value.ContainsKey(scene.name))
+ {
+ continue;
+ }
+ // The other NetworkManager only has to have an entry to
+ // disqualify this scene instance
+ if (netManEntry.Value[scene.name].ContainsKey(scene.handle))
+ {
+ return true;
+ }
}
+
+ return false;
}
public bool DoesSceneHaveUnassignedEntry(string sceneName, NetworkManager networkManager)
{
- if (!SceneNameToSceneHandles.ContainsKey(networkManager))
+ var scenesWithSceneName = new List();
+ var scenesAssigned = new List();
+ for (int i = 0; i < SceneManager.sceneCount; i++)
{
- return false;
+ var scene = SceneManager.GetSceneAt(i);
+ if (scene.name == sceneName)
+ {
+ scenesWithSceneName.Add(scene);
+ }
}
- if (SceneNameToSceneHandles[networkManager].ContainsKey(sceneName))
+
+ // Check for other NetworkManager instances already having been assigned this scene
+ foreach (var netManEntry in SceneNameToSceneHandles)
{
- foreach (var sceneHandleEntry in SceneNameToSceneHandles[networkManager][sceneName])
+ // Ignore this NetworkManager instance at this stage
+ if (netManEntry.Key == networkManager)
{
- if (!sceneHandleEntry.Value.IsAssigned)
+ continue;
+ }
+
+ foreach (var scene in scenesWithSceneName)
+ {
+ if (!netManEntry.Value.ContainsKey(scene.name))
+ {
+ continue;
+ }
+ // The other NetworkManager only has to have an entry to
+ // disqualify this scene instance
+ if (netManEntry.Value[scene.name].ContainsKey(scene.handle))
{
- return true;
+ scenesAssigned.Add(scene);
}
}
}
+
+ // Remove all of the assigned scenes from the list of scenes with the
+ // passed in scene name.
+ foreach (var assignedScene in scenesAssigned)
+ {
+ if (scenesWithSceneName.Contains(assignedScene))
+ {
+ scenesWithSceneName.Remove(assignedScene);
+ }
+ }
+
+ // If all currently loaded scenes with the scene name are taken
+ // then we return false
+ if (scenesWithSceneName.Count == 0)
+ {
+ return false;
+ }
+
+ // If we made it here, then no other NetworkManager is tracking this scene
+ // and if we don't have an entry for this NetworkManager then we can use any
+ // of the remaining scenes loaded with that name.
+ if (!SceneNameToSceneHandles.ContainsKey(networkManager))
+ {
+ return true;
+ }
+
+ // If we don't yet have a scene name in this NetworkManager's lookup table,
+ // then we can use any of the remaining availabel scenes with that scene name
+ if (!SceneNameToSceneHandles[networkManager].ContainsKey(sceneName))
+ {
+ return true;
+ }
+
+ foreach (var scene in scenesWithSceneName)
+ {
+ // If we don't have an entry for this scene handle (with the scene name) then we
+ // can use that scene
+ if (!SceneNameToSceneHandles[networkManager][scene.name].ContainsKey(scene.handle))
+ {
+ return true;
+ }
+
+ // This entry is not assigned, then we can use the associated scene
+ if (!SceneNameToSceneHandles[networkManager][scene.name][scene.handle].IsAssigned)
+ {
+ return true;
+ }
+ }
+
+ // None of the scenes with the same scene name can be used
return false;
}
public Scene GetSceneFromLoadedScenes(string sceneName, NetworkManager networkManager)
{
+
if (!SceneNameToSceneHandles.ContainsKey(networkManager))
{
return m_InvalidScene;
@@ -465,13 +564,26 @@ public Scene GetSceneFromLoadedScenes(string sceneName, NetworkManager networkMa
}
}
}
+ // This is tricky since NetworkManager instances share the same scene hierarchy during integration tests.
+ // TODO 2023: Determine if there is a better way to associate the active scene for client NetworkManager instances.
+ var activeScene = SceneManager.GetActiveScene();
+
+ if (sceneName == activeScene.name && networkManager.SceneManager.ClientSynchronizationMode == LoadSceneMode.Additive)
+ {
+ // For now, just return the current active scene
+ // Note: Clients will not be able to synchronize in-scene placed NetworkObjects in an integration test for
+ // scenes loaded that have in-scene placed NetworkObjects prior to the clients joining (i.e. there will only
+ // ever be one instance of the active scene). To test in-scene placed NetworkObjects and make an integration
+ // test loaded scene be the active scene, don't set scene as an active scene on the server side until all
+ // clients have connected and loaded the scene.
+ return activeScene;
+ }
// If we found nothing return an invalid scene
return m_InvalidScene;
}
public void PopulateLoadedScenes(ref Dictionary scenesLoaded, NetworkManager networkManager)
{
- SceneNameToSceneHandles.Clear();
if (!SceneNameToSceneHandles.ContainsKey(networkManager))
{
SceneNameToSceneHandles.Add(networkManager, new Dictionary>());
@@ -481,6 +593,18 @@ public void PopulateLoadedScenes(ref Dictionary scenesLoaded, Networ
for (int i = 0; i < sceneCount; i++)
{
var scene = SceneManager.GetSceneAt(i);
+ // Ignore scenes that belong to other NetworkManager instances
+
+ if (DoesANetworkManagerHoldThisScene(scene))
+ {
+ continue;
+ }
+
+ if (!DoesSceneHaveUnassignedEntry(scene.name, networkManager))
+ {
+ continue;
+ }
+
if (!SceneNameToSceneHandles[networkManager].ContainsKey(scene.name))
{
SceneNameToSceneHandles[networkManager].Add(scene.name, new Dictionary());
@@ -642,6 +766,93 @@ public void MoveObjectsFromSceneToDontDestroyOnLoad(ref NetworkManager networkMa
}
}
+ ///
+ /// Sets the client synchronization mode which impacts whether both the server or client take into consideration scenes loaded before
+ /// starting the .
+ ///
+ ///
+ /// : Does not take preloaded scenes into consideration
+ /// : Does take preloaded scenes into consideration
+ ///
+ /// relative instance
+ /// or
+ public void SetClientSynchronizationMode(ref NetworkManager networkManager, LoadSceneMode mode)
+ {
+ var sceneManager = networkManager.SceneManager;
+
+
+ // For additive client synchronization, we take into consideration scenes
+ // already loaded.
+ if (mode == LoadSceneMode.Additive)
+ {
+ if (networkManager.IsServer)
+ {
+ sceneManager.OnSceneEvent -= SceneManager_OnSceneEvent;
+ sceneManager.OnSceneEvent += SceneManager_OnSceneEvent;
+ }
+
+ if (!SceneNameToSceneHandles.ContainsKey(networkManager))
+ {
+ SceneNameToSceneHandles.Add(networkManager, new Dictionary>());
+ }
+
+ var networkManagerScenes = SceneNameToSceneHandles[networkManager];
+
+ for (int i = 0; i < SceneManager.sceneCount; i++)
+ {
+ var scene = SceneManager.GetSceneAt(i);
+
+ // Ignore scenes that belong to other NetworkManager instances
+ if (!DoesSceneHaveUnassignedEntry(scene.name, networkManager))
+ {
+ continue;
+ }
+
+ // If using scene verification
+ if (sceneManager.VerifySceneBeforeLoading != null)
+ {
+ // Determine if we should take this scene into consideration
+ if (!sceneManager.VerifySceneBeforeLoading.Invoke(scene.buildIndex, scene.name, LoadSceneMode.Additive))
+ {
+ continue;
+ }
+ }
+
+ // If the scene is not already in the ScenesLoaded list, then add it
+ if (!sceneManager.ScenesLoaded.ContainsKey(scene.handle))
+ {
+ StartTrackingScene(scene, true, networkManager);
+ sceneManager.ScenesLoaded.Add(scene.handle, scene);
+ }
+ }
+ }
+ // Set the client synchronization mode
+ sceneManager.ClientSynchronizationMode = mode;
+ }
+
+ ///
+ /// During integration testing, if the server loads a scene then
+ /// we want to start tracking it.
+ ///
+ ///
+ private void SceneManager_OnSceneEvent(SceneEvent sceneEvent)
+ {
+ // Filter for server only scene events
+ if (!NetworkManager.IsServer || sceneEvent.ClientId != NetworkManager.ServerClientId)
+ {
+ return;
+ }
+
+ switch (sceneEvent.SceneEventType)
+ {
+ case SceneEventType.LoadComplete:
+ {
+ StartTrackingScene(sceneEvent.Scene, true, NetworkManager);
+ break;
+ }
+ }
+ }
+
///
/// Constructor now must take NetworkManager
///
diff --git a/com.unity.netcode.gameobjects/TestHelpers/Runtime/NetcodeIntegrationTest.cs b/com.unity.netcode.gameobjects/TestHelpers/Runtime/NetcodeIntegrationTest.cs
index cce5d2f19b..f7c9d91325 100644
--- a/com.unity.netcode.gameobjects/TestHelpers/Runtime/NetcodeIntegrationTest.cs
+++ b/com.unity.netcode.gameobjects/TestHelpers/Runtime/NetcodeIntegrationTest.cs
@@ -692,6 +692,7 @@ protected virtual IEnumerator OnTearDown()
[UnityTearDown]
public IEnumerator TearDown()
{
+ IntegrationTestSceneHandler.SceneNameToSceneHandles.Clear();
VerboseDebug($"Entering {nameof(TearDown)}");
yield return OnTearDown();
diff --git a/testproject/Assets/Tests/Runtime/NetworkSceneManager/ClientSynchronizationModeTests.cs b/testproject/Assets/Tests/Runtime/NetworkSceneManager/ClientSynchronizationModeTests.cs
new file mode 100644
index 0000000000..9d15c892b1
--- /dev/null
+++ b/testproject/Assets/Tests/Runtime/NetworkSceneManager/ClientSynchronizationModeTests.cs
@@ -0,0 +1,283 @@
+using System.Collections;
+using System.Collections.Generic;
+using NUnit.Framework;
+using UnityEngine.SceneManagement;
+using UnityEngine.TestTools;
+using Unity.Netcode;
+using Unity.Netcode.TestHelpers.Runtime;
+
+namespace TestProject.RuntimeTests
+{
+ ///
+ /// Validates that set to
+ /// Will synchronize clients properly when scenes are preloaded and when the server changes the currently active scene
+ /// prior to a client joining.
+ ///
+ ///
+ /// Note: If a client does not preload a scene prior to the server setting it as the active scene then the client(s) and the
+ /// server will end up sharing the same scene. More info:
+ ///
+ [TestFixture(ServerPreloadStates.NoPreloadOnServer)]
+ [TestFixture(ServerPreloadStates.PreloadOnServer)]
+ public class ClientSynchronizationModeTests : NetcodeIntegrationTest
+ {
+ // Two scenes with different in-scene placed NetworkObjects and one empty scene used to test active scene switching and client synchronization.
+ private List m_TestScenes = new List() { "InSceneNetworkObject", "GenericInScenePlacedObject", "EmptyScene1" };
+
+ protected override int NumberOfClients => 0;
+
+ public enum ServerPreloadStates
+ {
+ PreloadOnServer,
+ NoPreloadOnServer
+ }
+
+ public enum ClientPreloadStates
+ {
+ PreloadOnClient,
+ NoPreloadOnClient
+ }
+
+ public enum ActiveSceneStates
+ {
+ DefaultActiveScene,
+ SwitchActiveScene,
+ }
+
+ private ServerPreloadStates m_ServerPreloadState;
+ private List m_ServerLoadedScenes = new List();
+ private List m_TempClientPreLoadedScenes = new List();
+
+ private Dictionary> m_ClientScenesLoaded = new Dictionary>();
+
+
+ public ClientSynchronizationModeTests(ServerPreloadStates serverPreloadStates)
+ {
+ m_ServerPreloadState = serverPreloadStates;
+ }
+
+ protected override IEnumerator OnSetup()
+ {
+ m_TempClientPreLoadedScenes.Clear();
+ m_ServerLoadedScenes.Clear();
+ if (m_ServerPreloadState == ServerPreloadStates.PreloadOnServer)
+ {
+ SceneManager.sceneLoaded += SceneManager_sceneLoaded;
+ yield return LoadScenesOnServer();
+ }
+ yield return base.OnSetup();
+ }
+
+ private IEnumerator LoadScenesOnServer()
+ {
+ if (m_ServerPreloadState == ServerPreloadStates.PreloadOnServer)
+ {
+ foreach (var sceneToLoad in m_TestScenes)
+ {
+ SceneManager.LoadSceneAsync(sceneToLoad, LoadSceneMode.Additive);
+ }
+ yield return WaitForConditionOrTimeOut(AllScenesLoadedOnServer);
+ SceneManager.sceneLoaded -= SceneManager_sceneLoaded;
+ AssertOnTimeout($"[{m_ServerPreloadState}] Timed out waiting for all server-side scenes to be loaded!");
+ }
+ else
+ {
+ foreach (var sceneToLoad in m_TestScenes)
+ {
+ m_ServerNetworkManager.SceneManager.LoadScene(sceneToLoad, LoadSceneMode.Additive);
+ yield return WaitForConditionOrTimeOut(() => SceneLoadedOnServer(sceneToLoad));
+ AssertOnTimeout($"[{m_ServerPreloadState}] Timed out waiting for scene {sceneToLoad} to be loaded!");
+ }
+ }
+ }
+
+ private bool SceneLoadedOnServer(string sceneName)
+ {
+ foreach (var scene in m_ServerLoadedScenes)
+ {
+ if (scene.name == sceneName)
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private bool AllScenesLoadedOnServer()
+ {
+ if (m_ServerLoadedScenes.Count == m_TestScenes.Count)
+ {
+ foreach (var loadedScene in m_ServerLoadedScenes)
+ {
+ if (!m_TestScenes.Contains(loadedScene.name))
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+
+ private bool AllScenesPreloadedForClient()
+ {
+ if (m_TempClientPreLoadedScenes.Count == m_TestScenes.Count)
+ {
+ foreach (var loadedScene in m_TempClientPreLoadedScenes)
+ {
+ if (!m_TestScenes.Contains(loadedScene.name))
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+
+ private bool AllScenesLoadedOnClients()
+ {
+ foreach (var clientNetworkManager in m_ClientNetworkManagers)
+ {
+ if (!m_ClientScenesLoaded.ContainsKey(clientNetworkManager.LocalClientId))
+ {
+ return false;
+ }
+
+ if (m_ClientScenesLoaded[clientNetworkManager.LocalClientId].Count != m_TestScenes.Count)
+ {
+ return false;
+ }
+ foreach (var loadedScene in m_ClientScenesLoaded[clientNetworkManager.LocalClientId])
+ {
+ if (!m_TestScenes.Contains(loadedScene.name))
+ {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ private void SceneManager_sceneLoaded(Scene scene, LoadSceneMode loadSceneMode)
+ {
+ m_ServerLoadedScenes.Add(scene);
+ }
+
+
+ protected override IEnumerator OnStartedServerAndClients()
+ {
+ m_ServerNetworkManager.SceneManager.SetClientSynchronizationMode(LoadSceneMode.Additive);
+ return base.OnStartedServerAndClients();
+ }
+
+ protected override void OnNewClientStarted(NetworkManager networkManager)
+ {
+ networkManager.SceneManager.OnSceneEvent += ClientSide_OnSceneEvent;
+ base.OnNewClientStarted(networkManager);
+ }
+
+ ///
+ /// Verifies that both clients and the server will utilize preloaded scenes and that
+ /// in-scene placed NetworkObjects synchronize properly if the active scene changes
+ /// prior to a client connecting.
+ ///
+ ///
+ /// Notes:
+ /// ClientPreloadStates.NoPreloadOnClient: verifies that if no scene that needs to
+ /// be synchronized is preloaded the client will load the scene to be synchronized.
+ ///
+ /// ClientPreloadStates.PreloadOnClient: verifies that if a client has scenes that
+ /// will be synchronized preloaded the client will use those scenes as opposed to
+ /// loading duplicates.
+ ///
+ [UnityTest]
+ public IEnumerator PreloadedScenesTest([Values] ClientPreloadStates clientPreloadStates, [Values] ActiveSceneStates activeSceneState)
+ {
+ // If we didn't preload the scenes, then load the scenes via NetworkSceneManager
+ if (m_ServerPreloadState == ServerPreloadStates.NoPreloadOnServer)
+ {
+ m_ServerNetworkManager.SceneManager.OnSceneEvent += ServerSide_OnSceneEvent;
+ yield return LoadScenesOnServer();
+ }
+
+ // This tests that a change in the active scene will not impact in-scene placed
+ // NetworkObject synchronization (if it does then the clients would get a soft
+ // synchronization error).
+ if (activeSceneState == ActiveSceneStates.SwitchActiveScene)
+ {
+ SceneManager.SetActiveScene(m_ServerLoadedScenes[2]);
+ }
+
+ // Late join some clients
+ for (int i = 0; i < 1; i++)
+ {
+ // This tests that clients can have scenes preloaded prior to
+ // connecting and will use those scenes for synchronization
+ if (clientPreloadStates == ClientPreloadStates.PreloadOnClient)
+ {
+ m_TempClientPreLoadedScenes.Clear();
+ SceneManager.sceneLoaded += PreLoadClient_SceneLoaded;
+ foreach (var sceneToLoad in m_TestScenes)
+ {
+ SceneManager.LoadSceneAsync(sceneToLoad, LoadSceneMode.Additive);
+ }
+ yield return WaitForConditionOrTimeOut(AllScenesPreloadedForClient);
+ SceneManager.sceneLoaded -= PreLoadClient_SceneLoaded;
+ AssertOnTimeout($"[{clientPreloadStates}] Timed out waiting for client-side scenes to be preloaded!");
+ }
+ yield return CreateAndStartNewClient();
+ AssertOnTimeout($"[Client Instance {i + 1}] Timed out waiting for client to start and connect!");
+
+ yield return WaitForConditionOrTimeOut(AllScenesLoadedOnClients);
+ AssertOnTimeout($"[Client-{m_ClientNetworkManagers[i].LocalClientId}] Timed out waiting for all scenes to be synchronized for new client!");
+ }
+ }
+
+ private void PreLoadClient_SceneLoaded(Scene scene, LoadSceneMode loadSceneMode)
+ {
+ m_TempClientPreLoadedScenes.Add(scene);
+ }
+
+ private void ClientSide_OnSceneEvent(SceneEvent sceneEvent)
+ {
+ switch (sceneEvent.SceneEventType)
+ {
+ case SceneEventType.LoadComplete:
+ {
+ if (!m_ClientScenesLoaded.ContainsKey(sceneEvent.ClientId))
+ {
+ m_ClientScenesLoaded.Add(sceneEvent.ClientId, new List());
+ }
+ m_ClientScenesLoaded[sceneEvent.ClientId].Add(sceneEvent.Scene);
+ break;
+ }
+ }
+ }
+ private void ServerSide_OnSceneEvent(SceneEvent sceneEvent)
+ {
+ // Filter for server-side only scene events
+ if (sceneEvent.ClientId != m_ServerNetworkManager.LocalClientId)
+ {
+ return;
+ }
+
+ switch (sceneEvent.SceneEventType)
+ {
+ case SceneEventType.LoadComplete:
+ {
+ m_ServerLoadedScenes.Add(sceneEvent.Scene);
+ break;
+ }
+ }
+ }
+
+ protected override IEnumerator OnTearDown()
+ {
+ SceneManager.sceneLoaded -= SceneManager_sceneLoaded;
+ m_TempClientPreLoadedScenes.Clear();
+ m_ClientScenesLoaded.Clear();
+ return base.OnTearDown();
+ }
+ }
+}
diff --git a/testproject/Assets/Tests/Runtime/NetworkSceneManager/ClientSynchronizationModeTests.cs.meta b/testproject/Assets/Tests/Runtime/NetworkSceneManager/ClientSynchronizationModeTests.cs.meta
new file mode 100644
index 0000000000..596123e1c6
--- /dev/null
+++ b/testproject/Assets/Tests/Runtime/NetworkSceneManager/ClientSynchronizationModeTests.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 26618a817fc595d4dbc1ad91cc89e53d
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkSceneManagerSeneVerification.cs b/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkSceneManagerSeneVerification.cs
index efeeb328da..185ec39360 100644
--- a/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkSceneManagerSeneVerification.cs
+++ b/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkSceneManagerSeneVerification.cs
@@ -161,10 +161,8 @@ private void InitializeSceneTestInfo(LoadSceneMode clientSynchronizationMode, bo
m_ScenesLoaded.Clear();
foreach (var manager in m_ClientNetworkManagers)
{
-
m_ShouldWaitList.Add(new SceneTestInfo() { ClientId = manager.LocalClientId, ShouldWait = false });
manager.SceneManager.VerifySceneBeforeLoading = m_ClientVerificationAction;
- manager.SceneManager.SetClientSynchronizationMode(clientSynchronizationMode);
}
}
@@ -255,11 +253,20 @@ private bool ContainsAllClients(List clients)
private bool ServerVerifySceneBeforeLoading(int sceneIndex, string sceneName, LoadSceneMode loadSceneMode)
{
- Assert.IsTrue(m_ExpectedSceneIndex == sceneIndex);
- Assert.IsTrue(m_ExpectedSceneName == sceneName);
- Assert.IsTrue(m_ExpectedLoadMode == loadSceneMode);
+ if (m_ExpectedSceneIndex != 0 && m_ExpectedSceneName != null)
+ {
+ // Ignore the test runner test scene.
+ if (sceneIndex != m_ExpectedSceneIndex && sceneName.Contains("InitTestScene"))
+ {
+ return false;
+ }
- return m_ServerVerifyScene;
+ Assert.IsTrue(m_ExpectedSceneIndex == sceneIndex);
+ Assert.IsTrue(m_ExpectedSceneName == sceneName);
+ Assert.IsTrue(m_ExpectedLoadMode == loadSceneMode);
+ return m_ServerVerifyScene;
+ }
+ return false;
}
private bool ClientVerifySceneBeforeLoading(int sceneIndex, string sceneName, LoadSceneMode loadSceneMode)
diff --git a/testproject/Assets/Tests/Runtime/ObjectParenting/ParentDynamicUnderInScenePlaced.cs b/testproject/Assets/Tests/Runtime/ObjectParenting/ParentDynamicUnderInScenePlaced.cs
index 7d0447ae84..dd5530d429 100644
--- a/testproject/Assets/Tests/Runtime/ObjectParenting/ParentDynamicUnderInScenePlaced.cs
+++ b/testproject/Assets/Tests/Runtime/ObjectParenting/ParentDynamicUnderInScenePlaced.cs
@@ -40,6 +40,12 @@ protected override void OnServerAndClientsCreated()
base.OnServerAndClientsCreated();
}
+ protected override IEnumerator OnStartedServerAndClients()
+ {
+ m_ServerNetworkManager.SceneManager.SetClientSynchronizationMode(LoadSceneMode.Additive);
+ return base.OnStartedServerAndClients();
+ }
+
protected override void OnNewClientCreated(NetworkManager networkManager)
{
foreach (var networkPrefab in m_ServerNetworkManager.NetworkConfig.Prefabs.Prefabs)
From 99737520019aa5a77ee976505db60c7984c373df Mon Sep 17 00:00:00 2001
From: NoelStephensUnity <73188597+NoelStephensUnity@users.noreply.github.com>
Date: Sun, 12 Feb 2023 17:15:24 -0600
Subject: [PATCH 20/25] style
removing commented out code.
---
.../TestHelpers/Runtime/IntegrationTestSceneHandler.cs | 7 -------
1 file changed, 7 deletions(-)
diff --git a/com.unity.netcode.gameobjects/TestHelpers/Runtime/IntegrationTestSceneHandler.cs b/com.unity.netcode.gameobjects/TestHelpers/Runtime/IntegrationTestSceneHandler.cs
index dc75646526..ff98c40feb 100644
--- a/com.unity.netcode.gameobjects/TestHelpers/Runtime/IntegrationTestSceneHandler.cs
+++ b/com.unity.netcode.gameobjects/TestHelpers/Runtime/IntegrationTestSceneHandler.cs
@@ -427,13 +427,6 @@ public void StartTrackingScene(Scene scene, bool assigned, NetworkManager networ
};
SceneNameToSceneHandles[networkManager][scene.name].Add(scene.handle, sceneEntry);
}
- //else
- //{
- // if (exceptionOnExisting)
- // {
- // throw new Exception($"[{networkManager.LocalClient.PlayerObject.name}][Duplicate Handle] Scene {scene.name} already has scene handle {scene.handle} registered!");
- // }
- //}
}
private bool DoesANetworkManagerHoldThisScene(Scene scene)
From 31d23e52bbf359d96a52f11beacb5fa3d014d1a9 Mon Sep 17 00:00:00 2001
From: NoelStephensUnity <73188597+NoelStephensUnity@users.noreply.github.com>
Date: Mon, 13 Feb 2023 14:56:22 -0600
Subject: [PATCH 21/25] fix
Found issue where NetworkSceneManager wasn't unsubscribing from active scene changed notifications.
---
.../Runtime/SceneManagement/NetworkSceneManager.cs | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs b/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs
index 4ebfce0bff..bc09156271 100644
--- a/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs
+++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs
@@ -518,8 +518,9 @@ internal bool RemoveServerClientSceneHandle(int serverHandle, int clientHandle)
///
public void Dispose()
{
+ // Always assure we no longer listen to scene changes when disposed.
+ SceneManager.activeSceneChanged -= SceneManager_ActiveSceneChanged;
SceneUnloadEventHandler.Shutdown();
-
foreach (var keypair in SceneEventDataStore)
{
if (NetworkLog.CurrentLogLevel == LogLevel.Developer)
From 8d372e6a7de854929bb553aaacd499978a31e8f8 Mon Sep 17 00:00:00 2001
From: NoelStephensUnity <73188597+NoelStephensUnity@users.noreply.github.com>
Date: Fri, 24 Feb 2023 17:38:22 -0600
Subject: [PATCH 22/25] changelog fix
Migrating last PRs changelog into the Unreleased section.
---
com.unity.netcode.gameobjects/CHANGELOG.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/com.unity.netcode.gameobjects/CHANGELOG.md b/com.unity.netcode.gameobjects/CHANGELOG.md
index 7a84d7c7e6..41c8bece9e 100644
--- a/com.unity.netcode.gameobjects/CHANGELOG.md
+++ b/com.unity.netcode.gameobjects/CHANGELOG.md
@@ -20,6 +20,7 @@ Additional documentation and release notes are available at [Multiplayer Documen
### Fixed
+- Fixed registry of public `NetworkVariable`s in derived `NetworkBehaviour`s (#2423)
- Fixed issue when the `NetworkSceneManager.ClientSynchronizationMode` is `LoadSceneMode.Additive` and the server changes the currently active scene prior to a client connecting then upon a client connecting and being synchronized the NetworkSceneManager would clear its internal ScenePlacedObjects list that could already be populated. (#2383)
- Fixed issue where a client would load duplicate scenes of already preloaded scenes during the initial client synchronization and `NetworkSceneManager.ClientSynchronizationMode` was set to `LoadSceneMode.Additive`. (#2383)
@@ -37,7 +38,6 @@ Additional documentation and release notes are available at [Multiplayer Documen
### Fixed
-- Fixed registry of public `NetworkVariable`s in derived `NetworkBehaviour`s (#2423)
- Fixed issue where changes to a layer's weight would not synchronize unless a state transition was occurring.(#2399)
- Fixed issue where `NetworkManager.LocalClientId` was returning the `NetworkTransport.ServerClientId` as opposed to the `NetworkManager.m_LocalClientId`. (#2398)
- Fixed issue where a dynamically spawned `NetworkObject` parented under an in-scene placed `NetworkObject` would have its `InScenePlaced` value changed to `true`. This would result in a soft synchronization error for late joining clients. (#2396)
From c39e0f7e09d2a6f9cdc41a6c56bb92ca61be457a Mon Sep 17 00:00:00 2001
From: NoelStephensUnity <73188597+NoelStephensUnity@users.noreply.github.com>
Date: Tue, 28 Feb 2023 11:02:14 -0600
Subject: [PATCH 23/25] update
Removed the HandleToScene dictionary. It was duplicating ScenesLoaded.
Replaced all places that used HandleToScene with ScenesLoaded.
---
.../SceneManagement/NetworkSceneManager.cs | 24 +++++++------------
1 file changed, 9 insertions(+), 15 deletions(-)
diff --git a/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs b/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs
index bc09156271..17d126a027 100644
--- a/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs
+++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs
@@ -393,7 +393,6 @@ public bool ActiveSceneSynchronizationEnabled
///
internal Dictionary ServerSceneHandleToClientSceneHandle = new Dictionary();
internal Dictionary ClientSceneHandleToServerSceneHandle = new Dictionary();
- internal Dictionary HandleToScene = new Dictionary();
///
/// Add the client to server (and vice versa) scene handle lookup.
@@ -420,13 +419,10 @@ internal bool UpdateServerClientSceneHandle(int serverHandle, int clientHandle,
return false;
}
- if (!HandleToScene.ContainsKey(clientHandle))
+ // It is "Ok" if this already has an entry
+ if (!ScenesLoaded.ContainsKey(clientHandle))
{
- HandleToScene.Add(clientHandle, localScene);
- }
- else
- {
- return false;
+ ScenesLoaded.Add(clientHandle, localScene);
}
return true;
@@ -456,9 +452,9 @@ internal bool RemoveServerClientSceneHandle(int serverHandle, int clientHandle)
return false;
}
- if (HandleToScene.ContainsKey(clientHandle))
+ if (ScenesLoaded.ContainsKey(clientHandle))
{
- HandleToScene.Remove(clientHandle);
+ ScenesLoaded.Remove(clientHandle);
}
else
{
@@ -703,7 +699,6 @@ internal NetworkSceneManager(NetworkManager networkManager)
// Add to the server to client scene handle table
UpdateServerClientSceneHandle(DontDestroyOnLoadScene.handle, DontDestroyOnLoadScene.handle, DontDestroyOnLoadScene);
- ScenesLoaded.Add(DontDestroyOnLoadScene.handle, DontDestroyOnLoadScene);
}
///
@@ -1150,7 +1145,6 @@ private void OnClientUnloadScene(uint sceneEventId)
sceneEventProgress.OnSceneEventCompleted = OnSceneUnloaded;
var sceneUnload = SceneManagerHandler.UnloadSceneAsync(scene, sceneEventProgress);
- ScenesLoaded.Remove(sceneHandle);
SceneManagerHandler.StopTrackingScene(sceneHandle, sceneName, m_NetworkManager);
// Remove our server to scene handle lookup
@@ -1910,9 +1904,9 @@ private void SynchronizeNetworkObjectScene()
// and move it to that scene.
if (networkObject.gameObject.scene.handle != networkObject.SceneOriginHandle && networkObject.transform.parent == null)
{
- if (HandleToScene.ContainsKey(networkObject.SceneOriginHandle))
+ if (ScenesLoaded.ContainsKey(networkObject.SceneOriginHandle))
{
- var scene = HandleToScene[networkObject.SceneOriginHandle];
+ var scene = ScenesLoaded[networkObject.SceneOriginHandle];
SceneManager.MoveGameObjectToScene(networkObject.gameObject, scene);
}
else if (m_NetworkManager.LogLevel <= LogLevel.Normal)
@@ -2380,9 +2374,9 @@ private void MigrateNetworkObjectsIntoScenes()
if (ServerSceneHandleToClientSceneHandle.ContainsKey(sceneEntry.Key))
{
var clientSceneHandle = ServerSceneHandleToClientSceneHandle[sceneEntry.Key];
- if (HandleToScene.ContainsKey(ServerSceneHandleToClientSceneHandle[sceneEntry.Key]))
+ if (ScenesLoaded.ContainsKey(ServerSceneHandleToClientSceneHandle[sceneEntry.Key]))
{
- var scene = HandleToScene[clientSceneHandle];
+ var scene = ScenesLoaded[clientSceneHandle];
foreach (var networkObject in sceneEntry.Value)
{
SceneManager.MoveGameObjectToScene(networkObject.gameObject, scene);
From 3aee658c20244b45fc50827b7d07fdf1e3054567 Mon Sep 17 00:00:00 2001
From: NoelStephensUnity <73188597+NoelStephensUnity@users.noreply.github.com>
Date: Thu, 2 Mar 2023 15:54:00 -0600
Subject: [PATCH 24/25] update and fix
Fixing an issue when ClientSynchronizationMode is LoadSceneMode.Single and the scene to be loaded is the primary scene and is already loaded on the client side. Caught this issue running the manual test: PreserveNetworkObjectsOnShutdown.
Migrating the "shouldPassThrough" logic to the handler as this behavior/logic is slightly different when integration testing due to the fact that there can only be one active scene during an integration test.
---
.../DefaultSceneManagerHandler.cs | 27 +++++++++++++++++
.../SceneManagement/ISceneManagerHandler.cs | 2 ++
.../SceneManagement/NetworkSceneManager.cs | 29 ++++++++++++-------
3 files changed, 48 insertions(+), 10 deletions(-)
diff --git a/com.unity.netcode.gameobjects/Runtime/SceneManagement/DefaultSceneManagerHandler.cs b/com.unity.netcode.gameobjects/Runtime/SceneManagement/DefaultSceneManagerHandler.cs
index fd03064c2d..c63fc11270 100644
--- a/com.unity.netcode.gameobjects/Runtime/SceneManagement/DefaultSceneManagerHandler.cs
+++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/DefaultSceneManagerHandler.cs
@@ -247,6 +247,33 @@ private void SceneManager_SceneUnloaded(Scene scene)
}
}
+ ///
+ /// Handles determining if a client should attempt to load a scene during synchronization.
+ ///
+ /// name of the scene to be loaded
+ /// when in client synchronization mode single, this determines if the scene is the primary active scene
+ /// the current client synchronization mode
+ /// instance
+ ///
+ public bool ClientShouldPassThrough(string sceneName, bool isPrimaryScene, LoadSceneMode clientSynchronizationMode, NetworkManager networkManager)
+ {
+ var shouldPassThrough = clientSynchronizationMode == LoadSceneMode.Single ? false : DoesSceneHaveUnassignedEntry(sceneName, networkManager);
+ var activeScene = SceneManager.GetActiveScene();
+
+ // If shouldPassThrough is not yet true and the scene to be loaded is the currently active scene
+ if (!shouldPassThrough && sceneName == activeScene.name)
+ {
+ // In additive mode we always pass through, but in LoadSceneMode.Single we only pass through if the currently active scene
+ // is the primary scene to be loaded
+ if (clientSynchronizationMode == LoadSceneMode.Additive || (isPrimaryScene && clientSynchronizationMode == LoadSceneMode.Single))
+ {
+ // don't try to reload this scene and pass through to post load processing.
+ shouldPassThrough = true;
+ }
+ }
+ return shouldPassThrough;
+ }
+
///
/// Handles migrating dynamically spawned NetworkObjects to the DDOL when a scene is unloaded
///
diff --git a/com.unity.netcode.gameobjects/Runtime/SceneManagement/ISceneManagerHandler.cs b/com.unity.netcode.gameobjects/Runtime/SceneManagement/ISceneManagerHandler.cs
index e761232440..240a5c94f6 100644
--- a/com.unity.netcode.gameobjects/Runtime/SceneManagement/ISceneManagerHandler.cs
+++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/ISceneManagerHandler.cs
@@ -30,5 +30,7 @@ internal interface ISceneManagerHandler
void MoveObjectsFromSceneToDontDestroyOnLoad(ref NetworkManager networkManager, Scene scene);
void SetClientSynchronizationMode(ref NetworkManager networkManager, LoadSceneMode mode);
+
+ bool ClientShouldPassThrough(string sceneName, bool isPrimaryScene, LoadSceneMode clientSynchronizationMode, NetworkManager networkManager);
}
}
diff --git a/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs b/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs
index 17d126a027..71833eab14 100644
--- a/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs
+++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs
@@ -1690,6 +1690,11 @@ internal void SynchronizeNetworkObjects(ulong clientId)
continue;
}
+ if (scene == DontDestroyOnLoadScene)
+ {
+ continue;
+ }
+
var sceneHash = SceneHashFromNameOrPath(scene.path);
// This would depend upon whether we are additive or not
@@ -1774,17 +1779,14 @@ private void OnClientBeginSync(uint sceneEventId)
return;
}
- var shouldPassThrough = ClientSynchronizationMode == LoadSceneMode.Single ? false : SceneManagerHandler.DoesSceneHaveUnassignedEntry(sceneName, m_NetworkManager);
var sceneLoad = (AsyncOperation)null;
- // Check to see if the client already has loaded the scene to be loaded and the client synchronization
- // mode is set to additive
- if (sceneName == activeScene.name && ClientSynchronizationMode == LoadSceneMode.Additive)
- {
- // If the client is already in the same scene, then pass through and
- // don't try to reload it.
- shouldPassThrough = true;
- }
+ // Determines if the client has the scene to be loaded already loaded, if so will return true and the client will skip loading this scene
+ // For ClientSynchronizationMode LoadSceneMode.Single, we pass in whether the scene being loaded is the first/primary active scene and if it is already loaded
+ // it should pass through to post load processing (ClientLoadedSynchronization).
+ // For ClientSynchronizationMode LoadSceneMode.Additive, if the scene is already loaded or the active scene is the scene to be loaded (does not require it to
+ // be the initial primary scene) then go ahead and pass through to post load processing (ClientLoadedSynchronization).
+ var shouldPassThrough = SceneManagerHandler.ClientShouldPassThrough(sceneName, sceneHash == sceneEventData.SceneHash, ClientSynchronizationMode, m_NetworkManager);
if (!shouldPassThrough)
{
@@ -1900,6 +1902,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)
@@ -1907,6 +1911,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 (m_NetworkManager.LogLevel <= LogLevel.Normal)
@@ -2257,7 +2266,7 @@ internal void PopulateScenePlacedObjects(Scene sceneToFilterBy, bool clearSceneP
foreach (var networkObjectInstance in networkObjects)
{
var globalObjectIdHash = networkObjectInstance.GlobalObjectIdHash;
- var sceneHandle = networkObjectInstance.GetSceneOriginHandle();
+ var sceneHandle = networkObjectInstance.gameObject.scene.handle;
// We check to make sure the NetworkManager instance is the same one to be "NetcodeIntegrationTestHelpers" compatible and filter the list on a per scene basis (for additive scenes)
if (networkObjectInstance.IsSceneObject != false && (networkObjectInstance.NetworkManager == m_NetworkManager ||
networkObjectInstance.NetworkManagerOwner == null) && sceneHandle == sceneToFilterBy.handle)
From 0ee065b1a59216f5a1d31fbb8ef15715a0ed1049 Mon Sep 17 00:00:00 2001
From: NoelStephensUnity <73188597+NoelStephensUnity@users.noreply.github.com>
Date: Thu, 2 Mar 2023 15:56:30 -0600
Subject: [PATCH 25/25] test
Updating the IntegrationTestSceneHandler with the recent changes to ISceneManagerHandler.
Fixing some minor issues preventing some of the manual tests from functioning properly.
---
.../Runtime/IntegrationTestSceneHandler.cs | 29 ++++++++++++
.../Assets/Scripts/ConnectionModeScript.cs | 7 ++-
.../SceneTransitioningTest.unity | 46 +++++++++++--------
.../SecondSceneToLoad.unity | 25 +++++++++-
.../Tests/Manual/Scripts/NetworkPrefabPool.cs | 9 ++--
.../NetworkObjectSceneMigrationTests.cs | 7 ++-
6 files changed, 97 insertions(+), 26 deletions(-)
diff --git a/com.unity.netcode.gameobjects/TestHelpers/Runtime/IntegrationTestSceneHandler.cs b/com.unity.netcode.gameobjects/TestHelpers/Runtime/IntegrationTestSceneHandler.cs
index ff98c40feb..fdba24e61d 100644
--- a/com.unity.netcode.gameobjects/TestHelpers/Runtime/IntegrationTestSceneHandler.cs
+++ b/com.unity.netcode.gameobjects/TestHelpers/Runtime/IntegrationTestSceneHandler.cs
@@ -846,6 +846,35 @@ private void SceneManager_OnSceneEvent(SceneEvent sceneEvent)
}
}
+ ///
+ /// Handles determining if a client should attempt to load a scene during synchronization.
+ ///
+ /// name of the scene to be loaded
+ /// when in client synchronization mode single, this determines if the scene is the primary active scene
+ /// the current client synchronization mode
+ /// relative instance
+ ///
+ public bool ClientShouldPassThrough(string sceneName, bool isPrimaryScene, LoadSceneMode clientSynchronizationMode, NetworkManager networkManager)
+ {
+ var shouldPassThrough = clientSynchronizationMode == LoadSceneMode.Single ? false : DoesSceneHaveUnassignedEntry(sceneName, networkManager);
+ var activeScene = SceneManager.GetActiveScene();
+
+ // If shouldPassThrough is not yet true and the scene to be loaded is the currently active scene
+ if (!shouldPassThrough && sceneName == activeScene.name)
+ {
+ // In additive client synchronization mode we always pass through.
+ // Unlike the default behavior(i.e. DefaultSceneManagerHandler), for integration testing we always return false
+ // if it is the active scene and the client synchronization mode is LoadSceneMode.Single because the client should
+ // load the active scene additively for this NetworkManager instance (i.e. can't have multiple active scenes).
+ if (clientSynchronizationMode == LoadSceneMode.Additive)
+ {
+ // don't try to reload this scene and pass through to post load processing.
+ shouldPassThrough = true;
+ }
+ }
+ return shouldPassThrough;
+ }
+
///
/// Constructor now must take NetworkManager
///
diff --git a/testproject/Assets/Scripts/ConnectionModeScript.cs b/testproject/Assets/Scripts/ConnectionModeScript.cs
index 1f80a95a19..6fc2750a1d 100644
--- a/testproject/Assets/Scripts/ConnectionModeScript.cs
+++ b/testproject/Assets/Scripts/ConnectionModeScript.cs
@@ -154,7 +154,7 @@ private void StartServer()
NetworkManager.Singleton.StartServer();
NetworkManager.Singleton.SceneManager.SetClientSynchronizationMode(m_ClientSynchronizationMode);
OnNotifyConnectionEventServer?.Invoke();
- m_ConnectionModeButtons.SetActive(false);
+ m_ConnectionModeButtons?.SetActive(false);
}
@@ -240,7 +240,10 @@ private void StartClient()
NetworkManager.Singleton.StartClient();
OnNotifyConnectionEventClient?.Invoke();
m_ConnectionModeButtons.SetActive(false);
- m_DisconnectClientButton.SetActive(true);
+ if (m_DisconnectClientButton != null)
+ {
+ m_DisconnectClientButton.SetActive(true);
+ }
}
public void DisconnectClient()
diff --git a/testproject/Assets/Tests/Manual/SceneTransitioning/SceneTransitioningTest.unity b/testproject/Assets/Tests/Manual/SceneTransitioning/SceneTransitioningTest.unity
index b253134e4e..eca5859239 100644
--- a/testproject/Assets/Tests/Manual/SceneTransitioning/SceneTransitioningTest.unity
+++ b/testproject/Assets/Tests/Manual/SceneTransitioning/SceneTransitioningTest.unity
@@ -178,7 +178,7 @@ MonoBehaviour:
m_SceneToSwitchTo: SecondSceneToLoad
m_EnableAutoSwitch: 1
m_AutoSwitchTimeOut: 300
- DisconnectClientUponLoadScene: 1
+ DisconnectClientUponLoadScene: 0
--- !u!114 &34066668
MonoBehaviour:
m_ObjectHideFlags: 0
@@ -193,6 +193,8 @@ MonoBehaviour:
m_EditorClassIdentifier:
GlobalObjectIdHash: 3270232563
AlwaysReplicateAsRoot: 0
+ ActiveSceneSynchronization: 0
+ SceneMigrationSynchronization: 1
DontDestroyWithOwner: 0
AutoObjectParentSync: 1
--- !u!1 &37242881
@@ -984,6 +986,8 @@ MonoBehaviour:
m_EditorClassIdentifier:
GlobalObjectIdHash: 2831848344
AlwaysReplicateAsRoot: 0
+ ActiveSceneSynchronization: 0
+ SceneMigrationSynchronization: 1
DontDestroyWithOwner: 0
AutoObjectParentSync: 1
--- !u!4 &797949416
@@ -1102,7 +1106,24 @@ MonoBehaviour:
NetworkTransport: {fileID: 1024114719}
PlayerPrefab: {fileID: 4079352819444256614, guid: c16f03336b6104576a565ef79ad643c0,
type: 3}
- NetworkPrefabs:
+ Prefabs:
+ NetworkPrefabsLists: []
+ TickRate: 30
+ ClientConnectionBufferTimeout: 10
+ ConnectionApproval: 0
+ ConnectionData:
+ EnableTimeResync: 0
+ TimeResyncInterval: 30
+ EnsureNetworkVariableLengthSafety: 0
+ EnableSceneManagement: 1
+ ForceSamePrefabs: 1
+ RecycleNetworkIds: 1
+ NetworkIdRecycleDelay: 120
+ RpcHashSize: 0
+ LoadSceneTimeOut: 120
+ SpawnTimeout: 1
+ EnableNetworkLogs: 1
+ OldPrefabList:
- Override: 1
Prefab: {fileID: 771575417923360811, guid: c0a45bdb516f341498d933b7a7ed4fc1,
type: 3}
@@ -1129,21 +1150,6 @@ MonoBehaviour:
SourcePrefabToOverride: {fileID: 0}
SourceHashToOverride: 0
OverridingTargetPrefab: {fileID: 0}
- TickRate: 30
- ClientConnectionBufferTimeout: 10
- ConnectionApproval: 0
- ConnectionData:
- EnableTimeResync: 0
- TimeResyncInterval: 30
- EnsureNetworkVariableLengthSafety: 0
- EnableSceneManagement: 1
- ForceSamePrefabs: 1
- RecycleNetworkIds: 1
- NetworkIdRecycleDelay: 120
- RpcHashSize: 0
- LoadSceneTimeOut: 120
- SpawnTimeout: 1
- EnableNetworkLogs: 1
--- !u!114 &1024114719
MonoBehaviour:
m_ObjectHideFlags: 0
@@ -1277,6 +1283,8 @@ MonoBehaviour:
m_EditorClassIdentifier:
GlobalObjectIdHash: 2782806529
AlwaysReplicateAsRoot: 0
+ ActiveSceneSynchronization: 0
+ SceneMigrationSynchronization: 1
DontDestroyWithOwner: 0
AutoObjectParentSync: 1
--- !u!1 &1332123091
@@ -1979,6 +1987,7 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 4f231c4fb786f3946a6b90b886c48677, type: 3}
m_Name:
m_EditorClassIdentifier:
+ m_SendPointerHoverToParent: 1
m_HorizontalAxis: Horizontal
m_VerticalAxis: Vertical
m_SubmitButton: Submit
@@ -2679,7 +2688,6 @@ MonoBehaviour:
m_EditorClassIdentifier:
m_ClientServerToggle: {fileID: 1588117327}
m_TrackSceneEvents: 1
- m_LogSceneEventsToConsole: 1
--- !u!114 &2107482023
MonoBehaviour:
m_ObjectHideFlags: 0
@@ -2694,6 +2702,8 @@ MonoBehaviour:
m_EditorClassIdentifier:
GlobalObjectIdHash: 2267100048
AlwaysReplicateAsRoot: 0
+ ActiveSceneSynchronization: 0
+ SceneMigrationSynchronization: 1
DontDestroyWithOwner: 0
AutoObjectParentSync: 1
--- !u!1001 &2848221156282925290
diff --git a/testproject/Assets/Tests/Manual/SceneTransitioning/SecondSceneToLoad.unity b/testproject/Assets/Tests/Manual/SceneTransitioning/SecondSceneToLoad.unity
index a3f679d703..cc072534b8 100644
--- a/testproject/Assets/Tests/Manual/SceneTransitioning/SecondSceneToLoad.unity
+++ b/testproject/Assets/Tests/Manual/SceneTransitioning/SecondSceneToLoad.unity
@@ -1031,6 +1031,8 @@ MonoBehaviour:
m_EditorClassIdentifier:
GlobalObjectIdHash: 2807568818
AlwaysReplicateAsRoot: 0
+ ActiveSceneSynchronization: 0
+ SceneMigrationSynchronization: 1
DontDestroyWithOwner: 0
AutoObjectParentSync: 1
--- !u!1 &1332123091
@@ -1465,6 +1467,7 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 4f231c4fb786f3946a6b90b886c48677, type: 3}
m_Name:
m_EditorClassIdentifier:
+ m_SendPointerHoverToParent: 1
m_HorizontalAxis: Horizontal
m_VerticalAxis: Vertical
m_SubmitButton: Submit
@@ -1950,6 +1953,7 @@ GameObject:
m_Component:
- component: {fileID: 2051885573}
- component: {fileID: 2051885574}
+ - component: {fileID: 2051885575}
m_Layer: 5
m_Name: SwitchSceneParent
m_TagString: Untagged
@@ -1995,6 +1999,24 @@ MonoBehaviour:
m_EnableAutoSwitch: 1
m_AutoSwitchTimeOut: 300
DisconnectClientUponLoadScene: 0
+--- !u!114 &2051885575
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 2051885572}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: d5a57f767e5e46a458fc5d3c628d0cbb, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
+ GlobalObjectIdHash: 3071296511
+ AlwaysReplicateAsRoot: 0
+ ActiveSceneSynchronization: 0
+ SceneMigrationSynchronization: 1
+ DontDestroyWithOwner: 0
+ AutoObjectParentSync: 1
--- !u!1 &2058276875
GameObject:
m_ObjectHideFlags: 0
@@ -2146,7 +2168,6 @@ MonoBehaviour:
m_EditorClassIdentifier:
m_ClientServerToggle: {fileID: 2126410740}
m_TrackSceneEvents: 1
- m_LogSceneEventsToConsole: 0
--- !u!114 &2107482023
MonoBehaviour:
m_ObjectHideFlags: 0
@@ -2161,6 +2182,8 @@ MonoBehaviour:
m_EditorClassIdentifier:
GlobalObjectIdHash: 4086995543
AlwaysReplicateAsRoot: 0
+ ActiveSceneSynchronization: 0
+ SceneMigrationSynchronization: 1
DontDestroyWithOwner: 0
AutoObjectParentSync: 1
--- !u!1 &2126410740
diff --git a/testproject/Assets/Tests/Manual/Scripts/NetworkPrefabPool.cs b/testproject/Assets/Tests/Manual/Scripts/NetworkPrefabPool.cs
index bacf986921..bfedee4698 100644
--- a/testproject/Assets/Tests/Manual/Scripts/NetworkPrefabPool.cs
+++ b/testproject/Assets/Tests/Manual/Scripts/NetworkPrefabPool.cs
@@ -56,9 +56,12 @@ private void OnEnable()
if (s_Instance != null && s_Instance != this)
{
var instancePool = s_Instance.GetComponent();
- instancePool.MoveBackToCurrentlyActiveScene();
- m_ObjectPool = new List(instancePool.m_ObjectPool);
- instancePool.m_ObjectPool.Clear();
+ if (instancePool.m_ObjectPool != null)
+ {
+ instancePool.MoveBackToCurrentlyActiveScene();
+ instancePool.m_ObjectPool.Clear();
+ m_ObjectPool = new List(instancePool.m_ObjectPool);
+ }
Destroy(s_Instance);
s_Instance = null;
}
diff --git a/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkObjectSceneMigrationTests.cs b/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkObjectSceneMigrationTests.cs
index 5ef2e14cb6..32e548b6c6 100644
--- a/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkObjectSceneMigrationTests.cs
+++ b/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkObjectSceneMigrationTests.cs
@@ -499,9 +499,12 @@ public override void OnNetworkDespawn()
public override void OnDestroy()
{
- if (NetworkManager.LocalClientId == NetworkManager.ServerClientId)
+ if (NetworkManager != null)
{
- NetworkObjectDestroyed?.Invoke(NetworkObject);
+ if (NetworkManager.LocalClientId == NetworkManager.ServerClientId)
+ {
+ NetworkObjectDestroyed?.Invoke(NetworkObject);
+ }
}
base.OnDestroy();
}