Skip to content

Commit ffece95

Browse files
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.
1 parent 9e4f0e5 commit ffece95

File tree

8 files changed

+123
-36
lines changed

8 files changed

+123
-36
lines changed

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

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -261,18 +261,17 @@ 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/>
@@ -312,16 +311,18 @@ private void UpdateAttachedState()
312311
return;
313312
}
314313

315-
// 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.
316316
if (isAttaching && m_AttachableNode != null && m_AttachState == AttachState.Attached)
317317
{
318-
// Run through the same process without being triggerd by a NetVar update.
318+
// Detach the current attachable
319319
NotifyAttachedStateChanged(AttachState.Detaching, m_AttachableNode);
320320
InternalDetach();
321321
NotifyAttachedStateChanged(AttachState.Detached, m_AttachableNode);
322-
323322
m_AttachableNode.Detach(this);
324323
m_AttachableNode = null;
324+
325+
// Now attach the new attachable
325326
}
326327

327328
// Change the state to attaching or detaching
@@ -466,7 +467,7 @@ public void Attach(AttachableNode attachableNode)
466467
/// </summary>
467468
internal void InternalDetach()
468469
{
469-
if (!IsDestroying && m_AttachableNode && !m_AttachableNode.IsDestroying)
470+
if (!IsDestroying && m_AttachableNode && (!m_AttachableNode.IsDestroying || m_AttachableNode.gameObject.scene.isLoaded))
470471
{
471472
if (m_DefaultParent)
472473
{
@@ -562,12 +563,33 @@ private void UpdateAttachStateRpc(NetworkBehaviourReference attachedNodeReferenc
562563
/// </summary>
563564
internal void OnAttachNodeDestroy()
564565
{
565-
// If this instance should force a detach on destroy
566-
if (AutoDetach.HasFlag(AutoDetachTypes.OnAttachNodeDestroy))
566+
// We force a detach on destroy if there is a flag =or= if we are attached to a node that is being destroyed.
567+
if (AutoDetach.HasFlag(AutoDetachTypes.OnAttachNodeDestroy) ||
568+
(AutoDetach.HasFlag(AutoDetachTypes.OnDespawn) && m_AttachState == AttachState.Attached && m_AttachableNode && m_AttachableNode.IsDestroying))
569+
{
570+
ForceDetach();
571+
}
572+
}
573+
574+
575+
/// <summary>
576+
/// When we know this instance is being destroyed or will be destroyed
577+
/// by something outside of NGO's realm of control, this gets invoked.
578+
/// We should detach from any AttachableNode when this is invoked.
579+
/// </summary>
580+
protected internal override void OnIsDestroying()
581+
{
582+
// If we are not already marked as being destroyed, attached, this instance is the authority instance, and the node we are attached
583+
// to is not in the middle of being destroyed...detach normally.
584+
if (!IsDestroying && HasAuthority && m_AttachState == AttachState.Attached && m_AttachableNode && !m_AttachableNode.IsDestroying)
585+
{
586+
Detach();
587+
}
588+
else // Otherwise force the detach.
567589
{
568-
// Force a detach
569590
ForceDetach();
570591
}
592+
base.OnIsDestroying();
571593
}
572594
}
573595
}

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

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -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: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -652,8 +652,31 @@ protected NetworkBehaviour GetNetworkBehaviour(ushort behaviourId)
652652
/// </remarks>
653653
internal bool IsDestroying { get; private set; }
654654

655-
internal void SetDestroying()
655+
/// <summary>
656+
/// This provides us with a way to track when something is in the middle
657+
/// of being destroyed or will be destroyed by something like SceneManager.
658+
/// root <see cref="GameObject"/> is
659+
/// </summary>
660+
protected internal virtual void OnIsDestroying()
661+
{
662+
}
663+
664+
/// <summary>
665+
/// Invoked by <see cref="NetworkObject.SetIsDestroying"/>.
666+
/// </summary>
667+
/// <remarks>
668+
/// We want to invoke the virtual method prior to setting the
669+
/// IsDestroying flag to be able to distinguish between knowing
670+
/// when something will be destroyed (i.e. scene manager unload
671+
/// or load in single mode) or is in the middle of being
672+
/// destroyed.
673+
/// Setting the flag provides a way for other instances or internals
674+
/// to determine if this <see cref="NetworkBehaviour"/> instance is
675+
/// in the middle of being destroyed.
676+
/// </remarks>
677+
internal void SetIsDestroying()
656678
{
679+
OnIsDestroying();
657680
IsDestroying = true;
658681
}
659682

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

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1703,29 +1703,30 @@ public static void NetworkHide(List<NetworkObject> networkObjects, ulong clientI
17031703
/// its child NetworkBehaviours. Private to assure this is
17041704
/// only invoked from within OnDestroy.
17051705
/// </summary>
1706-
private void SetIsDestroying()
1706+
internal void SetIsDestroying()
17071707
{
1708-
IsDestroying = true;
1709-
1710-
// Exit early if null
1711-
if (m_ChildNetworkBehaviours == null)
1708+
if (IsDestroying)
17121709
{
17131710
return;
17141711
}
17151712

1716-
foreach (var childBehaviour in m_ChildNetworkBehaviours)
1713+
if (m_ChildNetworkBehaviours != null)
17171714
{
1718-
// Just ignore and continue processing through the entries
1719-
if (!childBehaviour)
1715+
foreach (var childBehaviour in m_ChildNetworkBehaviours)
17201716
{
1721-
continue;
1722-
}
1717+
// Just ignore and continue processing through the entries
1718+
if (!childBehaviour)
1719+
{
1720+
continue;
1721+
}
17231722

1724-
// Keeping the property a private set to assure this is
1725-
// the only way it can be set as it should never be reset
1726-
// back to false once invoked.
1727-
childBehaviour.SetDestroying();
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+
}
17281728
}
1729+
IsDestroying = true;
17291730
}
17301731

17311732
private void OnDestroy()

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/Tests/Runtime/AttachableBehaviourTests.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -653,9 +653,11 @@ public IEnumerator AutoDetachTests([Values] DetachCombinations detachCombination
653653
attachable.AutoDetach = AttachableBehaviour.AutoDetachTypes.OnAttachNodeDestroy;
654654
}
655655
var attachableNodeName = m_AttachableNodeInstance.name;
656+
var attachableBehaviourName = m_AttachableBehaviourInstance.name;
657+
656658
Object.Destroy(m_TargetInstance.gameObject);
657659
yield return WaitForConditionOrTimeOut(AllInstancesDetached);
658-
AssertOnTimeout($"[OnAttachNodeDestroy] Timed out waiting for all clients to detach {m_AttachableBehaviourInstance.name} from {attachableNodeName}!\n {m_ErrorLog}");
660+
AssertOnTimeout($"[OnAttachNodeDestroy] Timed out waiting for all clients to detach {attachableBehaviourName} from {attachableNodeName}!\n {m_ErrorLog}");
659661
}
660662
}
661663

