Skip to content

Commit e10c266

Browse files
fix: in-scene placed NetworkObject will not spawn client-side when disabled upon being despawned [MTT-4832] (#2239)
* fix This resolves the issue with in-scene placed NetworkObjects that are disabled when despawned not being able to re-spawn again and handles synchronizing despawned in-scene placed NetworkObjects during a scene switch (LoadSceneMode.Single). * test Added integration test that validates disabling NetworkObjects when despawned works with currently connected clients, late joining clients, and when scene switching (LoadSceneMode.Single) while also having the server despawn the in-scene placed NetworkObject upon its first spawn (i.e. so it starts off not visible/active to the clients when they finish the scene switch).
1 parent 4393621 commit e10c266

File tree

8 files changed

+351
-106
lines changed

8 files changed

+351
-106
lines changed

com.unity.netcode.gameobjects/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ Additional documentation and release notes are available at [Multiplayer Documen
2828

2929
### Fixed
3030

31+
- Fixed issue where an in-scene placed NetworkObject would not invoke NetworkBehaviour.OnNetworkSpawn if the GameObject was disabled when it was despawned. (#2239)
3132
- Fixed issue where clients were not rebuilding the `NetworkConfig` hash value for each unique connection request. (#2226)
3233
- Fixed the issue where player objects were not taking the `DontDestroyWithOwner` property into consideration when a client disconnected. (#2225)
3334
- Fixed issue where `SceneEventProgress` would not complete if a client late joins while it is still in progress. (#2222)

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1441,6 +1441,9 @@ private void OnServerLoadedScene(uint sceneEventId, Scene scene)
14411441
}
14421442
}
14431443

1444+
// Add any despawned when spawned in-scene placed NetworkObjects to the scene event data
1445+
sceneEventData.AddDespawnedInSceneNetworkObjects();
1446+
14441447
// Set the server's scene's handle so the client can build a look up table
14451448
sceneEventData.SceneHandle = scene.handle;
14461449

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

Lines changed: 93 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,8 @@ internal void AddSpawnedNetworkObjects()
268268
internal void AddDespawnedInSceneNetworkObjects()
269269
{
270270
m_DespawnedInSceneObjectsSync.Clear();
271-
var inSceneNetworkObjects = UnityEngine.Object.FindObjectsOfType<NetworkObject>().Where((c) => c.NetworkManager == m_NetworkManager);
271+
// Find all active and non-active in-scene placed NetworkObjects
272+
var inSceneNetworkObjects = UnityEngine.Object.FindObjectsOfType<NetworkObject>(includeInactive: true).Where((c) => c.NetworkManager == m_NetworkManager);
272273
foreach (var sobj in inSceneNetworkObjects)
273274
{
274275
if (sobj.IsSceneObject.HasValue && sobj.IsSceneObject.Value && !sobj.IsSpawned)
@@ -461,7 +462,6 @@ internal void WriteSceneSynchronizationData(FastBufferWriter writer)
461462
for (var i = 0; i < m_DespawnedInSceneObjectsSync.Count; ++i)
462463
{
463464
var noStart = writer.Position;
464-
var sceneObject = m_DespawnedInSceneObjectsSync[i].GetMessageSceneObject(TargetClientId);
465465
BytePacker.WriteValuePacked(writer, m_DespawnedInSceneObjectsSync[i].GetSceneOriginHandle());
466466
BytePacker.WriteValuePacked(writer, m_DespawnedInSceneObjectsSync[i].GlobalObjectIdHash);
467467
var noStop = writer.Position;
@@ -507,6 +507,15 @@ internal void SerializeScenePlacedObjects(FastBufferWriter writer)
507507
}
508508
}
509509

510+
// Write the number of despawned in-scene placed NetworkObjects
511+
writer.WriteValueSafe(m_DespawnedInSceneObjectsSync.Count);
512+
// Write the scene handle and GlobalObjectIdHash value
513+
for (var i = 0; i < m_DespawnedInSceneObjectsSync.Count; ++i)
514+
{
515+
BytePacker.WriteValuePacked(writer, m_DespawnedInSceneObjectsSync[i].GetSceneOriginHandle());
516+
BytePacker.WriteValuePacked(writer, m_DespawnedInSceneObjectsSync[i].GlobalObjectIdHash);
517+
}
518+
510519
var tailPosition = writer.Position;
511520
// Reposition to our count position to the head before we wrote our object count
512521
writer.Seek(headPosition);
@@ -624,6 +633,8 @@ internal void DeserializeScenePlacedObjects()
624633
sceneObject.Deserialize(InternalBuffer);
625634
NetworkObject.AddSceneObject(sceneObject, InternalBuffer, m_NetworkManager);
626635
}
636+
// Now deserialize the despawned in-scene placed NetworkObjects list (if any)
637+
DeserializeDespawnedInScenePlacedNetworkObjects();
627638
}
628639
finally
629640
{
@@ -746,6 +757,84 @@ internal void WriteClientSynchronizationResults(FastBufferWriter writer)
746757
}
747758
}
748759

760+
/// <summary>
761+
/// For synchronizing any despawned in-scene placed NetworkObjects that were
762+
/// despawned by the server during synchronization or scene loading
763+
/// </summary>
764+
private void DeserializeDespawnedInScenePlacedNetworkObjects()
765+
{
766+
// Process all de-spawned in-scene NetworkObjects for this network session
767+
m_DespawnedInSceneObjects.Clear();
768+
InternalBuffer.ReadValueSafe(out int despawnedObjectsCount);
769+
var sceneCache = new Dictionary<int, Dictionary<uint, NetworkObject>>();
770+
771+
for (int i = 0; i < despawnedObjectsCount; i++)
772+
{
773+
// We just need to get the scene
774+
ByteUnpacker.ReadValuePacked(InternalBuffer, out int networkSceneHandle);
775+
ByteUnpacker.ReadValuePacked(InternalBuffer, out uint globalObjectIdHash);
776+
var sceneRelativeNetworkObjects = new Dictionary<uint, NetworkObject>();
777+
if (!sceneCache.ContainsKey(networkSceneHandle))
778+
{
779+
if (m_NetworkManager.SceneManager.ServerSceneHandleToClientSceneHandle.ContainsKey(networkSceneHandle))
780+
{
781+
var localSceneHandle = m_NetworkManager.SceneManager.ServerSceneHandleToClientSceneHandle[networkSceneHandle];
782+
if (m_NetworkManager.SceneManager.ScenesLoaded.ContainsKey(localSceneHandle))
783+
{
784+
var objectRelativeScene = m_NetworkManager.SceneManager.ScenesLoaded[localSceneHandle];
785+
786+
// Find all active and non-active in-scene placed NetworkObjects
787+
var inSceneNetworkObjects = UnityEngine.Object.FindObjectsOfType<NetworkObject>(includeInactive: true).Where((c) =>
788+
c.GetSceneOriginHandle() == localSceneHandle && (c.IsSceneObject != false)).ToList();
789+
790+
foreach (var inSceneObject in inSceneNetworkObjects)
791+
{
792+
if (!sceneRelativeNetworkObjects.ContainsKey(inSceneObject.GlobalObjectIdHash))
793+
{
794+
sceneRelativeNetworkObjects.Add(inSceneObject.GlobalObjectIdHash, inSceneObject);
795+
}
796+
}
797+
// Add this to a cache so we don't have to run this potentially multiple times (nothing will spawn or despawn during this time
798+
sceneCache.Add(networkSceneHandle, sceneRelativeNetworkObjects);
799+
}
800+
else
801+
{
802+
UnityEngine.Debug.LogError($"In-Scene NetworkObject GlobalObjectIdHash ({globalObjectIdHash}) cannot find its relative local scene handle {localSceneHandle}!");
803+
}
804+
}
805+
else
806+
{
807+
UnityEngine.Debug.LogError($"In-Scene NetworkObject GlobalObjectIdHash ({globalObjectIdHash}) cannot find its relative NetworkSceneHandle {networkSceneHandle}!");
808+
}
809+
}
810+
else // Use the cached NetworkObjects if they exist
811+
{
812+
sceneRelativeNetworkObjects = sceneCache[networkSceneHandle];
813+
}
814+
815+
// Now find the in-scene NetworkObject with the current GlobalObjectIdHash we are looking for
816+
if (sceneRelativeNetworkObjects.ContainsKey(globalObjectIdHash))
817+
{
818+
// Since this is a NetworkObject that was never spawned, we just need to send a notification
819+
// out that it was despawned so users can make adjustments
820+
sceneRelativeNetworkObjects[globalObjectIdHash].InvokeBehaviourNetworkDespawn();
821+
if (!m_NetworkManager.SceneManager.ScenePlacedObjects.ContainsKey(globalObjectIdHash))
822+
{
823+
m_NetworkManager.SceneManager.ScenePlacedObjects.Add(globalObjectIdHash, new Dictionary<int, NetworkObject>());
824+
}
825+
826+
if (!m_NetworkManager.SceneManager.ScenePlacedObjects[globalObjectIdHash].ContainsKey(sceneRelativeNetworkObjects[globalObjectIdHash].GetSceneOriginHandle()))
827+
{
828+
m_NetworkManager.SceneManager.ScenePlacedObjects[globalObjectIdHash].Add(sceneRelativeNetworkObjects[globalObjectIdHash].GetSceneOriginHandle(), sceneRelativeNetworkObjects[globalObjectIdHash]);
829+
}
830+
}
831+
else
832+
{
833+
UnityEngine.Debug.LogError($"In-Scene NetworkObject GlobalObjectIdHash ({globalObjectIdHash}) could not be found!");
834+
}
835+
}
836+
}
837+
749838
/// <summary>
750839
/// Client Side:
751840
/// During the processing of a server sent Event_Sync, this method will be called for each scene once
@@ -779,72 +868,9 @@ internal void SynchronizeSceneNetworkObjects(NetworkManager networkManager)
779868
}
780869
}
781870

