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