com.unity.netcode.gameobjects/Tests/Runtime/TestHelpers/IntegrationTestSceneHandler.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -761,17 +761,29 @@ public void MoveObjectsFromSceneToDontDestroyOnLoad(ref NetworkManager networkMa
761761
if (networkObject.HasAuthority && networkObject.NetworkManager == networkManager)
762762
{
763763
VerboseDebug($"[MoveObjects from {scene.name} | {scene.handle}] Destroying {networkObject.gameObject.name} because it is in scene {networkObject.gameObject.scene.name} with DWS = {networkObject.DestroyWithScene}.");
764+
// We know this instance is going to be destroyed (when it receives the destroy object message).
765+
// We have to invoke this prior to invoking despawn in order to know that we are de-spawning in
766+
// preparation of being destroyed by the SceneManager.
767+
networkObject.SetIsDestroying();
764768
networkObject.Despawn();
765769
}
766770
else //For integration testing purposes, migrate remaining into DDOL
767771
{
768772
if (networkObject.DestroyPendingSceneEvent)
769773
{
774+
// We know this instance is going to be destroyed (for integration testing we simulate the destroy).
775+
// We have to invoke this prior to invoking despawn in order to know that we are de-spawning in
776+
// preparation of being destroyed by the SceneManager.
777+
networkObject.SetIsDestroying();
770778
Object.Destroy(networkObject.gameObject);
771779
}
772780
else
773781
{
774782
VerboseDebug($"[MoveObjects from {scene.name} | {scene.handle}] Temporarily migrating {networkObject.gameObject.name} into DDOL to await server destroy message.");
783+
// We know this instance is going to be destroyed (when it receives the destroy object message).
784+
// We have to invoke this prior to invoking despawn in order to know that we are de-spawning in
785+
// preparation of being destroyed by the SceneManager.
786+
networkObject.SetIsDestroying();
775787
Object.DontDestroyOnLoad(networkObject.gameObject);
776788
}
777789
}

testproject/Assets/Tests/Runtime/NetworkSceneManager/AttachableBehaviourSceneLoadTests.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,7 @@ public IEnumerator AttachedUponSceneTransition([Values] bool detachOnDespawn)
284284
Assert.IsTrue(response == SceneEventProgressStatus.Started, $"Failed to begin scene loading event for {k_SceneToLoad} with a status of {response}!");
285285
yield return WaitForConditionOrTimeOut(() => m_AuthoritySceneLoaded.IsValid() && m_AuthoritySceneLoaded.isLoaded && m_SceneLoadCompleted);
286286
AssertOnTimeout($"Timed out waiting for all clients to load scene {k_SceneToLoad}!");
287-
287+
var persistedObjects = new Dictionary<NetworkManager, ulong>();
288288
// Now, make the newly loaded scene the currently active scene so everything instantiates in the scene authority's newly loaded scene instance.
289289
SceneManager.SetActiveScene(m_AuthoritySceneLoaded);
290290
foreach (var networkManager in m_NetworkManagers)
@@ -315,7 +315,10 @@ public IEnumerator AttachedUponSceneTransition([Values] bool detachOnDespawn)
315315

316316
// Keep track of the spawned instances we expect to not persist a scene load
317317
var doesNotPersist = m_Persists == Persists.AttachableNode ? m_SourceInstance.NetworkObjectId : m_TargetInstance.NetworkObjectId;
318+
var persists = m_Persists == Persists.AttachableNode ? m_TargetInstance.NetworkObjectId : m_SourceInstance.NetworkObjectId;
318319
m_DoesNotPersistNetworkObjectIds.Add(doesNotPersist);
320+
persistedObjects.Add(networkManager, persists);
321+
Debug.Log($"[{networkManager.name}] Spawned attachable and attached it.");
319322
}
320323

321324
// This is the actual validation point where the scene is unloaded and either the attachable or

0 commit comments

Comments
 (0)