This commit is contained in:
2025-06-16 15:24:27 +02:00
commit 83a46b2fc4
1452 changed files with 214261 additions and 0 deletions

View File

@ -0,0 +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")]

View File

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

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: b5dcf9618f5e14a4eb60bff5480284a6
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +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
}
}

View File

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

View File

@ -0,0 +1,470 @@
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
};
// 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();
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
}
}

View File

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

View File

@ -0,0 +1,132 @@
using System.Collections.Generic;
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 (networkDiscovery == null)
{
networkDiscovery = GetComponent<NetworkDiscovery>();
UnityEditor.Events.UnityEventTools.AddPersistentListener(networkDiscovery.OnServerFound, OnDiscoveredServer);
UnityEditor.Undo.RecordObjects(new 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)
{
// 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;
}
}
}

View File

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

View File

@ -0,0 +1,4 @@
namespace Mirror.Discovery
{
public struct ServerRequest : NetworkMessage {}
}

View File

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

View File

@ -0,0 +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;
}
}

View File

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

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: bfbf2a1f2b300c5489dcab219ef2846e
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,97 @@
using System;
using UnityEngine;
namespace Mirror.Experimental
{
[AddComponentMenu("Network/ Experimental/Network Lerp Rigidbody")]
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-lerp-rigidbody")]
[Obsolete("Use the new NetworkRigidbodyReliable/Unreliable component with Snapshot Interpolation instead.")]
public class NetworkLerpRigidbody : NetworkBehaviour
{
[Header("Settings")]
[SerializeField] internal Rigidbody target = null;
[Tooltip("How quickly current velocity approaches target velocity")]
[SerializeField] float lerpVelocityAmount = 0.5f;
[Tooltip("How quickly current position approaches target position")]
[SerializeField] float lerpPositionAmount = 0.5f;
[Tooltip("Set to true if moves come from owner client, set to false if moves always come from server")]
[SerializeField] bool clientAuthority = false;
double nextSyncTime;
[SyncVar()]
Vector3 targetVelocity;
[SyncVar()]
Vector3 targetPosition;
/// <summary>
/// Ignore value if is host or client with Authority
/// </summary>
bool IgnoreSync => isServer || ClientWithAuthority;
bool ClientWithAuthority => clientAuthority && isOwned;
protected override void OnValidate()
{
base.OnValidate();
Reset();
}
public virtual void Reset()
{
if (target == null)
target = GetComponent<Rigidbody>();
syncDirection = SyncDirection.ClientToServer;
}
void Update()
{
if (isServer)
SyncToClients();
else if (ClientWithAuthority)
SendToServer();
}
void SyncToClients()
{
targetVelocity = target.velocity;
targetPosition = target.position;
}
void SendToServer()
{
double now = NetworkTime.localTime; // Unity 2019 doesn't have Time.timeAsDouble yet
if (now > nextSyncTime)
{
nextSyncTime = now + syncInterval;
CmdSendState(target.velocity, target.position);
}
}
[Command]
void CmdSendState(Vector3 velocity, Vector3 position)
{
target.velocity = velocity;
target.position = position;
targetVelocity = velocity;
targetPosition = position;
}
void FixedUpdate()
{
if (IgnoreSync) { return; }
target.velocity = Vector3.Lerp(target.velocity, targetVelocity, lerpVelocityAmount);
target.position = Vector3.Lerp(target.position, targetPosition, lerpPositionAmount);
// add velocity to position as position would have moved on server at that velocity
target.position += target.velocity * Time.fixedDeltaTime;
// TODO does this also need to sync acceleration so and update velocity?
}
}
}

View File

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

View File

@ -0,0 +1,354 @@
using System;
using UnityEngine;
namespace Mirror.Experimental
{
[AddComponentMenu("Network/ Experimental/Network Rigidbody")]
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-rigidbody")]
[Obsolete("Use the new NetworkRigidbodyReliable/Unreliable component with Snapshot Interpolation instead.")]
public class NetworkRigidbody : NetworkBehaviour
{
[Header("Settings")]
[SerializeField] internal Rigidbody target = null;
[Tooltip("Set to true if moves come from owner client, set to false if moves always come from server")]
public bool clientAuthority = false;
[Header("Velocity")]
[Tooltip("Syncs Velocity every SyncInterval")]
[SerializeField] bool syncVelocity = true;
[Tooltip("Set velocity to 0 each frame (only works if syncVelocity is false")]
[SerializeField] bool clearVelocity = false;
[Tooltip("Only Syncs Value if distance between previous and current is great than sensitivity")]
[SerializeField] float velocitySensitivity = 0.1f;
[Header("Angular Velocity")]
[Tooltip("Syncs AngularVelocity every SyncInterval")]
[SerializeField] bool syncAngularVelocity = true;
[Tooltip("Set angularVelocity to 0 each frame (only works if syncAngularVelocity is false")]
[SerializeField] bool clearAngularVelocity = false;
[Tooltip("Only Syncs Value if distance between previous and current is great than sensitivity")]
[SerializeField] float angularVelocitySensitivity = 0.1f;
/// <summary>
/// Values sent on client with authority after they are sent to the server
/// </summary>
readonly ClientSyncState previousValue = new ClientSyncState();
protected override void OnValidate()
{
base.OnValidate();
Reset();
}
public virtual void Reset()
{
if (target == null)
target = GetComponent<Rigidbody>();
syncDirection = SyncDirection.ClientToServer;
}
#region Sync vars
[SyncVar(hook = nameof(OnVelocityChanged))]
Vector3 velocity;
[SyncVar(hook = nameof(OnAngularVelocityChanged))]
Vector3 angularVelocity;
[SyncVar(hook = nameof(OnIsKinematicChanged))]
bool isKinematic;
[SyncVar(hook = nameof(OnUseGravityChanged))]
bool useGravity;
[SyncVar(hook = nameof(OnuDragChanged))]
float drag;
[SyncVar(hook = nameof(OnAngularDragChanged))]
float angularDrag;
/// <summary>
/// Ignore value if is host or client with Authority
/// </summary>
/// <returns></returns>
bool IgnoreSync => isServer || ClientWithAuthority;
bool ClientWithAuthority => clientAuthority && isOwned;
void OnVelocityChanged(Vector3 _, Vector3 newValue)
{
if (IgnoreSync)
return;
target.velocity = newValue;
}
void OnAngularVelocityChanged(Vector3 _, Vector3 newValue)
{
if (IgnoreSync)
return;
target.angularVelocity = newValue;
}
void OnIsKinematicChanged(bool _, bool newValue)
{
if (IgnoreSync)
return;
target.isKinematic = newValue;
}
void OnUseGravityChanged(bool _, bool newValue)
{
if (IgnoreSync)
return;
target.useGravity = newValue;
}
void OnuDragChanged(float _, float newValue)
{
if (IgnoreSync)
return;
target.drag = newValue;
}
void OnAngularDragChanged(float _, float newValue)
{
if (IgnoreSync)
return;
target.angularDrag = newValue;
}
#endregion
internal void Update()
{
if (isServer)
SyncToClients();
else if (ClientWithAuthority)
SendToServer();
}
internal void FixedUpdate()
{
if (clearAngularVelocity && !syncAngularVelocity)
target.angularVelocity = Vector3.zero;
if (clearVelocity && !syncVelocity)
target.velocity = Vector3.zero;
}
/// <summary>
/// Updates sync var values on server so that they sync to the client
/// </summary>
[Server]
void SyncToClients()
{
// only update if they have changed more than Sensitivity
Vector3 currentVelocity = syncVelocity ? target.velocity : default;
Vector3 currentAngularVelocity = syncAngularVelocity ? target.angularVelocity : default;
bool velocityChanged = syncVelocity && ((previousValue.velocity - currentVelocity).sqrMagnitude > velocitySensitivity * velocitySensitivity);
bool angularVelocityChanged = syncAngularVelocity && ((previousValue.angularVelocity - currentAngularVelocity).sqrMagnitude > angularVelocitySensitivity * angularVelocitySensitivity);
if (velocityChanged)
{
velocity = currentVelocity;
previousValue.velocity = currentVelocity;
}
if (angularVelocityChanged)
{
angularVelocity = currentAngularVelocity;
previousValue.angularVelocity = currentAngularVelocity;
}
// other rigidbody settings
isKinematic = target.isKinematic;
useGravity = target.useGravity;
drag = target.drag;
angularDrag = target.angularDrag;
}
/// <summary>
/// Uses Command to send values to server
/// </summary>
[Client]
void SendToServer()
{
if (!isOwned)
{
Debug.LogWarning("SendToServer called without authority");
return;
}
SendVelocity();
SendRigidBodySettings();
}
[Client]
void SendVelocity()
{
double now = NetworkTime.localTime; // Unity 2019 doesn't have Time.timeAsDouble yet
if (now < previousValue.nextSyncTime)
return;
Vector3 currentVelocity = syncVelocity ? target.velocity : default;
Vector3 currentAngularVelocity = syncAngularVelocity ? target.angularVelocity : default;
bool velocityChanged = syncVelocity && ((previousValue.velocity - currentVelocity).sqrMagnitude > velocitySensitivity * velocitySensitivity);
bool angularVelocityChanged = syncAngularVelocity && ((previousValue.angularVelocity - currentAngularVelocity).sqrMagnitude > angularVelocitySensitivity * angularVelocitySensitivity);
// if angularVelocity has changed it is likely that velocity has also changed so just sync both values
// however if only velocity has changed just send velocity
if (angularVelocityChanged)
{
CmdSendVelocityAndAngular(currentVelocity, currentAngularVelocity);
previousValue.velocity = currentVelocity;
previousValue.angularVelocity = currentAngularVelocity;
}
else if (velocityChanged)
{
CmdSendVelocity(currentVelocity);
previousValue.velocity = currentVelocity;
}
// only update syncTime if either has changed
if (angularVelocityChanged || velocityChanged)
previousValue.nextSyncTime = now + syncInterval;
}
[Client]
void SendRigidBodySettings()
{
// These shouldn't change often so it is ok to send in their own Command
if (previousValue.isKinematic != target.isKinematic)
{
CmdSendIsKinematic(target.isKinematic);
previousValue.isKinematic = target.isKinematic;
}
if (previousValue.useGravity != target.useGravity)
{
CmdSendUseGravity(target.useGravity);
previousValue.useGravity = target.useGravity;
}
if (previousValue.drag != target.drag)
{
CmdSendDrag(target.drag);
previousValue.drag = target.drag;
}
if (previousValue.angularDrag != target.angularDrag)
{
CmdSendAngularDrag(target.angularDrag);
previousValue.angularDrag = target.angularDrag;
}
}
/// <summary>
/// Called when only Velocity has changed on the client
/// </summary>
[Command]
void CmdSendVelocity(Vector3 velocity)
{
// Ignore messages from client if not in client authority mode
if (!clientAuthority)
return;
this.velocity = velocity;
target.velocity = velocity;
}
/// <summary>
/// Called when angularVelocity has changed on the client
/// </summary>
[Command]
void CmdSendVelocityAndAngular(Vector3 velocity, Vector3 angularVelocity)
{
// Ignore messages from client if not in client authority mode
if (!clientAuthority)
return;
if (syncVelocity)
{
this.velocity = velocity;
target.velocity = velocity;
}
this.angularVelocity = angularVelocity;
target.angularVelocity = angularVelocity;
}
[Command]
void CmdSendIsKinematic(bool isKinematic)
{
// Ignore messages from client if not in client authority mode
if (!clientAuthority)
return;
this.isKinematic = isKinematic;
target.isKinematic = isKinematic;
}
[Command]
void CmdSendUseGravity(bool useGravity)
{
// Ignore messages from client if not in client authority mode
if (!clientAuthority)
return;
this.useGravity = useGravity;
target.useGravity = useGravity;
}
[Command]
void CmdSendDrag(float drag)
{
// Ignore messages from client if not in client authority mode
if (!clientAuthority)
return;
this.drag = drag;
target.drag = drag;
}
[Command]
void CmdSendAngularDrag(float angularDrag)
{
// Ignore messages from client if not in client authority mode
if (!clientAuthority)
return;
this.angularDrag = angularDrag;
target.angularDrag = angularDrag;
}
/// <summary>
/// holds previously synced values
/// </summary>
public class ClientSyncState
{
/// <summary>
/// Next sync time that velocity will be synced, based on syncInterval.
/// </summary>
public double nextSyncTime;
public Vector3 velocity;
public Vector3 angularVelocity;
public bool isKinematic;
public bool useGravity;
public float drag;
public float angularDrag;
}
}
}

View File

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

View File

@ -0,0 +1,349 @@
using UnityEngine;
namespace Mirror.Experimental
{
[AddComponentMenu("Network/ Experimental/Network Rigidbody 2D")]
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-rigidbody")]
public class NetworkRigidbody2D : NetworkBehaviour
{
[Header("Settings")]
[SerializeField] internal Rigidbody2D target = null;
[Tooltip("Set to true if moves come from owner client, set to false if moves always come from server")]
public bool clientAuthority = false;
[Header("Velocity")]
[Tooltip("Syncs Velocity every SyncInterval")]
[SerializeField] bool syncVelocity = true;
[Tooltip("Set velocity to 0 each frame (only works if syncVelocity is false")]
[SerializeField] bool clearVelocity = false;
[Tooltip("Only Syncs Value if distance between previous and current is great than sensitivity")]
[SerializeField] float velocitySensitivity = 0.1f;
[Header("Angular Velocity")]
[Tooltip("Syncs AngularVelocity every SyncInterval")]
[SerializeField] bool syncAngularVelocity = true;
[Tooltip("Set angularVelocity to 0 each frame (only works if syncAngularVelocity is false")]
[SerializeField] bool clearAngularVelocity = false;
[Tooltip("Only Syncs Value if distance between previous and current is great than sensitivity")]
[SerializeField] float angularVelocitySensitivity = 0.1f;
/// <summary>
/// Values sent on client with authority after they are sent to the server
/// </summary>
readonly ClientSyncState previousValue = new ClientSyncState();
protected override void OnValidate()
{
base.OnValidate();
Reset();
}
public virtual void Reset()
{
if (target == null)
target = GetComponent<Rigidbody2D>();
syncDirection = SyncDirection.ClientToServer;
}
#region Sync vars
[SyncVar(hook = nameof(OnVelocityChanged))]
Vector2 velocity;
[SyncVar(hook = nameof(OnAngularVelocityChanged))]
float angularVelocity;
[SyncVar(hook = nameof(OnIsKinematicChanged))]
bool isKinematic;
[SyncVar(hook = nameof(OnGravityScaleChanged))]
float gravityScale;
[SyncVar(hook = nameof(OnuDragChanged))]
float drag;
[SyncVar(hook = nameof(OnAngularDragChanged))]
float angularDrag;
/// <summary>
/// Ignore value if is host or client with Authority
/// </summary>
bool IgnoreSync => isServer || ClientWithAuthority;
bool ClientWithAuthority => clientAuthority && isOwned;
void OnVelocityChanged(Vector2 _, Vector2 newValue)
{
if (IgnoreSync)
return;
target.linearVelocity = newValue;
}
void OnAngularVelocityChanged(float _, float newValue)
{
if (IgnoreSync)
return;
target.angularVelocity = newValue;
}
void OnIsKinematicChanged(bool _, bool newValue)
{
if (IgnoreSync)
return;
target.isKinematic = newValue;
}
void OnGravityScaleChanged(float _, float newValue)
{
if (IgnoreSync)
return;
target.gravityScale = newValue;
}
void OnuDragChanged(float _, float newValue)
{
if (IgnoreSync)
return;
target.linearDamping = newValue;
}
void OnAngularDragChanged(float _, float newValue)
{
if (IgnoreSync)
return;
target.angularDamping = newValue;
}
#endregion
internal void Update()
{
if (isServer)
SyncToClients();
else if (ClientWithAuthority)
SendToServer();
}
internal void FixedUpdate()
{
if (clearAngularVelocity && !syncAngularVelocity)
target.angularVelocity = 0f;
if (clearVelocity && !syncVelocity)
target.linearVelocity = Vector2.zero;
}
/// <summary>
/// Updates sync var values on server so that they sync to the client
/// </summary>
[Server]
void SyncToClients()
{
// only update if they have changed more than Sensitivity
Vector2 currentVelocity = syncVelocity ? target.linearVelocity : default;
float currentAngularVelocity = syncAngularVelocity ? target.angularVelocity : default;
bool velocityChanged = syncVelocity && ((previousValue.velocity - currentVelocity).sqrMagnitude > velocitySensitivity * velocitySensitivity);
bool angularVelocityChanged = syncAngularVelocity && ((previousValue.angularVelocity - currentAngularVelocity) > angularVelocitySensitivity);
if (velocityChanged)
{
velocity = currentVelocity;
previousValue.velocity = currentVelocity;
}
if (angularVelocityChanged)
{
angularVelocity = currentAngularVelocity;
previousValue.angularVelocity = currentAngularVelocity;
}
// other rigidbody settings
isKinematic = target.isKinematic;
gravityScale = target.gravityScale;
drag = target.linearDamping;
angularDrag = target.angularDamping;
}
/// <summary>
/// Uses Command to send values to server
/// </summary>
[Client]
void SendToServer()
{
if (!isOwned)
{
Debug.LogWarning("SendToServer called without authority");
return;
}
SendVelocity();
SendRigidBodySettings();
}
[Client]
void SendVelocity()
{
float now = Time.time;
if (now < previousValue.nextSyncTime)
return;
Vector2 currentVelocity = syncVelocity ? target.linearVelocity : default;
float currentAngularVelocity = syncAngularVelocity ? target.angularVelocity : default;
bool velocityChanged = syncVelocity && ((previousValue.velocity - currentVelocity).sqrMagnitude > velocitySensitivity * velocitySensitivity);
bool angularVelocityChanged = syncAngularVelocity && previousValue.angularVelocity != currentAngularVelocity;//((previousValue.angularVelocity - currentAngularVelocity).sqrMagnitude > angularVelocitySensitivity * angularVelocitySensitivity);
// if angularVelocity has changed it is likely that velocity has also changed so just sync both values
// however if only velocity has changed just send velocity
if (angularVelocityChanged)
{
CmdSendVelocityAndAngular(currentVelocity, currentAngularVelocity);
previousValue.velocity = currentVelocity;
previousValue.angularVelocity = currentAngularVelocity;
}
else if (velocityChanged)
{
CmdSendVelocity(currentVelocity);
previousValue.velocity = currentVelocity;
}
// only update syncTime if either has changed
if (angularVelocityChanged || velocityChanged)
previousValue.nextSyncTime = now + syncInterval;
}
[Client]
void SendRigidBodySettings()
{
// These shouldn't change often so it is ok to send in their own Command
if (previousValue.isKinematic != target.isKinematic)
{
CmdSendIsKinematic(target.isKinematic);
previousValue.isKinematic = target.isKinematic;
}
if (previousValue.gravityScale != target.gravityScale)
{
CmdChangeGravityScale(target.gravityScale);
previousValue.gravityScale = target.gravityScale;
}
if (previousValue.drag != target.linearDamping)
{
CmdSendDrag(target.linearDamping);
previousValue.drag = target.linearDamping;
}
if (previousValue.angularDrag != target.angularDamping)
{
CmdSendAngularDrag(target.angularDamping);
previousValue.angularDrag = target.angularDamping;
}
}
/// <summary>
/// Called when only Velocity has changed on the client
/// </summary>
[Command]
void CmdSendVelocity(Vector2 velocity)
{
// Ignore messages from client if not in client authority mode
if (!clientAuthority)
return;
this.velocity = velocity;
target.linearVelocity = velocity;
}
/// <summary>
/// Called when angularVelocity has changed on the client
/// </summary>
[Command]
void CmdSendVelocityAndAngular(Vector2 velocity, float angularVelocity)
{
// Ignore messages from client if not in client authority mode
if (!clientAuthority)
return;
if (syncVelocity)
{
this.velocity = velocity;
target.linearVelocity = velocity;
}
this.angularVelocity = angularVelocity;
target.angularVelocity = angularVelocity;
}
[Command]
void CmdSendIsKinematic(bool isKinematic)
{
// Ignore messages from client if not in client authority mode
if (!clientAuthority)
return;
this.isKinematic = isKinematic;
target.isKinematic = isKinematic;
}
[Command]
void CmdChangeGravityScale(float gravityScale)
{
// Ignore messages from client if not in client authority mode
if (!clientAuthority)
return;
this.gravityScale = gravityScale;
target.gravityScale = gravityScale;
}
[Command]
void CmdSendDrag(float drag)
{
// Ignore messages from client if not in client authority mode
if (!clientAuthority)
return;
this.drag = drag;
target.linearDamping = drag;
}
[Command]
void CmdSendAngularDrag(float angularDrag)
{
// Ignore messages from client if not in client authority mode
if (!clientAuthority)
return;
this.angularDrag = angularDrag;
target.angularDamping = angularDrag;
}
/// <summary>
/// holds previously synced values
/// </summary>
public class ClientSyncState
{
/// <summary>
/// Next sync time that velocity will be synced, based on syncInterval.
/// </summary>
public float nextSyncTime;
public Vector2 velocity;
public float angularVelocity;
public bool isKinematic;
public float gravityScale;
public float drag;
public float angularDrag;
}
}
}