782-
// Process all de-spawned in-scene NetworkObjects for this network session
783-
m_DespawnedInSceneObjects.Clear();
784-
InternalBuffer.ReadValueSafe(out int despawnedObjectsCount);
785-
var sceneCache = new Dictionary<int, Dictionary<uint, NetworkObject>>();
786-
787-
for (int i = 0; i < despawnedObjectsCount; i++)
788-
{
789-
// We just need to get the scene
790-
ByteUnpacker.ReadValuePacked(InternalBuffer, out int networkSceneHandle);
791-
ByteUnpacker.ReadValuePacked(InternalBuffer, out uint globalObjectIdHash);
792-
var sceneRelativeNetworkObjects = new Dictionary<uint, NetworkObject>();
793-
if (!sceneCache.ContainsKey(networkSceneHandle))
794-
{
795-
if (m_NetworkManager.SceneManager.ServerSceneHandleToClientSceneHandle.ContainsKey(networkSceneHandle))
796-
{
797-
var localSceneHandle = m_NetworkManager.SceneManager.ServerSceneHandleToClientSceneHandle[networkSceneHandle];
798-
if (m_NetworkManager.SceneManager.ScenesLoaded.ContainsKey(localSceneHandle))
799-
{
800-
var objectRelativeScene = m_NetworkManager.SceneManager.ScenesLoaded[localSceneHandle];
801-
var inSceneNetworkObjects = UnityEngine.Object.FindObjectsOfType<NetworkObject>().Where((c) =>
802-
c.GetSceneOriginHandle() == localSceneHandle && (c.IsSceneObject != false)).ToList();
871+
// Now deserialize the despawned in-scene placed NetworkObjects list (if any)
872+
DeserializeDespawnedInScenePlacedNetworkObjects();
803873

804-
foreach (var inSceneObject in inSceneNetworkObjects)
805-
{
806-
sceneRelativeNetworkObjects.Add(inSceneObject.GlobalObjectIdHash, inSceneObject);
807-
}
808-
// Add this to a cache so we don't have to run this potentially multiple times (nothing will spawn or despawn during this time
809-
sceneCache.Add(networkSceneHandle, sceneRelativeNetworkObjects);
810-
}
811-
else
812-
{
813-
UnityEngine.Debug.LogError($"In-Scene NetworkObject GlobalObjectIdHash ({globalObjectIdHash}) cannot find its relative local scene handle {localSceneHandle}!");
814-
}
815-
}
816-
else
817-
{
818-
UnityEngine.Debug.LogError($"In-Scene NetworkObject GlobalObjectIdHash ({globalObjectIdHash}) cannot find its relative NetworkSceneHandle {networkSceneHandle}!");
819-
}
820-
}
821-
else // Use the cached NetworkObjects if they exist
822-
{
823-
sceneRelativeNetworkObjects = sceneCache[networkSceneHandle];
824-
}
825-
826-
// Now find the in-scene NetworkObject with the current GlobalObjectIdHash we are looking for
827-
if (sceneRelativeNetworkObjects.ContainsKey(globalObjectIdHash))
828-
{
829-
// Since this is a NetworkObject that was never spawned, we just need to send a notification
830-
// out that it was despawned so users can make adjustments
831-
sceneRelativeNetworkObjects[globalObjectIdHash].InvokeBehaviourNetworkDespawn();
832-
if (!m_NetworkManager.SceneManager.ScenePlacedObjects.ContainsKey(globalObjectIdHash))
833-
{
834-
m_NetworkManager.SceneManager.ScenePlacedObjects.Add(globalObjectIdHash, new Dictionary<int, NetworkObject>());
835-
}
836-
837-
if (!m_NetworkManager.SceneManager.ScenePlacedObjects[globalObjectIdHash].ContainsKey(sceneRelativeNetworkObjects[globalObjectIdHash].GetSceneOriginHandle()))
838-
{
839-
m_NetworkManager.SceneManager.ScenePlacedObjects[globalObjectIdHash].Add(sceneRelativeNetworkObjects[globalObjectIdHash].GetSceneOriginHandle(), sceneRelativeNetworkObjects[globalObjectIdHash]);
840-
}
841-
842-
}
843-
else
844-
{
845-
UnityEngine.Debug.LogError($"In-Scene NetworkObject GlobalObjectIdHash ({globalObjectIdHash}) could not be found!");
846-
}
847-
}
848874
}
849875
finally
850876
{

com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,13 @@ internal NetworkObject CreateLocalNetworkObject(NetworkObject.SceneObject sceneO
388388
NetworkLog.LogError($"{nameof(NetworkPrefab)} hash was not found! In-Scene placed {nameof(NetworkObject)} soft synchronization failure for Hash: {globalObjectIdHash}!");
389389
}
390390
}
391+
392+
// Since this NetworkObject is an in-scene placed NetworkObject, if it is disabled then enable it so
393+
// NetworkBehaviours will have their OnNetworkSpawn method invoked
394+
if (networkObject != null && !networkObject.gameObject.activeInHierarchy)
395+
{
396+
networkObject.gameObject.SetActive(true);
397+
}
391398
}
392399

