CHANGED TO MIRROR

This commit is contained in:
DerTyp187
2021-10-25 09:20:01 +02:00
parent bd712107b7
commit e509a919b6
611 changed files with 38291 additions and 1216 deletions

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: fa4cbc6b9c584db4971985cb9f369077
timeCreated: 1613110605

View File

@@ -0,0 +1,69 @@
// straight forward Vector3.Distance based interest management.
using System.Collections.Generic;
using UnityEngine;
namespace Mirror
{
public class DistanceInterestManagement : InterestManagement
{
[Tooltip("The maximum range that objects will be visible at. Add DistanceInterestManagementCustomRange onto NetworkIdentities for custom ranges.")]
public int visRange = 10;
[Tooltip("Rebuild all every 'rebuildInterval' seconds.")]
public float rebuildInterval = 1;
double lastRebuildTime;
// helper function to get vis range for a given object, or default.
int GetVisRange(NetworkIdentity identity)
{
DistanceInterestManagementCustomRange custom = identity.GetComponent<DistanceInterestManagementCustomRange>();
return custom != null ? custom.visRange : visRange;
}
public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnection newObserver)
{
int range = GetVisRange(identity);
return Vector3.Distance(identity.transform.position, newObserver.identity.transform.position) < range;
}
public override void OnRebuildObservers(NetworkIdentity identity, HashSet<NetworkConnection> newObservers, bool initialize)
{
// 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);
}
}
}
}
void Update()
{
// only on server
if (!NetworkServer.active) return;
// rebuild all spawned NetworkIdentity's observers every interval
if (NetworkTime.localTime >= lastRebuildTime + rebuildInterval)
{
RebuildAll();
lastRebuildTime = NetworkTime.localTime;
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8f60becab051427fbdd3c8ac9ab4712b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,14 @@
// add this to NetworkIdentities for custom range if needed.
// only works with DistanceInterestManagement.
using UnityEngine;
namespace Mirror
{
[RequireComponent(typeof(NetworkIdentity))]
[DisallowMultipleComponent]
public class DistanceInterestManagementCustomRange : MonoBehaviour
{
[Tooltip("The maximum range that objects will be visible at.")]
public int visRange = 20;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b2e242ee38a14076a39934172a19079b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 5eca5245ae6bb460e9a92f7e14d5493a
timeCreated: 1622649517

View File

@@ -0,0 +1,147 @@
using System;
using System.Collections.Generic;
namespace Mirror
{
public class MatchInterestManagement : InterestManagement
{
readonly Dictionary<Guid, HashSet<NetworkIdentity>> matchObjects =
new Dictionary<Guid, HashSet<NetworkIdentity>>();
readonly Dictionary<NetworkIdentity, Guid> lastObjectMatch =
new Dictionary<NetworkIdentity, Guid>();
HashSet<Guid> dirtyMatches = new HashSet<Guid>();
public override void OnSpawned(NetworkIdentity identity)
{
if (!identity.TryGetComponent<NetworkMatch>(out NetworkMatch networkMatch))
return;
Guid currentMatch = networkMatch.matchId;
lastObjectMatch[identity] = currentMatch;
// Guid.Empty is never a valid matchId...do not add to matchObjects collection
if (currentMatch == Guid.Empty)
return;
// Debug.Log($"MatchInterestManagement.OnSpawned({identity.name}) currentMatch: {currentMatch}");
if (!matchObjects.TryGetValue(currentMatch, out HashSet<NetworkIdentity> objects))
{
objects = new HashSet<NetworkIdentity>();
matchObjects.Add(currentMatch, objects);
}
objects.Add(identity);
}
public override void OnDestroyed(NetworkIdentity identity)
{
lastObjectMatch.TryGetValue(identity, out Guid currentMatch);
lastObjectMatch.Remove(identity);
if (currentMatch != Guid.Empty && matchObjects.TryGetValue(currentMatch, out HashSet<NetworkIdentity> objects) && objects.Remove(identity))
RebuildMatchObservers(currentMatch);
}
[ServerCallback]
void Update()
{
// for each spawned:
// if match changed:
// add previous to dirty
// add new to dirty
foreach (NetworkIdentity netIdentity in NetworkServer.spawned.Values)
{
// Ignore objects that don't have a NetworkMatch component
if (!netIdentity.TryGetComponent<NetworkMatch>(out NetworkMatch networkMatch))
continue;
Guid newMatch = networkMatch.matchId;
lastObjectMatch.TryGetValue(netIdentity, out Guid currentMatch);
// Guid.Empty is never a valid matchId
// Nothing to do if matchId hasn't changed
if (newMatch == Guid.Empty || newMatch == currentMatch)
continue;
// Mark new/old matches as dirty so they get rebuilt
UpdateDirtyMatches(newMatch, currentMatch);
// This object is in a new match so observers in the prior match
// and the new match need to rebuild their respective observers lists.
UpdateMatchObjects(netIdentity, newMatch, currentMatch);
}
// rebuild all dirty matchs
foreach (Guid dirtyMatch in dirtyMatches)
RebuildMatchObservers(dirtyMatch);
dirtyMatches.Clear();
}
void UpdateDirtyMatches(Guid newMatch, Guid currentMatch)
{
// Guid.Empty is never a valid matchId
if (currentMatch != Guid.Empty)
dirtyMatches.Add(currentMatch);
dirtyMatches.Add(newMatch);
}
void UpdateMatchObjects(NetworkIdentity netIdentity, Guid newMatch, Guid currentMatch)
{
// Remove this object from the hashset of the match it just left
// Guid.Empty is never a valid matchId
if (currentMatch != Guid.Empty)
matchObjects[currentMatch].Remove(netIdentity);
// Set this to the new match this object just entered
lastObjectMatch[netIdentity] = newMatch;
// Make sure this new match is in the dictionary
if (!matchObjects.ContainsKey(newMatch))
matchObjects.Add(newMatch, new HashSet<NetworkIdentity>());
// Add this object to the hashset of the new match
matchObjects[newMatch].Add(netIdentity);
}
void RebuildMatchObservers(Guid matchId)
{
foreach (NetworkIdentity netIdentity in matchObjects[matchId])
if (netIdentity != null)
NetworkServer.RebuildObservers(netIdentity, false);
}
public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnection newObserver)
{
if (!identity.TryGetComponent<NetworkMatch>(out NetworkMatch identityNetworkMatch))
return false;
if (!newObserver.identity.TryGetComponent<NetworkMatch>(out NetworkMatch newObserverNetworkMatch))
return false;
return identityNetworkMatch.matchId == newObserverNetworkMatch.matchId;
}
public override void OnRebuildObservers(NetworkIdentity identity, HashSet<NetworkConnection> newObservers, bool initialize)
{
if (!identity.TryGetComponent<NetworkMatch>(out NetworkMatch networkMatch))
return;
Guid matchId = networkMatch.matchId;
// Guid.Empty is never a valid matchId
if (matchId == Guid.Empty)
return;
if (!matchObjects.TryGetValue(matchId, out HashSet<NetworkIdentity> objects))
return;
// Add everything in the hashset for this object's current match
foreach (NetworkIdentity networkIdentity in objects)
if (networkIdentity != null && networkIdentity.connectionToClient != null)
newObservers.Add(networkIdentity.connectionToClient);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d09f5c8bf2f4747b7a9284ef5d9ce2a7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 7655d309a46a4bd4860edf964228b3f6
timeCreated: 1622649517

View File

@@ -0,0 +1,109 @@
using System.Collections.Generic;
using UnityEngine.SceneManagement;
namespace Mirror
{
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>();
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);
}
public override void OnDestroyed(NetworkIdentity identity)
{
Scene currentScene = lastObjectScene[identity];
lastObjectScene.Remove(identity);
if (sceneObjects.TryGetValue(currentScene, out HashSet<NetworkIdentity> objects) && objects.Remove(identity))
RebuildSceneObservers(currentScene);
}
void Update()
{
// only on server
if (!NetworkServer.active) return;
// for each spawned:
// if scene changed:
// add previous to dirty
// add new to dirty
foreach (NetworkIdentity identity in NetworkServer.spawned.Values)
{
Scene currentScene = lastObjectScene[identity];
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, NetworkConnection newObserver)
{
return identity.gameObject.scene == newObserver.identity.gameObject.scene;
}
public override void OnRebuildObservers(NetworkIdentity identity, HashSet<NetworkConnection> newObservers,
bool initialize)
{
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);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b979f26c95d34324ba005bfacfa9c4fc
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: cfa12b73503344d49b398b01bcb07967
timeCreated: 1613110634

View File

@@ -0,0 +1,88 @@
// Grid2D from uMMORPG: get/set values of type T at any point
// -> not named 'Grid' because Unity already has a Grid type. causes warnings.
using System.Collections.Generic;
using UnityEngine;
namespace Mirror
{
public class 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!
Dictionary<Vector2Int, HashSet<T>> grid = new Dictionary<Vector2Int, HashSet<T>>();
// cache a 9 neighbor grid of vector2 offsets so we can use them more easily
Vector2Int[] neighbourOffsets =
{
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))
{
hashSet = new HashSet<T>();
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();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7c5232a4d2854116a35d52b80ec07752
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,139 @@
// extremely fast spatial hashing interest management based on uMMORPG GridChecker.
// => 30x faster in initial tests
// => scales way higher
using System.Collections.Generic;
using UnityEngine;
namespace Mirror
{
public class SpatialHashingInterestManagement : InterestManagement
{
[Tooltip("The maximum range that objects will be visible at.")]
public int visRange = 30;
// if we see 8 neighbors then 1 entry is visRange/3
public int resolution => visRange / 3;
[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;
// debugging
public bool showSlider;
// the grid
Grid2D<NetworkConnection> grid = new Grid2D<NetworkConnection>();
// 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, NetworkConnection 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<NetworkConnection> newObservers, bool initialize)
{
// 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);
}
// update everyone's position in the grid
// (internal so we can update from tests)
internal void Update()
{
// only on server
if (!NetworkServer.active) return;
// 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
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 39adc6e09d5544ed955a50ce8600355a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant: