aha
This commit is contained in:
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fa4cbc6b9c584db4971985cb9f369077
|
||||
timeCreated: 1613110605
|
@ -0,0 +1,89 @@
|
||||
// straight forward Vector3.Distance based interest management.
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
[AddComponentMenu("Network/ Interest Management/ Distance/Distance Interest Management")]
|
||||
public class DistanceInterestManagement : InterestManagement
|
||||
{
|
||||
[Tooltip("The maximum range that objects will be visible at. Add DistanceInterestManagementCustomRange onto NetworkIdentities for custom ranges.")]
|
||||
public int visRange = 500;
|
||||
|
||||
[Tooltip("Rebuild all every 'rebuildInterval' seconds.")]
|
||||
public float rebuildInterval = 1;
|
||||
double lastRebuildTime;
|
||||
|
||||
// cache custom ranges to avoid runtime TryGetComponent lookups
|
||||
readonly Dictionary<NetworkIdentity, DistanceInterestManagementCustomRange> CustomRanges = new Dictionary<NetworkIdentity, DistanceInterestManagementCustomRange>();
|
||||
|
||||
// helper function to get vis range for a given object, or default.
|
||||
[ServerCallback]
|
||||
int GetVisRange(NetworkIdentity identity)
|
||||
{
|
||||
return CustomRanges.TryGetValue(identity, out DistanceInterestManagementCustomRange custom) ? custom.visRange : visRange;
|
||||
}
|
||||
|
||||
[ServerCallback]
|
||||
public override void ResetState()
|
||||
{
|
||||
lastRebuildTime = 0D;
|
||||
CustomRanges.Clear();
|
||||
}
|
||||
|
||||
public override void OnSpawned(NetworkIdentity identity)
|
||||
{
|
||||
if (identity.TryGetComponent(out DistanceInterestManagementCustomRange custom))
|
||||
CustomRanges[identity] = custom;
|
||||
}
|
||||
|
||||
public override void OnDestroyed(NetworkIdentity identity)
|
||||
{
|
||||
CustomRanges.Remove(identity);
|
||||
}
|
||||
|
||||
public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnectionToClient newObserver)
|
||||
{
|
||||
int range = GetVisRange(identity);
|
||||
return Vector3.Distance(identity.transform.position, newObserver.identity.transform.position) < range;
|
||||
}
|
||||
|
||||
public override void OnRebuildObservers(NetworkIdentity identity, HashSet<NetworkConnectionToClient> newObservers)
|
||||
{
|
||||
// cache range and .transform because both call GetComponent.
|
||||
int range = GetVisRange(identity);
|
||||
Vector3 position = identity.transform.position;
|
||||
|
||||
// brute force distance check
|
||||
// -> only player connections can be observers, so it's enough if we
|
||||
// go through all connections instead of all spawned identities.
|
||||
// -> compared to UNET's sphere cast checking, this one is orders of
|
||||
// magnitude faster. if we have 10k monsters and run a sphere
|
||||
// cast 10k times, we will see a noticeable lag even with physics
|
||||
// layers. but checking to every connection is fast.
|
||||
foreach (NetworkConnectionToClient conn in NetworkServer.connections.Values)
|
||||
{
|
||||
// authenticated and joined world with a player?
|
||||
if (conn != null && conn.isAuthenticated && conn.identity != null)
|
||||
{
|
||||
// check distance
|
||||
if (Vector3.Distance(conn.identity.transform.position, position) < range)
|
||||
{
|
||||
newObservers.Add(conn);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[ServerCallback]
|
||||
void LateUpdate()
|
||||
{
|
||||
// rebuild all spawned NetworkIdentity's observers every interval
|
||||
if (NetworkTime.localTime >= lastRebuildTime + rebuildInterval)
|
||||
{
|
||||
RebuildAll();
|
||||
lastRebuildTime = NetworkTime.localTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8f60becab051427fbdd3c8ac9ab4712b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Components/InterestManagement/Distance/DistanceInterestManagement.cs
|
||||
uploadId: 736421
|
@ -0,0 +1,15 @@
|
||||
// add this to NetworkIdentities for custom range if needed.
|
||||
// only works with DistanceInterestManagement.
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
[DisallowMultipleComponent]
|
||||
[AddComponentMenu("Network/ Interest Management/ Distance/Distance Custom Range")]
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/guides/interest-management")]
|
||||
public class DistanceInterestManagementCustomRange : NetworkBehaviour
|
||||
{
|
||||
[Tooltip("The maximum range that objects will be visible at.")]
|
||||
public int visRange = 100;
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b2e242ee38a14076a39934172a19079b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Components/InterestManagement/Distance/DistanceInterestManagementCustomRange.cs
|
||||
uploadId: 736421
|
3
Assets/Mirror/Components/InterestManagement/Match.meta
Normal file
3
Assets/Mirror/Components/InterestManagement/Match.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5eca5245ae6bb460e9a92f7e14d5493a
|
||||
timeCreated: 1622649517
|
@ -0,0 +1,164 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
[AddComponentMenu("Network/ Interest Management/ Match/Match Interest Management")]
|
||||
public class MatchInterestManagement : InterestManagement
|
||||
{
|
||||
[Header("Diagnostics")]
|
||||
[ReadOnly, SerializeField]
|
||||
internal ushort matchCount;
|
||||
|
||||
readonly Dictionary<Guid, HashSet<NetworkMatch>> matchObjects =
|
||||
new Dictionary<Guid, HashSet<NetworkMatch>>();
|
||||
|
||||
readonly HashSet<Guid> dirtyMatches = new HashSet<Guid>();
|
||||
|
||||
// LateUpdate so that all spawns/despawns/changes are done
|
||||
[ServerCallback]
|
||||
void LateUpdate()
|
||||
{
|
||||
// Rebuild all dirty matches
|
||||
// dirtyMatches will be empty if no matches changed members
|
||||
// by spawning or destroying or changing matchId in this frame.
|
||||
foreach (Guid dirtyMatch in dirtyMatches)
|
||||
{
|
||||
// rebuild always, even if matchObjects[dirtyMatch] is empty.
|
||||
// Players might have left the match, but they may still be spawned.
|
||||
RebuildMatchObservers(dirtyMatch);
|
||||
|
||||
// clean up empty entries in the dict
|
||||
if (matchObjects[dirtyMatch].Count == 0)
|
||||
matchObjects.Remove(dirtyMatch);
|
||||
}
|
||||
|
||||
dirtyMatches.Clear();
|
||||
|
||||
matchCount = (ushort)matchObjects.Count;
|
||||
}
|
||||
|
||||
[ServerCallback]
|
||||
void RebuildMatchObservers(Guid matchId)
|
||||
{
|
||||
foreach (NetworkMatch networkMatch in matchObjects[matchId])
|
||||
if (networkMatch.netIdentity != null)
|
||||
NetworkServer.RebuildObservers(networkMatch.netIdentity, false);
|
||||
}
|
||||
|
||||
// called by NetworkMatch.matchId setter
|
||||
[ServerCallback]
|
||||
internal void OnMatchChanged(NetworkMatch networkMatch, Guid oldMatch)
|
||||
{
|
||||
// This object is in a new match so observers in the prior match
|
||||
// and the new match need to rebuild their respective observers lists.
|
||||
|
||||
// Remove this object from the hashset of the match it just left
|
||||
// Guid.Empty is never a valid matchId
|
||||
if (oldMatch != Guid.Empty)
|
||||
{
|
||||
dirtyMatches.Add(oldMatch);
|
||||
matchObjects[oldMatch].Remove(networkMatch);
|
||||
}
|
||||
|
||||
// Guid.Empty is never a valid matchId
|
||||
if (networkMatch.matchId == Guid.Empty)
|
||||
return;
|
||||
|
||||
dirtyMatches.Add(networkMatch.matchId);
|
||||
|
||||
// Make sure this new match is in the dictionary
|
||||
if (!matchObjects.ContainsKey(networkMatch.matchId))
|
||||
matchObjects[networkMatch.matchId] = new HashSet<NetworkMatch>();
|
||||
|
||||
// Add this object to the hashset of the new match
|
||||
matchObjects[networkMatch.matchId].Add(networkMatch);
|
||||
}
|
||||
|
||||
[ServerCallback]
|
||||
public override void OnSpawned(NetworkIdentity identity)
|
||||
{
|
||||
if (!identity.TryGetComponent(out NetworkMatch networkMatch))
|
||||
return;
|
||||
|
||||
Guid networkMatchId = networkMatch.matchId;
|
||||
|
||||
// Guid.Empty is never a valid matchId...do not add to matchObjects collection
|
||||
if (networkMatchId == Guid.Empty)
|
||||
return;
|
||||
|
||||
// Debug.Log($"MatchInterestManagement.OnSpawned({identity.name}) currentMatch: {currentMatch}");
|
||||
if (!matchObjects.TryGetValue(networkMatchId, out HashSet<NetworkMatch> objects))
|
||||
{
|
||||
objects = new HashSet<NetworkMatch>();
|
||||
matchObjects.Add(networkMatchId, objects);
|
||||
}
|
||||
|
||||
objects.Add(networkMatch);
|
||||
|
||||
// Match ID could have been set in NetworkBehaviour::OnStartServer on this object.
|
||||
// Since that's after OnCheckObserver is called it would be missed, so force Rebuild here.
|
||||
// Add the current match to dirtyMatches for LateUpdate to rebuild it.
|
||||
dirtyMatches.Add(networkMatchId);
|
||||
}
|
||||
|
||||
[ServerCallback]
|
||||
public override void OnDestroyed(NetworkIdentity identity)
|
||||
{
|
||||
// Don't RebuildSceneObservers here - that will happen in LateUpdate.
|
||||
// Multiple objects could be destroyed in same frame and we don't
|
||||
// want to rebuild for each one...let LateUpdate do it once.
|
||||
// We must add the current match to dirtyMatches for LateUpdate to rebuild it.
|
||||
if (identity.TryGetComponent(out NetworkMatch currentMatch))
|
||||
{
|
||||
if (currentMatch.matchId != Guid.Empty &&
|
||||
matchObjects.TryGetValue(currentMatch.matchId, out HashSet<NetworkMatch> objects) &&
|
||||
objects.Remove(currentMatch))
|
||||
dirtyMatches.Add(currentMatch.matchId);
|
||||
}
|
||||
}
|
||||
|
||||
[ServerCallback]
|
||||
public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnectionToClient newObserver)
|
||||
{
|
||||
// Never observed if no NetworkMatch component
|
||||
if (!identity.TryGetComponent(out NetworkMatch identityNetworkMatch))
|
||||
return false;
|
||||
|
||||
// Guid.Empty is never a valid matchId
|
||||
if (identityNetworkMatch.matchId == Guid.Empty)
|
||||
return false;
|
||||
|
||||
// Never observed if no NetworkMatch component
|
||||
if (!newObserver.identity.TryGetComponent(out NetworkMatch newObserverNetworkMatch))
|
||||
return false;
|
||||
|
||||
// Guid.Empty is never a valid matchId
|
||||
if (newObserverNetworkMatch.matchId == Guid.Empty)
|
||||
return false;
|
||||
|
||||
return identityNetworkMatch.matchId == newObserverNetworkMatch.matchId;
|
||||
}
|
||||
|
||||
[ServerCallback]
|
||||
public override void OnRebuildObservers(NetworkIdentity identity, HashSet<NetworkConnectionToClient> newObservers)
|
||||
{
|
||||
if (!identity.TryGetComponent(out NetworkMatch networkMatch))
|
||||
return;
|
||||
|
||||
// Guid.Empty is never a valid matchId
|
||||
if (networkMatch.matchId == Guid.Empty)
|
||||
return;
|
||||
|
||||
// Abort if this match hasn't been created yet by OnSpawned or OnMatchChanged
|
||||
if (!matchObjects.TryGetValue(networkMatch.matchId, out HashSet<NetworkMatch> objects))
|
||||
return;
|
||||
|
||||
// Add everything in the hashset for this object's current match
|
||||
foreach (NetworkMatch netMatch in objects)
|
||||
if (netMatch.netIdentity != null && netMatch.netIdentity.connectionToClient != null)
|
||||
newObservers.Add(netMatch.netIdentity.connectionToClient);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d09f5c8bf2f4747b7a9284ef5d9ce2a7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Components/InterestManagement/Match/MatchInterestManagement.cs
|
||||
uploadId: 736421
|
@ -0,0 +1,42 @@
|
||||
// simple component that holds match information
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
[DisallowMultipleComponent]
|
||||
[AddComponentMenu("Network/ Interest Management/ Match/Network Match")]
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/guides/interest-management")]
|
||||
public class NetworkMatch : NetworkBehaviour
|
||||
{
|
||||
Guid _matchId;
|
||||
|
||||
#pragma warning disable IDE0052 // Suppress warning for unused field...this is for debugging purposes
|
||||
[SerializeField, ReadOnly]
|
||||
[Tooltip("Match ID is shown here on server for debugging purposes.")]
|
||||
string MatchID = string.Empty;
|
||||
#pragma warning restore IDE0052
|
||||
|
||||
///<summary>Set this to the same value on all networked objects that belong to a given match</summary>
|
||||
public Guid matchId
|
||||
{
|
||||
get => _matchId;
|
||||
set
|
||||
{
|
||||
if (!NetworkServer.active)
|
||||
throw new InvalidOperationException("matchId can only be set at runtime on active server");
|
||||
|
||||
if (_matchId == value)
|
||||
return;
|
||||
|
||||
Guid oldMatch = _matchId;
|
||||
_matchId = value;
|
||||
MatchID = value.ToString();
|
||||
|
||||
// Only inform the AOI if this netIdentity has been spawned (isServer) and only if using a MatchInterestManagement
|
||||
if (isServer && NetworkServer.aoi is MatchInterestManagement matchInterestManagement)
|
||||
matchInterestManagement.OnMatchChanged(this, oldMatch);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5d17e718851449a6879986e45c458fb7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Components/InterestManagement/Match/NetworkMatch.cs
|
||||
uploadId: 736421
|
3
Assets/Mirror/Components/InterestManagement/Scene.meta
Normal file
3
Assets/Mirror/Components/InterestManagement/Scene.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7655d309a46a4bd4860edf964228b3f6
|
||||
timeCreated: 1622649517
|
@ -0,0 +1,117 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
[AddComponentMenu("Network/ Interest Management/ Scene/Scene Interest Management")]
|
||||
public class SceneInterestManagement : InterestManagement
|
||||
{
|
||||
// Use Scene instead of string scene.name because when additively
|
||||
// loading multiples of a subscene the name won't be unique
|
||||
readonly Dictionary<Scene, HashSet<NetworkIdentity>> sceneObjects =
|
||||
new Dictionary<Scene, HashSet<NetworkIdentity>>();
|
||||
|
||||
readonly Dictionary<NetworkIdentity, Scene> lastObjectScene =
|
||||
new Dictionary<NetworkIdentity, Scene>();
|
||||
|
||||
HashSet<Scene> dirtyScenes = new HashSet<Scene>();
|
||||
|
||||
[ServerCallback]
|
||||
public override void OnSpawned(NetworkIdentity identity)
|
||||
{
|
||||
Scene currentScene = identity.gameObject.scene;
|
||||
lastObjectScene[identity] = currentScene;
|
||||
// Debug.Log($"SceneInterestManagement.OnSpawned({identity.name}) currentScene: {currentScene}");
|
||||
if (!sceneObjects.TryGetValue(currentScene, out HashSet<NetworkIdentity> objects))
|
||||
{
|
||||
objects = new HashSet<NetworkIdentity>();
|
||||
sceneObjects.Add(currentScene, objects);
|
||||
}
|
||||
|
||||
objects.Add(identity);
|
||||
}
|
||||
|
||||
[ServerCallback]
|
||||
public override void OnDestroyed(NetworkIdentity identity)
|
||||
{
|
||||
// Don't RebuildSceneObservers here - that will happen in LateUpdate.
|
||||
// Multiple objects could be destroyed in same frame and we don't
|
||||
// want to rebuild for each one...let LateUpdate do it once.
|
||||
// We must add the current scene to dirtyScenes for LateUpdate to rebuild it.
|
||||
if (lastObjectScene.TryGetValue(identity, out Scene currentScene))
|
||||
{
|
||||
lastObjectScene.Remove(identity);
|
||||
if (sceneObjects.TryGetValue(currentScene, out HashSet<NetworkIdentity> objects) && objects.Remove(identity))
|
||||
dirtyScenes.Add(currentScene);
|
||||
}
|
||||
}
|
||||
|
||||
[ServerCallback]
|
||||
void LateUpdate()
|
||||
{
|
||||
// for each spawned:
|
||||
// if scene changed:
|
||||
// add previous to dirty
|
||||
// add new to dirty
|
||||
foreach (NetworkIdentity identity in NetworkServer.spawned.Values)
|
||||
{
|
||||
if (!lastObjectScene.TryGetValue(identity, out Scene currentScene))
|
||||
continue;
|
||||
|
||||
Scene newScene = identity.gameObject.scene;
|
||||
if (newScene == currentScene)
|
||||
continue;
|
||||
|
||||
// Mark new/old scenes as dirty so they get rebuilt
|
||||
dirtyScenes.Add(currentScene);
|
||||
dirtyScenes.Add(newScene);
|
||||
|
||||
// This object is in a new scene so observers in the prior scene
|
||||
// and the new scene need to rebuild their respective observers lists.
|
||||
|
||||
// Remove this object from the hashset of the scene it just left
|
||||
sceneObjects[currentScene].Remove(identity);
|
||||
|
||||
// Set this to the new scene this object just entered
|
||||
lastObjectScene[identity] = newScene;
|
||||
|
||||
// Make sure this new scene is in the dictionary
|
||||
if (!sceneObjects.ContainsKey(newScene))
|
||||
sceneObjects.Add(newScene, new HashSet<NetworkIdentity>());
|
||||
|
||||
// Add this object to the hashset of the new scene
|
||||
sceneObjects[newScene].Add(identity);
|
||||
}
|
||||
|
||||
// rebuild all dirty scenes
|
||||
foreach (Scene dirtyScene in dirtyScenes)
|
||||
RebuildSceneObservers(dirtyScene);
|
||||
|
||||
dirtyScenes.Clear();
|
||||
}
|
||||
|
||||
void RebuildSceneObservers(Scene scene)
|
||||
{
|
||||
foreach (NetworkIdentity netIdentity in sceneObjects[scene])
|
||||
if (netIdentity != null)
|
||||
NetworkServer.RebuildObservers(netIdentity, false);
|
||||
}
|
||||
|
||||
public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnectionToClient newObserver)
|
||||
{
|
||||
return identity.gameObject.scene == newObserver.identity.gameObject.scene;
|
||||
}
|
||||
|
||||
public override void OnRebuildObservers(NetworkIdentity identity, HashSet<NetworkConnectionToClient> newObservers)
|
||||
{
|
||||
if (!sceneObjects.TryGetValue(identity.gameObject.scene, out HashSet<NetworkIdentity> objects))
|
||||
return;
|
||||
|
||||
// Add everything in the hashset for this object's current scene
|
||||
foreach (NetworkIdentity networkIdentity in objects)
|
||||
if (networkIdentity != null && networkIdentity.connectionToClient != null)
|
||||
newObservers.Add(networkIdentity.connectionToClient);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b979f26c95d34324ba005bfacfa9c4fc
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Components/InterestManagement/Scene/SceneInterestManagement.cs
|
||||
uploadId: 736421
|
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f4d8c634a8103664db5f90fe8bab9544
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -0,0 +1,178 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
[AddComponentMenu("Network/ Interest Management/ Scene/Scene Distance Interest Management")]
|
||||
public class SceneDistanceInterestManagement : InterestManagement
|
||||
{
|
||||
[Tooltip("The maximum range that objects will be visible at. Add DistanceInterestManagementCustomRange onto NetworkIdentities for custom ranges.")]
|
||||
public int visRange = 500;
|
||||
|
||||
[Tooltip("Rebuild all every 'rebuildInterval' seconds.")]
|
||||
public float rebuildInterval = 1;
|
||||
double lastRebuildTime;
|
||||
|
||||
// cache custom ranges to avoid runtime TryGetComponent lookups
|
||||
readonly Dictionary<NetworkIdentity, DistanceInterestManagementCustomRange> CustomRanges = new Dictionary<NetworkIdentity, DistanceInterestManagementCustomRange>();
|
||||
|
||||
// helper function to get vis range for a given object, or default.
|
||||
[ServerCallback]
|
||||
int GetVisRange(NetworkIdentity identity)
|
||||
{
|
||||
return CustomRanges.TryGetValue(identity, out DistanceInterestManagementCustomRange custom) ? custom.visRange : visRange;
|
||||
}
|
||||
|
||||
[ServerCallback]
|
||||
public override void ResetState()
|
||||
{
|
||||
lastRebuildTime = 0D;
|
||||
CustomRanges.Clear();
|
||||
}
|
||||
|
||||
// Use Scene instead of string scene.name because when additively
|
||||
// loading multiples of a subscene the name won't be unique
|
||||
readonly Dictionary<Scene, HashSet<NetworkIdentity>> sceneObjects =
|
||||
new Dictionary<Scene, HashSet<NetworkIdentity>>();
|
||||
|
||||
readonly Dictionary<NetworkIdentity, Scene> lastObjectScene =
|
||||
new Dictionary<NetworkIdentity, Scene>();
|
||||
|
||||
HashSet<Scene> dirtyScenes = new HashSet<Scene>();
|
||||
|
||||
[ServerCallback]
|
||||
public override void OnSpawned(NetworkIdentity identity)
|
||||
{
|
||||
if (identity.TryGetComponent(out DistanceInterestManagementCustomRange custom))
|
||||
CustomRanges[identity] = custom;
|
||||
|
||||
Scene currentScene = identity.gameObject.scene;
|
||||
lastObjectScene[identity] = currentScene;
|
||||
// Debug.Log($"SceneInterestManagement.OnSpawned({identity.name}) currentScene: {currentScene}");
|
||||
if (!sceneObjects.TryGetValue(currentScene, out HashSet<NetworkIdentity> objects))
|
||||
{
|
||||
objects = new HashSet<NetworkIdentity>();
|
||||
sceneObjects.Add(currentScene, objects);
|
||||
}
|
||||
|
||||
objects.Add(identity);
|
||||
}
|
||||
|
||||
[ServerCallback]
|
||||
public override void OnDestroyed(NetworkIdentity identity)
|
||||
{
|
||||
CustomRanges.Remove(identity);
|
||||
|
||||
// Don't RebuildSceneObservers here - that will happen in LateUpdate.
|
||||
// Multiple objects could be destroyed in same frame and we don't
|
||||
// want to rebuild for each one...let LateUpdate do it once.
|
||||
// We must add the current scene to dirtyScenes for LateUpdate to rebuild it.
|
||||
if (lastObjectScene.TryGetValue(identity, out Scene currentScene))
|
||||
{
|
||||
lastObjectScene.Remove(identity);
|
||||
if (sceneObjects.TryGetValue(currentScene, out HashSet<NetworkIdentity> objects) && objects.Remove(identity))
|
||||
dirtyScenes.Add(currentScene);
|
||||
}
|
||||
}
|
||||
|
||||
[ServerCallback]
|
||||
void LateUpdate()
|
||||
{
|
||||
// for each spawned:
|
||||
// if scene changed:
|
||||
// add previous to dirty
|
||||
// add new to dirty
|
||||
// else
|
||||
// if rebuild interval reached:
|
||||
// rebuild all
|
||||
foreach (NetworkIdentity identity in NetworkServer.spawned.Values)
|
||||
{
|
||||
if (!lastObjectScene.TryGetValue(identity, out Scene currentScene))
|
||||
continue;
|
||||
|
||||
Scene newScene = identity.gameObject.scene;
|
||||
if (newScene == currentScene)
|
||||
{
|
||||
if (NetworkTime.localTime >= lastRebuildTime + rebuildInterval)
|
||||
{
|
||||
RebuildAll();
|
||||
lastRebuildTime = NetworkTime.localTime;
|
||||
}
|
||||
|
||||
// no scene change, so we're done here
|
||||
continue;
|
||||
}
|
||||
|
||||
// Mark new/old scenes as dirty so they get rebuilt
|
||||
dirtyScenes.Add(currentScene);
|
||||
dirtyScenes.Add(newScene);
|
||||
|
||||
// This object is in a new scene so observers in the prior scene
|
||||
// and the new scene need to rebuild their respective observers lists.
|
||||
|
||||
// Remove this object from the hashset of the scene it just left
|
||||
sceneObjects[currentScene].Remove(identity);
|
||||
|
||||
// Set this to the new scene this object just entered
|
||||
lastObjectScene[identity] = newScene;
|
||||
|
||||
// Make sure this new scene is in the dictionary
|
||||
if (!sceneObjects.ContainsKey(newScene))
|
||||
sceneObjects.Add(newScene, new HashSet<NetworkIdentity>());
|
||||
|
||||
// Add this object to the hashset of the new scene
|
||||
sceneObjects[newScene].Add(identity);
|
||||
}
|
||||
|
||||
// rebuild all dirty scenes
|
||||
foreach (Scene dirtyScene in dirtyScenes)
|
||||
RebuildSceneObservers(dirtyScene);
|
||||
|
||||
dirtyScenes.Clear();
|
||||
}
|
||||
|
||||
void RebuildSceneObservers(Scene scene)
|
||||
{
|
||||
foreach (NetworkIdentity netIdentity in sceneObjects[scene])
|
||||
if (netIdentity != null)
|
||||
NetworkServer.RebuildObservers(netIdentity, false);
|
||||
}
|
||||
|
||||
public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnectionToClient newObserver)
|
||||
{
|
||||
// Check for scene match first, then distance
|
||||
if (identity.gameObject.scene != newObserver.identity.gameObject.scene) return false;
|
||||
|
||||
int range = GetVisRange(identity);
|
||||
return Vector3.Distance(identity.transform.position, newObserver.identity.transform.position) < range;
|
||||
}
|
||||
|
||||
public override void OnRebuildObservers(NetworkIdentity identity, HashSet<NetworkConnectionToClient> newObservers)
|
||||
{
|
||||
// abort if no entry in sceneObjects yet (created in OnSpawned)
|
||||
if (!sceneObjects.TryGetValue(identity.gameObject.scene, out HashSet<NetworkIdentity> objects))
|
||||
return;
|
||||
|
||||
int range = GetVisRange(identity);
|
||||
Vector3 position = identity.transform.position;
|
||||
|
||||
// Add everything in the hashset for this object's current scene if within range
|
||||
foreach (NetworkIdentity networkIdentity in objects)
|
||||
if (networkIdentity != null && networkIdentity.connectionToClient != null)
|
||||
{
|
||||
// brute force distance check
|
||||
// -> only player connections can be observers, so it's enough if we
|
||||
// go through all connections instead of all spawned identities.
|
||||
// -> compared to UNET's sphere cast checking, this one is orders of
|
||||
// magnitude faster. if we have 10k monsters and run a sphere
|
||||
// cast 10k times, we will see a noticeable lag even with physics
|
||||
// layers. but checking to every connection is fast.
|
||||
NetworkConnectionToClient conn = networkIdentity.connectionToClient;
|
||||
if (conn != null && conn.isAuthenticated && conn.identity != null)
|
||||
if (Vector3.Distance(conn.identity.transform.position, position) < range)
|
||||
newObservers.Add(conn);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6471bc9a8f893944783fd54e9bfb6ed2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Components/InterestManagement/SceneDistance/SceneDistanceInterestManagement.cs
|
||||
uploadId: 736421
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cfa12b73503344d49b398b01bcb07967
|
||||
timeCreated: 1613110634
|
@ -0,0 +1,104 @@
|
||||
// Grid2D from uMMORPG: get/set values of type T at any point
|
||||
// -> not named 'Grid' because Unity already has a Grid type. causes warnings.
|
||||
// -> struct to avoid memory indirection. it's accessed a lot.
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
// struct to avoid memory indirection. it's accessed a lot.
|
||||
public struct Grid2D<T>
|
||||
{
|
||||
// the grid
|
||||
// note that we never remove old keys.
|
||||
// => over time, HashSet<T>s will be allocated for every possible
|
||||
// grid position in the world
|
||||
// => Clear() doesn't clear them so we don't constantly reallocate the
|
||||
// entries when populating the grid in every Update() call
|
||||
// => makes the code a lot easier too
|
||||
// => this is FINE because in the worst case, every grid position in the
|
||||
// game world is filled with a player anyway!
|
||||
readonly Dictionary<Vector2Int, HashSet<T>> grid;
|
||||
|
||||
// cache a 9 neighbor grid of vector2 offsets so we can use them more easily
|
||||
readonly Vector2Int[] neighbourOffsets;
|
||||
|
||||
public Grid2D(int initialCapacity)
|
||||
{
|
||||
grid = new Dictionary<Vector2Int, HashSet<T>>(initialCapacity);
|
||||
|
||||
neighbourOffsets = new[] {
|
||||
Vector2Int.up,
|
||||
Vector2Int.up + Vector2Int.left,
|
||||
Vector2Int.up + Vector2Int.right,
|
||||
Vector2Int.left,
|
||||
Vector2Int.zero,
|
||||
Vector2Int.right,
|
||||
Vector2Int.down,
|
||||
Vector2Int.down + Vector2Int.left,
|
||||
Vector2Int.down + Vector2Int.right
|
||||
};
|
||||
}
|
||||
|
||||
// helper function so we can add an entry without worrying
|
||||
public void Add(Vector2Int position, T value)
|
||||
{
|
||||
// initialize set in grid if it's not in there yet
|
||||
if (!grid.TryGetValue(position, out HashSet<T> hashSet))
|
||||
{
|
||||
// each grid entry may hold hundreds of entities.
|
||||
// let's create the HashSet with a large initial capacity
|
||||
// in order to avoid resizing & allocations.
|
||||
#if !UNITY_2021_3_OR_NEWER
|
||||
// Unity 2019 doesn't have "new HashSet(capacity)" yet
|
||||
hashSet = new HashSet<T>();
|
||||
#else
|
||||
hashSet = new HashSet<T>(128);
|
||||
#endif
|
||||
grid[position] = hashSet;
|
||||
}
|
||||
|
||||
// add to it
|
||||
hashSet.Add(value);
|
||||
}
|
||||
|
||||
// helper function to get set at position without worrying
|
||||
// -> result is passed as parameter to avoid allocations
|
||||
// -> result is not cleared before. this allows us to pass the HashSet from
|
||||
// GetWithNeighbours and avoid .UnionWith which is very expensive.
|
||||
void GetAt(Vector2Int position, HashSet<T> result)
|
||||
{
|
||||
// return the set at position
|
||||
if (grid.TryGetValue(position, out HashSet<T> hashSet))
|
||||
{
|
||||
foreach (T entry in hashSet)
|
||||
result.Add(entry);
|
||||
}
|
||||
}
|
||||
|
||||
// helper function to get at position and it's 8 neighbors without worrying
|
||||
// -> result is passed as parameter to avoid allocations
|
||||
public void GetWithNeighbours(Vector2Int position, HashSet<T> result)
|
||||
{
|
||||
// clear result first
|
||||
result.Clear();
|
||||
|
||||
// add neighbours
|
||||
foreach (Vector2Int offset in neighbourOffsets)
|
||||
GetAt(position + offset, result);
|
||||
}
|
||||
|
||||
// clear: clears the whole grid
|
||||
// IMPORTANT: we already allocated HashSet<T>s and don't want to do
|
||||
// reallocate every single update when we rebuild the grid.
|
||||
// => so simply remove each position's entries, but keep
|
||||
// every position in there
|
||||
// => see 'grid' comments above!
|
||||
// => named ClearNonAlloc to make it more obvious!
|
||||
public void ClearNonAlloc()
|
||||
{
|
||||
foreach (HashSet<T> hashSet in grid.Values)
|
||||
hashSet.Clear();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7c5232a4d2854116a35d52b80ec07752
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Components/InterestManagement/SpatialHashing/Grid2D.cs
|
||||
uploadId: 736421
|
@ -0,0 +1,106 @@
|
||||
// Grid3D based on Grid2D
|
||||
// -> not named 'Grid' because Unity already has a Grid type. causes warnings.
|
||||
// -> struct to avoid memory indirection. it's accessed a lot.
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
// struct to avoid memory indirection. it's accessed a lot.
|
||||
public struct Grid3D<T>
|
||||
{
|
||||
// the grid
|
||||
// note that we never remove old keys.
|
||||
// => over time, HashSet<T>s will be allocated for every possible
|
||||
// grid position in the world
|
||||
// => Clear() doesn't clear them so we don't constantly reallocate the
|
||||
// entries when populating the grid in every Update() call
|
||||
// => makes the code a lot easier too
|
||||
// => this is FINE because in the worst case, every grid position in the
|
||||
// game world is filled with a player anyway!
|
||||
readonly Dictionary<Vector3Int, HashSet<T>> grid;
|
||||
|
||||
// cache a 9 x 3 neighbor grid of vector3 offsets so we can use them more easily
|
||||
readonly Vector3Int[] neighbourOffsets;
|
||||
|
||||
public Grid3D(int initialCapacity)
|
||||
{
|
||||
grid = new Dictionary<Vector3Int, HashSet<T>>(initialCapacity);
|
||||
|
||||
neighbourOffsets = new Vector3Int[9 * 3];
|
||||
int i = 0;
|
||||
for (int x = -1; x <= 1; x++)
|
||||
{
|
||||
for (int y = -1; y <= 1; y++)
|
||||
{
|
||||
for (int z = -1; z <= 1; z++)
|
||||
{
|
||||
neighbourOffsets[i] = new Vector3Int(x, y, z);
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// helper function so we can add an entry without worrying
|
||||
public void Add(Vector3Int position, T value)
|
||||
{
|
||||
// initialize set in grid if it's not in there yet
|
||||
if (!grid.TryGetValue(position, out HashSet<T> hashSet))
|
||||
{
|
||||
// each grid entry may hold hundreds of entities.
|
||||
// let's create the HashSet with a large initial capacity
|
||||
// in order to avoid resizing & allocations.
|
||||
#if !UNITY_2021_3_OR_NEWER
|
||||
// Unity 2019 doesn't have "new HashSet(capacity)" yet
|
||||
hashSet = new HashSet<T>();
|
||||
#else
|
||||
hashSet = new HashSet<T>(128);
|
||||
#endif
|
||||
grid[position] = hashSet;
|
||||
}
|
||||
|
||||
// add to it
|
||||
hashSet.Add(value);
|
||||
}
|
||||
|
||||
// helper function to get set at position without worrying
|
||||
// -> result is passed as parameter to avoid allocations
|
||||
// -> result is not cleared before. this allows us to pass the HashSet from
|
||||
// GetWithNeighbours and avoid .UnionWith which is very expensive.
|
||||
void GetAt(Vector3Int position, HashSet<T> result)
|
||||
{
|
||||
// return the set at position
|
||||
if (grid.TryGetValue(position, out HashSet<T> hashSet))
|
||||
{
|
||||
foreach (T entry in hashSet)
|
||||
result.Add(entry);
|
||||
}
|
||||
}
|
||||
|
||||
// helper function to get at position and it's 8 neighbors without worrying
|
||||
// -> result is passed as parameter to avoid allocations
|
||||
public void GetWithNeighbours(Vector3Int position, HashSet<T> result)
|
||||
{
|
||||
// clear result first
|
||||
result.Clear();
|
||||
|
||||
// add neighbours
|
||||
foreach (Vector3Int offset in neighbourOffsets)
|
||||
GetAt(position + offset, result);
|
||||
}
|
||||
|
||||
// clear: clears the whole grid
|
||||
// IMPORTANT: we already allocated HashSet<T>s and don't want to do
|
||||
// reallocate every single update when we rebuild the grid.
|
||||
// => so simply remove each position's entries, but keep
|
||||
// every position in there
|
||||
// => see 'grid' comments above!
|
||||
// => named ClearNonAlloc to make it more obvious!
|
||||
public void ClearNonAlloc()
|
||||
{
|
||||
foreach (HashSet<T> hashSet in grid.Values)
|
||||
hashSet.Clear();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b157c08313c64752b0856469b1b70771
|
||||
timeCreated: 1713533175
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Components/InterestManagement/SpatialHashing/Grid3D.cs
|
||||
uploadId: 736421
|
@ -0,0 +1,170 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
internal class HexGrid2D
|
||||
{
|
||||
// Radius of each hexagonal cell (half the width)
|
||||
internal float cellRadius;
|
||||
|
||||
// Offset applied to align the grid with the world origin
|
||||
Vector2 originOffset;
|
||||
|
||||
// Precomputed constants for hexagon math to improve performance
|
||||
readonly float sqrt3Div3; // sqrt(3) / 3, used in coordinate conversions
|
||||
readonly float oneDiv3; // 1 / 3, used in coordinate conversions
|
||||
readonly float twoDiv3; // 2 / 3, used in coordinate conversions
|
||||
readonly float sqrt3; // sqrt(3), used in world coordinate calculations
|
||||
readonly float sqrt3Div2; // sqrt(3) / 2, used in world coordinate calculations
|
||||
|
||||
internal HexGrid2D(ushort visRange)
|
||||
{
|
||||
// Set cell radius as half the visibility range
|
||||
cellRadius = visRange / 2f;
|
||||
|
||||
// Offset to center the grid at world origin (2D XZ plane)
|
||||
originOffset = Vector2.zero;
|
||||
|
||||
// Precompute mathematical constants for efficiency
|
||||
sqrt3Div3 = Mathf.Sqrt(3) / 3f;
|
||||
oneDiv3 = 1f / 3f;
|
||||
twoDiv3 = 2f / 3f;
|
||||
sqrt3 = Mathf.Sqrt(3);
|
||||
sqrt3Div2 = Mathf.Sqrt(3) / 2f;
|
||||
}
|
||||
|
||||
// Precomputed array of neighbor offsets as Cell2D structs (center + 6 neighbors)
|
||||
static readonly Cell2D[] neighborCellsBase = new Cell2D[]
|
||||
{
|
||||
new Cell2D(0, 0), // Center
|
||||
new Cell2D(1, -1), // Top-right
|
||||
new Cell2D(1, 0), // Right
|
||||
new Cell2D(0, 1), // Bottom-right
|
||||
new Cell2D(-1, 1), // Bottom-left
|
||||
new Cell2D(-1, 0), // Left
|
||||
new Cell2D(0, -1) // Top-left
|
||||
};
|
||||
|
||||
// Converts a grid cell (q, r) to a world position (x, z)
|
||||
internal Vector2 CellToWorld(Cell2D cell)
|
||||
{
|
||||
// Calculate X and Z using hexagonal coordinate formulas
|
||||
float x = cellRadius * (sqrt3 * cell.q + sqrt3Div2 * cell.r);
|
||||
float z = cellRadius * (1.5f * cell.r);
|
||||
|
||||
// Subtract the origin offset to align with world space and return the position
|
||||
return new Vector2(x, z) - originOffset;
|
||||
}
|
||||
|
||||
// Converts a world position (x, z) to a grid cell (q, r)
|
||||
internal Cell2D WorldToCell(Vector2 position)
|
||||
{
|
||||
// Apply the origin offset to adjust the position before conversion
|
||||
position += originOffset;
|
||||
|
||||
// Convert world X, Z to axial q, r coordinates using inverse hexagonal formulas
|
||||
float q = (sqrt3Div3 * position.x - oneDiv3 * position.y) / cellRadius;
|
||||
float r = (twoDiv3 * position.y) / cellRadius;
|
||||
|
||||
// Round to the nearest valid cell and return
|
||||
return RoundToCell(q, r);
|
||||
}
|
||||
|
||||
// Rounds floating-point axial coordinates (q, r) to the nearest integer cell coordinates
|
||||
Cell2D RoundToCell(float q, float r)
|
||||
{
|
||||
// Calculate the third hexagonal coordinate (s) for consistency
|
||||
float s = -q - r;
|
||||
int qInt = Mathf.RoundToInt(q); // Round q to nearest integer
|
||||
int rInt = Mathf.RoundToInt(r); // Round r to nearest integer
|
||||
int sInt = Mathf.RoundToInt(s); // Round s to nearest integer
|
||||
|
||||
// Calculate differences to determine which coordinate needs adjustment
|
||||
float qDiff = Mathf.Abs(q - qInt);
|
||||
float rDiff = Mathf.Abs(r - rInt);
|
||||
float sDiff = Mathf.Abs(s - sInt);
|
||||
|
||||
// Adjust q or r based on which has the largest rounding error (ensures q + r + s = 0)
|
||||
if (qDiff > rDiff && qDiff > sDiff)
|
||||
qInt = -rInt - sInt; // Adjust q if it has the largest error
|
||||
else if (rDiff > sDiff)
|
||||
rInt = -qInt - sInt; // Adjust r if it has the largest error
|
||||
|
||||
return new Cell2D(qInt, rInt);
|
||||
}
|
||||
|
||||
// Populates the provided array with neighboring cells around a given center cell
|
||||
internal void GetNeighborCells(Cell2D center, Cell2D[] neighbors)
|
||||
{
|
||||
// Ensure the array has the correct size (7: center + 6 neighbors)
|
||||
if (neighbors.Length != 7)
|
||||
throw new System.ArgumentException("Neighbor array must have exactly 7 elements");
|
||||
|
||||
// Populate the array by adjusting precomputed offsets with the center cell's coordinates
|
||||
for (int i = 0; i < neighborCellsBase.Length; i++)
|
||||
{
|
||||
neighbors[i] = new Cell2D(
|
||||
center.q + neighborCellsBase[i].q,
|
||||
center.r + neighborCellsBase[i].r
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
// Draws a 2D hexagonal gizmo in the Unity Editor for visualization
|
||||
internal void DrawHexGizmo(Vector3 center, float radius, HexSpatialHash2DInterestManagement.CheckMethod checkMethod)
|
||||
{
|
||||
// Hexagon has 6 sides
|
||||
const int segments = 6;
|
||||
|
||||
// Array to store the 6 corner points in 3D
|
||||
Vector3[] corners = new Vector3[segments];
|
||||
|
||||
// Calculate the corner positions based on the plane (XZ or XY)
|
||||
for (int i = 0; i < segments; i++)
|
||||
{
|
||||
// Angle for each corner, offset by 90 degrees
|
||||
float angle = 2 * Mathf.PI / segments * i + Mathf.PI / 2;
|
||||
|
||||
if (checkMethod == HexSpatialHash2DInterestManagement.CheckMethod.XZ_FOR_3D)
|
||||
{
|
||||
// XZ plane: flat hexagon, Y=0
|
||||
corners[i] = center + new Vector3(radius * Mathf.Cos(angle), 0, radius * Mathf.Sin(angle));
|
||||
}
|
||||
else // XY_FOR_2D
|
||||
{
|
||||
// XY plane: vertical hexagon, Z=0
|
||||
corners[i] = center + new Vector3(radius * Mathf.Cos(angle), radius * Mathf.Sin(angle), 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw each side of the hexagon
|
||||
for (int i = 0; i < segments; i++)
|
||||
{
|
||||
Vector3 cornerA = corners[i];
|
||||
Vector3 cornerB = corners[(i + 1) % segments];
|
||||
Gizmos.DrawLine(cornerA, cornerB);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// Struct representing a single cell in the 2D hexagonal grid
|
||||
internal struct Cell2D
|
||||
{
|
||||
internal readonly int q; // Axial q coordinate (horizontal axis)
|
||||
internal readonly int r; // Axial r coordinate (diagonal axis)
|
||||
|
||||
internal Cell2D(int q, int r)
|
||||
{
|
||||
this.q = q;
|
||||
this.r = r;
|
||||
}
|
||||
|
||||
public override bool Equals(object obj) =>
|
||||
obj is Cell2D other && q == other.q && r == other.r;
|
||||
|
||||
// Generate a unique hash code for the cell
|
||||
public override int GetHashCode() => (q << 16) ^ r;
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e9b8dc0273250624c91b6681065741ff
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Components/InterestManagement/SpatialHashing/HexGrid2D.cs
|
||||
uploadId: 736421
|
@ -0,0 +1,243 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
internal class HexGrid3D
|
||||
{
|
||||
// Radius of each hexagonal cell (half the width)
|
||||
internal float cellRadius;
|
||||
|
||||
// Height of each cell along the Y-axis
|
||||
internal float cellHeight;
|
||||
|
||||
// Offset applied to align the grid with the world origin
|
||||
Vector3 originOffset;
|
||||
|
||||
// Precomputed constants for hexagon math to improve performance
|
||||
readonly float sqrt3Div3; // sqrt(3) / 3, used in coordinate conversions
|
||||
readonly float oneDiv3; // 1 / 3, used in coordinate conversions
|
||||
readonly float twoDiv3; // 2 / 3, used in coordinate conversions
|
||||
readonly float sqrt3; // sqrt(3), used in world coordinate calculations
|
||||
readonly float sqrt3Div2; // sqrt(3) / 2, used in world coordinate calculations
|
||||
|
||||
internal HexGrid3D(ushort visRange, ushort height)
|
||||
{
|
||||
// Set cell radius as half the visibility range
|
||||
cellRadius = visRange / 2f;
|
||||
|
||||
// Cell3D height is absolute...don't double it
|
||||
cellHeight = height;
|
||||
|
||||
// Offset to center the grid at world origin
|
||||
// Cell3D height must be divided by 2 for vertical centering
|
||||
originOffset = new Vector3(0, -cellHeight / 2, 0);
|
||||
|
||||
// Precompute mathematical constants for efficiency
|
||||
sqrt3Div3 = Mathf.Sqrt(3) / 3f;
|
||||
oneDiv3 = 1f / 3f;
|
||||
twoDiv3 = 2f / 3f;
|
||||
sqrt3 = Mathf.Sqrt(3);
|
||||
sqrt3Div2 = Mathf.Sqrt(3) / 2f;
|
||||
}
|
||||
|
||||
// Precomputed array of neighbor offsets as Cell3D structs (center + 6 per layer x 3 layers)
|
||||
static readonly Cell3D[] neighborCellsBase = new Cell3D[]
|
||||
{
|
||||
// Center
|
||||
new Cell3D(0, 0, 0),
|
||||
// Upper layer (1) and its 6 neighbors
|
||||
new Cell3D(0, 0, 1),
|
||||
new Cell3D(1, -1, 1), new Cell3D(1, 0, 1), new Cell3D(0, 1, 1),
|
||||
new Cell3D(-1, 1, 1), new Cell3D(-1, 0, 1), new Cell3D(0, -1, 1),
|
||||
// Same layer (0) - 6 neighbors
|
||||
new Cell3D(1, -1, 0), new Cell3D(1, 0, 0), new Cell3D(0, 1, 0),
|
||||
new Cell3D(-1, 1, 0), new Cell3D(-1, 0, 0), new Cell3D(0, -1, 0),
|
||||
// Lower layer (-1) and its 6 neighbors
|
||||
new Cell3D(0, 0, -1),
|
||||
new Cell3D(1, -1, -1), new Cell3D(1, 0, -1), new Cell3D(0, 1, -1),
|
||||
new Cell3D(-1, 1, -1), new Cell3D(-1, 0, -1), new Cell3D(0, -1, -1)
|
||||
};
|
||||
|
||||
// Converts a grid cell (q, r, layer) to a world position (x, y, z)
|
||||
internal Vector3 CellToWorld(Cell3D cell)
|
||||
{
|
||||
// Calculate X and Z using hexagonal coordinate formulas
|
||||
float x = cellRadius * (sqrt3 * cell.q + sqrt3Div2 * cell.r);
|
||||
float z = cellRadius * (1.5f * cell.r);
|
||||
|
||||
// Calculate Y based on layer and cell height
|
||||
float y = cell.layer * cellHeight + cellHeight / 2;
|
||||
|
||||
// Subtract the origin offset to align with world space and return the position
|
||||
return new Vector3(x, y, z) - originOffset;
|
||||
}
|
||||
|
||||
// Converts a world position (x, y, z) to a grid cell (q, r, layer)
|
||||
internal Cell3D WorldToCell(Vector3 position)
|
||||
{
|
||||
// Apply the origin offset to adjust the position before conversion
|
||||
position += originOffset;
|
||||
|
||||
// Calculate the vertical layer based on Y position
|
||||
int layer = Mathf.FloorToInt(position.y / cellHeight);
|
||||
|
||||
// Convert world X, Z to axial q, r coordinates using inverse hexagonal formulas
|
||||
float q = (sqrt3Div3 * position.x - oneDiv3 * position.z) / cellRadius;
|
||||
float r = (twoDiv3 * position.z) / cellRadius;
|
||||
|
||||
// Round to the nearest valid cell and return
|
||||
return RoundToCell(q, r, layer);
|
||||
}
|
||||
|
||||
// Rounds floating-point axial coordinates (q, r) to the nearest integer cell coordinates
|
||||
Cell3D RoundToCell(float q, float r, int layer)
|
||||
{
|
||||
// Calculate the third hexagonal coordinate (s) for consistency
|
||||
float s = -q - r;
|
||||
int qInt = Mathf.RoundToInt(q); // Round q to nearest integer
|
||||
int rInt = Mathf.RoundToInt(r); // Round r to nearest integer
|
||||
int sInt = Mathf.RoundToInt(s); // Round s to nearest integer
|
||||
|
||||
// Calculate differences to determine which coordinate needs adjustment
|
||||
float qDiff = Mathf.Abs(q - qInt);
|
||||
float rDiff = Mathf.Abs(r - rInt);
|
||||
float sDiff = Mathf.Abs(s - sInt);
|
||||
|
||||
// Adjust q or r based on which has the largest rounding error (ensures q + r + s = 0)
|
||||
if (qDiff > rDiff && qDiff > sDiff)
|
||||
qInt = -rInt - sInt; // Adjust q if it has the largest error
|
||||
else if (rDiff > sDiff)
|
||||
rInt = -qInt - sInt; // Adjust r if it has the largest error
|
||||
|
||||
return new Cell3D(qInt, rInt, layer);
|
||||
}
|
||||
|
||||
// Populates the provided array with neighboring cells around a given center cell
|
||||
internal void GetNeighborCells(Cell3D center, Cell3D[] neighbors)
|
||||
{
|
||||
// Ensure the array has the correct size
|
||||
if (neighbors.Length != 21)
|
||||
throw new System.ArgumentException("Neighbor array must have exactly 21 elements");
|
||||
|
||||
// Populate the array by adjusting precomputed offsets with the center cell's coordinates
|
||||
for (int i = 0; i < neighborCellsBase.Length; i++)
|
||||
{
|
||||
neighbors[i] = new Cell3D(
|
||||
center.q + neighborCellsBase[i].q,
|
||||
center.r + neighborCellsBase[i].r,
|
||||
center.layer + neighborCellsBase[i].layer
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
// Draws a hexagonal gizmo in the Unity Editor for visualization
|
||||
internal void DrawHexGizmo(Vector3 center, float radius, float height, int relativeLayer)
|
||||
{
|
||||
// Hexagon has 6 sides
|
||||
const int segments = 6;
|
||||
|
||||
// Array to store the 6 corner points
|
||||
Vector3[] corners = new Vector3[segments];
|
||||
|
||||
// Calculate the corner positions of the hexagon in the XZ plane
|
||||
for (int i = 0; i < segments; i++)
|
||||
{
|
||||
// Angle for each corner, offset by 90 degrees
|
||||
float angle = 2 * Mathf.PI / segments * i + Mathf.PI / 2;
|
||||
|
||||
// Calculate the corner position based on the angle and radius
|
||||
corners[i] = center + new Vector3(radius * Mathf.Cos(angle), 0, radius * Mathf.Sin(angle));
|
||||
}
|
||||
|
||||
// Set gizmo color based on the relative layer for easy identification
|
||||
Color gizmoColor;
|
||||
switch (relativeLayer)
|
||||
{
|
||||
case 1:
|
||||
gizmoColor = Color.green; // Upper layer (positive Y)
|
||||
break;
|
||||
case 0:
|
||||
gizmoColor = Color.cyan; // Same layer as the reference point
|
||||
break;
|
||||
case -1:
|
||||
gizmoColor = Color.yellow; // Lower layer (negative Y)
|
||||
break;
|
||||
default:
|
||||
gizmoColor = Color.red; // Fallback for unexpected layers
|
||||
break;
|
||||
}
|
||||
|
||||
// Store the current Gizmos color to restore later
|
||||
Color previousColor = Gizmos.color;
|
||||
|
||||
// Apply the chosen color
|
||||
Gizmos.color = gizmoColor;
|
||||
|
||||
// Draw each side of the hexagon as a 3D quad (wall)
|
||||
for (int i = 0; i < segments; i++)
|
||||
{
|
||||
// Current corner
|
||||
Vector3 cornerA = corners[i];
|
||||
|
||||
// Next corner (wraps around at 6)
|
||||
Vector3 cornerB = corners[(i + 1) % segments];
|
||||
|
||||
// Calculate top and bottom corners to form a vertical quad
|
||||
Vector3 cornerATop = cornerA + Vector3.up * (height / 2);
|
||||
Vector3 cornerBTop = cornerB + Vector3.up * (height / 2);
|
||||
Vector3 cornerABottom = cornerA - Vector3.up * (height / 2);
|
||||
Vector3 cornerBBottom = cornerB - Vector3.up * (height / 2);
|
||||
|
||||
// Draw the four lines of the quad to visualize the wall
|
||||
Gizmos.DrawLine(cornerATop, cornerBTop);
|
||||
Gizmos.DrawLine(cornerBTop, cornerBBottom);
|
||||
Gizmos.DrawLine(cornerBBottom, cornerABottom);
|
||||
Gizmos.DrawLine(cornerABottom, cornerATop);
|
||||
}
|
||||
|
||||
// Restore the original Gizmos color
|
||||
Gizmos.color = previousColor;
|
||||
}
|
||||
|
||||
#endif
|
||||
}
|
||||
|
||||
// Custom struct for neighbor offsets (reduced memory usage)
|
||||
internal struct HexOffset
|
||||
{
|
||||
internal int qOffset; // Offset in the q (axial) coordinate
|
||||
internal int rOffset; // Offset in the r (axial) coordinate
|
||||
|
||||
internal HexOffset(int q, int r)
|
||||
{
|
||||
qOffset = q;
|
||||
rOffset = r;
|
||||
}
|
||||
}
|
||||
|
||||
// Struct representing a single cell in the 3D hexagonal grid
|
||||
internal struct Cell3D
|
||||
{
|
||||
internal readonly int q; // Axial q coordinate (horizontal axis)
|
||||
internal readonly int r; // Axial r coordinate (diagonal axis)
|
||||
internal readonly int layer; // Vertical layer index (Y-axis stacking)
|
||||
|
||||
internal Cell3D(int q, int r, int layer)
|
||||
{
|
||||
this.q = q;
|
||||
this.r = r;
|
||||
this.layer = layer;
|
||||
}
|
||||
|
||||
public override bool Equals(object obj) =>
|
||||
obj is Cell3D other
|
||||
&& q == other.q
|
||||
&& r == other.r
|
||||
&& layer == other.layer;
|
||||
|
||||
// Generate a unique hash code for the cell
|
||||
public override int GetHashCode() => (q << 16) ^ (r << 8) ^ layer;
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9c4fe05752c9a85458b8e44611fe832b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Components/InterestManagement/SpatialHashing/HexGrid3D.cs
|
||||
uploadId: 736421
|
@ -0,0 +1,345 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
[AddComponentMenu("Network/ Interest Management/ Spatial Hash/Hex Spatial Hash (2D)")]
|
||||
public class HexSpatialHash2DInterestManagement : InterestManagement
|
||||
{
|
||||
[Range(1, 60), Tooltip("Time interval in seconds between observer rebuilds")]
|
||||
public byte rebuildInterval = 1;
|
||||
|
||||
[Range(1, 60), Tooltip("Time interval in seconds between static object rebuilds")]
|
||||
public byte staticRebuildInterval = 10;
|
||||
|
||||
[Range(10, 5000), Tooltip("Radius of super hex.\nSet to 10% larger than camera far clip plane.")]
|
||||
public ushort visRange = 1100;
|
||||
|
||||
[Range(1, 100), Tooltip("Distance an object must move for updating cell positions")]
|
||||
public ushort minMoveDistance = 1;
|
||||
|
||||
[Tooltip("Spatial Hashing supports XZ for 3D games or XY for 2D games.")]
|
||||
public CheckMethod checkMethod = CheckMethod.XZ_FOR_3D;
|
||||
|
||||
double lastRebuildTime;
|
||||
|
||||
// Counter for batching static object updates
|
||||
byte rebuildCounter = 0;
|
||||
|
||||
HexGrid2D grid;
|
||||
|
||||
// Sparse array mapping cell indices to sets of NetworkIdentities
|
||||
readonly List<HashSet<NetworkIdentity>> cells = new List<HashSet<NetworkIdentity>>();
|
||||
|
||||
// Tracks the last known cell position and world position of each NetworkIdentity
|
||||
readonly Dictionary<NetworkIdentity, (Cell2D cell, Vector2 worldPos)> lastIdentityPositions = new Dictionary<NetworkIdentity, (Cell2D, Vector2)>();
|
||||
|
||||
// Tracks the last known cell position and world position of each player's connection (observer)
|
||||
readonly Dictionary<NetworkConnectionToClient, (Cell2D cell, Vector2 worldPos)> lastConnectionPositions = new Dictionary<NetworkConnectionToClient, (Cell2D, Vector2)>();
|
||||
|
||||
// Pre-allocated array for storing neighbor cells (center + 6 neighbors)
|
||||
readonly Cell2D[] neighborCells = new Cell2D[7];
|
||||
|
||||
// Maps each connection to the set of NetworkIdentities it can observe, precomputed for rebuilds
|
||||
readonly Dictionary<NetworkConnectionToClient, HashSet<NetworkIdentity>> connectionObservers = new Dictionary<NetworkConnectionToClient, HashSet<NetworkIdentity>>();
|
||||
|
||||
// Reusable list for safe iteration over NetworkIdentities, avoiding ToList() allocations
|
||||
readonly List<NetworkIdentity> identityKeys = new List<NetworkIdentity>();
|
||||
|
||||
// Pool of reusable HashSet<NetworkIdentity> instances to reduce allocations
|
||||
readonly Stack<HashSet<NetworkIdentity>> cellPool = new Stack<HashSet<NetworkIdentity>>();
|
||||
|
||||
// Set of static NetworkIdentities that don't move, updated less frequently
|
||||
readonly HashSet<NetworkIdentity> staticObjects = new HashSet<NetworkIdentity>();
|
||||
|
||||
// Scene bounds: <20>9 km (18 km total) in each dimension
|
||||
const int MAX_Q = 19; // Covers -9 to 9 (~18 km)
|
||||
const int MAX_R = 23; // Covers -11 to 11 (~18 km)
|
||||
const ushort MAX_AREA = 9000; // Maximum area in meters
|
||||
|
||||
public enum CheckMethod
|
||||
{
|
||||
XZ_FOR_3D,
|
||||
XY_FOR_2D
|
||||
}
|
||||
|
||||
void Awake()
|
||||
{
|
||||
grid = new HexGrid2D(visRange);
|
||||
// Initialize cells list with null entries up to max size (<28>9 km bounds)
|
||||
int maxSize = MAX_Q * MAX_R;
|
||||
for (int i = 0; i < maxSize; i++)
|
||||
cells.Add(null);
|
||||
}
|
||||
|
||||
// Project 3D world position to 2D grid position based on checkMethod
|
||||
Vector2 ProjectToGrid(Vector3 position) =>
|
||||
checkMethod == CheckMethod.XZ_FOR_3D
|
||||
? new Vector2(position.x, position.z)
|
||||
: new Vector2(position.x, position.y);
|
||||
|
||||
void LateUpdate()
|
||||
{
|
||||
if (NetworkTime.time - lastRebuildTime >= rebuildInterval)
|
||||
{
|
||||
// Update positions of all active connections (players) in the network
|
||||
foreach (NetworkConnectionToClient conn in NetworkServer.connections.Values)
|
||||
if (conn?.identity != null) // Ensure connection and its identity exist
|
||||
{
|
||||
Vector2 position = ProjectToGrid(conn.identity.transform.position);
|
||||
// Only update if the position has changed significantly
|
||||
if (!lastConnectionPositions.TryGetValue(conn, out (Cell2D cell, Vector2 worldPos) last) ||
|
||||
Vector2.Distance(position, last.worldPos) >= minMoveDistance)
|
||||
{
|
||||
Cell2D cell = grid.WorldToCell(position); // Convert world position to grid cell
|
||||
lastConnectionPositions[conn] = (cell, position); // Store the player's cell and position
|
||||
}
|
||||
}
|
||||
|
||||
// Populate the reusable list with current keys for safe iteration
|
||||
identityKeys.Clear();
|
||||
identityKeys.AddRange(lastIdentityPositions.Keys);
|
||||
|
||||
// Update dynamic objects every rebuild, static objects every staticRebuildInterval
|
||||
bool updateStatic = rebuildCounter >= staticRebuildInterval;
|
||||
foreach (NetworkIdentity identity in identityKeys)
|
||||
if (updateStatic || !staticObjects.Contains(identity))
|
||||
UpdateIdentityPosition(identity); // Refresh cell position for dynamic or scheduled static objects
|
||||
|
||||
if (updateStatic)
|
||||
rebuildCounter = 0; // Reset the counter after updating static objects
|
||||
else
|
||||
rebuildCounter++;
|
||||
|
||||
// Precompute observer sets for each connection before rebuilding
|
||||
connectionObservers.Clear();
|
||||
foreach (NetworkConnectionToClient conn in NetworkServer.connections.Values)
|
||||
{
|
||||
if (conn?.identity == null || !lastConnectionPositions.TryGetValue(conn, out (Cell2D cell, Vector2 worldPos) connPos))
|
||||
continue;
|
||||
|
||||
// Get cells visible from the player's position
|
||||
grid.GetNeighborCells(connPos.cell, neighborCells);
|
||||
|
||||
// Initialize the observer set for this connection
|
||||
HashSet<NetworkIdentity> observers = new HashSet<NetworkIdentity>();
|
||||
connectionObservers[conn] = observers;
|
||||
|
||||
// Add all identities in visible cells to the observer set
|
||||
for (int i = 0; i < neighborCells.Length; i++)
|
||||
{
|
||||
int index = GetCellIndex(neighborCells[i]);
|
||||
if (index >= 0 && index < cells.Count && cells[index] != null)
|
||||
{
|
||||
foreach (NetworkIdentity identity in cells[index])
|
||||
observers.Add(identity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RebuildAll invokes NetworkServer.RebuildObservers on all spawned objects
|
||||
base.RebuildAll();
|
||||
|
||||
// Update the last rebuild time
|
||||
lastRebuildTime = NetworkTime.time;
|
||||
}
|
||||
}
|
||||
|
||||
// Called when a new networked object is spawned on the server
|
||||
public override void OnSpawned(NetworkIdentity identity)
|
||||
{
|
||||
// Register the new object's position in the grid system
|
||||
UpdateIdentityPosition(identity);
|
||||
|
||||
// Check if the object is statically batched (indicating it won't move)
|
||||
Renderer[] renderers = identity.gameObject.GetComponentsInChildren<Renderer>();
|
||||
if (renderers.Any(r => r.isPartOfStaticBatch))
|
||||
staticObjects.Add(identity);
|
||||
}
|
||||
|
||||
// Updates the grid cell position of a NetworkIdentity when it moves or spawns
|
||||
void UpdateIdentityPosition(NetworkIdentity identity)
|
||||
{
|
||||
// Get the current world position of the object
|
||||
Vector2 position = ProjectToGrid(identity.transform.position);
|
||||
|
||||
// Convert position to grid cell coordinates
|
||||
Cell2D newCell = grid.WorldToCell(position);
|
||||
|
||||
// Check if the object is within <20>9 km bounds
|
||||
if (Mathf.Abs(position.x) > MAX_AREA || Mathf.Abs(position.y) > MAX_AREA)
|
||||
return; // Ignore objects outside bounds
|
||||
|
||||
// Check if the object was previously tracked
|
||||
if (lastIdentityPositions.TryGetValue(identity, out (Cell2D cell, Vector2 worldPos) previous))
|
||||
{
|
||||
// Only update if the position has changed significantly or the cell has changed
|
||||
if (Vector2.Distance(position, previous.worldPos) >= minMoveDistance || !newCell.Equals(previous.cell))
|
||||
{
|
||||
if (!newCell.Equals(previous.cell))
|
||||
{
|
||||
// Object moved to a new cell
|
||||
// Remove it from the old cell's set and add it to the new cell's set
|
||||
int oldIndex = GetCellIndex(previous.cell);
|
||||
if (oldIndex >= 0 && oldIndex < cells.Count && cells[oldIndex] != null)
|
||||
cells[oldIndex].Remove(identity);
|
||||
AddToCell(newCell, identity);
|
||||
}
|
||||
// Update the stored position and cell
|
||||
lastIdentityPositions[identity] = (newCell, position);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// New object - add it to the grid and track its position
|
||||
AddToCell(newCell, identity);
|
||||
lastIdentityPositions[identity] = (newCell, position);
|
||||
}
|
||||
}
|
||||
|
||||
// Adds a NetworkIdentity to a specific cell's set of objects
|
||||
void AddToCell(Cell2D cell, NetworkIdentity identity)
|
||||
{
|
||||
int index = GetCellIndex(cell);
|
||||
if (index < 0 || index >= cells.Count)
|
||||
return; // Out of bounds, ignore
|
||||
|
||||
// If the cell doesn't exist in the array yet, fetch or create a new set from the pool
|
||||
if (cells[index] == null)
|
||||
{
|
||||
cells[index] = cellPool.Count > 0 ? cellPool.Pop() : new HashSet<NetworkIdentity>();
|
||||
}
|
||||
cells[index].Add(identity);
|
||||
}
|
||||
|
||||
// Determines if a new observer can see a given NetworkIdentity
|
||||
public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnectionToClient newObserver)
|
||||
{
|
||||
// Check if we have position data for both the object and the observer
|
||||
if (!lastIdentityPositions.TryGetValue(identity, out (Cell2D cell, Vector2 worldPos) identityPos) ||
|
||||
!lastConnectionPositions.TryGetValue(newObserver, out (Cell2D cell, Vector2 worldPos) observerPos))
|
||||
return false; // If not, assume no visibility
|
||||
|
||||
// Populate the pre-allocated array with visible cells from the observer's position
|
||||
grid.GetNeighborCells(observerPos.cell, neighborCells);
|
||||
|
||||
// Check if the object's cell is among the visible ones
|
||||
for (int i = 0; i < neighborCells.Length; i++)
|
||||
if (neighborCells[i].Equals(identityPos.cell))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Rebuilds the set of observers for a specific NetworkIdentity
|
||||
public override void OnRebuildObservers(NetworkIdentity identity, HashSet<NetworkConnectionToClient> newObservers)
|
||||
{
|
||||
// If the object's position isn't tracked, skip rebuilding
|
||||
if (!lastIdentityPositions.TryGetValue(identity, out (Cell2D cell, Vector2 worldPos) identityPos))
|
||||
return;
|
||||
|
||||
// Use the precomputed observer sets to determine visibility
|
||||
foreach (NetworkConnectionToClient conn in NetworkServer.connections.Values)
|
||||
{
|
||||
// Skip if the connection or its identity is null
|
||||
if (conn?.identity == null)
|
||||
continue;
|
||||
|
||||
// Check if this connection can observe the identity
|
||||
if (connectionObservers.TryGetValue(conn, out HashSet<NetworkIdentity> observers) && observers.Contains(identity))
|
||||
newObservers.Add(conn);
|
||||
}
|
||||
}
|
||||
|
||||
public override void ResetState()
|
||||
{
|
||||
lastRebuildTime = 0;
|
||||
// Clear and return all cell sets to the pool
|
||||
for (int i = 0; i < cells.Count; i++)
|
||||
{
|
||||
if (cells[i] != null)
|
||||
{
|
||||
cells[i].Clear();
|
||||
cellPool.Push(cells[i]);
|
||||
cells[i] = null;
|
||||
}
|
||||
}
|
||||
lastIdentityPositions.Clear();
|
||||
lastConnectionPositions.Clear();
|
||||
connectionObservers.Clear();
|
||||
identityKeys.Clear();
|
||||
staticObjects.Clear();
|
||||
rebuildCounter = 0;
|
||||
}
|
||||
|
||||
public override void OnDestroyed(NetworkIdentity identity)
|
||||
{
|
||||
// If the object was tracked, remove it from its cell and position records
|
||||
if (lastIdentityPositions.TryGetValue(identity, out (Cell2D cell, Vector2 worldPos) pos))
|
||||
{
|
||||
int index = GetCellIndex(pos.cell);
|
||||
if (index >= 0 && index < cells.Count && cells[index] != null)
|
||||
{
|
||||
cells[index].Remove(identity); // Remove from the cell's set
|
||||
// If the cell's set is now empty, return it to the pool
|
||||
if (cells[index].Count == 0)
|
||||
{
|
||||
cellPool.Push(cells[index]);
|
||||
cells[index] = null;
|
||||
}
|
||||
}
|
||||
lastIdentityPositions.Remove(identity); // Remove from position tracking
|
||||
staticObjects.Remove(identity); // Ensure it's removed from static set if present
|
||||
}
|
||||
}
|
||||
|
||||
// Computes a unique index for a cell in the sparse array, supporting <20>9 km bounds
|
||||
int GetCellIndex(Cell2D cell)
|
||||
{
|
||||
int qOffset = cell.q + MAX_Q / 2; // Shift -9 to 9 -> 0 to 18
|
||||
int rOffset = cell.r + MAX_R / 2; // Shift -11 to 11 -> 0 to 22
|
||||
return qOffset + rOffset * MAX_Q;
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
// Draws debug gizmos in the Unity Editor to visualize the 2D grid
|
||||
void OnDrawGizmos()
|
||||
{
|
||||
// Initialize the grid if it hasn<73>t been created yet (e.g., before Awake)
|
||||
if (grid == null)
|
||||
grid = new HexGrid2D(visRange);
|
||||
|
||||
// Only draw if there<72>s a local player to base the visualization on
|
||||
if (NetworkClient.localPlayer != null)
|
||||
{
|
||||
Vector3 playerPosition = NetworkClient.localPlayer.transform.position;
|
||||
|
||||
// Convert to grid cell using the full Vector3 for proper plane projection
|
||||
Vector2 projectedPos = ProjectToGrid(playerPosition);
|
||||
Cell2D playerCell = grid.WorldToCell(projectedPos);
|
||||
|
||||
// Get all visible cells around the player into the pre-allocated array
|
||||
grid.GetNeighborCells(playerCell, neighborCells);
|
||||
|
||||
// Set gizmo color for visibility
|
||||
Gizmos.color = Color.cyan;
|
||||
|
||||
// Draw each visible cell as a 2D hexagon, oriented based on checkMethod
|
||||
for (int i = 0; i < neighborCells.Length; i++)
|
||||
{
|
||||
// Convert cell to world coordinates (2D)
|
||||
Vector2 worldPos2D = grid.CellToWorld(neighborCells[i]);
|
||||
|
||||
// Convert to 3D position based on checkMethod
|
||||
Vector3 worldPos = checkMethod == CheckMethod.XZ_FOR_3D
|
||||
? new Vector3(worldPos2D.x, 0, worldPos2D.y) // XZ plane, flat
|
||||
: new Vector3(worldPos2D.x, worldPos2D.y, 0); // XY plane, vertical
|
||||
|
||||
grid.DrawHexGizmo(worldPos, grid.cellRadius, checkMethod);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9b8b055f11f85ff428da471a0e625dd4
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Components/InterestManagement/SpatialHashing/HexSpatialHash2DInterestManagement.cs
|
||||
uploadId: 736421
|
@ -0,0 +1,336 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
[AddComponentMenu("Network/ Interest Management/ Spatial Hash/Hex Spatial Hash (3D)")]
|
||||
public class HexSpatialHash3DInterestManagement : InterestManagement
|
||||
{
|
||||
[Range(1, 60), Tooltip("Time interval in seconds between observer rebuilds")]
|
||||
public byte rebuildInterval = 1;
|
||||
|
||||
[Range(1, 60), Tooltip("Time interval in seconds between static object rebuilds")]
|
||||
public byte staticRebuildInterval = 10;
|
||||
|
||||
[Range(10, 5000), Tooltip("Radius of super hex.\nSet to 10% larger than camera far clip plane.")]
|
||||
public ushort visRange = 1100;
|
||||
|
||||
[Range(10, 5000), Tooltip("Cell3D height effects all 3 layers")]
|
||||
public ushort cellHeight = 500;
|
||||
|
||||
[Range(1, 100), Tooltip("Distance an object must move for updating cell positions")]
|
||||
public ushort minMoveDistance = 1;
|
||||
|
||||
double lastRebuildTime;
|
||||
|
||||
// Counter for batching static object updates
|
||||
byte rebuildCounter = 0;
|
||||
|
||||
HexGrid3D grid;
|
||||
|
||||
// Sparse array mapping cell indices to sets of NetworkIdentities
|
||||
readonly List<HashSet<NetworkIdentity>> cells = new List<HashSet<NetworkIdentity>>();
|
||||
|
||||
// Tracks the last known cell position and world position of each NetworkIdentity for efficient updates
|
||||
readonly Dictionary<NetworkIdentity, (Cell3D cell, Vector3 worldPos)> lastIdentityPositions = new Dictionary<NetworkIdentity, (Cell3D, Vector3)>();
|
||||
|
||||
// Tracks the last known cell position and world position of each player's connection (observer)
|
||||
readonly Dictionary<NetworkConnectionToClient, (Cell3D cell, Vector3 worldPos)> lastConnectionPositions = new Dictionary<NetworkConnectionToClient, (Cell3D, Vector3)>();
|
||||
|
||||
// Pre-allocated array for storing neighbor cells (center + 6 neighbors per layer x 3 layers)
|
||||
readonly Cell3D[] neighborCells = new Cell3D[21];
|
||||
|
||||
// Maps each connection to the set of NetworkIdentities it can observe, precomputed for rebuilds
|
||||
readonly Dictionary<NetworkConnectionToClient, HashSet<NetworkIdentity>> connectionObservers = new Dictionary<NetworkConnectionToClient, HashSet<NetworkIdentity>>();
|
||||
|
||||
// Reusable list for safe iteration over NetworkIdentities, avoiding ToList() allocations
|
||||
readonly List<NetworkIdentity> identityKeys = new List<NetworkIdentity>();
|
||||
|
||||
// Pool of reusable HashSet<NetworkIdentity> instances to reduce allocations
|
||||
readonly Stack<HashSet<NetworkIdentity>> cellPool = new Stack<HashSet<NetworkIdentity>>();
|
||||
|
||||
// Set of static NetworkIdentities that don't move, updated less frequently
|
||||
readonly HashSet<NetworkIdentity> staticObjects = new HashSet<NetworkIdentity>();
|
||||
|
||||
// Scene bounds: ±9 km (18 km total) in each dimension
|
||||
const int MAX_Q = 19; // Covers -9 to 9 (~18 km)
|
||||
const int MAX_R = 23; // Covers -11 to 11 (~18 km)
|
||||
const int LAYER_OFFSET = 18; // Offset for -18 to 17 layers
|
||||
const int MAX_LAYERS = 36; // Total layers for ±9 km (18 km)
|
||||
const ushort MAX_AREA = 9000; // Maximum area in meters
|
||||
|
||||
void Awake()
|
||||
{
|
||||
grid = new HexGrid3D(visRange, cellHeight);
|
||||
// Initialize cells list with null entries up to max size (±9 km bounds)
|
||||
int maxSize = MAX_Q * MAX_R * MAX_LAYERS;
|
||||
for (int i = 0; i < maxSize; i++)
|
||||
cells.Add(null);
|
||||
}
|
||||
|
||||
void LateUpdate()
|
||||
{
|
||||
if (NetworkTime.time - lastRebuildTime >= rebuildInterval)
|
||||
{
|
||||
// Update positions of all active connections (players) in the network
|
||||
foreach (NetworkConnectionToClient conn in NetworkServer.connections.Values)
|
||||
if (conn?.identity != null) // Ensure connection and its identity exist
|
||||
{
|
||||
Vector3 position = conn.identity.transform.position;
|
||||
// Only update if the position has changed significantly
|
||||
if (!lastConnectionPositions.TryGetValue(conn, out (Cell3D cell, Vector3 worldPos) last) ||
|
||||
Vector3.Distance(position, last.worldPos) >= minMoveDistance)
|
||||
{
|
||||
Cell3D cell = grid.WorldToCell(position); // Convert world position to grid cell
|
||||
lastConnectionPositions[conn] = (cell, position); // Store the player's cell and position
|
||||
}
|
||||
}
|
||||
|
||||
// Populate the reusable list with current keys for safe iteration
|
||||
identityKeys.Clear();
|
||||
identityKeys.AddRange(lastIdentityPositions.Keys);
|
||||
|
||||
// Update dynamic objects every rebuild, static objects every staticRebuildInterval
|
||||
bool updateStatic = rebuildCounter >= staticRebuildInterval;
|
||||
foreach (NetworkIdentity identity in identityKeys)
|
||||
if (updateStatic || !staticObjects.Contains(identity))
|
||||
UpdateIdentityPosition(identity); // Refresh cell position for dynamic or scheduled static objects
|
||||
|
||||
if (updateStatic)
|
||||
rebuildCounter = 0; // Reset the counter after updating static objects
|
||||
else
|
||||
rebuildCounter++;
|
||||
|
||||
// Precompute observer sets for each connection before rebuilding
|
||||
connectionObservers.Clear();
|
||||
foreach (NetworkConnectionToClient conn in NetworkServer.connections.Values)
|
||||
{
|
||||
if (conn?.identity == null || !lastConnectionPositions.TryGetValue(conn, out (Cell3D cell, Vector3 worldPos) connPos))
|
||||
continue;
|
||||
|
||||
// Get cells visible from the player's position
|
||||
grid.GetNeighborCells(connPos.cell, neighborCells);
|
||||
|
||||
// Initialize the observer set for this connection
|
||||
HashSet<NetworkIdentity> observers = new HashSet<NetworkIdentity>();
|
||||
connectionObservers[conn] = observers;
|
||||
|
||||
// Add all identities in visible cells to the observer set
|
||||
for (int i = 0; i < neighborCells.Length; i++)
|
||||
{
|
||||
int index = GetCellIndex(neighborCells[i]);
|
||||
if (index >= 0 && index < cells.Count && cells[index] != null)
|
||||
{
|
||||
foreach (NetworkIdentity identity in cells[index])
|
||||
observers.Add(identity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RebuildAll invokes NetworkServer.RebuildObservers on all spawned objects
|
||||
base.RebuildAll();
|
||||
|
||||
// Update the last rebuild time
|
||||
lastRebuildTime = NetworkTime.time;
|
||||
}
|
||||
}
|
||||
|
||||
// Called when a new networked object is spawned on the server
|
||||
public override void OnSpawned(NetworkIdentity identity)
|
||||
{
|
||||
// Register the new object's position in the grid system
|
||||
UpdateIdentityPosition(identity);
|
||||
|
||||
// Check if the object is statically batched (indicating it won't move)
|
||||
Renderer[] renderers = identity.gameObject.GetComponentsInChildren<Renderer>();
|
||||
if (renderers.Any(r => r.isPartOfStaticBatch))
|
||||
staticObjects.Add(identity);
|
||||
}
|
||||
|
||||
// Updates the grid cell position of a NetworkIdentity when it moves or spawns
|
||||
void UpdateIdentityPosition(NetworkIdentity identity)
|
||||
{
|
||||
// Get the current world position of the object
|
||||
Vector3 position = identity.transform.position;
|
||||
|
||||
// Convert player position to grid cell coordinates
|
||||
Cell3D newCell = grid.WorldToCell(position);
|
||||
|
||||
// Check if the object is within ±9 km bounds
|
||||
if (Mathf.Abs(position.x) > MAX_AREA || Mathf.Abs(position.y) > MAX_AREA || Mathf.Abs(position.z) > MAX_AREA)
|
||||
return; // Ignore objects outside bounds
|
||||
|
||||
// Check if the object was previously tracked
|
||||
if (lastIdentityPositions.TryGetValue(identity, out (Cell3D cell, Vector3 worldPos) previous))
|
||||
{
|
||||
// Only update if the position has changed significantly or the cell has changed
|
||||
if (Vector3.Distance(position, previous.worldPos) >= minMoveDistance || !newCell.Equals(previous.cell))
|
||||
{
|
||||
if (!newCell.Equals(previous.cell))
|
||||
{
|
||||
// Object moved to a new cell
|
||||
// Remove it from the old cell's set and add it to the new cell's set
|
||||
int oldIndex = GetCellIndex(previous.cell);
|
||||
if (oldIndex >= 0 && oldIndex < cells.Count && cells[oldIndex] != null)
|
||||
cells[oldIndex].Remove(identity);
|
||||
AddToCell(newCell, identity);
|
||||
}
|
||||
// Update the stored position and cell
|
||||
lastIdentityPositions[identity] = (newCell, position);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// New object - add it to the grid and track its position
|
||||
AddToCell(newCell, identity);
|
||||
lastIdentityPositions[identity] = (newCell, position);
|
||||
}
|
||||
}
|
||||
|
||||
// Adds a NetworkIdentity to a specific cell's set of objects
|
||||
void AddToCell(Cell3D cell, NetworkIdentity identity)
|
||||
{
|
||||
int index = GetCellIndex(cell);
|
||||
if (index < 0 || index >= cells.Count)
|
||||
return; // Out of bounds, ignore
|
||||
|
||||
// If the cell doesn't exist in the array yet, fetch or create a new set from the pool
|
||||
if (cells[index] == null)
|
||||
{
|
||||
cells[index] = cellPool.Count > 0 ? cellPool.Pop() : new HashSet<NetworkIdentity>();
|
||||
}
|
||||
cells[index].Add(identity);
|
||||
}
|
||||
|
||||
// Determines if a new observer can see a given NetworkIdentity
|
||||
public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnectionToClient newObserver)
|
||||
{
|
||||
// Check if we have position data for both the object and the observer
|
||||
if (!lastIdentityPositions.TryGetValue(identity, out (Cell3D cell, Vector3 worldPos) identityPos) ||
|
||||
!lastConnectionPositions.TryGetValue(newObserver, out (Cell3D cell, Vector3 worldPos) observerPos))
|
||||
return false; // If not, assume no visibility
|
||||
|
||||
// Populate the pre-allocated array with visible cells from the observer's position
|
||||
grid.GetNeighborCells(observerPos.cell, neighborCells);
|
||||
|
||||
// Check if the object's cell is among the visible ones
|
||||
for (int i = 0; i < neighborCells.Length; i++)
|
||||
if (neighborCells[i].Equals(identityPos.cell))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Rebuilds the set of observers for a specific NetworkIdentity
|
||||
public override void OnRebuildObservers(NetworkIdentity identity, HashSet<NetworkConnectionToClient> newObservers)
|
||||
{
|
||||
// If the object's position isn't tracked, skip rebuilding
|
||||
if (!lastIdentityPositions.TryGetValue(identity, out (Cell3D cell, Vector3 worldPos) identityPos))
|
||||
return;
|
||||
|
||||
// Use the precomputed observer sets to determine visibility
|
||||
foreach (NetworkConnectionToClient conn in NetworkServer.connections.Values)
|
||||
{
|
||||
// Skip if the connection or its identity is null
|
||||
if (conn?.identity == null)
|
||||
continue;
|
||||
|
||||
// Check if this connection can observe the identity
|
||||
if (connectionObservers.TryGetValue(conn, out HashSet<NetworkIdentity> observers) && observers.Contains(identity))
|
||||
newObservers.Add(conn);
|
||||
}
|
||||
}
|
||||
|
||||
public override void ResetState()
|
||||
{
|
||||
lastRebuildTime = 0;
|
||||
// Clear and return all cell sets to the pool
|
||||
for (int i = 0; i < cells.Count; i++)
|
||||
{
|
||||
if (cells[i] != null)
|
||||
{
|
||||
cells[i].Clear();
|
||||
cellPool.Push(cells[i]);
|
||||
cells[i] = null;
|
||||
}
|
||||
}
|
||||
lastIdentityPositions.Clear();
|
||||
lastConnectionPositions.Clear();
|
||||
connectionObservers.Clear();
|
||||
identityKeys.Clear();
|
||||
staticObjects.Clear();
|
||||
rebuildCounter = 0;
|
||||
}
|
||||
|
||||
public override void OnDestroyed(NetworkIdentity identity)
|
||||
{
|
||||
// If the object was tracked, remove it from its cell and position records
|
||||
if (lastIdentityPositions.TryGetValue(identity, out (Cell3D cell, Vector3 worldPos) pos))
|
||||
{
|
||||
int index = GetCellIndex(pos.cell);
|
||||
if (index >= 0 && index < cells.Count && cells[index] != null)
|
||||
{
|
||||
cells[index].Remove(identity); // Remove from the cell's set
|
||||
// If the cell's set is now empty, return it to the pool
|
||||
if (cells[index].Count == 0)
|
||||
{
|
||||
cellPool.Push(cells[index]);
|
||||
cells[index] = null;
|
||||
}
|
||||
}
|
||||
lastIdentityPositions.Remove(identity); // Remove from position tracking
|
||||
staticObjects.Remove(identity); // Ensure it's removed from static set if present
|
||||
}
|
||||
}
|
||||
|
||||
// Computes a unique index for a cell in the sparse array, supporting ±9 km bounds
|
||||
int GetCellIndex(Cell3D cell)
|
||||
{
|
||||
int qOffset = cell.q + MAX_Q / 2; // Shift -9 to 9 -> 0 to 18
|
||||
int rOffset = cell.r + MAX_R / 2; // Shift -11 to 11 -> 0 to 22
|
||||
int layerOffset = cell.layer + LAYER_OFFSET; // Shift -18 to 17 -> 0 to 35
|
||||
return qOffset + rOffset * MAX_Q + layerOffset * MAX_Q * MAX_R;
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
// Draws debug gizmos in the Unity Editor to visualize the grid
|
||||
void OnDrawGizmos()
|
||||
{
|
||||
// Initialize the grid if it hasn't been created yet (e.g., before Awake)
|
||||
if (grid == null)
|
||||
grid = new HexGrid3D(visRange, cellHeight);
|
||||
|
||||
// Only draw if there's a local player to base the visualization on
|
||||
if (NetworkClient.localPlayer != null)
|
||||
{
|
||||
Vector3 playerPosition = NetworkClient.localPlayer.transform.position;
|
||||
|
||||
// Convert to grid cell
|
||||
Cell3D playerCell = grid.WorldToCell(playerPosition);
|
||||
|
||||
// Get all visible cells around the player into the pre-allocated array
|
||||
grid.GetNeighborCells(playerCell, neighborCells);
|
||||
|
||||
// Set default gizmo color (though overridden per cell)
|
||||
Gizmos.color = Color.cyan;
|
||||
|
||||
// Draw each visible cell as a hexagonal prism
|
||||
for (int i = 0; i < neighborCells.Length; i++)
|
||||
{
|
||||
// Convert cell to world coordinates
|
||||
Vector3 worldPos = grid.CellToWorld(neighborCells[i]);
|
||||
|
||||
// Determine the layer relative to the player's cell for color coding
|
||||
int relativeLayer = neighborCells[i].layer - playerCell.layer;
|
||||
|
||||
// Draw the hexagonal cell with appropriate color based on layer
|
||||
grid.DrawHexGizmo(worldPos, grid.cellRadius, grid.cellHeight, relativeLayer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 58e492e77a2a1a3488412ceed5c2aa2d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Components/InterestManagement/SpatialHashing/HexSpatialHash3DInterestManagement.cs
|
||||
uploadId: 736421
|
@ -0,0 +1,146 @@
|
||||
// extremely fast spatial hashing interest management based on uMMORPG GridChecker.
|
||||
// => 30x faster in initial tests
|
||||
// => scales way higher
|
||||
// checks on three dimensions (XYZ) which includes the vertical axes.
|
||||
// this is slower than XY checking for regular spatial hashing.
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
[AddComponentMenu("Network/ Interest Management/ Spatial Hash/Spatial Hashing Interest Management")]
|
||||
public class SpatialHashing3DInterestManagement : InterestManagement
|
||||
{
|
||||
[Tooltip("The maximum range that objects will be visible at.")]
|
||||
public int visRange = 30;
|
||||
|
||||
// we use a 9 neighbour grid.
|
||||
// so we always see in a distance of 2 grids.
|
||||
// for example, our own grid and then one on top / below / left / right.
|
||||
//
|
||||
// this means that grid resolution needs to be distance / 2.
|
||||
// so for example, for distance = 30 we see 2 cells = 15 * 2 distance.
|
||||
//
|
||||
// on first sight, it seems we need distance / 3 (we see left/us/right).
|
||||
// but that's not the case.
|
||||
// resolution would be 10, and we only see 1 cell far, so 10+10=20.
|
||||
public int resolution => visRange / 2; // same as XY because if XY is rotated 90 degree for 3D, it's still the same distance
|
||||
|
||||
[Tooltip("Rebuild all every 'rebuildInterval' seconds.")]
|
||||
public float rebuildInterval = 1;
|
||||
double lastRebuildTime;
|
||||
|
||||
[Header("Debug Settings")]
|
||||
public bool showSlider;
|
||||
|
||||
// the grid
|
||||
// begin with a large capacity to avoid resizing & allocations.
|
||||
Grid3D<NetworkConnectionToClient> grid = new Grid3D<NetworkConnectionToClient>(1024);
|
||||
|
||||
// project 3d world position to grid position
|
||||
Vector3Int ProjectToGrid(Vector3 position) =>
|
||||
Vector3Int.RoundToInt(position / resolution);
|
||||
|
||||
public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnectionToClient newObserver)
|
||||
{
|
||||
// calculate projected positions
|
||||
Vector3Int projected = ProjectToGrid(identity.transform.position);
|
||||
Vector3Int observerProjected = ProjectToGrid(newObserver.identity.transform.position);
|
||||
|
||||
// distance needs to be at max one of the 8 neighbors, which is
|
||||
// 1 for the direct neighbors
|
||||
// 1.41 for the diagonal neighbors (= sqrt(2))
|
||||
// => use sqrMagnitude and '2' to avoid computations. same result.
|
||||
return (projected - observerProjected).sqrMagnitude <= 2; // same as XY because if XY is rotated 90 degree for 3D, it's still the same distance
|
||||
}
|
||||
|
||||
public override void OnRebuildObservers(NetworkIdentity identity, HashSet<NetworkConnectionToClient> newObservers)
|
||||
{
|
||||
// add everyone in 9 neighbour grid
|
||||
// -> pass observers to GetWithNeighbours directly to avoid allocations
|
||||
// and expensive .UnionWith computations.
|
||||
Vector3Int current = ProjectToGrid(identity.transform.position);
|
||||
grid.GetWithNeighbours(current, newObservers);
|
||||
}
|
||||
|
||||
[ServerCallback]
|
||||
public override void ResetState()
|
||||
{
|
||||
lastRebuildTime = 0D;
|
||||
}
|
||||
|
||||
// update everyone's position in the grid
|
||||
// (internal so we can update from tests)
|
||||
[ServerCallback]
|
||||
internal void Update()
|
||||
{
|
||||
// NOTE: unlike Scene/MatchInterestManagement, this rebuilds ALL
|
||||
// entities every INTERVAL. consider the other approach later.
|
||||
|
||||
// IMPORTANT: refresh grid every update!
|
||||
// => newly spawned entities get observers assigned via
|
||||
// OnCheckObservers. this can happen any time and we don't want
|
||||
// them broadcast to old (moved or destroyed) connections.
|
||||
// => players do move all the time. we want them to always be in the
|
||||
// correct grid position.
|
||||
// => note that the actual 'rebuildall' doesn't need to happen all
|
||||
// the time.
|
||||
// NOTE: consider refreshing grid only every 'interval' too. but not
|
||||
// for now. stability & correctness matter.
|
||||
|
||||
// clear old grid results before we update everyone's position.
|
||||
// (this way we get rid of destroyed connections automatically)
|
||||
//
|
||||
// NOTE: keeps allocated HashSets internally.
|
||||
// clearing & populating every frame works without allocations
|
||||
grid.ClearNonAlloc();
|
||||
|
||||
// put every connection into the grid at it's main player's position
|
||||
// NOTE: player sees in a radius around him. NOT around his pet too.
|
||||
foreach (NetworkConnectionToClient connection in NetworkServer.connections.Values)
|
||||
{
|
||||
// authenticated and joined world with a player?
|
||||
if (connection.isAuthenticated && connection.identity != null)
|
||||
{
|
||||
// calculate current grid position
|
||||
Vector3Int position = ProjectToGrid(connection.identity.transform.position);
|
||||
|
||||
// put into grid
|
||||
grid.Add(position, connection);
|
||||
}
|
||||
}
|
||||
|
||||
// rebuild all spawned entities' observers every 'interval'
|
||||
// this will call OnRebuildObservers which then returns the
|
||||
// observers at grid[position] for each entity.
|
||||
if (NetworkTime.localTime >= lastRebuildTime + rebuildInterval)
|
||||
{
|
||||
RebuildAll();
|
||||
lastRebuildTime = NetworkTime.localTime;
|
||||
}
|
||||
}
|
||||
|
||||
// OnGUI allocates even if it does nothing. avoid in release.
|
||||
#if UNITY_EDITOR || DEVELOPMENT_BUILD
|
||||
// slider from dotsnet. it's nice to play around with in the benchmark
|
||||
// demo.
|
||||
void OnGUI()
|
||||
{
|
||||
if (!showSlider) return;
|
||||
|
||||
// only show while server is running. not on client, etc.
|
||||
if (!NetworkServer.active) return;
|
||||
|
||||
int height = 30;
|
||||
int width = 250;
|
||||
GUILayout.BeginArea(new Rect(Screen.width / 2 - width / 2, Screen.height - height, width, height));
|
||||
GUILayout.BeginHorizontal("Box");
|
||||
GUILayout.Label("Radius:");
|
||||
visRange = Mathf.RoundToInt(GUILayout.HorizontalSlider(visRange, 0, 200, GUILayout.Width(150)));
|
||||
GUILayout.Label(visRange.ToString());
|
||||
GUILayout.EndHorizontal();
|
||||
GUILayout.EndArea();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 120b4d6121d94e0280cd2ec536b0ea8f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Components/InterestManagement/SpatialHashing/SpatialHashing3DInterestManagement.cs
|
||||
uploadId: 736421
|
@ -0,0 +1,156 @@
|
||||
// extremely fast spatial hashing interest management based on uMMORPG GridChecker.
|
||||
// => 30x faster in initial tests
|
||||
// => scales way higher
|
||||
// checks on two dimensions only(!), for example: XZ for 3D games or XY for 2D games.
|
||||
// this is faster than XYZ checking but doesn't check vertical distance.
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
[AddComponentMenu("Network/ Interest Management/ Spatial Hash/Spatial Hashing Interest Management")]
|
||||
public class SpatialHashingInterestManagement : InterestManagement
|
||||
{
|
||||
[Tooltip("The maximum range that objects will be visible at.")]
|
||||
public int visRange = 30;
|
||||
|
||||
// we use a 9 neighbour grid.
|
||||
// so we always see in a distance of 2 grids.
|
||||
// for example, our own grid and then one on top / below / left / right.
|
||||
//
|
||||
// this means that grid resolution needs to be distance / 2.
|
||||
// so for example, for distance = 30 we see 2 cells = 15 * 2 distance.
|
||||
//
|
||||
// on first sight, it seems we need distance / 3 (we see left/us/right).
|
||||
// but that's not the case.
|
||||
// resolution would be 10, and we only see 1 cell far, so 10+10=20.
|
||||
public int resolution => visRange / 2;
|
||||
|
||||
[Tooltip("Rebuild all every 'rebuildInterval' seconds.")]
|
||||
public float rebuildInterval = 1;
|
||||
double lastRebuildTime;
|
||||
|
||||
public enum CheckMethod
|
||||
{
|
||||
XZ_FOR_3D,
|
||||
XY_FOR_2D
|
||||
}
|
||||
[Tooltip("Spatial Hashing supports 3D (XZ) and 2D (XY) games.")]
|
||||
public CheckMethod checkMethod = CheckMethod.XZ_FOR_3D;
|
||||
|
||||
[Header("Debug Settings")]
|
||||
public bool showSlider;
|
||||
|
||||
// the grid
|
||||
// begin with a large capacity to avoid resizing & allocations.
|
||||
Grid2D<NetworkConnectionToClient> grid = new Grid2D<NetworkConnectionToClient>(1024);
|
||||
|
||||
// project 3d world position to grid position
|
||||
Vector2Int ProjectToGrid(Vector3 position) =>
|
||||
checkMethod == CheckMethod.XZ_FOR_3D
|
||||
? Vector2Int.RoundToInt(new Vector2(position.x, position.z) / resolution)
|
||||
: Vector2Int.RoundToInt(new Vector2(position.x, position.y) / resolution);
|
||||
|
||||
public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnectionToClient newObserver)
|
||||
{
|
||||
// calculate projected positions
|
||||
Vector2Int projected = ProjectToGrid(identity.transform.position);
|
||||
Vector2Int observerProjected = ProjectToGrid(newObserver.identity.transform.position);
|
||||
|
||||
// distance needs to be at max one of the 8 neighbors, which is
|
||||
// 1 for the direct neighbors
|
||||
// 1.41 for the diagonal neighbors (= sqrt(2))
|
||||
// => use sqrMagnitude and '2' to avoid computations. same result.
|
||||
return (projected - observerProjected).sqrMagnitude <= 2;
|
||||
}
|
||||
|
||||
public override void OnRebuildObservers(NetworkIdentity identity, HashSet<NetworkConnectionToClient> newObservers)
|
||||
{
|
||||
// add everyone in 9 neighbour grid
|
||||
// -> pass observers to GetWithNeighbours directly to avoid allocations
|
||||
// and expensive .UnionWith computations.
|
||||
Vector2Int current = ProjectToGrid(identity.transform.position);
|
||||
grid.GetWithNeighbours(current, newObservers);
|
||||
}
|
||||
|
||||
[ServerCallback]
|
||||
public override void ResetState()
|
||||
{
|
||||
lastRebuildTime = 0D;
|
||||
}
|
||||
|
||||
// update everyone's position in the grid
|
||||
// (internal so we can update from tests)
|
||||
[ServerCallback]
|
||||
internal void Update()
|
||||
{
|
||||
// NOTE: unlike Scene/MatchInterestManagement, this rebuilds ALL
|
||||
// entities every INTERVAL. consider the other approach later.
|
||||
|
||||
// IMPORTANT: refresh grid every update!
|
||||
// => newly spawned entities get observers assigned via
|
||||
// OnCheckObservers. this can happen any time and we don't want
|
||||
// them broadcast to old (moved or destroyed) connections.
|
||||
// => players do move all the time. we want them to always be in the
|
||||
// correct grid position.
|
||||
// => note that the actual 'rebuildall' doesn't need to happen all
|
||||
// the time.
|
||||
// NOTE: consider refreshing grid only every 'interval' too. but not
|
||||
// for now. stability & correctness matter.
|
||||
|
||||
// clear old grid results before we update everyone's position.
|
||||
// (this way we get rid of destroyed connections automatically)
|
||||
//
|
||||
// NOTE: keeps allocated HashSets internally.
|
||||
// clearing & populating every frame works without allocations
|
||||
grid.ClearNonAlloc();
|
||||
|
||||
// put every connection into the grid at it's main player's position
|
||||
// NOTE: player sees in a radius around him. NOT around his pet too.
|
||||
foreach (NetworkConnectionToClient connection in NetworkServer.connections.Values)
|
||||
{
|
||||
// authenticated and joined world with a player?
|
||||
if (connection.isAuthenticated && connection.identity != null)
|
||||
{
|
||||
// calculate current grid position
|
||||
Vector2Int position = ProjectToGrid(connection.identity.transform.position);
|
||||
|
||||
// put into grid
|
||||
grid.Add(position, connection);
|
||||
}
|
||||
}
|
||||
|
||||
// rebuild all spawned entities' observers every 'interval'
|
||||
// this will call OnRebuildObservers which then returns the
|
||||
// observers at grid[position] for each entity.
|
||||
if (NetworkTime.localTime >= lastRebuildTime + rebuildInterval)
|
||||
{
|
||||
RebuildAll();
|
||||
lastRebuildTime = NetworkTime.localTime;
|
||||
}
|
||||
}
|
||||
|
||||
// OnGUI allocates even if it does nothing. avoid in release.
|
||||
#if UNITY_EDITOR || DEVELOPMENT_BUILD
|
||||
// slider from dotsnet. it's nice to play around with in the benchmark
|
||||
// demo.
|
||||
void OnGUI()
|
||||
{
|
||||
if (!showSlider) return;
|
||||
|
||||
// only show while server is running. not on client, etc.
|
||||
if (!NetworkServer.active) return;
|
||||
|
||||
int height = 30;
|
||||
int width = 250;
|
||||
GUILayout.BeginArea(new Rect(Screen.width / 2 - width / 2, Screen.height - height, width, height));
|
||||
GUILayout.BeginHorizontal("Box");
|
||||
GUILayout.Label("Radius:");
|
||||
visRange = Mathf.RoundToInt(GUILayout.HorizontalSlider(visRange, 0, 200, GUILayout.Width(150)));
|
||||
GUILayout.Label(visRange.ToString());
|
||||
GUILayout.EndHorizontal();
|
||||
GUILayout.EndArea();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 39adc6e09d5544ed955a50ce8600355a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Components/InterestManagement/SpatialHashing/SpatialHashingInterestManagement.cs
|
||||
uploadId: 736421
|
8
Assets/Mirror/Components/InterestManagement/Team.meta
Normal file
8
Assets/Mirror/Components/InterestManagement/Team.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2d418e60072433b4bbebbf5f3a7de1bb
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -0,0 +1,39 @@
|
||||
// simple component that holds team information
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
[DisallowMultipleComponent]
|
||||
[AddComponentMenu("Network/ Interest Management/ Team/Network Team")]
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/guides/interest-management")]
|
||||
public class NetworkTeam : NetworkBehaviour
|
||||
{
|
||||
[SerializeField]
|
||||
[Tooltip("Set teamId on Server at runtime to the same value on all networked objects that belong to a given team")]
|
||||
string _teamId;
|
||||
|
||||
public string teamId
|
||||
{
|
||||
get => _teamId;
|
||||
set
|
||||
{
|
||||
if (Application.IsPlaying(gameObject) && !NetworkServer.active)
|
||||
throw new InvalidOperationException("teamId can only be set at runtime on active server");
|
||||
|
||||
if (_teamId == value)
|
||||
return;
|
||||
|
||||
string oldTeam = _teamId;
|
||||
_teamId = value;
|
||||
|
||||
//Only inform the AOI if this netIdentity has been spawned(isServer) and only if using a TeamInterestManagement
|
||||
if (isServer && NetworkServer.aoi is TeamInterestManagement teamInterestManagement)
|
||||
teamInterestManagement.OnTeamChanged(this, oldTeam);
|
||||
}
|
||||
}
|
||||
|
||||
[Tooltip("When enabled this object is visible to all clients. Typically this would be true for player objects")]
|
||||
public bool forceShown;
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2576730625b1632468cbcbfe5e721f88
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Components/InterestManagement/Team/NetworkTeam.cs
|
||||
uploadId: 736421
|
@ -0,0 +1,182 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
[AddComponentMenu("Network/ Interest Management/ Team/Team Interest Management")]
|
||||
public class TeamInterestManagement : InterestManagement
|
||||
{
|
||||
readonly Dictionary<string, HashSet<NetworkTeam>> teamObjects =
|
||||
new Dictionary<string, HashSet<NetworkTeam>>();
|
||||
|
||||
readonly HashSet<string> dirtyTeams = new HashSet<string>();
|
||||
|
||||
// LateUpdate so that all spawns/despawns/changes are done
|
||||
[ServerCallback]
|
||||
void LateUpdate()
|
||||
{
|
||||
// Rebuild all dirty teams
|
||||
// dirtyTeams will be empty if no teams changed members
|
||||
// by spawning or destroying or changing teamId in this frame.
|
||||
foreach (string dirtyTeam in dirtyTeams)
|
||||
{
|
||||
// rebuild always, even if teamObjects[dirtyTeam] is empty.
|
||||
// Players might have left the team, but they may still be spawned.
|
||||
RebuildTeamObservers(dirtyTeam);
|
||||
|
||||
// clean up empty entries in the dict
|
||||
if (teamObjects[dirtyTeam].Count == 0)
|
||||
teamObjects.Remove(dirtyTeam);
|
||||
}
|
||||
|
||||
dirtyTeams.Clear();
|
||||
}
|
||||
|
||||
[ServerCallback]
|
||||
void RebuildTeamObservers(string teamId)
|
||||
{
|
||||
foreach (NetworkTeam networkTeam in teamObjects[teamId])
|
||||
if (networkTeam.netIdentity != null)
|
||||
NetworkServer.RebuildObservers(networkTeam.netIdentity, false);
|
||||
}
|
||||
|
||||
// called by NetworkTeam.teamId setter
|
||||
[ServerCallback]
|
||||
internal void OnTeamChanged(NetworkTeam networkTeam, string oldTeam)
|
||||
{
|
||||
// This object is in a new team so observers in the prior team
|
||||
// and the new team need to rebuild their respective observers lists.
|
||||
|
||||
// Remove this object from the hashset of the team it just left
|
||||
// Null / Empty string is never a valid teamId
|
||||
if (!string.IsNullOrWhiteSpace(oldTeam))
|
||||
{
|
||||
dirtyTeams.Add(oldTeam);
|
||||
teamObjects[oldTeam].Remove(networkTeam);
|
||||
}
|
||||
|
||||
// Null / Empty string is never a valid teamId
|
||||
if (string.IsNullOrWhiteSpace(networkTeam.teamId))
|
||||
return;
|
||||
|
||||
dirtyTeams.Add(networkTeam.teamId);
|
||||
|
||||
// Make sure this new team is in the dictionary
|
||||
if (!teamObjects.ContainsKey(networkTeam.teamId))
|
||||
teamObjects[networkTeam.teamId] = new HashSet<NetworkTeam>();
|
||||
|
||||
// Add this object to the hashset of the new team
|
||||
teamObjects[networkTeam.teamId].Add(networkTeam);
|
||||
}
|
||||
|
||||
[ServerCallback]
|
||||
public override void OnSpawned(NetworkIdentity identity)
|
||||
{
|
||||
if (!identity.TryGetComponent(out NetworkTeam networkTeam))
|
||||
return;
|
||||
|
||||
string networkTeamId = networkTeam.teamId;
|
||||
|
||||
// Null / Empty string is never a valid teamId...do not add to teamObjects collection
|
||||
if (string.IsNullOrWhiteSpace(networkTeamId))
|
||||
return;
|
||||
|
||||
// Debug.Log($"TeamInterestManagement.OnSpawned({identity.name}) currentTeam: {currentTeam}");
|
||||
if (!teamObjects.TryGetValue(networkTeamId, out HashSet<NetworkTeam> objects))
|
||||
{
|
||||
objects = new HashSet<NetworkTeam>();
|
||||
teamObjects.Add(networkTeamId, objects);
|
||||
}
|
||||
|
||||
objects.Add(networkTeam);
|
||||
|
||||
// Team ID could have been set in NetworkBehaviour::OnStartServer on this object.
|
||||
// Since that's after OnCheckObserver is called it would be missed, so force Rebuild here.
|
||||
// Add the current team to dirtyTeames for LateUpdate to rebuild it.
|
||||
dirtyTeams.Add(networkTeamId);
|
||||
}
|
||||
|
||||
[ServerCallback]
|
||||
public override void OnDestroyed(NetworkIdentity identity)
|
||||
{
|
||||
// Don't RebuildSceneObservers here - that will happen in LateUpdate.
|
||||
// Multiple objects could be destroyed in same frame and we don't
|
||||
// want to rebuild for each one...let LateUpdate do it once.
|
||||
// We must add the current team to dirtyTeames for LateUpdate to rebuild it.
|
||||
if (identity.TryGetComponent(out NetworkTeam currentTeam))
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(currentTeam.teamId) &&
|
||||
teamObjects.TryGetValue(currentTeam.teamId, out HashSet<NetworkTeam> objects) &&
|
||||
objects.Remove(currentTeam))
|
||||
dirtyTeams.Add(currentTeam.teamId);
|
||||
}
|
||||
}
|
||||
|
||||
public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnectionToClient newObserver)
|
||||
{
|
||||
// Always observed if no NetworkTeam component
|
||||
if (!identity.TryGetComponent(out NetworkTeam identityNetworkTeam))
|
||||
return true;
|
||||
|
||||
if (identityNetworkTeam.forceShown)
|
||||
return true;
|
||||
|
||||
// Null / Empty string is never a valid teamId
|
||||
if (string.IsNullOrWhiteSpace(identityNetworkTeam.teamId))
|
||||
return false;
|
||||
|
||||
// Always observed if no NetworkTeam component
|
||||
if (!newObserver.identity.TryGetComponent(out NetworkTeam newObserverNetworkTeam))
|
||||
return true;
|
||||
|
||||
// Null / Empty string is never a valid teamId
|
||||
if (string.IsNullOrWhiteSpace(newObserverNetworkTeam.teamId))
|
||||
return false;
|
||||
|
||||
//Debug.Log($"TeamInterestManagement.OnCheckObserver {identity.name} {identityNetworkTeam.teamId} | {newObserver.identity.name} {newObserverNetworkTeam.teamId}");
|
||||
|
||||
// Observed only if teamId's team
|
||||
return identityNetworkTeam.teamId == newObserverNetworkTeam.teamId;
|
||||
}
|
||||
|
||||
public override void OnRebuildObservers(NetworkIdentity identity, HashSet<NetworkConnectionToClient> newObservers)
|
||||
{
|
||||
// If this object doesn't have a NetworkTeam then it's visible to all clients
|
||||
if (!identity.TryGetComponent(out NetworkTeam networkTeam))
|
||||
{
|
||||
AddAllConnections(newObservers);
|
||||
return;
|
||||
}
|
||||
|
||||
// If this object has NetworkTeam and forceShown == true then it's visible to all clients
|
||||
if (networkTeam.forceShown)
|
||||
{
|
||||
AddAllConnections(newObservers);
|
||||
return;
|
||||
}
|
||||
|
||||
// Null / Empty string is never a valid teamId
|
||||
if (string.IsNullOrWhiteSpace(networkTeam.teamId))
|
||||
return;
|
||||
|
||||
// Abort if this team hasn't been created yet by OnSpawned or OnTeamChanged
|
||||
if (!teamObjects.TryGetValue(networkTeam.teamId, out HashSet<NetworkTeam> objects))
|
||||
return;
|
||||
|
||||
// Add everything in the hashset for this object's current team
|
||||
foreach (NetworkTeam netTeam in objects)
|
||||
if (netTeam.netIdentity != null && netTeam.netIdentity.connectionToClient != null)
|
||||
newObservers.Add(netTeam.netIdentity.connectionToClient);
|
||||
}
|
||||
|
||||
void AddAllConnections(HashSet<NetworkConnectionToClient> newObservers)
|
||||
{
|
||||
foreach (NetworkConnectionToClient conn in NetworkServer.connections.Values)
|
||||
{
|
||||
// authenticated and joined world with a player?
|
||||
if (conn != null && conn.isAuthenticated && conn.identity != null)
|
||||
newObservers.Add(conn);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dceb9a7085758fd4590419ff5b14b636
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Components/InterestManagement/Team/TeamInterestManagement.cs
|
||||
uploadId: 736421
|
Reference in New Issue
Block a user