Skip to content

Commit 7ab9947

Browse files
fix: Parented in-scene objects under dynamically spawned objects are destroyed when the network session ends but original scene remains loaded. [MTT-7560] (#2737)
* fix Fixes the issue with an in-scene placed NetworkObject being parented under a dynamically spawned NetworkObject and a sudden shutdown/exit occurs. This will remove the child in-scene placed NetworkObject from the parent during the NetworkManager shutdown process where it destroys all dynamically spawned NetworkObjects.' Adjusting the parenting rules slightly to only deny parenting if the child or parent is not spawned. * test updating the test to reflect the updated error message when a client attempts to destroy a spawned NetworkObject while still connected to a session. Updating the scene objects not destroyed test. Adding `ChildSceneObjectsDoNotDestroyOnShutdown` Adding ChildSceneObjectsDoNotDestroyOnShutdown test to validate this PR. Updating ParentedNetworkTransformTest to provide enough time to let interpolation to finish.
1 parent 39d0a2e commit 7ab9947

File tree

6 files changed

+166
-119
lines changed

6 files changed

+166
-119
lines changed

com.unity.netcode.gameobjects/CHANGELOG.md

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,23 +19,26 @@ Additional documentation and release notes are available at [Multiplayer Documen
1919
- Added `SceneEventProgress.SceneManagementNotEnabled` return status to be returned when a `NetworkSceneManager` method is invoked and scene management is not enabled. (#2735)
2020
- Added `SceneEventProgress.ServerOnlyAction` return status to be returned when a `NetworkSceneManager` method is invoked by a client. (#2735)
2121

22-
### Changed
23-
24-
- `NetworkManager.ConnectedClientsIds` is now accessible on the client side and will contain the list of all clients in the session, including the host client if the server is operating in host mode (#2762)
25-
2622
### Fixed
2723

28-
- 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)
29-
- 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)
30-
- 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)
3124
- 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)
3225
- 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)
3326
- Fixed issue where a `NetworkTransform` instance with interpolation enabled would result in wide visual motion gaps (stuttering) under above normal latency conditions and a 1-5% or higher packet are drop rate. (#2713)
3427

3528
### Changed
36-
- Changed `NetworkTransform` authoritative instance tick registration so a single `NetworkTransform` specific tick event update will update all authoritative instances to improve perofmance. (#2713)
3729

30+
- `NetworkManager.ConnectedClientsIds` is now accessible on the client side and will contain the list of all clients in the session, including the host client if the server is operating in host mode (#2762)
3831
- 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)
32+
- Changed `NetworkTransform` authoritative instance tick registration so a single `NetworkTransform` specific tick event update will update all authoritative instances to improve perofmance. (#2713)
33+
34+
## [1.7.1] - 2023-11-15
35+
36+
### Added
37+
38+
### Fixed
39+
40+
- 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)
41+
- 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)
3942

4043
## [1.7.0] - 2023-10-11
4144

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

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -703,7 +703,7 @@ private void OnDestroy()
703703
// Since we still have a session connection, log locally and on the server to inform user of this issue.
704704
if (NetworkManager.LogLevel <= LogLevel.Error)
705705
{
706-
NetworkLog.LogErrorServer($"Destroy a spawned {nameof(NetworkObject)} on a non-host client is not valid. Call {nameof(Destroy)} or {nameof(Despawn)} on the server/host instead.");
706+
NetworkLog.LogErrorServer($"[Invalid Destroy][{gameObject.name}][NetworkObjectId:{NetworkObjectId}] Destroy a spawned {nameof(NetworkObject)} on a non-host client is not valid. Call {nameof(Destroy)} or {nameof(Despawn)} on the server/host instead.");
707707
}
708708
return;
709709
}
@@ -969,20 +969,21 @@ public bool TrySetParent(NetworkObject parent, bool worldPositionStays = true)
969969
return false;
970970
}
971971

972-
if (!NetworkManager.IsServer)
972+
if (!NetworkManager.IsServer && !NetworkManager.ShutdownInProgress)
973973
{
974974
return false;
975975
}
976976

