Skip to content

Commit 8d555b4

Browse files
fix: Attachables destroy order of operations (#3931)
* fix Fixing some issues based on varying destroy order of operations (more recent change where instantiation order does not reflect the order in which they will be destroyed via SceneManager during an in-session scene load (single mode). * test Adding the test for this fix. * fix The actual fix for the issue with additional handling when an attachable's NetworkObject is destroyed while it is still attached to something else. * update & style Adding change log entry. Fixing some typos. * style removing white space. * style fixing spelling typo. * style-pvp Adding inheritdoc * refactor Going ahead and adding an (currently) internal "IsDestroying" flag that is set only when the NetworkObject.OnDestroy method is invoked. * update Applying suggested adjustment to logic within OnNetworkPreDespawn. * style adding a whitespace * refactor and fix This includes some additional edge case fixes... specifically for the scenario where we know a scene is being unloaded that will result in either an `AttachableNode` or `AttachableBehaviour` being destroyed and they are attached. * style Removing comment that was just mistakenly left in. * update + fix Fixing an edge case that requires manually testing due to how integration tests additively load everything to avoid unloading the test runner test scene when loading a scene. Two parts: - When a server is destroying spawned scene objects due to a single mode scene load, it should also include a check for this and handle marking the NetworkObject and associated NetworkBehaviours as being destroyed and then destroy the object. - Just prior to handling a scene loading event, we need to mark anything that will be destroyed (i.e. destroy with scene) as being destroyed when migrating spawned objects into the DDOL. * style Updating some comments based on AI suggestions. * style UnityEngine namespace included and no longer needed to prefix.
1 parent e02c3e7 commit 8d555b4

12 files changed

Lines changed: 646 additions & 42 deletions

File tree

com.unity.netcode.gameobjects/CHANGELOG.md

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

3030
### Fixed
3131

32+
- Fixed issue where either an `AttachableBehaviour` or an `AttachableNode` can throw an exception if they are attached during a scene unload where one of the two persists the scene unload event and the other does not. (#3931)
3233
- Fixed issue where attempts to use `NetworkLog` when there is no `NetworkManager` instance would result in an exception. (#3917)
3334

3435
### Security

com.unity.netcode.gameobjects/Runtime/Components/Helpers/AttachableBehaviour.cs

Lines changed: 50 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -261,23 +261,30 @@ internal void ForceDetach()
261261
ForceComponentChange(false, true);
262262

263263
InternalDetach();
264-
// Notify of the changed attached state
265-
NotifyAttachedStateChanged(m_AttachState, m_AttachableNode);
266-
267-
m_AttachedNodeReference = new NetworkBehaviourReference(null);
268264

269-
// When detaching, we want to make our final action
270-
// the invocation of the AttachableNode's Detach method.
271-
if (m_AttachableNode)
265+
if (m_AttachableNode != null && !m_AttachableNode.IsDestroying)
272266
{
267+
// Notify of the changed attached state
268+
NotifyAttachedStateChanged(m_AttachState, m_AttachableNode);
269+
// Only notify of the detach if the node is still valid.
273270
m_AttachableNode.Detach(this);
274-
m_AttachableNode = null;
275271
}
272+
273+
m_AttachedNodeReference = new NetworkBehaviourReference(null);
274+
m_AttachableNode = null;
276275
}
277276

278277
/// <inheritdoc/>
279278
public override void OnNetworkPreDespawn()
280279
{
280+
// If the NetworkObject is being destroyed and not completely detached, then destroy the GameObject for
281+
// this attachable since the associated default parent is being destroyed.
282+
if (IsDestroying && m_AttachState != AttachState.Detached)
283+
{
284+
Destroy(gameObject);
285+
return;
286+
}
287+
281288
if (NetworkManager.ShutdownInProgress || AutoDetach.HasFlag(AutoDetachTypes.OnDespawn))
282289
{
283290
ForceDetach();
@@ -286,7 +293,7 @@ public override void OnNetworkPreDespawn()
286293
}
287294

288295
/// <summary>
289-
/// This will apply the final attach or detatch state based on the current value of <see cref="m_AttachedNodeReference"/>.
296+
/// This will apply the final attach or detach state based on the current value of <see cref="m_AttachedNodeReference"/>.
290297
/// </summary>
291298
[MethodImpl(MethodImplOptions.AggressiveInlining)]
292299
private void UpdateAttachedState()
@@ -304,16 +311,18 @@ private void UpdateAttachedState()
304311
return;
305312
}
306313

307-
// If we are attached to some other AttachableNode, then detach from that before attaching to a new one.
314+
// If we are attaching and already attached to some other AttachableNode,
315+
// then detach from that before attaching to a new one.
308316
if (isAttaching && m_AttachableNode != null && m_AttachState == AttachState.Attached)
309317
{
310-
// Run through the same process without being triggerd by a NetVar update.
318+
// Detach the current attachable
311319
NotifyAttachedStateChanged(AttachState.Detaching, m_AttachableNode);
312320
InternalDetach();
313321
NotifyAttachedStateChanged(AttachState.Detached, m_AttachableNode);
314-
315322
m_AttachableNode.Detach(this);
316323
m_AttachableNode = null;
324+
325+
// Now attach the new attachable
317326
}
318327

319328
// Change the state to attaching or detaching
@@ -392,7 +401,8 @@ internal void ForceComponentChange(bool isAttaching, bool forcedChange)
392401

393402
foreach (var componentControllerEntry in ComponentControllers)
394403
{
395-
if (componentControllerEntry.AutoTrigger.HasFlag(triggerType))
404+
// Only if the component controller still exists and has the appropriate flag.
405+
if (componentControllerEntry.ComponentController && componentControllerEntry.AutoTrigger.HasFlag(triggerType))
396406
{
397407
componentControllerEntry.ComponentController.ForceChangeEnabled(componentControllerEntry.EnableOnAttach ? isAttaching : !isAttaching, forcedChange);
398408
}
@@ -457,7 +467,9 @@ public void Attach(AttachableNode attachableNode)
457467
/// </summary>
458468
internal void InternalDetach()
459469
{
460-
if (m_AttachableNode)
470+
// If this instance is not in the middle of being destroyed, the attachable node is not null, and the node is not destroying
471+
// =or= the scene it is located in is in the middle of being unloaded, then re-parent under the default parent.
472+
if (!IsDestroying && m_AttachableNode && (!m_AttachableNode.IsDestroying || m_AttachableNode.gameObject.scene.isLoaded))
461473
{
462474
if (m_DefaultParent)
463475
{
@@ -553,12 +565,33 @@ private void UpdateAttachStateRpc(NetworkBehaviourReference attachedNodeReferenc
553565
/// </summary>
554566
internal void OnAttachNodeDestroy()
555567
{
556-
// If this instance should force a detach on destroy
557-
if (AutoDetach.HasFlag(AutoDetachTypes.OnAttachNodeDestroy))
568+
// We force a detach on destroy if there is a flag =or= if we are attached to a node that is being destroyed.
569+
if (AutoDetach.HasFlag(AutoDetachTypes.OnAttachNodeDestroy) ||
570+
(AutoDetach.HasFlag(AutoDetachTypes.OnDespawn) && m_AttachState == AttachState.Attached && m_AttachableNode && m_AttachableNode.IsDestroying))
571+
{
572+
ForceDetach();
573+
}
574+
}
575+
576+
577+
/// <summary>
578+
/// When we know this instance is being destroyed or will be destroyed
579+
/// by something outside of NGO's realm of control, this gets invoked.
580+
/// We should detach from any AttachableNode when this is invoked.
581+
/// </summary>
582+
protected internal override void OnIsDestroying()
583+
{
584+
// If we are not already marked as being destroyed, attached, this instance is the authority instance, and the node we are attached
585+
// to is not in the middle of being destroyed...detach normally.
586+
if (!IsDestroying && HasAuthority && m_AttachState == AttachState.Attached && m_AttachableNode && !m_AttachableNode.IsDestroying)
587+
{
588+
Detach();
589+
}
590+
else // Otherwise force the detach.
558591
{
559-
// Force a detach
560592
ForceDetach();
561593
}
594+
base.OnIsDestroying();
562595
}
563596
}
564597
}

com.unity.netcode.gameobjects/Runtime/Components/Helpers/AttachableNode.cs

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -71,20 +71,20 @@ public override void OnNetworkPreDespawn()
7171
{
7272
for (int i = m_AttachedBehaviours.Count - 1; i >= 0; i--)
7373
{
74-
if (!m_AttachedBehaviours[i])
74+
var attachable = m_AttachedBehaviours[i];
75+
if (!attachable)
7576
{
7677
continue;
7778
}
78-
// If we don't have authority but should detach on despawn,
79-
// then proceed to detach.
80-
if (!m_AttachedBehaviours[i].HasAuthority)
79+
80+
if (attachable.HasAuthority && attachable.IsSpawned)
8181
{
82-
m_AttachedBehaviours[i].ForceDetach();
82+
// Detach the normal way with authority
83+
attachable.Detach();
8384
}
84-
else
85+
else if (!attachable.HasAuthority || !attachable.IsDestroying)
8586
{
86-
// Detach the normal way with authority
87-
m_AttachedBehaviours[i].Detach();
87+
attachable.ForceDetach();
8888
}
8989
}
9090
}
@@ -93,12 +93,10 @@ public override void OnNetworkPreDespawn()
9393

9494
internal override void InternalOnDestroy()
9595
{
96-
// Notify any attached behaviours that this node is being destroyed.
97-
for (int i = m_AttachedBehaviours.Count - 1; i >= 0; i--)
96+
if (m_AttachedBehaviours.Count > 0)
9897
{
99-
m_AttachedBehaviours[i]?.OnAttachNodeDestroy();
98+
OnIsDestroying();
10099
}
101-
m_AttachedBehaviours.Clear();
102100
base.InternalOnDestroy();
103101
}
104102

@@ -141,4 +139,21 @@ internal void Detach(AttachableBehaviour attachableBehaviour)
141139
m_AttachedBehaviours.Remove(attachableBehaviour);
142140
OnDetached(attachableBehaviour);
143141
}
142+
143+
/// <summary>
144+
/// When we know this instance is being destroyed or will be destroyed
145+
/// by something outside of NGO's realm of control, this gets invoked.
146+
/// We should notify any attached AttachableBehaviour that this node
147+
/// is being destroyed.
148+
/// </summary>
149+
protected internal override void OnIsDestroying()
150+
{
151+
// Notify any attached behaviours that this node is being destroyed.
152+
for (int i = m_AttachedBehaviours.Count - 1; i >= 0; i--)
153+
{
154+
m_AttachedBehaviours[i]?.OnAttachNodeDestroy();
155+
}
156+
m_AttachedBehaviours.Clear();
157+
base.OnIsDestroying();
158+
}
144159
}

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

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -643,6 +643,42 @@ protected NetworkBehaviour GetNetworkBehaviour(ushort behaviourId)
643643
/// </summary>
644644
public ulong OwnerClientId { get; internal set; }
645645

646+
/// <summary>
647+
/// Returns true if the NetworkObject is in the middle of being destroyed.
648+
/// </summary>
649+
/// <remarks>
650+
/// <see cref="SetIsDestroying"/>
651+
/// </remarks>
652+
internal bool IsDestroying { get; private set; }
653+
654+
/// <summary>
655+
/// This provides us with a way to track when something is in the middle
656+
/// of being destroyed or will be destroyed by something like SceneManager.
657+
/// </summary>
658+
protected internal virtual void OnIsDestroying()
659+
{
660+
}
661+
662+
/// <summary>
663+
/// Invoked by <see cref="NetworkObject.SetIsDestroying"/>.
664+
/// </summary>
665+
/// <remarks>
666+
/// We want to invoke the virtual method prior to setting the
667+
/// IsDestroying flag to be able to distinguish between knowing
668+
/// when something will be destroyed (i.e. scene manager unload
669+
/// or load in single mode) or is in the middle of being
670+
/// destroyed.
671+
/// Setting the flag provides a way for other instances or internals
672+
/// to determine if this <see cref="NetworkBehaviour"/> instance is
673+
/// in the middle of being destroyed.
674+
/// </remarks>
675+
internal void SetIsDestroying()
676+
{
677+
// We intentionally invoke this before setting the IsDestroying flag.
678+
OnIsDestroying();
679+
IsDestroying = true;
680+
}
681+
646682
/// <summary>
647683
/// Updates properties with network session related
648684
/// dependencies such as a NetworkObject's spawned

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

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1688,8 +1688,52 @@ public static void NetworkHide(List<NetworkObject> networkObjects, ulong clientI
16881688
}
16891689
}
16901690

1691+
/// <summary>
1692+
/// Returns true if the NetworkObject is in the middle of being destroyed.
1693+
/// </summary>
1694+
/// <remarks>
1695+
/// This is particularly useful when determining if something is being de-spawned
1696+
/// normally or if it is being de-spawned because the NetworkObject/GameObject is
1697+
/// being destroyed.
1698+
/// </remarks>
1699+
internal bool IsDestroying { get; private set; }
1700+
1701+
/// <summary>
1702+
/// Applies the despawning flag for the local instance and
1703+
/// its child NetworkBehaviours. Private to assure this is
1704+
/// only invoked from within OnDestroy.
1705+
/// </summary>
1706+
internal void SetIsDestroying()
1707+
{
1708+
if (IsDestroying)
1709+
{
1710+
return;
1711+
}
1712+
1713+
if (m_ChildNetworkBehaviours != null)
1714+
{
1715+
foreach (var childBehaviour in m_ChildNetworkBehaviours)
1716+
{
1717+
// Just ignore and continue processing through the entries
1718+
if (!childBehaviour)
1719+
{
1720+
continue;
1721+
}
1722+
1723+
// Keeping the property a private set to assure this is
1724+
// the only way it can be set as it should never be reset
1725+
// back to false once invoked.
1726+
childBehaviour.SetIsDestroying();
1727+
}
1728+
}
1729+
IsDestroying = true;
1730+
}
1731+
16911732
private void OnDestroy()
16921733
{
1734+
// Apply the is destroying flag
1735+
SetIsDestroying();
1736+
16931737
var networkManager = NetworkManager;
16941738
// If no NetworkManager is assigned, then just exit early
16951739
if (!networkManager)

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,10 +318,19 @@ public void MoveObjectsFromSceneToDontDestroyOnLoad(ref NetworkManager networkMa
318318
}
319319
else if (networkObject.HasAuthority)
320320
{
321+
// We know this instance is going to be destroyed (when it receives the destroy object message).
322+
// We have to invoke this prior to invoking despawn in order to know that we are de-spawning in
323+
// preparation of being destroyed by the SceneManager.
324+
networkObject.SetIsDestroying();
325+
// This sends a de-spawn message prior to the scene event.
321326
networkObject.Despawn();
322327
}
323328
else // We are a client, migrate the object into the DDOL temporarily until it receives the destroy command from the server
324329
{
330+
// We know this instance is going to be destroyed (when it receives the destroy object message).
331+
// We have to invoke this prior to invoking despawn in order to know that we are de-spawning in
332+
// preparation of being destroyed by the SceneManager.
333+
networkObject.SetIsDestroying();
325334
UnityEngine.Object.DontDestroyOnLoad(networkObject.gameObject);
326335
}
327336
}

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2720,7 +2720,11 @@ internal void MoveObjectsToDontDestroyOnLoad()
27202720
}
27212721
else if (networkObject.HasAuthority)
27222722
{
2723-
networkObject.Despawn();
2723+
networkObject.SetIsDestroying();
2724+
var isSceneObject = networkObject.IsSceneObject;
2725+
// Only destroy non-scene placed NetworkObjects to avoid warnings about destroying in-scene placed NetworkObjects.
2726+
// (MoveObjectsToDontDestroyOnLoad is only invoked during a scene event type of load and the load scene mode is single)
2727+
networkObject.Despawn(isSceneObject.HasValue && isSceneObject.Value == false);
27242728
}
27252729
}
27262730
}

0 commit comments

Comments
 (0)