View File

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

View File

@ -0,0 +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();
}
}
}

View File

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

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c66f27e006ab94253b39a55a3b213651
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

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

View File

@ -0,0 +1,90 @@
// 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);
}
}
}
}
// internal so we can update from tests
[ServerCallback]
internal void Update()
{
// rebuild all spawned NetworkIdentity's observers every interval
if (NetworkTime.localTime >= lastRebuildTime + rebuildInterval)
{
RebuildAll();
lastRebuildTime = NetworkTime.localTime;
}
}
}
}

View File

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

View File

@ -0,0 +1,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;
}
}

View File

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

View File

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

View File

@ -0,0 +1,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);
}
}
}

View File

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

View File

@ -0,0 +1,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);
}
}
}
}

View File

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

View File

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

View File

@ -0,0 +1,118 @@
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 Update.
// Multiple objects could be destroyed in same frame and we don't
// want to rebuild for each one...let Update do it once.
// We must add the current scene to dirtyScenes for Update 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);
}
}
// internal so we can update from tests
[ServerCallback]
internal void Update()
{
// 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);
}
}
}

View File

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

View File

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

View File

@ -0,0 +1,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();
}
}
}

View File

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

View File

@ -0,0 +1,154 @@
// extremely fast spatial hashing interest management based on uMMORPG GridChecker.
// => 30x faster in initial tests
// => scales way higher
using System.Collections.Generic;
using UnityEngine;
namespace Mirror
{
[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;
// debugging
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
}
}

View File

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

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 2d418e60072433b4bbebbf5f3a7de1bb
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,39 @@
// simple component that holds team information
using System;
using UnityEngine;
namespace Mirror
{
[DisallowMultipleComponent]
[AddComponentMenu("Network/ Interest Management/ Team/Network Team")]
[HelpURL("https://mirror-networking.gitbook.io/docs/guides/interest-management")]
public class NetworkTeam : NetworkBehaviour
{
[SerializeField]
[Tooltip("Set teamId on Server at runtime to the same value on all networked objects that belong to a given team")]
string _teamId;
public string teamId
{
get => _teamId;
set
{
if (Application.IsPlaying(gameObject) && !NetworkServer.active)
throw new InvalidOperationException("teamId can only be set at runtime on active server");
if (_teamId == value)
return;
string oldTeam = _teamId;
_teamId = value;
//Only inform the AOI if this netIdentity has been spawned(isServer) and only if using a TeamInterestManagement
if (isServer && NetworkServer.aoi is TeamInterestManagement teamInterestManagement)
teamInterestManagement.OnTeamChanged(this, oldTeam);
}
}
[Tooltip("When enabled this object is visible to all clients. Typically this would be true for player objects")]
public bool forceShown;
}
}

View File

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

View File

@ -0,0 +1,182 @@
using System.Collections.Generic;
using UnityEngine;
namespace Mirror
{
[AddComponentMenu("Network/ Interest Management/ Team/Team Interest Management")]
public class TeamInterestManagement : InterestManagement
{
readonly Dictionary<string, HashSet<NetworkTeam>> teamObjects =
new Dictionary<string, HashSet<NetworkTeam>>();
readonly HashSet<string> dirtyTeams = new HashSet<string>();
// LateUpdate so that all spawns/despawns/changes are done
[ServerCallback]
void LateUpdate()
{
// Rebuild all dirty teams
// dirtyTeams will be empty if no teams changed members
// by spawning or destroying or changing teamId in this frame.
foreach (string dirtyTeam in dirtyTeams)
{
// rebuild always, even if teamObjects[dirtyTeam] is empty.
// Players might have left the team, but they may still be spawned.
RebuildTeamObservers(dirtyTeam);
// clean up empty entries in the dict
if (teamObjects[dirtyTeam].Count == 0)
teamObjects.Remove(dirtyTeam);
}
dirtyTeams.Clear();
}
[ServerCallback]
void RebuildTeamObservers(string teamId)
{
foreach (NetworkTeam networkTeam in teamObjects[teamId])
if (networkTeam.netIdentity != null)
NetworkServer.RebuildObservers(networkTeam.netIdentity, false);
}
// called by NetworkTeam.teamId setter
[ServerCallback]
internal void OnTeamChanged(NetworkTeam networkTeam, string oldTeam)
{
// This object is in a new team so observers in the prior team
// and the new team need to rebuild their respective observers lists.
// Remove this object from the hashset of the team it just left
// Null / Empty string is never a valid teamId
if (!string.IsNullOrWhiteSpace(oldTeam))
{
dirtyTeams.Add(oldTeam);
teamObjects[oldTeam].Remove(networkTeam);
}
// Null / Empty string is never a valid teamId
if (string.IsNullOrWhiteSpace(networkTeam.teamId))
return;
dirtyTeams.Add(networkTeam.teamId);
// Make sure this new team is in the dictionary
if (!teamObjects.ContainsKey(networkTeam.teamId))
teamObjects[networkTeam.teamId] = new HashSet<NetworkTeam>();
// Add this object to the hashset of the new team
teamObjects[networkTeam.teamId].Add(networkTeam);
}
[ServerCallback]
public override void OnSpawned(NetworkIdentity identity)
{
if (!identity.TryGetComponent(out NetworkTeam networkTeam))
return;
string networkTeamId = networkTeam.teamId;
// Null / Empty string is never a valid teamId...do not add to teamObjects collection
if (string.IsNullOrWhiteSpace(networkTeamId))
return;
// Debug.Log($"TeamInterestManagement.OnSpawned({identity.name}) currentTeam: {currentTeam}");
if (!teamObjects.TryGetValue(networkTeamId, out HashSet<NetworkTeam> objects))
{
objects = new HashSet<NetworkTeam>();
teamObjects.Add(networkTeamId, objects);
}
objects.Add(networkTeam);
// Team ID could have been set in NetworkBehaviour::OnStartServer on this object.
// Since that's after OnCheckObserver is called it would be missed, so force Rebuild here.
// Add the current team to dirtyTeames for LateUpdate to rebuild it.
dirtyTeams.Add(networkTeamId);
}
[ServerCallback]
public override void OnDestroyed(NetworkIdentity identity)
{
// Don't RebuildSceneObservers here - that will happen in LateUpdate.
// Multiple objects could be destroyed in same frame and we don't
// want to rebuild for each one...let LateUpdate do it once.
// We must add the current team to dirtyTeames for LateUpdate to rebuild it.
if (identity.TryGetComponent(out NetworkTeam currentTeam))
{
if (!string.IsNullOrWhiteSpace(currentTeam.teamId) &&
teamObjects.TryGetValue(currentTeam.teamId, out HashSet<NetworkTeam> objects) &&
objects.Remove(currentTeam))
dirtyTeams.Add(currentTeam.teamId);
}
}
public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnectionToClient newObserver)
{
// Always observed if no NetworkTeam component
if (!identity.TryGetComponent(out NetworkTeam identityNetworkTeam))
return true;
if (identityNetworkTeam.forceShown)
return true;
// Null / Empty string is never a valid teamId
if (string.IsNullOrWhiteSpace(identityNetworkTeam.teamId))
return false;
// Always observed if no NetworkTeam component
if (!newObserver.identity.TryGetComponent(out NetworkTeam newObserverNetworkTeam))
return true;
// Null / Empty string is never a valid teamId
if (string.IsNullOrWhiteSpace(newObserverNetworkTeam.teamId))
return false;
//Debug.Log($"TeamInterestManagement.OnCheckObserver {identity.name} {identityNetworkTeam.teamId} | {newObserver.identity.name} {newObserverNetworkTeam.teamId}");
// Observed only if teamId's team
return identityNetworkTeam.teamId == newObserverNetworkTeam.teamId;
}
public override void OnRebuildObservers(NetworkIdentity identity, HashSet<NetworkConnectionToClient> newObservers)
{
// If this object doesn't have a NetworkTeam then it's visible to all clients
if (!identity.TryGetComponent(out NetworkTeam networkTeam))
{
AddAllConnections(newObservers);
return;
}
// If this object has NetworkTeam and forceShown == true then it's visible to all clients
if (networkTeam.forceShown)
{
AddAllConnections(newObservers);
return;
}
// Null / Empty string is never a valid teamId
if (string.IsNullOrWhiteSpace(networkTeam.teamId))
return;
// Abort if this team hasn't been created yet by OnSpawned or OnTeamChanged
if (!teamObjects.TryGetValue(networkTeam.teamId, out HashSet<NetworkTeam> objects))
return;
// Add everything in the hashset for this object's current team
foreach (NetworkTeam netTeam in objects)
if (netTeam.netIdentity != null && netTeam.netIdentity.connectionToClient != null)
newObservers.Add(netTeam.netIdentity.connectionToClient);
}
void AddAllConnections(HashSet<NetworkConnectionToClient> newObservers)
{
foreach (NetworkConnectionToClient conn in NetworkServer.connections.Values)
{
// authenticated and joined world with a player?
if (conn != null && conn.isAuthenticated && conn.identity != null)
newObservers.Add(conn);
}
}
}
}

View File

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

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 00ac1d0527f234939aba22b4d7cbf280
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,107 @@
// 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
{
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);
}
}
}

View File

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

View File

@ -0,0 +1,196 @@
// 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})";
}
[Obsolete("This is a preview version. Community feedback is welcome!")]
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;
protected virtual void Update()
{
// only capture on server
if (!NetworkServer.active) return;
// 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();
}
}
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).
[Server]
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.
[Server]
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.
[Server]
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;
}
}
}

View File

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

View File