393400
if (networkObject != null)

testproject/Assets/Tests/Manual/SceneTransitioningAdditive/DespawnInSceneNetworkObject.cs

Lines changed: 37 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -8,87 +8,91 @@ namespace TestProject.ManualTests
88
/// <summary>
99
/// Used for manually testing spawning and despawning in-scene
1010
/// placed NetworkObjects
11-
///
12-
/// Note: We do not destroy in-scene placed NetworkObjects, but
13-
/// users must handle visibility (rendering wise) when the in-scene
14-
/// NetworkObject is spawned and despawned. This class just enables
15-
/// or disabled the mesh renderer.
1611
/// </summary>
1712
public class DespawnInSceneNetworkObject : NetworkBehaviour
1813
{
14+
[Tooltip("When set, the server will despawn the NetworkObject upon its first spawn.")]
15+
public bool StartDespawned;
16+
1917
private Coroutine m_ScanInputHandle;
20-
private MeshRenderer m_MeshRenderer;
2118

22-
private void Start()
23-
{
24-
if (!IsSpawned)
25-
{
26-
m_MeshRenderer = GetComponent<MeshRenderer>();
27-
if (m_MeshRenderer != null)
28-
{
29-
m_MeshRenderer.enabled = false;
30-
}
31-
}
32-
}
19+
// Used to prevent the server from despawning
20+
// the in-scene placed NetworkObject after the
21+
// first spawn (only if StartDespawned is true)
22+
private bool m_ServerDespawnedOnFirstSpawn;
23+
24+
private NetworkManager m_CachedNetworkManager;
3325

3426
public override void OnNetworkSpawn()
3527
{
3628
Debug.Log($"{name} spawned!");
37-
m_MeshRenderer = GetComponent<MeshRenderer>();
38-
if (m_MeshRenderer != null)
39-
{
40-
m_MeshRenderer.enabled = true;
41-
}
29+
4230
if (!IsServer)
4331
{
4432
return;
4533
}
34+
35+
m_CachedNetworkManager = NetworkManager;
36+
4637
if (m_ScanInputHandle == null)
4738
{
48-
m_ScanInputHandle = StartCoroutine(ScanInput());
39+
// Using the NetworkManager to create the coroutine so it is not deactivated
40+
// when the GameObject this NetworkBehaviour is attached to is disabled.
41+
m_ScanInputHandle = NetworkManager.StartCoroutine(ScanInput(NetworkObject));
42+
}
43+
44+
// m_ServerDespawnedOnFirstSpawn prevents the server from always
45+
// despawning on the server-side after the first spawn.
46+
if (StartDespawned && !m_ServerDespawnedOnFirstSpawn)
47+
{
48+
m_ServerDespawnedOnFirstSpawn = true;
49+
NetworkObject.Despawn(false);
4950
}
5051
}
5152

5253
public override void OnNetworkDespawn()
5354
{
54-
if (m_MeshRenderer != null)
55-
{
56-
m_MeshRenderer.enabled = false;
57-
}
55+
// It is OK to disable in-scene placed NetworkObjects upon
56+
// despawning. When re-spawned the client-side will re-activate
57+
// the GameObject, while the server-side must set the GameObject
58+
// active itself.
59+
gameObject.SetActive(false);
60+
5861
Debug.Log($"{name} despawned!");
5962
base.OnNetworkDespawn();
6063
}
6164

6265
public override void OnDestroy()
6366
{
64-
if (m_ScanInputHandle != null)
67+
if (m_ScanInputHandle != null && m_CachedNetworkManager != null)
6568
{
66-
StopCoroutine(m_ScanInputHandle);
69+
m_CachedNetworkManager.StopCoroutine(m_ScanInputHandle);
6770
}
6871
m_ScanInputHandle = null;
6972
base.OnDestroy();
7073
}
7174

72-
private IEnumerator ScanInput()
75+
private IEnumerator ScanInput(NetworkObject networkObject)
7376
{
7477
while (true)
7578
{
7679
try
7780
{
78-
if (IsSpawned)
81+
if (networkObject.IsSpawned)
7982
{
8083
if (Input.GetKeyDown(KeyCode.Backspace))
8184
{
8285
Debug.Log($"{name} should despawn.");
83-
NetworkObject.Despawn(false);
86+
networkObject.Despawn(false);
8487
}
8588
}
8689
else if (NetworkManager.Singleton && NetworkManager.Singleton.IsListening)
8790
{
8891
if (Input.GetKeyDown(KeyCode.Backspace))
8992
{
9093
Debug.Log($"{name} should spawn.");
91-
NetworkObject.Spawn();
94+
networkObject.gameObject.SetActive(true);
95+
networkObject.Spawn();
9296
}
9397
}
9498
}
@@ -99,7 +103,6 @@ private IEnumerator ScanInput()
99103

100104
yield return null;
101105
}
102-
103106
}
104107
}
105108
}

testproject/Assets/Tests/Manual/SceneTransitioningAdditive/SceneTransitioningBase1.unity

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2250,6 +2250,11 @@ PrefabInstance:
22502250
propertyPath: m_Name
22512251
value: InSceneObjectToDespawn
22522252
objectReference: {fileID: 0}
2253+
- target: {fileID: 4518755925279129999, guid: 3a854a190ab5b1b4fb00bec725fdda9e,
2254+
type: 3}
2255+
propertyPath: StartDespawned
2256+
value: 1
2257+
objectReference: {fileID: 0}
22532258
m_RemovedComponents: []
22542259
m_SourcePrefab: {fileID: 100100000, guid: 3a854a190ab5b1b4fb00bec725fdda9e, type: 3}
22552260
--- !u!1 &1008611498
@@ -2466,7 +2471,6 @@ MonoBehaviour:
24662471
m_ProtocolType: 0
24672472
m_MaxPacketQueueSize: 128
24682473
m_MaxPayloadSize: 512000
2469-
m_MaxSendQueueSize: 4096000
24702474
m_HeartbeatTimeoutMS: 500
24712475
m_ConnectTimeoutMS: 1000
24722476
m_MaxConnectAttempts: 60

0 commit comments

Comments
 (0)