Skip to content

Commit

Permalink
feat: NetworkMatchChecker Component (#1688)
Browse files Browse the repository at this point in the history
* feat: NetworkMatchChecker Component

* Added docs

* feat: Use logger framework for NetworkClient (#1685)

* Use logger framework for NetworkClient

* Update Assets/Mirror/Runtime/NetworkClient.cs

Co-authored-by: vis2k <info@noobtuts.com>

* breaking: NetworkVisbility component (#1681)

* backup

* breaking: NetworkProximityCheck abstract class. Simplifies code, reduces complexity, improves performance because if 10k identities have 10 components each, we don't have to iterate 100k components each time we rebuild observers.

* update tests and checkers

* DisallowMultipleComponents

* fix tests

* split OnCheckObserver check

* fix tests

* syntax

* update comment

* renamed to NetworkVisibility

* forgot to remove comment

* breaking: Network Visibility Component

* changing namespaces of performance tests (#1689)

* Updated to use NetworkVisibility

* Updated comments

* Updated OnCheckObserver and removed OnSetHostVisibility

* tests for OnCheckObserver

* adding check for empty guid

* tests for changing matchId

* RebuildObservers if player left a match

* Refactored to make it simpler

Co-authored-by: Paul Pacheco <paulpach@gmail.com>
Co-authored-by: vis2k <info@noobtuts.com>
Co-authored-by: James Frowen <jamesfrowendev@gmail.com>
  • Loading branch information
4 people committed Apr 17, 2020
1 parent cbc2a47 commit 21acf66
Show file tree
Hide file tree
Showing 7 changed files with 364 additions and 0 deletions.
131 changes: 131 additions & 0 deletions Assets/Mirror/Components/NetworkMatchChecker.cs
@@ -0,0 +1,131 @@
using System;
using System.Collections.Generic;
using UnityEngine;

namespace Mirror
{
/// <summary>
/// Component that controls visibility of networked objects based on match id.
/// <para>Any object with this component on it will only be visible to other objects in the same match.</para>
/// <para>This would be used to isolate players to their respective matches within a single game server instance. </para>
/// </summary>
[AddComponentMenu("Network/NetworkMatchChecker")]
[RequireComponent(typeof(NetworkIdentity))]
[HelpURL("https://mirror-networking.com/docs/Components/NetworkMatchChecker.html")]
public class NetworkMatchChecker : NetworkVisibility
{
static readonly Dictionary<Guid, HashSet<NetworkIdentity>> matchPlayers = new Dictionary<Guid, HashSet<NetworkIdentity>>();

Guid currentMatch = Guid.Empty;

[Header("Diagnostics")]
[SyncVar]
public string currentMatchDebug;

/// <summary>
/// Set this to the same value on all networked objects that belong to a given match
/// </summary>
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<NetworkIdentity>());

// 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<NetworkIdentity>());

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

/// <summary>
/// Callback used by the visibility system to determine if an observer (player) can see this object.
/// <para>If this function returns true, the network connection will be added as an observer.</para>
/// </summary>
/// <param name="conn">Network connection of a player.</param>
/// <returns>True if the player can see this object.</returns>
public override bool OnCheckObserver(NetworkConnection conn)
{
// Not Visible if not in a match
if (matchId == Guid.Empty)
return false;

NetworkMatchChecker networkMatchChecker = conn.identity.GetComponent<NetworkMatchChecker>();

if (networkMatchChecker == null)
return false;

return networkMatchChecker.matchId == matchId;
}

/// <summary>
/// Callback used by the visibility system to (re)construct the set of observers that can see this object.
/// <para>Implementations of this callback should add network connections of players that can see this object to the observers set.</para>
/// </summary>
/// <param name="observers">The new set of observers for this object.</param>
/// <param name="initialize">True if the set of observers is being built for the first time.</param>
public override void OnRebuildObservers(HashSet<NetworkConnection> 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
}
}
11 changes: 11 additions & 0 deletions Assets/Mirror/Components/NetworkMatchChecker.cs.meta

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

196 changes: 196 additions & 0 deletions 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<Guid, HashSet<NetworkIdentity>> matchPlayers;

[SetUp]
public void Setup()
{
transportGO = new GameObject("transportGO");
Transport.activeTransport = transportGO.AddComponent<TelepathyTransport>();

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<NetworkMatchChecker>();
player2MatchChecker = player2.GetComponent<NetworkMatchChecker>();


player1Connection = CreateNetworkConnection(player1);
player2Connection = CreateNetworkConnection(player2);
player3Connection = CreateNetworkConnection(player3);
Dictionary<Guid, HashSet<NetworkIdentity>> g = GetMatchPlayersDictionary();
matchPlayers = g;
}

private static Dictionary<Guid, HashSet<NetworkIdentity>> GetMatchPlayersDictionary()
{
Type type = typeof(NetworkMatchChecker);
FieldInfo fieldInfo = type.GetField("matchPlayers", BindingFlags.Static | BindingFlags.NonPublic);
return (Dictionary<Guid, HashSet<NetworkIdentity>>)fieldInfo.GetValue(null);
}

static NetworkConnection CreateNetworkConnection(GameObject player)
{
NetworkConnectionToClient connection = new NetworkConnectionToClient(++nextConnectionId);
connection.identity = player.GetComponent<NetworkIdentity>();
connection.identity.connectionToClient = connection;
connection.identity.observers = new Dictionary<int, NetworkConnection>();
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));
}
}
}
11 changes: 11 additions & 0 deletions Assets/Mirror/Tests/Editor/NetworkMatchCheckerTest.cs.meta

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

13 changes: 13 additions & 0 deletions 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<NetworkMatchChecker>().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.
Binary file added doc/Components/NetworkMatchChecker.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions doc/Components/index.md
Expand Up @@ -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)
Expand Down

0 comments on commit 21acf66

Please sign in to comment.