@ -0,0 +1,16 @@
{
"name": "Mirror.Components",
"rootNamespace": "",
"references": [
"GUID:30817c1a0e6d646d99c048fc403f5979"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": true,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 72872094b21c16e48b631b2224833d49
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,655 @@
using System.Linq;
using UnityEngine;
using UnityEngine.Serialization;
namespace Mirror
{
/// <summary>
/// A component to synchronize Mecanim animation states for networked objects.
/// </summary>
/// <remarks>
/// <para>The animation of game objects can be networked by this component. There are two models of authority for networked movement:</para>
/// <para>If the object has authority on the client, then it should be animated locally on the owning client. The animation state information will be sent from the owning client to the server, then broadcast to all of the other clients. This is common for player objects.</para>
/// <para>If the object has authority on the server, then it should be animated on the server and state information will be sent to all clients. This is common for objects not related to a specific client, such as an enemy unit.</para>
/// <para>The NetworkAnimator synchronizes all animation parameters of the selected Animator. It does not automatically synchronize triggers. The function SetTrigger can by used by an object with authority to fire an animation trigger on other clients.</para>
/// </remarks>
// [RequireComponent(typeof(NetworkIdentity))] disabled to allow child NetworkBehaviours
[AddComponentMenu("Network/Network Animator")]
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-animator")]
public class NetworkAnimator : NetworkBehaviour
{
[Header("Authority")]
[Tooltip("Set to true if animations come from owner client, set to false if animations always come from server")]
public bool clientAuthority;
/// <summary>
/// The animator component to synchronize.
/// </summary>
[FormerlySerializedAs("m_Animator")]
[Header("Animator")]
[Tooltip("Animator that will have parameters synchronized")]
public Animator animator;
/// <summary>
/// Syncs animator.speed
/// </summary>
[SyncVar(hook = nameof(OnAnimatorSpeedChanged))]
float animatorSpeed;
float previousSpeed;
// Note: not an object[] array because otherwise initialization is real annoying
int[] lastIntParameters;
float[] lastFloatParameters;
bool[] lastBoolParameters;
AnimatorControllerParameter[] parameters;
// multiple layers
int[] animationHash;
int[] transitionHash;
float[] layerWeight;
double nextSendTime;
bool SendMessagesAllowed
{
get
{
if (isServer)
{
if (!clientAuthority)
return true;
// This is a special case where we have client authority but we have not assigned the client who has
// authority over it, no animator data will be sent over the network by the server.
//
// So we check here for a connectionToClient and if it is null we will
// let the server send animation data until we receive an owner.
if (netIdentity != null && netIdentity.connectionToClient == null)
return true;
}
return (isOwned && clientAuthority);
}
}
void Initialize()
{
// store the animator parameters in a variable - the "Animator.parameters" getter allocates
// a new parameter array every time it is accessed so we should avoid doing it in a loop
parameters = animator.parameters
.Where(par => !animator.IsParameterControlledByCurve(par.nameHash))
.ToArray();
lastIntParameters = new int[parameters.Length];
lastFloatParameters = new float[parameters.Length];
lastBoolParameters = new bool[parameters.Length];
animationHash = new int[animator.layerCount];
transitionHash = new int[animator.layerCount];
layerWeight = new float[animator.layerCount];
}
// fix https://github.com/MirrorNetworking/Mirror/issues/2810
// both Awake and Enable need to initialize arrays.
// in case users call SetActive(false) -> SetActive(true).
void Awake() => Initialize();
void OnEnable() => Initialize();
public virtual void Reset()
{
syncDirection = SyncDirection.ClientToServer;
}
void FixedUpdate()
{
if (!SendMessagesAllowed)
return;
if (!animator.enabled)
return;
CheckSendRate();
for (int i = 0; i < animator.layerCount; i++)
{
int stateHash;
float normalizedTime;
if (!CheckAnimStateChanged(out stateHash, out normalizedTime, i))
{
continue;
}
using (NetworkWriterPooled writer = NetworkWriterPool.Get())
{
WriteParameters(writer);
SendAnimationMessage(stateHash, normalizedTime, i, layerWeight[i], writer.ToArray());
}
}
CheckSpeed();
}
void CheckSpeed()
{
float newSpeed = animator.speed;
if (Mathf.Abs(previousSpeed - newSpeed) > 0.001f)
{
previousSpeed = newSpeed;
if (isServer)
{
animatorSpeed = newSpeed;
}
else if (isClient)
{
CmdSetAnimatorSpeed(newSpeed);
}
}
}
void OnAnimatorSpeedChanged(float _, float value)
{
// skip if host or client with authority
// they will have already set the speed so don't set again
if (isServer || (isOwned && clientAuthority))
return;
animator.speed = value;
}
bool CheckAnimStateChanged(out int stateHash, out float normalizedTime, int layerId)
{
bool change = false;
stateHash = 0;
normalizedTime = 0;
float lw = animator.GetLayerWeight(layerId);
if (Mathf.Abs(lw - layerWeight[layerId]) > 0.001f)
{
layerWeight[layerId] = lw;
change = true;
}
if (animator.IsInTransition(layerId))
{
AnimatorTransitionInfo tt = animator.GetAnimatorTransitionInfo(layerId);
if (tt.fullPathHash != transitionHash[layerId])
{
// first time in this transition
transitionHash[layerId] = tt.fullPathHash;
animationHash[layerId] = 0;
return true;
}
return change;
}
AnimatorStateInfo st = animator.GetCurrentAnimatorStateInfo(layerId);
if (st.fullPathHash != animationHash[layerId])
{
// first time in this animation state
if (animationHash[layerId] != 0)
{
// came from another animation directly - from Play()
stateHash = st.fullPathHash;
normalizedTime = st.normalizedTime;
}
transitionHash[layerId] = 0;
animationHash[layerId] = st.fullPathHash;
return true;
}
return change;
}
void CheckSendRate()
{
double now = NetworkTime.localTime;
if (SendMessagesAllowed && syncInterval >= 0 && now > nextSendTime)
{
nextSendTime = now + syncInterval;
using (NetworkWriterPooled writer = NetworkWriterPool.Get())
{
if (WriteParameters(writer))
SendAnimationParametersMessage(writer.ToArray());
}
}
}
void SendAnimationMessage(int stateHash, float normalizedTime, int layerId, float weight, byte[] parameters)
{
if (isServer)
{
RpcOnAnimationClientMessage(stateHash, normalizedTime, layerId, weight, parameters);
}
else if (isClient)
{
CmdOnAnimationServerMessage(stateHash, normalizedTime, layerId, weight, parameters);
}
}
void SendAnimationParametersMessage(byte[] parameters)
{
if (isServer)
{
RpcOnAnimationParametersClientMessage(parameters);
}
else if (isClient)
{
CmdOnAnimationParametersServerMessage(parameters);
}
}
void HandleAnimMsg(int stateHash, float normalizedTime, int layerId, float weight, NetworkReader reader)
{
if (isOwned && clientAuthority)
return;
// usually transitions will be triggered by parameters, if not, play anims directly.
// NOTE: this plays "animations", not transitions, so any transitions will be skipped.
// NOTE: there is no API to play a transition(?)
if (stateHash != 0 && animator.enabled)
{
animator.Play(stateHash, layerId, normalizedTime);
}
animator.SetLayerWeight(layerId, weight);
ReadParameters(reader);
}
void HandleAnimParamsMsg(NetworkReader reader)
{
if (isOwned && clientAuthority)
return;
ReadParameters(reader);
}
void HandleAnimTriggerMsg(int hash)
{
if (animator.enabled)
animator.SetTrigger(hash);
}
void HandleAnimResetTriggerMsg(int hash)
{
if (animator.enabled)
animator.ResetTrigger(hash);
}
ulong NextDirtyBits()
{
ulong dirtyBits = 0;
for (int i = 0; i < parameters.Length; i++)
{
AnimatorControllerParameter par = parameters[i];
bool changed = false;
if (par.type == AnimatorControllerParameterType.Int)
{
int newIntValue = animator.GetInteger(par.nameHash);
changed = newIntValue != lastIntParameters[i];
if (changed)
lastIntParameters[i] = newIntValue;
}
else if (par.type == AnimatorControllerParameterType.Float)
{
float newFloatValue = animator.GetFloat(par.nameHash);
changed = Mathf.Abs(newFloatValue - lastFloatParameters[i]) > 0.001f;
// only set lastValue if it was changed, otherwise value could slowly drift within the 0.001f limit each frame
if (changed)
lastFloatParameters[i] = newFloatValue;
}
else if (par.type == AnimatorControllerParameterType.Bool)
{
bool newBoolValue = animator.GetBool(par.nameHash);
changed = newBoolValue != lastBoolParameters[i];
if (changed)
lastBoolParameters[i] = newBoolValue;
}
if (changed)
{
dirtyBits |= 1ul << i;
}
}
return dirtyBits;
}
bool WriteParameters(NetworkWriter writer, bool forceAll = false)
{
// fix: https://github.com/MirrorNetworking/Mirror/issues/2852
// serialize parameterCount to be 100% sure we deserialize correct amount of bytes.
// (255 parameters should be enough for everyone, write it as byte)
byte parameterCount = (byte)parameters.Length;
writer.WriteByte(parameterCount);
ulong dirtyBits = forceAll ? (~0ul) : NextDirtyBits();
writer.WriteULong(dirtyBits);
// iterate on byte count. if it's >256, it won't break
// serialization - just not serialize excess layers.
for (int i = 0; i < parameterCount; i++)
{
if ((dirtyBits & (1ul << i)) == 0)
continue;
AnimatorControllerParameter par = parameters[i];
if (par.type == AnimatorControllerParameterType.Int)
{
int newIntValue = animator.GetInteger(par.nameHash);
writer.WriteInt(newIntValue);
}
else if (par.type == AnimatorControllerParameterType.Float)
{
float newFloatValue = animator.GetFloat(par.nameHash);
writer.WriteFloat(newFloatValue);
}
else if (par.type == AnimatorControllerParameterType.Bool)
{
bool newBoolValue = animator.GetBool(par.nameHash);
writer.WriteBool(newBoolValue);
}
}
return dirtyBits != 0;
}
void ReadParameters(NetworkReader reader)
{
// fix: https://github.com/MirrorNetworking/Mirror/issues/2852
// serialize parameterCount to be 100% sure we deserialize correct amount of bytes.
// mismatch shows error to make this super easy to debug.
byte parameterCount = reader.ReadByte();
if (parameterCount != parameters.Length)
{
Debug.LogError($"NetworkAnimator: serialized parameter count={parameterCount} does not match expected parameter count={parameters.Length}. Are you changing animators at runtime?", gameObject);
return;
}
bool animatorEnabled = animator.enabled;
// need to read values from NetworkReader even if animator is disabled
ulong dirtyBits = reader.ReadULong();
for (int i = 0; i < parameterCount; i++)
{
if ((dirtyBits & (1ul << i)) == 0)
continue;
AnimatorControllerParameter par = parameters[i];
if (par.type == AnimatorControllerParameterType.Int)
{
int newIntValue = reader.ReadInt();
if (animatorEnabled)
animator.SetInteger(par.nameHash, newIntValue);
}
else if (par.type == AnimatorControllerParameterType.Float)
{
float newFloatValue = reader.ReadFloat();
if (animatorEnabled)
animator.SetFloat(par.nameHash, newFloatValue);
}
else if (par.type == AnimatorControllerParameterType.Bool)
{
bool newBoolValue = reader.ReadBool();
if (animatorEnabled)
animator.SetBool(par.nameHash, newBoolValue);
}
}
}
public override void OnSerialize(NetworkWriter writer, bool initialState)
{
base.OnSerialize(writer, initialState);
if (initialState)
{
// fix: https://github.com/MirrorNetworking/Mirror/issues/2852
// serialize layerCount to be 100% sure we deserialize correct amount of bytes.
// (255 layers should be enough for everyone, write it as byte)
byte layerCount = (byte)animator.layerCount;
writer.WriteByte(layerCount);
// iterate on byte count. if it's >256, it won't break
// serialization - just not serialize excess layers.
for (int i = 0; i < layerCount; i++)
{
AnimatorStateInfo st = animator.IsInTransition(i)
? animator.GetNextAnimatorStateInfo(i)
: animator.GetCurrentAnimatorStateInfo(i);
writer.WriteInt(st.fullPathHash);
writer.WriteFloat(st.normalizedTime);
writer.WriteFloat(animator.GetLayerWeight(i));
}
WriteParameters(writer, true);
}
}
public override void OnDeserialize(NetworkReader reader, bool initialState)
{
base.OnDeserialize(reader, initialState);
if (initialState)
{
// fix: https://github.com/MirrorNetworking/Mirror/issues/2852
// serialize layerCount to be 100% sure we deserialize correct amount of bytes.
// mismatch shows error to make this super easy to debug.
byte layerCount = reader.ReadByte();
if (layerCount != animator.layerCount)
{
Debug.LogError($"NetworkAnimator: serialized layer count={layerCount} does not match expected layer count={animator.layerCount}. Are you changing animators at runtime?", gameObject);
return;
}
for (int i = 0; i < layerCount; i++)
{
int stateHash = reader.ReadInt();
float normalizedTime = reader.ReadFloat();
float weight = reader.ReadFloat();
animator.SetLayerWeight(i, weight);
animator.Play(stateHash, i, normalizedTime);
}
ReadParameters(reader);
}
}
/// <summary>
/// Causes an animation trigger to be invoked for a networked object.
/// <para>If local authority is set, and this is called from the client, then the trigger will be invoked on the server and all clients. If not, then this is called on the server, and the trigger will be called on all clients.</para>
/// </summary>
/// <param name="triggerName">Name of trigger.</param>
public void SetTrigger(string triggerName)
{
SetTrigger(Animator.StringToHash(triggerName));
}
/// <summary>
/// Causes an animation trigger to be invoked for a networked object.
/// </summary>
/// <param name="hash">Hash id of trigger (from the Animator).</param>
public void SetTrigger(int hash)
{
if (clientAuthority)
{
if (!isClient)
{
Debug.LogWarning("Tried to set animation in the server for a client-controlled animator", gameObject);
return;
}
if (!isOwned)
{
Debug.LogWarning("Only the client with authority can set animations", gameObject);
return;
}
if (isClient)
CmdOnAnimationTriggerServerMessage(hash);
// call on client right away
HandleAnimTriggerMsg(hash);
}
else
{
if (!isServer)
{
Debug.LogWarning("Tried to set animation in the client for a server-controlled animator", gameObject);
return;
}
HandleAnimTriggerMsg(hash);
RpcOnAnimationTriggerClientMessage(hash);
}
}
/// <summary>
/// Causes an animation trigger to be reset for a networked object.
/// <para>If local authority is set, and this is called from the client, then the trigger will be reset on the server and all clients. If not, then this is called on the server, and the trigger will be reset on all clients.</para>
/// </summary>
/// <param name="triggerName">Name of trigger.</param>
public void ResetTrigger(string triggerName)
{
ResetTrigger(Animator.StringToHash(triggerName));
}
/// <summary>Causes an animation trigger to be reset for a networked object.</summary>
/// <param name="hash">Hash id of trigger (from the Animator).</param>
public void ResetTrigger(int hash)
{
if (clientAuthority)
{
if (!isClient)
{
Debug.LogWarning("Tried to reset animation in the server for a client-controlled animator", gameObject);
return;
}
if (!isOwned)
{
Debug.LogWarning("Only the client with authority can reset animations", gameObject);
return;
}
if (isClient)
CmdOnAnimationResetTriggerServerMessage(hash);
// call on client right away
HandleAnimResetTriggerMsg(hash);
}
else
{
if (!isServer)
{
Debug.LogWarning("Tried to reset animation in the client for a server-controlled animator", gameObject);
return;
}
HandleAnimResetTriggerMsg(hash);
RpcOnAnimationResetTriggerClientMessage(hash);
}
}
#region server message handlers
[Command]
void CmdOnAnimationServerMessage(int stateHash, float normalizedTime, int layerId, float weight, byte[] parameters)
{
// Ignore messages from client if not in client authority mode
if (!clientAuthority)
return;
//Debug.Log($"OnAnimationMessage for netId {netId}");
// handle and broadcast
using (NetworkReaderPooled networkReader = NetworkReaderPool.Get(parameters))
{
HandleAnimMsg(stateHash, normalizedTime, layerId, weight, networkReader);
RpcOnAnimationClientMessage(stateHash, normalizedTime, layerId, weight, parameters);
}
}
[Command]
void CmdOnAnimationParametersServerMessage(byte[] parameters)
{
// Ignore messages from client if not in client authority mode
if (!clientAuthority)
return;
// handle and broadcast
using (NetworkReaderPooled networkReader = NetworkReaderPool.Get(parameters))
{
HandleAnimParamsMsg(networkReader);
RpcOnAnimationParametersClientMessage(parameters);
}
}
[Command]
void CmdOnAnimationTriggerServerMessage(int hash)
{
// Ignore messages from client if not in client authority mode
if (!clientAuthority)
return;
// handle and broadcast
// host should have already the trigger
bool isHostOwner = isClient && isOwned;
if (!isHostOwner)
{
HandleAnimTriggerMsg(hash);
}
RpcOnAnimationTriggerClientMessage(hash);
}
[Command]
void CmdOnAnimationResetTriggerServerMessage(int hash)
{
// Ignore messages from client if not in client authority mode
if (!clientAuthority)
return;
// handle and broadcast
// host should have already the trigger
bool isHostOwner = isClient && isOwned;
if (!isHostOwner)
{
HandleAnimResetTriggerMsg(hash);
}
RpcOnAnimationResetTriggerClientMessage(hash);
}
[Command]
void CmdSetAnimatorSpeed(float newSpeed)
{
// set animator
animator.speed = newSpeed;
animatorSpeed = newSpeed;
}
#endregion
#region client message handlers
[ClientRpc]
void RpcOnAnimationClientMessage(int stateHash, float normalizedTime, int layerId, float weight, byte[] parameters)
{
using (NetworkReaderPooled networkReader = NetworkReaderPool.Get(parameters))
HandleAnimMsg(stateHash, normalizedTime, layerId, weight, networkReader);
}
[ClientRpc]
void RpcOnAnimationParametersClientMessage(byte[] parameters)
{
using (NetworkReaderPooled networkReader = NetworkReaderPool.Get(parameters))
HandleAnimParamsMsg(networkReader);
}
[ClientRpc(includeOwner = false)]
void RpcOnAnimationTriggerClientMessage(int hash)
{
HandleAnimTriggerMsg(hash);
}
[ClientRpc(includeOwner = false)]
void RpcOnAnimationResetTriggerClientMessage(int hash)
{
HandleAnimResetTriggerMsg(hash);
}
#endregion
}
}

View File

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

View File

@ -0,0 +1,30 @@
using UnityEngine;
namespace Mirror
{
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;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: bc9f0a0fe4124424b8f9d4927795ee01
timeCreated: 1700945893

View File

@ -0,0 +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 {}
}

View File

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

View File

@ -0,0 +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 {}
}

View File

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

View File

@ -0,0 +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;
}
}
}

View File

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

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 80106690aef541a5b8e2f8fb3d5949ad
timeCreated: 1686733778

View File

@ -0,0 +1,96 @@
using UnityEngine;
namespace Mirror
{
// [RequireComponent(typeof(Rigidbody))] <- OnValidate ensures this is on .target
public class NetworkRigidbodyReliable : NetworkTransformReliable
{
bool clientAuthority => syncDirection == SyncDirection.ClientToServer;
Rigidbody rb;
bool wasKinematic;
// 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 OnValidate()
{
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);
}
}
}
}

View File

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

View File

@ -0,0 +1,96 @@
using UnityEngine;
namespace Mirror
{
// [RequireComponent(typeof(Rigidbody))] <- OnValidate ensures this is on .target
public class NetworkRigidbodyReliable2D : NetworkTransformReliable
{
bool clientAuthority => syncDirection == SyncDirection.ClientToServer;
Rigidbody2D rb;
bool wasKinematic;
// 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;
}
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 OnValidate()
{
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);
}
}
}
}

View File

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

View File

@ -0,0 +1,96 @@
using UnityEngine;
namespace Mirror
{
// [RequireComponent(typeof(Rigidbody))] <- OnValidate ensures this is on .target
public class NetworkRigidbodyUnreliable : NetworkTransformUnreliable
{
bool clientAuthority => syncDirection == SyncDirection.ClientToServer;
Rigidbody rb;
bool wasKinematic;
// 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 OnValidate()
{
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);
}
}
}
}

View File

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

View File

@ -0,0 +1,96 @@
using UnityEngine;
namespace Mirror
{
// [RequireComponent(typeof(Rigidbody))] <- OnValidate ensures this is on .target
public class NetworkRigidbodyUnreliable2D : NetworkTransformUnreliable
{
bool clientAuthority => syncDirection == SyncDirection.ClientToServer;
Rigidbody2D rb;
bool wasKinematic;
// 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;
}
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 OnValidate()
{
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);
}
}
}
}

View File

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

View File

@ -0,0 +1,685 @@
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.Serialization;
namespace Mirror
{
/// <summary>
/// This is a specialized NetworkManager that includes a networked room.
/// </summary>
/// <remarks>
/// <para>The room has slots that track the joined players, and a maximum player count that is enforced. It requires that the NetworkRoomPlayer component be on the room player objects.</para>
/// <para>NetworkRoomManager 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 NetworkRoomManager, there are new virtual functions on the NetworkRoomManager that begin with "OnRoom". These should be used on classes derived from NetworkRoomManager instead of the virtual functions on NetworkManager.</para>
/// <para>The OnRoom*() functions have empty implementations on the NetworkRoomManager base class, so the base class functions do not have to be called.</para>
/// </remarks>
[AddComponentMenu("Network/Network Room Manager")]
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-room-manager")]
public class NetworkRoomManager : NetworkManager
{
public struct PendingPlayer
{
public NetworkConnectionToClient conn;
public GameObject roomPlayer;
}
[Header("Room Settings")]
[FormerlySerializedAs("m_ShowRoomGUI")]
[SerializeField]
[Tooltip("This flag controls whether the default UI is shown for the room")]
public bool showRoomGUI = true;
[FormerlySerializedAs("m_MinPlayers")]
[SerializeField]
[Tooltip("Minimum number of players to auto-start the game")]
public int minPlayers = 1;
[FormerlySerializedAs("m_RoomPlayerPrefab")]
[SerializeField]
[Tooltip("Prefab to use for the Room Player")]
public NetworkRoomPlayer roomPlayerPrefab;
/// <summary>
/// The scene to use for the room. This is similar to the offlineScene of the NetworkManager.
/// </summary>
[Scene]
public string RoomScene;
/// <summary>
/// The scene to use for the playing the game from the room. This is similar to the onlineScene of the NetworkManager.
/// </summary>
[Scene]
public string GameplayScene;
/// <summary>
/// List of players that are in the Room
/// </summary>
[FormerlySerializedAs("m_PendingPlayers")]
public List<PendingPlayer> pendingPlayers = new List<PendingPlayer>();
[Header("Diagnostics")]
/// <summary>
/// True when all players have submitted a Ready message
/// </summary>
[Tooltip("Diagnostic flag indicating all players are ready to play")]
[FormerlySerializedAs("allPlayersReady")]
[ReadOnly, SerializeField] bool _allPlayersReady;
/// <summary>
/// These slots track players that enter the room.
/// <para>The slotId on players is global to the game - across all players.</para>
/// </summary>
[ReadOnly, Tooltip("List of Room Player objects")]
public List<NetworkRoomPlayer> roomSlots = new List<NetworkRoomPlayer>();
public bool allPlayersReady
{
get => _allPlayersReady;
set
{
bool wasReady = _allPlayersReady;
bool nowReady = value;
if (wasReady != nowReady)
{
_allPlayersReady = value;
if (nowReady)
{
OnRoomServerPlayersReady();
}
else
{
OnRoomServerPlayersNotReady();
}
}
}
}
public override void OnValidate()
{
base.OnValidate();
// always <= maxConnections
minPlayers = Mathf.Min(minPlayers, maxConnections);
// always >= 0
minPlayers = Mathf.Max(minPlayers, 0);
if (roomPlayerPrefab != null)
{
NetworkIdentity identity = roomPlayerPrefab.GetComponent<NetworkIdentity>();
if (identity == null)
{
roomPlayerPrefab = null;
Debug.LogError("RoomPlayer prefab must have a NetworkIdentity component.");
}
}
}
void SceneLoadedForPlayer(NetworkConnectionToClient conn, GameObject roomPlayer)
{
//Debug.Log($"NetworkRoom SceneLoadedForPlayer scene: {SceneManager.GetActiveScene().path} {conn}");
if (Utils.IsSceneActive(RoomScene))
{
// cant be ready in room, add to ready list
PendingPlayer pending;
pending.conn = conn;
pending.roomPlayer = roomPlayer;
pendingPlayers.Add(pending);
return;
}
GameObject gamePlayer = OnRoomServerCreateGamePlayer(conn, roomPlayer);
if (gamePlayer == null)
{
// get start position from base class
Transform startPos = GetStartPosition();
gamePlayer = startPos != null
? Instantiate(playerPrefab, startPos.position, startPos.rotation)
: Instantiate(playerPrefab, Vector3.zero, Quaternion.identity);
}
if (!OnRoomServerSceneLoadedForPlayer(conn, roomPlayer, gamePlayer))
return;
// replace room player with game player
NetworkServer.ReplacePlayerForConnection(conn, gamePlayer, true);
}
internal void CallOnClientEnterRoom()
{
OnRoomClientEnter();
foreach (NetworkRoomPlayer player in roomSlots)
if (player != null)
{
player.OnClientEnterRoom();
}
}
internal void CallOnClientExitRoom()
{
OnRoomClientExit();
foreach (NetworkRoomPlayer player in roomSlots)
if (player != null)
{
player.OnClientExitRoom();
}
}
/// <summary>
/// CheckReadyToBegin checks all of the players in the room to see if their readyToBegin flag is set.
/// <para>If all of the players are ready, then the server switches from the RoomScene to the PlayScene, essentially starting the game. This is called automatically in response to NetworkRoomPlayer.CmdChangeReadyState.</para>
/// </summary>
public void CheckReadyToBegin()
{
if (!Utils.IsSceneActive(RoomScene))
return;
int numberOfReadyPlayers = NetworkServer.connections.Count(conn =>
conn.Value != null &&
conn.Value.identity != null &&
conn.Value.identity.TryGetComponent(out NetworkRoomPlayer nrp) &&
nrp.readyToBegin);
bool enoughReadyPlayers = minPlayers <= 0 || numberOfReadyPlayers >= minPlayers;
if (enoughReadyPlayers)
{
pendingPlayers.Clear();
allPlayersReady = true;
}
else
allPlayersReady = false;
}
#region server handlers
/// <summary>
/// Called on the server when a new client connects.
/// <para>Unity calls this on the Server when a Client connects to the Server. Use an override to tell the NetworkManager what to do when a client connects to the server.</para>
/// </summary>
/// <param name="conn">Connection from client.</param>
public override void OnServerConnect(NetworkConnectionToClient conn)
{
// cannot join game in progress
if (!Utils.IsSceneActive(RoomScene))
{
Debug.Log($"Not in Room scene...disconnecting {conn}");
conn.Disconnect();
return;
}
base.OnServerConnect(conn);
OnRoomServerConnect(conn);
}
/// <summary>
/// Called on the server when a client disconnects.
/// <para>This is called on the Server when a Client disconnects from the Server. Use an override to decide what should happen when a disconnection is detected.</para>
/// </summary>
/// <param name="conn">Connection from client.</param>
public override void OnServerDisconnect(NetworkConnectionToClient conn)
{
if (conn.identity != null)
{
NetworkRoomPlayer roomPlayer = conn.identity.GetComponent<NetworkRoomPlayer>();
if (roomPlayer != null)
roomSlots.Remove(roomPlayer);
foreach (NetworkIdentity clientOwnedObject in conn.owned)
{
roomPlayer = clientOwnedObject.GetComponent<NetworkRoomPlayer>();
if (roomPlayer != null)
roomSlots.Remove(roomPlayer);
}
}
allPlayersReady = false;
foreach (NetworkRoomPlayer player in roomSlots)
{
if (player != null)
player.GetComponent<NetworkRoomPlayer>().readyToBegin = false;
}
if (Utils.IsSceneActive(RoomScene))
RecalculateRoomPlayerIndices();
OnRoomServerDisconnect(conn);
base.OnServerDisconnect(conn);
if (Utils.IsHeadless())
{
if (numPlayers < 1)
StopServer();
}
}
// Sequential index used in round-robin deployment of players into instances and score positioning
public int clientIndex;
/// <summary>
/// Called on the server when a client is ready.
/// <para>The default implementation of this function calls NetworkServer.SetClientReady() to continue the network setup process.</para>
/// </summary>
/// <param name="conn">Connection from client.</param>
public override void OnServerReady(NetworkConnectionToClient conn)
{
//Debug.Log($"NetworkRoomManager OnServerReady {conn}");
base.OnServerReady(conn);
if (conn != null && conn.identity != null)
{
GameObject roomPlayer = conn.identity.gameObject;
// if null or not a room player, don't replace it
if (roomPlayer != null && roomPlayer.GetComponent<NetworkRoomPlayer>() != null)
SceneLoadedForPlayer(conn, roomPlayer);
}
}
/// <summary>
/// Called on the server when a client adds a new player with NetworkClient.AddPlayer.
/// <para>The default implementation for this function creates a new player object from the playerPrefab.</para>
/// </summary>
/// <param name="conn">Connection from client.</param>
public override void OnServerAddPlayer(NetworkConnectionToClient conn)
{
// increment the index before adding the player, so first player starts at 1
clientIndex++;
if (Utils.IsSceneActive(RoomScene))
{
allPlayersReady = false;
//Debug.Log("NetworkRoomManager.OnServerAddPlayer playerPrefab: {roomPlayerPrefab.name}");
GameObject newRoomGameObject = OnRoomServerCreateRoomPlayer(conn);
if (newRoomGameObject == null)
newRoomGameObject = Instantiate(roomPlayerPrefab.gameObject, Vector3.zero, Quaternion.identity);
NetworkServer.AddPlayerForConnection(conn, newRoomGameObject);
}
else
{
// Late joiners not supported...should've been kicked by OnServerDisconnect
Debug.Log($"Not in Room scene...disconnecting {conn}");
conn.Disconnect();
}
}
[Server]
public void RecalculateRoomPlayerIndices()
{
if (roomSlots.Count > 0)
{
for (int i = 0; i < roomSlots.Count; i++)
{
roomSlots[i].index = i;
}
}
}
/// <summary>
/// This causes the server to switch scenes and sets the networkSceneName.
/// <para>Clients that connect to this server will automatically switch to this scene. This is called automatically if onlineScene or offlineScene are set, but it can be called from user code to switch scenes again while the game is in progress. This automatically sets clients to be not-ready. The clients must call NetworkClient.Ready() again to participate in the new scene.</para>
/// </summary>
/// <param name="newSceneName"></param>
public override void ServerChangeScene(string newSceneName)
{
if (newSceneName == RoomScene)
{
foreach (NetworkRoomPlayer roomPlayer in roomSlots)
{
if (roomPlayer == null)
continue;
// find the game-player object for this connection, and destroy it
NetworkIdentity identity = roomPlayer.GetComponent<NetworkIdentity>();
if (NetworkServer.active)
{
// re-add the room object
roomPlayer.GetComponent<NetworkRoomPlayer>().readyToBegin = false;
NetworkServer.ReplacePlayerForConnection(identity.connectionToClient, roomPlayer.gameObject);
}
}
allPlayersReady = false;
}
base.ServerChangeScene(newSceneName);
}
/// <summary>
/// Called on the server when a scene is completed loaded, when the scene load was initiated by the server with ServerChangeScene().
/// </summary>
/// <param name="sceneName">The name of the new scene.</param>
public override void OnServerSceneChanged(string sceneName)
{
if (sceneName != RoomScene)
{
// call SceneLoadedForPlayer on any players that become ready while we were loading the scene.
foreach (PendingPlayer pending in pendingPlayers)
SceneLoadedForPlayer(pending.conn, pending.roomPlayer);
pendingPlayers.Clear();
}
OnRoomServerSceneChanged(sceneName);
}
/// <summary>
/// This is invoked when a server is started - including when a host is started.
/// <para>StartServer has multiple signatures, but they all cause this hook to be called.</para>
/// </summary>
public override void OnStartServer()
{
if (string.IsNullOrWhiteSpace(RoomScene))
{
Debug.LogError("NetworkRoomManager RoomScene is empty. Set the RoomScene in the inspector for the NetworkRoomManager");
return;
}
if (string.IsNullOrWhiteSpace(GameplayScene))
{
Debug.LogError("NetworkRoomManager PlayScene is empty. Set the PlayScene in the inspector for the NetworkRoomManager");
return;
}
OnRoomStartServer();
}
/// <summary>
/// This is invoked when a host is started.
/// <para>StartHost has multiple signatures, but they all cause this hook to be called.</para>
/// </summary>
public override void OnStartHost()
{
OnRoomStartHost();
}
/// <summary>
/// This is called when a server is stopped - including when a host is stopped.
/// </summary>
public override void OnStopServer()
{
roomSlots.Clear();
OnRoomStopServer();
}
/// <summary>
/// This is called when a host is stopped.
/// </summary>
public override void OnStopHost()
{
OnRoomStopHost();
}
#endregion
#region client handlers
/// <summary>
/// This is invoked when the client is started.
/// </summary>
public override void OnStartClient()
{
if (roomPlayerPrefab == null || roomPlayerPrefab.gameObject == null)
Debug.LogError("NetworkRoomManager no RoomPlayer prefab is registered. Please add a RoomPlayer prefab.");
else
NetworkClient.RegisterPrefab(roomPlayerPrefab.gameObject);
if (playerPrefab == null)
Debug.LogError("NetworkRoomManager no GamePlayer prefab is registered. Please add a GamePlayer prefab.");
OnRoomStartClient();
}
/// <summary>
/// Called on the client when connected to a server.
/// <para>The default implementation of this function sets the client as ready and adds a player. Override the function to dictate what happens when the client connects.</para>
/// </summary>
public override void OnClientConnect()
{
OnRoomClientConnect();
base.OnClientConnect();
}
/// <summary>
/// Called on clients when disconnected from a server.
/// <para>This is called on the client when it disconnects from the server. Override this function to decide what happens when the client disconnects.</para>
/// </summary>
public override void OnClientDisconnect()
{
OnRoomClientDisconnect();
base.OnClientDisconnect();
}
/// <summary>
/// This is called when a client is stopped.
/// </summary>
public override void OnStopClient()
{
OnRoomStopClient();
CallOnClientExitRoom();
roomSlots.Clear();
}
/// <summary>
/// Called on clients when a scene has completed loaded, when the scene load was initiated by the server.
/// <para>Scene changes can cause player objects to be destroyed. The default implementation of OnClientSceneChanged in the NetworkManager is to add a player object for the connection if no player object exists.</para>
/// </summary>
public override void OnClientSceneChanged()
{
if (Utils.IsSceneActive(RoomScene))
{
if (NetworkClient.isConnected)
CallOnClientEnterRoom();
}
else
CallOnClientExitRoom();
base.OnClientSceneChanged();
OnRoomClientSceneChanged();
}
#endregion
#region room server virtuals
/// <summary>
/// This is called on the host when a host is started.
/// </summary>
public virtual void OnRoomStartHost() {}
/// <summary>
/// This is called on the host when the host is stopped.
/// </summary>
public virtual void OnRoomStopHost() {}
/// <summary>
/// This is called on the server when the server is started - including when a host is started.
/// </summary>
public virtual void OnRoomStartServer() {}
/// <summary>
/// This is called on the server when the server is started - including when a host is stopped.
/// </summary>
public virtual void OnRoomStopServer() {}
/// <summary>
/// This is called on the server when a new client connects to the server.
/// </summary>
/// <param name="conn">The new connection.</param>
public virtual void OnRoomServerConnect(NetworkConnectionToClient conn) {}
/// <summary>
/// This is called on the server when a client disconnects.
/// </summary>
/// <param name="conn">The connection that disconnected.</param>
public virtual void OnRoomServerDisconnect(NetworkConnectionToClient conn) {}
/// <summary>
/// This is called on the server when a networked scene finishes loading.
/// </summary>
/// <param name="sceneName">Name of the new scene.</param>
public virtual void OnRoomServerSceneChanged(string sceneName) {}
/// <summary>
/// This allows customization of the creation of the room-player object on the server.
/// <para>By default the roomPlayerPrefab is used to create the room-player, but this function allows that behaviour to be customized.</para>
/// </summary>
/// <param name="conn">The connection the player object is for.</param>
/// <returns>The new room-player object.</returns>
public virtual GameObject OnRoomServerCreateRoomPlayer(NetworkConnectionToClient conn)
{
return null;
}
/// <summary>
/// This allows customization of the creation of the GamePlayer object on the server.
/// <para>By default the gamePlayerPrefab is used to create the game-player, but this function allows that behaviour to be customized. The object returned from the function will be used to replace the room-player on the connection.</para>
/// </summary>
/// <param name="conn">The connection the player object is for.</param>
/// <param name="roomPlayer">The room player object for this connection.</param>
/// <returns>A new GamePlayer object.</returns>
public virtual GameObject OnRoomServerCreateGamePlayer(NetworkConnectionToClient conn, GameObject roomPlayer)
{
return null;
}
/// <summary>
/// This allows customization of the creation of the GamePlayer object on the server.
/// <para>This is only called for subsequent GamePlay scenes after the first one.</para>
/// <para>See <see cref="OnRoomServerCreateGamePlayer(NetworkConnectionToClient, GameObject)">OnRoomServerCreateGamePlayer(NetworkConnection, GameObject)</see> to customize the player object for the initial GamePlay scene.</para>
/// </summary>
/// <param name="conn">The connection the player object is for.</param>
public virtual void OnRoomServerAddPlayer(NetworkConnectionToClient conn)
{
base.OnServerAddPlayer(conn);
}
// for users to apply settings from their room player object to their in-game player object
/// <summary>
/// This is called on the server when it is told that a client has finished switching from the room scene to a game player scene.
/// <para>When switching from the room, the room-player is replaced with a game-player object. This callback function gives an opportunity to apply state from the room-player to the game-player object.</para>
/// </summary>
/// <param name="conn">The connection of the player</param>
/// <param name="roomPlayer">The room player object.</param>
/// <param name="gamePlayer">The game player object.</param>
/// <returns>False to not allow this player to replace the room player.</returns>
public virtual bool OnRoomServerSceneLoadedForPlayer(NetworkConnectionToClient conn, GameObject roomPlayer, GameObject gamePlayer)
{
return true;
}
/// <summary>
/// This is called on server from NetworkRoomPlayer.CmdChangeReadyState when client indicates change in Ready status.
/// </summary>
public virtual void ReadyStatusChanged()
{
int CurrentPlayers = 0;
int ReadyPlayers = 0;
foreach (NetworkRoomPlayer item in roomSlots)
{
if (item != null)
{
CurrentPlayers++;
if (item.readyToBegin)
ReadyPlayers++;
}
}
if (CurrentPlayers == ReadyPlayers)
CheckReadyToBegin();
else
allPlayersReady = false;
}
/// <summary>
/// This is called on the server when all the players in the room are ready.
/// <para>The default implementation of this function uses ServerChangeScene() to switch to the game player scene. By implementing this callback you can customize what happens when all the players in the room are ready, such as adding a countdown or a confirmation for a group leader.</para>
/// </summary>
public virtual void OnRoomServerPlayersReady()
{
// all players are readyToBegin, start the game
ServerChangeScene(GameplayScene);
}
/// <summary>
/// This is called on the server when CheckReadyToBegin finds that players are not ready
/// <para>May be called multiple times while not ready players are joining</para>
/// </summary>
public virtual void OnRoomServerPlayersNotReady() {}
#endregion
#region room client virtuals
/// <summary>
/// This is a hook to allow custom behaviour when the game client enters the room.
/// </summary>
public virtual void OnRoomClientEnter() {}
/// <summary>
/// This is a hook to allow custom behaviour when the game client exits the room.
/// </summary>
public virtual void OnRoomClientExit() {}
/// <summary>
/// This is called on the client when it connects to server.
/// </summary>
public virtual void OnRoomClientConnect() {}
/// <summary>
/// This is called on the client when disconnected from a server.
/// </summary>
public virtual void OnRoomClientDisconnect() {}
/// <summary>
/// This is called on the client when a client is started.
/// </summary>
public virtual void OnRoomStartClient() {}
/// <summary>
/// This is called on the client when the client stops.
/// </summary>
public virtual void OnRoomStopClient() {}
/// <summary>
/// This is called on the client when the client is finished loading a new networked scene.
/// </summary>
public virtual void OnRoomClientSceneChanged() {}
#endregion
#region optional UI
/// <summary>
/// virtual so inheriting classes can roll their own
/// </summary>
public virtual void OnGUI()
{
if (!showRoomGUI)
return;
if (NetworkServer.active && Utils.IsSceneActive(GameplayScene))
{
GUILayout.BeginArea(new Rect(Screen.width - 150f, 10f, 140f, 30f));
if (GUILayout.Button("Return to Room"))
ServerChangeScene(RoomScene);
GUILayout.EndArea();
}
if (Utils.IsSceneActive(RoomScene))
GUI.Box(new Rect(10f, 180f, 520f, 150f), "PLAYERS");
}
#endregion
}
}

