Keep local version of files
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Mirror.Tests.Common")]
|
||||
[assembly: InternalsVisibleTo("Mirror.Tests")]
|
||||
// need to use Unity.*.CodeGen assembly name to import Unity.CompilationPipeline
|
||||
// for ILPostProcessor tests.
|
||||
[assembly: InternalsVisibleTo("Unity.Mirror.Tests.CodeGen")]
|
||||
[assembly: InternalsVisibleTo("Mirror.Tests.Generated")]
|
||||
[assembly: InternalsVisibleTo("Mirror.Tests.Runtime")]
|
||||
[assembly: InternalsVisibleTo("Mirror.Tests.Performance.Editor")]
|
||||
[assembly: InternalsVisibleTo("Mirror.Tests.Performance.Runtime")]
|
||||
[assembly: InternalsVisibleTo("Mirror.Editor")]
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Mirror.Tests.Common")]
|
||||
[assembly: InternalsVisibleTo("Mirror.Tests")]
|
||||
// need to use Unity.*.CodeGen assembly name to import Unity.CompilationPipeline
|
||||
// for ILPostProcessor tests.
|
||||
[assembly: InternalsVisibleTo("Unity.Mirror.Tests.CodeGen")]
|
||||
[assembly: InternalsVisibleTo("Mirror.Tests.Generated")]
|
||||
[assembly: InternalsVisibleTo("Mirror.Tests.Runtime")]
|
||||
[assembly: InternalsVisibleTo("Mirror.Tests.Performance.Editor")]
|
||||
[assembly: InternalsVisibleTo("Mirror.Tests.Performance.Runtime")]
|
||||
[assembly: InternalsVisibleTo("Mirror.Editor")]
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a65b9283f7a724e70b8e17cb277f4c1e
|
||||
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/AssemblyInfo.cs
|
||||
uploadId: 736421
|
||||
fileFormatVersion: 2
|
||||
guid: a65b9283f7a724e70b8e17cb277f4c1e
|
||||
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/AssemblyInfo.cs
|
||||
uploadId: 736421
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b5dcf9618f5e14a4eb60bff5480284a6
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
fileFormatVersion: 2
|
||||
guid: b5dcf9618f5e14a4eb60bff5480284a6
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
||||
@@ -1,93 +1,93 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
|
||||
namespace Mirror.Discovery
|
||||
{
|
||||
[Serializable]
|
||||
public class ServerFoundUnityEvent<TResponseType> : UnityEvent<TResponseType> {};
|
||||
|
||||
[DisallowMultipleComponent]
|
||||
[AddComponentMenu("Network/Network Discovery")]
|
||||
public class NetworkDiscovery : NetworkDiscoveryBase<ServerRequest, ServerResponse>
|
||||
{
|
||||
#region Server
|
||||
|
||||
/// <summary>
|
||||
/// Process the request from a client
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Override if you wish to provide more information to the clients
|
||||
/// such as the name of the host player
|
||||
/// </remarks>
|
||||
/// <param name="request">Request coming from client</param>
|
||||
/// <param name="endpoint">Address of the client that sent the request</param>
|
||||
/// <returns>The message to be sent back to the client or null</returns>
|
||||
protected override ServerResponse ProcessRequest(ServerRequest request, IPEndPoint endpoint)
|
||||
{
|
||||
// In this case we don't do anything with the request
|
||||
// but other discovery implementations might want to use the data
|
||||
// in there, This way the client can ask for
|
||||
// specific game mode or something
|
||||
|
||||
try
|
||||
{
|
||||
// this is an example reply message, return your own
|
||||
// to include whatever is relevant for your game
|
||||
return new ServerResponse
|
||||
{
|
||||
serverId = ServerId,
|
||||
uri = transport.ServerUri()
|
||||
};
|
||||
}
|
||||
catch (NotImplementedException)
|
||||
{
|
||||
Debug.LogError($"Transport {transport} does not support network discovery");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Client
|
||||
|
||||
/// <summary>
|
||||
/// Create a message that will be broadcasted on the network to discover servers
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Override if you wish to include additional data in the discovery message
|
||||
/// such as desired game mode, language, difficulty, etc... </remarks>
|
||||
/// <returns>An instance of ServerRequest with data to be broadcasted</returns>
|
||||
protected override ServerRequest GetRequest() => new ServerRequest();
|
||||
|
||||
/// <summary>
|
||||
/// Process the answer from a server
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A client receives a reply from a server, this method processes the
|
||||
/// reply and raises an event
|
||||
/// </remarks>
|
||||
/// <param name="response">Response that came from the server</param>
|
||||
/// <param name="endpoint">Address of the server that replied</param>
|
||||
protected override void ProcessResponse(ServerResponse response, IPEndPoint endpoint)
|
||||
{
|
||||
// we received a message from the remote endpoint
|
||||
response.EndPoint = endpoint;
|
||||
|
||||
// although we got a supposedly valid url, we may not be able to resolve
|
||||
// the provided host
|
||||
// However we know the real ip address of the server because we just
|
||||
// received a packet from it, so use that as host.
|
||||
UriBuilder realUri = new UriBuilder(response.uri)
|
||||
{
|
||||
Host = response.EndPoint.Address.ToString()
|
||||
};
|
||||
response.uri = realUri.Uri;
|
||||
|
||||
OnServerFound.Invoke(response);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using System.Net;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
|
||||
namespace Mirror.Discovery
|
||||
{
|
||||
[Serializable]
|
||||
public class ServerFoundUnityEvent<TResponseType> : UnityEvent<TResponseType> {};
|
||||
|
||||
[DisallowMultipleComponent]
|
||||
[AddComponentMenu("Network/Network Discovery")]
|
||||
public class NetworkDiscovery : NetworkDiscoveryBase<ServerRequest, ServerResponse>
|
||||
{
|
||||
#region Server
|
||||
|
||||
/// <summary>
|
||||
/// Process the request from a client
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Override if you wish to provide more information to the clients
|
||||
/// such as the name of the host player
|
||||
/// </remarks>
|
||||
/// <param name="request">Request coming from client</param>
|
||||
/// <param name="endpoint">Address of the client that sent the request</param>
|
||||
/// <returns>The message to be sent back to the client or null</returns>
|
||||
protected override ServerResponse ProcessRequest(ServerRequest request, IPEndPoint endpoint)
|
||||
{
|
||||
// In this case we don't do anything with the request
|
||||
// but other discovery implementations might want to use the data
|
||||
// in there, This way the client can ask for
|
||||
// specific game mode or something
|
||||
|
||||
try
|
||||
{
|
||||
// this is an example reply message, return your own
|
||||
// to include whatever is relevant for your game
|
||||
return new ServerResponse
|
||||
{
|
||||
serverId = ServerId,
|
||||
uri = transport.ServerUri()
|
||||
};
|
||||
}
|
||||
catch (NotImplementedException)
|
||||
{
|
||||
Debug.LogError($"Transport {transport} does not support network discovery");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Client
|
||||
|
||||
/// <summary>
|
||||
/// Create a message that will be broadcasted on the network to discover servers
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Override if you wish to include additional data in the discovery message
|
||||
/// such as desired game mode, language, difficulty, etc... </remarks>
|
||||
/// <returns>An instance of ServerRequest with data to be broadcasted</returns>
|
||||
protected override ServerRequest GetRequest() => new ServerRequest();
|
||||
|
||||
/// <summary>
|
||||
/// Process the answer from a server
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A client receives a reply from a server, this method processes the
|
||||
/// reply and raises an event
|
||||
/// </remarks>
|
||||
/// <param name="response">Response that came from the server</param>
|
||||
/// <param name="endpoint">Address of the server that replied</param>
|
||||
protected override void ProcessResponse(ServerResponse response, IPEndPoint endpoint)
|
||||
{
|
||||
// we received a message from the remote endpoint
|
||||
response.EndPoint = endpoint;
|
||||
|
||||
// although we got a supposedly valid url, we may not be able to resolve
|
||||
// the provided host
|
||||
// However we know the real ip address of the server because we just
|
||||
// received a packet from it, so use that as host.
|
||||
UriBuilder realUri = new UriBuilder(response.uri)
|
||||
{
|
||||
Host = response.EndPoint.Address.ToString()
|
||||
};
|
||||
response.uri = realUri.Uri;
|
||||
|
||||
OnServerFound.Invoke(response);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c761308e733c51245b2e8bb4201f46dc
|
||||
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/Discovery/NetworkDiscovery.cs
|
||||
uploadId: 736421
|
||||
fileFormatVersion: 2
|
||||
guid: c761308e733c51245b2e8bb4201f46dc
|
||||
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/Discovery/NetworkDiscovery.cs
|
||||
uploadId: 736421
|
||||
|
||||
@@ -1,473 +1,473 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
|
||||
// Based on https://github.com/EnlightenedOne/MirrorNetworkDiscovery
|
||||
// forked from https://github.com/in0finite/MirrorNetworkDiscovery
|
||||
// Both are MIT Licensed
|
||||
|
||||
namespace Mirror.Discovery
|
||||
{
|
||||
/// <summary>
|
||||
/// Base implementation for Network Discovery. Extend this component
|
||||
/// to provide custom discovery with game specific data
|
||||
/// <see cref="NetworkDiscovery">NetworkDiscovery</see> for a sample implementation
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-discovery")]
|
||||
public abstract class NetworkDiscoveryBase<Request, Response> : MonoBehaviour
|
||||
where Request : NetworkMessage
|
||||
where Response : NetworkMessage
|
||||
{
|
||||
public static bool SupportedOnThisPlatform { get { return Application.platform != RuntimePlatform.WebGLPlayer; } }
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("If true, broadcasts a discovery request every ActiveDiscoveryInterval seconds")]
|
||||
public bool enableActiveDiscovery = true;
|
||||
|
||||
// broadcast address needs to be configurable on iOS:
|
||||
// https://github.com/vis2k/Mirror/pull/3255
|
||||
[Tooltip("iOS may require LAN IP address here (e.g. 192.168.x.x), otherwise leave blank.")]
|
||||
public string BroadcastAddress = "";
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("The UDP port the server will listen for multi-cast messages")]
|
||||
protected int serverBroadcastListenPort = 47777;
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("Time in seconds between multi-cast messages")]
|
||||
[Range(1, 60)]
|
||||
float ActiveDiscoveryInterval = 3;
|
||||
|
||||
[Tooltip("Transport to be advertised during discovery")]
|
||||
public Transport transport;
|
||||
|
||||
[Tooltip("Invoked when a server is found")]
|
||||
public ServerFoundUnityEvent<Response> OnServerFound;
|
||||
|
||||
// Each game should have a random unique handshake,
|
||||
// this way you can tell if this is the same game or not
|
||||
[HideInInspector]
|
||||
public long secretHandshake;
|
||||
|
||||
public long ServerId { get; private set; }
|
||||
|
||||
protected UdpClient serverUdpClient;
|
||||
protected UdpClient clientUdpClient;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
public virtual void OnValidate()
|
||||
{
|
||||
if (transport == null)
|
||||
transport = GetComponent<Transport>();
|
||||
|
||||
if (secretHandshake == 0)
|
||||
{
|
||||
secretHandshake = RandomLong();
|
||||
UnityEditor.Undo.RecordObject(this, "Set secret handshake");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// virtual so that inheriting classes' Start() can call base.Start() too
|
||||
/// </summary>
|
||||
public virtual void Start()
|
||||
{
|
||||
ServerId = RandomLong();
|
||||
|
||||
// active transport gets initialized in Awake
|
||||
// so make sure we set it here in Start() after Awake
|
||||
// Or just let the user assign it in the inspector
|
||||
if (transport == null)
|
||||
transport = Transport.active;
|
||||
|
||||
// Server mode? then start advertising
|
||||
if (Utils.IsHeadless())
|
||||
{
|
||||
AdvertiseServer();
|
||||
}
|
||||
}
|
||||
|
||||
public static long RandomLong()
|
||||
{
|
||||
int value1 = UnityEngine.Random.Range(int.MinValue, int.MaxValue);
|
||||
int value2 = UnityEngine.Random.Range(int.MinValue, int.MaxValue);
|
||||
return value1 + ((long)value2 << 32);
|
||||
}
|
||||
|
||||
// Ensure the ports are cleared no matter when Game/Unity UI exits
|
||||
void OnApplicationQuit()
|
||||
{
|
||||
//Debug.Log("NetworkDiscoveryBase OnApplicationQuit");
|
||||
Shutdown();
|
||||
}
|
||||
|
||||
void OnDisable()
|
||||
{
|
||||
//Debug.Log("NetworkDiscoveryBase OnDisable");
|
||||
Shutdown();
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
//Debug.Log("NetworkDiscoveryBase OnDestroy");
|
||||
Shutdown();
|
||||
}
|
||||
|
||||
void Shutdown()
|
||||
{
|
||||
EndpMulticastLock();
|
||||
if (serverUdpClient != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
serverUdpClient.Close();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// it is just close, swallow the error
|
||||
}
|
||||
|
||||
serverUdpClient = null;
|
||||
}
|
||||
|
||||
if (clientUdpClient != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
clientUdpClient.Close();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// it is just close, swallow the error
|
||||
}
|
||||
|
||||
clientUdpClient = null;
|
||||
}
|
||||
|
||||
CancelInvoke();
|
||||
}
|
||||
|
||||
#region Server
|
||||
|
||||
/// <summary>
|
||||
/// Advertise this server in the local network
|
||||
/// </summary>
|
||||
public void AdvertiseServer()
|
||||
{
|
||||
if (!SupportedOnThisPlatform)
|
||||
throw new PlatformNotSupportedException("Network discovery not supported in this platform");
|
||||
|
||||
StopDiscovery();
|
||||
|
||||
// Setup port -- may throw exception
|
||||
serverUdpClient = new UdpClient(serverBroadcastListenPort)
|
||||
{
|
||||
EnableBroadcast = true,
|
||||
MulticastLoopback = false
|
||||
};
|
||||
|
||||
//Debug.Log($"Discovery: Advertising Server {Dns.GetHostName()}");
|
||||
|
||||
// listen for client pings
|
||||
_ = ServerListenAsync();
|
||||
}
|
||||
|
||||
public async Task ServerListenAsync()
|
||||
{
|
||||
BeginMulticastLock();
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
{
|
||||
await ReceiveRequestAsync(serverUdpClient);
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// socket has been closed
|
||||
break;
|
||||
}
|
||||
catch (Exception) {}
|
||||
}
|
||||
}
|
||||
|
||||
async Task ReceiveRequestAsync(UdpClient udpClient)
|
||||
{
|
||||
// only proceed if there is available data in network buffer, or otherwise Receive() will block
|
||||
// average time for UdpClient.Available : 10 us
|
||||
|
||||
UdpReceiveResult udpReceiveResult = await udpClient.ReceiveAsync();
|
||||
|
||||
using (NetworkReaderPooled networkReader = NetworkReaderPool.Get(udpReceiveResult.Buffer))
|
||||
{
|
||||
long handshake = networkReader.ReadLong();
|
||||
if (handshake != secretHandshake)
|
||||
{
|
||||
// message is not for us
|
||||
throw new ProtocolViolationException("Invalid handshake");
|
||||
}
|
||||
|
||||
Request request = networkReader.Read<Request>();
|
||||
|
||||
ProcessClientRequest(request, udpReceiveResult.RemoteEndPoint);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reply to the client to inform it of this server
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Override if you wish to ignore server requests based on
|
||||
/// custom criteria such as language, full server game mode or difficulty
|
||||
/// </remarks>
|
||||
/// <param name="request">Request coming from client</param>
|
||||
/// <param name="endpoint">Address of the client that sent the request</param>
|
||||
protected virtual void ProcessClientRequest(Request request, IPEndPoint endpoint)
|
||||
{
|
||||
Response info = ProcessRequest(request, endpoint);
|
||||
|
||||
if (info == null)
|
||||
return;
|
||||
|
||||
using (NetworkWriterPooled writer = NetworkWriterPool.Get())
|
||||
{
|
||||
try
|
||||
{
|
||||
writer.WriteLong(secretHandshake);
|
||||
|
||||
writer.Write(info);
|
||||
|
||||
ArraySegment<byte> data = writer.ToArraySegment();
|
||||
// signature matches
|
||||
// send response
|
||||
serverUdpClient.Send(data.Array, data.Count, endpoint);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogException(ex, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process the request from a client
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Override if you wish to provide more information to the clients
|
||||
/// such as the name of the host player
|
||||
/// </remarks>
|
||||
/// <param name="request">Request coming from client</param>
|
||||
/// <param name="endpoint">Address of the client that sent the request</param>
|
||||
/// <returns>The message to be sent back to the client or null</returns>
|
||||
protected abstract Response ProcessRequest(Request request, IPEndPoint endpoint);
|
||||
|
||||
// Android Multicast fix: https://github.com/vis2k/Mirror/pull/2887
|
||||
#if UNITY_ANDROID
|
||||
AndroidJavaObject multicastLock;
|
||||
bool hasMulticastLock;
|
||||
#endif
|
||||
|
||||
void BeginMulticastLock()
|
||||
{
|
||||
#if UNITY_ANDROID
|
||||
if (hasMulticastLock) return;
|
||||
|
||||
if (Application.platform == RuntimePlatform.Android)
|
||||
{
|
||||
using (AndroidJavaObject activity = new AndroidJavaClass("com.unity3d.player.UnityPlayer").GetStatic<AndroidJavaObject>("currentActivity"))
|
||||
{
|
||||
using (var wifiManager = activity.Call<AndroidJavaObject>("getSystemService", "wifi"))
|
||||
{
|
||||
multicastLock = wifiManager.Call<AndroidJavaObject>("createMulticastLock", "lock");
|
||||
multicastLock.Call("acquire");
|
||||
hasMulticastLock = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void EndpMulticastLock()
|
||||
{
|
||||
#if UNITY_ANDROID
|
||||
if (!hasMulticastLock) return;
|
||||
|
||||
multicastLock?.Call("release");
|
||||
hasMulticastLock = false;
|
||||
#endif
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Client
|
||||
|
||||
/// <summary>
|
||||
/// Start Active Discovery
|
||||
/// </summary>
|
||||
public void StartDiscovery()
|
||||
{
|
||||
if (!SupportedOnThisPlatform)
|
||||
throw new PlatformNotSupportedException("Network discovery not supported in this platform");
|
||||
|
||||
StopDiscovery();
|
||||
|
||||
try
|
||||
{
|
||||
// Setup port
|
||||
clientUdpClient = new UdpClient(0)
|
||||
{
|
||||
EnableBroadcast = true,
|
||||
MulticastLoopback = false
|
||||
};
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Free the port if we took it
|
||||
//Debug.LogError("NetworkDiscoveryBase StartDiscovery Exception");
|
||||
Shutdown();
|
||||
throw;
|
||||
}
|
||||
|
||||
_ = ClientListenAsync();
|
||||
|
||||
if (enableActiveDiscovery) InvokeRepeating(nameof(BroadcastDiscoveryRequest), 0, ActiveDiscoveryInterval);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stop Active Discovery
|
||||
/// </summary>
|
||||
public void StopDiscovery()
|
||||
{
|
||||
//Debug.Log("NetworkDiscoveryBase StopDiscovery");
|
||||
Shutdown();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Awaits for server response
|
||||
/// </summary>
|
||||
/// <returns>ClientListenAsync Task</returns>
|
||||
public async Task ClientListenAsync()
|
||||
{
|
||||
// while clientUpdClient to fix:
|
||||
// https://github.com/vis2k/Mirror/pull/2908
|
||||
//
|
||||
// If, you cancel discovery the clientUdpClient is set to null.
|
||||
// However, nothing cancels ClientListenAsync. If we change the if(true)
|
||||
// to check if the client is null. You can properly cancel the discovery,
|
||||
// and kill the listen thread.
|
||||
//
|
||||
// Prior to this fix, if you cancel the discovery search. It crashes the
|
||||
// thread, and is super noisy in the output. As well as causes issues on
|
||||
// the quest.
|
||||
while (clientUdpClient != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await ReceiveGameBroadcastAsync(clientUdpClient);
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// socket was closed, no problem
|
||||
return;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogException(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends discovery request from client
|
||||
/// </summary>
|
||||
public void BroadcastDiscoveryRequest()
|
||||
{
|
||||
if (clientUdpClient == null)
|
||||
return;
|
||||
|
||||
if (NetworkClient.isConnected)
|
||||
{
|
||||
StopDiscovery();
|
||||
return;
|
||||
}
|
||||
|
||||
IPEndPoint endPoint = new IPEndPoint(IPAddress.Broadcast, serverBroadcastListenPort);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(BroadcastAddress))
|
||||
{
|
||||
try
|
||||
{
|
||||
endPoint = new IPEndPoint(IPAddress.Parse(BroadcastAddress), serverBroadcastListenPort);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
using (NetworkWriterPooled writer = NetworkWriterPool.Get())
|
||||
{
|
||||
writer.WriteLong(secretHandshake);
|
||||
|
||||
try
|
||||
{
|
||||
Request request = GetRequest();
|
||||
|
||||
writer.Write(request);
|
||||
|
||||
ArraySegment<byte> data = writer.ToArraySegment();
|
||||
|
||||
//Debug.Log($"Discovery: Sending BroadcastDiscoveryRequest {request}");
|
||||
clientUdpClient.SendAsync(data.Array, data.Count, endPoint);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// It is ok if we can't broadcast to one of the addresses
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a message that will be broadcasted on the network to discover servers
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Override if you wish to include additional data in the discovery message
|
||||
/// such as desired game mode, language, difficulty, etc... </remarks>
|
||||
/// <returns>An instance of ServerRequest with data to be broadcasted</returns>
|
||||
protected virtual Request GetRequest() => default;
|
||||
|
||||
async Task ReceiveGameBroadcastAsync(UdpClient udpClient)
|
||||
{
|
||||
// only proceed if there is available data in network buffer, or otherwise Receive() will block
|
||||
// average time for UdpClient.Available : 10 us
|
||||
|
||||
UdpReceiveResult udpReceiveResult = await udpClient.ReceiveAsync();
|
||||
|
||||
using (NetworkReaderPooled networkReader = NetworkReaderPool.Get(udpReceiveResult.Buffer))
|
||||
{
|
||||
if (networkReader.ReadLong() != secretHandshake)
|
||||
return;
|
||||
|
||||
Response response = networkReader.Read<Response>();
|
||||
|
||||
ProcessResponse(response, udpReceiveResult.RemoteEndPoint);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process the answer from a server
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A client receives a reply from a server, this method processes the
|
||||
/// reply and raises an event
|
||||
/// </remarks>
|
||||
/// <param name="response">Response that came from the server</param>
|
||||
/// <param name="endpoint">Address of the server that replied</param>
|
||||
protected abstract void ProcessResponse(Response response, IPEndPoint endpoint);
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
|
||||
// Based on https://github.com/EnlightenedOne/MirrorNetworkDiscovery
|
||||
// forked from https://github.com/in0finite/MirrorNetworkDiscovery
|
||||
// Both are MIT Licensed
|
||||
|
||||
namespace Mirror.Discovery
|
||||
{
|
||||
/// <summary>
|
||||
/// Base implementation for Network Discovery. Extend this component
|
||||
/// to provide custom discovery with game specific data
|
||||
/// <see cref="NetworkDiscovery">NetworkDiscovery</see> for a sample implementation
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-discovery")]
|
||||
public abstract class NetworkDiscoveryBase<Request, Response> : MonoBehaviour
|
||||
where Request : NetworkMessage
|
||||
where Response : NetworkMessage
|
||||
{
|
||||
public static bool SupportedOnThisPlatform { get { return Application.platform != RuntimePlatform.WebGLPlayer; } }
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("If true, broadcasts a discovery request every ActiveDiscoveryInterval seconds")]
|
||||
public bool enableActiveDiscovery = true;
|
||||
|
||||
// broadcast address needs to be configurable on iOS:
|
||||
// https://github.com/vis2k/Mirror/pull/3255
|
||||
[Tooltip("iOS may require LAN IP address here (e.g. 192.168.x.x), otherwise leave blank.")]
|
||||
public string BroadcastAddress = "";
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("The UDP port the server will listen for multi-cast messages")]
|
||||
protected int serverBroadcastListenPort = 47777;
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("Time in seconds between multi-cast messages")]
|
||||
[Range(1, 60)]
|
||||
float ActiveDiscoveryInterval = 3;
|
||||
|
||||
[Tooltip("Transport to be advertised during discovery")]
|
||||
public Transport transport;
|
||||
|
||||
[Tooltip("Invoked when a server is found")]
|
||||
public ServerFoundUnityEvent<Response> OnServerFound;
|
||||
|
||||
// Each game should have a random unique handshake,
|
||||
// this way you can tell if this is the same game or not
|
||||
[HideInInspector]
|
||||
public long secretHandshake;
|
||||
|
||||
public long ServerId { get; private set; }
|
||||
|
||||
protected UdpClient serverUdpClient;
|
||||
protected UdpClient clientUdpClient;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
public virtual void OnValidate()
|
||||
{
|
||||
if (transport == null)
|
||||
transport = GetComponent<Transport>();
|
||||
|
||||
if (secretHandshake == 0)
|
||||
{
|
||||
secretHandshake = RandomLong();
|
||||
UnityEditor.Undo.RecordObject(this, "Set secret handshake");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// virtual so that inheriting classes' Start() can call base.Start() too
|
||||
/// </summary>
|
||||
public virtual void Start()
|
||||
{
|
||||
ServerId = RandomLong();
|
||||
|
||||
// active transport gets initialized in Awake
|
||||
// so make sure we set it here in Start() after Awake
|
||||
// Or just let the user assign it in the inspector
|
||||
if (transport == null)
|
||||
transport = Transport.active;
|
||||
|
||||
// Server mode? then start advertising
|
||||
if (Utils.IsHeadless())
|
||||
{
|
||||
AdvertiseServer();
|
||||
}
|
||||
}
|
||||
|
||||
public static long RandomLong()
|
||||
{
|
||||
int value1 = UnityEngine.Random.Range(int.MinValue, int.MaxValue);
|
||||
int value2 = UnityEngine.Random.Range(int.MinValue, int.MaxValue);
|
||||
return value1 + ((long)value2 << 32);
|
||||
}
|
||||
|
||||
// Ensure the ports are cleared no matter when Game/Unity UI exits
|
||||
void OnApplicationQuit()
|
||||
{
|
||||
//Debug.Log("NetworkDiscoveryBase OnApplicationQuit");
|
||||
Shutdown();
|
||||
}
|
||||
|
||||
void OnDisable()
|
||||
{
|
||||
//Debug.Log("NetworkDiscoveryBase OnDisable");
|
||||
Shutdown();
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
//Debug.Log("NetworkDiscoveryBase OnDestroy");
|
||||
Shutdown();
|
||||
}
|
||||
|
||||
void Shutdown()
|
||||
{
|
||||
EndpMulticastLock();
|
||||
if (serverUdpClient != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
serverUdpClient.Close();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// it is just close, swallow the error
|
||||
}
|
||||
|
||||
serverUdpClient = null;
|
||||
}
|
||||
|
||||
if (clientUdpClient != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
clientUdpClient.Close();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// it is just close, swallow the error
|
||||
}
|
||||
|
||||
clientUdpClient = null;
|
||||
}
|
||||
|
||||
CancelInvoke();
|
||||
}
|
||||
|
||||
#region Server
|
||||
|
||||
/// <summary>
|
||||
/// Advertise this server in the local network
|
||||
/// </summary>
|
||||
public void AdvertiseServer()
|
||||
{
|
||||
if (!SupportedOnThisPlatform)
|
||||
throw new PlatformNotSupportedException("Network discovery not supported in this platform");
|
||||
|
||||
StopDiscovery();
|
||||
|
||||
// Setup port -- may throw exception
|
||||
serverUdpClient = new UdpClient(serverBroadcastListenPort)
|
||||
{
|
||||
EnableBroadcast = true,
|
||||
MulticastLoopback = false
|
||||
};
|
||||
|
||||
//Debug.Log($"Discovery: Advertising Server {Dns.GetHostName()}");
|
||||
|
||||
// listen for client pings
|
||||
_ = ServerListenAsync();
|
||||
}
|
||||
|
||||
public async Task ServerListenAsync()
|
||||
{
|
||||
BeginMulticastLock();
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
{
|
||||
await ReceiveRequestAsync(serverUdpClient);
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// socket has been closed
|
||||
break;
|
||||
}
|
||||
catch (Exception) {}
|
||||
}
|
||||
}
|
||||
|
||||
async Task ReceiveRequestAsync(UdpClient udpClient)
|
||||
{
|
||||
// only proceed if there is available data in network buffer, or otherwise Receive() will block
|
||||
// average time for UdpClient.Available : 10 us
|
||||
|
||||
UdpReceiveResult udpReceiveResult = await udpClient.ReceiveAsync();
|
||||
|
||||
using (NetworkReaderPooled networkReader = NetworkReaderPool.Get(udpReceiveResult.Buffer))
|
||||
{
|
||||
long handshake = networkReader.ReadLong();
|
||||
if (handshake != secretHandshake)
|
||||
{
|
||||
// message is not for us
|
||||
throw new ProtocolViolationException("Invalid handshake");
|
||||
}
|
||||
|
||||
Request request = networkReader.Read<Request>();
|
||||
|
||||
ProcessClientRequest(request, udpReceiveResult.RemoteEndPoint);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reply to the client to inform it of this server
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Override if you wish to ignore server requests based on
|
||||
/// custom criteria such as language, full server game mode or difficulty
|
||||
/// </remarks>
|
||||
/// <param name="request">Request coming from client</param>
|
||||
/// <param name="endpoint">Address of the client that sent the request</param>
|
||||
protected virtual void ProcessClientRequest(Request request, IPEndPoint endpoint)
|
||||
{
|
||||
Response info = ProcessRequest(request, endpoint);
|
||||
|
||||
if (info == null)
|
||||
return;
|
||||
|
||||
using (NetworkWriterPooled writer = NetworkWriterPool.Get())
|
||||
{
|
||||
try
|
||||
{
|
||||
writer.WriteLong(secretHandshake);
|
||||
|
||||
writer.Write(info);
|
||||
|
||||
ArraySegment<byte> data = writer.ToArraySegment();
|
||||
// signature matches
|
||||
// send response
|
||||
serverUdpClient.Send(data.Array, data.Count, endpoint);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogException(ex, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process the request from a client
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Override if you wish to provide more information to the clients
|
||||
/// such as the name of the host player
|
||||
/// </remarks>
|
||||
/// <param name="request">Request coming from client</param>
|
||||
/// <param name="endpoint">Address of the client that sent the request</param>
|
||||
/// <returns>The message to be sent back to the client or null</returns>
|
||||
protected abstract Response ProcessRequest(Request request, IPEndPoint endpoint);
|
||||
|
||||
// Android Multicast fix: https://github.com/vis2k/Mirror/pull/2887
|
||||
#if UNITY_ANDROID
|
||||
AndroidJavaObject multicastLock;
|
||||
bool hasMulticastLock;
|
||||
#endif
|
||||
|
||||
void BeginMulticastLock()
|
||||
{
|
||||
#if UNITY_ANDROID
|
||||
if (hasMulticastLock) return;
|
||||
|
||||
if (Application.platform == RuntimePlatform.Android)
|
||||
{
|
||||
using (AndroidJavaObject activity = new AndroidJavaClass("com.unity3d.player.UnityPlayer").GetStatic<AndroidJavaObject>("currentActivity"))
|
||||
{
|
||||
using (var wifiManager = activity.Call<AndroidJavaObject>("getSystemService", "wifi"))
|
||||
{
|
||||
multicastLock = wifiManager.Call<AndroidJavaObject>("createMulticastLock", "lock");
|
||||
multicastLock.Call("acquire");
|
||||
hasMulticastLock = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void EndpMulticastLock()
|
||||
{
|
||||
#if UNITY_ANDROID
|
||||
if (!hasMulticastLock) return;
|
||||
|
||||
multicastLock?.Call("release");
|
||||
hasMulticastLock = false;
|
||||
#endif
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Client
|
||||
|
||||
/// <summary>
|
||||
/// Start Active Discovery
|
||||
/// </summary>
|
||||
public void StartDiscovery()
|
||||
{
|
||||
if (!SupportedOnThisPlatform)
|
||||
throw new PlatformNotSupportedException("Network discovery not supported in this platform");
|
||||
|
||||
StopDiscovery();
|
||||
|
||||
try
|
||||
{
|
||||
// Setup port
|
||||
clientUdpClient = new UdpClient(0)
|
||||
{
|
||||
EnableBroadcast = true,
|
||||
MulticastLoopback = false
|
||||
};
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Free the port if we took it
|
||||
//Debug.LogError("NetworkDiscoveryBase StartDiscovery Exception");
|
||||
Shutdown();
|
||||
throw;
|
||||
}
|
||||
|
||||
_ = ClientListenAsync();
|
||||
|
||||
if (enableActiveDiscovery) InvokeRepeating(nameof(BroadcastDiscoveryRequest), 0, ActiveDiscoveryInterval);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stop Active Discovery
|
||||
/// </summary>
|
||||
public void StopDiscovery()
|
||||
{
|
||||
//Debug.Log("NetworkDiscoveryBase StopDiscovery");
|
||||
Shutdown();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Awaits for server response
|
||||
/// </summary>
|
||||
/// <returns>ClientListenAsync Task</returns>
|
||||
public async Task ClientListenAsync()
|
||||
{
|
||||
// while clientUpdClient to fix:
|
||||
// https://github.com/vis2k/Mirror/pull/2908
|
||||
//
|
||||
// If, you cancel discovery the clientUdpClient is set to null.
|
||||
// However, nothing cancels ClientListenAsync. If we change the if(true)
|
||||
// to check if the client is null. You can properly cancel the discovery,
|
||||
// and kill the listen thread.
|
||||
//
|
||||
// Prior to this fix, if you cancel the discovery search. It crashes the
|
||||
// thread, and is super noisy in the output. As well as causes issues on
|
||||
// the quest.
|
||||
while (clientUdpClient != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await ReceiveGameBroadcastAsync(clientUdpClient);
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// socket was closed, no problem
|
||||
return;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogException(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends discovery request from client
|
||||
/// </summary>
|
||||
public void BroadcastDiscoveryRequest()
|
||||
{
|
||||
if (clientUdpClient == null)
|
||||
return;
|
||||
|
||||
if (NetworkClient.isConnected)
|
||||
{
|
||||
StopDiscovery();
|
||||
return;
|
||||
}
|
||||
|
||||
IPEndPoint endPoint = new IPEndPoint(IPAddress.Broadcast, serverBroadcastListenPort);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(BroadcastAddress))
|
||||
{
|
||||
try
|
||||
{
|
||||
endPoint = new IPEndPoint(IPAddress.Parse(BroadcastAddress), serverBroadcastListenPort);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
using (NetworkWriterPooled writer = NetworkWriterPool.Get())
|
||||
{
|
||||
writer.WriteLong(secretHandshake);
|
||||
|
||||
try
|
||||
{
|
||||
Request request = GetRequest();
|
||||
|
||||
writer.Write(request);
|
||||
|
||||
ArraySegment<byte> data = writer.ToArraySegment();
|
||||
|
||||
//Debug.Log($"Discovery: Sending BroadcastDiscoveryRequest {request}");
|
||||
clientUdpClient.SendAsync(data.Array, data.Count, endPoint);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// It is ok if we can't broadcast to one of the addresses
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a message that will be broadcasted on the network to discover servers
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Override if you wish to include additional data in the discovery message
|
||||
/// such as desired game mode, language, difficulty, etc... </remarks>
|
||||
/// <returns>An instance of ServerRequest with data to be broadcasted</returns>
|
||||
protected virtual Request GetRequest() => default;
|
||||
|
||||
async Task ReceiveGameBroadcastAsync(UdpClient udpClient)
|
||||
{
|
||||
// only proceed if there is available data in network buffer, or otherwise Receive() will block
|
||||
// average time for UdpClient.Available : 10 us
|
||||
|
||||
UdpReceiveResult udpReceiveResult = await udpClient.ReceiveAsync();
|
||||
|
||||
using (NetworkReaderPooled networkReader = NetworkReaderPool.Get(udpReceiveResult.Buffer))
|
||||
{
|
||||
if (networkReader.ReadLong() != secretHandshake)
|
||||
return;
|
||||
|
||||
Response response = networkReader.Read<Response>();
|
||||
|
||||
ProcessResponse(response, udpReceiveResult.RemoteEndPoint);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process the answer from a server
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A client receives a reply from a server, this method processes the
|
||||
/// reply and raises an event
|
||||
/// </remarks>
|
||||
/// <param name="response">Response that came from the server</param>
|
||||
/// <param name="endpoint">Address of the server that replied</param>
|
||||
protected abstract void ProcessResponse(Response response, IPEndPoint endpoint);
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b9971d60ce61f4e39b07cd9e7e0c68fa
|
||||
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/Discovery/NetworkDiscoveryBase.cs
|
||||
uploadId: 736421
|
||||
fileFormatVersion: 2
|
||||
guid: b9971d60ce61f4e39b07cd9e7e0c68fa
|
||||
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/Discovery/NetworkDiscoveryBase.cs
|
||||
uploadId: 736421
|
||||
|
||||
@@ -1,144 +1,144 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror.Discovery
|
||||
{
|
||||
[DisallowMultipleComponent]
|
||||
[AddComponentMenu("Network/Network Discovery HUD")]
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-discovery")]
|
||||
[RequireComponent(typeof(NetworkDiscovery))]
|
||||
public class NetworkDiscoveryHUD : MonoBehaviour
|
||||
{
|
||||
readonly Dictionary<long, ServerResponse> discoveredServers = new Dictionary<long, ServerResponse>();
|
||||
Vector2 scrollViewPos = Vector2.zero;
|
||||
|
||||
public NetworkDiscovery networkDiscovery;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
void OnValidate()
|
||||
{
|
||||
if (Application.isPlaying) return;
|
||||
Reset();
|
||||
}
|
||||
|
||||
void Reset()
|
||||
{
|
||||
networkDiscovery = GetComponent<NetworkDiscovery>();
|
||||
|
||||
// Add default event handler if not already present
|
||||
if (!Enumerable.Range(0, networkDiscovery.OnServerFound.GetPersistentEventCount())
|
||||
.Any(i => networkDiscovery.OnServerFound.GetPersistentMethodName(i) == nameof(OnDiscoveredServer)))
|
||||
{
|
||||
UnityEditor.Events.UnityEventTools.AddPersistentListener(networkDiscovery.OnServerFound, OnDiscoveredServer);
|
||||
UnityEditor.Undo.RecordObjects(new UnityEngine.Object[] { this, networkDiscovery }, "Set NetworkDiscovery");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void OnGUI()
|
||||
{
|
||||
if (NetworkManager.singleton == null)
|
||||
return;
|
||||
|
||||
if (!NetworkClient.isConnected && !NetworkServer.active && !NetworkClient.active)
|
||||
DrawGUI();
|
||||
|
||||
if (NetworkServer.active || NetworkClient.active)
|
||||
StopButtons();
|
||||
}
|
||||
|
||||
void DrawGUI()
|
||||
{
|
||||
GUILayout.BeginArea(new Rect(10, 10, 300, 500));
|
||||
GUILayout.BeginHorizontal();
|
||||
|
||||
if (GUILayout.Button("Find Servers"))
|
||||
{
|
||||
discoveredServers.Clear();
|
||||
networkDiscovery.StartDiscovery();
|
||||
}
|
||||
|
||||
// LAN Host
|
||||
if (GUILayout.Button("Start Host"))
|
||||
{
|
||||
discoveredServers.Clear();
|
||||
NetworkManager.singleton.StartHost();
|
||||
networkDiscovery.AdvertiseServer();
|
||||
}
|
||||
|
||||
// Dedicated server
|
||||
if (GUILayout.Button("Start Server"))
|
||||
{
|
||||
discoveredServers.Clear();
|
||||
NetworkManager.singleton.StartServer();
|
||||
networkDiscovery.AdvertiseServer();
|
||||
}
|
||||
|
||||
GUILayout.EndHorizontal();
|
||||
|
||||
// show list of found server
|
||||
|
||||
GUILayout.Label($"Discovered Servers [{discoveredServers.Count}]:");
|
||||
|
||||
// servers
|
||||
scrollViewPos = GUILayout.BeginScrollView(scrollViewPos);
|
||||
|
||||
foreach (ServerResponse info in discoveredServers.Values)
|
||||
if (GUILayout.Button(info.EndPoint.Address.ToString()))
|
||||
Connect(info);
|
||||
|
||||
GUILayout.EndScrollView();
|
||||
GUILayout.EndArea();
|
||||
}
|
||||
|
||||
void StopButtons()
|
||||
{
|
||||
GUILayout.BeginArea(new Rect(10, 40, 100, 25));
|
||||
|
||||
// stop host if host mode
|
||||
if (NetworkServer.active && NetworkClient.isConnected)
|
||||
{
|
||||
if (GUILayout.Button("Stop Host"))
|
||||
{
|
||||
NetworkManager.singleton.StopHost();
|
||||
networkDiscovery.StopDiscovery();
|
||||
}
|
||||
}
|
||||
// stop client if client-only
|
||||
else if (NetworkClient.isConnected)
|
||||
{
|
||||
if (GUILayout.Button("Stop Client"))
|
||||
{
|
||||
NetworkManager.singleton.StopClient();
|
||||
networkDiscovery.StopDiscovery();
|
||||
}
|
||||
}
|
||||
// stop server if server-only
|
||||
else if (NetworkServer.active)
|
||||
{
|
||||
if (GUILayout.Button("Stop Server"))
|
||||
{
|
||||
NetworkManager.singleton.StopServer();
|
||||
networkDiscovery.StopDiscovery();
|
||||
}
|
||||
}
|
||||
|
||||
GUILayout.EndArea();
|
||||
}
|
||||
|
||||
void Connect(ServerResponse info)
|
||||
{
|
||||
networkDiscovery.StopDiscovery();
|
||||
NetworkManager.singleton.StartClient(info.uri);
|
||||
}
|
||||
|
||||
public void OnDiscoveredServer(ServerResponse info)
|
||||
{
|
||||
Debug.Log($"Discovered Server: {info.serverId} | {info.EndPoint} | {info.uri}");
|
||||
|
||||
// Note that you can check the versioning to decide if you can connect to the server or not using this method
|
||||
discoveredServers[info.serverId] = info;
|
||||
}
|
||||
}
|
||||
}
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror.Discovery
|
||||
{
|
||||
[DisallowMultipleComponent]
|
||||
[AddComponentMenu("Network/Network Discovery HUD")]
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-discovery")]
|
||||
[RequireComponent(typeof(NetworkDiscovery))]
|
||||
public class NetworkDiscoveryHUD : MonoBehaviour
|
||||
{
|
||||
readonly Dictionary<long, ServerResponse> discoveredServers = new Dictionary<long, ServerResponse>();
|
||||
Vector2 scrollViewPos = Vector2.zero;
|
||||
|
||||
public NetworkDiscovery networkDiscovery;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
void OnValidate()
|
||||
{
|
||||
if (Application.isPlaying) return;
|
||||
Reset();
|
||||
}
|
||||
|
||||
void Reset()
|
||||
{
|
||||
networkDiscovery = GetComponent<NetworkDiscovery>();
|
||||
|
||||
// Add default event handler if not already present
|
||||
if (!Enumerable.Range(0, networkDiscovery.OnServerFound.GetPersistentEventCount())
|
||||
.Any(i => networkDiscovery.OnServerFound.GetPersistentMethodName(i) == nameof(OnDiscoveredServer)))
|
||||
{
|
||||
UnityEditor.Events.UnityEventTools.AddPersistentListener(networkDiscovery.OnServerFound, OnDiscoveredServer);
|
||||
UnityEditor.Undo.RecordObjects(new UnityEngine.Object[] { this, networkDiscovery }, "Set NetworkDiscovery");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void OnGUI()
|
||||
{
|
||||
if (NetworkManager.singleton == null)
|
||||
return;
|
||||
|
||||
if (!NetworkClient.isConnected && !NetworkServer.active && !NetworkClient.active)
|
||||
DrawGUI();
|
||||
|
||||
if (NetworkServer.active || NetworkClient.active)
|
||||
StopButtons();
|
||||
}
|
||||
|
||||
void DrawGUI()
|
||||
{
|
||||
GUILayout.BeginArea(new Rect(10, 10, 300, 500));
|
||||
GUILayout.BeginHorizontal();
|
||||
|
||||
if (GUILayout.Button("Find Servers"))
|
||||
{
|
||||
discoveredServers.Clear();
|
||||
networkDiscovery.StartDiscovery();
|
||||
}
|
||||
|
||||
// LAN Host
|
||||
if (GUILayout.Button("Start Host"))
|
||||
{
|
||||
discoveredServers.Clear();
|
||||
NetworkManager.singleton.StartHost();
|
||||
networkDiscovery.AdvertiseServer();
|
||||
}
|
||||
|
||||
// Dedicated server
|
||||
if (GUILayout.Button("Start Server"))
|
||||
{
|
||||
discoveredServers.Clear();
|
||||
NetworkManager.singleton.StartServer();
|
||||
networkDiscovery.AdvertiseServer();
|
||||
}
|
||||
|
||||
GUILayout.EndHorizontal();
|
||||
|
||||
// show list of found server
|
||||
|
||||
GUILayout.Label($"Discovered Servers [{discoveredServers.Count}]:");
|
||||
|
||||
// servers
|
||||
scrollViewPos = GUILayout.BeginScrollView(scrollViewPos);
|
||||
|
||||
foreach (ServerResponse info in discoveredServers.Values)
|
||||
if (GUILayout.Button(info.EndPoint.Address.ToString()))
|
||||
Connect(info);
|
||||
|
||||
GUILayout.EndScrollView();
|
||||
GUILayout.EndArea();
|
||||
}
|
||||
|
||||
void StopButtons()
|
||||
{
|
||||
GUILayout.BeginArea(new Rect(10, 40, 100, 25));
|
||||
|
||||
// stop host if host mode
|
||||
if (NetworkServer.active && NetworkClient.isConnected)
|
||||
{
|
||||
if (GUILayout.Button("Stop Host"))
|
||||
{
|
||||
NetworkManager.singleton.StopHost();
|
||||
networkDiscovery.StopDiscovery();
|
||||
}
|
||||
}
|
||||
// stop client if client-only
|
||||
else if (NetworkClient.isConnected)
|
||||
{
|
||||
if (GUILayout.Button("Stop Client"))
|
||||
{
|
||||
NetworkManager.singleton.StopClient();
|
||||
networkDiscovery.StopDiscovery();
|
||||
}
|
||||
}
|
||||
// stop server if server-only
|
||||
else if (NetworkServer.active)
|
||||
{
|
||||
if (GUILayout.Button("Stop Server"))
|
||||
{
|
||||
NetworkManager.singleton.StopServer();
|
||||
networkDiscovery.StopDiscovery();
|
||||
}
|
||||
}
|
||||
|
||||
GUILayout.EndArea();
|
||||
}
|
||||
|
||||
void Connect(ServerResponse info)
|
||||
{
|
||||
networkDiscovery.StopDiscovery();
|
||||
NetworkManager.singleton.StartClient(info.uri);
|
||||
}
|
||||
|
||||
public void OnDiscoveredServer(ServerResponse info)
|
||||
{
|
||||
Debug.Log($"Discovered Server: {info.serverId} | {info.EndPoint} | {info.uri}");
|
||||
|
||||
// Note that you can check the versioning to decide if you can connect to the server or not using this method
|
||||
discoveredServers[info.serverId] = info;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 88c37d3deca7a834d80cfd8d3cfcc510
|
||||
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/Discovery/NetworkDiscoveryHUD.cs
|
||||
uploadId: 736421
|
||||
fileFormatVersion: 2
|
||||
guid: 88c37d3deca7a834d80cfd8d3cfcc510
|
||||
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/Discovery/NetworkDiscoveryHUD.cs
|
||||
uploadId: 736421
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Mirror.Discovery
|
||||
{
|
||||
public struct ServerRequest : NetworkMessage {}
|
||||
}
|
||||
namespace Mirror.Discovery
|
||||
{
|
||||
public struct ServerRequest : NetworkMessage {}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ea7254bf7b9454da4adad881d94cd141
|
||||
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/Discovery/ServerRequest.cs
|
||||
uploadId: 736421
|
||||
fileFormatVersion: 2
|
||||
guid: ea7254bf7b9454da4adad881d94cd141
|
||||
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/Discovery/ServerRequest.cs
|
||||
uploadId: 736421
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
|
||||
namespace Mirror.Discovery
|
||||
{
|
||||
public struct ServerResponse : NetworkMessage
|
||||
{
|
||||
// The server that sent this
|
||||
// this is a property so that it is not serialized, but the
|
||||
// client fills this up after we receive it
|
||||
public IPEndPoint EndPoint { get; set; }
|
||||
|
||||
public Uri uri;
|
||||
|
||||
// Prevent duplicate server appearance when a connection can be made via LAN on multiple NICs
|
||||
public long serverId;
|
||||
}
|
||||
using System;
|
||||
using System.Net;
|
||||
|
||||
namespace Mirror.Discovery
|
||||
{
|
||||
public struct ServerResponse : NetworkMessage
|
||||
{
|
||||
// The server that sent this
|
||||
// this is a property so that it is not serialized, but the
|
||||
// client fills this up after we receive it
|
||||
public IPEndPoint EndPoint { get; set; }
|
||||
|
||||
public Uri uri;
|
||||
|
||||
// Prevent duplicate server appearance when a connection can be made via LAN on multiple NICs
|
||||
public long serverId;
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 36f97227fdf2d7a4e902db5bfc43039c
|
||||
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/Discovery/ServerResponse.cs
|
||||
uploadId: 736421
|
||||
fileFormatVersion: 2
|
||||
guid: 36f97227fdf2d7a4e902db5bfc43039c
|
||||
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/Discovery/ServerResponse.cs
|
||||
uploadId: 736421
|
||||
|
||||
@@ -1,133 +1,133 @@
|
||||
// People should be able to see and report errors to the developer very easily.
|
||||
//
|
||||
// Unity's Developer Console only works in development builds and it only shows
|
||||
// errors. This class provides a console that works in all builds and also shows
|
||||
// log and warnings in development builds.
|
||||
//
|
||||
// Note: we don't include the stack trace, because that can also be grabbed from
|
||||
// the log files if needed.
|
||||
//
|
||||
// Note: there is no 'hide' button because we DO want people to see those errors
|
||||
// and report them back to us.
|
||||
//
|
||||
// Note: normal Debug.Log messages can be shown by building in Debug/Development
|
||||
// mode.
|
||||
using UnityEngine;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
struct LogEntry
|
||||
{
|
||||
public string message;
|
||||
public LogType type;
|
||||
|
||||
public LogEntry(string message, LogType type)
|
||||
{
|
||||
this.message = message;
|
||||
this.type = type;
|
||||
}
|
||||
}
|
||||
|
||||
public class GUIConsole : MonoBehaviour
|
||||
{
|
||||
public int height = 80;
|
||||
public int offsetY = 40;
|
||||
|
||||
// only keep the recent 'n' entries. otherwise memory would grow forever
|
||||
// and drawing would get slower and slower.
|
||||
public int maxLogCount = 50;
|
||||
|
||||
// Unity Editor has the Console window, we don't need to show it there.
|
||||
// unless for testing, so keep it as option.
|
||||
public bool showInEditor = false;
|
||||
|
||||
// log as queue so we can remove the first entry easily
|
||||
readonly Queue<LogEntry> log = new Queue<LogEntry>();
|
||||
|
||||
// hotkey to show/hide at runtime for easier debugging
|
||||
// (sometimes we need to temporarily hide/show it)
|
||||
// Default is BackQuote, because F keys are already assigned in browsers
|
||||
[Tooltip("Hotkey to show/hide the console at runtime\nBack Quote is usually on the left above Tab\nChange with caution - F keys are generally already taken in Browsers")]
|
||||
public KeyCode hotKey = KeyCode.BackQuote;
|
||||
|
||||
// GUI
|
||||
bool visible;
|
||||
Vector2 scroll = Vector2.zero;
|
||||
|
||||
// only show at runtime, or if showInEditor is enabled
|
||||
bool show => !Application.isEditor || showInEditor;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
// only show at runtime, or if showInEditor is enabled
|
||||
if (show)
|
||||
Application.logMessageReceived += OnLog;
|
||||
}
|
||||
|
||||
// OnLog logs everything, even Debug.Log messages in release builds
|
||||
// => this makes a lot of things easier. e.g. addon initialization logs.
|
||||
// => it's really better to have than not to have those
|
||||
void OnLog(string message, string stackTrace, LogType type)
|
||||
{
|
||||
// is this important?
|
||||
// => always show exceptions & errors
|
||||
// => usually a good idea to show warnings too, otherwise it's too
|
||||
// easy to miss OnDeserialize warnings etc. in builds
|
||||
bool isImportant = type == LogType.Error || type == LogType.Exception || type == LogType.Warning;
|
||||
|
||||
// use stack trace only if important
|
||||
// (otherwise users would have to find and search the log file.
|
||||
// seeing it in the console directly is way easier to deal with.)
|
||||
// => only add \n if stack trace is available (only in debug builds)
|
||||
if (isImportant && !string.IsNullOrWhiteSpace(stackTrace))
|
||||
message += $"\n{stackTrace}";
|
||||
|
||||
// add to queue
|
||||
log.Enqueue(new LogEntry(message, type));
|
||||
|
||||
// respect max entries
|
||||
if (log.Count > maxLogCount)
|
||||
log.Dequeue();
|
||||
|
||||
// become visible if it was important
|
||||
// (no need to become visible for regular log. let the user decide.)
|
||||
if (isImportant)
|
||||
visible = true;
|
||||
|
||||
// auto scroll
|
||||
scroll.y = float.MaxValue;
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (show && Input.GetKeyDown(hotKey))
|
||||
visible = !visible;
|
||||
}
|
||||
|
||||
void OnGUI()
|
||||
{
|
||||
if (!visible) return;
|
||||
|
||||
// If this offset is changed, also change width in NetworkManagerHUD::OnGUI
|
||||
int offsetX = 300 + 20;
|
||||
|
||||
GUILayout.BeginArea(new Rect(offsetX, offsetY, Screen.width - offsetX - 10, height));
|
||||
|
||||
scroll = GUILayout.BeginScrollView(scroll, "Box", GUILayout.Width(Screen.width - offsetX - 10), GUILayout.Height(height));
|
||||
foreach (LogEntry entry in log)
|
||||
{
|
||||
if (entry.type == LogType.Error || entry.type == LogType.Exception)
|
||||
GUI.color = Color.red;
|
||||
else if (entry.type == LogType.Warning)
|
||||
GUI.color = Color.yellow;
|
||||
|
||||
GUILayout.Label(entry.message);
|
||||
GUI.color = Color.white;
|
||||
}
|
||||
GUILayout.EndScrollView();
|
||||
|
||||
GUILayout.EndArea();
|
||||
}
|
||||
}
|
||||
}
|
||||
// People should be able to see and report errors to the developer very easily.
|
||||
//
|
||||
// Unity's Developer Console only works in development builds and it only shows
|
||||
// errors. This class provides a console that works in all builds and also shows
|
||||
// log and warnings in development builds.
|
||||
//
|
||||
// Note: we don't include the stack trace, because that can also be grabbed from
|
||||
// the log files if needed.
|
||||
//
|
||||
// Note: there is no 'hide' button because we DO want people to see those errors
|
||||
// and report them back to us.
|
||||
//
|
||||
// Note: normal Debug.Log messages can be shown by building in Debug/Development
|
||||
// mode.
|
||||
using UnityEngine;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
struct LogEntry
|
||||
{
|
||||
public string message;
|
||||
public LogType type;
|
||||
|
||||
public LogEntry(string message, LogType type)
|
||||
{
|
||||
this.message = message;
|
||||
this.type = type;
|
||||
}
|
||||
}
|
||||
|
||||
public class GUIConsole : MonoBehaviour
|
||||
{
|
||||
public int height = 80;
|
||||
public int offsetY = 40;
|
||||
|
||||
// only keep the recent 'n' entries. otherwise memory would grow forever
|
||||
// and drawing would get slower and slower.
|
||||
public int maxLogCount = 50;
|
||||
|
||||
// Unity Editor has the Console window, we don't need to show it there.
|
||||
// unless for testing, so keep it as option.
|
||||
public bool showInEditor = false;
|
||||
|
||||
// log as queue so we can remove the first entry easily
|
||||
readonly Queue<LogEntry> log = new Queue<LogEntry>();
|
||||
|
||||
// hotkey to show/hide at runtime for easier debugging
|
||||
// (sometimes we need to temporarily hide/show it)
|
||||
// Default is BackQuote, because F keys are already assigned in browsers
|
||||
[Tooltip("Hotkey to show/hide the console at runtime\nBack Quote is usually on the left above Tab\nChange with caution - F keys are generally already taken in Browsers")]
|
||||
public KeyCode hotKey = KeyCode.BackQuote;
|
||||
|
||||
// GUI
|
||||
bool visible;
|
||||
Vector2 scroll = Vector2.zero;
|
||||
|
||||
// only show at runtime, or if showInEditor is enabled
|
||||
bool show => !Application.isEditor || showInEditor;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
// only show at runtime, or if showInEditor is enabled
|
||||
if (show)
|
||||
Application.logMessageReceived += OnLog;
|
||||
}
|
||||
|
||||
// OnLog logs everything, even Debug.Log messages in release builds
|
||||
// => this makes a lot of things easier. e.g. addon initialization logs.
|
||||
// => it's really better to have than not to have those
|
||||
void OnLog(string message, string stackTrace, LogType type)
|
||||
{
|
||||
// is this important?
|
||||
// => always show exceptions & errors
|
||||
// => usually a good idea to show warnings too, otherwise it's too
|
||||
// easy to miss OnDeserialize warnings etc. in builds
|
||||
bool isImportant = type == LogType.Error || type == LogType.Exception || type == LogType.Warning;
|
||||
|
||||
// use stack trace only if important
|
||||
// (otherwise users would have to find and search the log file.
|
||||
// seeing it in the console directly is way easier to deal with.)
|
||||
// => only add \n if stack trace is available (only in debug builds)
|
||||
if (isImportant && !string.IsNullOrWhiteSpace(stackTrace))
|
||||
message += $"\n{stackTrace}";
|
||||
|
||||
// add to queue
|
||||
log.Enqueue(new LogEntry(message, type));
|
||||
|
||||
// respect max entries
|
||||
if (log.Count > maxLogCount)
|
||||
log.Dequeue();
|
||||
|
||||
// become visible if it was important
|
||||
// (no need to become visible for regular log. let the user decide.)
|
||||
if (isImportant)
|
||||
visible = true;
|
||||
|
||||
// auto scroll
|
||||
scroll.y = float.MaxValue;
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (show && Input.GetKeyDown(hotKey))
|
||||
visible = !visible;
|
||||
}
|
||||
|
||||
void OnGUI()
|
||||
{
|
||||
if (!visible) return;
|
||||
|
||||
// If this offset is changed, also change width in NetworkManagerHUD::OnGUI
|
||||
int offsetX = 300 + 20;
|
||||
|
||||
GUILayout.BeginArea(new Rect(offsetX, offsetY, Screen.width - offsetX - 10, height));
|
||||
|
||||
scroll = GUILayout.BeginScrollView(scroll, "Box", GUILayout.Width(Screen.width - offsetX - 10), GUILayout.Height(height));
|
||||
foreach (LogEntry entry in log)
|
||||
{
|
||||
if (entry.type == LogType.Error || entry.type == LogType.Exception)
|
||||
GUI.color = Color.red;
|
||||
else if (entry.type == LogType.Warning)
|
||||
GUI.color = Color.yellow;
|
||||
|
||||
GUILayout.Label(entry.message);
|
||||
GUI.color = Color.white;
|
||||
}
|
||||
GUILayout.EndScrollView();
|
||||
|
||||
GUILayout.EndArea();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9021b6cc314944290986ab6feb48db79
|
||||
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/GUIConsole.cs
|
||||
uploadId: 736421
|
||||
fileFormatVersion: 2
|
||||
guid: 9021b6cc314944290986ab6feb48db79
|
||||
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/GUIConsole.cs
|
||||
uploadId: 736421
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c66f27e006ab94253b39a55a3b213651
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
fileFormatVersion: 2
|
||||
guid: c66f27e006ab94253b39a55a3b213651
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fa4cbc6b9c584db4971985cb9f369077
|
||||
timeCreated: 1613110605
|
||||
fileFormatVersion: 2
|
||||
guid: fa4cbc6b9c584db4971985cb9f369077
|
||||
timeCreated: 1613110605
|
||||
|
||||
+89
-89
@@ -1,89 +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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+18
-18
@@ -1,18 +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
|
||||
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
|
||||
|
||||
+15
-15
@@ -1,15 +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;
|
||||
}
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
+18
-18
@@ -1,18 +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
|
||||
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
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5eca5245ae6bb460e9a92f7e14d5493a
|
||||
timeCreated: 1622649517
|
||||
fileFormatVersion: 2
|
||||
guid: 5eca5245ae6bb460e9a92f7e14d5493a
|
||||
timeCreated: 1622649517
|
||||
|
||||
@@ -1,164 +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);
|
||||
}
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +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
|
||||
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
|
||||
|
||||
@@ -1,42 +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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +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
|
||||
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
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7655d309a46a4bd4860edf964228b3f6
|
||||
timeCreated: 1622649517
|
||||
fileFormatVersion: 2
|
||||
guid: 7655d309a46a4bd4860edf964228b3f6
|
||||
timeCreated: 1622649517
|
||||
|
||||
@@ -1,117 +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);
|
||||
}
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +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
|
||||
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
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f4d8c634a8103664db5f90fe8bab9544
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
fileFormatVersion: 2
|
||||
guid: f4d8c634a8103664db5f90fe8bab9544
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
||||
+178
-178
@@ -1,178 +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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+18
-18
@@ -1,18 +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
|
||||
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
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cfa12b73503344d49b398b01bcb07967
|
||||
timeCreated: 1613110634
|
||||
fileFormatVersion: 2
|
||||
guid: cfa12b73503344d49b398b01bcb07967
|
||||
timeCreated: 1613110634
|
||||
|
||||
@@ -1,104 +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();
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +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
|
||||
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
|
||||
|
||||
@@ -1,106 +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();
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +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
|
||||
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
|
||||
|
||||
@@ -1,170 +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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,18 +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
|
||||
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
|
||||
|
||||
@@ -1,243 +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;
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +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
|
||||
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
|
||||
|
||||
+344
-344
@@ -1,345 +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: ±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 (±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 ±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 ±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’t been created yet (e.g., before Awake)
|
||||
if (grid == null)
|
||||
grid = new HexGrid2D(visRange);
|
||||
|
||||
// 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 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
|
||||
}
|
||||
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: ±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 (±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 ±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 ±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’t been created yet (e.g., before Awake)
|
||||
if (grid == null)
|
||||
grid = new HexGrid2D(visRange);
|
||||
|
||||
// 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 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
|
||||
}
|
||||
}
|
||||
+18
-18
@@ -1,18 +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
|
||||
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
|
||||
|
||||
+336
-336
@@ -1,336 +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
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
+18
-18
@@ -1,18 +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
|
||||
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
|
||||
|
||||
+146
-146
@@ -1,146 +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
|
||||
}
|
||||
}
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
+18
-18
@@ -1,18 +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
|
||||
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
|
||||
|
||||
+156
-156
@@ -1,156 +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
|
||||
}
|
||||
}
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
+18
-18
@@ -1,18 +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
|
||||
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
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2d418e60072433b4bbebbf5f3a7de1bb
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
fileFormatVersion: 2
|
||||
guid: 2d418e60072433b4bbebbf5f3a7de1bb
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
||||
@@ -1,39 +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;
|
||||
}
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +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
|
||||
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
|
||||
|
||||
@@ -1,182 +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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +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
|
||||
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
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 00ac1d0527f234939aba22b4d7cbf280
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
fileFormatVersion: 2
|
||||
guid: 00ac1d0527f234939aba22b4d7cbf280
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
||||
@@ -1,109 +1,109 @@
|
||||
// Applies HistoryBounds to the physics world by projecting to a trigger Collider.
|
||||
// This way we can use Physics.Raycast on it.
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
[DisallowMultipleComponent]
|
||||
[AddComponentMenu("Network/ Lag Compensation/ History Collider")]
|
||||
public class HistoryCollider : MonoBehaviour
|
||||
{
|
||||
[Header("Components")]
|
||||
[Tooltip("The object's actual collider. We need to know where it is, and how large it is.")]
|
||||
public Collider actualCollider;
|
||||
|
||||
[Tooltip("The helper collider that the history bounds are projected onto.\nNeeds to be added to a child GameObject to counter-rotate an axis aligned Bounding Box onto it.\nThis is only used by this component.")]
|
||||
public BoxCollider boundsCollider;
|
||||
|
||||
[Header("History")]
|
||||
[Tooltip("Keep this many past bounds in the buffer. The larger this is, the further we can raycast into the past.\nMaximum time := historyAmount * captureInterval")]
|
||||
public int boundsLimit = 8;
|
||||
|
||||
[Tooltip("Gather N bounds at a time into a bucket for faster encapsulation. A factor of 2 will be twice as fast, etc.")]
|
||||
public int boundsPerBucket = 2;
|
||||
|
||||
[Tooltip("Capture bounds every 'captureInterval' seconds. Larger values will require fewer computations, but may not capture every small move.")]
|
||||
public float captureInterval = 0.100f; // 100 ms
|
||||
double lastCaptureTime = 0;
|
||||
|
||||
[Header("Debug")]
|
||||
public Color historyColor = new Color(1.0f, 0.5f, 0.0f, 1.0f);
|
||||
public Color currentColor = Color.red;
|
||||
|
||||
protected HistoryBounds history = null;
|
||||
|
||||
protected virtual void Awake()
|
||||
{
|
||||
history = new HistoryBounds(boundsLimit, boundsPerBucket);
|
||||
|
||||
// ensure colliders were set.
|
||||
// bounds collider should always be a trigger.
|
||||
if (actualCollider == null) Debug.LogError("HistoryCollider: actualCollider was not set.");
|
||||
if (boundsCollider == null) Debug.LogError("HistoryCollider: boundsCollider was not set.");
|
||||
if (boundsCollider.transform.parent != transform) Debug.LogError("HistoryCollider: boundsCollider must be a child of this GameObject.");
|
||||
if (!boundsCollider.isTrigger) Debug.LogError("HistoryCollider: boundsCollider must be a trigger.");
|
||||
}
|
||||
|
||||
// capturing and projecting onto colliders should use physics update
|
||||
protected virtual void FixedUpdate()
|
||||
{
|
||||
// capture current bounds every interval
|
||||
if (NetworkTime.localTime >= lastCaptureTime + captureInterval)
|
||||
{
|
||||
lastCaptureTime = NetworkTime.localTime;
|
||||
CaptureBounds();
|
||||
}
|
||||
|
||||
// project bounds onto helper collider
|
||||
ProjectBounds();
|
||||
}
|
||||
|
||||
protected virtual void CaptureBounds()
|
||||
{
|
||||
// grab current collider bounds
|
||||
// this is in world space coordinates, and axis aligned
|
||||
// TODO double check
|
||||
Bounds bounds = actualCollider.bounds;
|
||||
|
||||
// insert into history
|
||||
history.Insert(bounds);
|
||||
}
|
||||
|
||||
protected virtual void ProjectBounds()
|
||||
{
|
||||
// grab total collider encapsulating all of history
|
||||
Bounds total = history.total;
|
||||
|
||||
// don't assign empty bounds, this will throw a Unity warning
|
||||
if (history.boundsCount == 0) return;
|
||||
|
||||
// scale projection doesn't work yet.
|
||||
// for now, don't allow scale changes.
|
||||
if (transform.lossyScale != Vector3.one)
|
||||
{
|
||||
Debug.LogWarning($"HistoryCollider: {name}'s transform global scale must be (1,1,1).");
|
||||
return;
|
||||
}
|
||||
|
||||
// counter rotate the child collider against the gameobject's rotation.
|
||||
// we need this to always be axis aligned.
|
||||
boundsCollider.transform.localRotation = Quaternion.Inverse(transform.rotation);
|
||||
|
||||
// project world space bounds to collider's local space
|
||||
boundsCollider.center = boundsCollider.transform.InverseTransformPoint(total.center);
|
||||
boundsCollider.size = total.size; // TODO projection?
|
||||
}
|
||||
|
||||
// TODO runtime drawing for debugging?
|
||||
protected virtual void OnDrawGizmos()
|
||||
{
|
||||
// draw total bounds
|
||||
Gizmos.color = historyColor;
|
||||
Gizmos.DrawWireCube(history.total.center, history.total.size);
|
||||
|
||||
// draw current bounds
|
||||
Gizmos.color = currentColor;
|
||||
Gizmos.DrawWireCube(actualCollider.bounds.center, actualCollider.bounds.size);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Applies HistoryBounds to the physics world by projecting to a trigger Collider.
|
||||
// This way we can use Physics.Raycast on it.
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
[DisallowMultipleComponent]
|
||||
[AddComponentMenu("Network/ Lag Compensation/ History Collider")]
|
||||
public class HistoryCollider : MonoBehaviour
|
||||
{
|
||||
[Header("Components")]
|
||||
[Tooltip("The object's actual collider. We need to know where it is, and how large it is.")]
|
||||
public Collider actualCollider;
|
||||
|
||||
[Tooltip("The helper collider that the history bounds are projected onto.\nNeeds to be added to a child GameObject to counter-rotate an axis aligned Bounding Box onto it.\nThis is only used by this component.")]
|
||||
public BoxCollider boundsCollider;
|
||||
|
||||
[Header("History")]
|
||||
[Tooltip("Keep this many past bounds in the buffer. The larger this is, the further we can raycast into the past.\nMaximum time := historyAmount * captureInterval")]
|
||||
public int boundsLimit = 8;
|
||||
|
||||
[Tooltip("Gather N bounds at a time into a bucket for faster encapsulation. A factor of 2 will be twice as fast, etc.")]
|
||||
public int boundsPerBucket = 2;
|
||||
|
||||
[Tooltip("Capture bounds every 'captureInterval' seconds. Larger values will require fewer computations, but may not capture every small move.")]
|
||||
public float captureInterval = 0.100f; // 100 ms
|
||||
double lastCaptureTime = 0;
|
||||
|
||||
[Header("Debug")]
|
||||
public Color historyColor = new Color(1.0f, 0.5f, 0.0f, 1.0f);
|
||||
public Color currentColor = Color.red;
|
||||
|
||||
protected HistoryBounds history = null;
|
||||
|
||||
protected virtual void Awake()
|
||||
{
|
||||
history = new HistoryBounds(boundsLimit, boundsPerBucket);
|
||||
|
||||
// ensure colliders were set.
|
||||
// bounds collider should always be a trigger.
|
||||
if (actualCollider == null) Debug.LogError("HistoryCollider: actualCollider was not set.");
|
||||
if (boundsCollider == null) Debug.LogError("HistoryCollider: boundsCollider was not set.");
|
||||
if (boundsCollider.transform.parent != transform) Debug.LogError("HistoryCollider: boundsCollider must be a child of this GameObject.");
|
||||
if (!boundsCollider.isTrigger) Debug.LogError("HistoryCollider: boundsCollider must be a trigger.");
|
||||
}
|
||||
|
||||
// capturing and projecting onto colliders should use physics update
|
||||
protected virtual void FixedUpdate()
|
||||
{
|
||||
// capture current bounds every interval
|
||||
if (NetworkTime.localTime >= lastCaptureTime + captureInterval)
|
||||
{
|
||||
lastCaptureTime = NetworkTime.localTime;
|
||||
CaptureBounds();
|
||||
}
|
||||
|
||||
// project bounds onto helper collider
|
||||
ProjectBounds();
|
||||
}
|
||||
|
||||
protected virtual void CaptureBounds()
|
||||
{
|
||||
// grab current collider bounds
|
||||
// this is in world space coordinates, and axis aligned
|
||||
// TODO double check
|
||||
Bounds bounds = actualCollider.bounds;
|
||||
|
||||
// insert into history
|
||||
history.Insert(bounds);
|
||||
}
|
||||
|
||||
protected virtual void ProjectBounds()
|
||||
{
|
||||
// grab total collider encapsulating all of history
|
||||
Bounds total = history.total;
|
||||
|
||||
// don't assign empty bounds, this will throw a Unity warning
|
||||
if (history.boundsCount == 0) return;
|
||||
|
||||
// scale projection doesn't work yet.
|
||||
// for now, don't allow scale changes.
|
||||
if (transform.lossyScale != Vector3.one)
|
||||
{
|
||||
Debug.LogWarning($"HistoryCollider: {name}'s transform global scale must be (1,1,1).");
|
||||
return;
|
||||
}
|
||||
|
||||
// counter rotate the child collider against the gameobject's rotation.
|
||||
// we need this to always be axis aligned.
|
||||
boundsCollider.transform.localRotation = Quaternion.Inverse(transform.rotation);
|
||||
|
||||
// project world space bounds to collider's local space
|
||||
boundsCollider.center = boundsCollider.transform.InverseTransformPoint(total.center);
|
||||
boundsCollider.size = total.size; // TODO projection?
|
||||
}
|
||||
|
||||
// TODO runtime drawing for debugging?
|
||||
protected virtual void OnDrawGizmos()
|
||||
{
|
||||
// draw total bounds
|
||||
Gizmos.color = historyColor;
|
||||
Gizmos.DrawWireCube(history.total.center, history.total.size);
|
||||
|
||||
// draw current bounds
|
||||
Gizmos.color = currentColor;
|
||||
Gizmos.DrawWireCube(actualCollider.bounds.center, actualCollider.bounds.size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f5f2158d9776d4b569858f793be4da60
|
||||
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/LagCompensation/HistoryCollider.cs
|
||||
uploadId: 736421
|
||||
fileFormatVersion: 2
|
||||
guid: f5f2158d9776d4b569858f793be4da60
|
||||
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/LagCompensation/HistoryCollider.cs
|
||||
uploadId: 736421
|
||||
|
||||
@@ -1,197 +1,197 @@
|
||||
// Add this component to a Player object with collider.
|
||||
// Automatically keeps a history for lag compensation.
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
public struct Capture3D : Capture
|
||||
{
|
||||
public double timestamp { get; set; }
|
||||
public Vector3 position;
|
||||
public Vector3 size;
|
||||
|
||||
public Capture3D(double timestamp, Vector3 position, Vector3 size)
|
||||
{
|
||||
this.timestamp = timestamp;
|
||||
this.position = position;
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
public void DrawGizmo()
|
||||
{
|
||||
Gizmos.DrawWireCube(position, size);
|
||||
}
|
||||
|
||||
public static Capture3D Interpolate(Capture3D from, Capture3D to, double t) =>
|
||||
new Capture3D(
|
||||
0, // interpolated snapshot is applied directly. don't need timestamps.
|
||||
Vector3.LerpUnclamped(from.position, to.position, (float)t),
|
||||
Vector3.LerpUnclamped(from.size, to.size, (float)t)
|
||||
);
|
||||
|
||||
public override string ToString() => $"(time={timestamp} pos={position} size={size})";
|
||||
}
|
||||
|
||||
[DisallowMultipleComponent]
|
||||
[AddComponentMenu("Network/ Lag Compensation/ Lag Compensator")]
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/manual/general/lag-compensation")]
|
||||
public class LagCompensator : NetworkBehaviour
|
||||
{
|
||||
[Header("Components")]
|
||||
[Tooltip("The collider to keep a history of.")]
|
||||
public Collider trackedCollider; // assign this in inspector
|
||||
|
||||
[Header("Settings")]
|
||||
public LagCompensationSettings lagCompensationSettings = new LagCompensationSettings();
|
||||
double lastCaptureTime;
|
||||
|
||||
// lag compensation history of <timestamp, capture>
|
||||
readonly Queue<KeyValuePair<double, Capture3D>> history = new Queue<KeyValuePair<double, Capture3D>>();
|
||||
|
||||
[Header("Debugging")]
|
||||
public Color historyColor = Color.white;
|
||||
|
||||
[ServerCallback]
|
||||
protected virtual void Update()
|
||||
{
|
||||
// capture lag compensation snapshots every interval.
|
||||
// NetworkTime.localTime because Unity 2019 doesn't have 'double' time yet.
|
||||
if (NetworkTime.localTime >= lastCaptureTime + lagCompensationSettings.captureInterval)
|
||||
{
|
||||
lastCaptureTime = NetworkTime.localTime;
|
||||
Capture();
|
||||
}
|
||||
}
|
||||
|
||||
[ServerCallback]
|
||||
protected virtual void Capture()
|
||||
{
|
||||
// capture current state
|
||||
Capture3D capture = new Capture3D(
|
||||
NetworkTime.localTime,
|
||||
trackedCollider.bounds.center,
|
||||
trackedCollider.bounds.size
|
||||
);
|
||||
|
||||
// insert into history
|
||||
LagCompensation.Insert(history, lagCompensationSettings.historyLimit, NetworkTime.localTime, capture);
|
||||
}
|
||||
|
||||
protected virtual void OnDrawGizmos()
|
||||
{
|
||||
// draw history
|
||||
Gizmos.color = historyColor;
|
||||
LagCompensation.DrawGizmos(history);
|
||||
}
|
||||
|
||||
// sampling ////////////////////////////////////////////////////////////
|
||||
// sample the sub-tick (=interpolated) history of this object for a hit test.
|
||||
// 'viewer' needs to be the player who fired!
|
||||
// for example, if A fires at B, then call B.Sample(viewer, point, tolerance).
|
||||
[ServerCallback]
|
||||
public virtual bool Sample(NetworkConnectionToClient viewer, out Capture3D sample)
|
||||
{
|
||||
// never trust the client: estimate client time instead.
|
||||
// https://developer.valvesoftware.com/wiki/Source_Multiplayer_Networking
|
||||
// the estimation is very good. the error is as low as ~6ms for the demo.
|
||||
// note that passing 'rtt' is fine: EstimateTime halves it to latency.
|
||||
double estimatedTime = LagCompensation.EstimateTime(NetworkTime.localTime, viewer.rtt, NetworkClient.bufferTime);
|
||||
|
||||
// sample the history to get the nearest snapshots around 'timestamp'
|
||||
if (LagCompensation.Sample(history, estimatedTime, lagCompensationSettings.captureInterval, out Capture3D resultBefore, out Capture3D resultAfter, out double t))
|
||||
{
|
||||
// interpolate to get a decent estimation at exactly 'timestamp'
|
||||
sample = Capture3D.Interpolate(resultBefore, resultAfter, t);
|
||||
return true;
|
||||
}
|
||||
else Debug.Log($"CmdClicked: history doesn't contain {estimatedTime:F3}");
|
||||
|
||||
sample = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
// convenience tests ///////////////////////////////////////////////////
|
||||
// there are multiple different ways to check a hit against the sample:
|
||||
// - raycasting
|
||||
// - bounds.contains
|
||||
// - increasing bounds by tolerance and checking contains
|
||||
// - threshold to bounds.closestpoint
|
||||
// let's offer a few solutions directly and see which users prefer.
|
||||
|
||||
// bounds check: checks distance to closest point on bounds in history @ -rtt.
|
||||
// 'viewer' needs to be the player who fired!
|
||||
// for example, if A fires at B, then call B.Sample(viewer, point, tolerance).
|
||||
// this is super simple and fast, but not 100% physically accurate since we don't raycast.
|
||||
[ServerCallback]
|
||||
public virtual bool BoundsCheck(
|
||||
NetworkConnectionToClient viewer,
|
||||
Vector3 hitPoint,
|
||||
float toleranceDistance,
|
||||
out float distance,
|
||||
out Vector3 nearest)
|
||||
{
|
||||
// first, sample the history at -rtt of the viewer.
|
||||
if (Sample(viewer, out Capture3D capture))
|
||||
{
|
||||
// now that we know where the other player was at that time,
|
||||
// we can see if the hit point was within tolerance of it.
|
||||
// TODO consider rotations???
|
||||
// TODO consider original collider shape??
|
||||
Bounds bounds = new Bounds(capture.position, capture.size);
|
||||
nearest = bounds.ClosestPoint(hitPoint);
|
||||
distance = Vector3.Distance(nearest, hitPoint);
|
||||
return distance <= toleranceDistance;
|
||||
}
|
||||
nearest = hitPoint;
|
||||
distance = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
// raycast check: creates a collider the sampled position and raycasts to hitPoint.
|
||||
// 'viewer' needs to be the player who fired!
|
||||
// for example, if A fires at B, then call B.Sample(viewer, point, tolerance).
|
||||
// this is physically accurate (checks against walls etc.), with the cost
|
||||
// of a runtime instantiation.
|
||||
//
|
||||
// originPoint: where the player fired the weapon.
|
||||
// hitPoint: where the player's local raycast hit.
|
||||
// tolerance: scale up the sampled collider by % in order to have a bit of a tolerance.
|
||||
// 0 means no extra tolerance, 0.05 means 5% extra tolerance.
|
||||
// layerMask: the layer mask to use for the raycast.
|
||||
[ServerCallback]
|
||||
public virtual bool RaycastCheck(
|
||||
NetworkConnectionToClient viewer,
|
||||
Vector3 originPoint,
|
||||
Vector3 hitPoint,
|
||||
float tolerancePercent,
|
||||
int layerMask,
|
||||
out RaycastHit hit)
|
||||
{
|
||||
// first, sample the history at -rtt of the viewer.
|
||||
if (Sample(viewer, out Capture3D capture))
|
||||
{
|
||||
// instantiate a real physics collider on demand.
|
||||
// TODO rotation??
|
||||
// TODO different collier types??
|
||||
GameObject temp = new GameObject("LagCompensatorTest");
|
||||
temp.transform.position = capture.position;
|
||||
BoxCollider tempCollider = temp.AddComponent<BoxCollider>();
|
||||
tempCollider.size = capture.size * (1 + tolerancePercent);
|
||||
|
||||
// raycast
|
||||
Vector3 direction = hitPoint - originPoint;
|
||||
float maxDistance = direction.magnitude * 2;
|
||||
bool result = Physics.Raycast(originPoint, direction, out hit, maxDistance, layerMask);
|
||||
|
||||
// cleanup
|
||||
Destroy(temp);
|
||||
return result;
|
||||
}
|
||||
|
||||
hit = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Add this component to a Player object with collider.
|
||||
// Automatically keeps a history for lag compensation.
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
public struct Capture3D : Capture
|
||||
{
|
||||
public double timestamp { get; set; }
|
||||
public Vector3 position;
|
||||
public Vector3 size;
|
||||
|
||||
public Capture3D(double timestamp, Vector3 position, Vector3 size)
|
||||
{
|
||||
this.timestamp = timestamp;
|
||||
this.position = position;
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
public void DrawGizmo()
|
||||
{
|
||||
Gizmos.DrawWireCube(position, size);
|
||||
}
|
||||
|
||||
public static Capture3D Interpolate(Capture3D from, Capture3D to, double t) =>
|
||||
new Capture3D(
|
||||
0, // interpolated snapshot is applied directly. don't need timestamps.
|
||||
Vector3.LerpUnclamped(from.position, to.position, (float)t),
|
||||
Vector3.LerpUnclamped(from.size, to.size, (float)t)
|
||||
);
|
||||
|
||||
public override string ToString() => $"(time={timestamp} pos={position} size={size})";
|
||||
}
|
||||
|
||||
[DisallowMultipleComponent]
|
||||
[AddComponentMenu("Network/ Lag Compensation/ Lag Compensator")]
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/manual/general/lag-compensation")]
|
||||
public class LagCompensator : NetworkBehaviour
|
||||
{
|
||||
[Header("Components")]
|
||||
[Tooltip("The collider to keep a history of.")]
|
||||
public Collider trackedCollider; // assign this in inspector
|
||||
|
||||
[Header("Settings")]
|
||||
public LagCompensationSettings lagCompensationSettings = new LagCompensationSettings();
|
||||
double lastCaptureTime;
|
||||
|
||||
// lag compensation history of <timestamp, capture>
|
||||
readonly Queue<KeyValuePair<double, Capture3D>> history = new Queue<KeyValuePair<double, Capture3D>>();
|
||||
|
||||
[Header("Debugging")]
|
||||
public Color historyColor = Color.white;
|
||||
|
||||
[ServerCallback]
|
||||
protected virtual void Update()
|
||||
{
|
||||
// capture lag compensation snapshots every interval.
|
||||
// NetworkTime.localTime because Unity 2019 doesn't have 'double' time yet.
|
||||
if (NetworkTime.localTime >= lastCaptureTime + lagCompensationSettings.captureInterval)
|
||||
{
|
||||
lastCaptureTime = NetworkTime.localTime;
|
||||
Capture();
|
||||
}
|
||||
}
|
||||
|
||||
[ServerCallback]
|
||||
protected virtual void Capture()
|
||||
{
|
||||
// capture current state
|
||||
Capture3D capture = new Capture3D(
|
||||
NetworkTime.localTime,
|
||||
trackedCollider.bounds.center,
|
||||
trackedCollider.bounds.size
|
||||
);
|
||||
|
||||
// insert into history
|
||||
LagCompensation.Insert(history, lagCompensationSettings.historyLimit, NetworkTime.localTime, capture);
|
||||
}
|
||||
|
||||
protected virtual void OnDrawGizmos()
|
||||
{
|
||||
// draw history
|
||||
Gizmos.color = historyColor;
|
||||
LagCompensation.DrawGizmos(history);
|
||||
}
|
||||
|
||||
// sampling ////////////////////////////////////////////////////////////
|
||||
// sample the sub-tick (=interpolated) history of this object for a hit test.
|
||||
// 'viewer' needs to be the player who fired!
|
||||
// for example, if A fires at B, then call B.Sample(viewer, point, tolerance).
|
||||
[ServerCallback]
|
||||
public virtual bool Sample(NetworkConnectionToClient viewer, out Capture3D sample)
|
||||
{
|
||||
// never trust the client: estimate client time instead.
|
||||
// https://developer.valvesoftware.com/wiki/Source_Multiplayer_Networking
|
||||
// the estimation is very good. the error is as low as ~6ms for the demo.
|
||||
// note that passing 'rtt' is fine: EstimateTime halves it to latency.
|
||||
double estimatedTime = LagCompensation.EstimateTime(NetworkTime.localTime, viewer.rtt, NetworkClient.bufferTime);
|
||||
|
||||
// sample the history to get the nearest snapshots around 'timestamp'
|
||||
if (LagCompensation.Sample(history, estimatedTime, lagCompensationSettings.captureInterval, out Capture3D resultBefore, out Capture3D resultAfter, out double t))
|
||||
{
|
||||
// interpolate to get a decent estimation at exactly 'timestamp'
|
||||
sample = Capture3D.Interpolate(resultBefore, resultAfter, t);
|
||||
return true;
|
||||
}
|
||||
else Debug.Log($"CmdClicked: history doesn't contain {estimatedTime:F3}");
|
||||
|
||||
sample = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
// convenience tests ///////////////////////////////////////////////////
|
||||
// there are multiple different ways to check a hit against the sample:
|
||||
// - raycasting
|
||||
// - bounds.contains
|
||||
// - increasing bounds by tolerance and checking contains
|
||||
// - threshold to bounds.closestpoint
|
||||
// let's offer a few solutions directly and see which users prefer.
|
||||
|
||||
// bounds check: checks distance to closest point on bounds in history @ -rtt.
|
||||
// 'viewer' needs to be the player who fired!
|
||||
// for example, if A fires at B, then call B.Sample(viewer, point, tolerance).
|
||||
// this is super simple and fast, but not 100% physically accurate since we don't raycast.
|
||||
[ServerCallback]
|
||||
public virtual bool BoundsCheck(
|
||||
NetworkConnectionToClient viewer,
|
||||
Vector3 hitPoint,
|
||||
float toleranceDistance,
|
||||
out float distance,
|
||||
out Vector3 nearest)
|
||||
{
|
||||
// first, sample the history at -rtt of the viewer.
|
||||
if (Sample(viewer, out Capture3D capture))
|
||||
{
|
||||
// now that we know where the other player was at that time,
|
||||
// we can see if the hit point was within tolerance of it.
|
||||
// TODO consider rotations???
|
||||
// TODO consider original collider shape??
|
||||
Bounds bounds = new Bounds(capture.position, capture.size);
|
||||
nearest = bounds.ClosestPoint(hitPoint);
|
||||
distance = Vector3.Distance(nearest, hitPoint);
|
||||
return distance <= toleranceDistance;
|
||||
}
|
||||
nearest = hitPoint;
|
||||
distance = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
// raycast check: creates a collider the sampled position and raycasts to hitPoint.
|
||||
// 'viewer' needs to be the player who fired!
|
||||
// for example, if A fires at B, then call B.Sample(viewer, point, tolerance).
|
||||
// this is physically accurate (checks against walls etc.), with the cost
|
||||
// of a runtime instantiation.
|
||||
//
|
||||
// originPoint: where the player fired the weapon.
|
||||
// hitPoint: where the player's local raycast hit.
|
||||
// tolerance: scale up the sampled collider by % in order to have a bit of a tolerance.
|
||||
// 0 means no extra tolerance, 0.05 means 5% extra tolerance.
|
||||
// layerMask: the layer mask to use for the raycast.
|
||||
[ServerCallback]
|
||||
public virtual bool RaycastCheck(
|
||||
NetworkConnectionToClient viewer,
|
||||
Vector3 originPoint,
|
||||
Vector3 hitPoint,
|
||||
float tolerancePercent,
|
||||
int layerMask,
|
||||
out RaycastHit hit)
|
||||
{
|
||||
// first, sample the history at -rtt of the viewer.
|
||||
if (Sample(viewer, out Capture3D capture))
|
||||
{
|
||||
// instantiate a real physics collider on demand.
|
||||
// TODO rotation??
|
||||
// TODO different collier types??
|
||||
GameObject temp = new GameObject("LagCompensatorTest");
|
||||
temp.transform.position = capture.position;
|
||||
BoxCollider tempCollider = temp.AddComponent<BoxCollider>();
|
||||
tempCollider.size = capture.size * (1 + tolerancePercent);
|
||||
|
||||
// raycast
|
||||
Vector3 direction = hitPoint - originPoint;
|
||||
float maxDistance = direction.magnitude * 2;
|
||||
bool result = Physics.Raycast(originPoint, direction, out hit, maxDistance, layerMask);
|
||||
|
||||
// cleanup
|
||||
Destroy(temp);
|
||||
return result;
|
||||
}
|
||||
|
||||
hit = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a898831dd60c4cdfbfd9a6ea5702ed01
|
||||
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/LagCompensation/LagCompensator.cs
|
||||
uploadId: 736421
|
||||
fileFormatVersion: 2
|
||||
guid: a898831dd60c4cdfbfd9a6ea5702ed01
|
||||
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/LagCompensation/LagCompensator.cs
|
||||
uploadId: 736421
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"name": "Mirror.Components",
|
||||
"rootNamespace": "",
|
||||
"references": [
|
||||
"GUID:30817c1a0e6d646d99c048fc403f5979"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": true,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
{
|
||||
"name": "Mirror.Components",
|
||||
"rootNamespace": "",
|
||||
"references": [
|
||||
"GUID:30817c1a0e6d646d99c048fc403f5979"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": true,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 72872094b21c16e48b631b2224833d49
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Components/Mirror.Components.asmdef
|
||||
uploadId: 736421
|
||||
fileFormatVersion: 2
|
||||
guid: 72872094b21c16e48b631b2224833d49
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Components/Mirror.Components.asmdef
|
||||
uploadId: 736421
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,18 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7f6f3bf89aa97405989c802ba270f815
|
||||
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/NetworkAnimator.cs
|
||||
uploadId: 736421
|
||||
fileFormatVersion: 2
|
||||
guid: 7f6f3bf89aa97405989c802ba270f815
|
||||
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/NetworkAnimator.cs
|
||||
uploadId: 736421
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
[AddComponentMenu("Network/Network Diagnostics Debugger")]
|
||||
public class NetworkDiagnosticsDebugger : MonoBehaviour
|
||||
{
|
||||
public bool logInMessages = true;
|
||||
public bool logOutMessages = true;
|
||||
void OnInMessage(NetworkDiagnostics.MessageInfo msgInfo)
|
||||
{
|
||||
if (logInMessages)
|
||||
Debug.Log(msgInfo);
|
||||
}
|
||||
void OnOutMessage(NetworkDiagnostics.MessageInfo msgInfo)
|
||||
{
|
||||
if (logOutMessages)
|
||||
Debug.Log(msgInfo);
|
||||
}
|
||||
void OnEnable()
|
||||
{
|
||||
NetworkDiagnostics.InMessageEvent += OnInMessage;
|
||||
NetworkDiagnostics.OutMessageEvent += OnOutMessage;
|
||||
}
|
||||
void OnDisable()
|
||||
{
|
||||
NetworkDiagnostics.InMessageEvent -= OnInMessage;
|
||||
NetworkDiagnostics.OutMessageEvent -= OnOutMessage;
|
||||
}
|
||||
}
|
||||
}
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
[AddComponentMenu("Network/Network Diagnostics Debugger")]
|
||||
public class NetworkDiagnosticsDebugger : MonoBehaviour
|
||||
{
|
||||
public bool logInMessages = true;
|
||||
public bool logOutMessages = true;
|
||||
void OnInMessage(NetworkDiagnostics.MessageInfo msgInfo)
|
||||
{
|
||||
if (logInMessages)
|
||||
Debug.Log(msgInfo);
|
||||
}
|
||||
void OnOutMessage(NetworkDiagnostics.MessageInfo msgInfo)
|
||||
{
|
||||
if (logOutMessages)
|
||||
Debug.Log(msgInfo);
|
||||
}
|
||||
void OnEnable()
|
||||
{
|
||||
NetworkDiagnostics.InMessageEvent += OnInMessage;
|
||||
NetworkDiagnostics.OutMessageEvent += OnOutMessage;
|
||||
}
|
||||
void OnDisable()
|
||||
{
|
||||
NetworkDiagnostics.InMessageEvent -= OnInMessage;
|
||||
NetworkDiagnostics.OutMessageEvent -= OnOutMessage;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bc9f0a0fe4124424b8f9d4927795ee01
|
||||
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/NetworkDiagnosticsDebugger.cs
|
||||
uploadId: 736421
|
||||
fileFormatVersion: 2
|
||||
guid: bc9f0a0fe4124424b8f9d4927795ee01
|
||||
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/NetworkDiagnosticsDebugger.cs
|
||||
uploadId: 736421
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
/// <summary>
|
||||
/// This is a specialized NetworkManager that includes a networked lobby.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>The lobby has slots that track the joined players, and a maximum player count that is enforced. It requires that the NetworkLobbyPlayer component be on the lobby player objects.</para>
|
||||
/// <para>NetworkLobbyManager is derived from NetworkManager, and so it implements many of the virtual functions provided by the NetworkManager class. To avoid accidentally replacing functionality of the NetworkLobbyManager, there are new virtual functions on the NetworkLobbyManager that begin with "OnLobby". These should be used on classes derived from NetworkLobbyManager instead of the virtual functions on NetworkManager.</para>
|
||||
/// <para>The OnLobby*() functions have empty implementations on the NetworkLobbyManager base class, so the base class functions do not have to be called.</para>
|
||||
/// </remarks>
|
||||
[AddComponentMenu("Network/Network Lobby Manager")]
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-room-manager")]
|
||||
[Obsolete("Use / inherit from NetworkRoomManager instead")]
|
||||
public class NetworkLobbyManager : NetworkRoomManager {}
|
||||
}
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
/// <summary>
|
||||
/// This is a specialized NetworkManager that includes a networked lobby.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>The lobby has slots that track the joined players, and a maximum player count that is enforced. It requires that the NetworkLobbyPlayer component be on the lobby player objects.</para>
|
||||
/// <para>NetworkLobbyManager is derived from NetworkManager, and so it implements many of the virtual functions provided by the NetworkManager class. To avoid accidentally replacing functionality of the NetworkLobbyManager, there are new virtual functions on the NetworkLobbyManager that begin with "OnLobby". These should be used on classes derived from NetworkLobbyManager instead of the virtual functions on NetworkManager.</para>
|
||||
/// <para>The OnLobby*() functions have empty implementations on the NetworkLobbyManager base class, so the base class functions do not have to be called.</para>
|
||||
/// </remarks>
|
||||
[AddComponentMenu("Network/Network Lobby Manager")]
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-room-manager")]
|
||||
[Obsolete("Use / inherit from NetworkRoomManager instead")]
|
||||
public class NetworkLobbyManager : NetworkRoomManager {}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a4c96e6dd99826849ab1431f94547141
|
||||
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/NetworkLobbyManager.cs
|
||||
uploadId: 736421
|
||||
fileFormatVersion: 2
|
||||
guid: a4c96e6dd99826849ab1431f94547141
|
||||
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/NetworkLobbyManager.cs
|
||||
uploadId: 736421
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
/// <summary>
|
||||
/// This component works in conjunction with the NetworkLobbyManager to make up the multiplayer lobby system.
|
||||
/// <para>The LobbyPrefab object of the NetworkLobbyManager must have this component on it. This component holds basic lobby player data required for the lobby to function. Game specific data for lobby players can be put in other components on the LobbyPrefab or in scripts derived from NetworkLobbyPlayer.</para>
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
[AddComponentMenu("Network/Network Lobby Player")]
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-room-player")]
|
||||
[Obsolete("Use / inherit from NetworkRoomPlayer instead")]
|
||||
public class NetworkLobbyPlayer : NetworkRoomPlayer {}
|
||||
}
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
/// <summary>
|
||||
/// This component works in conjunction with the NetworkLobbyManager to make up the multiplayer lobby system.
|
||||
/// <para>The LobbyPrefab object of the NetworkLobbyManager must have this component on it. This component holds basic lobby player data required for the lobby to function. Game specific data for lobby players can be put in other components on the LobbyPrefab or in scripts derived from NetworkLobbyPlayer.</para>
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
[AddComponentMenu("Network/Network Lobby Player")]
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-room-player")]
|
||||
[Obsolete("Use / inherit from NetworkRoomPlayer instead")]
|
||||
public class NetworkLobbyPlayer : NetworkRoomPlayer {}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 777a368af85f2e84da7ea5666581921b
|
||||
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/NetworkLobbyPlayer.cs
|
||||
uploadId: 736421
|
||||
fileFormatVersion: 2
|
||||
guid: 777a368af85f2e84da7ea5666581921b
|
||||
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/NetworkLobbyPlayer.cs
|
||||
uploadId: 736421
|
||||
|
||||
@@ -1,39 +1,39 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
/// <summary>
|
||||
/// Component that will display the clients ping in milliseconds
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
[AddComponentMenu("Network/Network Ping Display")]
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-ping-display")]
|
||||
public class NetworkPingDisplay : MonoBehaviour
|
||||
{
|
||||
public Color color = Color.white;
|
||||
public int padding = 2;
|
||||
public int width = 150;
|
||||
public int height = 25;
|
||||
|
||||
void OnGUI()
|
||||
{
|
||||
// only while client is active
|
||||
if (!NetworkClient.active) return;
|
||||
|
||||
// show stats in bottom right corner, right aligned
|
||||
GUI.color = color;
|
||||
Rect rect = new Rect(Screen.width - width - padding, Screen.height - height - padding, width, height);
|
||||
GUILayout.BeginArea(rect);
|
||||
GUIStyle style = GUI.skin.GetStyle("Label");
|
||||
style.alignment = TextAnchor.MiddleRight;
|
||||
GUILayout.BeginHorizontal(style);
|
||||
GUILayout.Label($"RTT: {Math.Round(NetworkTime.rtt * 1000)}ms");
|
||||
GUI.color = NetworkClient.connectionQuality.ColorCode();
|
||||
GUILayout.Label($"Q: {new string('-', (int)NetworkClient.connectionQuality)}");
|
||||
GUILayout.EndHorizontal();
|
||||
GUILayout.EndArea();
|
||||
GUI.color = Color.white;
|
||||
}
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
/// <summary>
|
||||
/// Component that will display the clients ping in milliseconds
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
[AddComponentMenu("Network/Network Ping Display")]
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-ping-display")]
|
||||
public class NetworkPingDisplay : MonoBehaviour
|
||||
{
|
||||
public Color color = Color.white;
|
||||
public int padding = 2;
|
||||
public int width = 150;
|
||||
public int height = 25;
|
||||
|
||||
void OnGUI()
|
||||
{
|
||||
// only while client is active
|
||||
if (!NetworkClient.active) return;
|
||||
|
||||
// show stats in bottom right corner, right aligned
|
||||
GUI.color = color;
|
||||
Rect rect = new Rect(Screen.width - width - padding, Screen.height - height - padding, width, height);
|
||||
GUILayout.BeginArea(rect);
|
||||
GUIStyle style = GUI.skin.GetStyle("Label");
|
||||
style.alignment = TextAnchor.MiddleRight;
|
||||
GUILayout.BeginHorizontal(style);
|
||||
GUILayout.Label($"RTT: {Math.Round(NetworkTime.rtt * 1000)}ms");
|
||||
GUI.color = NetworkClient.connectionQuality.ColorCode();
|
||||
GUILayout.Label($"Q: {new string('-', (int)NetworkClient.connectionQuality)}");
|
||||
GUILayout.EndHorizontal();
|
||||
GUILayout.EndArea();
|
||||
GUI.color = Color.white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bc654f29862fc2643b948f772ebb9e68
|
||||
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/NetworkPingDisplay.cs
|
||||
uploadId: 736421
|
||||
fileFormatVersion: 2
|
||||
guid: bc654f29862fc2643b948f772ebb9e68
|
||||
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/NetworkPingDisplay.cs
|
||||
uploadId: 736421
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 80106690aef541a5b8e2f8fb3d5949ad
|
||||
timeCreated: 1686733778
|
||||
fileFormatVersion: 2
|
||||
guid: 80106690aef541a5b8e2f8fb3d5949ad
|
||||
timeCreated: 1686733778
|
||||
|
||||
@@ -1,115 +1,115 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
// [RequireComponent(typeof(Rigidbody))] <- OnValidate ensures this is on .target
|
||||
[AddComponentMenu("Network/Network Rigidbody (Reliable)")]
|
||||
public class NetworkRigidbodyReliable : NetworkTransformReliable
|
||||
{
|
||||
bool clientAuthority => syncDirection == SyncDirection.ClientToServer;
|
||||
|
||||
Rigidbody rb;
|
||||
bool wasKinematic;
|
||||
|
||||
protected override void OnValidate()
|
||||
{
|
||||
// Skip if Editor is in Play mode
|
||||
if (Application.isPlaying) return;
|
||||
|
||||
base.OnValidate();
|
||||
|
||||
// we can't overwrite .target to be a Rigidbody.
|
||||
// but we can ensure that .target has a Rigidbody, and use it.
|
||||
if (target.GetComponent<Rigidbody>() == null)
|
||||
{
|
||||
Debug.LogWarning($"{name}'s NetworkRigidbody.target {target.name} is missing a Rigidbody", this);
|
||||
}
|
||||
}
|
||||
|
||||
// cach Rigidbody and original isKinematic setting
|
||||
protected override void Awake()
|
||||
{
|
||||
// we can't overwrite .target to be a Rigidbody.
|
||||
// but we can use its Rigidbody component.
|
||||
rb = target.GetComponent<Rigidbody>();
|
||||
if (rb == null)
|
||||
{
|
||||
Debug.LogError($"{name}'s NetworkRigidbody.target {target.name} is missing a Rigidbody", this);
|
||||
return;
|
||||
}
|
||||
wasKinematic = rb.isKinematic;
|
||||
base.Awake();
|
||||
}
|
||||
|
||||
// reset forced isKinematic flag to original.
|
||||
// otherwise the overwritten value would remain between sessions forever.
|
||||
// for example, a game may run as client, set rigidbody.iskinematic=true,
|
||||
// then run as server, where .iskinematic isn't touched and remains at
|
||||
// the overwritten=true, even though the user set it to false originally.
|
||||
public override void OnStopServer() => rb.isKinematic = wasKinematic;
|
||||
public override void OnStopClient() => rb.isKinematic = wasKinematic;
|
||||
|
||||
// overwriting Construct() and Apply() to set Rigidbody.MovePosition
|
||||
// would give more jittery movement.
|
||||
|
||||
// FixedUpdate for physics
|
||||
void FixedUpdate()
|
||||
{
|
||||
// who ever has authority moves the Rigidbody with physics.
|
||||
// everyone else simply sets it to kinematic.
|
||||
// so that only the Transform component is synced.
|
||||
|
||||
// host mode
|
||||
if (isServer && isClient)
|
||||
{
|
||||
// in host mode, we own it it if:
|
||||
// clientAuthority is disabled (hence server / we own it)
|
||||
// clientAuthority is enabled and we have authority over this object.
|
||||
bool owned = !clientAuthority || IsClientWithAuthority;
|
||||
|
||||
// only set to kinematic if we don't own it
|
||||
// otherwise don't touch isKinematic.
|
||||
// the authority owner might use it either way.
|
||||
if (!owned) rb.isKinematic = true;
|
||||
}
|
||||
// client only
|
||||
else if (isClient)
|
||||
{
|
||||
// on the client, we own it only if clientAuthority is enabled,
|
||||
// and we have authority over this object.
|
||||
bool owned = IsClientWithAuthority;
|
||||
|
||||
// only set to kinematic if we don't own it
|
||||
// otherwise don't touch isKinematic.
|
||||
// the authority owner might use it either way.
|
||||
if (!owned) rb.isKinematic = true;
|
||||
}
|
||||
// server only
|
||||
else if (isServer)
|
||||
{
|
||||
// on the server, we always own it if clientAuthority is disabled.
|
||||
bool owned = !clientAuthority;
|
||||
|
||||
// only set to kinematic if we don't own it
|
||||
// otherwise don't touch isKinematic.
|
||||
// the authority owner might use it either way.
|
||||
if (!owned) rb.isKinematic = true;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnTeleport(Vector3 destination)
|
||||
{
|
||||
base.OnTeleport(destination);
|
||||
|
||||
rb.position = transform.position;
|
||||
}
|
||||
|
||||
protected override void OnTeleport(Vector3 destination, Quaternion rotation)
|
||||
{
|
||||
base.OnTeleport(destination, rotation);
|
||||
|
||||
rb.position = transform.position;
|
||||
rb.rotation = transform.rotation;
|
||||
}
|
||||
}
|
||||
}
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
// [RequireComponent(typeof(Rigidbody))] <- OnValidate ensures this is on .target
|
||||
[AddComponentMenu("Network/Network Rigidbody (Reliable)")]
|
||||
public class NetworkRigidbodyReliable : NetworkTransformReliable
|
||||
{
|
||||
bool clientAuthority => syncDirection == SyncDirection.ClientToServer;
|
||||
|
||||
Rigidbody rb;
|
||||
bool wasKinematic;
|
||||
|
||||
protected override void OnValidate()
|
||||
{
|
||||
// Skip if Editor is in Play mode
|
||||
if (Application.isPlaying) return;
|
||||
|
||||
base.OnValidate();
|
||||
|
||||
// we can't overwrite .target to be a Rigidbody.
|
||||
// but we can ensure that .target has a Rigidbody, and use it.
|
||||
if (target.GetComponent<Rigidbody>() == null)
|
||||
{
|
||||
Debug.LogWarning($"{name}'s NetworkRigidbody.target {target.name} is missing a Rigidbody", this);
|
||||
}
|
||||
}
|
||||
|
||||
// cach Rigidbody and original isKinematic setting
|
||||
protected override void Awake()
|
||||
{
|
||||
// we can't overwrite .target to be a Rigidbody.
|
||||
// but we can use its Rigidbody component.
|
||||
rb = target.GetComponent<Rigidbody>();
|
||||
if (rb == null)
|
||||
{
|
||||
Debug.LogError($"{name}'s NetworkRigidbody.target {target.name} is missing a Rigidbody", this);
|
||||
return;
|
||||
}
|
||||
wasKinematic = rb.isKinematic;
|
||||
base.Awake();
|
||||
}
|
||||
|
||||
// reset forced isKinematic flag to original.
|
||||
// otherwise the overwritten value would remain between sessions forever.
|
||||
// for example, a game may run as client, set rigidbody.iskinematic=true,
|
||||
// then run as server, where .iskinematic isn't touched and remains at
|
||||
// the overwritten=true, even though the user set it to false originally.
|
||||
public override void OnStopServer() => rb.isKinematic = wasKinematic;
|
||||
public override void OnStopClient() => rb.isKinematic = wasKinematic;
|
||||
|
||||
// overwriting Construct() and Apply() to set Rigidbody.MovePosition
|
||||
// would give more jittery movement.
|
||||
|
||||
// FixedUpdate for physics
|
||||
void FixedUpdate()
|
||||
{
|
||||
// who ever has authority moves the Rigidbody with physics.
|
||||
// everyone else simply sets it to kinematic.
|
||||
// so that only the Transform component is synced.
|
||||
|
||||
// host mode
|
||||
if (isServer && isClient)
|
||||
{
|
||||
// in host mode, we own it it if:
|
||||
// clientAuthority is disabled (hence server / we own it)
|
||||
// clientAuthority is enabled and we have authority over this object.
|
||||
bool owned = !clientAuthority || IsClientWithAuthority;
|
||||
|
||||
// only set to kinematic if we don't own it
|
||||
// otherwise don't touch isKinematic.
|
||||
// the authority owner might use it either way.
|
||||
if (!owned) rb.isKinematic = true;
|
||||
}
|
||||
// client only
|
||||
else if (isClient)
|
||||
{
|
||||
// on the client, we own it only if clientAuthority is enabled,
|
||||
// and we have authority over this object.
|
||||
bool owned = IsClientWithAuthority;
|
||||
|
||||
// only set to kinematic if we don't own it
|
||||
// otherwise don't touch isKinematic.
|
||||
// the authority owner might use it either way.
|
||||
if (!owned) rb.isKinematic = true;
|
||||
}
|
||||
// server only
|
||||
else if (isServer)
|
||||
{
|
||||
// on the server, we always own it if clientAuthority is disabled.
|
||||
bool owned = !clientAuthority;
|
||||
|
||||
// only set to kinematic if we don't own it
|
||||
// otherwise don't touch isKinematic.
|
||||
// the authority owner might use it either way.
|
||||
if (!owned) rb.isKinematic = true;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnTeleport(Vector3 destination)
|
||||
{
|
||||
base.OnTeleport(destination);
|
||||
|
||||
rb.position = transform.position;
|
||||
}
|
||||
|
||||
protected override void OnTeleport(Vector3 destination, Quaternion rotation)
|
||||
{
|
||||
base.OnTeleport(destination, rotation);
|
||||
|
||||
rb.position = transform.position;
|
||||
rb.rotation = transform.rotation;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cb803efbe62c34d7baece46c9ffebad9
|
||||
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/NetworkRigidbody/NetworkRigidbodyReliable.cs
|
||||
uploadId: 736421
|
||||
fileFormatVersion: 2
|
||||
guid: cb803efbe62c34d7baece46c9ffebad9
|
||||
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/NetworkRigidbody/NetworkRigidbodyReliable.cs
|
||||
uploadId: 736421
|
||||
|
||||
@@ -1,135 +1,135 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
// [RequireComponent(typeof(Rigidbody))] <- OnValidate ensures this is on .target
|
||||
[AddComponentMenu("Network/Network Rigidbody 2D (Reliable)")]
|
||||
public class NetworkRigidbodyReliable2D : NetworkTransformReliable
|
||||
{
|
||||
bool clientAuthority => syncDirection == SyncDirection.ClientToServer;
|
||||
|
||||
Rigidbody2D rb;
|
||||
bool wasKinematic;
|
||||
|
||||
protected override void OnValidate()
|
||||
{
|
||||
if (Application.isPlaying) return;
|
||||
|
||||
base.OnValidate();
|
||||
|
||||
// we can't overwrite .target to be a Rigidbody.
|
||||
// but we can ensure that .target has a Rigidbody, and use it.
|
||||
if (target.GetComponent<Rigidbody2D>() == null)
|
||||
{
|
||||
Debug.LogWarning($"{name}'s NetworkRigidbody2D.target {target.name} is missing a Rigidbody2D", this);
|
||||
}
|
||||
}
|
||||
|
||||
// cach Rigidbody and original isKinematic setting
|
||||
protected override void Awake()
|
||||
{
|
||||
// we can't overwrite .target to be a Rigidbody.
|
||||
// but we can use its Rigidbody component.
|
||||
rb = target.GetComponent<Rigidbody2D>();
|
||||
if (rb == null)
|
||||
{
|
||||
Debug.LogError($"{name}'s NetworkRigidbody2D.target {target.name} is missing a Rigidbody2D", this);
|
||||
return;
|
||||
}
|
||||
#if UNITY_6000_0_OR_NEWER
|
||||
wasKinematic = rb.bodyType.HasFlag(RigidbodyType2D.Kinematic);
|
||||
#else
|
||||
wasKinematic = rb.isKinematic;
|
||||
#endif
|
||||
base.Awake();
|
||||
}
|
||||
|
||||
// reset forced isKinematic flag to original.
|
||||
// otherwise the overwritten value would remain between sessions forever.
|
||||
// for example, a game may run as client, set rigidbody.iskinematic=true,
|
||||
// then run as server, where .iskinematic isn't touched and remains at
|
||||
// the overwritten=true, even though the user set it to false originally.
|
||||
#if UNITY_6000_0_OR_NEWER
|
||||
public override void OnStopServer() => rb.bodyType = wasKinematic ? RigidbodyType2D.Kinematic : RigidbodyType2D.Dynamic;
|
||||
public override void OnStopClient() => rb.bodyType = wasKinematic ? RigidbodyType2D.Kinematic : RigidbodyType2D.Dynamic;
|
||||
#else
|
||||
public override void OnStopServer() => rb.isKinematic = wasKinematic;
|
||||
public override void OnStopClient() => rb.isKinematic = wasKinematic;
|
||||
#endif
|
||||
|
||||
// overwriting Construct() and Apply() to set Rigidbody.MovePosition
|
||||
// would give more jittery movement.
|
||||
|
||||
// FixedUpdate for physics
|
||||
void FixedUpdate()
|
||||
{
|
||||
// who ever has authority moves the Rigidbody with physics.
|
||||
// everyone else simply sets it to kinematic.
|
||||
// so that only the Transform component is synced.
|
||||
|
||||
// host mode
|
||||
if (isServer && isClient)
|
||||
{
|
||||
// in host mode, we own it it if:
|
||||
// clientAuthority is disabled (hence server / we own it)
|
||||
// clientAuthority is enabled and we have authority over this object.
|
||||
bool owned = !clientAuthority || IsClientWithAuthority;
|
||||
|
||||
// only set to kinematic if we don't own it
|
||||
// otherwise don't touch isKinematic.
|
||||
// the authority owner might use it either way.
|
||||
#if UNITY_6000_0_OR_NEWER
|
||||
if (!owned) rb.bodyType = RigidbodyType2D.Kinematic;
|
||||
#else
|
||||
if (!owned) rb.isKinematic = true;
|
||||
#endif
|
||||
}
|
||||
// client only
|
||||
else if (isClient)
|
||||
{
|
||||
// on the client, we own it only if clientAuthority is enabled,
|
||||
// and we have authority over this object.
|
||||
bool owned = IsClientWithAuthority;
|
||||
|
||||
// only set to kinematic if we don't own it
|
||||
// otherwise don't touch isKinematic.
|
||||
// the authority owner might use it either way.
|
||||
#if UNITY_6000_0_OR_NEWER
|
||||
if (!owned) rb.bodyType = RigidbodyType2D.Kinematic;
|
||||
#else
|
||||
if (!owned) rb.isKinematic = true;
|
||||
#endif
|
||||
}
|
||||
// server only
|
||||
else if (isServer)
|
||||
{
|
||||
// on the server, we always own it if clientAuthority is disabled.
|
||||
bool owned = !clientAuthority;
|
||||
|
||||
// only set to kinematic if we don't own it
|
||||
// otherwise don't touch isKinematic.
|
||||
// the authority owner might use it either way.
|
||||
#if UNITY_6000_0_OR_NEWER
|
||||
if (!owned) rb.bodyType = RigidbodyType2D.Kinematic;
|
||||
#else
|
||||
if (!owned) rb.isKinematic = true;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnTeleport(Vector3 destination)
|
||||
{
|
||||
base.OnTeleport(destination);
|
||||
|
||||
rb.position = transform.position;
|
||||
}
|
||||
|
||||
protected override void OnTeleport(Vector3 destination, Quaternion rotation)
|
||||
{
|
||||
base.OnTeleport(destination, rotation);
|
||||
|
||||
rb.position = transform.position;
|
||||
rb.rotation = transform.rotation.eulerAngles.z;
|
||||
}
|
||||
}
|
||||
}
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
// [RequireComponent(typeof(Rigidbody))] <- OnValidate ensures this is on .target
|
||||
[AddComponentMenu("Network/Network Rigidbody 2D (Reliable)")]
|
||||
public class NetworkRigidbodyReliable2D : NetworkTransformReliable
|
||||
{
|
||||
bool clientAuthority => syncDirection == SyncDirection.ClientToServer;
|
||||
|
||||
Rigidbody2D rb;
|
||||
bool wasKinematic;
|
||||
|
||||
protected override void OnValidate()
|
||||
{
|
||||
if (Application.isPlaying) return;
|
||||
|
||||
base.OnValidate();
|
||||
|
||||
// we can't overwrite .target to be a Rigidbody.
|
||||
// but we can ensure that .target has a Rigidbody, and use it.
|
||||
if (target.GetComponent<Rigidbody2D>() == null)
|
||||
{
|
||||
Debug.LogWarning($"{name}'s NetworkRigidbody2D.target {target.name} is missing a Rigidbody2D", this);
|
||||
}
|
||||
}
|
||||
|
||||
// cach Rigidbody and original isKinematic setting
|
||||
protected override void Awake()
|
||||
{
|
||||
// we can't overwrite .target to be a Rigidbody.
|
||||
// but we can use its Rigidbody component.
|
||||
rb = target.GetComponent<Rigidbody2D>();
|
||||
if (rb == null)
|
||||
{
|
||||
Debug.LogError($"{name}'s NetworkRigidbody2D.target {target.name} is missing a Rigidbody2D", this);
|
||||
return;
|
||||
}
|
||||
#if UNITY_6000_0_OR_NEWER
|
||||
wasKinematic = rb.bodyType.HasFlag(RigidbodyType2D.Kinematic);
|
||||
#else
|
||||
wasKinematic = rb.isKinematic;
|
||||
#endif
|
||||
base.Awake();
|
||||
}
|
||||
|
||||
// reset forced isKinematic flag to original.
|
||||
// otherwise the overwritten value would remain between sessions forever.
|
||||
// for example, a game may run as client, set rigidbody.iskinematic=true,
|
||||
// then run as server, where .iskinematic isn't touched and remains at
|
||||
// the overwritten=true, even though the user set it to false originally.
|
||||
#if UNITY_6000_0_OR_NEWER
|
||||
public override void OnStopServer() => rb.bodyType = wasKinematic ? RigidbodyType2D.Kinematic : RigidbodyType2D.Dynamic;
|
||||
public override void OnStopClient() => rb.bodyType = wasKinematic ? RigidbodyType2D.Kinematic : RigidbodyType2D.Dynamic;
|
||||
#else
|
||||
public override void OnStopServer() => rb.isKinematic = wasKinematic;
|
||||
public override void OnStopClient() => rb.isKinematic = wasKinematic;
|
||||
#endif
|
||||
|
||||
// overwriting Construct() and Apply() to set Rigidbody.MovePosition
|
||||
// would give more jittery movement.
|
||||
|
||||
// FixedUpdate for physics
|
||||
void FixedUpdate()
|
||||
{
|
||||
// who ever has authority moves the Rigidbody with physics.
|
||||
// everyone else simply sets it to kinematic.
|
||||
// so that only the Transform component is synced.
|
||||
|
||||
// host mode
|
||||
if (isServer && isClient)
|
||||
{
|
||||
// in host mode, we own it it if:
|
||||
// clientAuthority is disabled (hence server / we own it)
|
||||
// clientAuthority is enabled and we have authority over this object.
|
||||
bool owned = !clientAuthority || IsClientWithAuthority;
|
||||
|
||||
// only set to kinematic if we don't own it
|
||||
// otherwise don't touch isKinematic.
|
||||
// the authority owner might use it either way.
|
||||
#if UNITY_6000_0_OR_NEWER
|
||||
if (!owned) rb.bodyType = RigidbodyType2D.Kinematic;
|
||||
#else
|
||||
if (!owned) rb.isKinematic = true;
|
||||
#endif
|
||||
}
|
||||
// client only
|
||||
else if (isClient)
|
||||
{
|
||||
// on the client, we own it only if clientAuthority is enabled,
|
||||
// and we have authority over this object.
|
||||
bool owned = IsClientWithAuthority;
|
||||
|
||||
// only set to kinematic if we don't own it
|
||||
// otherwise don't touch isKinematic.
|
||||
// the authority owner might use it either way.
|
||||
#if UNITY_6000_0_OR_NEWER
|
||||
if (!owned) rb.bodyType = RigidbodyType2D.Kinematic;
|
||||
#else
|
||||
if (!owned) rb.isKinematic = true;
|
||||
#endif
|
||||
}
|
||||
// server only
|
||||
else if (isServer)
|
||||
{
|
||||
// on the server, we always own it if clientAuthority is disabled.
|
||||
bool owned = !clientAuthority;
|
||||
|
||||
// only set to kinematic if we don't own it
|
||||
// otherwise don't touch isKinematic.
|
||||
// the authority owner might use it either way.
|
||||
#if UNITY_6000_0_OR_NEWER
|
||||
if (!owned) rb.bodyType = RigidbodyType2D.Kinematic;
|
||||
#else
|
||||
if (!owned) rb.isKinematic = true;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnTeleport(Vector3 destination)
|
||||
{
|
||||
base.OnTeleport(destination);
|
||||
|
||||
rb.position = transform.position;
|
||||
}
|
||||
|
||||
protected override void OnTeleport(Vector3 destination, Quaternion rotation)
|
||||
{
|
||||
base.OnTeleport(destination, rotation);
|
||||
|
||||
rb.position = transform.position;
|
||||
rb.rotation = transform.rotation.eulerAngles.z;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7ec4f7556ca1e4b55a3381fc6a02b1bc
|
||||
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/NetworkRigidbody/NetworkRigidbodyReliable2D.cs
|
||||
uploadId: 736421
|
||||
fileFormatVersion: 2
|
||||
guid: 7ec4f7556ca1e4b55a3381fc6a02b1bc
|
||||
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/NetworkRigidbody/NetworkRigidbodyReliable2D.cs
|
||||
uploadId: 736421
|
||||
|
||||
@@ -1,115 +1,115 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
// [RequireComponent(typeof(Rigidbody))] <- OnValidate ensures this is on .target
|
||||
[AddComponentMenu("Network/Network Rigidbody (Unreliable)")]
|
||||
public class NetworkRigidbodyUnreliable : NetworkTransformUnreliable
|
||||
{
|
||||
bool clientAuthority => syncDirection == SyncDirection.ClientToServer;
|
||||
|
||||
Rigidbody rb;
|
||||
bool wasKinematic;
|
||||
|
||||
protected override void OnValidate()
|
||||
{
|
||||
// Skip if Editor is in Play mode
|
||||
if (Application.isPlaying) return;
|
||||
|
||||
base.OnValidate();
|
||||
|
||||
// we can't overwrite .target to be a Rigidbody.
|
||||
// but we can ensure that .target has a Rigidbody, and use it.
|
||||
if (target.GetComponent<Rigidbody>() == null)
|
||||
{
|
||||
Debug.LogWarning($"{name}'s NetworkRigidbody.target {target.name} is missing a Rigidbody", this);
|
||||
}
|
||||
}
|
||||
|
||||
// cach Rigidbody and original isKinematic setting
|
||||
protected override void Awake()
|
||||
{
|
||||
// we can't overwrite .target to be a Rigidbody.
|
||||
// but we can use its Rigidbody component.
|
||||
rb = target.GetComponent<Rigidbody>();
|
||||
if (rb == null)
|
||||
{
|
||||
Debug.LogError($"{name}'s NetworkRigidbody.target {target.name} is missing a Rigidbody", this);
|
||||
return;
|
||||
}
|
||||
wasKinematic = rb.isKinematic;
|
||||
base.Awake();
|
||||
}
|
||||
|
||||
// reset forced isKinematic flag to original.
|
||||
// otherwise the overwritten value would remain between sessions forever.
|
||||
// for example, a game may run as client, set rigidbody.iskinematic=true,
|
||||
// then run as server, where .iskinematic isn't touched and remains at
|
||||
// the overwritten=true, even though the user set it to false originally.
|
||||
public override void OnStopServer() => rb.isKinematic = wasKinematic;
|
||||
public override void OnStopClient() => rb.isKinematic = wasKinematic;
|
||||
|
||||
// overwriting Construct() and Apply() to set Rigidbody.MovePosition
|
||||
// would give more jittery movement.
|
||||
|
||||
// FixedUpdate for physics
|
||||
void FixedUpdate()
|
||||
{
|
||||
// who ever has authority moves the Rigidbody with physics.
|
||||
// everyone else simply sets it to kinematic.
|
||||
// so that only the Transform component is synced.
|
||||
|
||||
// host mode
|
||||
if (isServer && isClient)
|
||||
{
|
||||
// in host mode, we own it it if:
|
||||
// clientAuthority is disabled (hence server / we own it)
|
||||
// clientAuthority is enabled and we have authority over this object.
|
||||
bool owned = !clientAuthority || IsClientWithAuthority;
|
||||
|
||||
// only set to kinematic if we don't own it
|
||||
// otherwise don't touch isKinematic.
|
||||
// the authority owner might use it either way.
|
||||
if (!owned) rb.isKinematic = true;
|
||||
}
|
||||
// client only
|
||||
else if (isClient)
|
||||
{
|
||||
// on the client, we own it only if clientAuthority is enabled,
|
||||
// and we have authority over this object.
|
||||
bool owned = IsClientWithAuthority;
|
||||
|
||||
// only set to kinematic if we don't own it
|
||||
// otherwise don't touch isKinematic.
|
||||
// the authority owner might use it either way.
|
||||
if (!owned) rb.isKinematic = true;
|
||||
}
|
||||
// server only
|
||||
else if (isServer)
|
||||
{
|
||||
// on the server, we always own it if clientAuthority is disabled.
|
||||
bool owned = !clientAuthority;
|
||||
|
||||
// only set to kinematic if we don't own it
|
||||
// otherwise don't touch isKinematic.
|
||||
// the authority owner might use it either way.
|
||||
if (!owned) rb.isKinematic = true;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnTeleport(Vector3 destination)
|
||||
{
|
||||
base.OnTeleport(destination);
|
||||
|
||||
rb.position = transform.position;
|
||||
}
|
||||
|
||||
protected override void OnTeleport(Vector3 destination, Quaternion rotation)
|
||||
{
|
||||
base.OnTeleport(destination, rotation);
|
||||
|
||||
rb.position = transform.position;
|
||||
rb.rotation = transform.rotation;
|
||||
}
|
||||
}
|
||||
}
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
// [RequireComponent(typeof(Rigidbody))] <- OnValidate ensures this is on .target
|
||||
[AddComponentMenu("Network/Network Rigidbody (Unreliable)")]
|
||||
public class NetworkRigidbodyUnreliable : NetworkTransformUnreliable
|
||||
{
|
||||
bool clientAuthority => syncDirection == SyncDirection.ClientToServer;
|
||||
|
||||
Rigidbody rb;
|
||||
bool wasKinematic;
|
||||
|
||||
protected override void OnValidate()
|
||||
{
|
||||
// Skip if Editor is in Play mode
|
||||
if (Application.isPlaying) return;
|
||||
|
||||
base.OnValidate();
|
||||
|
||||
// we can't overwrite .target to be a Rigidbody.
|
||||
// but we can ensure that .target has a Rigidbody, and use it.
|
||||
if (target.GetComponent<Rigidbody>() == null)
|
||||
{
|
||||
Debug.LogWarning($"{name}'s NetworkRigidbody.target {target.name} is missing a Rigidbody", this);
|
||||
}
|
||||
}
|
||||
|
||||
// cach Rigidbody and original isKinematic setting
|
||||
protected override void Awake()
|
||||
{
|
||||
// we can't overwrite .target to be a Rigidbody.
|
||||
// but we can use its Rigidbody component.
|
||||
rb = target.GetComponent<Rigidbody>();
|
||||
if (rb == null)
|
||||
{
|
||||
Debug.LogError($"{name}'s NetworkRigidbody.target {target.name} is missing a Rigidbody", this);
|
||||
return;
|
||||
}
|
||||
wasKinematic = rb.isKinematic;
|
||||
base.Awake();
|
||||
}
|
||||
|
||||
// reset forced isKinematic flag to original.
|
||||
// otherwise the overwritten value would remain between sessions forever.
|
||||
// for example, a game may run as client, set rigidbody.iskinematic=true,
|
||||
// then run as server, where .iskinematic isn't touched and remains at
|
||||
// the overwritten=true, even though the user set it to false originally.
|
||||
public override void OnStopServer() => rb.isKinematic = wasKinematic;
|
||||
public override void OnStopClient() => rb.isKinematic = wasKinematic;
|
||||
|
||||
// overwriting Construct() and Apply() to set Rigidbody.MovePosition
|
||||
// would give more jittery movement.
|
||||
|
||||
// FixedUpdate for physics
|
||||
void FixedUpdate()
|
||||
{
|
||||
// who ever has authority moves the Rigidbody with physics.
|
||||
// everyone else simply sets it to kinematic.
|
||||
// so that only the Transform component is synced.
|
||||
|
||||
// host mode
|
||||
if (isServer && isClient)
|
||||
{
|
||||
// in host mode, we own it it if:
|
||||
// clientAuthority is disabled (hence server / we own it)
|
||||
// clientAuthority is enabled and we have authority over this object.
|
||||
bool owned = !clientAuthority || IsClientWithAuthority;
|
||||
|
||||
// only set to kinematic if we don't own it
|
||||
// otherwise don't touch isKinematic.
|
||||
// the authority owner might use it either way.
|
||||
if (!owned) rb.isKinematic = true;
|
||||
}
|
||||
// client only
|
||||
else if (isClient)
|
||||
{
|
||||
// on the client, we own it only if clientAuthority is enabled,
|
||||
// and we have authority over this object.
|
||||
bool owned = IsClientWithAuthority;
|
||||
|
||||
// only set to kinematic if we don't own it
|
||||
// otherwise don't touch isKinematic.
|
||||
// the authority owner might use it either way.
|
||||
if (!owned) rb.isKinematic = true;
|
||||
}
|
||||
// server only
|
||||
else if (isServer)
|
||||
{
|
||||
// on the server, we always own it if clientAuthority is disabled.
|
||||
bool owned = !clientAuthority;
|
||||
|
||||
// only set to kinematic if we don't own it
|
||||
// otherwise don't touch isKinematic.
|
||||
// the authority owner might use it either way.
|
||||
if (!owned) rb.isKinematic = true;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnTeleport(Vector3 destination)
|
||||
{
|
||||
base.OnTeleport(destination);
|
||||
|
||||
rb.position = transform.position;
|
||||
}
|
||||
|
||||
protected override void OnTeleport(Vector3 destination, Quaternion rotation)
|
||||
{
|
||||
base.OnTeleport(destination, rotation);
|
||||
|
||||
rb.position = transform.position;
|
||||
rb.rotation = transform.rotation;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3b20dc110904e47f8a154cdcf6433eae
|
||||
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/NetworkRigidbody/NetworkRigidbodyUnreliable.cs
|
||||
uploadId: 736421
|
||||
fileFormatVersion: 2
|
||||
guid: 3b20dc110904e47f8a154cdcf6433eae
|
||||
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/NetworkRigidbody/NetworkRigidbodyUnreliable.cs
|
||||
uploadId: 736421
|
||||
|
||||
@@ -1,136 +1,136 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
// [RequireComponent(typeof(Rigidbody))] <- OnValidate ensures this is on .target
|
||||
[AddComponentMenu("Network/Network Rigidbody 2D (Unreliable)")]
|
||||
public class NetworkRigidbodyUnreliable2D : NetworkTransformUnreliable
|
||||
{
|
||||
bool clientAuthority => syncDirection == SyncDirection.ClientToServer;
|
||||
|
||||
Rigidbody2D rb;
|
||||
bool wasKinematic;
|
||||
|
||||
protected override void OnValidate()
|
||||
{
|
||||
// Skip if Editor is in Play mode
|
||||
if (Application.isPlaying) return;
|
||||
|
||||
base.OnValidate();
|
||||
|
||||
// we can't overwrite .target to be a Rigidbody.
|
||||
// but we can ensure that .target has a Rigidbody, and use it.
|
||||
if (target.GetComponent<Rigidbody2D>() == null)
|
||||
{
|
||||
Debug.LogWarning($"{name}'s NetworkRigidbody2D.target {target.name} is missing a Rigidbody2D", this);
|
||||
}
|
||||
}
|
||||
|
||||
// cach Rigidbody and original isKinematic setting
|
||||
protected override void Awake()
|
||||
{
|
||||
// we can't overwrite .target to be a Rigidbody.
|
||||
// but we can use its Rigidbody component.
|
||||
rb = target.GetComponent<Rigidbody2D>();
|
||||
if (rb == null)
|
||||
{
|
||||
Debug.LogError($"{name}'s NetworkRigidbody2D.target {target.name} is missing a Rigidbody2D", this);
|
||||
return;
|
||||
}
|
||||
|
||||
#if UNITY_6000_0_OR_NEWER
|
||||
wasKinematic = rb.bodyType.HasFlag(RigidbodyType2D.Kinematic);
|
||||
#else
|
||||
wasKinematic = rb.isKinematic;
|
||||
#endif
|
||||
base.Awake();
|
||||
}
|
||||
|
||||
// reset forced isKinematic flag to original.
|
||||
// otherwise the overwritten value would remain between sessions forever.
|
||||
// for example, a game may run as client, set rigidbody.iskinematic=true,
|
||||
// then run as server, where .iskinematic isn't touched and remains at
|
||||
// the overwritten=true, even though the user set it to false originally.
|
||||
#if UNITY_6000_0_OR_NEWER
|
||||
public override void OnStopServer() => rb.bodyType = wasKinematic ? RigidbodyType2D.Kinematic : RigidbodyType2D.Dynamic;
|
||||
public override void OnStopClient() => rb.bodyType = wasKinematic ? RigidbodyType2D.Kinematic : RigidbodyType2D.Dynamic;
|
||||
#else
|
||||
public override void OnStopServer() => rb.isKinematic = wasKinematic;
|
||||
public override void OnStopClient() => rb.isKinematic = wasKinematic;
|
||||
#endif
|
||||
// overwriting Construct() and Apply() to set Rigidbody.MovePosition
|
||||
// would give more jittery movement.
|
||||
|
||||
// FixedUpdate for physics
|
||||
void FixedUpdate()
|
||||
{
|
||||
// who ever has authority moves the Rigidbody with physics.
|
||||
// everyone else simply sets it to kinematic.
|
||||
// so that only the Transform component is synced.
|
||||
|
||||
// host mode
|
||||
if (isServer && isClient)
|
||||
{
|
||||
// in host mode, we own it it if:
|
||||
// clientAuthority is disabled (hence server / we own it)
|
||||
// clientAuthority is enabled and we have authority over this object.
|
||||
bool owned = !clientAuthority || IsClientWithAuthority;
|
||||
|
||||
// only set to kinematic if we don't own it
|
||||
// otherwise don't touch isKinematic.
|
||||
// the authority owner might use it either way.
|
||||
#if UNITY_6000_0_OR_NEWER
|
||||
if (!owned) rb.bodyType = RigidbodyType2D.Kinematic;
|
||||
#else
|
||||
if (!owned) rb.isKinematic = true;
|
||||
#endif
|
||||
}
|
||||
// client only
|
||||
else if (isClient)
|
||||
{
|
||||
// on the client, we own it only if clientAuthority is enabled,
|
||||
// and we have authority over this object.
|
||||
bool owned = IsClientWithAuthority;
|
||||
|
||||
// only set to kinematic if we don't own it
|
||||
// otherwise don't touch isKinematic.
|
||||
// the authority owner might use it either way.
|
||||
#if UNITY_6000_0_OR_NEWER
|
||||
if (!owned) rb.bodyType = RigidbodyType2D.Kinematic;
|
||||
#else
|
||||
if (!owned) rb.isKinematic = true;
|
||||
#endif
|
||||
}
|
||||
// server only
|
||||
else if (isServer)
|
||||
{
|
||||
// on the server, we always own it if clientAuthority is disabled.
|
||||
bool owned = !clientAuthority;
|
||||
|
||||
// only set to kinematic if we don't own it
|
||||
// otherwise don't touch isKinematic.
|
||||
// the authority owner might use it either way.
|
||||
#if UNITY_6000_0_OR_NEWER
|
||||
if (!owned) rb.bodyType = RigidbodyType2D.Kinematic;
|
||||
#else
|
||||
if (!owned) rb.isKinematic = true;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnTeleport(Vector3 destination)
|
||||
{
|
||||
base.OnTeleport(destination);
|
||||
|
||||
rb.position = transform.position;
|
||||
}
|
||||
|
||||
protected override void OnTeleport(Vector3 destination, Quaternion rotation)
|
||||
{
|
||||
base.OnTeleport(destination, rotation);
|
||||
|
||||
rb.position = transform.position;
|
||||
rb.rotation = transform.rotation.eulerAngles.z;
|
||||
}
|
||||
}
|
||||
}
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
// [RequireComponent(typeof(Rigidbody))] <- OnValidate ensures this is on .target
|
||||
[AddComponentMenu("Network/Network Rigidbody 2D (Unreliable)")]
|
||||
public class NetworkRigidbodyUnreliable2D : NetworkTransformUnreliable
|
||||
{
|
||||
bool clientAuthority => syncDirection == SyncDirection.ClientToServer;
|
||||
|
||||
Rigidbody2D rb;
|
||||
bool wasKinematic;
|
||||
|
||||
protected override void OnValidate()
|
||||
{
|
||||
// Skip if Editor is in Play mode
|
||||
if (Application.isPlaying) return;
|
||||
|
||||
base.OnValidate();
|
||||
|
||||
// we can't overwrite .target to be a Rigidbody.
|
||||
// but we can ensure that .target has a Rigidbody, and use it.
|
||||
if (target.GetComponent<Rigidbody2D>() == null)
|
||||
{
|
||||
Debug.LogWarning($"{name}'s NetworkRigidbody2D.target {target.name} is missing a Rigidbody2D", this);
|
||||
}
|
||||
}
|
||||
|
||||
// cach Rigidbody and original isKinematic setting
|
||||
protected override void Awake()
|
||||
{
|
||||
// we can't overwrite .target to be a Rigidbody.
|
||||
// but we can use its Rigidbody component.
|
||||
rb = target.GetComponent<Rigidbody2D>();
|
||||
if (rb == null)
|
||||
{
|
||||
Debug.LogError($"{name}'s NetworkRigidbody2D.target {target.name} is missing a Rigidbody2D", this);
|
||||
return;
|
||||
}
|
||||
|
||||
#if UNITY_6000_0_OR_NEWER
|
||||
wasKinematic = rb.bodyType.HasFlag(RigidbodyType2D.Kinematic);
|
||||
#else
|
||||
wasKinematic = rb.isKinematic;
|
||||
#endif
|
||||
base.Awake();
|
||||
}
|
||||
|
||||
// reset forced isKinematic flag to original.
|
||||
// otherwise the overwritten value would remain between sessions forever.
|
||||
// for example, a game may run as client, set rigidbody.iskinematic=true,
|
||||
// then run as server, where .iskinematic isn't touched and remains at
|
||||
// the overwritten=true, even though the user set it to false originally.
|
||||
#if UNITY_6000_0_OR_NEWER
|
||||
public override void OnStopServer() => rb.bodyType = wasKinematic ? RigidbodyType2D.Kinematic : RigidbodyType2D.Dynamic;
|
||||
public override void OnStopClient() => rb.bodyType = wasKinematic ? RigidbodyType2D.Kinematic : RigidbodyType2D.Dynamic;
|
||||
#else
|
||||
public override void OnStopServer() => rb.isKinematic = wasKinematic;
|
||||
public override void OnStopClient() => rb.isKinematic = wasKinematic;
|
||||
#endif
|
||||
// overwriting Construct() and Apply() to set Rigidbody.MovePosition
|
||||
// would give more jittery movement.
|
||||
|
||||
// FixedUpdate for physics
|
||||
void FixedUpdate()
|
||||
{
|
||||
// who ever has authority moves the Rigidbody with physics.
|
||||
// everyone else simply sets it to kinematic.
|
||||
// so that only the Transform component is synced.
|
||||
|
||||
// host mode
|
||||
if (isServer && isClient)
|
||||
{
|
||||
// in host mode, we own it it if:
|
||||
// clientAuthority is disabled (hence server / we own it)
|
||||
// clientAuthority is enabled and we have authority over this object.
|
||||
bool owned = !clientAuthority || IsClientWithAuthority;
|
||||
|
||||
// only set to kinematic if we don't own it
|
||||
// otherwise don't touch isKinematic.
|
||||
// the authority owner might use it either way.
|
||||
#if UNITY_6000_0_OR_NEWER
|
||||
if (!owned) rb.bodyType = RigidbodyType2D.Kinematic;
|
||||
#else
|
||||
if (!owned) rb.isKinematic = true;
|
||||
#endif
|
||||
}
|
||||
// client only
|
||||
else if (isClient)
|
||||
{
|
||||
// on the client, we own it only if clientAuthority is enabled,
|
||||
// and we have authority over this object.
|
||||
bool owned = IsClientWithAuthority;
|
||||
|
||||
// only set to kinematic if we don't own it
|
||||
// otherwise don't touch isKinematic.
|
||||
// the authority owner might use it either way.
|
||||
#if UNITY_6000_0_OR_NEWER
|
||||
if (!owned) rb.bodyType = RigidbodyType2D.Kinematic;
|
||||
#else
|
||||
if (!owned) rb.isKinematic = true;
|
||||
#endif
|
||||
}
|
||||
// server only
|
||||
else if (isServer)
|
||||
{
|
||||
// on the server, we always own it if clientAuthority is disabled.
|
||||
bool owned = !clientAuthority;
|
||||
|
||||
// only set to kinematic if we don't own it
|
||||
// otherwise don't touch isKinematic.
|
||||
// the authority owner might use it either way.
|
||||
#if UNITY_6000_0_OR_NEWER
|
||||
if (!owned) rb.bodyType = RigidbodyType2D.Kinematic;
|
||||
#else
|
||||
if (!owned) rb.isKinematic = true;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnTeleport(Vector3 destination)
|
||||
{
|
||||
base.OnTeleport(destination);
|
||||
|
||||
rb.position = transform.position;
|
||||
}
|
||||
|
||||
protected override void OnTeleport(Vector3 destination, Quaternion rotation)
|
||||
{
|
||||
base.OnTeleport(destination, rotation);
|
||||
|
||||
rb.position = transform.position;
|
||||
rb.rotation = transform.rotation.eulerAngles.z;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1c7e12ad9b9ae443c9fdf37e9f5ecd36
|
||||
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/NetworkRigidbody/NetworkRigidbodyUnreliable2D.cs
|
||||
uploadId: 736421
|
||||
fileFormatVersion: 2
|
||||
guid: 1c7e12ad9b9ae443c9fdf37e9f5ecd36
|
||||
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/NetworkRigidbody/NetworkRigidbodyUnreliable2D.cs
|
||||
uploadId: 736421
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,18 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 615e6c6589cf9e54cad646b5a11e0529
|
||||
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/NetworkRoomManager.cs
|
||||
uploadId: 736421
|
||||
fileFormatVersion: 2
|
||||
guid: 615e6c6589cf9e54cad646b5a11e0529
|
||||
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/NetworkRoomManager.cs
|
||||
uploadId: 736421
|
||||
|
||||
@@ -1,195 +1,195 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
/// <summary>
|
||||
/// This component works in conjunction with the NetworkRoomManager to make up the multiplayer room system.
|
||||
/// <para>The RoomPrefab object of the NetworkRoomManager must have this component on it. This component holds basic room player data required for the room to function. Game specific data for room players can be put in other components on the RoomPrefab or in scripts derived from NetworkRoomPlayer.</para>
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
[AddComponentMenu("Network/Network Room Player")]
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-room-player")]
|
||||
public class NetworkRoomPlayer : NetworkBehaviour
|
||||
{
|
||||
/// <summary>
|
||||
/// This flag controls whether the default UI is shown for the room player.
|
||||
/// <para>As this UI is rendered using the old GUI system, it is only recommended for testing purposes.</para>
|
||||
/// </summary>
|
||||
[Tooltip("This flag controls whether the default UI is shown for the room player")]
|
||||
public bool showRoomGUI = true;
|
||||
|
||||
[Header("Diagnostics")]
|
||||
|
||||
/// <summary>
|
||||
/// Diagnostic flag indicating whether this player is ready for the game to begin.
|
||||
/// <para>Invoke CmdChangeReadyState method on the client to set this flag.</para>
|
||||
/// <para>When all players are ready to begin, the game will start. This should not be set directly, CmdChangeReadyState should be called on the client to set it on the server.</para>
|
||||
/// </summary>
|
||||
[ReadOnly, Tooltip("Diagnostic flag indicating whether this player is ready for the game to begin")]
|
||||
[SyncVar(hook = nameof(ReadyStateChanged))]
|
||||
public bool readyToBegin;
|
||||
|
||||
/// <summary>
|
||||
/// Diagnostic index of the player, e.g. Player1, Player2, etc.
|
||||
/// </summary>
|
||||
[ReadOnly, Tooltip("Diagnostic index of the player, e.g. Player1, Player2, etc.")]
|
||||
[SyncVar(hook = nameof(IndexChanged))]
|
||||
public int index;
|
||||
|
||||
#region Unity Callbacks
|
||||
|
||||
/// <summary>
|
||||
/// Do not use Start - Override OnStartHost / OnStartClient instead!
|
||||
/// </summary>
|
||||
public virtual void Start()
|
||||
{
|
||||
if (NetworkManager.singleton is NetworkRoomManager room)
|
||||
{
|
||||
// NetworkRoomPlayer object must be set to DontDestroyOnLoad along with NetworkRoomManager
|
||||
// in server and all clients, otherwise it will be respawned in the game scene which would
|
||||
// have undesirable effects.
|
||||
if (room.dontDestroyOnLoad)
|
||||
DontDestroyOnLoad(gameObject);
|
||||
|
||||
room.roomSlots.Add(this);
|
||||
|
||||
if (NetworkServer.active)
|
||||
room.RecalculateRoomPlayerIndices();
|
||||
|
||||
if (NetworkClient.active)
|
||||
room.CallOnClientEnterRoom();
|
||||
}
|
||||
else Debug.LogError("RoomPlayer could not find a NetworkRoomManager. The RoomPlayer requires a NetworkRoomManager object to function. Make sure that there is one in the scene.");
|
||||
}
|
||||
|
||||
public virtual void OnDisable()
|
||||
{
|
||||
if (NetworkClient.active && NetworkManager.singleton is NetworkRoomManager room)
|
||||
{
|
||||
// only need to call this on client as server removes it before object is destroyed
|
||||
room.roomSlots.Remove(this);
|
||||
|
||||
room.CallOnClientExitRoom();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Commands
|
||||
|
||||
[Command]
|
||||
public void CmdChangeReadyState(bool readyState)
|
||||
{
|
||||
readyToBegin = readyState;
|
||||
NetworkRoomManager room = NetworkManager.singleton as NetworkRoomManager;
|
||||
if (room != null)
|
||||
{
|
||||
room.ReadyStatusChanged();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region SyncVar Hooks
|
||||
|
||||
/// <summary>
|
||||
/// This is a hook that is invoked on clients when the index changes.
|
||||
/// </summary>
|
||||
/// <param name="oldIndex">The old index value</param>
|
||||
/// <param name="newIndex">The new index value</param>
|
||||
public virtual void IndexChanged(int oldIndex, int newIndex) {}
|
||||
|
||||
/// <summary>
|
||||
/// This is a hook that is invoked on clients when a RoomPlayer switches between ready or not ready.
|
||||
/// <para>This function is called when the a client player calls CmdChangeReadyState.</para>
|
||||
/// </summary>
|
||||
/// <param name="newReadyState">New Ready State</param>
|
||||
public virtual void ReadyStateChanged(bool oldReadyState, bool newReadyState) {}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Room Client Virtuals
|
||||
|
||||
/// <summary>
|
||||
/// This is a hook that is invoked on clients for all room player objects when entering the room.
|
||||
/// <para>Note: isLocalPlayer is not guaranteed to be set until OnStartLocalPlayer is called.</para>
|
||||
/// </summary>
|
||||
public virtual void OnClientEnterRoom() {}
|
||||
|
||||
/// <summary>
|
||||
/// This is a hook that is invoked on clients for all room player objects when exiting the room.
|
||||
/// </summary>
|
||||
public virtual void OnClientExitRoom() {}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Optional UI
|
||||
|
||||
/// <summary>
|
||||
/// Render a UI for the room. Override to provide your own UI
|
||||
/// </summary>
|
||||
public virtual void OnGUI()
|
||||
{
|
||||
if (!showRoomGUI)
|
||||
return;
|
||||
|
||||
NetworkRoomManager room = NetworkManager.singleton as NetworkRoomManager;
|
||||
if (room)
|
||||
{
|
||||
if (!room.showRoomGUI)
|
||||
return;
|
||||
|
||||
if (!Utils.IsSceneActive(room.RoomScene))
|
||||
return;
|
||||
|
||||
DrawPlayerReadyState();
|
||||
DrawPlayerReadyButton();
|
||||
}
|
||||
}
|
||||
|
||||
void DrawPlayerReadyState()
|
||||
{
|
||||
GUILayout.BeginArea(new Rect(20f + (index * 100), 200f, 90f, 130f));
|
||||
|
||||
GUILayout.Label($"Player [{index + 1}]");
|
||||
|
||||
if (readyToBegin)
|
||||
GUILayout.Label("Ready");
|
||||
else
|
||||
GUILayout.Label("Not Ready");
|
||||
|
||||
if (((isServer && index > 0) || isServerOnly) && GUILayout.Button("REMOVE"))
|
||||
{
|
||||
// This button only shows on the Host for all players other than the Host
|
||||
// Host and Players can't remove themselves (stop the client instead)
|
||||
// Host can kick a Player this way.
|
||||
GetComponent<NetworkIdentity>().connectionToClient.Disconnect();
|
||||
}
|
||||
|
||||
GUILayout.EndArea();
|
||||
}
|
||||
|
||||
void DrawPlayerReadyButton()
|
||||
{
|
||||
if (NetworkClient.active && isLocalPlayer)
|
||||
{
|
||||
GUILayout.BeginArea(new Rect(20f, 300f, 120f, 20f));
|
||||
|
||||
if (readyToBegin)
|
||||
{
|
||||
if (GUILayout.Button("Cancel"))
|
||||
CmdChangeReadyState(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (GUILayout.Button("Ready"))
|
||||
CmdChangeReadyState(true);
|
||||
}
|
||||
|
||||
GUILayout.EndArea();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
/// <summary>
|
||||
/// This component works in conjunction with the NetworkRoomManager to make up the multiplayer room system.
|
||||
/// <para>The RoomPrefab object of the NetworkRoomManager must have this component on it. This component holds basic room player data required for the room to function. Game specific data for room players can be put in other components on the RoomPrefab or in scripts derived from NetworkRoomPlayer.</para>
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
[AddComponentMenu("Network/Network Room Player")]
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-room-player")]
|
||||
public class NetworkRoomPlayer : NetworkBehaviour
|
||||
{
|
||||
/// <summary>
|
||||
/// This flag controls whether the default UI is shown for the room player.
|
||||
/// <para>As this UI is rendered using the old GUI system, it is only recommended for testing purposes.</para>
|
||||
/// </summary>
|
||||
[Tooltip("This flag controls whether the default UI is shown for the room player")]
|
||||
public bool showRoomGUI = true;
|
||||
|
||||
[Header("Diagnostics")]
|
||||
|
||||
/// <summary>
|
||||
/// Diagnostic flag indicating whether this player is ready for the game to begin.
|
||||
/// <para>Invoke CmdChangeReadyState method on the client to set this flag.</para>
|
||||
/// <para>When all players are ready to begin, the game will start. This should not be set directly, CmdChangeReadyState should be called on the client to set it on the server.</para>
|
||||
/// </summary>
|
||||
[ReadOnly, Tooltip("Diagnostic flag indicating whether this player is ready for the game to begin")]
|
||||
[SyncVar(hook = nameof(ReadyStateChanged))]
|
||||
public bool readyToBegin;
|
||||
|
||||
/// <summary>
|
||||
/// Diagnostic index of the player, e.g. Player1, Player2, etc.
|
||||
/// </summary>
|
||||
[ReadOnly, Tooltip("Diagnostic index of the player, e.g. Player1, Player2, etc.")]
|
||||
[SyncVar(hook = nameof(IndexChanged))]
|
||||
public int index;
|
||||
|
||||
#region Unity Callbacks
|
||||
|
||||
/// <summary>
|
||||
/// Do not use Start - Override OnStartHost / OnStartClient instead!
|
||||
/// </summary>
|
||||
public virtual void Start()
|
||||
{
|
||||
if (NetworkManager.singleton is NetworkRoomManager room)
|
||||
{
|
||||
// NetworkRoomPlayer object must be set to DontDestroyOnLoad along with NetworkRoomManager
|
||||
// in server and all clients, otherwise it will be respawned in the game scene which would
|
||||
// have undesirable effects.
|
||||
if (room.dontDestroyOnLoad)
|
||||
DontDestroyOnLoad(gameObject);
|
||||
|
||||
room.roomSlots.Add(this);
|
||||
|
||||
if (NetworkServer.active)
|
||||
room.RecalculateRoomPlayerIndices();
|
||||
|
||||
if (NetworkClient.active)
|
||||
room.CallOnClientEnterRoom();
|
||||
}
|
||||
else Debug.LogError("RoomPlayer could not find a NetworkRoomManager. The RoomPlayer requires a NetworkRoomManager object to function. Make sure that there is one in the scene.");
|
||||
}
|
||||
|
||||
public virtual void OnDisable()
|
||||
{
|
||||
if (NetworkClient.active && NetworkManager.singleton is NetworkRoomManager room)
|
||||
{
|
||||
// only need to call this on client as server removes it before object is destroyed
|
||||
room.roomSlots.Remove(this);
|
||||
|
||||
room.CallOnClientExitRoom();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Commands
|
||||
|
||||
[Command]
|
||||
public void CmdChangeReadyState(bool readyState)
|
||||
{
|
||||
readyToBegin = readyState;
|
||||
NetworkRoomManager room = NetworkManager.singleton as NetworkRoomManager;
|
||||
if (room != null)
|
||||
{
|
||||
room.ReadyStatusChanged();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region SyncVar Hooks
|
||||
|
||||
/// <summary>
|
||||
/// This is a hook that is invoked on clients when the index changes.
|
||||
/// </summary>
|
||||
/// <param name="oldIndex">The old index value</param>
|
||||
/// <param name="newIndex">The new index value</param>
|
||||
public virtual void IndexChanged(int oldIndex, int newIndex) {}
|
||||
|
||||
/// <summary>
|
||||
/// This is a hook that is invoked on clients when a RoomPlayer switches between ready or not ready.
|
||||
/// <para>This function is called when the a client player calls CmdChangeReadyState.</para>
|
||||
/// </summary>
|
||||
/// <param name="newReadyState">New Ready State</param>
|
||||
public virtual void ReadyStateChanged(bool oldReadyState, bool newReadyState) {}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Room Client Virtuals
|
||||
|
||||
/// <summary>
|
||||
/// This is a hook that is invoked on clients for all room player objects when entering the room.
|
||||
/// <para>Note: isLocalPlayer is not guaranteed to be set until OnStartLocalPlayer is called.</para>
|
||||
/// </summary>
|
||||
public virtual void OnClientEnterRoom() {}
|
||||
|
||||
/// <summary>
|
||||
/// This is a hook that is invoked on clients for all room player objects when exiting the room.
|
||||
/// </summary>
|
||||
public virtual void OnClientExitRoom() {}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Optional UI
|
||||
|
||||
/// <summary>
|
||||
/// Render a UI for the room. Override to provide your own UI
|
||||
/// </summary>
|
||||
public virtual void OnGUI()
|
||||
{
|
||||
if (!showRoomGUI)
|
||||
return;
|
||||
|
||||
NetworkRoomManager room = NetworkManager.singleton as NetworkRoomManager;
|
||||
if (room)
|
||||
{
|
||||
if (!room.showRoomGUI)
|
||||
return;
|
||||
|
||||
if (!Utils.IsSceneActive(room.RoomScene))
|
||||
return;
|
||||
|
||||
DrawPlayerReadyState();
|
||||
DrawPlayerReadyButton();
|
||||
}
|
||||
}
|
||||
|
||||
void DrawPlayerReadyState()
|
||||
{
|
||||
GUILayout.BeginArea(new Rect(20f + (index * 100), 200f, 90f, 130f));
|
||||
|
||||
GUILayout.Label($"Player [{index + 1}]");
|
||||
|
||||
if (readyToBegin)
|
||||
GUILayout.Label("Ready");
|
||||
else
|
||||
GUILayout.Label("Not Ready");
|
||||
|
||||
if (((isServer && index > 0) || isServerOnly) && GUILayout.Button("REMOVE"))
|
||||
{
|
||||
// This button only shows on the Host for all players other than the Host
|
||||
// Host and Players can't remove themselves (stop the client instead)
|
||||
// Host can kick a Player this way.
|
||||
GetComponent<NetworkIdentity>().connectionToClient.Disconnect();
|
||||
}
|
||||
|
||||
GUILayout.EndArea();
|
||||
}
|
||||
|
||||
void DrawPlayerReadyButton()
|
||||
{
|
||||
if (NetworkClient.active && isLocalPlayer)
|
||||
{
|
||||
GUILayout.BeginArea(new Rect(20f, 300f, 120f, 20f));
|
||||
|
||||
if (readyToBegin)
|
||||
{
|
||||
if (GUILayout.Button("Cancel"))
|
||||
CmdChangeReadyState(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (GUILayout.Button("Ready"))
|
||||
CmdChangeReadyState(true);
|
||||
}
|
||||
|
||||
GUILayout.EndArea();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 79874ac94d5b1314788ecf0e86bd23fd
|
||||
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/NetworkRoomPlayer.cs
|
||||
uploadId: 736421
|
||||
fileFormatVersion: 2
|
||||
guid: 79874ac94d5b1314788ecf0e86bd23fd
|
||||
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/NetworkRoomPlayer.cs
|
||||
uploadId: 736421
|
||||
|
||||
@@ -1,194 +1,194 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
/// <summary>
|
||||
/// Shows Network messages and bytes sent and received per second.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>Add this component to the same object as Network Manager.</para>
|
||||
/// </remarks>
|
||||
[AddComponentMenu("Network/Network Statistics")]
|
||||
[DisallowMultipleComponent]
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-statistics")]
|
||||
public class NetworkStatistics : MonoBehaviour
|
||||
{
|
||||
// update interval
|
||||
double intervalStartTime;
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
// CLIENT (public fields for other components to grab statistics)
|
||||
// long bytes to support >2GB
|
||||
[HideInInspector] public int clientIntervalReceivedPackets;
|
||||
[HideInInspector] public long clientIntervalReceivedBytes;
|
||||
[HideInInspector] public int clientIntervalSentPackets;
|
||||
[HideInInspector] public long clientIntervalSentBytes;
|
||||
|
||||
// results from last interval
|
||||
// long bytes to support >2GB
|
||||
[HideInInspector] public int clientReceivedPacketsPerSecond;
|
||||
[HideInInspector] public long clientReceivedBytesPerSecond;
|
||||
[HideInInspector] public int clientSentPacketsPerSecond;
|
||||
[HideInInspector] public long clientSentBytesPerSecond;
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
// SERVER (public fields for other components to grab statistics)
|
||||
// capture interval
|
||||
// long bytes to support >2GB
|
||||
[HideInInspector] public int serverIntervalReceivedPackets;
|
||||
[HideInInspector] public long serverIntervalReceivedBytes;
|
||||
[HideInInspector] public int serverIntervalSentPackets;
|
||||
[HideInInspector] public long serverIntervalSentBytes;
|
||||
|
||||
// results from last interval
|
||||
// long bytes to support >2GB
|
||||
[HideInInspector] public int serverReceivedPacketsPerSecond;
|
||||
[HideInInspector] public long serverReceivedBytesPerSecond;
|
||||
[HideInInspector] public int serverSentPacketsPerSecond;
|
||||
[HideInInspector] public long serverSentBytesPerSecond;
|
||||
|
||||
// NetworkManager sets Transport.active in Awake().
|
||||
// so let's hook into it in Start().
|
||||
void Start()
|
||||
{
|
||||
// find available transport
|
||||
Transport transport = Transport.active;
|
||||
if (transport != null)
|
||||
{
|
||||
transport.OnClientDataReceived += OnClientReceive;
|
||||
transport.OnClientDataSent += OnClientSend;
|
||||
transport.OnServerDataReceived += OnServerReceive;
|
||||
transport.OnServerDataSent += OnServerSend;
|
||||
}
|
||||
else Debug.LogError($"NetworkStatistics: no available or active Transport found on this platform: {Application.platform}");
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
// remove transport hooks
|
||||
Transport transport = Transport.active;
|
||||
if (transport != null)
|
||||
{
|
||||
transport.OnClientDataReceived -= OnClientReceive;
|
||||
transport.OnClientDataSent -= OnClientSend;
|
||||
transport.OnServerDataReceived -= OnServerReceive;
|
||||
transport.OnServerDataSent -= OnServerSend;
|
||||
}
|
||||
}
|
||||
|
||||
void OnClientReceive(ArraySegment<byte> data, int channelId)
|
||||
{
|
||||
++clientIntervalReceivedPackets;
|
||||
clientIntervalReceivedBytes += data.Count;
|
||||
}
|
||||
|
||||
void OnClientSend(ArraySegment<byte> data, int channelId)
|
||||
{
|
||||
++clientIntervalSentPackets;
|
||||
clientIntervalSentBytes += data.Count;
|
||||
}
|
||||
|
||||
void OnServerReceive(int connectionId, ArraySegment<byte> data, int channelId)
|
||||
{
|
||||
++serverIntervalReceivedPackets;
|
||||
serverIntervalReceivedBytes += data.Count;
|
||||
}
|
||||
|
||||
void OnServerSend(int connectionId, ArraySegment<byte> data, int channelId)
|
||||
{
|
||||
++serverIntervalSentPackets;
|
||||
serverIntervalSentBytes += data.Count;
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
// calculate results every second
|
||||
if (NetworkTime.localTime >= intervalStartTime + 1)
|
||||
{
|
||||
if (NetworkClient.active) UpdateClient();
|
||||
if (NetworkServer.active) UpdateServer();
|
||||
|
||||
intervalStartTime = NetworkTime.localTime;
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateClient()
|
||||
{
|
||||
clientReceivedPacketsPerSecond = clientIntervalReceivedPackets;
|
||||
clientReceivedBytesPerSecond = clientIntervalReceivedBytes;
|
||||
clientSentPacketsPerSecond = clientIntervalSentPackets;
|
||||
clientSentBytesPerSecond = clientIntervalSentBytes;
|
||||
|
||||
clientIntervalReceivedPackets = 0;
|
||||
clientIntervalReceivedBytes = 0;
|
||||
clientIntervalSentPackets = 0;
|
||||
clientIntervalSentBytes = 0;
|
||||
}
|
||||
|
||||
void UpdateServer()
|
||||
{
|
||||
serverReceivedPacketsPerSecond = serverIntervalReceivedPackets;
|
||||
serverReceivedBytesPerSecond = serverIntervalReceivedBytes;
|
||||
serverSentPacketsPerSecond = serverIntervalSentPackets;
|
||||
serverSentBytesPerSecond = serverIntervalSentBytes;
|
||||
|
||||
serverIntervalReceivedPackets = 0;
|
||||
serverIntervalReceivedBytes = 0;
|
||||
serverIntervalSentPackets = 0;
|
||||
serverIntervalSentBytes = 0;
|
||||
}
|
||||
|
||||
void OnGUI()
|
||||
{
|
||||
// only show if either server or client active
|
||||
if (NetworkClient.active || NetworkServer.active)
|
||||
{
|
||||
// create main GUI area
|
||||
// 120 is below NetworkManager HUD in all cases.
|
||||
GUILayout.BeginArea(new Rect(10, 120, 215, 300));
|
||||
|
||||
// show client / server stats if active
|
||||
if (NetworkClient.active) OnClientGUI();
|
||||
if (NetworkServer.active) OnServerGUI();
|
||||
|
||||
// end of GUI area
|
||||
GUILayout.EndArea();
|
||||
}
|
||||
}
|
||||
|
||||
void OnClientGUI()
|
||||
{
|
||||
// background
|
||||
GUILayout.BeginVertical("Box");
|
||||
GUILayout.Label("<b>Client Statistics</b>");
|
||||
|
||||
// sending ("msgs" instead of "packets" to fit larger numbers)
|
||||
GUILayout.Label($"Send: {clientSentPacketsPerSecond} msgs @ {Utils.PrettyBytes(clientSentBytesPerSecond)}/s");
|
||||
|
||||
// receiving ("msgs" instead of "packets" to fit larger numbers)
|
||||
GUILayout.Label($"Recv: {clientReceivedPacketsPerSecond} msgs @ {Utils.PrettyBytes(clientReceivedBytesPerSecond)}/s");
|
||||
|
||||
// end background
|
||||
GUILayout.EndVertical();
|
||||
}
|
||||
|
||||
void OnServerGUI()
|
||||
{
|
||||
// background
|
||||
GUILayout.BeginVertical("Box");
|
||||
GUILayout.Label("<b>Server Statistics</b>");
|
||||
|
||||
// sending ("msgs" instead of "packets" to fit larger numbers)
|
||||
GUILayout.Label($"Send: {serverSentPacketsPerSecond} msgs @ {Utils.PrettyBytes(serverSentBytesPerSecond)}/s");
|
||||
|
||||
// receiving ("msgs" instead of "packets" to fit larger numbers)
|
||||
GUILayout.Label($"Recv: {serverReceivedPacketsPerSecond} msgs @ {Utils.PrettyBytes(serverReceivedBytesPerSecond)}/s");
|
||||
|
||||
// end background
|
||||
GUILayout.EndVertical();
|
||||
}
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
/// <summary>
|
||||
/// Shows Network messages and bytes sent and received per second.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>Add this component to the same object as Network Manager.</para>
|
||||
/// </remarks>
|
||||
[AddComponentMenu("Network/Network Statistics")]
|
||||
[DisallowMultipleComponent]
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-statistics")]
|
||||
public class NetworkStatistics : MonoBehaviour
|
||||
{
|
||||
// update interval
|
||||
double intervalStartTime;
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
// CLIENT (public fields for other components to grab statistics)
|
||||
// long bytes to support >2GB
|
||||
[HideInInspector] public int clientIntervalReceivedPackets;
|
||||
[HideInInspector] public long clientIntervalReceivedBytes;
|
||||
[HideInInspector] public int clientIntervalSentPackets;
|
||||
[HideInInspector] public long clientIntervalSentBytes;
|
||||
|
||||
// results from last interval
|
||||
// long bytes to support >2GB
|
||||
[HideInInspector] public int clientReceivedPacketsPerSecond;
|
||||
[HideInInspector] public long clientReceivedBytesPerSecond;
|
||||
[HideInInspector] public int clientSentPacketsPerSecond;
|
||||
[HideInInspector] public long clientSentBytesPerSecond;
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
// SERVER (public fields for other components to grab statistics)
|
||||
// capture interval
|
||||
// long bytes to support >2GB
|
||||
[HideInInspector] public int serverIntervalReceivedPackets;
|
||||
[HideInInspector] public long serverIntervalReceivedBytes;
|
||||
[HideInInspector] public int serverIntervalSentPackets;
|
||||
[HideInInspector] public long serverIntervalSentBytes;
|
||||
|
||||
// results from last interval
|
||||
// long bytes to support >2GB
|
||||
[HideInInspector] public int serverReceivedPacketsPerSecond;
|
||||
[HideInInspector] public long serverReceivedBytesPerSecond;
|
||||
[HideInInspector] public int serverSentPacketsPerSecond;
|
||||
[HideInInspector] public long serverSentBytesPerSecond;
|
||||
|
||||
// NetworkManager sets Transport.active in Awake().
|
||||
// so let's hook into it in Start().
|
||||
void Start()
|
||||
{
|
||||
// find available transport
|
||||
Transport transport = Transport.active;
|
||||
if (transport != null)
|
||||
{
|
||||
transport.OnClientDataReceived += OnClientReceive;
|
||||
transport.OnClientDataSent += OnClientSend;
|
||||
transport.OnServerDataReceived += OnServerReceive;
|
||||
transport.OnServerDataSent += OnServerSend;
|
||||
}
|
||||
else Debug.LogError($"NetworkStatistics: no available or active Transport found on this platform: {Application.platform}");
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
// remove transport hooks
|
||||
Transport transport = Transport.active;
|
||||
if (transport != null)
|
||||
{
|
||||
transport.OnClientDataReceived -= OnClientReceive;
|
||||
transport.OnClientDataSent -= OnClientSend;
|
||||
transport.OnServerDataReceived -= OnServerReceive;
|
||||
transport.OnServerDataSent -= OnServerSend;
|
||||
}
|
||||
}
|
||||
|
||||
void OnClientReceive(ArraySegment<byte> data, int channelId)
|
||||
{
|
||||
++clientIntervalReceivedPackets;
|
||||
clientIntervalReceivedBytes += data.Count;
|
||||
}
|
||||
|
||||
void OnClientSend(ArraySegment<byte> data, int channelId)
|
||||
{
|
||||
++clientIntervalSentPackets;
|
||||
clientIntervalSentBytes += data.Count;
|
||||
}
|
||||
|
||||
void OnServerReceive(int connectionId, ArraySegment<byte> data, int channelId)
|
||||
{
|
||||
++serverIntervalReceivedPackets;
|
||||
serverIntervalReceivedBytes += data.Count;
|
||||
}
|
||||
|
||||
void OnServerSend(int connectionId, ArraySegment<byte> data, int channelId)
|
||||
{
|
||||
++serverIntervalSentPackets;
|
||||
serverIntervalSentBytes += data.Count;
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
// calculate results every second
|
||||
if (NetworkTime.localTime >= intervalStartTime + 1)
|
||||
{
|
||||
if (NetworkClient.active) UpdateClient();
|
||||
if (NetworkServer.active) UpdateServer();
|
||||
|
||||
intervalStartTime = NetworkTime.localTime;
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateClient()
|
||||
{
|
||||
clientReceivedPacketsPerSecond = clientIntervalReceivedPackets;
|
||||
clientReceivedBytesPerSecond = clientIntervalReceivedBytes;
|
||||
clientSentPacketsPerSecond = clientIntervalSentPackets;
|
||||
clientSentBytesPerSecond = clientIntervalSentBytes;
|
||||
|
||||
clientIntervalReceivedPackets = 0;
|
||||
clientIntervalReceivedBytes = 0;
|
||||
clientIntervalSentPackets = 0;
|
||||
clientIntervalSentBytes = 0;
|
||||
}
|
||||
|
||||
void UpdateServer()
|
||||
{
|
||||
serverReceivedPacketsPerSecond = serverIntervalReceivedPackets;
|
||||
serverReceivedBytesPerSecond = serverIntervalReceivedBytes;
|
||||
serverSentPacketsPerSecond = serverIntervalSentPackets;
|
||||
serverSentBytesPerSecond = serverIntervalSentBytes;
|
||||
|
||||
serverIntervalReceivedPackets = 0;
|
||||
serverIntervalReceivedBytes = 0;
|
||||
serverIntervalSentPackets = 0;
|
||||
serverIntervalSentBytes = 0;
|
||||
}
|
||||
|
||||
void OnGUI()
|
||||
{
|
||||
// only show if either server or client active
|
||||
if (NetworkClient.active || NetworkServer.active)
|
||||
{
|
||||
// create main GUI area
|
||||
// 120 is below NetworkManager HUD in all cases.
|
||||
GUILayout.BeginArea(new Rect(10, 120, 215, 300));
|
||||
|
||||
// show client / server stats if active
|
||||
if (NetworkClient.active) OnClientGUI();
|
||||
if (NetworkServer.active) OnServerGUI();
|
||||
|
||||
// end of GUI area
|
||||
GUILayout.EndArea();
|
||||
}
|
||||
}
|
||||
|
||||
void OnClientGUI()
|
||||
{
|
||||
// background
|
||||
GUILayout.BeginVertical("Box");
|
||||
GUILayout.Label("<b>Client Statistics</b>");
|
||||
|
||||
// sending ("msgs" instead of "packets" to fit larger numbers)
|
||||
GUILayout.Label($"Send: {clientSentPacketsPerSecond} msgs @ {Utils.PrettyBytes(clientSentBytesPerSecond)}/s");
|
||||
|
||||
// receiving ("msgs" instead of "packets" to fit larger numbers)
|
||||
GUILayout.Label($"Recv: {clientReceivedPacketsPerSecond} msgs @ {Utils.PrettyBytes(clientReceivedBytesPerSecond)}/s");
|
||||
|
||||
// end background
|
||||
GUILayout.EndVertical();
|
||||
}
|
||||
|
||||
void OnServerGUI()
|
||||
{
|
||||
// background
|
||||
GUILayout.BeginVertical("Box");
|
||||
GUILayout.Label("<b>Server Statistics</b>");
|
||||
|
||||
// sending ("msgs" instead of "packets" to fit larger numbers)
|
||||
GUILayout.Label($"Send: {serverSentPacketsPerSecond} msgs @ {Utils.PrettyBytes(serverSentBytesPerSecond)}/s");
|
||||
|
||||
// receiving ("msgs" instead of "packets" to fit larger numbers)
|
||||
GUILayout.Label($"Recv: {serverReceivedPacketsPerSecond} msgs @ {Utils.PrettyBytes(serverReceivedBytesPerSecond)}/s");
|
||||
|
||||
// end background
|
||||
GUILayout.EndVertical();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6d7da4e566d24ea7b0e12178d934b648
|
||||
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/NetworkStatistics.cs
|
||||
uploadId: 736421
|
||||
fileFormatVersion: 2
|
||||
guid: 6d7da4e566d24ea7b0e12178d934b648
|
||||
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/NetworkStatistics.cs
|
||||
uploadId: 736421
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 36de72d9255741659bcbd1971ed29822
|
||||
timeCreated: 1668358590
|
||||
fileFormatVersion: 2
|
||||
guid: 36de72d9255741659bcbd1971ed29822
|
||||
timeCreated: 1668358590
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,18 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7c44135fde488424eaf28566206ce473
|
||||
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/NetworkTransform/NetworkTransformBase.cs
|
||||
uploadId: 736421
|
||||
fileFormatVersion: 2
|
||||
guid: 7c44135fde488424eaf28566206ce473
|
||||
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/NetworkTransform/NetworkTransformBase.cs
|
||||
uploadId: 736421
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,18 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8f63ea2e505fd484193fb31c5c55ca73
|
||||
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/NetworkTransform/NetworkTransformHybrid.cs
|
||||
uploadId: 736421
|
||||
fileFormatVersion: 2
|
||||
guid: 8f63ea2e505fd484193fb31c5c55ca73
|
||||
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/NetworkTransform/NetworkTransformHybrid.cs
|
||||
uploadId: 736421
|
||||
|
||||
@@ -1,448 +1,448 @@
|
||||
// NetworkTransform V3 (reliable) by mischa (2022-10)
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
[AddComponentMenu("Network/Network Transform (Reliable)")]
|
||||
public class NetworkTransformReliable : NetworkTransformBase
|
||||
{
|
||||
uint sendIntervalCounter = 0;
|
||||
double lastSendIntervalTime = double.MinValue;
|
||||
TransformSnapshot? pendingSnapshot;
|
||||
|
||||
[Header("Additional Settings")]
|
||||
[Tooltip("If we only sync on change, then we need to correct old snapshots if more time than sendInterval * multiplier has elapsed.\n\nOtherwise the first move will always start interpolating from the last move sequence's time, which will make it stutter when starting every time.")]
|
||||
public float onlySyncOnChangeCorrectionMultiplier = 2;
|
||||
public bool useFixedUpdate;
|
||||
|
||||
[Header("Rotation")]
|
||||
[Tooltip("Sensitivity of changes needed before an updated state is sent over the network")]
|
||||
public float rotationSensitivity = 0.01f;
|
||||
|
||||
// delta compression is capable of detecting byte-level changes.
|
||||
// if we scale float position to bytes,
|
||||
// then small movements will only change one byte.
|
||||
// this gives optimal bandwidth.
|
||||
// benchmark with 0.01 precision: 130 KB/s => 60 KB/s
|
||||
// benchmark with 0.1 precision: 130 KB/s => 30 KB/s
|
||||
[Header("Precision")]
|
||||
[Tooltip("Position is rounded in order to drastically minimize bandwidth.\n\nFor example, a precision of 0.01 rounds to a centimeter. In other words, sub-centimeter movements aren't synced until they eventually exceeded an actual centimeter.\n\nDepending on how important the object is, a precision of 0.01-0.10 (1-10 cm) is recommended.\n\nFor example, even a 1cm precision combined with delta compression cuts the Benchmark demo's bandwidth in half, compared to sending every tiny change.")]
|
||||
[Range(0.00_01f, 1f)] // disallow 0 division. 1mm to 1m precision is enough range.
|
||||
public float positionPrecision = 0.01f; // 1 cm
|
||||
[Range(0.00_01f, 1f)] // disallow 0 division. 1mm to 1m precision is enough range.
|
||||
public float scalePrecision = 0.01f; // 1 cm
|
||||
|
||||
// delta compression needs to remember 'last' to compress against
|
||||
protected Vector3Long lastSerializedPosition = Vector3Long.zero;
|
||||
protected Vector3Long lastDeserializedPosition = Vector3Long.zero;
|
||||
|
||||
protected Vector3Long lastSerializedScale = Vector3Long.zero;
|
||||
protected Vector3Long lastDeserializedScale = Vector3Long.zero;
|
||||
|
||||
// Used to store last sent snapshots
|
||||
protected TransformSnapshot last;
|
||||
|
||||
// update //////////////////////////////////////////////////////////////
|
||||
void Update()
|
||||
{
|
||||
// if server then always sync to others.
|
||||
if (isServer) UpdateServer();
|
||||
// 'else if' because host mode shouldn't send anything to server.
|
||||
// it is the server. don't overwrite anything there.
|
||||
else if (isClient) UpdateClient();
|
||||
}
|
||||
|
||||
void FixedUpdate()
|
||||
{
|
||||
if (!useFixedUpdate) return;
|
||||
|
||||
if (pendingSnapshot.HasValue && !IsClientWithAuthority)
|
||||
{
|
||||
// Apply via base method, but in FixedUpdate
|
||||
Apply(pendingSnapshot.Value, pendingSnapshot.Value);
|
||||
pendingSnapshot = null;
|
||||
}
|
||||
}
|
||||
|
||||
void LateUpdate()
|
||||
{
|
||||
// set dirty to trigger OnSerialize. either always, or only if changed.
|
||||
// It has to be checked in LateUpdate() for onlySyncOnChange to avoid
|
||||
// the possibility of Update() running first before the object's movement
|
||||
// script's Update(), which then causes NT to send every alternate frame
|
||||
// instead.
|
||||
if (isServer || (IsClientWithAuthority && NetworkClient.ready))
|
||||
{
|
||||
if (sendIntervalCounter == sendIntervalMultiplier && (!onlySyncOnChange || Changed(Construct())))
|
||||
SetDirty();
|
||||
|
||||
CheckLastSendTime();
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void UpdateServer()
|
||||
{
|
||||
// apply buffered snapshots IF client authority
|
||||
// -> in server authority, server moves the object
|
||||
// so no need to apply any snapshots there.
|
||||
// -> don't apply for host mode player objects either, even if in
|
||||
// client authority mode. if it doesn't go over the network,
|
||||
// then we don't need to do anything.
|
||||
// -> connectionToClient is briefly null after scene changes:
|
||||
// https://github.com/MirrorNetworking/Mirror/issues/3329
|
||||
if (syncDirection == SyncDirection.ClientToServer &&
|
||||
connectionToClient != null &&
|
||||
!isOwned)
|
||||
{
|
||||
if (serverSnapshots.Count > 0)
|
||||
{
|
||||
// step the transform interpolation without touching time.
|
||||
// NetworkClient is responsible for time globally.
|
||||
SnapshotInterpolation.StepInterpolation(
|
||||
serverSnapshots,
|
||||
connectionToClient.remoteTimeline,
|
||||
out TransformSnapshot from,
|
||||
out TransformSnapshot to,
|
||||
out double t);
|
||||
|
||||
// interpolate & apply
|
||||
TransformSnapshot computed = TransformSnapshot.Interpolate(from, to, t);
|
||||
Apply(computed, to);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void UpdateClient()
|
||||
{
|
||||
if (useFixedUpdate)
|
||||
{
|
||||
if (!IsClientWithAuthority && clientSnapshots.Count > 0)
|
||||
{
|
||||
SnapshotInterpolation.StepInterpolation(
|
||||
clientSnapshots,
|
||||
NetworkTime.time,
|
||||
out TransformSnapshot from,
|
||||
out TransformSnapshot to,
|
||||
out double t
|
||||
);
|
||||
pendingSnapshot = TransformSnapshot.Interpolate(from, to, t);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// client authority, and local player (= allowed to move myself)?
|
||||
if (!IsClientWithAuthority)
|
||||
{
|
||||
// only while we have snapshots
|
||||
if (clientSnapshots.Count > 0)
|
||||
{
|
||||
// step the interpolation without touching time.
|
||||
// NetworkClient is responsible for time globally.
|
||||
SnapshotInterpolation.StepInterpolation(
|
||||
clientSnapshots,
|
||||
NetworkTime.time, // == NetworkClient.localTimeline from snapshot interpolation
|
||||
out TransformSnapshot from,
|
||||
out TransformSnapshot to,
|
||||
out double t);
|
||||
|
||||
// interpolate & apply
|
||||
TransformSnapshot computed = TransformSnapshot.Interpolate(from, to, t);
|
||||
Apply(computed, to);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void CheckLastSendTime()
|
||||
{
|
||||
// timeAsDouble not available in older Unity versions.
|
||||
if (AccurateInterval.Elapsed(NetworkTime.localTime, NetworkServer.sendInterval, ref lastSendIntervalTime))
|
||||
{
|
||||
if (sendIntervalCounter == sendIntervalMultiplier)
|
||||
sendIntervalCounter = 0;
|
||||
sendIntervalCounter++;
|
||||
}
|
||||
}
|
||||
|
||||
// check if position / rotation / scale changed since last sync
|
||||
protected virtual bool Changed(TransformSnapshot current) =>
|
||||
// position is quantized and delta compressed.
|
||||
// only consider it changed if the quantized representation is changed.
|
||||
// careful: don't use 'serialized / deserialized last'. as it depends on sync mode etc.
|
||||
QuantizedChanged(last.position, current.position, positionPrecision) ||
|
||||
// rotation isn't quantized / delta compressed.
|
||||
// check with sensitivity.
|
||||
Quaternion.Angle(last.rotation, current.rotation) > rotationSensitivity ||
|
||||
// scale is quantized and delta compressed.
|
||||
// only consider it changed if the quantized representation is changed.
|
||||
// careful: don't use 'serialized / deserialized last'. as it depends on sync mode etc.
|
||||
QuantizedChanged(last.scale, current.scale, scalePrecision);
|
||||
|
||||
// helper function to compare quantized representations of a Vector3
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected bool QuantizedChanged(Vector3 u, Vector3 v, float precision)
|
||||
{
|
||||
Compression.ScaleToLong(u, precision, out Vector3Long uQuantized);
|
||||
Compression.ScaleToLong(v, precision, out Vector3Long vQuantized);
|
||||
return uQuantized != vQuantized;
|
||||
}
|
||||
|
||||
// NT may be used on client/server/host to Owner/Observers with
|
||||
// ServerToClient or ClientToServer.
|
||||
// however, OnSerialize should always delta against last.
|
||||
public override void OnSerialize(NetworkWriter writer, bool initialState)
|
||||
{
|
||||
// get current snapshot for broadcasting.
|
||||
TransformSnapshot snapshot = Construct();
|
||||
|
||||
// ClientToServer optimization:
|
||||
// for interpolated client owned identities,
|
||||
// always broadcast the latest known snapshot so other clients can
|
||||
// interpolate immediately instead of catching up too
|
||||
|
||||
// TODO dirty mask? [compression is very good w/o it already]
|
||||
// each vector's component is delta compressed.
|
||||
// an unchanged component would still require 1 byte.
|
||||
// let's use a dirty bit mask to filter those out as well.
|
||||
|
||||
// initial
|
||||
if (initialState)
|
||||
{
|
||||
// If there is a last serialized snapshot, we use it.
|
||||
// This prevents the new client getting a snapshot that is different
|
||||
// from what the older clients last got. If this happens, and on the next
|
||||
// regular serialisation the delta compression will get wrong values.
|
||||
// Notes:
|
||||
// 1. Interestingly only the older clients have it wrong, because at the end
|
||||
// of this function, last = snapshot which is the initial state's snapshot
|
||||
// 2. Regular NTR gets by this bug because it sends every frame anyway so initialstate
|
||||
// snapshot constructed would have been the same as the last anyway.
|
||||
if (last.remoteTime > 0) snapshot = last;
|
||||
if (syncPosition) writer.WriteVector3(snapshot.position);
|
||||
if (syncRotation)
|
||||
{
|
||||
// (optional) smallest three compression for now. no delta.
|
||||
if (compressRotation)
|
||||
writer.WriteUInt(Compression.CompressQuaternion(snapshot.rotation));
|
||||
else
|
||||
writer.WriteQuaternion(snapshot.rotation);
|
||||
}
|
||||
if (syncScale) writer.WriteVector3(snapshot.scale);
|
||||
}
|
||||
// delta
|
||||
else
|
||||
{
|
||||
// int before = writer.Position;
|
||||
|
||||
if (syncPosition)
|
||||
{
|
||||
// quantize -> delta -> varint
|
||||
Compression.ScaleToLong(snapshot.position, positionPrecision, out Vector3Long quantized);
|
||||
DeltaCompression.Compress(writer, lastSerializedPosition, quantized);
|
||||
}
|
||||
if (syncRotation)
|
||||
{
|
||||
// (optional) smallest three compression for now. no delta.
|
||||
if (compressRotation)
|
||||
writer.WriteUInt(Compression.CompressQuaternion(snapshot.rotation));
|
||||
else
|
||||
writer.WriteQuaternion(snapshot.rotation);
|
||||
}
|
||||
if (syncScale)
|
||||
{
|
||||
// quantize -> delta -> varint
|
||||
Compression.ScaleToLong(snapshot.scale, scalePrecision, out Vector3Long quantized);
|
||||
DeltaCompression.Compress(writer, lastSerializedScale, quantized);
|
||||
}
|
||||
}
|
||||
|
||||
// save serialized as 'last' for next delta compression
|
||||
if (syncPosition) Compression.ScaleToLong(snapshot.position, positionPrecision, out lastSerializedPosition);
|
||||
if (syncScale) Compression.ScaleToLong(snapshot.scale, scalePrecision, out lastSerializedScale);
|
||||
|
||||
// set 'last'
|
||||
last = snapshot;
|
||||
}
|
||||
|
||||
public override void OnDeserialize(NetworkReader reader, bool initialState)
|
||||
{
|
||||
Vector3? position = null;
|
||||
Quaternion? rotation = null;
|
||||
Vector3? scale = null;
|
||||
|
||||
// initial
|
||||
if (initialState)
|
||||
{
|
||||
if (syncPosition) position = reader.ReadVector3();
|
||||
if (syncRotation)
|
||||
{
|
||||
// (optional) smallest three compression for now. no delta.
|
||||
if (compressRotation)
|
||||
rotation = Compression.DecompressQuaternion(reader.ReadUInt());
|
||||
else
|
||||
rotation = reader.ReadQuaternion();
|
||||
}
|
||||
if (syncScale) scale = reader.ReadVector3();
|
||||
}
|
||||
// delta
|
||||
else
|
||||
{
|
||||
// varint -> delta -> quantize
|
||||
if (syncPosition)
|
||||
{
|
||||
Vector3Long quantized = DeltaCompression.Decompress(reader, lastDeserializedPosition);
|
||||
position = Compression.ScaleToFloat(quantized, positionPrecision);
|
||||
}
|
||||
if (syncRotation)
|
||||
{
|
||||
// (optional) smallest three compression for now. no delta.
|
||||
if (compressRotation)
|
||||
rotation = Compression.DecompressQuaternion(reader.ReadUInt());
|
||||
else
|
||||
rotation = reader.ReadQuaternion();
|
||||
}
|
||||
if (syncScale)
|
||||
{
|
||||
Vector3Long quantized = DeltaCompression.Decompress(reader, lastDeserializedScale);
|
||||
scale = Compression.ScaleToFloat(quantized, scalePrecision);
|
||||
}
|
||||
}
|
||||
|
||||
// handle depending on server / client / host.
|
||||
// server has priority for host mode.
|
||||
if (isServer) OnClientToServerSync(position, rotation, scale);
|
||||
else if (isClient) OnServerToClientSync(position, rotation, scale);
|
||||
|
||||
// save deserialized as 'last' for next delta compression
|
||||
if (syncPosition) Compression.ScaleToLong(position.Value, positionPrecision, out lastDeserializedPosition);
|
||||
if (syncScale) Compression.ScaleToLong(scale.Value, scalePrecision, out lastDeserializedScale);
|
||||
}
|
||||
|
||||
// sync ////////////////////////////////////////////////////////////////
|
||||
|
||||
// local authority client sends sync message to server for broadcasting
|
||||
protected virtual void OnClientToServerSync(Vector3? position, Quaternion? rotation, Vector3? scale)
|
||||
{
|
||||
// only apply if in client authority mode
|
||||
if (syncDirection != SyncDirection.ClientToServer) return;
|
||||
|
||||
// protect against ever growing buffer size attacks
|
||||
if (serverSnapshots.Count >= connectionToClient.snapshotBufferSizeLimit) return;
|
||||
|
||||
// 'only sync on change' needs a correction on every new move sequence.
|
||||
if (onlySyncOnChange &&
|
||||
NeedsCorrection(serverSnapshots, connectionToClient.remoteTimeStamp, NetworkServer.sendInterval * sendIntervalMultiplier, onlySyncOnChangeCorrectionMultiplier))
|
||||
{
|
||||
RewriteHistory(
|
||||
serverSnapshots,
|
||||
connectionToClient.remoteTimeStamp,
|
||||
NetworkTime.localTime, // arrival remote timestamp. NOT remote timeline.
|
||||
NetworkServer.sendInterval * sendIntervalMultiplier, // Unity 2019 doesn't have timeAsDouble yet
|
||||
GetPosition(),
|
||||
GetRotation(),
|
||||
GetScale());
|
||||
}
|
||||
|
||||
// add a small timeline offset to account for decoupled arrival of
|
||||
// NetworkTime and NetworkTransform snapshots.
|
||||
// needs to be sendInterval. half sendInterval doesn't solve it.
|
||||
// https://github.com/MirrorNetworking/Mirror/issues/3427
|
||||
// remove this after LocalWorldState.
|
||||
AddSnapshot(serverSnapshots, connectionToClient.remoteTimeStamp + timeStampAdjustment + offset, position, rotation, scale);
|
||||
}
|
||||
|
||||
// server broadcasts sync message to all clients
|
||||
protected virtual void OnServerToClientSync(Vector3? position, Quaternion? rotation, Vector3? scale)
|
||||
{
|
||||
// don't apply for local player with authority
|
||||
if (IsClientWithAuthority) return;
|
||||
|
||||
// 'only sync on change' needs a correction on every new move sequence.
|
||||
if (onlySyncOnChange &&
|
||||
NeedsCorrection(clientSnapshots, NetworkClient.connection.remoteTimeStamp, NetworkClient.sendInterval * sendIntervalMultiplier, onlySyncOnChangeCorrectionMultiplier))
|
||||
{
|
||||
RewriteHistory(
|
||||
clientSnapshots,
|
||||
NetworkClient.connection.remoteTimeStamp, // arrival remote timestamp. NOT remote timeline.
|
||||
NetworkTime.localTime, // Unity 2019 doesn't have timeAsDouble yet
|
||||
NetworkClient.sendInterval * sendIntervalMultiplier,
|
||||
GetPosition(),
|
||||
GetRotation(),
|
||||
GetScale());
|
||||
}
|
||||
|
||||
// add a small timeline offset to account for decoupled arrival of
|
||||
// NetworkTime and NetworkTransform snapshots.
|
||||
// needs to be sendInterval. half sendInterval doesn't solve it.
|
||||
// https://github.com/MirrorNetworking/Mirror/issues/3427
|
||||
// remove this after LocalWorldState.
|
||||
AddSnapshot(clientSnapshots, NetworkClient.connection.remoteTimeStamp + timeStampAdjustment + offset, position, rotation, scale);
|
||||
}
|
||||
|
||||
// only sync on change /////////////////////////////////////////////////
|
||||
// snap interp. needs a continous flow of packets.
|
||||
// 'only sync on change' interrupts it while not changed.
|
||||
// once it restarts, snap interp. will interp from the last old position.
|
||||
// this will cause very noticeable stutter for the first move each time.
|
||||
// the fix is quite simple.
|
||||
|
||||
// 1. detect if the remaining snapshot is too old from a past move.
|
||||
static bool NeedsCorrection(
|
||||
SortedList<double, TransformSnapshot> snapshots,
|
||||
double remoteTimestamp,
|
||||
double bufferTime,
|
||||
double toleranceMultiplier) =>
|
||||
snapshots.Count == 1 &&
|
||||
remoteTimestamp - snapshots.Keys[0] >= bufferTime * toleranceMultiplier;
|
||||
|
||||
// 2. insert a fake snapshot at current position,
|
||||
// exactly one 'sendInterval' behind the newly received one.
|
||||
static void RewriteHistory(
|
||||
SortedList<double, TransformSnapshot> snapshots,
|
||||
// timestamp of packet arrival, not interpolated remote time!
|
||||
double remoteTimeStamp,
|
||||
double localTime,
|
||||
double sendInterval,
|
||||
Vector3 position,
|
||||
Quaternion rotation,
|
||||
Vector3 scale)
|
||||
{
|
||||
// clear the previous snapshot
|
||||
snapshots.Clear();
|
||||
|
||||
// insert a fake one at where we used to be,
|
||||
// 'sendInterval' behind the new one.
|
||||
SnapshotInterpolation.InsertIfNotExists(
|
||||
snapshots,
|
||||
NetworkClient.snapshotSettings.bufferLimit,
|
||||
new TransformSnapshot(
|
||||
remoteTimeStamp - sendInterval, // arrival remote timestamp. NOT remote time.
|
||||
localTime - sendInterval, // Unity 2019 doesn't have timeAsDouble yet
|
||||
position,
|
||||
rotation,
|
||||
scale
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// reset state for next session.
|
||||
// do not ever call this during a session (i.e. after teleport).
|
||||
// calling this will break delta compression.
|
||||
public override void ResetState()
|
||||
{
|
||||
base.ResetState();
|
||||
|
||||
// reset delta
|
||||
lastSerializedPosition = Vector3Long.zero;
|
||||
lastDeserializedPosition = Vector3Long.zero;
|
||||
|
||||
lastSerializedScale = Vector3Long.zero;
|
||||
lastDeserializedScale = Vector3Long.zero;
|
||||
|
||||
// reset 'last' for delta too
|
||||
last = new TransformSnapshot(0, 0, Vector3.zero, Quaternion.identity, Vector3.zero);
|
||||
}
|
||||
}
|
||||
}
|
||||
// NetworkTransform V3 (reliable) by mischa (2022-10)
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
[AddComponentMenu("Network/Network Transform (Reliable)")]
|
||||
public class NetworkTransformReliable : NetworkTransformBase
|
||||
{
|
||||
uint sendIntervalCounter = 0;
|
||||
double lastSendIntervalTime = double.MinValue;
|
||||
TransformSnapshot? pendingSnapshot;
|
||||
|
||||
[Header("Additional Settings")]
|
||||
[Tooltip("If we only sync on change, then we need to correct old snapshots if more time than sendInterval * multiplier has elapsed.\n\nOtherwise the first move will always start interpolating from the last move sequence's time, which will make it stutter when starting every time.")]
|
||||
public float onlySyncOnChangeCorrectionMultiplier = 2;
|
||||
public bool useFixedUpdate;
|
||||
|
||||
[Header("Rotation")]
|
||||
[Tooltip("Sensitivity of changes needed before an updated state is sent over the network")]
|
||||
public float rotationSensitivity = 0.01f;
|
||||
|
||||
// delta compression is capable of detecting byte-level changes.
|
||||
// if we scale float position to bytes,
|
||||
// then small movements will only change one byte.
|
||||
// this gives optimal bandwidth.
|
||||
// benchmark with 0.01 precision: 130 KB/s => 60 KB/s
|
||||
// benchmark with 0.1 precision: 130 KB/s => 30 KB/s
|
||||
[Header("Precision")]
|
||||
[Tooltip("Position is rounded in order to drastically minimize bandwidth.\n\nFor example, a precision of 0.01 rounds to a centimeter. In other words, sub-centimeter movements aren't synced until they eventually exceeded an actual centimeter.\n\nDepending on how important the object is, a precision of 0.01-0.10 (1-10 cm) is recommended.\n\nFor example, even a 1cm precision combined with delta compression cuts the Benchmark demo's bandwidth in half, compared to sending every tiny change.")]
|
||||
[Range(0.00_01f, 1f)] // disallow 0 division. 1mm to 1m precision is enough range.
|
||||
public float positionPrecision = 0.01f; // 1 cm
|
||||
[Range(0.00_01f, 1f)] // disallow 0 division. 1mm to 1m precision is enough range.
|
||||
public float scalePrecision = 0.01f; // 1 cm
|
||||
|
||||
// delta compression needs to remember 'last' to compress against
|
||||
protected Vector3Long lastSerializedPosition = Vector3Long.zero;
|
||||
protected Vector3Long lastDeserializedPosition = Vector3Long.zero;
|
||||
|
||||
protected Vector3Long lastSerializedScale = Vector3Long.zero;
|
||||
protected Vector3Long lastDeserializedScale = Vector3Long.zero;
|
||||
|
||||
// Used to store last sent snapshots
|
||||
protected TransformSnapshot last;
|
||||
|
||||
// update //////////////////////////////////////////////////////////////
|
||||
void Update()
|
||||
{
|
||||
// if server then always sync to others.
|
||||
if (isServer) UpdateServer();
|
||||
// 'else if' because host mode shouldn't send anything to server.
|
||||
// it is the server. don't overwrite anything there.
|
||||
else if (isClient) UpdateClient();
|
||||
}
|
||||
|
||||
void FixedUpdate()
|
||||
{
|
||||
if (!useFixedUpdate) return;
|
||||
|
||||
if (pendingSnapshot.HasValue && !IsClientWithAuthority)
|
||||
{
|
||||
// Apply via base method, but in FixedUpdate
|
||||
Apply(pendingSnapshot.Value, pendingSnapshot.Value);
|
||||
pendingSnapshot = null;
|
||||
}
|
||||
}
|
||||
|
||||
void LateUpdate()
|
||||
{
|
||||
// set dirty to trigger OnSerialize. either always, or only if changed.
|
||||
// It has to be checked in LateUpdate() for onlySyncOnChange to avoid
|
||||
// the possibility of Update() running first before the object's movement
|
||||
// script's Update(), which then causes NT to send every alternate frame
|
||||
// instead.
|
||||
if (isServer || (IsClientWithAuthority && NetworkClient.ready))
|
||||
{
|
||||
if (sendIntervalCounter == sendIntervalMultiplier && (!onlySyncOnChange || Changed(Construct())))
|
||||
SetDirty();
|
||||
|
||||
CheckLastSendTime();
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void UpdateServer()
|
||||
{
|
||||
// apply buffered snapshots IF client authority
|
||||
// -> in server authority, server moves the object
|
||||
// so no need to apply any snapshots there.
|
||||
// -> don't apply for host mode player objects either, even if in
|
||||
// client authority mode. if it doesn't go over the network,
|
||||
// then we don't need to do anything.
|
||||
// -> connectionToClient is briefly null after scene changes:
|
||||
// https://github.com/MirrorNetworking/Mirror/issues/3329
|
||||
if (syncDirection == SyncDirection.ClientToServer &&
|
||||
connectionToClient != null &&
|
||||
!isOwned)
|
||||
{
|
||||
if (serverSnapshots.Count > 0)
|
||||
{
|
||||
// step the transform interpolation without touching time.
|
||||
// NetworkClient is responsible for time globally.
|
||||
SnapshotInterpolation.StepInterpolation(
|
||||
serverSnapshots,
|
||||
connectionToClient.remoteTimeline,
|
||||
out TransformSnapshot from,
|
||||
out TransformSnapshot to,
|
||||
out double t);
|
||||
|
||||
// interpolate & apply
|
||||
TransformSnapshot computed = TransformSnapshot.Interpolate(from, to, t);
|
||||
Apply(computed, to);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void UpdateClient()
|
||||
{
|
||||
if (useFixedUpdate)
|
||||
{
|
||||
if (!IsClientWithAuthority && clientSnapshots.Count > 0)
|
||||
{
|
||||
SnapshotInterpolation.StepInterpolation(
|
||||
clientSnapshots,
|
||||
NetworkTime.time,
|
||||
out TransformSnapshot from,
|
||||
out TransformSnapshot to,
|
||||
out double t
|
||||
);
|
||||
pendingSnapshot = TransformSnapshot.Interpolate(from, to, t);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// client authority, and local player (= allowed to move myself)?
|
||||
if (!IsClientWithAuthority)
|
||||
{
|
||||
// only while we have snapshots
|
||||
if (clientSnapshots.Count > 0)
|
||||
{
|
||||
// step the interpolation without touching time.
|
||||
// NetworkClient is responsible for time globally.
|
||||
SnapshotInterpolation.StepInterpolation(
|
||||
clientSnapshots,
|
||||
NetworkTime.time, // == NetworkClient.localTimeline from snapshot interpolation
|
||||
out TransformSnapshot from,
|
||||
out TransformSnapshot to,
|
||||
out double t);
|
||||
|
||||
// interpolate & apply
|
||||
TransformSnapshot computed = TransformSnapshot.Interpolate(from, to, t);
|
||||
Apply(computed, to);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void CheckLastSendTime()
|
||||
{
|
||||
// timeAsDouble not available in older Unity versions.
|
||||
if (AccurateInterval.Elapsed(NetworkTime.localTime, NetworkServer.sendInterval, ref lastSendIntervalTime))
|
||||
{
|
||||
if (sendIntervalCounter == sendIntervalMultiplier)
|
||||
sendIntervalCounter = 0;
|
||||
sendIntervalCounter++;
|
||||
}
|
||||
}
|
||||
|
||||
// check if position / rotation / scale changed since last sync
|
||||
protected virtual bool Changed(TransformSnapshot current) =>
|
||||
// position is quantized and delta compressed.
|
||||
// only consider it changed if the quantized representation is changed.
|
||||
// careful: don't use 'serialized / deserialized last'. as it depends on sync mode etc.
|
||||
QuantizedChanged(last.position, current.position, positionPrecision) ||
|
||||
// rotation isn't quantized / delta compressed.
|
||||
// check with sensitivity.
|
||||
Quaternion.Angle(last.rotation, current.rotation) > rotationSensitivity ||
|
||||
// scale is quantized and delta compressed.
|
||||
// only consider it changed if the quantized representation is changed.
|
||||
// careful: don't use 'serialized / deserialized last'. as it depends on sync mode etc.
|
||||
QuantizedChanged(last.scale, current.scale, scalePrecision);
|
||||
|
||||
// helper function to compare quantized representations of a Vector3
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected bool QuantizedChanged(Vector3 u, Vector3 v, float precision)
|
||||
{
|
||||
Compression.ScaleToLong(u, precision, out Vector3Long uQuantized);
|
||||
Compression.ScaleToLong(v, precision, out Vector3Long vQuantized);
|
||||
return uQuantized != vQuantized;
|
||||
}
|
||||
|
||||
// NT may be used on client/server/host to Owner/Observers with
|
||||
// ServerToClient or ClientToServer.
|
||||
// however, OnSerialize should always delta against last.
|
||||
public override void OnSerialize(NetworkWriter writer, bool initialState)
|
||||
{
|
||||
// get current snapshot for broadcasting.
|
||||
TransformSnapshot snapshot = Construct();
|
||||
|
||||
// ClientToServer optimization:
|
||||
// for interpolated client owned identities,
|
||||
// always broadcast the latest known snapshot so other clients can
|
||||
// interpolate immediately instead of catching up too
|
||||
|
||||
// TODO dirty mask? [compression is very good w/o it already]
|
||||
// each vector's component is delta compressed.
|
||||
// an unchanged component would still require 1 byte.
|
||||
// let's use a dirty bit mask to filter those out as well.
|
||||
|
||||
// initial
|
||||
if (initialState)
|
||||
{
|
||||
// If there is a last serialized snapshot, we use it.
|
||||
// This prevents the new client getting a snapshot that is different
|
||||
// from what the older clients last got. If this happens, and on the next
|
||||
// regular serialisation the delta compression will get wrong values.
|
||||
// Notes:
|
||||
// 1. Interestingly only the older clients have it wrong, because at the end
|
||||
// of this function, last = snapshot which is the initial state's snapshot
|
||||
// 2. Regular NTR gets by this bug because it sends every frame anyway so initialstate
|
||||
// snapshot constructed would have been the same as the last anyway.
|
||||
if (last.remoteTime > 0) snapshot = last;
|
||||
if (syncPosition) writer.WriteVector3(snapshot.position);
|
||||
if (syncRotation)
|
||||
{
|
||||
// (optional) smallest three compression for now. no delta.
|
||||
if (compressRotation)
|
||||
writer.WriteUInt(Compression.CompressQuaternion(snapshot.rotation));
|
||||
else
|
||||
writer.WriteQuaternion(snapshot.rotation);
|
||||
}
|
||||
if (syncScale) writer.WriteVector3(snapshot.scale);
|
||||
}
|
||||
// delta
|
||||
else
|
||||
{
|
||||
// int before = writer.Position;
|
||||
|
||||
if (syncPosition)
|
||||
{
|
||||
// quantize -> delta -> varint
|
||||
Compression.ScaleToLong(snapshot.position, positionPrecision, out Vector3Long quantized);
|
||||
DeltaCompression.Compress(writer, lastSerializedPosition, quantized);
|
||||
}
|
||||
if (syncRotation)
|
||||
{
|
||||
// (optional) smallest three compression for now. no delta.
|
||||
if (compressRotation)
|
||||
writer.WriteUInt(Compression.CompressQuaternion(snapshot.rotation));
|
||||
else
|
||||
writer.WriteQuaternion(snapshot.rotation);
|
||||
}
|
||||
if (syncScale)
|
||||
{
|
||||
// quantize -> delta -> varint
|
||||
Compression.ScaleToLong(snapshot.scale, scalePrecision, out Vector3Long quantized);
|
||||
DeltaCompression.Compress(writer, lastSerializedScale, quantized);
|
||||
}
|
||||
}
|
||||
|
||||
// save serialized as 'last' for next delta compression
|
||||
if (syncPosition) Compression.ScaleToLong(snapshot.position, positionPrecision, out lastSerializedPosition);
|
||||
if (syncScale) Compression.ScaleToLong(snapshot.scale, scalePrecision, out lastSerializedScale);
|
||||
|
||||
// set 'last'
|
||||
last = snapshot;
|
||||
}
|
||||
|
||||
public override void OnDeserialize(NetworkReader reader, bool initialState)
|
||||
{
|
||||
Vector3? position = null;
|
||||
Quaternion? rotation = null;
|
||||
Vector3? scale = null;
|
||||
|
||||
// initial
|
||||
if (initialState)
|
||||
{
|
||||
if (syncPosition) position = reader.ReadVector3();
|
||||
if (syncRotation)
|
||||
{
|
||||
// (optional) smallest three compression for now. no delta.
|
||||
if (compressRotation)
|
||||
rotation = Compression.DecompressQuaternion(reader.ReadUInt());
|
||||
else
|
||||
rotation = reader.ReadQuaternion();
|
||||
}
|
||||
if (syncScale) scale = reader.ReadVector3();
|
||||
}
|
||||
// delta
|
||||
else
|
||||
{
|
||||
// varint -> delta -> quantize
|
||||
if (syncPosition)
|
||||
{
|
||||
Vector3Long quantized = DeltaCompression.Decompress(reader, lastDeserializedPosition);
|
||||
position = Compression.ScaleToFloat(quantized, positionPrecision);
|
||||
}
|
||||
if (syncRotation)
|
||||
{
|
||||
// (optional) smallest three compression for now. no delta.
|
||||
if (compressRotation)
|
||||
rotation = Compression.DecompressQuaternion(reader.ReadUInt());
|
||||
else
|
||||
rotation = reader.ReadQuaternion();
|
||||
}
|
||||
if (syncScale)
|
||||
{
|
||||
Vector3Long quantized = DeltaCompression.Decompress(reader, lastDeserializedScale);
|
||||
scale = Compression.ScaleToFloat(quantized, scalePrecision);
|
||||
}
|
||||
}
|
||||
|
||||
// handle depending on server / client / host.
|
||||
// server has priority for host mode.
|
||||
if (isServer) OnClientToServerSync(position, rotation, scale);
|
||||
else if (isClient) OnServerToClientSync(position, rotation, scale);
|
||||
|
||||
// save deserialized as 'last' for next delta compression
|
||||
if (syncPosition) Compression.ScaleToLong(position.Value, positionPrecision, out lastDeserializedPosition);
|
||||
if (syncScale) Compression.ScaleToLong(scale.Value, scalePrecision, out lastDeserializedScale);
|
||||
}
|
||||
|
||||
// sync ////////////////////////////////////////////////////////////////
|
||||
|
||||
// local authority client sends sync message to server for broadcasting
|
||||
protected virtual void OnClientToServerSync(Vector3? position, Quaternion? rotation, Vector3? scale)
|
||||
{
|
||||
// only apply if in client authority mode
|
||||
if (syncDirection != SyncDirection.ClientToServer) return;
|
||||
|
||||
// protect against ever growing buffer size attacks
|
||||
if (serverSnapshots.Count >= connectionToClient.snapshotBufferSizeLimit) return;
|
||||
|
||||
// 'only sync on change' needs a correction on every new move sequence.
|
||||
if (onlySyncOnChange &&
|
||||
NeedsCorrection(serverSnapshots, connectionToClient.remoteTimeStamp, NetworkServer.sendInterval * sendIntervalMultiplier, onlySyncOnChangeCorrectionMultiplier))
|
||||
{
|
||||
RewriteHistory(
|
||||
serverSnapshots,
|
||||
connectionToClient.remoteTimeStamp,
|
||||
NetworkTime.localTime, // arrival remote timestamp. NOT remote timeline.
|
||||
NetworkServer.sendInterval * sendIntervalMultiplier, // Unity 2019 doesn't have timeAsDouble yet
|
||||
GetPosition(),
|
||||
GetRotation(),
|
||||
GetScale());
|
||||
}
|
||||
|
||||
// add a small timeline offset to account for decoupled arrival of
|
||||
// NetworkTime and NetworkTransform snapshots.
|
||||
// needs to be sendInterval. half sendInterval doesn't solve it.
|
||||
// https://github.com/MirrorNetworking/Mirror/issues/3427
|
||||
// remove this after LocalWorldState.
|
||||
AddSnapshot(serverSnapshots, connectionToClient.remoteTimeStamp + timeStampAdjustment + offset, position, rotation, scale);
|
||||
}
|
||||
|
||||
// server broadcasts sync message to all clients
|
||||
protected virtual void OnServerToClientSync(Vector3? position, Quaternion? rotation, Vector3? scale)
|
||||
{
|
||||
// don't apply for local player with authority
|
||||
if (IsClientWithAuthority) return;
|
||||
|
||||
// 'only sync on change' needs a correction on every new move sequence.
|
||||
if (onlySyncOnChange &&
|
||||
NeedsCorrection(clientSnapshots, NetworkClient.connection.remoteTimeStamp, NetworkClient.sendInterval * sendIntervalMultiplier, onlySyncOnChangeCorrectionMultiplier))
|
||||
{
|
||||
RewriteHistory(
|
||||
clientSnapshots,
|
||||
NetworkClient.connection.remoteTimeStamp, // arrival remote timestamp. NOT remote timeline.
|
||||
NetworkTime.localTime, // Unity 2019 doesn't have timeAsDouble yet
|
||||
NetworkClient.sendInterval * sendIntervalMultiplier,
|
||||
GetPosition(),
|
||||
GetRotation(),
|
||||
GetScale());
|
||||
}
|
||||
|
||||
// add a small timeline offset to account for decoupled arrival of
|
||||
// NetworkTime and NetworkTransform snapshots.
|
||||
// needs to be sendInterval. half sendInterval doesn't solve it.
|
||||
// https://github.com/MirrorNetworking/Mirror/issues/3427
|
||||
// remove this after LocalWorldState.
|
||||
AddSnapshot(clientSnapshots, NetworkClient.connection.remoteTimeStamp + timeStampAdjustment + offset, position, rotation, scale);
|
||||
}
|
||||
|
||||
// only sync on change /////////////////////////////////////////////////
|
||||
// snap interp. needs a continous flow of packets.
|
||||
// 'only sync on change' interrupts it while not changed.
|
||||
// once it restarts, snap interp. will interp from the last old position.
|
||||
// this will cause very noticeable stutter for the first move each time.
|
||||
// the fix is quite simple.
|
||||
|
||||
// 1. detect if the remaining snapshot is too old from a past move.
|
||||
static bool NeedsCorrection(
|
||||
SortedList<double, TransformSnapshot> snapshots,
|
||||
double remoteTimestamp,
|
||||
double bufferTime,
|
||||
double toleranceMultiplier) =>
|
||||
snapshots.Count == 1 &&
|
||||
remoteTimestamp - snapshots.Keys[0] >= bufferTime * toleranceMultiplier;
|
||||
|
||||
// 2. insert a fake snapshot at current position,
|
||||
// exactly one 'sendInterval' behind the newly received one.
|
||||
static void RewriteHistory(
|
||||
SortedList<double, TransformSnapshot> snapshots,
|
||||
// timestamp of packet arrival, not interpolated remote time!
|
||||
double remoteTimeStamp,
|
||||
double localTime,
|
||||
double sendInterval,
|
||||
Vector3 position,
|
||||
Quaternion rotation,
|
||||
Vector3 scale)
|
||||
{
|
||||
// clear the previous snapshot
|
||||
snapshots.Clear();
|
||||
|
||||
// insert a fake one at where we used to be,
|
||||
// 'sendInterval' behind the new one.
|
||||
SnapshotInterpolation.InsertIfNotExists(
|
||||
snapshots,
|
||||
NetworkClient.snapshotSettings.bufferLimit,
|
||||
new TransformSnapshot(
|
||||
remoteTimeStamp - sendInterval, // arrival remote timestamp. NOT remote time.
|
||||
localTime - sendInterval, // Unity 2019 doesn't have timeAsDouble yet
|
||||
position,
|
||||
rotation,
|
||||
scale
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// reset state for next session.
|
||||
// do not ever call this during a session (i.e. after teleport).
|
||||
// calling this will break delta compression.
|
||||
public override void ResetState()
|
||||
{
|
||||
base.ResetState();
|
||||
|
||||
// reset delta
|
||||
lastSerializedPosition = Vector3Long.zero;
|
||||
lastDeserializedPosition = Vector3Long.zero;
|
||||
|
||||
lastSerializedScale = Vector3Long.zero;
|
||||
lastDeserializedScale = Vector3Long.zero;
|
||||
|
||||
// reset 'last' for delta too
|
||||
last = new TransformSnapshot(0, 0, Vector3.zero, Quaternion.identity, Vector3.zero);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8ff3ba0becae47b8b9381191598957c8
|
||||
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/NetworkTransform/NetworkTransformReliable.cs
|
||||
uploadId: 736421
|
||||
fileFormatVersion: 2
|
||||
guid: 8ff3ba0becae47b8b9381191598957c8
|
||||
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/NetworkTransform/NetworkTransformReliable.cs
|
||||
uploadId: 736421
|
||||
|
||||
@@ -1,462 +1,462 @@
|
||||
// NetworkTransform V2 by mischa (2021-07)
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
[AddComponentMenu("Network/Network Transform (Unreliable)")]
|
||||
public class NetworkTransformUnreliable : NetworkTransformBase
|
||||
{
|
||||
uint sendIntervalCounter = 0;
|
||||
double lastSendIntervalTime = double.MinValue;
|
||||
TransformSnapshot? pendingSnapshot;
|
||||
|
||||
[Header("Additional Settings")]
|
||||
// Testing under really bad network conditions, 2%-5% packet loss and 250-1200ms ping, 5 proved to eliminate any twitching, however this should not be the default as it is a rare case Developers may want to cover.
|
||||
[Tooltip("How much time, as a multiple of send interval, has passed before clearing buffers.\nA larger buffer means more delay, but results in smoother movement.\nExample: 1 for faster responses minimal smoothing, 5 covers bad pings but has noticable delay, 3 is recommended for balanced results.")]
|
||||
public float bufferResetMultiplier = 3;
|
||||
public bool useFixedUpdate;
|
||||
|
||||
[Header("Sensitivity"), Tooltip("Sensitivity of changes needed before an updated state is sent over the network")]
|
||||
public float positionSensitivity = 0.01f;
|
||||
public float rotationSensitivity = 0.01f;
|
||||
public float scaleSensitivity = 0.01f;
|
||||
|
||||
// Used to store last sent snapshots
|
||||
protected TransformSnapshot lastSnapshot;
|
||||
protected Changed cachedChangedComparison;
|
||||
protected bool hasSentUnchangedPosition;
|
||||
|
||||
// update //////////////////////////////////////////////////////////////
|
||||
// Update applies interpolation
|
||||
void Update()
|
||||
{
|
||||
if (isServer) UpdateServerInterpolation();
|
||||
// for all other clients (and for local player if !authority),
|
||||
// we need to apply snapshots from the buffer.
|
||||
// 'else if' because host mode shouldn't interpolate client
|
||||
else if (isClient && !IsClientWithAuthority) UpdateClientInterpolation();
|
||||
}
|
||||
|
||||
void FixedUpdate()
|
||||
{
|
||||
if (!useFixedUpdate) return;
|
||||
|
||||
if (pendingSnapshot.HasValue)
|
||||
{
|
||||
Apply(pendingSnapshot.Value, pendingSnapshot.Value);
|
||||
pendingSnapshot = null;
|
||||
}
|
||||
}
|
||||
|
||||
// LateUpdate broadcasts.
|
||||
// movement scripts may change positions in Update.
|
||||
// use LateUpdate to ensure changes are detected in the same frame.
|
||||
// otherwise this may run before user update, delaying detection until next frame.
|
||||
// this could cause visible jitter.
|
||||
void LateUpdate()
|
||||
{
|
||||
// if server then always sync to others.
|
||||
if (isServer) UpdateServerBroadcast();
|
||||
// client authority, and local player (= allowed to move myself)?
|
||||
// 'else if' because host mode shouldn't send anything to server.
|
||||
// it is the server. don't overwrite anything there.
|
||||
else if (isClient && IsClientWithAuthority) UpdateClientBroadcast();
|
||||
}
|
||||
|
||||
protected virtual void CheckLastSendTime()
|
||||
{
|
||||
// We check interval every frame, and then send if interval is reached.
|
||||
// So by the time sendIntervalCounter == sendIntervalMultiplier, data is sent,
|
||||
// thus we reset the counter here.
|
||||
// This fixes previous issue of, if sendIntervalMultiplier = 1, we send every frame,
|
||||
// because intervalCounter is always = 1 in the previous version.
|
||||
|
||||
// Changing == to >= https://github.com/MirrorNetworking/Mirror/issues/3571
|
||||
|
||||
if (sendIntervalCounter >= sendIntervalMultiplier)
|
||||
sendIntervalCounter = 0;
|
||||
|
||||
// timeAsDouble not available in older Unity versions.
|
||||
if (AccurateInterval.Elapsed(NetworkTime.localTime, NetworkServer.sendInterval, ref lastSendIntervalTime))
|
||||
sendIntervalCounter++;
|
||||
}
|
||||
|
||||
void UpdateServerBroadcast()
|
||||
{
|
||||
// broadcast to all clients each 'sendInterval'
|
||||
// (client with authority will drop the rpc)
|
||||
// NetworkTime.localTime for double precision until Unity has it too
|
||||
//
|
||||
// IMPORTANT:
|
||||
// snapshot interpolation requires constant sending.
|
||||
// DO NOT only send if position changed. for example:
|
||||
// ---
|
||||
// * client sends first position at t=0
|
||||
// * ... 10s later ...
|
||||
// * client moves again, sends second position at t=10
|
||||
// ---
|
||||
// * server gets first position at t=0
|
||||
// * server gets second position at t=10
|
||||
// * server moves from first to second within a time of 10s
|
||||
// => would be a super slow move, instead of a wait & move.
|
||||
//
|
||||
// IMPORTANT:
|
||||
// DO NOT send nulls if not changed 'since last send' either. we
|
||||
// send unreliable and don't know which 'last send' the other end
|
||||
// received successfully.
|
||||
//
|
||||
// Checks to ensure server only sends snapshots if object is
|
||||
// on server authority(!clientAuthority) mode because on client
|
||||
// authority mode snapshots are broadcasted right after the authoritative
|
||||
// client updates server in the command function(see above), OR,
|
||||
// since host does not send anything to update the server, any client
|
||||
// authoritative movement done by the host will have to be broadcasted
|
||||
// here by checking IsClientWithAuthority.
|
||||
// TODO send same time that NetworkServer sends time snapshot?
|
||||
CheckLastSendTime();
|
||||
|
||||
if (sendIntervalCounter == sendIntervalMultiplier && // same interval as time interpolation!
|
||||
(syncDirection == SyncDirection.ServerToClient || IsClientWithAuthority))
|
||||
{
|
||||
// send snapshot without timestamp.
|
||||
// receiver gets it from batch timestamp to save bandwidth.
|
||||
TransformSnapshot snapshot = Construct();
|
||||
|
||||
cachedChangedComparison = CompareChangedSnapshots(snapshot);
|
||||
|
||||
if ((cachedChangedComparison == Changed.None || cachedChangedComparison == Changed.CompressRot) && hasSentUnchangedPosition && onlySyncOnChange) { return; }
|
||||
|
||||
SyncData syncData = new SyncData(cachedChangedComparison, snapshot);
|
||||
|
||||
RpcServerToClientSync(syncData);
|
||||
|
||||
if (cachedChangedComparison == Changed.None || cachedChangedComparison == Changed.CompressRot)
|
||||
{
|
||||
hasSentUnchangedPosition = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
hasSentUnchangedPosition = false;
|
||||
UpdateLastSentSnapshot(cachedChangedComparison, snapshot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateServerInterpolation()
|
||||
{
|
||||
// apply buffered snapshots IF client authority
|
||||
// -> in server authority, server moves the object
|
||||
// so no need to apply any snapshots there.
|
||||
// -> don't apply for host mode player objects either, even if in
|
||||
// client authority mode. if it doesn't go over the network,
|
||||
// then we don't need to do anything.
|
||||
// -> connectionToClient is briefly null after scene changes:
|
||||
// https://github.com/MirrorNetworking/Mirror/issues/3329
|
||||
if (syncDirection == SyncDirection.ClientToServer &&
|
||||
connectionToClient != null &&
|
||||
!isOwned)
|
||||
{
|
||||
if (serverSnapshots.Count == 0) return;
|
||||
|
||||
// step the transform interpolation without touching time.
|
||||
// NetworkClient is responsible for time globally.
|
||||
SnapshotInterpolation.StepInterpolation(
|
||||
serverSnapshots,
|
||||
connectionToClient.remoteTimeline,
|
||||
out TransformSnapshot from,
|
||||
out TransformSnapshot to,
|
||||
out double t);
|
||||
|
||||
// interpolate & apply
|
||||
TransformSnapshot computed = TransformSnapshot.Interpolate(from, to, t);
|
||||
if (useFixedUpdate)
|
||||
pendingSnapshot = computed;
|
||||
else
|
||||
Apply(computed, to);
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateClientBroadcast()
|
||||
{
|
||||
// https://github.com/vis2k/Mirror/pull/2992/
|
||||
if (!NetworkClient.ready) return;
|
||||
|
||||
// send to server each 'sendInterval'
|
||||
// NetworkTime.localTime for double precision until Unity has it too
|
||||
//
|
||||
// IMPORTANT:
|
||||
// snapshot interpolation requires constant sending.
|
||||
// DO NOT only send if position changed. for example:
|
||||
// ---
|
||||
// * client sends first position at t=0
|
||||
// * ... 10s later ...
|
||||
// * client moves again, sends second position at t=10
|
||||
// ---
|
||||
// * server gets first position at t=0
|
||||
// * server gets second position at t=10
|
||||
// * server moves from first to second within a time of 10s
|
||||
// => would be a super slow move, instead of a wait & move.
|
||||
//
|
||||
// IMPORTANT:
|
||||
// DO NOT send nulls if not changed 'since last send' either. we
|
||||
// send unreliable and don't know which 'last send' the other end
|
||||
// received successfully.
|
||||
CheckLastSendTime();
|
||||
if (sendIntervalCounter == sendIntervalMultiplier) // same interval as time interpolation!
|
||||
{
|
||||
// send snapshot without timestamp.
|
||||
// receiver gets it from batch timestamp to save bandwidth.
|
||||
TransformSnapshot snapshot = Construct();
|
||||
|
||||
cachedChangedComparison = CompareChangedSnapshots(snapshot);
|
||||
|
||||
if ((cachedChangedComparison == Changed.None || cachedChangedComparison == Changed.CompressRot) && hasSentUnchangedPosition && onlySyncOnChange) { return; }
|
||||
|
||||
SyncData syncData = new SyncData(cachedChangedComparison, snapshot);
|
||||
|
||||
CmdClientToServerSync(syncData);
|
||||
|
||||
if (cachedChangedComparison == Changed.None || cachedChangedComparison == Changed.CompressRot)
|
||||
{
|
||||
hasSentUnchangedPosition = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
hasSentUnchangedPosition = false;
|
||||
UpdateLastSentSnapshot(cachedChangedComparison, snapshot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateClientInterpolation()
|
||||
{
|
||||
// only while we have snapshots
|
||||
if (clientSnapshots.Count == 0) return;
|
||||
|
||||
// step the interpolation without touching time.
|
||||
// NetworkClient is responsible for time globally.
|
||||
SnapshotInterpolation.StepInterpolation(
|
||||
clientSnapshots,
|
||||
NetworkTime.time, // == NetworkClient.localTimeline from snapshot interpolation
|
||||
out TransformSnapshot from,
|
||||
out TransformSnapshot to,
|
||||
out double t);
|
||||
|
||||
// interpolate & apply
|
||||
TransformSnapshot computed = TransformSnapshot.Interpolate(from, to, t);
|
||||
if (useFixedUpdate)
|
||||
pendingSnapshot = computed;
|
||||
else
|
||||
Apply(computed, to);
|
||||
}
|
||||
|
||||
public override void OnSerialize(NetworkWriter writer, bool initialState)
|
||||
{
|
||||
// sync target component's position on spawn.
|
||||
// fixes https://github.com/vis2k/Mirror/pull/3051/
|
||||
// (Spawn message wouldn't sync NTChild positions either)
|
||||
if (initialState)
|
||||
{
|
||||
if (syncPosition) writer.WriteVector3(GetPosition());
|
||||
if (syncRotation) writer.WriteQuaternion(GetRotation());
|
||||
if (syncScale) writer.WriteVector3(GetScale());
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnDeserialize(NetworkReader reader, bool initialState)
|
||||
{
|
||||
// sync target component's position on spawn.
|
||||
// fixes https://github.com/vis2k/Mirror/pull/3051/
|
||||
// (Spawn message wouldn't sync NTChild positions either)
|
||||
if (initialState)
|
||||
{
|
||||
if (syncPosition) SetPosition(reader.ReadVector3());
|
||||
if (syncRotation) SetRotation(reader.ReadQuaternion());
|
||||
if (syncScale) SetScale(reader.ReadVector3());
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void UpdateLastSentSnapshot(Changed change, TransformSnapshot currentSnapshot)
|
||||
{
|
||||
if (change == Changed.None || change == Changed.CompressRot) return;
|
||||
|
||||
if ((change & Changed.PosX) > 0) lastSnapshot.position.x = currentSnapshot.position.x;
|
||||
if ((change & Changed.PosY) > 0) lastSnapshot.position.y = currentSnapshot.position.y;
|
||||
if ((change & Changed.PosZ) > 0) lastSnapshot.position.z = currentSnapshot.position.z;
|
||||
|
||||
if (compressRotation)
|
||||
{
|
||||
if ((change & Changed.Rot) > 0) lastSnapshot.rotation = currentSnapshot.rotation;
|
||||
}
|
||||
else
|
||||
{
|
||||
Vector3 newRotation;
|
||||
newRotation.x = (change & Changed.RotX) > 0 ? currentSnapshot.rotation.eulerAngles.x : lastSnapshot.rotation.eulerAngles.x;
|
||||
newRotation.y = (change & Changed.RotY) > 0 ? currentSnapshot.rotation.eulerAngles.y : lastSnapshot.rotation.eulerAngles.y;
|
||||
newRotation.z = (change & Changed.RotZ) > 0 ? currentSnapshot.rotation.eulerAngles.z : lastSnapshot.rotation.eulerAngles.z;
|
||||
|
||||
lastSnapshot.rotation = Quaternion.Euler(newRotation);
|
||||
}
|
||||
|
||||
if ((change & Changed.Scale) > 0) lastSnapshot.scale = currentSnapshot.scale;
|
||||
}
|
||||
|
||||
// Returns true if position, rotation AND scale are unchanged, within given sensitivity range.
|
||||
// Note the sensitivity comparison are different for pos, rot and scale.
|
||||
protected virtual Changed CompareChangedSnapshots(TransformSnapshot currentSnapshot)
|
||||
{
|
||||
Changed change = Changed.None;
|
||||
|
||||
if (syncPosition)
|
||||
{
|
||||
bool positionChanged = Vector3.SqrMagnitude(lastSnapshot.position - currentSnapshot.position) > positionSensitivity * positionSensitivity;
|
||||
if (positionChanged)
|
||||
{
|
||||
if (Mathf.Abs(lastSnapshot.position.x - currentSnapshot.position.x) > positionSensitivity) change |= Changed.PosX;
|
||||
if (Mathf.Abs(lastSnapshot.position.y - currentSnapshot.position.y) > positionSensitivity) change |= Changed.PosY;
|
||||
if (Mathf.Abs(lastSnapshot.position.z - currentSnapshot.position.z) > positionSensitivity) change |= Changed.PosZ;
|
||||
}
|
||||
}
|
||||
|
||||
if (syncRotation)
|
||||
{
|
||||
if (compressRotation)
|
||||
{
|
||||
bool rotationChanged = Quaternion.Angle(lastSnapshot.rotation, currentSnapshot.rotation) > rotationSensitivity;
|
||||
if (rotationChanged)
|
||||
{
|
||||
// Here we set all Rot enum flags, to tell us if there was a change in rotation
|
||||
// when using compression. If no change, we don't write the compressed Quat.
|
||||
change |= Changed.CompressRot;
|
||||
change |= Changed.Rot;
|
||||
}
|
||||
else
|
||||
{
|
||||
change |= Changed.CompressRot;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Mathf.Abs(lastSnapshot.rotation.eulerAngles.x - currentSnapshot.rotation.eulerAngles.x) > rotationSensitivity) change |= Changed.RotX;
|
||||
if (Mathf.Abs(lastSnapshot.rotation.eulerAngles.y - currentSnapshot.rotation.eulerAngles.y) > rotationSensitivity) change |= Changed.RotY;
|
||||
if (Mathf.Abs(lastSnapshot.rotation.eulerAngles.z - currentSnapshot.rotation.eulerAngles.z) > rotationSensitivity) change |= Changed.RotZ;
|
||||
}
|
||||
}
|
||||
|
||||
if (syncScale)
|
||||
{
|
||||
if (Vector3.SqrMagnitude(lastSnapshot.scale - currentSnapshot.scale) > scaleSensitivity * scaleSensitivity) change |= Changed.Scale;
|
||||
}
|
||||
|
||||
return change;
|
||||
}
|
||||
|
||||
[Command(channel = Channels.Unreliable)]
|
||||
void CmdClientToServerSync(SyncData syncData)
|
||||
{
|
||||
OnClientToServerSync(syncData);
|
||||
//For client authority, immediately pass on the client snapshot to all other
|
||||
//clients instead of waiting for server to send its snapshots.
|
||||
if (syncDirection == SyncDirection.ClientToServer)
|
||||
RpcServerToClientSync(syncData);
|
||||
}
|
||||
|
||||
protected virtual void OnClientToServerSync(SyncData syncData)
|
||||
{
|
||||
// only apply if in client authority mode
|
||||
if (syncDirection != SyncDirection.ClientToServer) return;
|
||||
|
||||
// protect against ever growing buffer size attacks
|
||||
if (serverSnapshots.Count >= connectionToClient.snapshotBufferSizeLimit) return;
|
||||
|
||||
// only player owned objects (with a connection) can send to
|
||||
// server. we can get the timestamp from the connection.
|
||||
double timestamp = connectionToClient.remoteTimeStamp;
|
||||
|
||||
if (onlySyncOnChange)
|
||||
{
|
||||
double timeIntervalCheck = bufferResetMultiplier * sendIntervalMultiplier * NetworkClient.sendInterval;
|
||||
|
||||
if (serverSnapshots.Count > 0 && serverSnapshots.Values[serverSnapshots.Count - 1].remoteTime + timeIntervalCheck < timestamp)
|
||||
ResetState();
|
||||
}
|
||||
|
||||
UpdateSyncData(ref syncData, serverSnapshots);
|
||||
|
||||
AddSnapshot(serverSnapshots, connectionToClient.remoteTimeStamp + timeStampAdjustment + offset, syncData.position, syncData.quatRotation, syncData.scale);
|
||||
}
|
||||
|
||||
[ClientRpc(channel = Channels.Unreliable)]
|
||||
void RpcServerToClientSync(SyncData syncData) =>
|
||||
OnServerToClientSync(syncData);
|
||||
|
||||
protected virtual void OnServerToClientSync(SyncData syncData)
|
||||
{
|
||||
// in host mode, the server sends rpcs to all clients.
|
||||
// the host client itself will receive them too.
|
||||
// -> host server is always the source of truth
|
||||
// -> we can ignore any rpc on the host client
|
||||
// => otherwise host objects would have ever growing clientBuffers
|
||||
// (rpc goes to clients. if isServer is true too then we are host)
|
||||
if (isServer) return;
|
||||
|
||||
// don't apply for local player with authority
|
||||
if (IsClientWithAuthority) return;
|
||||
|
||||
// on the client, we receive rpcs for all entities.
|
||||
// not all of them have a connectionToServer.
|
||||
// but all of them go through NetworkClient.connection.
|
||||
// we can get the timestamp from there.
|
||||
double timestamp = NetworkClient.connection.remoteTimeStamp;
|
||||
|
||||
if (onlySyncOnChange)
|
||||
{
|
||||
double timeIntervalCheck = bufferResetMultiplier * sendIntervalMultiplier * NetworkServer.sendInterval;
|
||||
|
||||
if (clientSnapshots.Count > 0 && clientSnapshots.Values[clientSnapshots.Count - 1].remoteTime + timeIntervalCheck < timestamp)
|
||||
ResetState();
|
||||
}
|
||||
|
||||
UpdateSyncData(ref syncData, clientSnapshots);
|
||||
|
||||
AddSnapshot(clientSnapshots, NetworkClient.connection.remoteTimeStamp + timeStampAdjustment + offset, syncData.position, syncData.quatRotation, syncData.scale);
|
||||
}
|
||||
|
||||
protected virtual void UpdateSyncData(ref SyncData syncData, SortedList<double, TransformSnapshot> snapshots)
|
||||
{
|
||||
if (syncData.changedDataByte == Changed.None || syncData.changedDataByte == Changed.CompressRot)
|
||||
{
|
||||
syncData.position = snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].position : GetPosition();
|
||||
syncData.quatRotation = snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].rotation : GetRotation();
|
||||
syncData.scale = snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].scale : GetScale();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Just going to update these without checking if syncposition or not,
|
||||
// because if not syncing position, NT will not apply any position data
|
||||
// to the target during Apply().
|
||||
|
||||
syncData.position.x = (syncData.changedDataByte & Changed.PosX) > 0 ? syncData.position.x : (snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].position.x : GetPosition().x);
|
||||
syncData.position.y = (syncData.changedDataByte & Changed.PosY) > 0 ? syncData.position.y : (snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].position.y : GetPosition().y);
|
||||
syncData.position.z = (syncData.changedDataByte & Changed.PosZ) > 0 ? syncData.position.z : (snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].position.z : GetPosition().z);
|
||||
|
||||
// If compressRot is true, we already have the Quat in syncdata.
|
||||
if ((syncData.changedDataByte & Changed.CompressRot) == 0)
|
||||
{
|
||||
syncData.vecRotation.x = (syncData.changedDataByte & Changed.RotX) > 0 ? syncData.vecRotation.x : (snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].rotation.eulerAngles.x : GetRotation().eulerAngles.x);
|
||||
syncData.vecRotation.y = (syncData.changedDataByte & Changed.RotY) > 0 ? syncData.vecRotation.y : (snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].rotation.eulerAngles.y : GetRotation().eulerAngles.y); ;
|
||||
syncData.vecRotation.z = (syncData.changedDataByte & Changed.RotZ) > 0 ? syncData.vecRotation.z : (snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].rotation.eulerAngles.z : GetRotation().eulerAngles.z);
|
||||
|
||||
syncData.quatRotation = Quaternion.Euler(syncData.vecRotation);
|
||||
}
|
||||
else
|
||||
{
|
||||
syncData.quatRotation = (syncData.changedDataByte & Changed.Rot) > 0 ? syncData.quatRotation : (snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].rotation : GetRotation());
|
||||
}
|
||||
|
||||
syncData.scale = (syncData.changedDataByte & Changed.Scale) > 0 ? syncData.scale : (snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].scale : GetScale());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// NetworkTransform V2 by mischa (2021-07)
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
[AddComponentMenu("Network/Network Transform (Unreliable)")]
|
||||
public class NetworkTransformUnreliable : NetworkTransformBase
|
||||
{
|
||||
uint sendIntervalCounter = 0;
|
||||
double lastSendIntervalTime = double.MinValue;
|
||||
TransformSnapshot? pendingSnapshot;
|
||||
|
||||
[Header("Additional Settings")]
|
||||
// Testing under really bad network conditions, 2%-5% packet loss and 250-1200ms ping, 5 proved to eliminate any twitching, however this should not be the default as it is a rare case Developers may want to cover.
|
||||
[Tooltip("How much time, as a multiple of send interval, has passed before clearing buffers.\nA larger buffer means more delay, but results in smoother movement.\nExample: 1 for faster responses minimal smoothing, 5 covers bad pings but has noticable delay, 3 is recommended for balanced results.")]
|
||||
public float bufferResetMultiplier = 3;
|
||||
public bool useFixedUpdate;
|
||||
|
||||
[Header("Sensitivity"), Tooltip("Sensitivity of changes needed before an updated state is sent over the network")]
|
||||
public float positionSensitivity = 0.01f;
|
||||
public float rotationSensitivity = 0.01f;
|
||||
public float scaleSensitivity = 0.01f;
|
||||
|
||||
// Used to store last sent snapshots
|
||||
protected TransformSnapshot lastSnapshot;
|
||||
protected Changed cachedChangedComparison;
|
||||
protected bool hasSentUnchangedPosition;
|
||||
|
||||
// update //////////////////////////////////////////////////////////////
|
||||
// Update applies interpolation
|
||||
void Update()
|
||||
{
|
||||
if (isServer) UpdateServerInterpolation();
|
||||
// for all other clients (and for local player if !authority),
|
||||
// we need to apply snapshots from the buffer.
|
||||
// 'else if' because host mode shouldn't interpolate client
|
||||
else if (isClient && !IsClientWithAuthority) UpdateClientInterpolation();
|
||||
}
|
||||
|
||||
void FixedUpdate()
|
||||
{
|
||||
if (!useFixedUpdate) return;
|
||||
|
||||
if (pendingSnapshot.HasValue)
|
||||
{
|
||||
Apply(pendingSnapshot.Value, pendingSnapshot.Value);
|
||||
pendingSnapshot = null;
|
||||
}
|
||||
}
|
||||
|
||||
// LateUpdate broadcasts.
|
||||
// movement scripts may change positions in Update.
|
||||
// use LateUpdate to ensure changes are detected in the same frame.
|
||||
// otherwise this may run before user update, delaying detection until next frame.
|
||||
// this could cause visible jitter.
|
||||
void LateUpdate()
|
||||
{
|
||||
// if server then always sync to others.
|
||||
if (isServer) UpdateServerBroadcast();
|
||||
// client authority, and local player (= allowed to move myself)?
|
||||
// 'else if' because host mode shouldn't send anything to server.
|
||||
// it is the server. don't overwrite anything there.
|
||||
else if (isClient && IsClientWithAuthority) UpdateClientBroadcast();
|
||||
}
|
||||
|
||||
protected virtual void CheckLastSendTime()
|
||||
{
|
||||
// We check interval every frame, and then send if interval is reached.
|
||||
// So by the time sendIntervalCounter == sendIntervalMultiplier, data is sent,
|
||||
// thus we reset the counter here.
|
||||
// This fixes previous issue of, if sendIntervalMultiplier = 1, we send every frame,
|
||||
// because intervalCounter is always = 1 in the previous version.
|
||||
|
||||
// Changing == to >= https://github.com/MirrorNetworking/Mirror/issues/3571
|
||||
|
||||
if (sendIntervalCounter >= sendIntervalMultiplier)
|
||||
sendIntervalCounter = 0;
|
||||
|
||||
// timeAsDouble not available in older Unity versions.
|
||||
if (AccurateInterval.Elapsed(NetworkTime.localTime, NetworkServer.sendInterval, ref lastSendIntervalTime))
|
||||
sendIntervalCounter++;
|
||||
}
|
||||
|
||||
void UpdateServerBroadcast()
|
||||
{
|
||||
// broadcast to all clients each 'sendInterval'
|
||||
// (client with authority will drop the rpc)
|
||||
// NetworkTime.localTime for double precision until Unity has it too
|
||||
//
|
||||
// IMPORTANT:
|
||||
// snapshot interpolation requires constant sending.
|
||||
// DO NOT only send if position changed. for example:
|
||||
// ---
|
||||
// * client sends first position at t=0
|
||||
// * ... 10s later ...
|
||||
// * client moves again, sends second position at t=10
|
||||
// ---
|
||||
// * server gets first position at t=0
|
||||
// * server gets second position at t=10
|
||||
// * server moves from first to second within a time of 10s
|
||||
// => would be a super slow move, instead of a wait & move.
|
||||
//
|
||||
// IMPORTANT:
|
||||
// DO NOT send nulls if not changed 'since last send' either. we
|
||||
// send unreliable and don't know which 'last send' the other end
|
||||
// received successfully.
|
||||
//
|
||||
// Checks to ensure server only sends snapshots if object is
|
||||
// on server authority(!clientAuthority) mode because on client
|
||||
// authority mode snapshots are broadcasted right after the authoritative
|
||||
// client updates server in the command function(see above), OR,
|
||||
// since host does not send anything to update the server, any client
|
||||
// authoritative movement done by the host will have to be broadcasted
|
||||
// here by checking IsClientWithAuthority.
|
||||
// TODO send same time that NetworkServer sends time snapshot?
|
||||
CheckLastSendTime();
|
||||
|
||||
if (sendIntervalCounter == sendIntervalMultiplier && // same interval as time interpolation!
|
||||
(syncDirection == SyncDirection.ServerToClient || IsClientWithAuthority))
|
||||
{
|
||||
// send snapshot without timestamp.
|
||||
// receiver gets it from batch timestamp to save bandwidth.
|
||||
TransformSnapshot snapshot = Construct();
|
||||
|
||||
cachedChangedComparison = CompareChangedSnapshots(snapshot);
|
||||
|
||||
if ((cachedChangedComparison == Changed.None || cachedChangedComparison == Changed.CompressRot) && hasSentUnchangedPosition && onlySyncOnChange) { return; }
|
||||
|
||||
SyncData syncData = new SyncData(cachedChangedComparison, snapshot);
|
||||
|
||||
RpcServerToClientSync(syncData);
|
||||
|
||||
if (cachedChangedComparison == Changed.None || cachedChangedComparison == Changed.CompressRot)
|
||||
{
|
||||
hasSentUnchangedPosition = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
hasSentUnchangedPosition = false;
|
||||
UpdateLastSentSnapshot(cachedChangedComparison, snapshot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateServerInterpolation()
|
||||
{
|
||||
// apply buffered snapshots IF client authority
|
||||
// -> in server authority, server moves the object
|
||||
// so no need to apply any snapshots there.
|
||||
// -> don't apply for host mode player objects either, even if in
|
||||
// client authority mode. if it doesn't go over the network,
|
||||
// then we don't need to do anything.
|
||||
// -> connectionToClient is briefly null after scene changes:
|
||||
// https://github.com/MirrorNetworking/Mirror/issues/3329
|
||||
if (syncDirection == SyncDirection.ClientToServer &&
|
||||
connectionToClient != null &&
|
||||
!isOwned)
|
||||
{
|
||||
if (serverSnapshots.Count == 0) return;
|
||||
|
||||
// step the transform interpolation without touching time.
|
||||
// NetworkClient is responsible for time globally.
|
||||
SnapshotInterpolation.StepInterpolation(
|
||||
serverSnapshots,
|
||||
connectionToClient.remoteTimeline,
|
||||
out TransformSnapshot from,
|
||||
out TransformSnapshot to,
|
||||
out double t);
|
||||
|
||||
// interpolate & apply
|
||||
TransformSnapshot computed = TransformSnapshot.Interpolate(from, to, t);
|
||||
if (useFixedUpdate)
|
||||
pendingSnapshot = computed;
|
||||
else
|
||||
Apply(computed, to);
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateClientBroadcast()
|
||||
{
|
||||
// https://github.com/vis2k/Mirror/pull/2992/
|
||||
if (!NetworkClient.ready) return;
|
||||
|
||||
// send to server each 'sendInterval'
|
||||
// NetworkTime.localTime for double precision until Unity has it too
|
||||
//
|
||||
// IMPORTANT:
|
||||
// snapshot interpolation requires constant sending.
|
||||
// DO NOT only send if position changed. for example:
|
||||
// ---
|
||||
// * client sends first position at t=0
|
||||
// * ... 10s later ...
|
||||
// * client moves again, sends second position at t=10
|
||||
// ---
|
||||
// * server gets first position at t=0
|
||||
// * server gets second position at t=10
|
||||
// * server moves from first to second within a time of 10s
|
||||
// => would be a super slow move, instead of a wait & move.
|
||||
//
|
||||
// IMPORTANT:
|
||||
// DO NOT send nulls if not changed 'since last send' either. we
|
||||
// send unreliable and don't know which 'last send' the other end
|
||||
// received successfully.
|
||||
CheckLastSendTime();
|
||||
if (sendIntervalCounter == sendIntervalMultiplier) // same interval as time interpolation!
|
||||
{
|
||||
// send snapshot without timestamp.
|
||||
// receiver gets it from batch timestamp to save bandwidth.
|
||||
TransformSnapshot snapshot = Construct();
|
||||
|
||||
cachedChangedComparison = CompareChangedSnapshots(snapshot);
|
||||
|
||||
if ((cachedChangedComparison == Changed.None || cachedChangedComparison == Changed.CompressRot) && hasSentUnchangedPosition && onlySyncOnChange) { return; }
|
||||
|
||||
SyncData syncData = new SyncData(cachedChangedComparison, snapshot);
|
||||
|
||||
CmdClientToServerSync(syncData);
|
||||
|
||||
if (cachedChangedComparison == Changed.None || cachedChangedComparison == Changed.CompressRot)
|
||||
{
|
||||
hasSentUnchangedPosition = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
hasSentUnchangedPosition = false;
|
||||
UpdateLastSentSnapshot(cachedChangedComparison, snapshot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateClientInterpolation()
|
||||
{
|
||||
// only while we have snapshots
|
||||
if (clientSnapshots.Count == 0) return;
|
||||
|
||||
// step the interpolation without touching time.
|
||||
// NetworkClient is responsible for time globally.
|
||||
SnapshotInterpolation.StepInterpolation(
|
||||
clientSnapshots,
|
||||
NetworkTime.time, // == NetworkClient.localTimeline from snapshot interpolation
|
||||
out TransformSnapshot from,
|
||||
out TransformSnapshot to,
|
||||
out double t);
|
||||
|
||||
// interpolate & apply
|
||||
TransformSnapshot computed = TransformSnapshot.Interpolate(from, to, t);
|
||||
if (useFixedUpdate)
|
||||
pendingSnapshot = computed;
|
||||
else
|
||||
Apply(computed, to);
|
||||
}
|
||||
|
||||
public override void OnSerialize(NetworkWriter writer, bool initialState)
|
||||
{
|
||||
// sync target component's position on spawn.
|
||||
// fixes https://github.com/vis2k/Mirror/pull/3051/
|
||||
// (Spawn message wouldn't sync NTChild positions either)
|
||||
if (initialState)
|
||||
{
|
||||
if (syncPosition) writer.WriteVector3(GetPosition());
|
||||
if (syncRotation) writer.WriteQuaternion(GetRotation());
|
||||
if (syncScale) writer.WriteVector3(GetScale());
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnDeserialize(NetworkReader reader, bool initialState)
|
||||
{
|
||||
// sync target component's position on spawn.
|
||||
// fixes https://github.com/vis2k/Mirror/pull/3051/
|
||||
// (Spawn message wouldn't sync NTChild positions either)
|
||||
if (initialState)
|
||||
{
|
||||
if (syncPosition) SetPosition(reader.ReadVector3());
|
||||
if (syncRotation) SetRotation(reader.ReadQuaternion());
|
||||
if (syncScale) SetScale(reader.ReadVector3());
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void UpdateLastSentSnapshot(Changed change, TransformSnapshot currentSnapshot)
|
||||
{
|
||||
if (change == Changed.None || change == Changed.CompressRot) return;
|
||||
|
||||
if ((change & Changed.PosX) > 0) lastSnapshot.position.x = currentSnapshot.position.x;
|
||||
if ((change & Changed.PosY) > 0) lastSnapshot.position.y = currentSnapshot.position.y;
|
||||
if ((change & Changed.PosZ) > 0) lastSnapshot.position.z = currentSnapshot.position.z;
|
||||
|
||||
if (compressRotation)
|
||||
{
|
||||
if ((change & Changed.Rot) > 0) lastSnapshot.rotation = currentSnapshot.rotation;
|
||||
}
|
||||
else
|
||||
{
|
||||
Vector3 newRotation;
|
||||
newRotation.x = (change & Changed.RotX) > 0 ? currentSnapshot.rotation.eulerAngles.x : lastSnapshot.rotation.eulerAngles.x;
|
||||
newRotation.y = (change & Changed.RotY) > 0 ? currentSnapshot.rotation.eulerAngles.y : lastSnapshot.rotation.eulerAngles.y;
|
||||
newRotation.z = (change & Changed.RotZ) > 0 ? currentSnapshot.rotation.eulerAngles.z : lastSnapshot.rotation.eulerAngles.z;
|
||||
|
||||
lastSnapshot.rotation = Quaternion.Euler(newRotation);
|
||||
}
|
||||
|
||||
if ((change & Changed.Scale) > 0) lastSnapshot.scale = currentSnapshot.scale;
|
||||
}
|
||||
|
||||
// Returns true if position, rotation AND scale are unchanged, within given sensitivity range.
|
||||
// Note the sensitivity comparison are different for pos, rot and scale.
|
||||
protected virtual Changed CompareChangedSnapshots(TransformSnapshot currentSnapshot)
|
||||
{
|
||||
Changed change = Changed.None;
|
||||
|
||||
if (syncPosition)
|
||||
{
|
||||
bool positionChanged = Vector3.SqrMagnitude(lastSnapshot.position - currentSnapshot.position) > positionSensitivity * positionSensitivity;
|
||||
if (positionChanged)
|
||||
{
|
||||
if (Mathf.Abs(lastSnapshot.position.x - currentSnapshot.position.x) > positionSensitivity) change |= Changed.PosX;
|
||||
if (Mathf.Abs(lastSnapshot.position.y - currentSnapshot.position.y) > positionSensitivity) change |= Changed.PosY;
|
||||
if (Mathf.Abs(lastSnapshot.position.z - currentSnapshot.position.z) > positionSensitivity) change |= Changed.PosZ;
|
||||
}
|
||||
}
|
||||
|
||||
if (syncRotation)
|
||||
{
|
||||
if (compressRotation)
|
||||
{
|
||||
bool rotationChanged = Quaternion.Angle(lastSnapshot.rotation, currentSnapshot.rotation) > rotationSensitivity;
|
||||
if (rotationChanged)
|
||||
{
|
||||
// Here we set all Rot enum flags, to tell us if there was a change in rotation
|
||||
// when using compression. If no change, we don't write the compressed Quat.
|
||||
change |= Changed.CompressRot;
|
||||
change |= Changed.Rot;
|
||||
}
|
||||
else
|
||||
{
|
||||
change |= Changed.CompressRot;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Mathf.Abs(lastSnapshot.rotation.eulerAngles.x - currentSnapshot.rotation.eulerAngles.x) > rotationSensitivity) change |= Changed.RotX;
|
||||
if (Mathf.Abs(lastSnapshot.rotation.eulerAngles.y - currentSnapshot.rotation.eulerAngles.y) > rotationSensitivity) change |= Changed.RotY;
|
||||
if (Mathf.Abs(lastSnapshot.rotation.eulerAngles.z - currentSnapshot.rotation.eulerAngles.z) > rotationSensitivity) change |= Changed.RotZ;
|
||||
}
|
||||
}
|
||||
|
||||
if (syncScale)
|
||||
{
|
||||
if (Vector3.SqrMagnitude(lastSnapshot.scale - currentSnapshot.scale) > scaleSensitivity * scaleSensitivity) change |= Changed.Scale;
|
||||
}
|
||||
|
||||
return change;
|
||||
}
|
||||
|
||||
[Command(channel = Channels.Unreliable)]
|
||||
void CmdClientToServerSync(SyncData syncData)
|
||||
{
|
||||
OnClientToServerSync(syncData);
|
||||
//For client authority, immediately pass on the client snapshot to all other
|
||||
//clients instead of waiting for server to send its snapshots.
|
||||
if (syncDirection == SyncDirection.ClientToServer)
|
||||
RpcServerToClientSync(syncData);
|
||||
}
|
||||
|
||||
protected virtual void OnClientToServerSync(SyncData syncData)
|
||||
{
|
||||
// only apply if in client authority mode
|
||||
if (syncDirection != SyncDirection.ClientToServer) return;
|
||||
|
||||
// protect against ever growing buffer size attacks
|
||||
if (serverSnapshots.Count >= connectionToClient.snapshotBufferSizeLimit) return;
|
||||
|
||||
// only player owned objects (with a connection) can send to
|
||||
// server. we can get the timestamp from the connection.
|
||||
double timestamp = connectionToClient.remoteTimeStamp;
|
||||
|
||||
if (onlySyncOnChange)
|
||||
{
|
||||
double timeIntervalCheck = bufferResetMultiplier * sendIntervalMultiplier * NetworkClient.sendInterval;
|
||||
|
||||
if (serverSnapshots.Count > 0 && serverSnapshots.Values[serverSnapshots.Count - 1].remoteTime + timeIntervalCheck < timestamp)
|
||||
ResetState();
|
||||
}
|
||||
|
||||
UpdateSyncData(ref syncData, serverSnapshots);
|
||||
|
||||
AddSnapshot(serverSnapshots, connectionToClient.remoteTimeStamp + timeStampAdjustment + offset, syncData.position, syncData.quatRotation, syncData.scale);
|
||||
}
|
||||
|
||||
[ClientRpc(channel = Channels.Unreliable)]
|
||||
void RpcServerToClientSync(SyncData syncData) =>
|
||||
OnServerToClientSync(syncData);
|
||||
|
||||
protected virtual void OnServerToClientSync(SyncData syncData)
|
||||
{
|
||||
// in host mode, the server sends rpcs to all clients.
|
||||
// the host client itself will receive them too.
|
||||
// -> host server is always the source of truth
|
||||
// -> we can ignore any rpc on the host client
|
||||
// => otherwise host objects would have ever growing clientBuffers
|
||||
// (rpc goes to clients. if isServer is true too then we are host)
|
||||
if (isServer) return;
|
||||
|
||||
// don't apply for local player with authority
|
||||
if (IsClientWithAuthority) return;
|
||||
|
||||
// on the client, we receive rpcs for all entities.
|
||||
// not all of them have a connectionToServer.
|
||||
// but all of them go through NetworkClient.connection.
|
||||
// we can get the timestamp from there.
|
||||
double timestamp = NetworkClient.connection.remoteTimeStamp;
|
||||
|
||||
if (onlySyncOnChange)
|
||||
{
|
||||
double timeIntervalCheck = bufferResetMultiplier * sendIntervalMultiplier * NetworkServer.sendInterval;
|
||||
|
||||
if (clientSnapshots.Count > 0 && clientSnapshots.Values[clientSnapshots.Count - 1].remoteTime + timeIntervalCheck < timestamp)
|
||||
ResetState();
|
||||
}
|
||||
|
||||
UpdateSyncData(ref syncData, clientSnapshots);
|
||||
|
||||
AddSnapshot(clientSnapshots, NetworkClient.connection.remoteTimeStamp + timeStampAdjustment + offset, syncData.position, syncData.quatRotation, syncData.scale);
|
||||
}
|
||||
|
||||
protected virtual void UpdateSyncData(ref SyncData syncData, SortedList<double, TransformSnapshot> snapshots)
|
||||
{
|
||||
if (syncData.changedDataByte == Changed.None || syncData.changedDataByte == Changed.CompressRot)
|
||||
{
|
||||
syncData.position = snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].position : GetPosition();
|
||||
syncData.quatRotation = snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].rotation : GetRotation();
|
||||
syncData.scale = snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].scale : GetScale();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Just going to update these without checking if syncposition or not,
|
||||
// because if not syncing position, NT will not apply any position data
|
||||
// to the target during Apply().
|
||||
|
||||
syncData.position.x = (syncData.changedDataByte & Changed.PosX) > 0 ? syncData.position.x : (snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].position.x : GetPosition().x);
|
||||
syncData.position.y = (syncData.changedDataByte & Changed.PosY) > 0 ? syncData.position.y : (snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].position.y : GetPosition().y);
|
||||
syncData.position.z = (syncData.changedDataByte & Changed.PosZ) > 0 ? syncData.position.z : (snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].position.z : GetPosition().z);
|
||||
|
||||
// If compressRot is true, we already have the Quat in syncdata.
|
||||
if ((syncData.changedDataByte & Changed.CompressRot) == 0)
|
||||
{
|
||||
syncData.vecRotation.x = (syncData.changedDataByte & Changed.RotX) > 0 ? syncData.vecRotation.x : (snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].rotation.eulerAngles.x : GetRotation().eulerAngles.x);
|
||||
syncData.vecRotation.y = (syncData.changedDataByte & Changed.RotY) > 0 ? syncData.vecRotation.y : (snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].rotation.eulerAngles.y : GetRotation().eulerAngles.y); ;
|
||||
syncData.vecRotation.z = (syncData.changedDataByte & Changed.RotZ) > 0 ? syncData.vecRotation.z : (snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].rotation.eulerAngles.z : GetRotation().eulerAngles.z);
|
||||
|
||||
syncData.quatRotation = Quaternion.Euler(syncData.vecRotation);
|
||||
}
|
||||
else
|
||||
{
|
||||
syncData.quatRotation = (syncData.changedDataByte & Changed.Rot) > 0 ? syncData.quatRotation : (snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].rotation : GetRotation());
|
||||
}
|
||||
|
||||
syncData.scale = (syncData.changedDataByte & Changed.Scale) > 0 ? syncData.scale : (snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].scale : GetScale());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a553cb17010b2403e8523b558bffbc14
|
||||
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/NetworkTransform/NetworkTransformUnreliable.cs
|
||||
uploadId: 736421
|
||||
fileFormatVersion: 2
|
||||
guid: a553cb17010b2403e8523b558bffbc14
|
||||
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/NetworkTransform/NetworkTransformUnreliable.cs
|
||||
uploadId: 736421
|
||||
|
||||
@@ -1,68 +1,68 @@
|
||||
// snapshot for snapshot interpolation
|
||||
// https://gafferongames.com/post/snapshot_interpolation/
|
||||
// position, rotation, scale for compatibility for now.
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
// NetworkTransform Snapshot
|
||||
public struct TransformSnapshot : Snapshot
|
||||
{
|
||||
// time or sequence are needed to throw away older snapshots.
|
||||
//
|
||||
// glenn fiedler starts with a 16 bit sequence number.
|
||||
// supposedly this is meant as a simplified example.
|
||||
// in the end we need the remote timestamp for accurate interpolation
|
||||
// and buffering over time.
|
||||
//
|
||||
// note: in theory, IF server sends exactly(!) at the same interval then
|
||||
// the 16 bit ushort timestamp would be enough to calculate the
|
||||
// remote time (sequence * sendInterval). but Unity's update is
|
||||
// not guaranteed to run on the exact intervals / do catchup.
|
||||
// => remote timestamp is better for now
|
||||
//
|
||||
// [REMOTE TIME, NOT LOCAL TIME]
|
||||
// => DOUBLE for long term accuracy & batching gives us double anyway
|
||||
public double remoteTime { get; set; }
|
||||
|
||||
// the local timestamp (when we received it)
|
||||
// used to know if the first two snapshots are old enough to start.
|
||||
public double localTime { get; set; }
|
||||
|
||||
public Vector3 position;
|
||||
public Quaternion rotation;
|
||||
public Vector3 scale;
|
||||
|
||||
public TransformSnapshot(double remoteTime, double localTime, Vector3 position, Quaternion rotation, Vector3 scale)
|
||||
{
|
||||
this.remoteTime = remoteTime;
|
||||
this.localTime = localTime;
|
||||
this.position = position;
|
||||
this.rotation = rotation;
|
||||
this.scale = scale;
|
||||
}
|
||||
|
||||
public static TransformSnapshot Interpolate(TransformSnapshot from, TransformSnapshot to, double t)
|
||||
{
|
||||
// NOTE:
|
||||
// Vector3 & Quaternion components are float anyway, so we can
|
||||
// keep using the functions with 't' as float instead of double.
|
||||
return new TransformSnapshot(
|
||||
// interpolated snapshot is applied directly. don't need timestamps.
|
||||
0, 0,
|
||||
// lerp position/rotation/scale unclamped in case we ever need
|
||||
// to extrapolate. atm SnapshotInterpolation never does.
|
||||
Vector3.LerpUnclamped(from.position, to.position, (float)t),
|
||||
// IMPORTANT: LerpUnclamped(0, 60, 1.5) extrapolates to ~86.
|
||||
// SlerpUnclamped(0, 60, 1.5) extrapolates to 90!
|
||||
// (0, 90, 1.5) is even worse. for Lerp.
|
||||
// => Slerp works way better for our euler angles.
|
||||
Quaternion.SlerpUnclamped(from.rotation, to.rotation, (float)t),
|
||||
Vector3.LerpUnclamped(from.scale, to.scale, (float)t)
|
||||
);
|
||||
}
|
||||
|
||||
public override string ToString() =>
|
||||
$"TransformSnapshot(remoteTime={remoteTime:F2}, localTime={localTime:F2}, pos={position}, rot={rotation}, scale={scale})";
|
||||
}
|
||||
}
|
||||
// snapshot for snapshot interpolation
|
||||
// https://gafferongames.com/post/snapshot_interpolation/
|
||||
// position, rotation, scale for compatibility for now.
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
// NetworkTransform Snapshot
|
||||
public struct TransformSnapshot : Snapshot
|
||||
{
|
||||
// time or sequence are needed to throw away older snapshots.
|
||||
//
|
||||
// glenn fiedler starts with a 16 bit sequence number.
|
||||
// supposedly this is meant as a simplified example.
|
||||
// in the end we need the remote timestamp for accurate interpolation
|
||||
// and buffering over time.
|
||||
//
|
||||
// note: in theory, IF server sends exactly(!) at the same interval then
|
||||
// the 16 bit ushort timestamp would be enough to calculate the
|
||||
// remote time (sequence * sendInterval). but Unity's update is
|
||||
// not guaranteed to run on the exact intervals / do catchup.
|
||||
// => remote timestamp is better for now
|
||||
//
|
||||
// [REMOTE TIME, NOT LOCAL TIME]
|
||||
// => DOUBLE for long term accuracy & batching gives us double anyway
|
||||
public double remoteTime { get; set; }
|
||||
|
||||
// the local timestamp (when we received it)
|
||||
// used to know if the first two snapshots are old enough to start.
|
||||
public double localTime { get; set; }
|
||||
|
||||
public Vector3 position;
|
||||
public Quaternion rotation;
|
||||
public Vector3 scale;
|
||||
|
||||
public TransformSnapshot(double remoteTime, double localTime, Vector3 position, Quaternion rotation, Vector3 scale)
|
||||
{
|
||||
this.remoteTime = remoteTime;
|
||||
this.localTime = localTime;
|
||||
this.position = position;
|
||||
this.rotation = rotation;
|
||||
this.scale = scale;
|
||||
}
|
||||
|
||||
public static TransformSnapshot Interpolate(TransformSnapshot from, TransformSnapshot to, double t)
|
||||
{
|
||||
// NOTE:
|
||||
// Vector3 & Quaternion components are float anyway, so we can
|
||||
// keep using the functions with 't' as float instead of double.
|
||||
return new TransformSnapshot(
|
||||
// interpolated snapshot is applied directly. don't need timestamps.
|
||||
0, 0,
|
||||
// lerp position/rotation/scale unclamped in case we ever need
|
||||
// to extrapolate. atm SnapshotInterpolation never does.
|
||||
Vector3.LerpUnclamped(from.position, to.position, (float)t),
|
||||
// IMPORTANT: LerpUnclamped(0, 60, 1.5) extrapolates to ~86.
|
||||
// SlerpUnclamped(0, 60, 1.5) extrapolates to 90!
|
||||
// (0, 90, 1.5) is even worse. for Lerp.
|
||||
// => Slerp works way better for our euler angles.
|
||||
Quaternion.SlerpUnclamped(from.rotation, to.rotation, (float)t),
|
||||
Vector3.LerpUnclamped(from.scale, to.scale, (float)t)
|
||||
);
|
||||
}
|
||||
|
||||
public override string ToString() =>
|
||||
$"TransformSnapshot(remoteTime={remoteTime:F2}, localTime={localTime:F2}, pos={position}, rot={rotation}, scale={scale})";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d3dae77b43dc4e1dbb2012924b2da79c
|
||||
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/NetworkTransform/TransformSnapshot.cs
|
||||
uploadId: 736421
|
||||
fileFormatVersion: 2
|
||||
guid: d3dae77b43dc4e1dbb2012924b2da79c
|
||||
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/NetworkTransform/TransformSnapshot.cs
|
||||
uploadId: 736421
|
||||
|
||||
@@ -1,156 +1,156 @@
|
||||
using UnityEngine;
|
||||
using System;
|
||||
using Mirror;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
[Serializable]
|
||||
public struct SyncData
|
||||
{
|
||||
public Changed changedDataByte;
|
||||
public Vector3 position;
|
||||
public Quaternion quatRotation;
|
||||
public Vector3 vecRotation;
|
||||
public Vector3 scale;
|
||||
|
||||
public SyncData(Changed _dataChangedByte, Vector3 _position, Quaternion _rotation, Vector3 _scale)
|
||||
{
|
||||
this.changedDataByte = _dataChangedByte;
|
||||
this.position = _position;
|
||||
this.quatRotation = _rotation;
|
||||
this.vecRotation = quatRotation.eulerAngles;
|
||||
this.scale = _scale;
|
||||
}
|
||||
|
||||
public SyncData(Changed _dataChangedByte, TransformSnapshot _snapshot)
|
||||
{
|
||||
this.changedDataByte = _dataChangedByte;
|
||||
this.position = _snapshot.position;
|
||||
this.quatRotation = _snapshot.rotation;
|
||||
this.vecRotation = quatRotation.eulerAngles;
|
||||
this.scale = _snapshot.scale;
|
||||
}
|
||||
|
||||
public SyncData(Changed _dataChangedByte, Vector3 _position, Vector3 _vecRotation, Vector3 _scale)
|
||||
{
|
||||
this.changedDataByte = _dataChangedByte;
|
||||
this.position = _position;
|
||||
this.vecRotation = _vecRotation;
|
||||
this.quatRotation = Quaternion.Euler(vecRotation);
|
||||
this.scale = _scale;
|
||||
}
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum Changed : byte
|
||||
{
|
||||
None = 0,
|
||||
PosX = 1 << 0,
|
||||
PosY = 1 << 1,
|
||||
PosZ = 1 << 2,
|
||||
CompressRot = 1 << 3,
|
||||
RotX = 1 << 4,
|
||||
RotY = 1 << 5,
|
||||
RotZ = 1 << 6,
|
||||
Scale = 1 << 7,
|
||||
|
||||
Pos = PosX | PosY | PosZ,
|
||||
Rot = RotX | RotY | RotZ
|
||||
}
|
||||
|
||||
|
||||
public static class SyncDataReaderWriter
|
||||
{
|
||||
public static void WriteSyncData(this NetworkWriter writer, SyncData syncData)
|
||||
{
|
||||
writer.WriteByte((byte)syncData.changedDataByte);
|
||||
|
||||
// Write position
|
||||
if ((syncData.changedDataByte & Changed.PosX) > 0)
|
||||
{
|
||||
writer.WriteFloat(syncData.position.x);
|
||||
}
|
||||
|
||||
if ((syncData.changedDataByte & Changed.PosY) > 0)
|
||||
{
|
||||
writer.WriteFloat(syncData.position.y);
|
||||
}
|
||||
|
||||
if ((syncData.changedDataByte & Changed.PosZ) > 0)
|
||||
{
|
||||
writer.WriteFloat(syncData.position.z);
|
||||
}
|
||||
|
||||
// Write rotation
|
||||
if ((syncData.changedDataByte & Changed.CompressRot) > 0)
|
||||
{
|
||||
if((syncData.changedDataByte & Changed.Rot) > 0)
|
||||
{
|
||||
writer.WriteUInt(Compression.CompressQuaternion(syncData.quatRotation));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if ((syncData.changedDataByte & Changed.RotX) > 0)
|
||||
{
|
||||
writer.WriteFloat(syncData.quatRotation.eulerAngles.x);
|
||||
}
|
||||
|
||||
if ((syncData.changedDataByte & Changed.RotY) > 0)
|
||||
{
|
||||
writer.WriteFloat(syncData.quatRotation.eulerAngles.y);
|
||||
}
|
||||
|
||||
if ((syncData.changedDataByte & Changed.RotZ) > 0)
|
||||
{
|
||||
writer.WriteFloat(syncData.quatRotation.eulerAngles.z);
|
||||
}
|
||||
}
|
||||
|
||||
// Write scale
|
||||
if ((syncData.changedDataByte & Changed.Scale) > 0)
|
||||
{
|
||||
writer.WriteVector3(syncData.scale);
|
||||
}
|
||||
}
|
||||
|
||||
public static SyncData ReadSyncData(this NetworkReader reader)
|
||||
{
|
||||
Changed changedData = (Changed)reader.ReadByte();
|
||||
|
||||
// If we have nothing to read here, let's say because posX is unchanged, then we can write anything
|
||||
// for now, but in the NT, we will need to check changedData again, to put the right values of the axis
|
||||
// back. We don't have it here.
|
||||
|
||||
Vector3 position =
|
||||
new Vector3(
|
||||
(changedData & Changed.PosX) > 0 ? reader.ReadFloat() : 0,
|
||||
(changedData & Changed.PosY) > 0 ? reader.ReadFloat() : 0,
|
||||
(changedData & Changed.PosZ) > 0 ? reader.ReadFloat() : 0
|
||||
);
|
||||
|
||||
Vector3 vecRotation = new Vector3();
|
||||
Quaternion quatRotation = new Quaternion();
|
||||
|
||||
if ((changedData & Changed.CompressRot) > 0)
|
||||
{
|
||||
quatRotation = (changedData & Changed.RotX) > 0 ? Compression.DecompressQuaternion(reader.ReadUInt()) : new Quaternion();
|
||||
}
|
||||
else
|
||||
{
|
||||
vecRotation =
|
||||
new Vector3(
|
||||
(changedData & Changed.RotX) > 0 ? reader.ReadFloat() : 0,
|
||||
(changedData & Changed.RotY) > 0 ? reader.ReadFloat() : 0,
|
||||
(changedData & Changed.RotZ) > 0 ? reader.ReadFloat() : 0
|
||||
);
|
||||
}
|
||||
|
||||
Vector3 scale = (changedData & Changed.Scale) == Changed.Scale ? reader.ReadVector3() : new Vector3();
|
||||
|
||||
SyncData _syncData = (changedData & Changed.CompressRot) > 0 ? new SyncData(changedData, position, quatRotation, scale) : new SyncData(changedData, position, vecRotation, scale);
|
||||
|
||||
return _syncData;
|
||||
}
|
||||
}
|
||||
}
|
||||
using UnityEngine;
|
||||
using System;
|
||||
using Mirror;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
[Serializable]
|
||||
public struct SyncData
|
||||
{
|
||||
public Changed changedDataByte;
|
||||
public Vector3 position;
|
||||
public Quaternion quatRotation;
|
||||
public Vector3 vecRotation;
|
||||
public Vector3 scale;
|
||||
|
||||
public SyncData(Changed _dataChangedByte, Vector3 _position, Quaternion _rotation, Vector3 _scale)
|
||||
{
|
||||
this.changedDataByte = _dataChangedByte;
|
||||
this.position = _position;
|
||||
this.quatRotation = _rotation;
|
||||
this.vecRotation = quatRotation.eulerAngles;
|
||||
this.scale = _scale;
|
||||
}
|
||||
|
||||
public SyncData(Changed _dataChangedByte, TransformSnapshot _snapshot)
|
||||
{
|
||||
this.changedDataByte = _dataChangedByte;
|
||||
this.position = _snapshot.position;
|
||||
this.quatRotation = _snapshot.rotation;
|
||||
this.vecRotation = quatRotation.eulerAngles;
|
||||
this.scale = _snapshot.scale;
|
||||
}
|
||||
|
||||
public SyncData(Changed _dataChangedByte, Vector3 _position, Vector3 _vecRotation, Vector3 _scale)
|
||||
{
|
||||
this.changedDataByte = _dataChangedByte;
|
||||
this.position = _position;
|
||||
this.vecRotation = _vecRotation;
|
||||
this.quatRotation = Quaternion.Euler(vecRotation);
|
||||
this.scale = _scale;
|
||||
}
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum Changed : byte
|
||||
{
|
||||
None = 0,
|
||||
PosX = 1 << 0,
|
||||
PosY = 1 << 1,
|
||||
PosZ = 1 << 2,
|
||||
CompressRot = 1 << 3,
|
||||
RotX = 1 << 4,
|
||||
RotY = 1 << 5,
|
||||
RotZ = 1 << 6,
|
||||
Scale = 1 << 7,
|
||||
|
||||
Pos = PosX | PosY | PosZ,
|
||||
Rot = RotX | RotY | RotZ
|
||||
}
|
||||
|
||||
|
||||
public static class SyncDataReaderWriter
|
||||
{
|
||||
public static void WriteSyncData(this NetworkWriter writer, SyncData syncData)
|
||||
{
|
||||
writer.WriteByte((byte)syncData.changedDataByte);
|
||||
|
||||
// Write position
|
||||
if ((syncData.changedDataByte & Changed.PosX) > 0)
|
||||
{
|
||||
writer.WriteFloat(syncData.position.x);
|
||||
}
|
||||
|
||||
if ((syncData.changedDataByte & Changed.PosY) > 0)
|
||||
{
|
||||
writer.WriteFloat(syncData.position.y);
|
||||
}
|
||||
|
||||
if ((syncData.changedDataByte & Changed.PosZ) > 0)
|
||||
{
|
||||
writer.WriteFloat(syncData.position.z);
|
||||
}
|
||||
|
||||
// Write rotation
|
||||
if ((syncData.changedDataByte & Changed.CompressRot) > 0)
|
||||
{
|
||||
if((syncData.changedDataByte & Changed.Rot) > 0)
|
||||
{
|
||||
writer.WriteUInt(Compression.CompressQuaternion(syncData.quatRotation));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if ((syncData.changedDataByte & Changed.RotX) > 0)
|
||||
{
|
||||
writer.WriteFloat(syncData.quatRotation.eulerAngles.x);
|
||||
}
|
||||
|
||||
if ((syncData.changedDataByte & Changed.RotY) > 0)
|
||||
{
|
||||
writer.WriteFloat(syncData.quatRotation.eulerAngles.y);
|
||||
}
|
||||
|
||||
if ((syncData.changedDataByte & Changed.RotZ) > 0)
|
||||
{
|
||||
writer.WriteFloat(syncData.quatRotation.eulerAngles.z);
|
||||
}
|
||||
}
|
||||
|
||||
// Write scale
|
||||
if ((syncData.changedDataByte & Changed.Scale) > 0)
|
||||
{
|
||||
writer.WriteVector3(syncData.scale);
|
||||
}
|
||||
}
|
||||
|
||||
public static SyncData ReadSyncData(this NetworkReader reader)
|
||||
{
|
||||
Changed changedData = (Changed)reader.ReadByte();
|
||||
|
||||
// If we have nothing to read here, let's say because posX is unchanged, then we can write anything
|
||||
// for now, but in the NT, we will need to check changedData again, to put the right values of the axis
|
||||
// back. We don't have it here.
|
||||
|
||||
Vector3 position =
|
||||
new Vector3(
|
||||
(changedData & Changed.PosX) > 0 ? reader.ReadFloat() : 0,
|
||||
(changedData & Changed.PosY) > 0 ? reader.ReadFloat() : 0,
|
||||
(changedData & Changed.PosZ) > 0 ? reader.ReadFloat() : 0
|
||||
);
|
||||
|
||||
Vector3 vecRotation = new Vector3();
|
||||
Quaternion quatRotation = new Quaternion();
|
||||
|
||||
if ((changedData & Changed.CompressRot) > 0)
|
||||
{
|
||||
quatRotation = (changedData & Changed.RotX) > 0 ? Compression.DecompressQuaternion(reader.ReadUInt()) : new Quaternion();
|
||||
}
|
||||
else
|
||||
{
|
||||
vecRotation =
|
||||
new Vector3(
|
||||
(changedData & Changed.RotX) > 0 ? reader.ReadFloat() : 0,
|
||||
(changedData & Changed.RotY) > 0 ? reader.ReadFloat() : 0,
|
||||
(changedData & Changed.RotZ) > 0 ? reader.ReadFloat() : 0
|
||||
);
|
||||
}
|
||||
|
||||
Vector3 scale = (changedData & Changed.Scale) == Changed.Scale ? reader.ReadVector3() : new Vector3();
|
||||
|
||||
SyncData _syncData = (changedData & Changed.CompressRot) > 0 ? new SyncData(changedData, position, quatRotation, scale) : new SyncData(changedData, position, vecRotation, scale);
|
||||
|
||||
return _syncData;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a1c0832ca88e749ff96fe04cebb617ef
|
||||
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/NetworkTransform/TransformSyncData.cs
|
||||
uploadId: 736421
|
||||
fileFormatVersion: 2
|
||||
guid: a1c0832ca88e749ff96fe04cebb617ef
|
||||
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/NetworkTransform/TransformSyncData.cs
|
||||
uploadId: 736421
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 09cc6745984c453a8cfb4cf4244d2570
|
||||
timeCreated: 1693576410
|
||||
fileFormatVersion: 2
|
||||
guid: 09cc6745984c453a8cfb4cf4244d2570
|
||||
timeCreated: 1693576410
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user