diff --git a/Assets/Mirror/Components/NetworkMatchChecker.cs b/Assets/Mirror/Components/NetworkMatchChecker.cs new file mode 100644 index 000000000..f45085f8f --- /dev/null +++ b/Assets/Mirror/Components/NetworkMatchChecker.cs @@ -0,0 +1,131 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace Mirror +{ + /// + /// Component that controls visibility of networked objects based on match id. + /// Any object with this component on it will only be visible to other objects in the same match. + /// This would be used to isolate players to their respective matches within a single game server instance. + /// + [AddComponentMenu("Network/NetworkMatchChecker")] + [RequireComponent(typeof(NetworkIdentity))] + [HelpURL("https://mirror-networking.com/docs/Components/NetworkMatchChecker.html")] + public class NetworkMatchChecker : NetworkVisibility + { + static readonly Dictionary> matchPlayers = new Dictionary>(); + + Guid currentMatch = Guid.Empty; + + [Header("Diagnostics")] + [SyncVar] + public string currentMatchDebug; + + /// + /// Set this to the same value on all networked objects that belong to a given match + /// + public Guid matchId + { + get { return currentMatch; } + set + { + if (currentMatch == value) return; + + // cache previous match so observers in that match can be rebuilt + Guid previousMatch = currentMatch; + + // Set this to the new match this object just entered ... + currentMatch = value; + // ... and copy the string for the inspector because Unity can't show Guid directly + currentMatchDebug = currentMatch.ToString(); + + if (previousMatch != Guid.Empty) + { + // Remove this object from the hashset of the match it just left + matchPlayers[previousMatch].Remove(netIdentity); + + // RebuildObservers of all NetworkIdentity's in the match this object just left + RebuildMatchObservers(previousMatch); + } + + if (currentMatch != Guid.Empty) + { + // Make sure this new match is in the dictionary + if (!matchPlayers.ContainsKey(currentMatch)) + matchPlayers.Add(currentMatch, new HashSet()); + + // Add this object to the hashset of the new match + matchPlayers[currentMatch].Add(netIdentity); + + // RebuildObservers of all NetworkIdentity's in the match this object just entered + RebuildMatchObservers(currentMatch); + } + else + { + // Not in any match now...RebuildObservers will clear and add self + netIdentity.RebuildObservers(false); + } + } + } + + public override void OnStartServer() + { + if (currentMatch == Guid.Empty) return; + + if (!matchPlayers.ContainsKey(currentMatch)) + matchPlayers.Add(currentMatch, new HashSet()); + + matchPlayers[currentMatch].Add(netIdentity); + + // No need to rebuild anything here. + // identity.RebuildObservers is called right after this from NetworkServer.SpawnObject + } + + void RebuildMatchObservers(Guid specificMatch) + { + foreach (NetworkIdentity networkIdentity in matchPlayers[specificMatch]) + if (networkIdentity != null) + networkIdentity.RebuildObservers(false); + } + + #region Observers + + /// + /// Callback used by the visibility system to determine if an observer (player) can see this object. + /// If this function returns true, the network connection will be added as an observer. + /// + /// Network connection of a player. + /// True if the player can see this object. + public override bool OnCheckObserver(NetworkConnection conn) + { + // Not Visible if not in a match + if (matchId == Guid.Empty) + return false; + + NetworkMatchChecker networkMatchChecker = conn.identity.GetComponent(); + + if (networkMatchChecker == null) + return false; + + return networkMatchChecker.matchId == matchId; + } + + /// + /// Callback used by the visibility system to (re)construct the set of observers that can see this object. + /// Implementations of this callback should add network connections of players that can see this object to the observers set. + /// + /// The new set of observers for this object. + /// True if the set of observers is being built for the first time. + public override void OnRebuildObservers(HashSet observers, bool initialize) + { + if (currentMatch == Guid.Empty) return; + + foreach (NetworkIdentity networkIdentity in matchPlayers[currentMatch]) + if (networkIdentity != null && networkIdentity.connectionToClient != null) + observers.Add(networkIdentity.connectionToClient); + } + + #endregion + } +} diff --git a/Assets/Mirror/Components/NetworkMatchChecker.cs.meta b/Assets/Mirror/Components/NetworkMatchChecker.cs.meta new file mode 100644 index 000000000..7c7d6cfc4 --- /dev/null +++ b/Assets/Mirror/Components/NetworkMatchChecker.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1020a74962faada4b807ac5dc053a4cf +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Tests/Editor/NetworkMatchCheckerTest.cs b/Assets/Mirror/Tests/Editor/NetworkMatchCheckerTest.cs new file mode 100644 index 000000000..d030c4ae3 --- /dev/null +++ b/Assets/Mirror/Tests/Editor/NetworkMatchCheckerTest.cs @@ -0,0 +1,196 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using NUnit.Framework; +using UnityEngine; + +namespace Mirror.Tests +{ + public class NetworkMatchCheckerTest + { + private GameObject player1; + private GameObject player2; + private GameObject player3; + private NetworkMatchChecker player1MatchChecker; + private NetworkMatchChecker player2MatchChecker; + private NetworkConnection player1Connection; + private NetworkConnection player2Connection; + private NetworkConnection player3Connection; + private GameObject transportGO; + static int nextConnectionId; + private Dictionary> matchPlayers; + + [SetUp] + public void Setup() + { + transportGO = new GameObject("transportGO"); + Transport.activeTransport = transportGO.AddComponent(); + + player1 = new GameObject("TestPlayer1", typeof(NetworkIdentity), typeof(NetworkMatchChecker)); + player2 = new GameObject("TestPlayer2", typeof(NetworkIdentity), typeof(NetworkMatchChecker)); + player3 = new GameObject("TestPlayer3", typeof(NetworkIdentity)); + + player1MatchChecker = player1.GetComponent(); + player2MatchChecker = player2.GetComponent(); + + + player1Connection = CreateNetworkConnection(player1); + player2Connection = CreateNetworkConnection(player2); + player3Connection = CreateNetworkConnection(player3); + Dictionary> g = GetMatchPlayersDictionary(); + matchPlayers = g; + } + + private static Dictionary> GetMatchPlayersDictionary() + { + Type type = typeof(NetworkMatchChecker); + FieldInfo fieldInfo = type.GetField("matchPlayers", BindingFlags.Static | BindingFlags.NonPublic); + return (Dictionary>)fieldInfo.GetValue(null); + } + + static NetworkConnection CreateNetworkConnection(GameObject player) + { + NetworkConnectionToClient connection = new NetworkConnectionToClient(++nextConnectionId); + connection.identity = player.GetComponent(); + connection.identity.connectionToClient = connection; + connection.identity.observers = new Dictionary(); + connection.isReady = true; + return connection; + } + + [TearDown] + public void TearDown() + { + UnityEngine.Object.DestroyImmediate(player1); + UnityEngine.Object.DestroyImmediate(player2); + UnityEngine.Object.DestroyImmediate(player3); + UnityEngine.Object.DestroyImmediate(transportGO); + + matchPlayers.Clear(); + matchPlayers = null; + } + + static void SetMatchId(NetworkMatchChecker target, Guid guid) + { + // set using reflection so bypass property + FieldInfo field = typeof(NetworkMatchChecker).GetField("currentMatch", BindingFlags.Instance | BindingFlags.NonPublic); + field.SetValue(target, guid); + } + + [Test] + public void OnCheckObserverShouldBeTrueForSameMatchId() + { + string guid = Guid.NewGuid().ToString(); + + SetMatchId(player1MatchChecker, new Guid(guid)); + SetMatchId(player2MatchChecker, new Guid(guid)); + + bool player1Visable = player1MatchChecker.OnCheckObserver(player1Connection); + Assert.IsTrue(player1Visable); + + bool player2Visable = player1MatchChecker.OnCheckObserver(player2Connection); + Assert.IsTrue(player2Visable); + } + + [Test] + public void OnCheckObserverShouldBeFalseForDifferentMatchId() + { + string guid1 = Guid.NewGuid().ToString(); + string guid2 = Guid.NewGuid().ToString(); + + SetMatchId(player1MatchChecker, new Guid(guid1)); + SetMatchId(player2MatchChecker, new Guid(guid2)); + + bool player1VisableToPlayer1 = player1MatchChecker.OnCheckObserver(player1Connection); + Assert.IsTrue(player1VisableToPlayer1); + + bool player2VisableToPlayer1 = player1MatchChecker.OnCheckObserver(player2Connection); + Assert.IsFalse(player2VisableToPlayer1); + + + bool player1VisableToPlayer2 = player2MatchChecker.OnCheckObserver(player1Connection); + Assert.IsFalse(player1VisableToPlayer2); + + bool player2VisableToPlayer2 = player2MatchChecker.OnCheckObserver(player2Connection); + Assert.IsTrue(player2VisableToPlayer2); + } + + [Test] + public void OnCheckObserverShouldBeFalseIfObjectDoesNotHaveNetworkMatchChecker() + { + string guid = Guid.NewGuid().ToString(); + + SetMatchId(player1MatchChecker, new Guid(guid)); + + bool player3Visable = player1MatchChecker.OnCheckObserver(player3Connection); + Assert.IsFalse(player3Visable); + } + + [Test] + public void OnCheckObserverShouldBeFalseForEmptyGuid() + { + string guid = Guid.Empty.ToString(); + + SetMatchId(player1MatchChecker, new Guid(guid)); + SetMatchId(player2MatchChecker, new Guid(guid)); + + bool player1Visable = player1MatchChecker.OnCheckObserver(player1Connection); + Assert.IsFalse(player1Visable); + + bool player2Visable = player1MatchChecker.OnCheckObserver(player2Connection); + Assert.IsFalse(player2Visable); + } + + [Test] + public void SettingMatchIdShouldRebuildObservers() + { + string guidMatch1 = Guid.NewGuid().ToString(); + + // make players join same match + player1MatchChecker.matchId = new Guid(guidMatch1); + player2MatchChecker.matchId = new Guid(guidMatch1); + + // check player1's observers contains player 2 + Assert.IsTrue(player1MatchChecker.netIdentity.observers.ContainsValue(player2MatchChecker.connectionToClient)); + // check player2's observers contains player 1 + Assert.IsTrue(player2MatchChecker.netIdentity.observers.ContainsValue(player1MatchChecker.connectionToClient)); + } + + [Test] + public void ChangingMatchIdShouldRebuildObservers() + { + string guidMatch1 = Guid.NewGuid().ToString(); + string guidMatch2 = Guid.NewGuid().ToString(); + + // make players join same match + player1MatchChecker.matchId = new Guid(guidMatch1); + player2MatchChecker.matchId = new Guid(guidMatch1); + + // make player2 join different match + player2MatchChecker.matchId = new Guid(guidMatch2); + + // check player1's observers does NOT contain player 2 + Assert.IsFalse(player1MatchChecker.netIdentity.observers.ContainsValue(player2MatchChecker.connectionToClient)); + // check player2's observers does NOT contain player 1 + Assert.IsFalse(player2MatchChecker.netIdentity.observers.ContainsValue(player1MatchChecker.connectionToClient)); + } + + [Test] + public void ClearingMatchIdShouldRebuildObservers() + { + string guidMatch1 = Guid.NewGuid().ToString(); + + // make players join same match + player1MatchChecker.matchId = new Guid(guidMatch1); + player2MatchChecker.matchId = new Guid(guidMatch1); + + // make player 2 leave match + player2MatchChecker.matchId = Guid.Empty; + + // check player1's observers does NOT contain player 2 + Assert.IsFalse(player1MatchChecker.netIdentity.observers.ContainsValue(player2MatchChecker.connectionToClient)); + // check player2's observers does NOT contain player 1 + Assert.IsFalse(player2MatchChecker.netIdentity.observers.ContainsValue(player1MatchChecker.connectionToClient)); + } + } +} diff --git a/Assets/Mirror/Tests/Editor/NetworkMatchCheckerTest.cs.meta b/Assets/Mirror/Tests/Editor/NetworkMatchCheckerTest.cs.meta new file mode 100644 index 000000000..b3c51cf81 --- /dev/null +++ b/Assets/Mirror/Tests/Editor/NetworkMatchCheckerTest.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2fa0a455ab9b4cf47b9eab0f2b03ce0c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/doc/Components/NetworkMatchChecker.md b/doc/Components/NetworkMatchChecker.md new file mode 100644 index 000000000..5b145ebf6 --- /dev/null +++ b/doc/Components/NetworkMatchChecker.md @@ -0,0 +1,13 @@ +# Network Scene Checker + +The Network Match Checker component controls visibility of networked objects based on match id. + +![Network Scene Checker component](NetworkMatchChecker.png) + +Any object with this component on it will only be visible to other objects in the same match. + +This would be used to isolate players to their respective matches within a single game server instance. + +When you create a match, generate and store, in a List for example, a new match id with `System.Guid.NewGuid();` and assign the same match id to the Network Scene Checker via `GetComponent().matchId`. + +Mirror's built-in Observers system will isolate SyncVar's and ClientRpc's on networked objects to only send updates to clients with the same match id. diff --git a/doc/Components/NetworkMatchChecker.png b/doc/Components/NetworkMatchChecker.png new file mode 100644 index 000000000..c2d67ef08 Binary files /dev/null and b/doc/Components/NetworkMatchChecker.png differ diff --git a/doc/Components/index.md b/doc/Components/index.md index d2ff69696..a7dea9096 100644 --- a/doc/Components/index.md +++ b/doc/Components/index.md @@ -16,6 +16,8 @@ These core components are included in Mirror: The Network Proximity Checker component controls the visibility of game objects for network clients, based on proximity to players. - [Network Scene Checker](NetworkSceneChecker.md) The Network Scene Checker component controls visibility of networked objects between scenes. +- [Network Match Checker](NetworkMatchChecker.md) + The Network Match Checker component controls visibility of networked objects based on match id. - [Network Room Manager](NetworkRoomManager.md) The Network Room Manager is an extension component of Network Manager that provides a basic functional room. - [Network Room Player](NetworkRoomPlayer.md)