977-
if (!IsSpawned)
977+
// If the parent is not null fail only if either of the two is true:
978+
// - This instance is spawned and the parent is not.
979+
// - This instance is not spawned and the parent is.
980+
// Basically, don't allow parenting when either the child or parent is not spawned.
981+
// Caveat: if the parent is null then we can allow parenting whether the instance is or is not spawned.
982+
if (parent != null && (IsSpawned ^ parent.IsSpawned))
978983
{
979984
return false;
980985
}
981986

982-
if (parent != null && !parent.IsSpawned)
983-
{
984-
return false;
985-
}
986987
m_CachedWorldPositionStays = worldPositionStays;
987988

988989
if (parent == null)
@@ -1018,15 +1019,36 @@ private void OnTransformParentChanged()
10181019

10191020
if (!NetworkManager.IsServer)
10201021
{
1021-
transform.parent = m_CachedParent;
1022-
Debug.LogException(new NotServerException($"Only the server can reparent {nameof(NetworkObject)}s"));
1022+
// Log exception if we are a client and not shutting down.
1023+
if (!NetworkManager.ShutdownInProgress)
1024+
{
1025+
transform.parent = m_CachedParent;
1026+
Debug.LogException(new NotServerException($"Only the server can reparent {nameof(NetworkObject)}s"));
1027+
}
1028+
else // Otherwise, if we are removing a parent then go ahead and allow parenting to occur
1029+
if (transform.parent == null)
1030+
{
1031+
m_LatestParent = null;
1032+
m_CachedParent = null;
1033+
InvokeBehaviourOnNetworkObjectParentChanged(null);
1034+
}
10231035
return;
10241036
}
1025-
1037+
else // Otherwise, on the serer side if this instance is not spawned...
10261038
if (!IsSpawned)
10271039
{
1028-
transform.parent = m_CachedParent;
1029-
Debug.LogException(new SpawnStateException($"{nameof(NetworkObject)} can only be reparented after being spawned"));
1040+
// ,,,and we are removing the parent, then go ahead and allow parenting to occur
1041+
if (transform.parent == null)
1042+
{
1043+
m_LatestParent = null;
1044+
m_CachedParent = null;
1045+
InvokeBehaviourOnNetworkObjectParentChanged(null);
1046+
}
1047+
else
1048+
{
1049+
transform.parent = m_CachedParent;
1050+
Debug.LogException(new SpawnStateException($"{nameof(NetworkObject)} can only be reparented after being spawned"));
1051+
}
10301052
return;
10311053
}
10321054
var removeParent = false;

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

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -728,17 +728,43 @@ internal void DespawnAndDestroyNetworkObjects()
728728
// Leave destruction up to the handler
729729
NetworkManager.PrefabHandler.HandleNetworkPrefabDestroy(networkObjects[i]);
730730
}
731-
else if (networkObjects[i].IsSpawned)
731+
else
732732
{
733-
// If it is an in-scene placed NetworkObject then just despawn
734-
// and let it be destroyed when the scene is unloaded. Otherwise, despawn and destroy it.
733+
// If it is an in-scene placed NetworkObject then just despawn and let it be destroyed when the scene
734+
// is unloaded. Otherwise, despawn and destroy it.
735735
var shouldDestroy = !(networkObjects[i].IsSceneObject != null && networkObjects[i].IsSceneObject.Value);
736736

737-
OnDespawnObject(networkObjects[i], shouldDestroy);
738-
}
739-
else if (networkObjects[i].IsSceneObject != null && !networkObjects[i].IsSceneObject.Value)
740-
{
741-
UnityEngine.Object.Destroy(networkObjects[i].gameObject);
737+
// If we are going to destroy this NetworkObject, check for any in-scene placed children that need to be removed
738+
if (shouldDestroy)
739+
{
740+
// Check to see if there are any in-scene placed children that are marked to be destroyed with the scene
741+
var childrenObjects = networkObjects[i].GetComponentsInChildren<NetworkObject>();
742+
foreach (var childObject in childrenObjects)
743+
{
744+
if (childObject == networkObjects[i])
745+
{
746+
continue;
747+
}
748+
749+
// If the child is an in-scene placed NetworkObject then remove the child from the parent (which was dynamically spawned)
750+
// and set its parent to root
751+
if (childObject.IsSceneObject != null && childObject.IsSceneObject.Value)
752+
{
753+
childObject.TryRemoveParent(childObject.WorldPositionStays());
754+
}
755+
}
756+
}
757+
758+
// If spawned, then despawn and potentially destroy.
759+
if (networkObjects[i].IsSpawned)
760+
{
761+
OnDespawnObject(networkObjects[i], shouldDestroy);
762+
}
763+
else // Otherwise, if we are not spawned and we should destroy...then destroy.
764+
if (shouldDestroy)
765+
{
766+
UnityEngine.Object.Destroy(networkObjects[i].gameObject);
767+
}
742768
}
743769
}
744770
}

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ public enum ClientDestroyObject
5858
ShuttingDown,
5959
ActiveSession
6060
}
61+
62+
private string m_ClientPlayerName;
63+
private ulong m_ClientNetworkObjectId;
6164
/// <summary>
6265
/// Validates the expected behavior when the client-side destroys a <see cref="NetworkObject"/>
6366
/// </summary>
@@ -78,7 +81,8 @@ public IEnumerator TestNetworkObjectClientDestroy([Values] ClientDestroyObject c
7881
LogAssert.ignoreFailingMessages = true;
7982
NetworkLog.NetworkManagerOverride = m_ClientNetworkManagers[0];
8083
}
81-
84+
m_ClientPlayerName = clientPlayer.gameObject.name;
85+
m_ClientNetworkObjectId = clientPlayer.NetworkObjectId;
8286
Object.DestroyImmediate(clientPlayer.gameObject);
8387

