From 04128a1649de5999cef7165c931045fd1f549c88 Mon Sep 17 00:00:00 2001 From: Matt Walsh Date: Thu, 29 Apr 2021 17:01:16 -0700 Subject: [PATCH] feat: Interest system [MTT-239] --- .../Runtime/Core/NetworkBehaviourUpdater.cs | 9 +- .../Runtime/Core/NetworkManager.cs | 14 +- .../Runtime/Core/NetworkObject.cs | 36 ++ .../Runtime/Interest.meta | 8 + .../Runtime/Interest/InterestManager.cs | 92 ++++ .../Runtime/Interest/InterestManager.cs.meta | 11 + .../Runtime/Interest/InterestNode.cs | 19 + .../Runtime/Interest/InterestNode.cs.meta | 11 + .../Runtime/Interest/InterestNodeStatic.cs | 52 +++ .../Interest/InterestNodeStatic.cs.meta | 11 + .../Runtime/Interest/InterestSettings.cs | 16 + .../Runtime/Interest/InterestSettings.cs.meta | 11 + .../Runtime/Interest/RadiusInterestKernel.cs | 21 + .../Interest/RadiusInterestKernel.cs.meta | 11 + .../Runtime/Spawning/NetworkSpawnManager.cs | 4 + .../Tests/Runtime/HRTClock.cs | 42 ++ .../Tests/Runtime/HRTClock.cs.meta | 11 + .../Tests/Runtime/InterestTests.cs | 427 ++++++++++++++++++ .../Tests/Runtime/InterestTests.cs.meta | 11 + 19 files changed, 813 insertions(+), 4 deletions(-) create mode 100644 com.unity.netcode.gameobjects/Runtime/Interest.meta create mode 100644 com.unity.netcode.gameobjects/Runtime/Interest/InterestManager.cs create mode 100644 com.unity.netcode.gameobjects/Runtime/Interest/InterestManager.cs.meta create mode 100644 com.unity.netcode.gameobjects/Runtime/Interest/InterestNode.cs create mode 100644 com.unity.netcode.gameobjects/Runtime/Interest/InterestNode.cs.meta create mode 100644 com.unity.netcode.gameobjects/Runtime/Interest/InterestNodeStatic.cs create mode 100644 com.unity.netcode.gameobjects/Runtime/Interest/InterestNodeStatic.cs.meta create mode 100644 com.unity.netcode.gameobjects/Runtime/Interest/InterestSettings.cs create mode 100644 com.unity.netcode.gameobjects/Runtime/Interest/InterestSettings.cs.meta create mode 100644 com.unity.netcode.gameobjects/Runtime/Interest/RadiusInterestKernel.cs create mode 100644 com.unity.netcode.gameobjects/Runtime/Interest/RadiusInterestKernel.cs.meta create mode 100644 com.unity.netcode.gameobjects/Tests/Runtime/HRTClock.cs create mode 100644 com.unity.netcode.gameobjects/Tests/Runtime/HRTClock.cs.meta create mode 100644 com.unity.netcode.gameobjects/Tests/Runtime/InterestTests.cs create mode 100644 com.unity.netcode.gameobjects/Tests/Runtime/InterestTests.cs.meta diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviourUpdater.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviourUpdater.cs index d38209ad0f..d683069a11 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviourUpdater.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviourUpdater.cs @@ -6,6 +6,7 @@ namespace Unity.Netcode public class NetworkBehaviourUpdater { private HashSet m_Touched = new HashSet(); + private HashSet m_TouchedThisClient = new HashSet(); #if DEVELOPMENT_BUILD || UNITY_EDITOR private ProfilerMarker m_NetworkBehaviourUpdate = new ProfilerMarker($"{nameof(NetworkBehaviour)}.{nameof(NetworkBehaviourUpdate)}"); @@ -23,10 +24,12 @@ internal void NetworkBehaviourUpdate(NetworkManager networkManager) m_Touched.Clear(); for (int i = 0; i < networkManager.ConnectedClientsList.Count; i++) { + m_TouchedThisClient.Clear(); var client = networkManager.ConnectedClientsList[i]; - var spawnedObjs = networkManager.SpawnManager.SpawnedObjectsList; - m_Touched.UnionWith(spawnedObjs); - foreach (var sobj in spawnedObjs) + networkManager.InterestManager.QueryFor(client, m_TouchedThisClient); + + m_Touched.UnionWith(m_TouchedThisClient); + foreach (var sobj in m_TouchedThisClient) { // Sync just the variables for just the objects this client sees for (int k = 0; k < sobj.ChildNetworkBehaviours.Count; k++) diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs index 898a673f53..b2fa99621b 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs @@ -2,6 +2,7 @@ using System.Collections; using System.Collections.Generic; using System.Linq; +using Unity.Netcode.Interest; using UnityEngine; using Unity.Profiling; using Debug = UnityEngine.Debug; @@ -39,7 +40,7 @@ public class NetworkManager : MonoBehaviour, INetworkUpdateSystem internal MessageQueueContainer MessageQueueContainer { get; private set; } - + internal InterestManager InterestManager { get; private set; } internal SnapshotSystem SnapshotSystem { get; private set; } internal NetworkBehaviourUpdater BehaviourUpdater { get; private set; } @@ -244,6 +245,10 @@ internal void InvokeConnectionApproval(byte[] payload, ulong clientId, Connectio internal static event Action OnSingletonReady; + // the interest settings objects receive unless they have a pre-prefab override + public InterestSettings InterestSettings; + + #if UNITY_EDITOR private void OnValidate() { @@ -439,6 +444,7 @@ private void Initialize(bool server) MessageQueueContainer.Dispose(); MessageQueueContainer = null; } + InterestManager = new InterestManager(); // The MessageQueueContainer must be initialized within the Init method ONLY // It should ONLY be shutdown and destroyed in the Shutdown method (other than just above) @@ -878,6 +884,12 @@ public void Shutdown() NetworkTickSystem = null; } + if (InterestManager != null) + { + InterestManager.Dispose(); + InterestManager = null; + } + IsListening = false; IsServer = false; IsClient = false; diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs index e22d2c4063..a6bd200696 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs @@ -3,6 +3,8 @@ using System.IO; using System.Linq; using System.Runtime.CompilerServices; +using Unity.Netcode.Interest; + using UnityEngine; namespace Unity.Netcode @@ -12,8 +14,11 @@ namespace Unity.Netcode /// [AddComponentMenu("Netcode/" + nameof(NetworkObject), -99)] [DisallowMultipleComponent] + public sealed class NetworkObject : MonoBehaviour { + public List InterestNodes = new List(); + [HideInInspector] [SerializeField] internal uint GlobalObjectIdHash; @@ -177,6 +182,26 @@ internal set /// public bool AutoObjectParentSync = true; + public InterestSettings InterestSettingsOverride; + public InterestSettings InterestSettings + { + get + { + InterestSettings result = null; + if (InterestSettingsOverride) + { + result = InterestSettingsOverride; + } + else if (NetworkManager.InterestSettings) + { + result = NetworkManager.InterestSettings; + } + + return result; + } + set => InterestSettingsOverride = value; + } + internal readonly HashSet Observers = new HashSet(); /// @@ -193,6 +218,7 @@ public HashSet.Enumerator GetObservers() return Observers.GetEnumerator(); } + /// /// Whether or not this object is visible to a specific client /// @@ -1072,5 +1098,15 @@ internal uint HostCheckForGlobalObjectIdHashOverride() return GlobalObjectIdHash; } + + // Trigger the Interest system to do an update sweep on any Interest nodes + // I am associated with + public void UpdateInterest() + { + foreach (var node in InterestNodes) + { + node?.UpdateObject(this); + } + } } } diff --git a/com.unity.netcode.gameobjects/Runtime/Interest.meta b/com.unity.netcode.gameobjects/Runtime/Interest.meta new file mode 100644 index 0000000000..8b29cfbe41 --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Interest.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 9c4f730ce270849e588dfef7da1c8fce +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/com.unity.netcode.gameobjects/Runtime/Interest/InterestManager.cs b/com.unity.netcode.gameobjects/Runtime/Interest/InterestManager.cs new file mode 100644 index 0000000000..55df2d0294 --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Interest/InterestManager.cs @@ -0,0 +1,92 @@ +using System.Collections.Generic; +using Unity.Netcode.Interest; +using UnityEngine; + +namespace Unity.Netcode +{ + // interest *system* instead of interest node ? + public class InterestManager + { + private readonly InterestNodeStatic m_DefaultInterestNode; + + public InterestManager() + { + m_ChildNodes = new HashSet(); + + // This is the node objects will be added to if no replication group is + // specified, which means they always get replicated + m_DefaultInterestNode = ScriptableObject.CreateInstance(); + m_ChildNodes.Add(m_DefaultInterestNode); + } + + public void AddObject(in NetworkObject obj) + { + var nodes = obj.InterestNodes; + + // If this new object has no associated Interest Nodes, then we put it in the + // default node, which all clients will then get. + // + // That is, if you don't opt into the system behavior is the same as before + // the Interest system was added + if (nodes.Count == 0) + { + m_DefaultInterestNode.AddObject(obj); + } + // else add myself to whatever Interest Nodes I am associated with + else + { + // I am walking through each of the interest nodes that this object has + // I should probably optimize for this later vs. doing this for every add! + foreach (var node in nodes) + { + // cover the case with an empty list entry + if (node != null) + { + // the Interest Manager lazily adds nodes to itself when it sees + // new nodes that associate with the objects being added + m_ChildNodes.Add(node); + // tell this node to add this object to itself + node.AddObject(obj); + } + } + } + } + + public void RemoveObject(in NetworkObject oldObject) + { + var nodes = oldObject.InterestNodes; + + // if the node never had an InterestNode, then it was using the default + // interest node + if (nodes.Count == 0) + { + m_DefaultInterestNode.RemoveObject(oldObject); + } + else + { + foreach (var node in nodes) + { + if (node == null) + { + continue; + } + node.RemoveObject(oldObject); + } + } + } + + public void QueryFor(in NetworkClient client, HashSet results) + { + foreach (var c in m_ChildNodes) + { + c.QueryFor(client, results); + } + } + + public void Dispose() + { + } + + private HashSet m_ChildNodes; + } +} diff --git a/com.unity.netcode.gameobjects/Runtime/Interest/InterestManager.cs.meta b/com.unity.netcode.gameobjects/Runtime/Interest/InterestManager.cs.meta new file mode 100644 index 0000000000..a24625d665 --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Interest/InterestManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8839b9f658a8b48c4928e234700d9162 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/com.unity.netcode.gameobjects/Runtime/Interest/InterestNode.cs b/com.unity.netcode.gameobjects/Runtime/Interest/InterestNode.cs new file mode 100644 index 0000000000..af65ecd572 --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Interest/InterestNode.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using Unity.Netcode; +using UnityEngine; + +namespace Unity.Netcode.Interest +{ + public abstract class InterestNode : ScriptableObject + { + public abstract void QueryFor(in NetworkClient client, HashSet results); + public abstract void AddObject(in NetworkObject obj); + public abstract void RemoveObject(in NetworkObject obj); + public abstract void UpdateObject(in NetworkObject obj); + }; + + public abstract class InterestKernel : ScriptableObject + { + public abstract void QueryFor(in NetworkClient client, in NetworkObject obj, HashSet results); + } +} diff --git a/com.unity.netcode.gameobjects/Runtime/Interest/InterestNode.cs.meta b/com.unity.netcode.gameobjects/Runtime/Interest/InterestNode.cs.meta new file mode 100644 index 0000000000..88a1754252 --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Interest/InterestNode.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: bbd9219e8690647179ca1c1248437db3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/com.unity.netcode.gameobjects/Runtime/Interest/InterestNodeStatic.cs b/com.unity.netcode.gameobjects/Runtime/Interest/InterestNodeStatic.cs new file mode 100644 index 0000000000..a58e22f20c --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Interest/InterestNodeStatic.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using Unity.Netcode; +using UnityEngine; + +namespace Unity.Netcode.Interest +{ + [CreateAssetMenu(fileName = "StaticInterestNode", menuName = "Interest/Nodes/Static", order = 1)] + public class InterestNodeStatic : InterestNode + { + public List InterestKernels = new List(); + + // these are the objects under my purview + protected HashSet ManagedObjects; + + public void OnEnable() + { + ManagedObjects = new HashSet(); + } + + public override void AddObject(in NetworkObject obj) + { + ManagedObjects.Add(obj); + } + + public override void RemoveObject(in NetworkObject obj) + { + ManagedObjects.Remove(obj); + } + + public override void QueryFor(in NetworkClient client, HashSet results) + { + if (InterestKernels.Count > 0) + { + foreach (var obj in ManagedObjects) + { + foreach (var ik in InterestKernels) + { + ik.QueryFor(client, obj, results); + } + } + } + else + { + results.UnionWith(ManagedObjects); + } + } + + public override void UpdateObject(in NetworkObject obj) + { + } + } +} diff --git a/com.unity.netcode.gameobjects/Runtime/Interest/InterestNodeStatic.cs.meta b/com.unity.netcode.gameobjects/Runtime/Interest/InterestNodeStatic.cs.meta new file mode 100644 index 0000000000..b9069d2db8 --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Interest/InterestNodeStatic.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 25e4e556d52304e88b72bb66aba11c7d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/com.unity.netcode.gameobjects/Runtime/Interest/InterestSettings.cs b/com.unity.netcode.gameobjects/Runtime/Interest/InterestSettings.cs new file mode 100644 index 0000000000..9f6bedcd4b --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Interest/InterestSettings.cs @@ -0,0 +1,16 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +namespace Unity.Netcode.Interest +{ + [CreateAssetMenu(fileName = "ReplicationSettings", menuName = "Interest/Settings/InterestSettings", order = 1)] + + // these are settings used by the Interest management system to + // - adjust how it decides whether an item is replicated + // - adjust how prioritization occurs + public class InterestSettings : ScriptableObject + { + // TBD - add default interest settings here + } +} diff --git a/com.unity.netcode.gameobjects/Runtime/Interest/InterestSettings.cs.meta b/com.unity.netcode.gameobjects/Runtime/Interest/InterestSettings.cs.meta new file mode 100644 index 0000000000..303b80dd8b --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Interest/InterestSettings.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e264e8aae325f4296b5b192e9125a0f3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/com.unity.netcode.gameobjects/Runtime/Interest/RadiusInterestKernel.cs b/com.unity.netcode.gameobjects/Runtime/Interest/RadiusInterestKernel.cs new file mode 100644 index 0000000000..fcfa4317ce --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Interest/RadiusInterestKernel.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using Unity.Netcode.Interest; +using UnityEngine; + +namespace Unity.Netcode +{ + [CreateAssetMenu(fileName = "RadiusInterestKernel", menuName = "Interest/Kernels/Radius", order = 1)] + [Serializable] + public class RadiusInterestKernel : InterestKernel + { + public float Radius = 0.0f; + public override void QueryFor(in NetworkClient client, in NetworkObject obj, HashSet results) + { + if (Vector3.Distance(obj.transform.position, client.PlayerObject.transform.position) <= Radius) + { + results.Add(obj.GetComponent()); + } + } + } +} diff --git a/com.unity.netcode.gameobjects/Runtime/Interest/RadiusInterestKernel.cs.meta b/com.unity.netcode.gameobjects/Runtime/Interest/RadiusInterestKernel.cs.meta new file mode 100644 index 0000000000..d4ec520c07 --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Interest/RadiusInterestKernel.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: dd61eac17cf204a9e904a649d23145a5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs index 41dbb9b68b..a5cf7a462a 100644 --- a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs @@ -362,11 +362,13 @@ internal void SpawnNetworkObjectLocally(NetworkObject networkObject, ulong netwo networkObject.ApplyNetworkParenting(); NetworkObject.CheckOrphanChildren(); networkObject.InvokeBehaviourNetworkSpawn(); + NetworkManager.InterestManager.AddObject(networkObject); } internal void SendSpawnCallForObject(ulong clientId, ulong ownerClientId, NetworkObject networkObject) { if (!NetworkManager.NetworkConfig.UseSnapshotSpawn) + { //Currently, if this is called and the clientId (destination) is the server's client Id, this case //will be checked within the below Send function. To avoid unwarranted allocation of a PooledNetworkBuffer @@ -686,6 +688,8 @@ internal void OnDespawnObject(NetworkObject networkObject, bool destroyGameObjec } var gobj = networkObject.gameObject; + NetworkObject no = gobj.GetComponent(); + NetworkManager.InterestManager.RemoveObject(no); if (destroyGameObject && gobj != null) { diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/HRTClock.cs b/com.unity.netcode.gameobjects/Tests/Runtime/HRTClock.cs new file mode 100644 index 0000000000..55f6b3497f --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Runtime/HRTClock.cs @@ -0,0 +1,42 @@ +using System; +using System.Diagnostics; +using System.Threading; + +namespace MLAPI.RuntimeTests +{ + public sealed class HRTClock : IDisposable + { + private readonly long _maxIdleTime = TimeSpan.FromSeconds(10).Ticks; + private const long TicksMultiplier = 1000 * TimeSpan.TicksPerMillisecond; + + private readonly ThreadLocal _startTime = + new ThreadLocal(() => DateTime.UtcNow, false); + + private readonly ThreadLocal _startTimestamp = + new ThreadLocal(() => Stopwatch.GetTimestamp(), false); + + public DateTime UtcNow + { + get + { + double endTimestamp = Stopwatch.GetTimestamp(); + + var durationInTicks = (endTimestamp - _startTimestamp.Value) / Stopwatch.Frequency * TicksMultiplier; + if (durationInTicks >= _maxIdleTime) + { + _startTimestamp.Value = Stopwatch.GetTimestamp(); + _startTime.Value = DateTime.UtcNow; + return _startTime.Value; + } + + return _startTime.Value.AddTicks((long)durationInTicks); + } + } + + public void Dispose() + { + _startTime.Dispose(); + _startTimestamp.Dispose(); + } + } +} diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/HRTClock.cs.meta b/com.unity.netcode.gameobjects/Tests/Runtime/HRTClock.cs.meta new file mode 100644 index 0000000000..d50d050194 --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Runtime/HRTClock.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8865251bb31424f48b56e3472acf62bb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/InterestTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/InterestTests.cs new file mode 100644 index 0000000000..534c6b7bf3 --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Runtime/InterestTests.cs @@ -0,0 +1,427 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using UnityEngine; +using UnityEngine.TestTools; +using NUnit.Framework; +using Unity.Netcode; +using Unity.Netcode.Interest; +using Unity.Netcode.RuntimeTests; +using Debug = UnityEngine.Debug; + +namespace Unity.Netcode.RuntimeTests +{ + public class TestInterestSettings : InterestSettings + { + public int SomeSetting; + } + + public class InterestTests + { + [SetUp] + public void Setup() + { + // Create, instantiate, and host + NetworkManagerHelper.StartNetworkManager(out NetworkManager networkManager, + NetworkManagerHelper.NetworkManagerOperatingMode.Server); + } + + [TearDown] + public void TearDown() + { + // Stop, shutdown, and destroy + NetworkManagerHelper.ShutdownNetworkManager(); + } + + public class OddEvenInterestKernel : InterestKernel + { + public bool IsOdd = true; + public override void QueryFor(in NetworkClient client, in NetworkObject obj, HashSet results) + { + if (obj.NetworkObjectId % 2 == 0 ^ IsOdd) + { + results.Add(obj); + } + } + } + + public class OddsEvensNode : InterestNode + { + public void OnEnable() + { + m_Odds = CreateInstance(); + m_Evens = CreateInstance(); + } + + public override void AddObject(in NetworkObject obj) + { + if (obj.NetworkObjectId % 2 == 0) + { + m_Evens.AddObject(obj); + } + else + { + m_Odds.AddObject(obj); + } + } + + public override void RemoveObject(in NetworkObject obj) + { + if (obj.NetworkObjectId % 2 == 0) + { + m_Evens.RemoveObject(obj); + } + else + { + m_Odds.RemoveObject(obj); + } + } + + public override void QueryFor(in NetworkClient client, HashSet results) + { + // if a client with an odd NetworkObjectID queries, we return objects with odd NetworkObjectIDs + if (client.PlayerObject.NetworkObjectId % 2 == 0) + { + m_Evens.QueryFor(client, results); + } + else + { + m_Odds.QueryFor(client, results); + } + } + + public override void UpdateObject(in NetworkObject obj) + { + m_Odds.RemoveObject(obj); + m_Evens.RemoveObject(obj); + AddObject(obj); + } + + private InterestNodeStatic m_Odds; + private InterestNodeStatic m_Evens; + } + + private (NetworkObject, Guid) MakeGameInterestObjectHelper(InterestNode comn = null) + { + Guid objGuid = NetworkManagerHelper.AddGameNetworkObject(""); + NetworkObject no = (NetworkObject)NetworkManagerHelper.InstantiatedNetworkObjects[objGuid]; + + if (comn != null) + { + no.InterestNodes.Add(comn); + } + + return (no, objGuid); + } + + private (NetworkObject, Guid) MakeGameInterestObjectHelper(Vector3 coords, InterestNode comn = null) + { + Guid objGuid = NetworkManagerHelper.AddGameNetworkObject(""); + NetworkObject no = (NetworkObject)NetworkManagerHelper.InstantiatedNetworkObjects[objGuid]; + + if (comn != null) + { + no.InterestNodes.Add(comn); + no.transform.position = coords; + } + + return (no, objGuid); + } + + [Test] + // Start is called before the first frame update + public void InterestCustomStorageTests() + { + var oddsEvensNode = ScriptableObject.CreateInstance(); + + var results = new HashSet(); + var nc = new NetworkClient() + { + ClientId = 1, + }; + NetworkManager.Singleton.InterestManager.QueryFor(nc, results); + var objectsBeforeAdd = results.Count; + + var numNodes = 4; + var nodes = new NetworkObject[4]; + for (var i = 0; i < numNodes; i++) + { + var (thisObj, thisGuid) = MakeGameInterestObjectHelper(oddsEvensNode); + nodes[i] = thisObj; + nodes[i].NetworkObjectId = (ulong)(i + 100); + NetworkManagerHelper.SpawnNetworkObject(thisGuid); + } + + var (obj, guid) = MakeGameInterestObjectHelper(); + nc.PlayerObject = obj; + + results.Clear(); + nc.PlayerObject.NetworkObjectId = 100; + NetworkManager.Singleton.InterestManager.QueryFor(nc, results); + Assert.True(results.Count - objectsBeforeAdd == numNodes / 2); + + results.Clear(); + nc.PlayerObject.NetworkObjectId = 101; + NetworkManager.Singleton.InterestManager.QueryFor(nc, results); + Assert.True(results.Count - objectsBeforeAdd == numNodes / 2); + + // now re-number the nodes + for (var i = 0; i < numNodes; i++) + { + nodes[i].NetworkObjectId = (ulong)(2 * i + 101); // 101, 103, 105, 107 + } + + results.Clear(); + nc.PlayerObject.NetworkObjectId = 100; + NetworkManager.Singleton.InterestManager.QueryFor(nc, results); + Assert.True(results.Count - objectsBeforeAdd == numNodes / 2); // should be unchanged + + results.Clear(); + nc.PlayerObject.NetworkObjectId = 101; + NetworkManager.Singleton.InterestManager.QueryFor(nc, results); + Assert.True(results.Count - objectsBeforeAdd == numNodes / 2); // should be unchanged + + // now update the nodes + for (var i = 0; i < numNodes; i++) + { + nodes[i].UpdateInterest(); + } + + results.Clear(); + nc.PlayerObject.NetworkObjectId = 100; + NetworkManager.Singleton.InterestManager.QueryFor(nc, results); + Assert.True(results.Count - objectsBeforeAdd == 0); // now zero, all are odd + + results.Clear(); + nc.PlayerObject.NetworkObjectId = 101; + NetworkManager.Singleton.InterestManager.QueryFor(nc, results); + Assert.True(results.Count - objectsBeforeAdd == nodes.Length); // now 4, all are odd + } + + + [Test] + // Start is called before the first frame update + public void InterestRadiusCheck() + { + InterestNodeStatic naiveRadiusNode = ScriptableObject.CreateInstance(); + var naiveRadiusKernel = ScriptableObject.CreateInstance(); + naiveRadiusKernel.Radius = 1.5f; + naiveRadiusNode.InterestKernels.Add(naiveRadiusKernel); + var staticNode = ScriptableObject.CreateInstance(); + + var results = new HashSet(); + var nc = new NetworkClient() + { + ClientId = 1, + }; + + var (playerObj, playerGuid) = MakeGameInterestObjectHelper(new Vector3(0.0f, 0.0f, 0.0f), null); + nc.PlayerObject = playerObj; + + NetworkManager.Singleton.InterestManager.QueryFor(nc, results); + int objectsBeforeAdd = results.Count; + + var (ok1Obj, ok1Guid) = MakeGameInterestObjectHelper(new Vector3(0.5f, 0.0f, 0.0f), naiveRadiusNode); + NetworkManagerHelper.SpawnNetworkObject(ok1Guid); + + var (ok2Obj, ok2Guid) = MakeGameInterestObjectHelper(new Vector3(1.0f, 0.0f, 0.0f), naiveRadiusNode); + NetworkManagerHelper.SpawnNetworkObject(ok2Guid); + + var (tooFarObj, tooFarGuid) = MakeGameInterestObjectHelper(new Vector3(3.0f, 0.0f, 0.0f), naiveRadiusNode); + NetworkManagerHelper.SpawnNetworkObject(tooFarGuid); + + var (alwaysObj, alwaysGuid) = MakeGameInterestObjectHelper(new Vector3(99.0f, 99.0f, 99.0f), staticNode); + NetworkManagerHelper.SpawnNetworkObject(alwaysGuid); + + NetworkManagerHelper.SpawnNetworkObject(playerGuid); + + results.Clear(); + NetworkManager.Singleton.InterestManager.QueryFor(nc, results); + var hits = results.Count - objectsBeforeAdd; + Assert.True(results.Contains(ok1Obj)); + Assert.True(results.Contains(ok2Obj)); + Assert.True(results.Contains(nc.PlayerObject)); + Assert.True(results.Contains(alwaysObj)); + Assert.False(results.Contains(tooFarObj)); + Assert.True(hits == 4); + + // remove an object, should not be in replication manager + alwaysObj.Despawn(); + results.Clear(); + NetworkManager.Singleton.InterestManager.QueryFor(nc, results); + hits = results.Count - objectsBeforeAdd; + Assert.True(results.Contains(ok1Obj)); + Assert.True(results.Contains(ok2Obj)); + Assert.True(results.Contains(nc.PlayerObject)); + Assert.False(results.Contains(alwaysObj)); + Assert.False(results.Contains(tooFarObj)); + Assert.True(hits == 3); + } + + [Test] + public void CheckMultipleNodes() + { + var results = new HashSet(); + var nc = new NetworkClient() + { + ClientId = 1, + }; + + NetworkManager.Singleton.InterestManager.QueryFor(nc, results); + var objectsBeforeAdd = results.Count; + + var dualNode = ScriptableObject.CreateInstance(); + var oddKernel = ScriptableObject.CreateInstance(); + oddKernel.IsOdd = true; + var evenKernel = ScriptableObject.CreateInstance(); + evenKernel.IsOdd = false; + dualNode.InterestKernels.Add(oddKernel); + dualNode.InterestKernels.Add(evenKernel); + + var (object1, object1Guid) = MakeGameInterestObjectHelper(dualNode); + NetworkManagerHelper.SpawnNetworkObject(object1Guid); + + var (object2, object2Guid) = MakeGameInterestObjectHelper(dualNode); + NetworkManagerHelper.SpawnNetworkObject(object2Guid); + + var (playerObject, playerObjGuid) = MakeGameInterestObjectHelper(dualNode); + nc.PlayerObject = playerObject; + NetworkManagerHelper.SpawnNetworkObject(playerObjGuid); + + results.Clear(); + NetworkManager.Singleton.InterestManager.QueryFor(nc, results); + var hits = results.Count; + Assert.True(hits == (3 + objectsBeforeAdd)); + Assert.True(results.Contains(object1)); + Assert.True(results.Contains(object2)); + Assert.True(results.Contains(nc.PlayerObject)); + } + + + [Test] + public void PerfTest() + { + var clock = new HRTClock(); + var duration = TimeSpan.FromSeconds(5); + var distinctValues = new HashSet(); + var stopWatch = Stopwatch.StartNew(); + + var results = new HashSet(); + var nc = new NetworkClient() + { + ClientId = 1, + }; + + var t = clock.UtcNow; + for (var z = 0; z < 10000; z++) + { + NetworkManager.Singleton.InterestManager.QueryFor(nc, results); + } + + var tElapsed = clock.UtcNow - t; + Debug.Log($"Time: {tElapsed} ms"); + + var objectsBeforeAdd = results.Count; + + var objsToMakePerNode = 10; + var nodesToMake = 100; + var objsToMake = objsToMakePerNode * nodesToMake; + List nodes = new List(); + List objs = new List(); + + for (var i = 0; i < nodesToMake; ++i) + { + nodes.Add(ScriptableObject.CreateInstance()); + for (var j = 0; j < objsToMakePerNode; j++) + { + var (obj, guid) = MakeGameInterestObjectHelper(nodes[i]); + NetworkManagerHelper.SpawnNetworkObject(guid); + objs.Add(obj); + } + } + + var (playerObj, playerObjGuid) = MakeGameInterestObjectHelper(); + nc.PlayerObject = playerObj; + NetworkManagerHelper.SpawnNetworkObject(playerObjGuid); + + results.Clear(); + NetworkManager.Singleton.InterestManager.QueryFor(nc, results); + + // reality check + var hits = results.Count; + Debug.Log(hits); + Assert.True(hits == objsToMake + objectsBeforeAdd + 1); + + // each of the nodes should have returned objsToMakePerNode + for (var i = 0; i < nodesToMake; ++i) + { + var nodeResults = new HashSet(); + nodes[i].QueryFor(nc, nodeResults); + Assert.True(nodeResults.Count == objsToMakePerNode); + } + } + + [Test] + public void CheckPlainReplication() + { + var results = new HashSet(); + var nc = new NetworkClient() + { + ClientId = 1, + }; + + NetworkManager.Singleton.InterestManager.QueryFor(nc, results); + var objectsBeforeAdd = results.Count; + + var (object1, object1Guid) = MakeGameInterestObjectHelper(); + NetworkManagerHelper.SpawnNetworkObject(object1Guid); + + var (object2, object2Guid) = MakeGameInterestObjectHelper(); + NetworkManagerHelper.SpawnNetworkObject(object2Guid); + + var (playerObj, playerObjGuid) = MakeGameInterestObjectHelper(); + nc.PlayerObject = playerObj; + NetworkManagerHelper.SpawnNetworkObject(playerObjGuid); + + results.Clear(); + NetworkManager.Singleton.InterestManager.QueryFor(nc, results); + var hits = results.Count; + Assert.True(hits == (3 + objectsBeforeAdd)); + Assert.True(results.Contains(object1)); + Assert.True(results.Contains(object2)); + Assert.True(results.Contains(nc.PlayerObject)); + + // remove an object, should not be in replication manager + object1.Despawn(); + object2.Despawn(); + playerObj.Despawn(); + results.Clear(); + NetworkManager.Singleton.InterestManager.QueryFor(nc, results); + hits = results.Count; + Assert.True(hits == (objectsBeforeAdd)); + } + + [Test] + public void TestInterestSettings() + { + var rootSettings = ScriptableObject.CreateInstance(); + rootSettings.SomeSetting = 1; + NetworkManager.Singleton.InterestSettings = rootSettings; + + var objSettings = ScriptableObject.CreateInstance(); + objSettings.SomeSetting = 2; + var (object1Obj, object1Guid) = MakeGameInterestObjectHelper(); + object1Obj.InterestSettings = objSettings; + + // no override settings, should receive from NetworkManager + var (object2Obj, object2Guid) = MakeGameInterestObjectHelper(); + + var checkObj1 = (TestInterestSettings)object1Obj.InterestSettings; + var checkObj2 = (TestInterestSettings)object2Obj.InterestSettings; + + Assert.True(checkObj1.SomeSetting == 2); + Assert.True(checkObj2.SomeSetting == 1); + } + } +} diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/InterestTests.cs.meta b/com.unity.netcode.gameobjects/Tests/Runtime/InterestTests.cs.meta new file mode 100644 index 0000000000..325c1d142f --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Runtime/InterestTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6af04a8ba6cdf4185ba706d94a04ed64 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: