// wraps around a transport and adds latency/loss/scramble simulation. // // reliable: latency // unreliable: latency, loss, scramble (unreliable isn't ordered so we scramble) // // IMPORTANT: use Time.unscaledTime instead of Time.time. // some games might have Time.timeScale modified. // see also: https://github.com/vis2k/Mirror/issues/2907 using System; using System.Collections.Generic; using UnityEngine; namespace Mirror { struct QueuedMessage { public int connectionId; public byte[] bytes; public float time; } [HelpURL("https://mirror-networking.gitbook.io/docs/transports/latency-simulaton-transport")] [DisallowMultipleComponent] public class LatencySimulation : Transport { public Transport wrap; [Header("Common")] [Tooltip("Spike latency via perlin(Time * speedMultiplier) * spikeMultiplier")] [Range(0, 1)] public float latencySpikeMultiplier; [Tooltip("Spike latency via perlin(Time * speedMultiplier) * spikeMultiplier")] public float latencySpikeSpeedMultiplier = 1; [Header("Reliable Messages")] [Tooltip("Reliable latency in seconds")] public float reliableLatency; // note: packet loss over reliable manifests itself in latency. // don't need (and can't add) a loss option here. // note: reliable is ordered by definition. no need to scramble. [Header("Unreliable Messages")] [Tooltip("Packet loss in %")] [Range(0, 1)] public float unreliableLoss; [Tooltip("Unreliable latency in seconds")] public float unreliableLatency; [Tooltip("Scramble % of unreliable messages, just like over the real network. Mirror unreliable is unordered.")] [Range(0, 1)] public float unreliableScramble; // message queues // list so we can insert randomly (scramble) List reliableClientToServer = new List(); List reliableServerToClient = new List(); List unreliableClientToServer = new List(); List unreliableServerToClient = new List(); // random // UnityEngine.Random.value is [0, 1] with both upper and lower bounds inclusive // but we need the upper bound to be exclusive, so using System.Random instead. // => NextDouble() is NEVER < 0 so loss=0 never drops! // => NextDouble() is ALWAYS < 1 so loss=1 always drops! System.Random random = new System.Random(); public void Awake() { if (wrap == null) throw new Exception("PressureDrop requires an underlying transport to wrap around."); } // forward enable/disable to the wrapped transport void OnEnable() { wrap.enabled = true; } void OnDisable() { wrap.enabled = false; } // noise function can be replaced if needed protected virtual float Noise(float time) => Mathf.PerlinNoise(time, time); // helper function to simulate latency float SimulateLatency(int channeldId) { // spike over perlin noise. // no spikes isn't realistic. // sin is too predictable / no realistic. // perlin is still deterministic and random enough. float spike = Noise(Time.unscaledTime * latencySpikeSpeedMultiplier) * latencySpikeMultiplier; // base latency switch (channeldId) { case Channels.Reliable: return reliableLatency + spike; case Channels.Unreliable: return unreliableLatency + spike; default: return 0; } } // helper function to simulate a send with latency/loss/scramble void SimulateSend(int connectionId, ArraySegment segment, int channelId, float latency, List reliableQueue, List unreliableQueue) { // segment is only valid after returning. copy it. // (allocates for now. it's only for testing anyway.) byte[] bytes = new byte[segment.Count]; Buffer.BlockCopy(segment.Array, segment.Offset, bytes, 0, segment.Count); // enqueue message. send after latency interval. QueuedMessage message = new QueuedMessage { connectionId = connectionId, bytes = bytes, time = Time.unscaledTime + latency }; switch (channelId) { case Channels.Reliable: // simulate latency reliableQueue.Add(message); break; case Channels.Unreliable: // simulate packet loss bool drop = random.NextDouble() < unreliableLoss; if (!drop) { // simulate scramble (Random.Next is < max, so +1) bool scramble = random.NextDouble() < unreliableScramble; int last = unreliableQueue.Count; int index = scramble ? random.Next(0, last + 1) : last; // simulate latency unreliableQueue.Insert(index, message); } break; default: Debug.LogError($"{nameof(LatencySimulation)} unexpected channelId: {channelId}"); break; } } public override bool Available() => wrap.Available(); public override void ClientConnect(string address) { wrap.OnClientConnected = OnClientConnected; wrap.OnClientDataReceived = OnClientDataReceived; wrap.OnClientError = OnClientError; wrap.OnClientDisconnected = OnClientDisconnected; wrap.ClientConnect(address); } public override void ClientConnect(Uri uri) { wrap.OnClientConnected = OnClientConnected; wrap.OnClientDataReceived = OnClientDataReceived; wrap.OnClientError = OnClientError; wrap.OnClientDisconnected = OnClientDisconnected; wrap.ClientConnect(uri); } public override bool ClientConnected() => wrap.ClientConnected(); public override void ClientDisconnect() { wrap.ClientDisconnect(); reliableClientToServer.Clear(); unreliableClientToServer.Clear(); } public override void ClientSend(ArraySegment segment, int channelId) { float latency = SimulateLatency(channelId); SimulateSend(0, segment, channelId, latency, reliableClientToServer, unreliableClientToServer); } public override Uri ServerUri() => wrap.ServerUri(); public override bool ServerActive() => wrap.ServerActive(); public override string ServerGetClientAddress(int connectionId) => wrap.ServerGetClientAddress(connectionId); public override void ServerDisconnect(int connectionId) => wrap.ServerDisconnect(connectionId); public override void ServerSend(int connectionId, ArraySegment segment, int channelId) { float latency = SimulateLatency(channelId); SimulateSend(connectionId, segment, channelId, latency, reliableServerToClient, unreliableServerToClient); } public override void ServerStart() { wrap.OnServerConnected = OnServerConnected; wrap.OnServerDataReceived = OnServerDataReceived; wrap.OnServerError = OnServerError; wrap.OnServerDisconnected = OnServerDisconnected; wrap.ServerStart(); } public override void ServerStop() { wrap.ServerStop(); reliableServerToClient.Clear(); unreliableServerToClient.Clear(); } public override void ClientEarlyUpdate() => wrap.ClientEarlyUpdate(); public override void ServerEarlyUpdate() => wrap.ServerEarlyUpdate(); public override void ClientLateUpdate() { // flush reliable messages after latency while (reliableClientToServer.Count > 0) { // check the first message time QueuedMessage message = reliableClientToServer[0]; if (message.time <= Time.unscaledTime) { // send and eat wrap.ClientSend(new ArraySegment(message.bytes), Channels.Reliable); reliableClientToServer.RemoveAt(0); } // not enough time elapsed yet break; } // flush unreliable messages after latency while (unreliableClientToServer.Count > 0) { // check the first message time QueuedMessage message = unreliableClientToServer[0]; if (message.time <= Time.unscaledTime) { // send and eat wrap.ClientSend(new ArraySegment(message.bytes), Channels.Unreliable); unreliableClientToServer.RemoveAt(0); } // not enough time elapsed yet break; } // update wrapped transport too wrap.ClientLateUpdate(); } public override void ServerLateUpdate() { // flush reliable messages after latency while (reliableServerToClient.Count > 0) { // check the first message time QueuedMessage message = reliableServerToClient[0]; if (message.time <= Time.unscaledTime) { // send and eat wrap.ServerSend(message.connectionId, new ArraySegment(message.bytes), Channels.Reliable); reliableServerToClient.RemoveAt(0); } // not enough time elapsed yet break; } // flush unreliable messages after latency while (unreliableServerToClient.Count > 0) { // check the first message time QueuedMessage message = unreliableServerToClient[0]; if (message.time <= Time.unscaledTime) { // send and eat wrap.ServerSend(message.connectionId, new ArraySegment(message.bytes), Channels.Unreliable); unreliableServerToClient.RemoveAt(0); } // not enough time elapsed yet break; } // update wrapped transport too wrap.ServerLateUpdate(); } public override int GetBatchThreshold(int channelId) => wrap.GetBatchThreshold(channelId); public override int GetMaxPacketSize(int channelId = 0) => wrap.GetMaxPacketSize(channelId); public override void Shutdown() => wrap.Shutdown(); public override string ToString() => $"{nameof(LatencySimulation)} {wrap}"; } }