8488
// destroying a NetworkObject while a session is active is not allowed
@@ -91,12 +95,12 @@ public IEnumerator TestNetworkObjectClientDestroy([Values] ClientDestroyObject c
9195

9296
private bool HaveLogsBeenReceived()
9397
{
94-
if (!NetcodeLogAssert.HasLogBeenReceived(LogType.Error, "[Netcode] Destroy a spawned NetworkObject on a non-host client is not valid. Call Destroy or Despawn on the server/host instead."))
98+
if (!NetcodeLogAssert.HasLogBeenReceived(LogType.Error, $"[Netcode] [Invalid Destroy][{m_ClientPlayerName}][NetworkObjectId:{m_ClientNetworkObjectId}] Destroy a spawned {nameof(NetworkObject)} on a non-host client is not valid. Call Destroy or Despawn on the server/host instead."))
9599
{
96100
return false;
97101
}
98102

99-
if (!NetcodeLogAssert.HasLogBeenReceived(LogType.Error, $"[Netcode-Server Sender={m_ClientNetworkManagers[0].LocalClientId}] Destroy a spawned NetworkObject on a non-host client is not valid. Call Destroy or Despawn on the server/host instead."))
103+
if (!NetcodeLogAssert.HasLogBeenReceived(LogType.Error, $"[Netcode-Server Sender={m_ClientNetworkManagers[0].LocalClientId}] [Invalid Destroy][{m_ClientPlayerName}][NetworkObjectId:{m_ClientNetworkObjectId}] Destroy a spawned {nameof(NetworkObject)} on a non-host client is not valid. Call Destroy or Despawn on the server/host instead."))
100104
{
101105
return false;
102106
}

com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformTests.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -602,7 +602,10 @@ public void ParentedNetworkTransformTest([Values] Precision precision, [Values]
602602
success = WaitForConditionOrTimeOutWithTimeTravel(AllChildObjectInstancesHaveChild);
603603
Assert.True(success, "Timed out waiting for all instances to have parented a child!");
604604

605+
// Provide two network ticks for interpolation to finalize
605606
TimeTravelToNextTick();
607+
TimeTravelToNextTick();
608+
606609
// This validates each child instance has preserved their local space values
607610
AllChildrenLocalTransformValuesMatch(false, ChildrenTransformCheckType.Connected_Clients);
608611

0 commit comments

Comments
 (0)