Skip to content

fix: OnNetworkDespawn not called during shutdown #1390

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 26 commits into from
Nov 16, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
09a47b8
fix
NoelStephensUnity Nov 3, 2021
d4250af
fix
NoelStephensUnity Nov 3, 2021
c3f1e46
fix
NoelStephensUnity Nov 3, 2021
76cabca
fix
NoelStephensUnity Nov 3, 2021
09a2192
fix
NoelStephensUnity Nov 3, 2021
32009fa
test fix
NoelStephensUnity Nov 3, 2021
616dd96
Merge branch 'develop' into fix/OnNetworkDespawn-Not-Called-During-Sh…
NoelStephensUnity Nov 3, 2021
d7c69b9
fix
NoelStephensUnity Nov 3, 2021
6649ece
revert and fix
NoelStephensUnity Nov 3, 2021
f978896
test
NoelStephensUnity Nov 3, 2021
e674948
update changelog
NoelStephensUnity Nov 3, 2021
c857ae6
Merge branch 'develop' into fix/OnNetworkDespawn-Not-Called-During-Sh…
NoelStephensUnity Nov 8, 2021
8f0ed76
Merge branch 'develop' into fix/OnNetworkDespawn-Not-Called-During-Sh…
NoelStephensUnity Nov 8, 2021
f6eec53
Merge branch 'develop' into fix/OnNetworkDespawn-Not-Called-During-Sh…
NoelStephensUnity Nov 8, 2021
70045bc
Merge branch 'develop' into fix/OnNetworkDespawn-Not-Called-During-Sh…
NoelStephensUnity Nov 9, 2021
c9c6450
Merge branch 'develop' into fix/OnNetworkDespawn-Not-Called-During-Sh…
NoelStephensUnity Nov 10, 2021
419cd29
Merge develop into fix/OnNetworkDespawn-Not-Called-During-Shutdown
github-actions[bot] Nov 10, 2021
c87ad9d
Merge branch 'develop' into fix/OnNetworkDespawn-Not-Called-During-Sh…
andrews-unity Nov 10, 2021
292cd91
Merge branch 'develop' into fix/OnNetworkDespawn-Not-Called-During-Sh…
andrews-unity Nov 10, 2021
03d5c10
Merge branch 'develop' into fix/OnNetworkDespawn-Not-Called-During-Sh…
NoelStephensUnity Nov 10, 2021
7fa748e
Merge branch 'develop' into fix/OnNetworkDespawn-Not-Called-During-Sh…
NoelStephensUnity Nov 15, 2021
d538c1c
fix
NoelStephensUnity Nov 15, 2021
51698d9
refactor
NoelStephensUnity Nov 15, 2021
e2ca11a
style
NoelStephensUnity Nov 15, 2021
36f7d95
Revert "style"
NoelStephensUnity Nov 15, 2021
1b22d1e
fix
NoelStephensUnity Nov 15, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions com.unity.netcode.gameobjects/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ Additional documentation and release notes are available at [Multiplayer Documen
- NetworkConfig will no longer throw an OverflowException in GetConfig() when ForceSamePrefabs is enabled and the number of prefabs causes the config blob size to exceed 1300 bytes. (#1385)
- Fixed NetworkVariable not calling NetworkSerialize on INetworkSerializable types (#1383)

- Fixed NetworkObjects not being despawned before they are destroyed during shutdown for client, host, and server instances.

### Changed

- ServerRpcParams and ClientRpcParams must be the last parameter of an RPC in order to function properly. Added a compile-time check to ensure this is the case and trigger an error if they're placed elsewhere. (#1318)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ private void Awake()

private void FixedUpdate()
{
if (NetworkManager.IsListening)
if (IsSpawned)
{
if (HasAuthority != m_IsAuthority)
{
Expand Down Expand Up @@ -74,7 +74,6 @@ public override void OnNetworkSpawn()
/// <inheritdoc />
public override void OnNetworkDespawn()
{
m_IsAuthority = false;
UpdateRigidbodyKinematicMode();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -807,7 +807,7 @@ private void SetStateServerRpc(Vector3 pos, Quaternion rot, Vector3 scale, bool
// conditional to users only making transform update changes in FixedUpdate.
protected virtual void Update()
{
if (!NetworkObject.IsSpawned)
if (!IsSpawned)
{
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -282,9 +282,13 @@ public NetworkObject NetworkObject
m_NetworkObject = GetComponentInParent<NetworkObject>();
}

if (m_NetworkObject == null && NetworkLog.CurrentLogLevel <= LogLevel.Normal)
if (m_NetworkObject == null || NetworkManager.Singleton == null ||
(NetworkManager.Singleton != null && !NetworkManager.Singleton.ShutdownInProgress))
Comment on lines +285 to +286
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NetworkManager.Singleton check should be another if-check with potentially another log message IMO.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will revisit this.

{
NetworkLog.LogWarning($"Could not get {nameof(NetworkObject)} for the {nameof(NetworkBehaviour)}. Are you missing a {nameof(NetworkObject)} component?");
if (NetworkLog.CurrentLogLevel < LogLevel.Normal)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this area has a high probability to generate a lot of spam, I reduced it to require the NetworkLog level be Developer as there can be several frames (during Update and FixedUpdate) where NetworkBehaviours will make checks that require a reference to the NetworkObject but it doesn't exist.

The question here is do we want to generate a warning here since NetworkObject can be null and should be valid to return a null value for m_NetworkObject.

{
NetworkLog.LogWarning($"Could not get {nameof(NetworkObject)} for the {nameof(NetworkBehaviour)}. Are you missing a {nameof(NetworkObject)} component?");
}
}

return m_NetworkObject;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ public NetworkPrefabHandler PrefabHandler
}

private bool m_ShuttingDown;

private bool m_StopProcessingMessages;

private class NetworkManagerHooks : INetworkHooks
Expand Down Expand Up @@ -336,6 +337,9 @@ public IReadOnlyList<ulong> ConnectedClientsIds
/// </summary>
public bool IsConnectedClient { get; internal set; }


public bool ShutdownInProgress { get { return m_ShuttingDown; } }

/// <summary>
/// The callback to invoke once a client connects. This callback is only ran on the server and on the local client that connects.
/// </summary>
Expand Down Expand Up @@ -1098,7 +1102,7 @@ internal void ShutdownInternal()
if (SpawnManager != null)
{
SpawnManager.CleanupAllTriggers();
SpawnManager.DestroyNonSceneObjects();
SpawnManager.DespawnAndDestroyNetworkObjects();
SpawnManager.ServerResetShudownStateForSceneObjects();

SpawnManager = null;
Expand Down Expand Up @@ -1133,6 +1137,7 @@ internal void ShutdownInternal()
m_TransportIdToClientIdMap.Clear();

IsListening = false;
m_ShuttingDown = false;
}

// INetworkUpdateSystem
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ private bool TryGetNetworkClient(ulong clientId, out NetworkClient networkClient
return NetworkManager.ConnectedClients.TryGetValue(clientId, out networkClient);
}

if (clientId == NetworkManager.LocalClient.ClientId)
if (NetworkManager.LocalClient != null && clientId == NetworkManager.LocalClient.ClientId)
{
networkClient = NetworkManager.LocalClient;
return true;
Expand Down Expand Up @@ -598,25 +598,33 @@ internal void ServerDestroySpawnedSceneObjects()
}
}

internal void DestroyNonSceneObjects()
internal void DespawnAndDestroyNetworkObjects()
{
var networkObjects = UnityEngine.Object.FindObjectsOfType<NetworkObject>();

for (int i = 0; i < networkObjects.Length; i++)
{
if (networkObjects[i].NetworkManager == NetworkManager)
{
if (networkObjects[i].IsSceneObject != null && networkObjects[i].IsSceneObject.Value == false)
if (NetworkManager.PrefabHandler.ContainsHandler(networkObjects[i]))
{
if (NetworkManager.PrefabHandler.ContainsHandler(networkObjects[i]))
{
NetworkManager.PrefabHandler.HandleNetworkPrefabDestroy(networkObjects[i]);
OnDespawnObject(networkObjects[i], false);
}
else
{
UnityEngine.Object.Destroy(networkObjects[i].gameObject);
}
OnDespawnObject(networkObjects[i], false);
// Leave destruction up to the handler
NetworkManager.PrefabHandler.HandleNetworkPrefabDestroy(networkObjects[i]);
}
else if (networkObjects[i].IsSpawned)
{
// If it is an in-scene placed NetworkObject then just despawn
// and let it be destroyed when the scene is unloaded. Otherwise,
// despawn and destroy it.
var shouldDestroy = !(networkObjects[i].IsSceneObject != null
&& networkObjects[i].IsSceneObject.Value);

OnDespawnObject(networkObjects[i], shouldDestroy);
}
else
{
UnityEngine.Object.Destroy(networkObjects[i].gameObject);
}
}
}
Expand Down Expand Up @@ -686,17 +694,21 @@ internal void OnDespawnObject(NetworkObject networkObject, bool destroyGameObjec
return;
}

// Move child NetworkObjects to the root when parent NetworkObject is destroyed
foreach (var spawnedNetObj in SpawnedObjectsList)
// If we are shutting down the NetworkManager, then ignore resetting the parent
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MFatihMAR
This was my solution to handle the issue where changing the parent would cause NetworkObject.OnTransformParentChanged to log an exception because NetworkManager is null during shutdown when the NetworkObjects are being despawned. Adding an additional series of checks in NetworkObject.OnTransformParentChanged seemed less-than-optimal since this is specific to only despawning.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (NetworkManager == null || !NetworkManager.IsListening)
{
    transform.parent = m_CachedParent;
    Debug.LogException(new NotListeningException($"{nameof(NetworkManager)} is not listening, start a server or host before reparenting"));
    return;
}

is this the check you're referring to?

if so, we could split that logic up into 2 pieces: NetworkManager == null and NetworkManager.IsListening.
we'd return early if NetworkManager == null
we'd log exception if !NetworkManager.IsListening

also, I think we should not introduce the NetworkManager.ShutdownInProgress (see my other comment above) and also not inject this extra check here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is just below:

if (!NetworkManager.ShutdownInProgress)
{

Basically, if we a shutting down then ignore the parenting code...

if (!NetworkManager.ShutdownInProgress)
{
var (isReparented, latestParent) = spawnedNetObj.GetNetworkParenting();
if (isReparented && latestParent == networkObject.NetworkObjectId)
// Move child NetworkObjects to the root when parent NetworkObject is destroyed
foreach (var spawnedNetObj in SpawnedObjectsList)
{
spawnedNetObj.gameObject.transform.parent = null;

if (NetworkLog.CurrentLogLevel <= LogLevel.Normal)
var (isReparented, latestParent) = spawnedNetObj.GetNetworkParenting();
if (isReparented && latestParent == networkObject.NetworkObjectId)
{
NetworkLog.LogWarning($"{nameof(NetworkObject)} #{spawnedNetObj.NetworkObjectId} moved to the root because its parent {nameof(NetworkObject)} #{networkObject.NetworkObjectId} is destroyed");
spawnedNetObj.gameObject.transform.parent = null;

if (NetworkLog.CurrentLogLevel <= LogLevel.Normal)
{
NetworkLog.LogWarning($"{nameof(NetworkObject)} #{spawnedNetObj.NetworkObjectId} moved to the root because its parent {nameof(NetworkObject)} #{networkObject.NetworkObjectId} is destroyed");
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
using System.Collections;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;


namespace Unity.Netcode.RuntimeTests
{
/// <summary>
/// Tests that check OnNetworkDespawn being invoked
/// </summary>
public class NetworkObjectOnNetworkDespawnTests
{
private NetworkManager m_ServerHost;
private NetworkManager[] m_Clients;

private GameObject m_ObjectToSpawn;
private NetworkObject m_NetworkObject;

internal class OnNetworkDespawnTestComponent : NetworkBehaviour
{
public bool OnNetworkDespawnCalled { get; internal set; }

public override void OnNetworkSpawn()
{
OnNetworkDespawnCalled = false;
base.OnNetworkSpawn();
}

public override void OnNetworkDespawn()
{
OnNetworkDespawnCalled = true;
base.OnNetworkDespawn();
}
}

[UnitySetUp]
public IEnumerator Setup()
{
Assert.IsTrue(MultiInstanceHelpers.Create(1, out m_ServerHost, out m_Clients));

m_ObjectToSpawn = new GameObject();
m_NetworkObject = m_ObjectToSpawn.AddComponent<NetworkObject>();
m_ObjectToSpawn.AddComponent<OnNetworkDespawnTestComponent>();

// Make it a prefab
MultiInstanceHelpers.MakeNetworkObjectTestPrefab(m_NetworkObject);

var networkPrefab = new NetworkPrefab();
networkPrefab.Prefab = m_ObjectToSpawn;
m_ServerHost.NetworkConfig.NetworkPrefabs.Add(networkPrefab);

foreach (var client in m_Clients)
{
client.NetworkConfig.NetworkPrefabs.Add(networkPrefab);
}

yield return null;
}

[UnityTearDown]
public IEnumerator Teardown()
{
// Shutdown and clean up both of our NetworkManager instances
if (m_ObjectToSpawn)
{
Object.Destroy(m_ObjectToSpawn);
m_ObjectToSpawn = null;
}
MultiInstanceHelpers.Destroy();
yield return null;
}

public enum InstanceType
{
Server,
Host,
Client
}

/// <summary>
/// Tests that a spawned NetworkObject's associated NetworkBehaviours will have
/// their OnNetworkDespawn invoked during NetworkManager shutdown.
/// </summary>
[UnityTest]
public IEnumerator TestNetworkObjectDespawnOnShutdown([Values(InstanceType.Server, InstanceType.Host, InstanceType.Client)] InstanceType despawnCheck)
{
var useHost = despawnCheck == InstanceType.Server ? false : true;
var networkManager = despawnCheck == InstanceType.Host || despawnCheck == InstanceType.Server ? m_ServerHost : m_Clients[0];

// Start the instances
if (!MultiInstanceHelpers.Start(useHost, m_ServerHost, m_Clients))
{
Debug.LogError("Failed to start instances");
Assert.Fail("Failed to start instances");
}

// [Client-Side] Wait for a connection to the server
yield return MultiInstanceHelpers.WaitForClientsConnected(m_Clients, null, 512);

// [Host-Server-Side] Check to make sure all clients are connected
var clientCount = useHost ? m_Clients.Length + 1 : m_Clients.Length;
yield return MultiInstanceHelpers.WaitForClientsConnectedToServer(m_ServerHost, clientCount, null, 512);

// Spawn the test object
var spawnedObject = Object.Instantiate(m_NetworkObject);
var spawnedNetworkObject = spawnedObject.GetComponent<NetworkObject>();
spawnedNetworkObject.NetworkManagerOwner = m_ServerHost;
spawnedNetworkObject.Spawn(true);

// Get the spawned object relative to which NetworkManager instance we are testing.
var relativeSpawnedObject = new MultiInstanceHelpers.CoroutineResultWrapper<NetworkObject>();
yield return MultiInstanceHelpers.Run(MultiInstanceHelpers.GetNetworkObjectByRepresentation((x => x.GetComponent<OnNetworkDespawnTestComponent>() != null), networkManager, relativeSpawnedObject));
var onNetworkDespawnTestComponent = relativeSpawnedObject.Result.GetComponent<OnNetworkDespawnTestComponent>();

// Confirm it is not set before shutting down the NetworkManager
Assert.IsFalse(onNetworkDespawnTestComponent.OnNetworkDespawnCalled);

// Shutdown the NetworkManager instance we are testing.
networkManager.Shutdown();

// Since shutdown is now delayed until the post frame update
// just wait 2 frames before checking to see if OnNetworkDespawnCalled is true
var currentFrame = Time.frameCount + 2;
yield return new WaitUntil(() => Time.frameCount <= currentFrame);

// Confirm that OnNetworkDespawn is invoked after shutdown
Assert.IsTrue(onNetworkDespawnTestComponent.OnNetworkDespawnCalled);
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ public IEnumerator TestRigidbodyKinematicEnableDisable()

yield return WaitForTicks(m_ServerNetworkManager, 5);

// This should equal Kinematic
Assert.IsTrue(serverPlayer.GetComponent<Rigidbody2D>().isKinematic == Kinematic);

yield return WaitForTicks(m_ServerNetworkManager, 5);
Expand Down