Skip to content

Commit e0bd7eb

Browse files
fix: NetworkSceneManager clearing scene placed NetworkObjects list when ClientSynchronizationMode is additive (#2383)
* 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. A more robust fix for running the server-side NetworkSceneManager in client synchronization mode additive. Fixing minor issue where AddSceneObject was always passing false for the destroy with scene parameter. * 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. 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. 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. This includes the additional changes required to assure that clients are synchronized when a dynamically spawned NetworkObject is moved into a new scene.' Updating scene manager handler to include moving objects from a scene to the DDOL (for unloading a scene only). 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. * test Updating existing integration tests and the additive scene loading test to be able to manually 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. Adding tests for NetworkObject scene migration. 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. Updating the IntegrationTestSceneHandler with the recent changes to ISceneManagerHandler. Fixing some minor issues preventing some of the manual tests from functioning properly.
1 parent 8b8825b commit e0bd7eb

32 files changed

+3308
-147
lines changed

com.unity.netcode.gameobjects/CHANGELOG.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,27 @@ All notable changes to this project will be documented in this file.
55
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).
66

77
Additional documentation and release notes are available at [Multiplayer Documentation](https://docs-multiplayer.unity3d.com).
8-
98
## [Unreleased]
109

1110
### Added
1211

12+
- Added `NetworkSceneManager.ActiveSceneSynchronizationEnabled` property, disabled by default, that enables client synchronization of server-side active scene changes. (#2383)
13+
- Added `NetworkObject.ActiveSceneSynchronization`, disabled by default, that will automatically migrate a `NetworkObject` to a newly assigned active scene. (#2383)
14+
- 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)
15+
16+
### Changed
17+
18+
- Updated `NetworkSceneManager` to migrate dynamically spawned `NetworkObject`s with `DestroyWithScene` set to false into the active scene if their current scene is unloaded. (#2383)
19+
- Updated the server to synchronize its local `NetworkSceneManager.ClientSynchronizationMode` during the initial client synchronization. (#2383)
20+
21+
### Fixed
22+
23+
- Fixed registry of public `NetworkVariable`s in derived `NetworkBehaviour`s (#2423)
24+
- 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)
25+
- 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)
26+
27+
## [1.3.0]
28+
1329
### Changed
1430

1531
### Fixed

com.unity.netcode.gameobjects/Runtime/Configuration/NetworkPrefabs.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,23 @@ private void RemoveTriggeredByNetworkPrefabList(NetworkPrefab networkPrefab)
5252
}
5353

5454
~NetworkPrefabs()
55+
{
56+
Shutdown();
57+
}
58+
59+
/// <summary>
60+
/// Deregister from add and remove events
61+
/// Clear the list
62+
/// </summary>
63+
internal void Shutdown()
5564
{
5665
foreach (var list in NetworkPrefabsLists)
5766
{
5867
list.OnAdd -= AddTriggeredByNetworkPrefabList;
5968
list.OnRemove -= RemoveTriggeredByNetworkPrefabList;
6069
}
70+
71+
NetworkPrefabsLists.Clear();
6172
}
6273

6374
/// <summary>

com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1278,6 +1278,8 @@ internal void ShutdownInternal()
12781278
m_StopProcessingMessages = false;
12791279

12801280
ClearClients();
1281+
// This cleans up the internal prefabs list
1282+
NetworkConfig?.Prefabs.Shutdown();
12811283
}
12821284

12831285
/// <inheritdoc />
@@ -1395,6 +1397,10 @@ private void OnNetworkPostLateUpdate()
13951397

13961398
if (!m_ShuttingDown || !m_StopProcessingMessages)
13971399
{
1400+
// This should be invoked just prior to the MessagingSystem
1401+
// processes its outbound queue.
1402+
SceneManager.CheckForAndSendNetworkObjectSceneChanged();
1403+
13981404
MessagingSystem.ProcessSendQueues();
13991405
NetworkMetrics.UpdateNetworkObjectsCount(SpawnManager.SpawnedObjects.Count);
14001406
NetworkMetrics.UpdateConnectionsCount((IsServer) ? ConnectedClients.Count : 1);
@@ -1974,6 +1980,9 @@ internal void HandleConnectionApproval(ulong ownerClientId, ConnectionApprovalRe
19741980
var playerPrefabHash = response.PlayerPrefabHash ?? NetworkConfig.PlayerPrefab.GetComponent<NetworkObject>().GlobalObjectIdHash;
19751981

19761982
// Generate a SceneObject for the player object to spawn
1983+
// Note: This is only to create the local NetworkObject,
1984+
// many of the serialized properties of the player prefab
1985+
// will be set when instantiated.
19771986
var sceneObject = new NetworkObject.SceneObject
19781987
{
19791988
OwnerClientId = ownerClientId,

com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs

Lines changed: 191 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,55 @@ internal void GenerateGlobalObjectIdHash()
105105
/// </summary>
106106
public bool DestroyWithScene { get; set; }
107107

108+
/// <summary>
109+
/// When set to true and the active scene is changed, this will automatically migrate the <see cref="NetworkObject"/>
110+
/// into the new active scene on both the server and client instances.
111+
/// </summary>
112+
/// <remarks>
113+
/// - This only applies to dynamically spawned <see cref="NetworkObject"/>s.
114+
/// - This only works when using integrated scene management (<see cref="NetworkSceneManager"/>).
115+
///
116+
/// If there are more than one scenes loaded and the currently active scene is unloaded, then typically
117+
/// the <see cref="SceneManager"/> will automatically assign a new active scene. Similar to <see cref="DestroyWithScene"/>
118+
/// being set to <see cref="false"/>, this prevents any <see cref="NetworkObject"/> from being destroyed
119+
/// with the unloaded active scene by migrating it into the automatically assigned active scene.
120+
/// Additionally, this is can be useful in some seamless scene streaming implementations.
121+
/// Note:
122+
/// Only having <see cref="ActiveSceneSynchronization"/> set to true will *not* synchronize clients when
123+
/// changing a <see cref="NetworkObject"/>'s scene via <see cref="SceneManager.MoveGameObjectToScene(GameObject, Scene)"/>.
124+
/// To synchronize clients of a <see cref="NetworkObject"/>'s scene being changed via <see cref="SceneManager.MoveGameObjectToScene(GameObject, Scene)"/>,
125+
/// make sure <see cref="SceneMigrationSynchronization"/> is enabled (it is by default).
126+
/// </remarks>
127+
public bool ActiveSceneSynchronization;
128+
129+
/// <summary>
130+
/// When enabled (the default), if a <see cref="NetworkObject"/> is migrated to a different scene (active or not)
131+
/// via <see cref="SceneManager.MoveGameObjectToScene(GameObject, Scene)"/> on the server side all client
132+
/// instances will be synchronized and the <see cref="NetworkObject"/> migrated into the newly assigned scene.
133+
/// The updated scene migration will get synchronized with late joining clients as well.
134+
/// </summary>
135+
/// <remarks>
136+
/// - This only applies to dynamically spawned <see cref="NetworkObject"/>s.
137+
/// - This only works when using integrated scene management (<see cref="NetworkSceneManager"/>).
138+
/// Note:
139+
/// You can have both <see cref="ActiveSceneSynchronization"/> and <see cref="SceneMigrationSynchronization"/> enabled.
140+
/// The primary difference between the two is that <see cref="SceneMigrationSynchronization"/> only synchronizes clients
141+
/// when the server migrates a <see cref="NetworkObject"/> to a new scene. If the scene is unloaded and <see cref="DestroyWithScene"/>
142+
/// is <see cref="true"/> and <see cref="ActiveSceneSynchronization"/> is <see cref="false"/> and the scene is not the currently
143+
/// active scene, then the <see cref="NetworkObject"/> will be destroyed.
144+
/// </remarks>
145+
public bool SceneMigrationSynchronization = true;
146+
147+
/// <summary>
148+
/// Notifies when the NetworkObject is migrated into a new scene
149+
/// </summary>
150+
/// <remarks>
151+
/// - <see cref="ActiveSceneSynchronization"/> or <see cref="SceneMigrationSynchronization"/> (or both) need to be enabled
152+
/// - This only applies to dynamically spawned <see cref="NetworkObject"/>s.
153+
/// - This only works when using integrated scene management (<see cref="NetworkSceneManager"/>).
154+
/// </remarks>
155+
public Action OnMigratedToNewScene;
156+
108157
/// <summary>
109158
/// Delegate type for checking visibility
110159
/// </summary>
@@ -188,6 +237,11 @@ public bool IsNetworkVisibleTo(ulong clientId)
188237
/// </summary>
189238
internal int SceneOriginHandle = 0;
190239

240+
/// <summary>
241+
/// The server-side scene origin handle
242+
/// </summary>
243+
internal int NetworkSceneHandle = 0;
244+
191245
private Scene m_SceneOrigin;
192246
/// <summary>
193247
/// The scene where the NetworkObject was first instantiated
@@ -1118,6 +1172,18 @@ public bool WorldPositionStays
11181172
set => ByteUtility.SetBit(ref m_BitField, 5, value);
11191173
}
11201174

1175+
/// <summary>
1176+
/// Even though the server sends notifications for NetworkObjects that get
1177+
/// destroyed when a scene is unloaded, we want to synchronize this so
1178+
/// the client side can use it as part of a filter for automatically migrating
1179+
/// to the current active scene when its scene is unloaded. (only for dynamically spawned)
1180+
/// </summary>
1181+
public bool DestroyWithScene
1182+
{
1183+
get => ByteUtility.GetBit(m_BitField, 6);
1184+
set => ByteUtility.SetBit(ref m_BitField, 6, value);
1185+
}
1186+
11211187
//If(Metadata.HasParent)
11221188
public ulong ParentObjectId;
11231189

@@ -1160,7 +1226,7 @@ public void Serialize(FastBufferWriter writer)
11601226

11611227
var writeSize = 0;
11621228
writeSize += HasTransform ? FastBufferWriter.GetWriteSize<TransformData>() : 0;
1163-
writeSize += IsSceneObject ? FastBufferWriter.GetWriteSize<int>() : 0;
1229+
writeSize += FastBufferWriter.GetWriteSize<int>();
11641230

11651231
if (!writer.TryBeginWrite(writeSize))
11661232
{
@@ -1172,14 +1238,9 @@ public void Serialize(FastBufferWriter writer)
11721238
writer.WriteValue(Transform);
11731239
}
11741240

1175-
// In-Scene NetworkObjects are uniquely identified NetworkPrefabs defined by their
1176-
// NetworkSceneHandle and GlobalObjectIdHash. Client-side NetworkSceneManagers use
1177-
// this to locate their local instance of the in-scene placed NetworkObject instance.
1178-
// Only written for in-scene placed NetworkObjects.
1179-
if (IsSceneObject)
1180-
{
1181-
writer.WriteValue(OwnerObject.GetSceneOriginHandle());
1182-
}
1241+
// The NetworkSceneHandle is the server-side relative
1242+
// scene handle that the NetworkObject resides in.
1243+
writer.WriteValue(OwnerObject.GetSceneOriginHandle());
11831244

11841245
// Synchronize NetworkVariables and NetworkBehaviours
11851246
var bufferSerializer = new BufferSerializer<BufferSerializerWriter>(new BufferSerializerWriter(writer));
@@ -1205,7 +1266,7 @@ public void Deserialize(FastBufferReader reader)
12051266

12061267
var readSize = 0;
12071268
readSize += HasTransform ? FastBufferWriter.GetWriteSize<TransformData>() : 0;
1208-
readSize += IsSceneObject ? FastBufferWriter.GetWriteSize<int>() : 0;
1269+
readSize += FastBufferWriter.GetWriteSize<int>();
12091270

12101271
// Try to begin reading the remaining bytes
12111272
if (!reader.TryBeginRead(readSize))
@@ -1218,14 +1279,9 @@ public void Deserialize(FastBufferReader reader)
12181279
reader.ReadValue(out Transform);
12191280
}
12201281

1221-
// In-Scene NetworkObjects are uniquely identified NetworkPrefabs defined by their
1222-
// NetworkSceneHandle and GlobalObjectIdHash. Client-side NetworkSceneManagers use
1223-
// this to locate their local instance of the in-scene placed NetworkObject instance.
1224-
// Only read for in-scene placed NetworkObjects
1225-
if (IsSceneObject)
1226-
{
1227-
reader.ReadValue(out NetworkSceneHandle);
1228-
}
1282+
// The NetworkSceneHandle is the server-side relative
1283+
// scene handle that the NetworkObject resides in.
1284+
reader.ReadValue(out NetworkSceneHandle);
12291285
}
12301286
}
12311287