View File

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

View File

@ -0,0 +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
}
}

View File

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

View File

@ -0,0 +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();
}
}
}

View File

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

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 36de72d9255741659bcbd1971ed29822
timeCreated: 1668358590

View File

@ -0,0 +1,8 @@
using System;
namespace Mirror
{
// DEPRECATED 2023-06-15
[Obsolete("NetworkTransform was renamed to NetworkTransformUnreliable.\nYou can easily swap the component's script by going into the Unity Inspector debug mode:\n1. Click the vertical dots on the top right in the Inspector tab.\n2. Find your NetworkTransform component\n3. Drag NetworkTransformUnreliable into the 'Script' field in the Inspector.\n4. Find the three dots and return to Normal mode.")]
public class NetworkTransform : NetworkTransformUnreliable {}
}

View File

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

View File

@ -0,0 +1,489 @@
// Snapshot Interpolation: https://gafferongames.com/post/snapshot_interpolation/
//
// Base class for NetworkTransform and NetworkTransformChild.
// => simple unreliable sync without any interpolation for now.
// => which means we don't need teleport detection either
//
// NOTE: several functions are virtual in case someone needs to modify a part.
//
// Channel: uses UNRELIABLE at all times.
// -> out of order packets are dropped automatically
// -> it's better than RELIABLE for several reasons:
// * head of line blocking would add delay
// * resending is mostly pointless
// * bigger data race:
// -> if we use a Cmd() at position X over reliable
// -> client gets Cmd() and X at the same time, but buffers X for bufferTime
// -> for unreliable, it would get X before the reliable Cmd(), still
// buffer for bufferTime but end up closer to the original time
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Mirror
{
public enum CoordinateSpace { Local, World }
public abstract class NetworkTransformBase : NetworkBehaviour
{
// target transform to sync. can be on a child.
// TODO this field is kind of unnecessary since we now support child NetworkBehaviours
[Header("Target")]
[Tooltip("The Transform component to sync. May be on on this GameObject, or on a child.")]
public Transform target;
// Is this a client with authority over this transform?
// This component could be on the player object or any object that has been assigned authority to this client.
protected bool IsClientWithAuthority => isClient && authority;
// snapshots with initial capacity to avoid early resizing & allocations: see NetworkRigidbodyBenchmark example.
public readonly SortedList<double, TransformSnapshot> clientSnapshots = new SortedList<double, TransformSnapshot>(16);
public readonly SortedList<double, TransformSnapshot> serverSnapshots = new SortedList<double, TransformSnapshot>(16);
// selective sync //////////////////////////////////////////////////////
[Header("Selective Sync\nDon't change these at Runtime")]
public bool syncPosition = true; // do not change at runtime!
public bool syncRotation = true; // do not change at runtime!
public bool syncScale = false; // do not change at runtime! rare. off by default.
[Header("Bandwidth Savings")]
[Tooltip("When true, changes are not sent unless greater than sensitivity values below.")]
public bool onlySyncOnChange = true;
[Tooltip("Apply smallest-three quaternion compression. This is lossy, you can disable it if the small rotation inaccuracies are noticeable in your project.")]
public bool compressRotation = true;
// interpolation is on by default, but can be disabled to jump to
// the destination immediately. some projects need this.
[Header("Interpolation")]
[Tooltip("Set to false to have a snap-like effect on position movement.")]
public bool interpolatePosition = true;
[Tooltip("Set to false to have a snap-like effect on rotations.")]
public bool interpolateRotation = true;
[Tooltip("Set to false to remove scale smoothing. Example use-case: Instant flipping of sprites that use -X and +X for direction.")]
public bool interpolateScale = true;
// CoordinateSpace ///////////////////////////////////////////////////////////
[Header("Coordinate Space")]
[Tooltip("Local by default. World may be better when changing hierarchy, or non-NetworkTransforms root position/rotation/scale values.")]
public CoordinateSpace coordinateSpace = CoordinateSpace.Local;
[Header("Send Interval Multiplier")]
[Tooltip("Check/Sync every multiple of Network Manager send interval (= 1 / NM Send Rate), instead of every send interval.\n(30 NM send rate, and 3 interval, is a send every 0.1 seconds)\nA larger interval means less network sends, which has a variety of upsides. The drawbacks are delays and lower accuracy, you should find a nice balance between not sending too much, but the results looking good for your particular scenario.")]
[Range(1, 120)]
public uint sendIntervalMultiplier = 1;
[Header("Timeline Offset")]
[Tooltip("Add a small timeline offset to account for decoupled arrival of NetworkTime and NetworkTransform snapshots.\nfixes: https://github.com/MirrorNetworking/Mirror/issues/3427")]
public bool timelineOffset = false;
// Ninja's Notes on offset & mulitplier:
//
// In a no multiplier scenario:
// 1. Snapshots are sent every frame (frame being 1 NM send interval).
// 2. Time Interpolation is set to be 'behind' by 2 frames times.
// In theory where everything works, we probably have around 2 snapshots before we need to interpolate snapshots. From NT perspective, we should always have around 2 snapshots ready, so no stutter.
//
// In a multiplier scenario:
// 1. Snapshots are sent every 10 frames.
// 2. Time Interpolation remains 'behind by 2 frames'.
// When everything works, we are receiving NT snapshots every 10 frames, but start interpolating after 2.
// Even if I assume we had 2 snapshots to begin with to start interpolating (which we don't), by the time we reach 13th frame, we are out of snapshots, and have to wait 7 frames for next snapshot to come. This is the reason why we absolutely need the timestamp adjustment. We are starting way too early to interpolate.
//
protected double timeStampAdjustment => NetworkServer.sendInterval * (sendIntervalMultiplier - 1);
protected double offset => timelineOffset ? NetworkServer.sendInterval * sendIntervalMultiplier : 0;
// debugging ///////////////////////////////////////////////////////////
[Header("Debug")]
public bool showGizmos;
public bool showOverlay;
public Color overlayColor = new Color(0, 0, 0, 0.5f);
// initialization //////////////////////////////////////////////////////
// make sure to call this when inheriting too!
protected virtual void Awake() { }
protected override void OnValidate()
{
base.OnValidate();
// set target to self if none yet
if (target == null) target = transform;
// time snapshot interpolation happens globally.
// value (transform) happens in here.
// both always need to be on the same send interval.
// force the setting to '0' in OnValidate to make it obvious that we
// actually use NetworkServer.sendInterval.
syncInterval = 0;
// Unity doesn't support setting world scale.
// OnValidate force disables syncScale in world mode.
if (coordinateSpace == CoordinateSpace.World) syncScale = false;
}
// snapshot functions //////////////////////////////////////////////////
// get local/world position
protected virtual Vector3 GetPosition() =>
coordinateSpace == CoordinateSpace.Local ? target.localPosition : target.position;
// get local/world rotation
protected virtual Quaternion GetRotation() =>
coordinateSpace == CoordinateSpace.Local ? target.localRotation : target.rotation;
// get local/world scale
protected virtual Vector3 GetScale() =>
coordinateSpace == CoordinateSpace.Local ? target.localScale : target.lossyScale;
// set local/world position
protected virtual void SetPosition(Vector3 position)
{
if (coordinateSpace == CoordinateSpace.Local)
target.localPosition = position;
else
target.position = position;
}
// set local/world rotation
protected virtual void SetRotation(Quaternion rotation)
{
if (coordinateSpace == CoordinateSpace.Local)
target.localRotation = rotation;
else
target.rotation = rotation;
}
// set local/world position
protected virtual void SetScale(Vector3 scale)
{
if (coordinateSpace == CoordinateSpace.Local)
target.localScale = scale;
// Unity doesn't support setting world scale.
// OnValidate disables syncScale in world mode.
// else
// target.lossyScale = scale; // TODO
}
// construct a snapshot of the current state
// => internal for testing
protected virtual TransformSnapshot Construct()
{
// NetworkTime.localTime for double precision until Unity has it too
return new TransformSnapshot(
// our local time is what the other end uses as remote time
NetworkTime.localTime, // Unity 2019 doesn't have timeAsDouble yet
0, // the other end fills out local time itself
GetPosition(),
GetRotation(),
GetScale()
);
}
protected void AddSnapshot(SortedList<double, TransformSnapshot> snapshots, double timeStamp, Vector3? position, Quaternion? rotation, Vector3? scale)
{
// position, rotation, scale can have no value if same as last time.
// saves bandwidth.
// but we still need to feed it to snapshot interpolation. we can't
// just have gaps in there if nothing has changed. for example, if
// client sends snapshot at t=0
// client sends nothing for 10s because not moved
// client sends snapshot at t=10
// then the server would assume that it's one super slow move and
// replay it for 10 seconds.
if (!position.HasValue) position = snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].position : GetPosition();
if (!rotation.HasValue) rotation = snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].rotation : GetRotation();
if (!scale.HasValue) scale = snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].scale : GetScale();
// insert transform snapshot
SnapshotInterpolation.InsertIfNotExists(
snapshots,
NetworkClient.snapshotSettings.bufferLimit,
new TransformSnapshot(
timeStamp, // arrival remote timestamp. NOT remote time.
NetworkTime.localTime, // Unity 2019 doesn't have timeAsDouble yet
position.Value,
rotation.Value,
scale.Value
)
);
}
// apply a snapshot to the Transform.
// -> start, end, interpolated are all passed in caes they are needed
// -> a regular game would apply the 'interpolated' snapshot
// -> a board game might want to jump to 'goal' directly
// (it's easier to always interpolate and then apply selectively,
// instead of manually interpolating x, y, z, ... depending on flags)
// => internal for testing
//
// NOTE: stuck detection is unnecessary here.
// we always set transform.position anyway, we can't get stuck.
protected virtual void Apply(TransformSnapshot interpolated, TransformSnapshot endGoal)
{
// local position/rotation for VR support
//
// if syncPosition/Rotation/Scale is disabled then we received nulls
// -> current position/rotation/scale would've been added as snapshot
// -> we still interpolated
// -> but simply don't apply it. if the user doesn't want to sync
// scale, then we should not touch scale etc.
// interpolate parts
if (syncPosition) SetPosition(interpolatePosition ? interpolated.position : endGoal.position);
if (syncRotation) SetRotation(interpolateRotation ? interpolated.rotation : endGoal.rotation);
if (syncScale) SetScale(interpolateScale ? interpolated.scale : endGoal.scale);
}
// client->server teleport to force position without interpolation.
// otherwise it would interpolate to a (far away) new position.
// => manually calling Teleport is the only 100% reliable solution.
[Command]
public void CmdTeleport(Vector3 destination)
{
// client can only teleport objects that it has authority over.
if (syncDirection != SyncDirection.ClientToServer) return;
// TODO what about host mode?
OnTeleport(destination);
// if a client teleports, we need to broadcast to everyone else too
// TODO the teleported client should ignore the rpc though.
// otherwise if it already moved again after teleporting,
// the rpc would come a little bit later and reset it once.
// TODO or not? if client ONLY calls Teleport(pos), the position
// would only be set after the rpc. unless the client calls
// BOTH Teleport(pos) and target.position=pos
RpcTeleport(destination);
}
// client->server teleport to force position and rotation without interpolation.
// otherwise it would interpolate to a (far away) new position.
// => manually calling Teleport is the only 100% reliable solution.
[Command]
public void CmdTeleport(Vector3 destination, Quaternion rotation)
{
// client can only teleport objects that it has authority over.
if (syncDirection != SyncDirection.ClientToServer) return;
// TODO what about host mode?
OnTeleport(destination, rotation);
// if a client teleports, we need to broadcast to everyone else too
// TODO the teleported client should ignore the rpc though.
// otherwise if it already moved again after teleporting,
// the rpc would come a little bit later and reset it once.
// TODO or not? if client ONLY calls Teleport(pos), the position
// would only be set after the rpc. unless the client calls
// BOTH Teleport(pos) and target.position=pos
RpcTeleport(destination, rotation);
}
// server->client teleport to force position without interpolation.
// otherwise it would interpolate to a (far away) new position.
// => manually calling Teleport is the only 100% reliable solution.
[ClientRpc]
public void RpcTeleport(Vector3 destination)
{
// NOTE: even in client authority mode, the server is always allowed
// to teleport the player. for example:
// * CmdEnterPortal() might teleport the player
// * Some people use client authority with server sided checks
// so the server should be able to reset position if needed.
// TODO what about host mode?
OnTeleport(destination);
}
// server->client teleport to force position and rotation without interpolation.
// otherwise it would interpolate to a (far away) new position.
// => manually calling Teleport is the only 100% reliable solution.
[ClientRpc]
public void RpcTeleport(Vector3 destination, Quaternion rotation)
{
// NOTE: even in client authority mode, the server is always allowed
// to teleport the player. for example:
// * CmdEnterPortal() might teleport the player
// * Some people use client authority with server sided checks
// so the server should be able to reset position if needed.
// TODO what about host mode?
OnTeleport(destination, rotation);
}
[ClientRpc]
void RpcResetState()
{
ResetState();
}
// common Teleport code for client->server and server->client
protected virtual void OnTeleport(Vector3 destination)
{
// set the new position.
// interpolation will automatically continue.
target.position = destination;
// reset interpolation to immediately jump to the new position.
// do not call Reset() here, this would cause delta compression to
// get out of sync for NetworkTransformReliable because NTReliable's
// 'override Reset()' resets lastDe/SerializedPosition:
// https://github.com/MirrorNetworking/Mirror/issues/3588
// because client's next OnSerialize() will delta compress,
// but server's last delta will have been reset, causing offsets.
//
// instead, simply clear snapshots.
serverSnapshots.Clear();
clientSnapshots.Clear();
// TODO
// what if we still receive a snapshot from before the interpolation?
// it could easily happen over unreliable.
// -> maybe add destination as first entry?
}
// common Teleport code for client->server and server->client
protected virtual void OnTeleport(Vector3 destination, Quaternion rotation)
{
// set the new position.
// interpolation will automatically continue.
target.position = destination;
target.rotation = rotation;
// reset interpolation to immediately jump to the new position.
// do not call Reset() here, this would cause delta compression to
// get out of sync for NetworkTransformReliable because NTReliable's
// 'override Reset()' resets lastDe/SerializedPosition:
// https://github.com/MirrorNetworking/Mirror/issues/3588
// because client's next OnSerialize() will delta compress,
// but server's last delta will have been reset, causing offsets.
//
// instead, simply clear snapshots.
serverSnapshots.Clear();
clientSnapshots.Clear();
// TODO
// what if we still receive a snapshot from before the interpolation?
// it could easily happen over unreliable.
// -> maybe add destination as first entry?
}
public virtual void ResetState()
{
// disabled objects aren't updated anymore.
// so let's clear the buffers.
serverSnapshots.Clear();
clientSnapshots.Clear();
}
public virtual void Reset()
{
ResetState();
// default to ClientToServer so this works immediately for users
syncDirection = SyncDirection.ClientToServer;
}
protected virtual void OnEnable()
{
ResetState();
if (NetworkServer.active)
NetworkIdentity.clientAuthorityCallback += OnClientAuthorityChanged;
}
protected virtual void OnDisable()
{
ResetState();
if (NetworkServer.active)
NetworkIdentity.clientAuthorityCallback -= OnClientAuthorityChanged;
}
[ServerCallback]
void OnClientAuthorityChanged(NetworkConnectionToClient conn, NetworkIdentity identity, bool authorityState)
{
if (identity != netIdentity) return;
// If server gets authority or syncdirection is server to client,
// we don't reset buffers.
// This is because if syncdirection is S to C, we will never have
// snapshot issues since there is only ever 1 source.
if (syncDirection == SyncDirection.ClientToServer)
{
ResetState();
RpcResetState();
}
}
// OnGUI allocates even if it does nothing. avoid in release.
#if UNITY_EDITOR || DEVELOPMENT_BUILD
// debug ///////////////////////////////////////////////////////////////
protected virtual void OnGUI()
{
if (!showOverlay) return;
if (!Camera.main) return;
// show data next to player for easier debugging. this is very useful!
// IMPORTANT: this is basically an ESP hack for shooter games.
// DO NOT make this available with a hotkey in release builds
if (!Debug.isDebugBuild) return;
// project position to screen
Vector3 point = Camera.main.WorldToScreenPoint(target.position);
// enough alpha, in front of camera and in screen?
if (point.z >= 0 && Utils.IsPointInScreen(point))
{
GUI.color = overlayColor;
GUILayout.BeginArea(new Rect(point.x, Screen.height - point.y, 200, 100));
// always show both client & server buffers so it's super
// obvious if we accidentally populate both.
GUILayout.Label($"Server Buffer:{serverSnapshots.Count}");
GUILayout.Label($"Client Buffer:{clientSnapshots.Count}");
GUILayout.EndArea();
GUI.color = Color.white;
}
}
protected virtual void DrawGizmos(SortedList<double, TransformSnapshot> buffer)
{
// only draw if we have at least two entries
if (buffer.Count < 2) return;
// calculate threshold for 'old enough' snapshots
double threshold = NetworkTime.localTime - NetworkClient.bufferTime;
Color oldEnoughColor = new Color(0, 1, 0, 0.5f);
Color notOldEnoughColor = new Color(0.5f, 0.5f, 0.5f, 0.3f);
// draw the whole buffer for easier debugging.
// it's worth seeing how much we have buffered ahead already
for (int i = 0; i < buffer.Count; ++i)
{
// color depends on if old enough or not
TransformSnapshot entry = buffer.Values[i];
bool oldEnough = entry.localTime <= threshold;
Gizmos.color = oldEnough ? oldEnoughColor : notOldEnoughColor;
Gizmos.DrawWireCube(entry.position, Vector3.one);
}
// extra: lines between start<->position<->goal
Gizmos.color = Color.green;
Gizmos.DrawLine(buffer.Values[0].position, target.position);
Gizmos.color = Color.white;
Gizmos.DrawLine(target.position, buffer.Values[1].position);
}
protected virtual void OnDrawGizmos()
{
// This fires in edit mode but that spams NRE's so check isPlaying
if (!Application.isPlaying) return;
if (!showGizmos) return;
if (isServer) DrawGizmos(serverSnapshots);
if (isClient) DrawGizmos(clientSnapshots);
}
#endif
}
}

