diff --git a/Assets/Mirror/Core/InterestManagement.cs b/Assets/Mirror/Core/InterestManagement.cs index 85568317406..e606f61055a 100644 --- a/Assets/Mirror/Core/InterestManagement.cs +++ b/Assets/Mirror/Core/InterestManagement.cs @@ -7,36 +7,11 @@ namespace Mirror { [DisallowMultipleComponent] [HelpURL("https://mirror-networking.gitbook.io/docs/guides/interest-management")] - public abstract class InterestManagement : MonoBehaviour + public abstract class InterestManagement : LowLevelInterestManagement { - // Awake configures InterestManagement in NetworkServer/Client - // Do NOT check for active server or client here. - // Awake must always set the static aoi references. - // make sure to call base.Awake when overwriting! - protected virtual void Awake() - { - if (NetworkServer.aoi == null) - { - NetworkServer.aoi = this; - } - else Debug.LogError($"Only one InterestManagement component allowed. {NetworkServer.aoi.GetType()} has been set up already."); - - if (NetworkClient.aoi == null) - { - NetworkClient.aoi = this; - } - else Debug.LogError($"Only one InterestManagement component allowed. {NetworkClient.aoi.GetType()} has been set up already."); - } - - [ServerCallback] - public virtual void Reset() {} - - // Callback used by the visibility system to determine if an observer - // (player) can see the NetworkIdentity. If this function returns true, - // the network connection will be added as an observer. - // conn: Network connection of a player. - // returns True if the player can see this object. - public abstract bool OnCheckObserver(NetworkIdentity identity, NetworkConnectionToClient newObserver); + // allocate newObservers helper HashSet only once + readonly HashSet newObservers = + new HashSet(); // rebuild observers for the given NetworkIdentity. // Server will automatically spawn/despawn added/removed ones. @@ -72,29 +47,100 @@ protected void RebuildAll() } } - // Callback used by the visibility system for objects on a host. - // Objects on a host (with a local client) cannot be disabled or - // destroyed when they are not visible to the local client. So this - // function is called to allow custom code to hide these objects. A - // typical implementation will disable renderer components on the - // object. This is only called on local clients on a host. - // => need the function in here and virtual so people can overwrite! - // => not everyone wants to hide renderers! - [ServerCallback] - public virtual void SetHostVisibility(NetworkIdentity identity, bool visible) + // rebuild observers via interest management system + public override void OnRequestRebuild(NetworkIdentity identity, bool initialize) { - foreach (Renderer rend in identity.GetComponentsInChildren()) - rend.enabled = visible; - } + // clear newObservers hashset before using it + newObservers.Clear(); - /// Called on the server when a new networked object is spawned. - // (useful for 'only rebuild if changed' interest management algorithms) - [ServerCallback] - public virtual void OnSpawned(NetworkIdentity identity) {} + // not force hidden? + if (identity.visible != Visibility.ForceHidden) + { + OnRebuildObservers(identity, newObservers); + } - /// Called on the server when a networked object is destroyed. - // (useful for 'only rebuild if changed' interest management algorithms) - [ServerCallback] - public virtual void OnDestroyed(NetworkIdentity identity) {} + // IMPORTANT: AFTER rebuilding add own player connection in any case + // to ensure player always sees himself no matter what. + // -> OnRebuildObservers might clear observers, so we need to add + // the player's own connection AFTER. 100% fail safe. + // -> fixes https://github.com/vis2k/Mirror/issues/692 where a + // player might teleport out of the ProximityChecker's cast, + // losing the own connection as observer. + if (identity.connectionToClient != null) + { + newObservers.Add(identity.connectionToClient); + } + + bool changed = false; + + // add all newObservers that aren't in .observers yet + foreach (NetworkConnectionToClient conn in newObservers) + { + // only add ready connections. + // otherwise the player might not be in the world yet or anymore + if (conn != null && conn.isReady) + { + if (initialize || !identity.observers.ContainsKey(conn.connectionId)) + { + // new observer + conn.AddToObserving(identity); + // Debug.Log($"New Observer for {gameObject} {conn}"); + changed = true; + } + } + } + + // remove all old .observers that aren't in newObservers anymore + foreach (NetworkConnectionToClient conn in identity.observers.Values) + { + if (!newObservers.Contains(conn)) + { + // removed observer + conn.RemoveFromObserving(identity, false); + // Debug.Log($"Removed Observer for {gameObject} {conn}"); + changed = true; + } + } + + // copy new observers to observers + if (changed) + { + identity.observers.Clear(); + foreach (NetworkConnectionToClient conn in newObservers) + { + if (conn != null && conn.isReady) + identity.observers.Add(conn.connectionId, conn); + } + } + + // special case for host mode: we use SetHostVisibility to hide + // NetworkIdentities that aren't in observer range from host. + // this is what games like Dota/Counter-Strike do too, where a host + // does NOT see all players by default. they are in memory, but + // hidden to the host player. + // + // this code is from UNET, it's a bit strange but it works: + // * it hides newly connected identities in host mode + // => that part was the intended behaviour + // * it hides ALL NetworkIdentities in host mode when the host + // connects but hasn't selected a character yet + // => this only works because we have no .localConnection != null + // check. at this stage, localConnection is null because + // StartHost starts the server first, then calls this code, + // then starts the client and sets .localConnection. so we can + // NOT add a null check without breaking host visibility here. + // * it hides ALL NetworkIdentities in server-only mode because + // observers never contain the 'null' .localConnection + // => that was not intended, but let's keep it as it is so we + // don't break anything in host mode. it's way easier than + // iterating all identities in a special function in StartHost. + if (initialize) + { + if (!newObservers.Contains(NetworkServer.localConnection)) + { + SetHostVisibility(identity, false); + } + } + } } } diff --git a/Assets/Mirror/Core/LowLevelInterestManagement.cs b/Assets/Mirror/Core/LowLevelInterestManagement.cs new file mode 100644 index 00000000000..2fe88cc1519 --- /dev/null +++ b/Assets/Mirror/Core/LowLevelInterestManagement.cs @@ -0,0 +1,91 @@ +// interest management component for custom solutions like +// distance based, spatial hashing, raycast based, etc. +using System.Collections.Generic; +using UnityEngine; + +namespace Mirror +{ + [DisallowMultipleComponent] + [HelpURL("https://mirror-networking.gitbook.io/docs/guides/interest-management")] + public abstract class LowLevelInterestManagement : MonoBehaviour + { + // Awake configures LowLevelInterestManagement in NetworkServer/Client + // Do NOT check for active server or client here. + // Awake must always set the static aoi references. + // make sure to call base.Awake when overwriting! + protected virtual void Awake() + { + if (NetworkServer.aoi == null) + { + NetworkServer.aoi = this; + } + else Debug.LogError($"Only one InterestManagement component allowed. {NetworkServer.aoi.GetType()} has been set up already."); + + if (NetworkClient.aoi == null) + { + NetworkClient.aoi = this; + } + else Debug.LogError($"Only one InterestManagement component allowed. {NetworkClient.aoi.GetType()} has been set up already."); + } + + [ServerCallback] + public virtual void Reset() {} + + // Callback used by the visibility system to determine if an observer + // (player) can see the NetworkIdentity. If this function returns true, + // the network connection will be added as an observer. + // conn: Network connection of a player. + // returns True if the player can see this object. + public abstract bool OnCheckObserver(NetworkIdentity identity, NetworkConnectionToClient newObserver); + + + // Callback used by the visibility system for objects on a host. + // Objects on a host (with a local client) cannot be disabled or + // destroyed when they are not visible to the local client. So this + // function is called to allow custom code to hide these objects. A + // typical implementation will disable renderer components on the + // object. This is only called on local clients on a host. + // => need the function in here and virtual so people can overwrite! + // => not everyone wants to hide renderers! + [ServerCallback] + public virtual void SetHostVisibility(NetworkIdentity identity, bool visible) + { + foreach (Renderer rend in identity.GetComponentsInChildren()) + rend.enabled = visible; + } + + /// Called on the server when a new networked object is spawned. + // (useful for 'only rebuild if changed' interest management algorithms) + [ServerCallback] + public virtual void OnSpawned(NetworkIdentity identity) {} + + /// Called on the server when a networked object is destroyed. + // (useful for 'only rebuild if changed' interest management algorithms) + [ServerCallback] + public virtual void OnDestroyed(NetworkIdentity identity) {} + + public abstract void OnRequestRebuild(NetworkIdentity identity, bool initialize); + + /// + /// Adds the specified connection to the observers of identity + /// + /// + /// + protected void AddObserver(NetworkConnectionToClient connection, NetworkIdentity identity) + { + connection.AddToObserving(identity); + identity.observers.Add(connection.connectionId, connection); + } + + /// + /// Removes the specified connection from the observers of identity + /// + /// + /// + protected void RemoveObserver(NetworkConnectionToClient connection, NetworkIdentity identity) + { + connection.RemoveFromObserving(identity, false); + identity.observers.Remove(connection.connectionId); + } + } +} diff --git a/Assets/Mirror/Core/LowLevelInterestManagement.cs.meta b/Assets/Mirror/Core/LowLevelInterestManagement.cs.meta new file mode 100644 index 00000000000..526e1192a83 --- /dev/null +++ b/Assets/Mirror/Core/LowLevelInterestManagement.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 18bd2ffe65a444f3b13d59bdac7f2228 +timeCreated: 1676129329 \ No newline at end of file diff --git a/Assets/Mirror/Core/NetworkClient.cs b/Assets/Mirror/Core/NetworkClient.cs index c94eea14e8c..79718e70a43 100644 --- a/Assets/Mirror/Core/NetworkClient.cs +++ b/Assets/Mirror/Core/NetworkClient.cs @@ -115,7 +115,7 @@ public static partial class NetworkClient // interest management component (optional) // only needed for SetHostVisibility - public static InterestManagement aoi; + public static LowLevelInterestManagement aoi; // scene loading public static bool isLoadingScene; diff --git a/Assets/Mirror/Core/NetworkServer.cs b/Assets/Mirror/Core/NetworkServer.cs index 8784fa2d5af..75ba15c90c0 100644 --- a/Assets/Mirror/Core/NetworkServer.cs +++ b/Assets/Mirror/Core/NetworkServer.cs @@ -68,7 +68,7 @@ public static partial class NetworkServer // interest management component (optional) // by default, everyone observes everyone - public static InterestManagement aoi; + public static LowLevelInterestManagement aoi; // OnConnected / OnDisconnected used to be NetworkMessages that were // invoked. this introduced a bug where external clients could send @@ -189,7 +189,6 @@ public static void Shutdown() connections.Clear(); connectionsCopy.Clear(); handlers.Clear(); - newObservers.Clear(); // destroy all spawned objects, _then_ set inactive. // make sure .active is still true before calling this. @@ -1534,10 +1533,7 @@ static void DestroyObject(NetworkIdentity identity, DestroyMode mode) // Helper function to add all server connections as observers. // This is used if none of the components provides their own // OnRebuildObservers function. - // allocate newObservers helper HashSet only once - // internal for tests - internal static readonly HashSet newObservers = - new HashSet(); + // rebuild observers default method (no AOI) - adds all connections static void RebuildObserversDefault(NetworkIdentity identity, bool initialize) @@ -1597,106 +1593,10 @@ public static void RebuildObservers(NetworkIdentity identity, bool initialize) // otherwise let interest management system rebuild else { - RebuildObserversCustom(identity, initialize); + aoi.OnRequestRebuild(identity, initialize); } } - // rebuild observers via interest management system - static void RebuildObserversCustom(NetworkIdentity identity, bool initialize) - { - // clear newObservers hashset before using it - newObservers.Clear(); - - // not force hidden? - if (identity.visible != Visibility.ForceHidden) - { - aoi.OnRebuildObservers(identity, newObservers); - } - - // IMPORTANT: AFTER rebuilding add own player connection in any case - // to ensure player always sees himself no matter what. - // -> OnRebuildObservers might clear observers, so we need to add - // the player's own connection AFTER. 100% fail safe. - // -> fixes https://github.com/vis2k/Mirror/issues/692 where a - // player might teleport out of the ProximityChecker's cast, - // losing the own connection as observer. - if (identity.connectionToClient != null) - { - newObservers.Add(identity.connectionToClient); - } - - bool changed = false; - - // add all newObservers that aren't in .observers yet - foreach (NetworkConnectionToClient conn in newObservers) - { - // only add ready connections. - // otherwise the player might not be in the world yet or anymore - if (conn != null && conn.isReady) - { - if (initialize || !identity.observers.ContainsKey(conn.connectionId)) - { - // new observer - conn.AddToObserving(identity); - // Debug.Log($"New Observer for {gameObject} {conn}"); - changed = true; - } - } - } - - // remove all old .observers that aren't in newObservers anymore - foreach (NetworkConnectionToClient conn in identity.observers.Values) - { - if (!newObservers.Contains(conn)) - { - // removed observer - conn.RemoveFromObserving(identity, false); - // Debug.Log($"Removed Observer for {gameObject} {conn}"); - changed = true; - } - } - - // copy new observers to observers - if (changed) - { - identity.observers.Clear(); - foreach (NetworkConnectionToClient conn in newObservers) - { - if (conn != null && conn.isReady) - identity.observers.Add(conn.connectionId, conn); - } - } - - // special case for host mode: we use SetHostVisibility to hide - // NetworkIdentities that aren't in observer range from host. - // this is what games like Dota/Counter-Strike do too, where a host - // does NOT see all players by default. they are in memory, but - // hidden to the host player. - // - // this code is from UNET, it's a bit strange but it works: - // * it hides newly connected identities in host mode - // => that part was the intended behaviour - // * it hides ALL NetworkIdentities in host mode when the host - // connects but hasn't selected a character yet - // => this only works because we have no .localConnection != null - // check. at this stage, localConnection is null because - // StartHost starts the server first, then calls this code, - // then starts the client and sets .localConnection. so we can - // NOT add a null check without breaking host visibility here. - // * it hides ALL NetworkIdentities in server-only mode because - // observers never contain the 'null' .localConnection - // => that was not intended, but let's keep it as it is so we - // don't break anything in host mode. it's way easier than - // iterating all identities in a special function in StartHost. - if (initialize) - { - if (!newObservers.Contains(localConnection)) - { - if (aoi != null) - aoi.SetHostVisibility(identity, false); - } - } - } // broadcasting //////////////////////////////////////////////////////// // helper function to get the right serialization for a connection diff --git a/Assets/Mirror/Tests/Editor/NetworkServerTest.cs b/Assets/Mirror/Tests/Editor/NetworkServerTest.cs index 0f6d5b90fb7..8562cbe87cd 100644 --- a/Assets/Mirror/Tests/Editor/NetworkServerTest.cs +++ b/Assets/Mirror/Tests/Editor/NetworkServerTest.cs @@ -1155,7 +1155,6 @@ public void ShutdownCleanup() Assert.That(NetworkServer.connections.Count, Is.EqualTo(0)); Assert.That(NetworkServer.connectionsCopy.Count, Is.EqualTo(0)); Assert.That(NetworkServer.handlers.Count, Is.EqualTo(0)); - Assert.That(NetworkServer.newObservers.Count, Is.EqualTo(0)); Assert.That(NetworkServer.spawned.Count, Is.EqualTo(0)); Assert.That(NetworkServer.localConnection, Is.Null);