Skip to content

Commit f966e07

Browse files
fix: DestroyWithScene causes errors on non-authority side (#3500)
### Description This PR fixes the issue where enabling the `NetworkObject.DestroyWithScene` would cause errors on non-authority instances when loading a new scene (in single mode) or unloading the scene that the `NetworkObject` instance currently resides and using the distributed authority network topology. Fix: #3144 [MTTB-1384](https://jira.unity3d.com/browse/MTTB-1384) ## Changelog - Fixed: Distributed authority related issue where enabling the `NetworkObject.DestroyWithScene` would cause errors when a destroying non-authority instances due to loading (single mode) or unloading scene events. ## Testing and Documentation - Includes new integration test: `NetworkObjectDestroyWithSceneTests`. - No documentation changes or additions were necessary. <!-- Uncomment and mark items off with a * if this PR deprecates any API: ### Deprecated API - [ ] An `[Obsolete]` attribute was added along with a `(RemovedAfter yyyy-mm-dd)` entry. - [ ] An [api updater] was added. - [ ] Deprecation of the API is explained in the CHANGELOG. - [ ] The users can understand why this API was removed and what they should use instead. --> ## Backport This is a v2.x distributed authority only issue. <!-- If this is a backport: - Add the following to the PR title: "\[Backport\] ..." . - Link to the original PR. If this needs a backport - state this here If a backport is not needed please provide the reason why. If the "Backports" section is not present it will lead to a CI test failure. -->
1 parent 22f9d6f commit f966e07

File tree

8 files changed

+335
-78
lines changed

8 files changed

+335
-78
lines changed

com.unity.netcode.gameobjects/CHANGELOG.md

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

1515
### Fixed
1616

17+
- Fixed distributed authority related issue where enabling the `NetworkObject.DestroyWithScene` would cause errors when a destroying non-authority instances due to loading (single mode) or unloading scene events. (#3500)
1718

1819
### Changed
1920

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1200,6 +1200,7 @@ public void SetSceneObjectStatus(bool isSceneObject = false)
12001200
/// Gets whether or not the object should be automatically removed when the scene is unloaded.
12011201
/// </summary>
12021202
public bool DestroyWithScene { get; set; }
1203+
internal bool DestroyPendingSceneEvent;
12031204

12041205
/// <summary>
12051206
/// When set to true and the active scene is changed, this will automatically migrate the <see cref="NetworkObject"/>
@@ -1720,10 +1721,11 @@ private void OnDestroy()
17201721
return;
17211722
}
17221723

1723-
// Authority is the server (client-server) and the owner or DAHost (distributed authority) when destroying a NetworkObject
1724-
var isAuthority = HasAuthority || NetworkManager.DAHost;
1724+
// An authorized destroy is when done by the authority instance or done due to a scene event and the NetworkObject
1725+
// was marked as destroy pending scene event (which means the destroy with scene property was set).
1726+
var isAuthorityDestroy = HasAuthority || NetworkManager.DAHost || DestroyPendingSceneEvent;
17251727

1726-
if (NetworkManager.IsListening && !isAuthority && IsSpawned &&
1728+
if (NetworkManager.IsListening && !isAuthorityDestroy && IsSpawned &&
17271729
(IsSceneObject == null || (IsSceneObject.Value != true)))
17281730
{
17291731
// If we destroyed a GameObject with a NetworkObject component on the non-authority side, handle cleaning up the SceneMigrationSynchronization.

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -293,13 +293,20 @@ public void MoveObjectsFromSceneToDontDestroyOnLoad(ref NetworkManager networkMa
293293
// Create a local copy of the spawned objects list since the spawn manager will adjust the list as objects
294294
// are despawned.
295295
var localSpawnedObjectsHashSet = new HashSet<NetworkObject>(networkManager.SpawnManager.SpawnedObjectsList);
296+
var distributedAuthority = networkManager.DistributedAuthorityMode;
296297
foreach (var networkObject in localSpawnedObjectsHashSet)
297298
{
298299
if (networkObject == null || (networkObject != null && networkObject.gameObject.scene.handle != scene.handle))
299300
{
300301
continue;
301302
}
302303

304+
// Check to determine if we need to allow destroying a non-authority instance
305+
if (distributedAuthority && networkObject.DestroyWithScene && !networkObject.HasAuthority)
306+
{
307+
networkObject.DestroyPendingSceneEvent = true;
308+
}
309+
303310
// Only NetworkObjects marked to not be destroyed with the scene and are not already in the DDOL are preserved
304311
if (!networkObject.DestroyWithScene && networkObject.gameObject.scene != networkManager.SceneManager.DontDestroyOnLoadScene)
305312
{
@@ -309,7 +316,7 @@ public void MoveObjectsFromSceneToDontDestroyOnLoad(ref NetworkManager networkMa
309316
UnityEngine.Object.DontDestroyOnLoad(networkObject.gameObject);
310317
}
311318
}
312-
else if (networkManager.IsServer)
319+
else if (networkObject.HasAuthority)
313320
{
314321
networkObject.Despawn();
315322
}

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

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1062,7 +1062,8 @@ internal NetworkObject GetSceneRelativeInSceneNetworkObject(uint globalObjectIdH
10621062
/// <param name="targetClientIds">array of client identifiers to receive the scene event message</param>
10631063
private void SendSceneEventData(uint sceneEventId, ulong[] targetClientIds)
10641064
{
1065-
if (targetClientIds.Length == 0 && !NetworkManager.DistributedAuthorityMode)
1065+
var distributedAuthority = NetworkManager.DistributedAuthorityMode;
1066+
if (targetClientIds.Length == 0 && !distributedAuthority)
10661067
{
10671068
// This would be the Host/Server with no clients connected
10681069
// Silently return as there is nothing to be done
@@ -1072,7 +1073,7 @@ private void SendSceneEventData(uint sceneEventId, ulong[] targetClientIds)
10721073
sceneEvent.SenderClientId = NetworkManager.LocalClientId;
10731074

10741075
// Send related message to the CMB service
1075-
if (NetworkManager.DistributedAuthorityMode && NetworkManager.CMBServiceConnection && HasSceneAuthority())
1076+
if (distributedAuthority && NetworkManager.CMBServiceConnection && HasSceneAuthority())
10761077
{
10771078
sceneEvent.TargetClientId = NetworkManager.ServerClientId;
10781079
var message = new SceneEventMessage
@@ -1092,7 +1093,7 @@ private void SendSceneEventData(uint sceneEventId, ulong[] targetClientIds)
10921093
{
10931094
EventData = sceneEvent,
10941095
};
1095-
var sendTarget = NetworkManager.CMBServiceConnection ? NetworkManager.ServerClientId : clientId;
1096+
var sendTarget = distributedAuthority && !NetworkManager.DAHost ? NetworkManager.ServerClientId : clientId;
10961097
var size = NetworkManager.ConnectionManager.SendMessage(ref message, k_DeliveryType, sendTarget);
10971098
NetworkManager.NetworkMetrics.TrackSceneEventSent(clientId, (uint)sceneEvent.SceneEventType, SceneNameFromHash(sceneEvent.SceneHash), size);
10981099
}
@@ -2679,13 +2680,20 @@ internal void MoveObjectsToDontDestroyOnLoad()
26792680
// Create a local copy of the spawned objects list since the spawn manager will adjust the list as objects
26802681
// are despawned.
26812682
var localSpawnedObjectsHashSet = new HashSet<NetworkObject>(NetworkManager.SpawnManager.SpawnedObjectsList);
2683+
var distributedAuthority = NetworkManager.DistributedAuthorityMode;
26822684
foreach (var networkObject in localSpawnedObjectsHashSet)
26832685
{
26842686
if (networkObject == null || (networkObject != null && networkObject.gameObject.scene == DontDestroyOnLoadScene))
26852687
{
26862688
continue;
26872689
}
26882690

2691+
// Check to determine if we need to allow destroying a non-authority instance
2692+
if (distributedAuthority && networkObject.DestroyWithScene && !networkObject.HasAuthority)
2693+
{
2694+
networkObject.DestroyPendingSceneEvent = true;
2695+
}
2696+
26892697
// Only NetworkObjects marked to not be destroyed with the scene
26902698
if (!networkObject.DestroyWithScene)
26912699
{
Lines changed: 94 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Collections;
2+
using System.Collections.Generic;
23
using NUnit.Framework;
34
using Unity.Netcode.TestHelpers.Runtime;
45
using UnityEngine;
@@ -21,56 +22,91 @@ internal class NetworkObjectDestroyTests : NetcodeIntegrationTest
2122
{
2223
protected override int NumberOfClients => 2;
2324

24-
// TODO: [CmbServiceTests] Adapt to run with the service
25-
protected override bool UseCMBService()
25+
public class DestroyTestComponent : NetworkBehaviour
2626
{
27-
return false;
27+
public static List<string> ObjectsDestroyed = new List<string>();
28+
29+
public override void OnDestroy()
30+
{
31+
ObjectsDestroyed.Add(gameObject.name);
32+
base.OnDestroy();
33+
}
2834
}
2935

3036
public NetworkObjectDestroyTests(NetworkTopologyTypes networkTopologyType) : base(networkTopologyType) { }
3137

38+
protected override IEnumerator OnSetup()
39+
{
40+
// Re-apply the default for each test
41+
LogAssert.ignoreFailingMessages = false;
42+
DestroyTestComponent.ObjectsDestroyed.Clear();
43+
return base.OnSetup();
44+
}
45+
3246
protected override void OnCreatePlayerPrefab()
3347
{
48+
m_PlayerPrefab.AddComponent<DestroyTestComponent>();
3449
var playerNetworkObject = m_PlayerPrefab.GetComponent<NetworkObject>();
3550
playerNetworkObject.SceneMigrationSynchronization = true;
3651
base.OnCreatePlayerPrefab();
3752
}
3853

54+
private NetworkManager GetAuthorityOfNetworkObject(ulong networkObjectId)
55+
{
56+
foreach (var networkManager in m_NetworkManagers)
57+
{
58+
if (!networkManager.SpawnManager.SpawnedObjects.ContainsKey(networkObjectId))
59+
{
60+
continue;
61+
}
62+
63+
if (networkManager.SpawnManager.SpawnedObjects[networkObjectId].HasAuthority)
64+
{
65+
return networkManager;
66+
}
67+
}
68+
return null;
69+
}
70+
71+
private bool NetworkObjectDoesNotExist(ulong networkObjectId)
72+
{
73+
foreach (var networkManager in m_NetworkManagers)
74+
{
75+
if (networkManager.SpawnManager.SpawnedObjects.ContainsKey(networkObjectId))
76+
{
77+
return false;
78+
}
79+
}
80+
return true;
81+
}
82+
3983
/// <summary>
40-
/// Tests that a server can destroy a NetworkObject and that it gets despawned correctly.
84+
/// Tests that the authority NetworkManager instance of a NetworkObject is allowed to destroy it.
4185
/// </summary>
42-
/// <returns></returns>
86+
/// <returns>IEnumerator</returns>
4387
[UnityTest]
4488
public IEnumerator TestNetworkObjectAuthorityDestroy()
4589
{
46-
// This is the *SERVER VERSION* of the *CLIENT PLAYER*
47-
var serverClientPlayerResult = new NetcodeIntegrationTestHelpers.ResultWrapper<NetworkObject>();
48-
yield return NetcodeIntegrationTestHelpers.GetNetworkObjectByRepresentation(x => x.IsPlayerObject && x.OwnerClientId == m_ClientNetworkManagers[0].LocalClientId, m_ServerNetworkManager, serverClientPlayerResult);
4990

50-
// This is the *CLIENT VERSION* of the *CLIENT PLAYER*
51-
var clientClientPlayerResult = new NetcodeIntegrationTestHelpers.ResultWrapper<NetworkObject>();
52-
yield return NetcodeIntegrationTestHelpers.GetNetworkObjectByRepresentation(x => x.IsPlayerObject && x.OwnerClientId == m_ClientNetworkManagers[0].LocalClientId, m_ClientNetworkManagers[0], clientClientPlayerResult);
91+
var ownerNetworkManager = m_ClientNetworkManagers[1];
92+
var clientId = ownerNetworkManager.LocalClientId;
93+
var localClientPlayer = ownerNetworkManager.LocalClient.PlayerObject;
94+
var localNetworkObjectId = localClientPlayer.NetworkObjectId;
5395

54-
Assert.IsNotNull(serverClientPlayerResult.Result.gameObject);
55-
Assert.IsNotNull(clientClientPlayerResult.Result.gameObject);
96+
var authorityNetworkManager = GetAuthorityOfNetworkObject(localClientPlayer.NetworkObjectId);
97+
Assert.True(authorityNetworkManager != null, $"Could not find the authority of {localClientPlayer}!");
5698

57-
var targetNetworkManager = m_ClientNetworkManagers[0];
58-
if (m_DistributedAuthority)
59-
{
60-
targetNetworkManager = m_ClientNetworkManagers[1];
61-
// destroy the authoritative player (distributed authority)
62-
Object.Destroy(clientClientPlayerResult.Result.gameObject);
63-
}
64-
else
65-
{
66-
// destroy the authoritative player (client-server)
67-
Object.Destroy(serverClientPlayerResult.Result.gameObject);
68-
}
99+
var authorityPlayerClone = authorityNetworkManager.ConnectedClients[clientId].PlayerObject;
100+
101+
// Have the authority NetworkManager destroy the player instance
102+
Object.Destroy(authorityPlayerClone.gameObject);
103+
104+
var messageListener = m_DistributedAuthority ? m_ClientNetworkManagers[0] : m_ClientNetworkManagers[1];
69105

70-
yield return NetcodeIntegrationTestHelpers.WaitForMessageOfTypeHandled<DestroyObjectMessage>(targetNetworkManager);
106+
yield return NetcodeIntegrationTestHelpers.WaitForMessageOfTypeHandled<DestroyObjectMessage>(messageListener);
71107

72-
Assert.IsTrue(serverClientPlayerResult.Result == null); // Assert.IsNull doesn't work here
73-
Assert.IsTrue(clientClientPlayerResult.Result == null);
108+
yield return WaitForConditionOrTimeOut(() => NetworkObjectDoesNotExist(localNetworkObjectId));
109+
AssertOnTimeout($"Not all network managers despawned and destroyed player instance NetworkObjectId: {localNetworkObjectId}");
74110

75111
// validate that any unspawned networkobject can be destroyed
76112
var go = new GameObject();
@@ -97,62 +133,47 @@ public enum ClientDestroyObject
97133
public IEnumerator TestNetworkObjectClientDestroy([Values] ClientDestroyObject clientDestroyObject)
98134
{
99135
var isShuttingDown = clientDestroyObject == ClientDestroyObject.ShuttingDown;
100-
var clientPlayer = m_ClientNetworkManagers[0].LocalClient.PlayerObject;
101-
var clientId = clientPlayer.OwnerClientId;
102136

103-
//destroying a NetworkObject while shutting down is allowed
137+
var localNetworkManager = m_ClientNetworkManagers[1];
138+
var clientId = localNetworkManager.LocalClientId;
139+
var localClientPlayer = localNetworkManager.LocalClient.PlayerObject;
140+
141+
var nonAuthorityClient = m_ClientNetworkManagers[0];
142+
var clientPlayerClone = nonAuthorityClient.ConnectedClients[clientId].PlayerObject;
143+
104144
if (isShuttingDown)
105145
{
106-
if (m_DistributedAuthority)
107-
{
108-
// Shutdown the 2nd client
109-
m_ClientNetworkManagers[1].Shutdown();
110-
}
111-
else
112-
{
113-
// Shutdown the
114-
m_ClientNetworkManagers[0].Shutdown();
115-
}
146+
// The non-authority client is allowed to destroy any spawned object it does not
147+
// have authority over when it shuts down.
148+
nonAuthorityClient.Shutdown();
116149
}
117150
else
118151
{
152+
// The non-authority client is =NOT= allowed to destroy any spawned object it does not
153+
// have authority over during runtime.
119154
LogAssert.ignoreFailingMessages = true;
120-
NetworkLog.NetworkManagerOverride = m_ClientNetworkManagers[0];
155+
NetworkLog.NetworkManagerOverride = nonAuthorityClient;
156+
Object.Destroy(clientPlayerClone.gameObject);
121157
}
122158

123-
m_ClientPlayerName = clientPlayer.gameObject.name;
124-
m_ClientNetworkObjectId = clientPlayer.NetworkObjectId;
125-
if (m_DistributedAuthority)
126-
{
127-
m_ClientPlayerName = m_PlayerNetworkObjects[m_ClientNetworkManagers[1].LocalClientId][m_ClientNetworkManagers[0].LocalClientId].gameObject.name;
128-
m_ClientNetworkObjectId = m_PlayerNetworkObjects[m_ClientNetworkManagers[1].LocalClientId][m_ClientNetworkManagers[0].LocalClientId].NetworkObjectId;
129-
130-
if (!isShuttingDown)
131-
{
132-
NetworkLog.NetworkManagerOverride = m_ClientNetworkManagers[1];
133-
}
134-
// the 2nd client attempts to destroy the 1st client's player object (if shutting down then "ok" if not then not "ok")
135-
Object.DestroyImmediate(m_PlayerNetworkObjects[m_ClientNetworkManagers[1].LocalClientId][m_ClientNetworkManagers[0].LocalClientId].gameObject);
136-
}
137-
else
138-
{
139-
// the 1st client attempts to destroy its own player object (if shutting down then "ok" if not then not "ok")
140-
Object.DestroyImmediate(m_ClientNetworkManagers[0].LocalClient.PlayerObject.gameObject);
141-
}
159+
m_ClientPlayerName = clientPlayerClone.gameObject.name;
160+
m_ClientNetworkObjectId = clientPlayerClone.NetworkObjectId;
142161

143162
// destroying a NetworkObject while a session is active is not allowed
144163
if (!isShuttingDown)
145164
{
146165
yield return WaitForConditionOrTimeOut(HaveLogsBeenReceived);
147166
AssertOnTimeout($"Not all expected logs were received when destroying a {nameof(NetworkObject)} on the client side during an active session!");
148167
}
149-
if (m_DistributedAuthority)
150-
{
151-
Assert.IsFalse(m_ClientNetworkManagers[1].SpawnManager.NetworkObjectsToSynchronizeSceneChanges.ContainsKey(m_ClientNetworkObjectId), $"Player object {m_ClientNetworkObjectId} still exists within {nameof(NetworkSpawnManager.NetworkObjectsToSynchronizeSceneChanges)}!");
152-
}
153168
else
154169
{
155-
Assert.IsFalse(m_ClientNetworkManagers[0].SpawnManager.NetworkObjectsToSynchronizeSceneChanges.ContainsKey(m_ClientNetworkObjectId), $"Player object {m_ClientNetworkObjectId} still exists within {nameof(NetworkSpawnManager.NetworkObjectsToSynchronizeSceneChanges)}!");
170+
bool NonAuthorityClientDestroyed()
171+
{
172+
return DestroyTestComponent.ObjectsDestroyed.Contains(m_ClientPlayerName);
173+
}
174+
175+
yield return WaitForConditionOrTimeOut(NonAuthorityClientDestroyed);
176+
AssertOnTimeout($"Timed out waiting for player object {m_ClientNetworkObjectId} to no longer exist within {nameof(NetworkSpawnManager.NetworkObjectsToSynchronizeSceneChanges)}!");
156177
}
157178
}
158179

@@ -183,8 +204,14 @@ private bool HaveLogsBeenReceived()
183204
protected override IEnumerator OnTearDown()
184205
{
185206
NetworkLog.NetworkManagerOverride = null;
186-
LogAssert.ignoreFailingMessages = false;
187207
return base.OnTearDown();
188208
}
209+
210+
protected override void OnOneTimeTearDown()
211+
{
212+
// Re-apply the default as the last exiting action
213+
LogAssert.ignoreFailingMessages = false;
214+
base.OnOneTimeTearDown();
215+
}
189216
}
190217
}

0 commit comments

Comments
 (0)