View File

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

View File

@ -0,0 +1,12 @@
// A component to synchronize the position of child transforms of networked objects.
// There must be a NetworkTransform on the root object of the hierarchy. There can be multiple NetworkTransformChild components on an object. This does not use physics for synchronization, it simply synchronizes the localPosition and localRotation of the child transform and lerps towards the recieved values.
using System;
using UnityEngine;
namespace Mirror
{
// Deprecated 2022-10-25
[AddComponentMenu("")]
[Obsolete("NetworkTransformChild is not needed anymore. The .target is now exposed in NetworkTransform itself. Note you can open the Inspector in debug view and replace the source script instead of reassigning everything.")]
public class NetworkTransformChild : NetworkTransform {}
}

View File

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

View File

@ -0,0 +1,420 @@
// 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;
[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;
[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;
protected int lastClientCount = 1;
// 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 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()
{
// 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);
}
lastClientCount = clientSnapshots.Count;
}
}
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);
}
}
}

View File

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

View File

@ -0,0 +1,678 @@
// 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;
// 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;
[Tooltip("Detect and send only changed data, such as Position X and Z, not the full Vector3 of X Y Z. Lowers network data at cost of extra calculations.")]
public bool changedDetection = true;
[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;
protected bool positionChanged;
protected bool rotationChanged;
protected bool scaleChanged;
// Used to store last sent snapshots
protected TransformSnapshot lastSnapshot;
protected bool cachedSnapshotComparison;
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();
}
// 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();
if (changedDetection)
{
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);
}
}
else
{
cachedSnapshotComparison = CompareSnapshots(snapshot);
if (cachedSnapshotComparison && hasSentUnchangedPosition && onlySyncOnChange) { return; }
if (compressRotation)
{
RpcServerToClientSyncCompressRotation(
// only sync what the user wants to sync
syncPosition && positionChanged ? snapshot.position : default(Vector3?),
syncRotation && rotationChanged ? Compression.CompressQuaternion(snapshot.rotation) : default(uint?),
syncScale && scaleChanged ? snapshot.scale : default(Vector3?)
);
}
else
{
RpcServerToClientSync(
// only sync what the user wants to sync
syncPosition && positionChanged ? snapshot.position : default(Vector3?),
syncRotation && rotationChanged ? snapshot.rotation : default(Quaternion?),
syncScale && scaleChanged ? snapshot.scale : default(Vector3?)
);
}
if (cachedSnapshotComparison)
{
hasSentUnchangedPosition = true;
}
else
{
hasSentUnchangedPosition = false;
// Fixes https://github.com/MirrorNetworking/Mirror/issues/3572
// This also fixes https://github.com/MirrorNetworking/Mirror/issues/3573
// with the exception of Quaternion.Angle sensitivity has to be > 0.16.
// Unity issue, we are leaving it as is.
if (positionChanged) lastSnapshot.position = snapshot.position;
if (rotationChanged) lastSnapshot.rotation = snapshot.rotation;
if (positionChanged) lastSnapshot.scale = snapshot.scale;
}
}
}
}
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);
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();
if (changedDetection)
{
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);
}
}
else
{
cachedSnapshotComparison = CompareSnapshots(snapshot);
if (cachedSnapshotComparison && hasSentUnchangedPosition && onlySyncOnChange) { return; }
if (compressRotation)
{
CmdClientToServerSyncCompressRotation(
// only sync what the user wants to sync
syncPosition && positionChanged ? snapshot.position : default(Vector3?),
syncRotation && rotationChanged ? Compression.CompressQuaternion(snapshot.rotation) : default(uint?),
syncScale && scaleChanged ? snapshot.scale : default(Vector3?)
);
}
else
{
CmdClientToServerSync(
// only sync what the user wants to sync
syncPosition && positionChanged ? snapshot.position : default(Vector3?),
syncRotation && rotationChanged ? snapshot.rotation : default(Quaternion?),
syncScale && scaleChanged ? snapshot.scale : default(Vector3?)
);
}
if (cachedSnapshotComparison)
{
hasSentUnchangedPosition = true;
}
else
{
hasSentUnchangedPosition = false;
// Fixes https://github.com/MirrorNetworking/Mirror/issues/3572
// This also fixes https://github.com/MirrorNetworking/Mirror/issues/3573
// with the exception of Quaternion.Angle sensitivity has to be > 0.16.
// Unity issue, we are leaving it as is.
if (positionChanged) lastSnapshot.position = snapshot.position;
if (rotationChanged) lastSnapshot.rotation = snapshot.rotation;
if (positionChanged) lastSnapshot.scale = snapshot.scale;
}
}
}
}
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);
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());
}
}
// Returns true if position, rotation AND scale are unchanged, within given sensitivity range.
protected virtual bool CompareSnapshots(TransformSnapshot currentSnapshot)
{
positionChanged = Vector3.SqrMagnitude(lastSnapshot.position - currentSnapshot.position) > positionSensitivity * positionSensitivity;
rotationChanged = Quaternion.Angle(lastSnapshot.rotation, currentSnapshot.rotation) > rotationSensitivity;
scaleChanged = Vector3.SqrMagnitude(lastSnapshot.scale - currentSnapshot.scale) > scaleSensitivity * scaleSensitivity;
return (!positionChanged && !rotationChanged && !scaleChanged);
}
// cmd /////////////////////////////////////////////////////////////////
// only unreliable. see comment above of this file.
[Command(channel = Channels.Unreliable)]
void CmdClientToServerSync(Vector3? position, Quaternion? rotation, Vector3? scale)
{
OnClientToServerSync(position, rotation, scale);
//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(position, rotation, scale);
}
// cmd /////////////////////////////////////////////////////////////////
// only unreliable. see comment above of this file.
[Command(channel = Channels.Unreliable)]
void CmdClientToServerSyncCompressRotation(Vector3? position, uint? rotation, Vector3? scale)
{
// A fix to not apply current interpolated GetRotation when receiving null/unchanged value, instead use last sent snapshot rotation.
Quaternion newRotation;
if (rotation.HasValue)
{
newRotation = Compression.DecompressQuaternion((uint)rotation);
}
else
{
newRotation = serverSnapshots.Count > 0 ? serverSnapshots.Values[serverSnapshots.Count - 1].rotation : GetRotation();
}
OnClientToServerSync(position, newRotation, scale);
//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)
RpcServerToClientSyncCompressRotation(position, rotation, scale);
}
// 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 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();
}
AddSnapshot(serverSnapshots, connectionToClient.remoteTimeStamp + timeStampAdjustment + offset, position, rotation, scale);
}
// rpc /////////////////////////////////////////////////////////////////
// only unreliable. see comment above of this file.
[ClientRpc(channel = Channels.Unreliable)]
void RpcServerToClientSync(Vector3? position, Quaternion? rotation, Vector3? scale) =>
OnServerToClientSync(position, rotation, scale);
// rpc /////////////////////////////////////////////////////////////////
// only unreliable. see comment above of this file.
[ClientRpc(channel = Channels.Unreliable)]
void RpcServerToClientSyncCompressRotation(Vector3? position, uint? rotation, Vector3? scale)
{
// A fix to not apply current interpolated GetRotation when receiving null/unchanged value, instead use last sent snapshot rotation.
Quaternion newRotation;
if (rotation.HasValue)
{
newRotation = Compression.DecompressQuaternion((uint)rotation);
}
else
{
newRotation = clientSnapshots.Count > 0 ? clientSnapshots.Values[clientSnapshots.Count - 1].rotation : GetRotation();
}
OnServerToClientSync(position, newRotation, scale);
}
// server broadcasts sync message to all clients
protected virtual void OnServerToClientSync(Vector3? position, Quaternion? rotation, Vector3? scale)
{
// 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();
}
AddSnapshot(clientSnapshots, NetworkClient.connection.remoteTimeStamp + timeStampAdjustment + offset, position, rotation, scale);
}
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());
}
}
// This is to extract position/rotation/scale data from payload. Override
// Construct and Deconstruct if you are implementing a different SyncData logic.
// Note however that snapshot interpolation still requires the basic 3 data
// position, rotation and scale, which are computed from here.
protected virtual void DeconstructSyncData(System.ArraySegment<byte> receivedPayload, out byte? changedFlagData, out Vector3? position, out Quaternion? rotation, out Vector3? scale)
{
using (NetworkReaderPooled reader = NetworkReaderPool.Get(receivedPayload))
{
SyncData syncData = reader.Read<SyncData>();
changedFlagData = (byte)syncData.changedDataByte;
position = syncData.position;
rotation = syncData.quatRotation;
scale = syncData.scale;
}
}
}
}

View File

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

View File

@ -0,0 +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})";
}
}

View File

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

View File

@ -0,0 +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;
}
}
}

View File

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

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 09cc6745984c453a8cfb4cf4244d2570
timeCreated: 1693576410

View File