@@ -1317,6 +1373,7 @@ internal SceneObject GetMessageSceneObject(ulong targetClientId)
13171373
OwnerClientId = OwnerClientId,
13181374
IsPlayerObject = IsPlayerObject,
13191375
IsSceneObject = IsSceneObject ?? true,
1376+
DestroyWithScene = DestroyWithScene,
13201377
Hash = HostCheckForGlobalObjectIdHashOverride(),
13211378
OwnerObject = this,
13221379
TargetClientId = targetClientId
@@ -1435,11 +1492,126 @@ internal static NetworkObject AddSceneObject(in SceneObject sceneObject, FastBuf
14351492
networkObject.SynchronizeNetworkBehaviours(ref bufferSerializer, networkManager.LocalClientId);
14361493

14371494
// Spawn the NetworkObject
1438-
networkManager.SpawnManager.SpawnNetworkObjectLocally(networkObject, sceneObject, false);
1495+
networkManager.SpawnManager.SpawnNetworkObjectLocally(networkObject, sceneObject, sceneObject.DestroyWithScene);
14391496

14401497
return networkObject;
14411498
}
14421499

1500+
/// <summary>
1501+
/// Subscribes to changes in the currently active scene
1502+
/// </summary>
1503+
/// <remarks>
1504+
/// Only for dynamically spawned NetworkObjects
1505+
/// </remarks>
1506+
internal void SubscribeToActiveSceneForSynch()
1507+
{
1508+
if (ActiveSceneSynchronization)
1509+
{
1510+
if (IsSceneObject.HasValue && !IsSceneObject.Value)
1511+
{
1512+
// Just in case it is a recycled NetworkObject, unsubscribe first
1513+
SceneManager.activeSceneChanged -= CurrentlyActiveSceneChanged;
1514+
SceneManager.activeSceneChanged += CurrentlyActiveSceneChanged;
1515+
}
1516+
}
1517+
}
1518+
1519+
/// <summary>
1520+
/// If AutoSynchActiveScene is enabled, then this is the callback that handles updating
1521+
/// a NetworkObject's scene information.
1522+
/// </summary>
1523+
private void CurrentlyActiveSceneChanged(Scene current, Scene next)
1524+
{
1525+
// Early exit if there is no NetworkManager assigned, the NetworkManager is shutting down, the NetworkObject
1526+
// is not spawned, or an in-scene placed NetworkObject
1527+
if (NetworkManager == null || NetworkManager.ShutdownInProgress || !IsSpawned || IsSceneObject != false)
1528+
{
1529+
return;
1530+
}
1531+
// This check is here in the event a user wants to disable this for some reason but also wants
1532+
// the NetworkObject to synchronize to changes in the currently active scene at some later time.
1533+
if (ActiveSceneSynchronization)
1534+
{
1535+
// Only dynamically spawned NetworkObjects that are not already in the newly assigned active scene will migrate
1536+
// and update their scene handles
1537+
if (IsSceneObject.HasValue && !IsSceneObject.Value && gameObject.scene != next && gameObject.transform.parent == null)
1538+
{
1539+
SceneManager.MoveGameObjectToScene(gameObject, next);
1540+
SceneChangedUpdate(next);
1541+
}
1542+
}
1543+
}
1544+
1545+
/// <summary>
1546+
/// Handles updating the NetworkObject's tracked scene handles
1547+
/// </summary>
1548+
internal void SceneChangedUpdate(Scene scene, bool notify = false)
1549+
{
1550+
// Avoiding edge case scenarios, if no NetworkSceneManager exit early
1551+
if (NetworkManager.SceneManager == null)
1552+
{
1553+
return;
1554+
}
1555+
1556+
SceneOriginHandle = scene.handle;
1557+
// Clients need to update the NetworkSceneHandle
1558+
if (!NetworkManager.IsServer && NetworkManager.SceneManager.ClientSceneHandleToServerSceneHandle.ContainsKey(SceneOriginHandle))
1559+
{
1560+
NetworkSceneHandle = NetworkManager.SceneManager.ClientSceneHandleToServerSceneHandle[SceneOriginHandle];
1561+
}
1562+
else if (NetworkManager.IsServer)
1563+
{
1564+
// Since the server is the source of truth for the NetworkSceneHandle,
1565+
// the NetworkSceneHandle is the same as the SceneOriginHandle.
1566+
NetworkSceneHandle = SceneOriginHandle;
1567+
}
1568+
else // Otherwise, the client did not find the client to server scene handle
1569+
if (NetworkManager.LogLevel == LogLevel.Developer)
1570+
{
1571+
// There could be a scenario where a user has some client-local scene loaded that they migrate the NetworkObject
1572+
// into, but that scenario seemed very edge case and under most instances a user should be notified that this
1573+
// server - client scene handle mismatch has occurred. It also seemed pertinent to make the message replicate to
1574+
// the server-side too.
1575+
NetworkLog.LogWarningServer($"[Client-{NetworkManager.LocalClientId}][{gameObject.name}] Server - " +
1576+
$"client scene mismatch detected! Client-side scene handle ({SceneOriginHandle}) for scene ({gameObject.scene.name})" +
1577+
$"has no associated server side (network) scene handle!");
1578+
}
1579+
OnMigratedToNewScene?.Invoke();
1580+
1581+
// Only the server side will notify clients of non-parented NetworkObject scene changes
1582+
if (NetworkManager.IsServer && notify && transform.parent == null)
1583+
{
1584+
NetworkManager.SceneManager.NotifyNetworkObjectSceneChanged(this);
1585+
}
1586+
}
1587+
1588+
/// <summary>
1589+
/// Update
1590+
/// Detects if a NetworkObject's scene has changed for both server and client instances
1591+
/// </summary>
1592+
/// <remarks>
1593+
/// About In-Scene Placed NetworkObjects:
1594+
/// Since the same scene can be loaded more than once and in-scene placed NetworkObjects GlobalObjectIdHash
1595+
/// values are only unique to the scene asset itself (and not per scene instance loaded), we will not be able
1596+
/// to add this same functionality to in-scene placed NetworkObjects until we have a way to generate
1597+
/// per-NetworkObject-instance unique GlobalObjectIdHash values for in-scene placed NetworkObjects.
1598+
/// </remarks>
1599+
private void Update()
1600+
{
1601+
// Early exit if SceneMigrationSynchronization is disabled, there is no NetworkManager assigned,
1602+
// the NetworkManager is shutting down, the NetworkObject is not spawned, it is an in-scene placed
1603+
// NetworkObject, or the GameObject's current scene handle is the same as the SceneOriginHandle
1604+
if (!SceneMigrationSynchronization || NetworkManager == null || NetworkManager.ShutdownInProgress || !IsSpawned
1605+
|| IsSceneObject != false || gameObject.scene.handle == SceneOriginHandle)
1606+
{
1607+
return;
1608+
}
1609+
1610+
// Otherwise, this has to be a dynamically spawned NetworkObject that has been
1611+
// migrated to a new scene.
1612+
SceneChangedUpdate(gameObject.scene, true);
1613+
}
1614+
14431615
/// <summary>
14441616
/// Only applies to Host mode.
14451617
/// Will return the registered source NetworkPrefab's GlobalObjectIdHash if one exists.

0 commit comments

Comments
 (0)