Skip to content

Commit 0f959eb

Browse files
fix: CreateObjectMessage created NetworkObjects can get destroyed during client synchronization [MTT-7539] (#2735)
* fix This resolves the issue where any NetworkObjects created during client synchronization when scene management is enabled. client synchronization mode is set to LoadSceneMode.Single, and the active scene (where the NetworkObjects are created) is unloaded during synchronization. * fix This fixes the exception that can occur when there are many (100's) NetworkObjects spawned and the application instance is exited (or you exit play mode in the editor). * style removing auto-added namespace * test Updating WhenManyObjectsAreSpawnedAtOnce_AllAreReceived test as it was not using the right logic to validate all NetworkObjects had been spawned. Adding WhenManyObjectsAreSpawnedDuringSynchronization_AllAreReceived to validate this PR. * update adding changelog entry * test Migrating WhenManyObjectsAreSpawnedDuringSynchronization_AllAreReceived to SpawnNetworkObjectsDuringSynchronizationTest.SpawnNetworkObjectsDuringSynchronization. * fix Scenes loaded in single mode, after a client is connected, will defer CreateObjectMessage driven spawning until the scene loading event is completed. Consolidated the logic into a single method. Added two new SceneEventProgress enums to be returned when a client attempts to load or unload a scene using NetworkSceneManager or any instance type tries to use the NetworkSceneManager when scene management is disabled (as opposed to throwing an exception it just logs a warning and returns the relative SceneEventProgress value). * test Made modifications due to SceneEventProgress adjustments. Renamed SpawnNetworkObjectsDuringSynchronizationTest to SpawnNetworkObjectsDuringSceneEventsTest and migrated it to the test project tests runtime NetworkSceneManager folder. Added an additional * update Updated the change log entry. * style removing unused namespace. * Update CHANGELOG.md * Update CHANGELOG.md Fixing bad changelog merge * Update SceneEventProgress.cs Fixing the issue with the enum changes breaking the InternalNetcodeError value * Update SceneEventProgress.cs * update Made adjustments based on PR review suggestions. Added additional changelog entries to better reflect the PR.
1 parent dc8bbc4 commit 0f959eb

File tree

9 files changed

+336
-67
lines changed

9 files changed

+336
-67
lines changed

com.unity.netcode.gameobjects/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,13 @@ Additional documentation and release notes are available at [Multiplayer Documen
1010

1111
### Added
1212

13+
- Added `SceneEventProgress.SceneManagementNotEnabled` return status to be returned when a `NetworkSceneManager` method is invoked and scene management is not enabled. (#2735)
14+
- Added `SceneEventProgress.ServerOnlyAction` return status to be returned when a `NetworkSceneManager` method is invoked by a client. (#2735)
15+
1316
### Fixed
1417

1518
- Fixed a bug where having a class with Rpcs that inherits from a class without Rpcs that inherits from NetworkVariable would cause a compile error. (#2751)
19+
- Fixed issue where during client synchronization and scene loading, when client synchronization or the scene loading mode are set to `LoadSceneMode.Single`, a `CreateObjectMessage` could be received, processed, and the resultant spawned `NetworkObject` could be instantiated in the client's currently active scene that could, towards the end of the client synchronization or loading process, be unloaded and cause the newly created `NetworkObject` to be destroyed (and throw and exception). (#2735)
1620
- Fixed issue where `NetworkBehaviour.Synchronize` was not truncating the write buffer if nothing was serialized during `NetworkBehaviour.OnSynchronize` causing an additional 6 bytes to be written per `NetworkBehaviour` component instance. (#2749)
1721
- Fixed issue where a parented in-scene placed NetworkObject would be destroyed upon a client or server exiting a network session but not unloading the original scene in which the NetworkObject was placed. (#2737)
1822
- Fixed issue where during client synchronization and scene loading, when client synchronization or the scene loading mode are set to `LoadSceneMode.Single`, a `CreateObjectMessage` could be received, processed, and the resultant spawned `NetworkObject` could be instantiated in the client's currently active scene that could, towards the end of the client synchronization or loading process, be unloaded and cause the newly created `NetworkObject` to be destroyed (and throw and exception). (#2735)
@@ -21,6 +25,8 @@ Additional documentation and release notes are available at [Multiplayer Documen
2125
### Changed
2226
- Changed `NetworkTransform` authoritative instance tick registration so a single `NetworkTransform` specific tick event update will update all authoritative instances to improve perofmance. (#2713)
2327

28+
- Changed `NetworkSceneManager` to return a `SceneEventProgress` status and not throw exceptions for methods invoked when scene management is disabled and when a client attempts to access a `NetworkSceneManager` method by a client. (#2735)
29+
2430
## [1.7.0] - 2023-10-11
2531

2632
### Added

com.unity.netcode.gameobjects/Runtime/Messaging/Messages/CreateObjectMessage.cs

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Runtime.CompilerServices;
12
namespace Unity.Netcode
23
{
34
internal struct CreateObjectMessage : INetworkMessage
@@ -34,9 +35,29 @@ public bool Deserialize(FastBufferReader reader, ref NetworkContext context, int
3435
public void Handle(ref NetworkContext context)
3536
{
3637
var networkManager = (NetworkManager)context.SystemOwner;
37-
var networkObject = NetworkObject.AddSceneObject(ObjectInfo, m_ReceivedNetworkVariableData, networkManager);
38+
// If a client receives a create object message and it is still synchronizing, then defer the object creation until it has finished synchronizing
39+
if (networkManager.SceneManager.ShouldDeferCreateObject())
40+
{
41+
networkManager.SceneManager.DeferCreateObject(context.SenderId, context.MessageSize, ObjectInfo, m_ReceivedNetworkVariableData);
42+
}
43+
else
44+
{
45+
CreateObject(ref networkManager, context.SenderId, context.MessageSize, ObjectInfo, m_ReceivedNetworkVariableData);
46+
}
47+
}
3848

39-
networkManager.NetworkMetrics.TrackObjectSpawnReceived(context.SenderId, networkObject, context.MessageSize);
49+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
50+
internal static void CreateObject(ref NetworkManager networkManager, ulong senderId, uint messageSize, NetworkObject.SceneObject sceneObject, FastBufferReader networkVariableData)
51+
{
52+
try
53+
{
54+
var networkObject = NetworkObject.AddSceneObject(sceneObject, networkVariableData, networkManager);
55+
networkManager.NetworkMetrics.TrackObjectSpawnReceived(senderId, networkObject, messageSize);
56+
}
57+
catch (System.Exception ex)
58+
{
59+
UnityEngine.Debug.LogException(ex);
60+
}
4061
}
4162
}
4263
}

com.unity.netcode.gameobjects/Runtime/Messaging/Messages/NamedMessage.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@ public bool Deserialize(FastBufferReader reader, ref NetworkContext context, int
2525
public void Handle(ref NetworkContext context)
2626
{
2727
var networkManager = (NetworkManager)context.SystemOwner;
28-
if (!networkManager.ShutdownInProgress)
28+
if (!networkManager.ShutdownInProgress && networkManager.CustomMessagingManager != null)
2929
{
30-
((NetworkManager)context.SystemOwner).CustomMessagingManager.InvokeNamedMessage(Hash, context.SenderId, m_ReceiveData, context.SerializedHeaderSize);
30+
networkManager.CustomMessagingManager.InvokeNamedMessage(Hash, context.SenderId, m_ReceiveData, context.SerializedHeaderSize);
3131
}
3232
}
3333
}

com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs

Lines changed: 107 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Linq;
4+
using Unity.Collections;
45
using UnityEngine;
56
using UnityEngine.SceneManagement;
67

@@ -597,6 +598,47 @@ internal void EndSceneEvent(uint sceneEventId)
597598
}
598599
}
599600

601+
/// <summary>
602+
/// Used for integration tests, normal runtime mode this will always be LoadSceneMode.Single
603+
/// </summary>
604+
internal LoadSceneMode DeferLoadingFilter = LoadSceneMode.Single;
605+
/// <summary>
606+
/// Determines if a remote client should defer object creation initiated by CreateObjectMessage
607+
/// until a scene event is completed.
608+
/// </summary>
609+
/// <remarks>
610+
/// Deferring object creation should only occur when there is a possibility the objects could be
611+
/// instantiated in a currently active scene that will be unloaded during single mode scene loading
612+
/// to prevent the newly created objects from being destroyed when the scene is unloaded.
613+
/// </remarks>
614+
internal bool ShouldDeferCreateObject()
615+
{
616+
// This applies only to remote clients and when scene management is enabled
617+
if (!NetworkManager.NetworkConfig.EnableSceneManagement || NetworkManager.IsServer)
618+
{
619+
return false;
620+
}
621+
var synchronizeEventDetected = false;
622+
var loadingEventDetected = false;
623+
foreach (var entry in SceneEventDataStore)
624+
{
625+
if (entry.Value.SceneEventType == SceneEventType.Synchronize)
626+
{
627+
synchronizeEventDetected = true;
628+
}
629+
630+
// When loading a scene and the load scene mode is single we should defer object creation
631+
if (entry.Value.SceneEventType == SceneEventType.Load && entry.Value.LoadSceneMode == DeferLoadingFilter)
632+
{
633+
loadingEventDetected = true;
634+
}
635+
}
636+
637+
// Synchronizing while in client synchronization mode single --> Defer
638+
// When not synchronizing but loading a scene in single mode --> Defer
639+
return (synchronizeEventDetected && ClientSynchronizationMode == LoadSceneMode.Single) || (!synchronizeEventDetected && loadingEventDetected);
640+
}
641+
600642
/// <summary>
601643
/// Gets the scene name from full path to the scene
602644
/// </summary>
@@ -969,17 +1011,16 @@ private void SendSceneEventData(uint sceneEventId, ulong[] targetClientIds)
9691011
/// <returns></returns>
9701012
private SceneEventProgress ValidateSceneEventUnloading(Scene scene)
9711013
{
972-
if (!NetworkManager.IsServer)
1014+
if (!NetworkManager.NetworkConfig.EnableSceneManagement)
9731015
{
974-
throw new NotServerException("Only server can start a scene event!");
1016+
Debug.LogWarning($"{nameof(LoadScene)} was called, but {nameof(NetworkConfig.EnableSceneManagement)} was not enabled! Enable {nameof(NetworkConfig.EnableSceneManagement)} prior to starting a client, host, or server prior to using {nameof(NetworkSceneManager)}!");
1017+
return new SceneEventProgress(null, SceneEventProgressStatus.SceneManagementNotEnabled);
9751018
}
9761019

977-
if (!NetworkManager.NetworkConfig.EnableSceneManagement)
1020+
if (!NetworkManager.IsServer)
9781021
{
979-
//Log message about enabling SceneManagement
980-
throw new Exception(
981-
$"{nameof(NetworkConfig.EnableSceneManagement)} flag is not enabled in the {nameof(Netcode.NetworkManager)}'s {nameof(NetworkConfig)}. " +
982-
$"Please set {nameof(NetworkConfig.EnableSceneManagement)} flag to true before calling {nameof(LoadScene)} or {nameof(UnloadScene)}.");
1022+
Debug.LogWarning($"[{nameof(SceneEventProgressStatus.ServerOnlyAction)}][Unload] Clients cannot invoke the {nameof(UnloadScene)} method!");
1023+
return new SceneEventProgress(null, SceneEventProgressStatus.ServerOnlyAction);
9831024
}
9841025

9851026
if (!scene.isLoaded)
@@ -998,16 +1039,16 @@ private SceneEventProgress ValidateSceneEventUnloading(Scene scene)
9981039
/// <returns></returns>
9991040
private SceneEventProgress ValidateSceneEventLoading(string sceneName)
10001041
{
1001-
if (!NetworkManager.IsServer)
1042+
if (!NetworkManager.NetworkConfig.EnableSceneManagement)
10021043
{
1003-
throw new NotServerException("Only server can start a scene event!");
1044+
Debug.LogWarning($"{nameof(LoadScene)} was called, but {nameof(NetworkConfig.EnableSceneManagement)} was not enabled! Enable {nameof(NetworkConfig.EnableSceneManagement)} prior to starting a client, host, or server prior to using {nameof(NetworkSceneManager)}!");
1045+
return new SceneEventProgress(null, SceneEventProgressStatus.SceneManagementNotEnabled);
10041046
}
1005-
if (!NetworkManager.NetworkConfig.EnableSceneManagement)
1047+
1048+
if (!NetworkManager.IsServer)
10061049
{
1007-
//Log message about enabling SceneManagement
1008-
throw new Exception(
1009-
$"{nameof(NetworkConfig.EnableSceneManagement)} flag is not enabled in the {nameof(Netcode.NetworkManager)}'s {nameof(NetworkConfig)}. " +
1010-
$"Please set {nameof(NetworkConfig.EnableSceneManagement)} flag to true before calling {nameof(LoadScene)} or {nameof(UnloadScene)}.");
1050+
Debug.LogWarning($"[{nameof(SceneEventProgressStatus.ServerOnlyAction)}][Load] Clients cannot invoke the {nameof(LoadScene)} method!");
1051+
return new SceneEventProgress(null, SceneEventProgressStatus.ServerOnlyAction);
10111052
}
10121053

10131054
return ValidateSceneEvent(sceneName);
@@ -1112,6 +1153,7 @@ public SceneEventProgressStatus UnloadScene(Scene scene)
11121153
{
11131154
var sceneName = scene.name;
11141155
var sceneHandle = scene.handle;
1156+
11151157
if (!scene.isLoaded)
11161158
{
11171159
Debug.LogWarning($"{nameof(UnloadScene)} was called, but the scene {scene.name} is not currently loaded!");
@@ -1697,6 +1739,9 @@ private void OnClientLoadedScene(uint sceneEventId, Scene scene)
16971739
SendSceneEventData(sceneEventId, new ulong[] { NetworkManager.ServerClientId });
16981740
m_IsSceneEventActive = false;
16991741

1742+
// Process any pending create object messages that the client received while loading a scene
1743+
ProcessDeferredCreateObjectMessages();
1744+
17001745
// Notify local client that the scene was loaded
17011746
OnSceneEvent?.Invoke(new SceneEvent()
17021747
{
@@ -2058,6 +2103,9 @@ private void HandleClientSceneEvent(uint sceneEventId)
20582103
// If needed, migrate dynamically spawned NetworkObjects to the same scene as they are on the server
20592104
SynchronizeNetworkObjectScene();
20602105

2106+
// Process any pending create object messages that the client received during synchronization
2107+
ProcessDeferredCreateObjectMessages();
2108+
20612109
sceneEventData.SceneEventType = SceneEventType.SynchronizeComplete;
20622110
SendSceneEventData(sceneEventId, new ulong[] { NetworkManager.ServerClientId });
20632111

@@ -2549,5 +2597,50 @@ internal struct DeferredObjectsMovedEvent
25492597
internal Dictionary<int, List<ulong>> ObjectsMigratedTable;
25502598
}
25512599
internal List<DeferredObjectsMovedEvent> DeferredObjectsMovedEvents = new List<DeferredObjectsMovedEvent>();
2600+
2601+
internal struct DeferredObjectCreation
2602+
{
2603+
internal ulong SenderId;
2604+
internal uint MessageSize;
2605+
internal NetworkObject.SceneObject SceneObject;
2606+
internal FastBufferReader FastBufferReader;
2607+
}
2608+
2609+
internal List<DeferredObjectCreation> DeferredObjectCreationList = new List<DeferredObjectCreation>();
2610+
internal int DeferredObjectCreationCount;
2611+
2612+
internal void DeferCreateObject(ulong senderId, uint messageSize, NetworkObject.SceneObject sceneObject, FastBufferReader fastBufferReader)
2613+
{
2614+
var deferredObjectCreationEntry = new DeferredObjectCreation()
2615+
{
2616+
SenderId = senderId,
2617+
MessageSize = messageSize,
2618+
SceneObject = sceneObject,
2619+
};
2620+
2621+
unsafe
2622+
{
2623+
deferredObjectCreationEntry.FastBufferReader = new FastBufferReader(fastBufferReader.GetUnsafePtrAtCurrentPosition(), Allocator.Persistent, fastBufferReader.Length - fastBufferReader.Position);
2624+
}
2625+
2626+
DeferredObjectCreationList.Add(deferredObjectCreationEntry);
2627+
}
2628+
2629+
private void ProcessDeferredCreateObjectMessages()
2630+
{
2631+
// If no pending create object messages exit early
2632+
if (DeferredObjectCreationList.Count == 0)
2633+
{
2634+
return;
2635+
}
2636+
var networkManager = NetworkManager;
2637+
// Process all deferred create object messages.
2638+
foreach (var deferredObjectCreation in DeferredObjectCreationList)
2639+
{
2640+
CreateObjectMessage.CreateObject(ref networkManager, deferredObjectCreation.SenderId, deferredObjectCreation.MessageSize, deferredObjectCreation.SceneObject, deferredObjectCreation.FastBufferReader);
2641+
}
2642+
DeferredObjectCreationCount = DeferredObjectCreationList.Count;
2643+
DeferredObjectCreationList.Clear();
2644+
}
25522645
}
25532646
}

com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventProgress.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,14 @@ public enum SceneEventProgressStatus
4747
/// If you receive this event then it is most likely due to a bug (<em>please open a GitHub issue with steps to replicate</em>).<br/>
4848
/// </summary>
4949
InternalNetcodeError,
50+
/// <summary>
51+
/// This is returned when an unload or load action is attempted and scene management is disabled
52+
/// </summary>
53+
SceneManagementNotEnabled,
54+
/// <summary>
55+
/// This is returned when a client attempts to perform a server only action
56+
/// </summary>
57+
ServerOnlyAction,
5058
}
5159

5260
/// <summary>

com.unity.netcode.gameobjects/Tests/Runtime/NetworkObject/NetworkObjectSpawnManyObjectsTests.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,9 @@ public IEnumerator WhenManyObjectsAreSpawnedAtOnce_AllAreReceived()
5757
serverObject.NetworkManagerOwner = m_ServerNetworkManager;
5858
serverObject.Spawn();
5959
}
60-
// ensure all objects are replicated before spawning more
61-
yield return WaitForConditionOrTimeOut(() => SpawnObjecTrackingComponent.SpawnedObjects < k_SpawnedObjects);
62-
Assert.False(s_GlobalTimeoutHelper.TimedOut, $"Timed out waiting for the client to spawn {k_SpawnedObjects} objects!");
60+
// ensure all objects are replicated
61+
yield return WaitForConditionOrTimeOut(() => SpawnObjecTrackingComponent.SpawnedObjects == k_SpawnedObjects);
62+
AssertOnTimeout($"Timed out waiting for the client to spawn {k_SpawnedObjects} objects!");
6363
}
6464
}
6565
}

0 commit comments

Comments
 (0)