diff --git a/Assets/Mirror/Runtime/NetworkBehaviour.cs b/Assets/Mirror/Runtime/NetworkBehaviour.cs index 0d9e4666f..8bfe284da 100644 --- a/Assets/Mirror/Runtime/NetworkBehaviour.cs +++ b/Assets/Mirror/Runtime/NetworkBehaviour.cs @@ -827,6 +827,9 @@ public virtual void OnStopClient() {} /// Like Start(), but only called on client and host for the local player object. public virtual void OnStartLocalPlayer() {} + /// Stop event, but only called on client and host for the local player object. + public virtual void OnStopLocalPlayer() {} + /// Like Start(), but only called for objects the client has authority over. public virtual void OnStartAuthority() {} diff --git a/Assets/Mirror/Runtime/NetworkClient.cs b/Assets/Mirror/Runtime/NetworkClient.cs index aa74c0310..a1196e56d 100644 --- a/Assets/Mirror/Runtime/NetworkClient.cs +++ b/Assets/Mirror/Runtime/NetworkClient.cs @@ -1308,6 +1308,9 @@ static void DestroyObject(uint netId) // Debug.Log($"NetworkClient.OnObjDestroy netId: {netId}"); if (spawned.TryGetValue(netId, out NetworkIdentity localObject) && localObject != null) { + if (localObject.isLocalPlayer) + localObject.OnStopLocalPlayer(); + localObject.OnStopClient(); // user handling @@ -1389,7 +1392,11 @@ public static void DestroyAllClientObjects() { if (identity != null && identity.gameObject != null) { + if (identity.isLocalPlayer) + identity.OnStopLocalPlayer(); + identity.OnStopClient(); + bool wasUnspawned = InvokeUnSpawnHandler(identity.assetId, identity.gameObject); if (!wasUnspawned) { diff --git a/Assets/Mirror/Runtime/NetworkIdentity.cs b/Assets/Mirror/Runtime/NetworkIdentity.cs index 5e3e149dd..71aa561d6 100644 --- a/Assets/Mirror/Runtime/NetworkIdentity.cs +++ b/Assets/Mirror/Runtime/NetworkIdentity.cs @@ -784,6 +784,26 @@ internal void OnStartLocalPlayer() } } + internal void OnStopLocalPlayer() + { + foreach (NetworkBehaviour comp in NetworkBehaviours) + { + // an exception in OnStopLocalPlayer should be caught, so that + // one component's exception doesn't stop all other components + // from being initialized + // => this is what Unity does for Start() etc. too. + // one exception doesn't stop all the other Start() calls! + try + { + comp.OnStopLocalPlayer(); + } + catch (Exception e) + { + Debug.LogException(e, comp); + } + } + } + bool hadAuthority; internal void NotifyAuthority() { diff --git a/Assets/Mirror/Runtime/NetworkServer.cs b/Assets/Mirror/Runtime/NetworkServer.cs index c88c2885a..215a8cfcc 100644 --- a/Assets/Mirror/Runtime/NetworkServer.cs +++ b/Assets/Mirror/Runtime/NetworkServer.cs @@ -1318,9 +1318,12 @@ static void DestroyObject(NetworkIdentity identity, DestroyMode mode) SendToObservers(identity, new ObjectDestroyMessage{netId = identity.netId}); identity.ClearObservers(); - // in host mode, call OnStopClient manually + // in host mode, call OnStopClient/OnStopLocalPlayer manually if (NetworkClient.active && localClientActive) { + if (identity.isLocalPlayer) + identity.OnStopLocalPlayer(); + identity.OnStopClient(); // The object may have been spawned with host client ownership, // e.g. a pet so we need to clear hasAuthority and call diff --git a/Assets/Mirror/Tests/Editor/NetworkBehaviourTests.cs b/Assets/Mirror/Tests/Editor/NetworkBehaviourTests.cs index 343f0c828..8c0af29e0 100644 --- a/Assets/Mirror/Tests/Editor/NetworkBehaviourTests.cs +++ b/Assets/Mirror/Tests/Editor/NetworkBehaviourTests.cs @@ -87,6 +87,13 @@ public class OnStartLocalPlayerComponent : NetworkBehaviour public override void OnStartLocalPlayer() => ++called; } + // we need to inherit from networkbehaviour to test protected functions + public class OnStopLocalPlayerComponent : NetworkBehaviour + { + public int called; + public override void OnStopLocalPlayer() => ++called; + } + public class NetworkBehaviourTests : MirrorEditModeTest { [TearDown] @@ -867,6 +874,14 @@ public void OnStartLocalPlayer() identity.OnStartLocalPlayer(); Assert.That(comp.called, Is.EqualTo(1)); } + + [Test] + public void OnStopLocalPlayer() + { + CreateNetworked(out GameObject _, out NetworkIdentity identity, out OnStopLocalPlayerComponent comp); + identity.OnStopLocalPlayer(); + Assert.That(comp.called, Is.EqualTo(1)); + } } // we need to inherit from networkbehaviour to test protected functions diff --git a/Assets/Mirror/Tests/Editor/NetworkIdentityTests.cs b/Assets/Mirror/Tests/Editor/NetworkIdentityTests.cs index d0b8dba43..1863fa36a 100644 --- a/Assets/Mirror/Tests/Editor/NetworkIdentityTests.cs +++ b/Assets/Mirror/Tests/Editor/NetworkIdentityTests.cs @@ -80,7 +80,7 @@ class StartLocalPlayerCalledNetworkBehaviour : NetworkBehaviour public override void OnStartLocalPlayer() => ++called; } - class NetworkDestroyExceptionNetworkBehaviour : NetworkBehaviour + class StopClientExceptionNetworkBehaviour : NetworkBehaviour { public int called; public override void OnStopClient() @@ -90,12 +90,28 @@ public override void OnStopClient() } } - class NetworkDestroyCalledNetworkBehaviour : NetworkBehaviour + class StopClientCalledNetworkBehaviour : NetworkBehaviour { public int called; public override void OnStopClient() => ++called; } + class StopLocalPlayerCalledNetworkBehaviour : NetworkBehaviour + { + public int called; + public override void OnStopLocalPlayer() => ++called; + } + + class StopLocalPlayerExceptionNetworkBehaviour : NetworkBehaviour + { + public int called; + public override void OnStopLocalPlayer() + { + ++called; + throw new Exception("some exception"); + } + } + class StopServerCalledNetworkBehaviour : NetworkBehaviour { public int called; @@ -755,11 +771,40 @@ public void OnStartLocalPlayer() Assert.That(comp.called, Is.EqualTo(1)); } + [Test] + public void OnStopLocalPlayer() + { + CreateNetworked(out GameObject _, out NetworkIdentity identity, + out StopLocalPlayerCalledNetworkBehaviour comp); + + // call OnStopLocalPlayer in identity + identity.OnStopLocalPlayer(); + Assert.That(comp.called, Is.EqualTo(1)); + } + + [Test] + public void OnStopLocalPlayerException() + { + CreateNetworked(out GameObject _, out NetworkIdentity identity, + out StopLocalPlayerExceptionNetworkBehaviour compEx, + out StopLocalPlayerCalledNetworkBehaviour comp); + + // call OnStopLocalPlayer in identity + // one component will throw an exception, but that shouldn't stop + // OnStopLocalPlayer from being called in the second one + // exception will log an error + LogAssert.ignoreFailingMessages = true; + identity.OnStopLocalPlayer(); + LogAssert.ignoreFailingMessages = false; + Assert.That(compEx.called, Is.EqualTo(1)); + Assert.That(comp.called, Is.EqualTo(1)); + } + [Test] public void OnStopClient() { CreateNetworked(out GameObject _, out NetworkIdentity identity, - out NetworkDestroyCalledNetworkBehaviour comp); + out StopClientCalledNetworkBehaviour comp); // call OnStopClient in identity identity.OnStopClient(); @@ -770,8 +815,8 @@ public void OnStopClient() public void OnStopClientException() { CreateNetworked(out GameObject _, out NetworkIdentity identity, - out NetworkDestroyExceptionNetworkBehaviour compEx, - out NetworkDestroyCalledNetworkBehaviour comp); + out StopClientExceptionNetworkBehaviour compEx, + out StopClientCalledNetworkBehaviour comp); // call OnStopClient in identity // one component will throw an exception, but that shouldn't stop