@ -0,0 +1,85 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!21 &2100000
Material:
serializedVersion: 8
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: LocalGhostMaterial
m_Shader: {fileID: 46, guid: 0000000000000000f000000000000000, type: 0}
m_Parent: {fileID: 0}
m_ModifiedSerializedProperties: 0
m_ValidKeywords:
- _ALPHAPREMULTIPLY_ON
m_InvalidKeywords: []
m_LightmapFlags: 4
m_EnableInstancingVariants: 0
m_DoubleSidedGI: 0
m_CustomRenderQueue: 3000
stringTagMap:
RenderType: Transparent
disabledShaderPasses: []
m_LockedProperties:
m_SavedProperties:
serializedVersion: 3
m_TexEnvs:
- _BumpMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _DetailAlbedoMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _DetailMask:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _DetailNormalMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _EmissionMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _MainTex:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _MetallicGlossMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _OcclusionMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _ParallaxMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
m_Ints: []
m_Floats:
- _BumpScale: 1
- _Cutoff: 0.5
- _DetailNormalMapScale: 1
- _DstBlend: 10
- _GlossMapScale: 1
- _Glossiness: 0.92
- _GlossyReflections: 1
- _Metallic: 0
- _Mode: 3
- _OcclusionStrength: 1
- _Parallax: 0.02
- _SmoothnessTextureChannel: 0
- _SpecularHighlights: 1
- _SrcBlend: 1
- _UVSec: 0
- _ZWrite: 0
m_Colors:
- _Color: {r: 1, g: 0, b: 0.067070484, a: 0.15686275}
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
m_BuildTextureStacks: []

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 411a48b4a197d4924bec3e3809bc9320
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 2100000
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,841 @@
// PredictedRigidbody which stores & indidvidually rewinds history per Rigidbody.
//
// This brings significant performance savings because:
// - if a scene has 1000 objects
// - and a player interacts with say 3 objects at a time
// - Physics.Simulate() would resimulate 1000 objects
// - where as this component only resimulates the 3 changed objects
//
// The downside is that history rewinding is done manually via Vector math,
// instead of real physics. It's not 100% correct - but it sure is fast!
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Mirror
{
public enum CorrectionMode
{
Set, // rigidbody.position/rotation = ...
Move, // rigidbody.MovePosition/Rotation
}
// [RequireComponent(typeof(Rigidbody))] <- RB is moved out at runtime, can't require it.
public class PredictedRigidbody : NetworkBehaviour
{
Transform tf; // this component is performance critical. cache .transform getter!
protected Rigidbody predictedRigidbody; // always valid, even while moved onto the ghost.
Vector3 lastPosition;
// motion smoothing happen on-demand, because it requires moving physics components to another GameObject.
// this only starts at a given velocity and ends when stopped moving.
// to avoid constant on/off/on effects, it also stays on for a minimum time.
[Header("Motion Smoothing")]
[Tooltip("Smoothing via Ghost-following only happens on demand, while moving with a minimum velocity.")]
public float motionSmoothingVelocityThreshold = 0.1f;
public float motionSmoothingAngularVelocityThreshold = 0.1f;
public float motionSmoothingTimeTolerance = 0.5f;
double motionSmoothingLastMovedTime;
// client keeps state history for correction & reconciliation.
// this needs to be a SortedList because we need to be able to insert inbetween.
// RingBuffer would be faster iteration, but can't do insertions.
[Header("State History")]
public int stateHistoryLimit = 32; // 32 x 50 ms = 1.6 seconds is definitely enough
readonly SortedList<double, RigidbodyState> stateHistory = new SortedList<double, RigidbodyState>();
public float recordInterval = 0.050f;
[Tooltip("(Optional) performance optimization where FixedUpdate.RecordState() only inserts state into history if the state actually changed.\nThis is generally a good idea.")]
public bool onlyRecordChanges = true;
[Tooltip("(Optional) performance optimization where received state is compared to the LAST recorded state first, before sampling the whole history.\n\nThis can save significant traversal overhead for idle objects with a tiny chance of missing corrections for objects which revisisted the same position in the recent history twice.")]
public bool compareLastFirst = true;
[Header("Reconciliation")]
[Tooltip("Correction threshold in meters. For example, 0.1 means that if the client is off by more than 10cm, it gets corrected.")]
public double positionCorrectionThreshold = 0.10;
[Tooltip("Correction threshold in degrees. For example, 5 means that if the client is off by more than 5 degrees, it gets corrected.")]
public double rotationCorrectionThreshold = 5;
[Tooltip("Applying server corrections one frame ahead gives much better results. We don't know why yet, so this is an option for now.")]
public bool oneFrameAhead = true;
[Header("Smoothing")]
[Tooltip("Configure how to apply the corrected state.")]
public CorrectionMode correctionMode = CorrectionMode.Move;
[Tooltip("Snap to the server state directly when velocity is < threshold. This is useful to reduce jitter/fighting effects before coming to rest.\nNote this applies position, rotation and velocity(!) so it's still smooth.")]
public float snapThreshold = 2; // 0.5 has too much fighting-at-rest, 2 seems ideal.
[Header("Visual Interpolation")]
[Tooltip("After creating the visual interpolation object, keep showing the original Rigidbody with a ghost (transparent) material for debugging.")]
public bool showGhost = true;
[Tooltip("Physics components are moved onto a ghost object beyond this threshold. Main object visually interpolates to it.")]
public float ghostVelocityThreshold = 0.1f;
[Tooltip("After creating the visual interpolation object, replace this object's renderer materials with the ghost (ideally transparent) material.")]
public Material localGhostMaterial;
public Material remoteGhostMaterial;
[Tooltip("How fast to interpolate to the target position, relative to how far we are away from it.\nHigher value will be more jitter but sharper moves, lower value will be less jitter but a little too smooth / rounded moves.")]
public float positionInterpolationSpeed = 15; // 10 is a little too low for billiards at least
public float rotationInterpolationSpeed = 10;
[Tooltip("Teleport if we are further than 'multiplier x collider size' behind.")]
public float teleportDistanceMultiplier = 10;
[Header("Bandwidth")]
[Tooltip("Reduce sends while velocity==0. Client's objects may slightly move due to gravity/physics, so we still want to send corrections occasionally even if an object is idle on the server the whole time.")]
public bool reduceSendsWhileIdle = true;
[Header("Debugging")]
public float lineTime = 10;
// Rigidbody & Collider are moved out into a separate object.
// this way the visual object can smoothly follow.
protected GameObject physicsCopy;
// protected Transform physicsCopyTransform; // caching to avoid GetComponent
// protected Rigidbody physicsCopyRigidbody => rb; // caching to avoid GetComponent
// protected Collider physicsCopyCollider; // caching to avoid GetComponent
float smoothFollowThreshold; // caching to avoid calculation in LateUpdate
// we also create one extra ghost for the exact known server state.
protected GameObject remoteCopy;
// joints
Vector3 initialPosition;
Quaternion initialRotation;
// Vector3 initialScale; // don't change scale for now. causes issues with parenting.
void Awake()
{
tf = transform;
predictedRigidbody = GetComponent<Rigidbody>();
if (predictedRigidbody == null) throw new InvalidOperationException($"Prediction: {name} is missing a Rigidbody component.");
// cache some threshold to avoid calculating them in LateUpdate
float colliderSize = GetComponentInChildren<Collider>().bounds.size.magnitude;
smoothFollowThreshold = colliderSize * teleportDistanceMultiplier;
// cache initial position/rotation/scale to be used when moving physics components (configurable joints' range of motion)
initialPosition = tf.position;
initialRotation = tf.rotation;
// initialScale = tf.localScale;
}
protected virtual void CopyRenderersAsGhost(GameObject destination, Material material)
{
// find the MeshRenderer component, which sometimes is on a child.
MeshRenderer originalMeshRenderer = GetComponentInChildren<MeshRenderer>(true);
MeshFilter originalMeshFilter = GetComponentInChildren<MeshFilter>(true);
if (originalMeshRenderer != null && originalMeshFilter != null)
{
MeshFilter meshFilter = destination.AddComponent<MeshFilter>();
meshFilter.mesh = originalMeshFilter.mesh;
MeshRenderer meshRenderer = destination.AddComponent<MeshRenderer>();
meshRenderer.material = originalMeshRenderer.material;
// renderers often have multiple materials. copy all.
if (originalMeshRenderer.materials != null)
{
Material[] materials = new Material[originalMeshRenderer.materials.Length];
for (int i = 0; i < materials.Length; ++i)
{
materials[i] = material;
}
meshRenderer.materials = materials; // need to reassign to see it in effect
}
}
// if we didn't find a renderer, show a warning
else Debug.LogWarning($"PredictedRigidbody: {name} found no renderer to copy onto the visual object. If you are using a custom setup, please overwrite PredictedRigidbody.CreateVisualCopy().");
}
// instantiate a physics-only copy of the gameobject to apply corrections.
// this way the main visual object can smoothly follow.
// it's best to separate the physics instead of separating the renderers.
// some projects have complex rendering / animation setups which we can't touch.
// besides, Rigidbody+Collider are two components, where as renders may be many.
protected virtual void CreateGhosts()
{
// skip if host mode or already separated
if (isServer || physicsCopy != null) return;
Debug.Log($"Separating Physics for {name}");
// create an empty GameObject with the same name + _Physical
// it's important to copy world position/rotation/scale, not local!
// because the original object may be a child of another.
//
// for example:
// parent (scale=1.5)
// child (scale=0.5)
//
// if we copy localScale then the copy has scale=0.5, where as the
// original would have a global scale of ~1.0.
physicsCopy = new GameObject($"{name}_Physical");
// assign the same Layer for the physics copy.
// games may use a custom physics collision matrix, layer matters.
physicsCopy.layer = gameObject.layer;
// add the PredictedRigidbodyPhysical component
PredictedRigidbodyPhysicsGhost physicsGhostRigidbody = physicsCopy.AddComponent<PredictedRigidbodyPhysicsGhost>();
physicsGhostRigidbody.target = tf;
// when moving (Configurable)Joints, their range of motion is
// relative to the initial position. if we move them after the
// GameObject rotated, the range of motion is wrong.
// the easiest solution is to move to initial position,
// then move physics components, then move back.
// => remember previous
Vector3 position = tf.position;
Quaternion rotation = tf.rotation;
// Vector3 scale = tf.localScale; // don't change scale for now. causes issues with parenting.
// => reset to initial
physicsGhostRigidbody.transform.position = tf.position = initialPosition;
physicsGhostRigidbody.transform.rotation = tf.rotation = initialRotation;
physicsGhostRigidbody.transform.localScale = tf.lossyScale;// world scale! // = initialScale; // don't change scale for now. causes issues with parenting.
// => move physics components
PredictionUtils.MovePhysicsComponents(gameObject, physicsCopy);
// => reset previous
physicsGhostRigidbody.transform.position = tf.position = position;
physicsGhostRigidbody.transform.rotation = tf.rotation = rotation;
//physicsGhostRigidbody.transform.localScale = tf.lossyScale; // world scale! //= scale; // don't change scale for now. causes issues with parenting.
// show ghost by copying all renderers / materials with ghost material applied
if (showGhost)
{
// one for the locally predicted rigidbody
CopyRenderersAsGhost(physicsCopy, localGhostMaterial);
// one for the latest remote state for comparison
// it's important to copy world position/rotation/scale, not local!
// because the original object may be a child of another.
//
// for example:
// parent (scale=1.5)
// child (scale=0.5)
//
// if we copy localScale then the copy has scale=0.5, where as the
// original would have a global scale of ~1.0.
remoteCopy = new GameObject($"{name}_Remote");
remoteCopy.transform.position = tf.position; // world position!
remoteCopy.transform.rotation = tf.rotation; // world rotation!
remoteCopy.transform.localScale = tf.lossyScale; // world scale!
CopyRenderersAsGhost(remoteCopy, remoteGhostMaterial);
}
// assign our Rigidbody reference to the ghost
predictedRigidbody = physicsCopy.GetComponent<Rigidbody>();
}
protected virtual void DestroyGhosts()
{
// move the copy's Rigidbody back onto self.
// important for scene objects which may be reused for AOI spawn/despawn.
// otherwise next time they wouldn't have a collider anymore.
if (physicsCopy != null)
{
// when moving (Configurable)Joints, their range of motion is
// relative to the initial position. if we move them after the
// GameObject rotated, the range of motion is wrong.
// the easiest solution is to move to initial position,
// then move physics components, then move back.
// => remember previous
Vector3 position = tf.position;
Quaternion rotation = tf.rotation;
Vector3 scale = tf.localScale;
// => reset to initial
physicsCopy.transform.position = tf.position = initialPosition;
physicsCopy.transform.rotation = tf.rotation = initialRotation;
physicsCopy.transform.localScale = tf.lossyScale;// = initialScale;
// => move physics components
PredictionUtils.MovePhysicsComponents(physicsCopy, gameObject);
// => reset previous
tf.position = position;
tf.rotation = rotation;
tf.localScale = scale;
// when moving components back, we need to undo the joints initial-delta rotation that we added.
Destroy(physicsCopy);
// reassign our Rigidbody reference
predictedRigidbody = GetComponent<Rigidbody>();
}
// simply destroy the remote copy
if (remoteCopy != null) Destroy(remoteCopy);
}
// this shows in profiler LateUpdates! need to make this as fast as possible!
protected virtual void SmoothFollowPhysicsCopy()
{
// hard follow:
// tf.position = physicsCopyCollider.position;
// tf.rotation = physicsCopyCollider.rotation;
// ORIGINAL VERSION: CLEAN AND SIMPLE
/*
// if we are further than N colliders sizes behind, then teleport
float colliderSize = physicsCopyCollider.bounds.size.magnitude;
float threshold = colliderSize * teleportDistanceMultiplier;
float distance = Vector3.Distance(tf.position, physicsCopyRigidbody.position);
if (distance > threshold)
{
tf.position = physicsCopyRigidbody.position;
tf.rotation = physicsCopyRigidbody.rotation;
Debug.Log($"[PredictedRigidbody] Teleported because distance to physics copy = {distance:F2} > threshold {threshold:F2}");
return;
}
// smoothly interpolate to the target position.
// speed relative to how far away we are
float positionStep = distance * positionInterpolationSpeed;
tf.position = Vector3.MoveTowards(tf.position, physicsCopyRigidbody.position, positionStep * Time.deltaTime);
// smoothly interpolate to the target rotation.
// Quaternion.RotateTowards doesn't seem to work at all, so let's use SLerp.
tf.rotation = Quaternion.Slerp(tf.rotation, physicsCopyRigidbody.rotation, rotationInterpolationSpeed * Time.deltaTime);
*/
// FAST VERSION: this shows in profiler a lot, so cache EVERYTHING!
tf.GetPositionAndRotation(out Vector3 currentPosition, out Quaternion currentRotation); // faster than tf.position + tf.rotation
Vector3 physicsPosition = predictedRigidbody.position;
Quaternion physicsRotation = predictedRigidbody.rotation;
float deltaTime = Time.deltaTime;
float distance = Vector3.Distance(currentPosition, physicsPosition);
if (distance > smoothFollowThreshold)
{
tf.SetPositionAndRotation(physicsPosition, physicsRotation); // faster than .position and .rotation manually
Debug.Log($"[PredictedRigidbody] Teleported because distance to physics copy = {distance:F2} > threshold {smoothFollowThreshold:F2}");
return;
}
// smoothly interpolate to the target position.
// speed relative to how far away we are.
// => speed increases by distance² because the further away, the
// sooner we need to catch the fuck up
// float positionStep = (distance * distance) * interpolationSpeed;
float positionStep = distance * positionInterpolationSpeed;
Vector3 newPosition = Vector3.MoveTowards(currentPosition, physicsPosition, positionStep * deltaTime);
// smoothly interpolate to the target rotation.
// Quaternion.RotateTowards doesn't seem to work at all, so let's use SLerp.
// Quaternions always need to be normalized in order to be a valid rotation after operations
Quaternion newRotation = Quaternion.Slerp(currentRotation, physicsRotation, rotationInterpolationSpeed * deltaTime).normalized;
// assign position and rotation together. faster than accessing manually.
tf.SetPositionAndRotation(newPosition, newRotation);
}
// destroy visual copy only in OnStopClient().
// OnDestroy() wouldn't be called for scene objects that are only disabled instead of destroyed.
public override void OnStopClient()
{
DestroyGhosts();
}
void UpdateServer()
{
// bandwidth optimization while idle.
if (reduceSendsWhileIdle)
{
// while moving, always sync every frame for immediate corrections.
// while idle, only sync once per second.
//
// we still need to sync occasionally because objects on client
// may still slide or move slightly due to gravity, physics etc.
// and those still need to get corrected if not moving on server.
//
// TODO
// next round of optimizations: if client received nothing for 1s,
// force correct to last received state. then server doesn't need
// to send once per second anymore.
syncInterval = IsMoving() ? 0 : 1;
}
// always set dirty to always serialize in next sync interval.
SetDirty();
}
// movement detection is virtual, in case projects want to use other methods.
protected virtual bool IsMoving() =>
predictedRigidbody.linearVelocity.magnitude >= motionSmoothingVelocityThreshold ||
predictedRigidbody.angularVelocity.magnitude >= motionSmoothingAngularVelocityThreshold;
void UpdateGhosting()
{
// client only uses ghosts on demand while interacting.
// this way 1000 GameObjects don't need +1000 Ghost GameObjects all the time!
// no ghost at the moment
if (physicsCopy == null)
{
// faster than velocity threshold? then create the ghosts.
// with 10% buffer zone so we don't flip flop all the time.
if (IsMoving())
{
CreateGhosts();
OnBeginPrediction();
}
}
// ghosting at the moment
else
{
// always set last moved time while moving.
// this way we can avoid on/off/oneffects when stopping.
if (IsMoving())
{
motionSmoothingLastMovedTime = NetworkTime.time;
}
// slower than velocity threshold? then destroy the ghosts.
// with a minimum time since starting to move, to avoid on/off/on effects.
else
{
if (NetworkTime.time >= motionSmoothingLastMovedTime + motionSmoothingTimeTolerance)
{
DestroyGhosts();
OnEndPrediction();
physicsCopy = null; // TESTING
}
}
}
}
void Update()
{
if (isServer) UpdateServer();
if (isClientOnly) UpdateGhosting();
}
void LateUpdate()
{
// only follow on client-only, not in server or host mode
if (isClientOnly && physicsCopy) SmoothFollowPhysicsCopy();
}
void FixedUpdate()
{
// on clients (not host) we record the current state every FixedUpdate.
// this is cheap, and allows us to keep a dense history.
if (isClientOnly)
{
// OPTIMIZATION: RecordState() is expensive because it inserts into a SortedList.
// only record if state actually changed!
// risks not having up to date states when correcting,
// but it doesn't matter since we'll always compare with the 'newest' anyway.
//
// we check in here instead of in RecordState() because RecordState() should definitely record if we call it!
if (onlyRecordChanges)
{
// TODO maybe don't reuse the correction thresholds?
tf.GetPositionAndRotation(out Vector3 position, out Quaternion rotation);
if (Vector3.Distance(lastRecorded.position, position) < positionCorrectionThreshold &&
Quaternion.Angle(lastRecorded.rotation, rotation) < rotationCorrectionThreshold)
{
// Debug.Log($"FixedUpdate for {name}: taking optimized early return instead of recording state.");
return;
}
}
RecordState();
}
}
// manually store last recorded so we can easily check against this
// without traversing the SortedList.
RigidbodyState lastRecorded;
double lastRecordTime;
void RecordState()
{
// instead of recording every fixedupdate, let's record in an interval.
// we don't want to record every tiny move and correct too hard.
if (NetworkTime.time < lastRecordTime + recordInterval) return;
lastRecordTime = NetworkTime.time;
// NetworkTime.time is always behind by bufferTime.
// prediction aims to be on the exact same server time (immediately).
// use predictedTime to record state, otherwise we would record in the past.
double predictedTime = NetworkTime.predictedTime;
// FixedUpdate may run twice in the same frame / NetworkTime.time.
// for now, simply don't record if already recorded there.
// previously we checked ContainsKey which is O(logN) for SortedList
// if (stateHistory.ContainsKey(predictedTime))
// return;
// instead, simply store the last recorded time and don't insert if same.
if (predictedTime == lastRecorded.timestamp) return;
// keep state history within limit
if (stateHistory.Count >= stateHistoryLimit)
stateHistory.RemoveAt(0);
// grab current position/rotation/velocity only once.
// this is performance critical, avoid calling .transform multiple times.
tf.GetPositionAndRotation(out Vector3 currentPosition, out Quaternion currentRotation); // faster than accessing .position + .rotation manually
Vector3 currentVelocity = predictedRigidbody.linearVelocity;
Vector3 currentAngularVelocity = predictedRigidbody.angularVelocity;
// calculate delta to previous state (if any)
Vector3 positionDelta = Vector3.zero;
Vector3 velocityDelta = Vector3.zero;
Vector3 angularVelocityDelta = Vector3.zero;
Quaternion rotationDelta = Quaternion.identity;
if (stateHistory.Count > 0)
{
RigidbodyState last = stateHistory.Values[stateHistory.Count - 1];
positionDelta = currentPosition - last.position;
velocityDelta = currentVelocity - last.velocity;
// Quaternions always need to be normalized in order to be valid rotations after operations
rotationDelta = (currentRotation * Quaternion.Inverse(last.rotation)).normalized;
angularVelocityDelta = currentAngularVelocity - last.angularVelocity;
// debug draw the recorded state
// Debug.DrawLine(last.position, currentPosition, Color.red, lineTime);
}
// create state to insert
RigidbodyState state = new RigidbodyState(
predictedTime,
positionDelta,
currentPosition,
rotationDelta,
currentRotation,
velocityDelta,
currentVelocity,
angularVelocityDelta,
currentAngularVelocity
);
// add state to history
stateHistory.Add(predictedTime, state);
// manually remember last inserted state for faster .Last comparisons
lastRecorded = state;
}
// optional user callbacks, in case people need to know about events.
protected virtual void OnSnappedIntoPlace() {}
protected virtual void OnBeforeApplyState() {}
protected virtual void OnCorrected() {}
protected virtual void OnBeginPrediction() {} // when the Rigidbody moved above threshold and we created a ghost
protected virtual void OnEndPrediction() {} // when the Rigidbody came to rest and we destroyed the ghost
void ApplyState(double timestamp, Vector3 position, Quaternion rotation, Vector3 velocity, Vector3 angularVelocity)
{
// fix rigidbodies seemingly dancing in place instead of coming to rest.
// hard snap to the position below a threshold velocity.
// this is fine because the visual object still smoothly interpolates to it.
// => consider both velocity and angular velocity (in case of Rigidbodies only rotating with joints etc.)
if (predictedRigidbody.linearVelocity.magnitude <= snapThreshold &&
predictedRigidbody.angularVelocity.magnitude <= snapThreshold)
{
// Debug.Log($"Prediction: snapped {name} into place because velocity {predictedRigidbody.velocity.magnitude:F3} <= {snapThreshold:F3}");
// apply server state immediately.
// important to apply velocity as well, instead of Vector3.zero.
// in case an object is still slightly moving, we don't want it
// to stop and start moving again on client - slide as well here.
predictedRigidbody.position = position;
predictedRigidbody.rotation = rotation;
// projects may keep Rigidbodies as kinematic sometimes. in that case, setting velocity would log an error
if (!predictedRigidbody.isKinematic)
{
predictedRigidbody.linearVelocity = velocity;
predictedRigidbody.angularVelocity = angularVelocity;
}
// clear history and insert the exact state we just applied.
// this makes future corrections more accurate.
stateHistory.Clear();
stateHistory.Add(timestamp, new RigidbodyState(
timestamp,
Vector3.zero,
position,
Quaternion.identity,
rotation,
Vector3.zero,
velocity,
Vector3.zero,
angularVelocity
));
// user callback
OnSnappedIntoPlace();
return;
}
// we have a callback for snapping into place (above).
// we also need one for corrections without snapping into place.
// call it before applying pos/rot/vel in case we need to set kinematic etc.
OnBeforeApplyState();
// Rigidbody .position teleports, while .MovePosition interpolates
// TODO is this a good idea? what about next capture while it's interpolating?
if (correctionMode == CorrectionMode.Move)
{
predictedRigidbody.MovePosition(position);
predictedRigidbody.MoveRotation(rotation);
}
else if (correctionMode == CorrectionMode.Set)
{
predictedRigidbody.position = position;
predictedRigidbody.rotation = rotation;
}
// there's only one way to set velocity.
// (projects may keep Rigidbodies as kinematic sometimes. in that case, setting velocity would log an error)
if (!predictedRigidbody.isKinematic)
{
predictedRigidbody.linearVelocity = velocity;
predictedRigidbody.angularVelocity = angularVelocity;
}
}
// process a received server state.
// compares it against our history and applies corrections if needed.
void OnReceivedState(double timestamp, RigidbodyState state)
{
// always update remote state ghost
if (remoteCopy != null)
{
Transform remoteCopyTransform = remoteCopy.transform;
remoteCopyTransform.SetPositionAndRotation(state.position, state.rotation); // faster than .position + .rotation setters
remoteCopyTransform.localScale = tf.lossyScale; // world scale! see CreateGhosts comment.
}
// OPTIONAL performance optimization when comparing idle objects.
// even idle objects will have a history of ~32 entries.
// sampling & traversing through them is unnecessarily costly.
// instead, compare directly against the current rigidbody position!
// => this is technically not 100% correct if an object runs in
// circles where it may revisit the same position twice.
// => but practically, objects that didn't move will have their
// whole history look like the last inserted state.
// => comparing against that is free and gives us a significant
// performance saving vs. a tiny chance of incorrect results due
// to objects running in circles.
// => the RecordState() call below is expensive too, so we want to
// do this before even recording the latest state. the only way
// to do this (in case last recorded state is too old), is to
// compare against live rigidbody.position without any recording.
// this is as fast as it gets for skipping idle objects.
//
// if this ever causes issues, feel free to disable it.
if (compareLastFirst &&
Vector3.Distance(state.position, predictedRigidbody.position) < positionCorrectionThreshold &&
Quaternion.Angle(state.rotation, predictedRigidbody.rotation) < rotationCorrectionThreshold)
{
// Debug.Log($"OnReceivedState for {name}: taking optimized early return!");
return;
}
// we only capture state every 'interval' milliseconds.
// so the newest entry in 'history' may be up to 'interval' behind 'now'.
// if there's no latency, we may receive a server state for 'now'.
// sampling would fail, if we haven't recorded anything in a while.
// to solve this, always record the current state when receiving a server state.
RecordState();
// correction requires at least 2 existing states for 'before' and 'after'.
// if we don't have two yet, drop this state and try again next time once we recorded more.
if (stateHistory.Count < 2) return;
RigidbodyState oldest = stateHistory.Values[0];
RigidbodyState newest = stateHistory.Values[stateHistory.Count - 1];
// edge case: is the state older than the oldest state in history?
// this can happen if the client gets so far behind the server
// that it doesn't have a recored history to sample from.
// in that case, we should hard correct the client.
// otherwise it could be out of sync as long as it's too far behind.
if (state.timestamp < oldest.timestamp)
{
// when starting, client may only have 2-3 states in history.
// it's expected that server states would be behind those 2-3.
// only show a warning if it's behind the full history limit!
if (stateHistory.Count >= stateHistoryLimit)
Debug.LogWarning($"Hard correcting client object {name} because the client is too far behind the server. History of size={stateHistory.Count} @ t={timestamp:F3} oldest={oldest.timestamp:F3} newest={newest.timestamp:F3}. This would cause the client to be out of sync as long as it's behind.");
// force apply the state
ApplyState(state.timestamp, state.position, state.rotation, state.velocity, state.angularVelocity);
return;
}
// edge case: is it newer than the newest state in history?
// this can happen if client's predictedTime predicts too far ahead of the server.
// in that case, log a warning for now but still apply the correction.
// otherwise it could be out of sync as long as it's too far ahead.
//
// for example, when running prediction on the same machine with near zero latency.
// when applying corrections here, this looks just fine on the local machine.
if (newest.timestamp < state.timestamp)
{
// the correction is for a state in the future.
// we clamp it to 'now'.
// but only correct if off by threshold.
// TODO maybe we should interpolate this back to 'now'?
if (Vector3.Distance(state.position, predictedRigidbody.position) >= positionCorrectionThreshold)
{
double ahead = state.timestamp - newest.timestamp;
Debug.Log($"Hard correction because the client is ahead of the server by {(ahead*1000):F1}ms. History of size={stateHistory.Count} @ t={timestamp:F3} oldest={oldest.timestamp:F3} newest={newest.timestamp:F3}. This can happen when latency is near zero, and is fine unless it shows jitter.");
ApplyState(state.timestamp, state.position, state.rotation, state.velocity, state.angularVelocity);
}
return;
}
// find the two closest client states between timestamp
if (!Prediction.Sample(stateHistory, timestamp, out RigidbodyState before, out RigidbodyState after, out int afterIndex, out double t))
{
// something went very wrong. sampling should've worked.
// hard correct to recover the error.
Debug.LogError($"Failed to sample history of size={stateHistory.Count} @ t={timestamp:F3} oldest={oldest.timestamp:F3} newest={newest.timestamp:F3}. This should never happen because the timestamp is within history.");
ApplyState(state.timestamp, state.position, state.rotation, state.velocity, state.angularVelocity);
return;
}
// interpolate between them to get the best approximation
RigidbodyState interpolated = RigidbodyState.Interpolate(before, after, (float)t);
// calculate the difference between where we were and where we should be
// TODO only position for now. consider rotation etc. too later
float positionDifference = Vector3.Distance(state.position, interpolated.position);
float rotationDifference = Quaternion.Angle(state.rotation, interpolated.rotation);
// Debug.Log($"Sampled history of size={stateHistory.Count} @ {timestamp:F3}: client={interpolated.position} server={state.position} difference={difference:F3} / {correctionThreshold:F3}");
// too far off? then correct it
if (positionDifference >= positionCorrectionThreshold ||
rotationDifference >= rotationCorrectionThreshold)
{
// Debug.Log($"CORRECTION NEEDED FOR {name} @ {timestamp:F3}: client={interpolated.position} server={state.position} difference={difference:F3}");
// show the received correction position + velocity for debugging.
// helps to compare with the interpolated/applied correction locally.
//Debug.DrawLine(state.position, state.position + state.velocity * 0.1f, Color.white, lineTime);
// insert the correction and correct the history on top of it.
// returns the final recomputed state after rewinding.
RigidbodyState recomputed = Prediction.CorrectHistory(stateHistory, stateHistoryLimit, state, before, after, afterIndex);
// log, draw & apply the final position.
// always do this here, not when iterating above, in case we aren't iterating.
// for example, on same machine with near zero latency.
// int correctedAmount = stateHistory.Count - afterIndex;
// Debug.Log($"Correcting {name}: {correctedAmount} / {stateHistory.Count} states to final position from: {rb.position} to: {last.position}");
//Debug.DrawLine(physicsCopyRigidbody.position, recomputed.position, Color.green, lineTime);
ApplyState(recomputed.timestamp, recomputed.position, recomputed.rotation, recomputed.velocity, recomputed.angularVelocity);
// user callback
OnCorrected();
}
}
// send state to clients every sendInterval.
// reliable for now.
// TODO we should use the one from FixedUpdate
public override void OnSerialize(NetworkWriter writer, bool initialState)
{
// Time.time was at the beginning of this frame.
// NetworkLateUpdate->Broadcast->OnSerialize is at the end of the frame.
// as result, client should use this to correct the _next_ frame.
// otherwise we see noticeable resets that seem off by one frame.
//
// to solve this, we can send the current deltaTime.
// server is technically supposed to be at a fixed frame rate, but this can vary.
// sending server's current deltaTime is the safest option.
// client then applies it on top of remoteTimestamp.
// FAST VERSION: this shows in profiler a lot, so cache EVERYTHING!
tf.GetPositionAndRotation(out Vector3 position, out Quaternion rotation); // faster than tf.position + tf.rotation. server's rigidbody is on the same transform.
writer.WriteFloat(Time.deltaTime);
writer.WriteVector3(position);
writer.WriteQuaternion(rotation);
writer.WriteVector3(predictedRigidbody.linearVelocity);
writer.WriteVector3(predictedRigidbody.angularVelocity);
}
// read the server's state, compare with client state & correct if necessary.
public override void OnDeserialize(NetworkReader reader, bool initialState)
{
// deserialize data
// we want to know the time on the server when this was sent, which is remoteTimestamp.
double timestamp = NetworkClient.connection.remoteTimeStamp;
// server send state at the end of the frame.
// parse and apply the server's delta time to our timestamp.
// otherwise we see noticeable resets that seem off by one frame.
double serverDeltaTime = reader.ReadFloat();
timestamp += serverDeltaTime;
// however, adding yet one more frame delay gives much(!) better results.
// we don't know why yet, so keep this as an option for now.
// possibly because client captures at the beginning of the frame,
// with physics happening at the end of the frame?
if (oneFrameAhead) timestamp += serverDeltaTime;
// parse state
Vector3 position = reader.ReadVector3();
Quaternion rotation = reader.ReadQuaternion();
Vector3 velocity = reader.ReadVector3();
Vector3 angularVelocity = reader.ReadVector3();
// process received state
OnReceivedState(timestamp, new RigidbodyState(timestamp, Vector3.zero, position, Quaternion.identity, rotation, Vector3.zero, velocity, Vector3.zero, angularVelocity));
}
protected override void OnValidate()
{
base.OnValidate();
// force syncDirection to be ServerToClient
syncDirection = SyncDirection.ServerToClient;
// state should be synced immediately for now.
// later when we have prediction fully dialed in,
// then we can maybe relax this a bit.
syncInterval = 0;
}
// helper function for Physics tests to check if a Rigidbody belongs to
// a PredictedRigidbody component (either on it, or on its ghost).
public static bool IsPredicted(Rigidbody rb, out PredictedRigidbody predictedRigidbody)
{
// by default, Rigidbody is on the PredictedRigidbody GameObject
if (rb.TryGetComponent(out predictedRigidbody))
return true;
// it might be on a ghost while interacting
if (rb.TryGetComponent(out PredictedRigidbodyPhysicsGhost ghost))
{
predictedRigidbody = ghost.target.GetComponent<PredictedRigidbody>();
return true;
}
// otherwise the Rigidbody does not belong to any PredictedRigidbody.
predictedRigidbody = null;
return false;
}
// helper function for Physics tests to check if a Collider (which may be in children) belongs to
// a PredictedRigidbody component (either on it, or on its ghost).
public static bool IsPredicted(Collider co, out PredictedRigidbody predictedRigidbody)
{
// by default, Collider is on the PredictedRigidbody GameObject or it's children.
predictedRigidbody = co.GetComponentInParent<PredictedRigidbody>();
if (predictedRigidbody != null)
return true;
// it might be on a ghost while interacting
PredictedRigidbodyPhysicsGhost ghost = co.GetComponentInParent<PredictedRigidbodyPhysicsGhost>();
if (ghost != null && ghost.target != null && ghost.target.TryGetComponent(out predictedRigidbody))
return true;
// otherwise the Rigidbody does not belong to any PredictedRigidbody.
predictedRigidbody = null;
return false;
}
}
}

View File

@ -0,0 +1,15 @@
fileFormatVersion: 2
guid: d38927cdc6024b9682b5fe9778b9ef99
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences:
- localGhostMaterial: {fileID: 2100000, guid: 411a48b4a197d4924bec3e3809bc9320,
type: 2}
- remoteGhostMaterial: {fileID: 2100000, guid: 04f0b2088c857414393bab3b80356776,
type: 2}
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,15 @@
// Prediction moves out the Rigidbody & Collider into a separate object.
// this component simply points back to the owner component.
// in case Raycasts hit it and need to know the owner, etc.
using UnityEngine;
namespace Mirror
{
public class PredictedRigidbodyPhysicsGhost : MonoBehaviour
{
// this is performance critical, so store target's .Transform instead of
// PredictedRigidbody, this way we don't need to call the .transform getter.
[Tooltip("The predicted rigidbody owner.")]
public Transform target;
}
}

View File

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

Some files were not shown because too many files have changed in this diff Show More