diff --git a/Assets/Mirror/Core/TickManager/NetworkPhysicsController.cs b/Assets/Mirror/Core/TickManager/NetworkPhysicsController.cs new file mode 100644 index 000000000..c6806fa79 --- /dev/null +++ b/Assets/Mirror/Core/TickManager/NetworkPhysicsController.cs @@ -0,0 +1,96 @@ +using UnityEngine; +using System; + +namespace Mirror{ + [DefaultExecutionOrder(-10)] + [DisallowMultipleComponent] + [AddComponentMenu("Network/Network Physics Controller")] + public class NetworkPhysicsController : MonoBehaviour{ + /// + /// Callback action to handle tick-forwarding logic. + /// Allows external classes to define custom behavior when the tick advances. + /// + public Action TickForwardCallback; + + private static int _reconcileStartTick = 0; + + /// + /// Advances the game state by a specified number of ticks. + /// Invokes the TickForwardCallback to allow external classes to handle tick-forwarding logic. + /// Typically called with `deltaTicks` = 1 from RunSimulate. + /// + /// The number of ticks to forward. + public virtual void TickForward(int deltaTicks) { + TickForwardCallback?.Invoke(deltaTicks); + } + + /// + /// Executes a single physics simulation step for the given delta time. + /// Uses Unity's Physics.Simulate to perform the physics tick. + /// Typically called with Time.fixedDeltaTime. + /// + /// The time interval to simulate physics for. + public virtual void PhysicsTick(float deltaTime) { + Physics.Simulate(deltaTime); // Using Unity's built-in physics engine. + } + + /// + /// Runs the simulation for the specified number of delta ticks. + /// This method performs multiple steps of entity updates and physics ticks + /// to bring the simulation in sync with the latest tick count. + /// + /// The number of ticks to simulate forward. + public void RunSimulate(int deltaTicks) { + var deltaTime = Time.fixedDeltaTime; + for (var step = 0; step < deltaTicks; step++) { + TickForward(1); + NetworkPhysicsEntity.RunBeforeNetworkUpdates(1, deltaTime); + NetworkPhysicsEntity.RunNetworkUpdates(1, deltaTime); + PhysicsTick(deltaTime); + NetworkPhysicsEntity.RunAfterNetworkUpdates(1, deltaTime); + } + } + + /// + /// Runs the simulation for the specified number of delta ticks as a single batch. + /// This method performs a single set of entity updates and a single physics tick + /// scaled to account for the total number of ticks, useful for batching simulations. + /// + /// The number of ticks to simulate forward in one batch. + public void RunBatchSimulate(int deltaTicks) { + var deltaTime = Time.fixedDeltaTime * deltaTicks; + TickForward(deltaTicks); + NetworkPhysicsEntity.RunBeforeNetworkUpdates(deltaTicks, deltaTime); + NetworkPhysicsEntity.RunNetworkUpdates(deltaTicks, deltaTime); + PhysicsTick(deltaTime); // Uses scaled deltaTime for batch processing + NetworkPhysicsEntity.RunAfterNetworkUpdates(deltaTicks, deltaTime); + } + + /// + /// Requests the reconciliation process to start from a specific tick. + /// Stores the earliest requested tick before reconciliation is executed. + /// + /// The tick from which to start reconciliation. + public static void RequestReconcileFromTick(int reconcileStartTick) { + if (_reconcileStartTick > reconcileStartTick || _reconcileStartTick == 0) { + _reconcileStartTick = reconcileStartTick; // the +1 is important to include the faulty tick + } + } + + /// + /// Retrieves the tick number from which reconciliation should start. + /// + /// The tick number from which to start reconciliation. + public int GetReconcileStartTick() { + return _reconcileStartTick; + } + + /// + /// Resets the reconciliation counter, marking the reconciliation process as complete. + /// Sets _ticksToReconcile to 0, indicating no further reconciliation is required. + /// + public void ResetReconcile() { + _reconcileStartTick = 0; + } + } +} \ No newline at end of file diff --git a/Assets/Mirror/Core/TickManager/NetworkPhysicsController.cs.meta b/Assets/Mirror/Core/TickManager/NetworkPhysicsController.cs.meta new file mode 100644 index 000000000..9e6d11c0c --- /dev/null +++ b/Assets/Mirror/Core/TickManager/NetworkPhysicsController.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: b36faa4b9565404b95ba6539a10fc47f +timeCreated: 1730317284 \ No newline at end of file diff --git a/Assets/Mirror/Core/TickManager/NetworkPhysicsEntity.cs b/Assets/Mirror/Core/TickManager/NetworkPhysicsEntity.cs new file mode 100644 index 000000000..bef492b3f --- /dev/null +++ b/Assets/Mirror/Core/TickManager/NetworkPhysicsEntity.cs @@ -0,0 +1,93 @@ +using System.Collections.Generic; + +namespace Mirror{ + /// + /// Interface representing a network item that requires updates at various stages of the network tick cycle. + /// Each method in this interface is intended to handle specific stages of the update process. + /// + public interface INetworkedItem{ + /// + /// Called before the main network update, allowing the item to perform any necessary preparation or pre-update logic. + /// + /// The number of ticks since the last update. + /// The time elapsed since the last update in seconds. + void BeforeNetworkUpdate(int deltaTicks, float deltaTime); + + /// + /// Called during the main network update, allowing the item to handle core updates related to network state, physics, or entity positioning. + /// + /// The number of ticks since the last update. + /// The time elapsed since the last update in seconds. + void OnNetworkUpdate(int deltaTicks, float deltaTime); + + /// + /// Called after the main network update, allowing the item to perform any necessary cleanup or post-update logic. + /// + /// The number of ticks since the last update. + /// The time elapsed since the last update in seconds. + void AfterNetworkUpdate(int deltaTicks, float deltaTime); + } + + /// + /// Manages network update sequences for entities requiring tick-based adjustments. + /// + public class NetworkPhysicsEntity{ + /// Stores items requiring updates on each tick, as a list of tuples with priority and item. + private static readonly List<(int priority, INetworkedItem item)> NetworkItems = new List<(int, INetworkedItem)>(); + + /// Adds a network entity to the collection for updates and sorts by priority. + /// The network item implementing that requires tick updates. + /// The priority for the entity, with lower numbers indicating higher priority. + public static void AddNetworkEntity(INetworkedItem item, int priority = 0) { + NetworkItems.Add((priority, item)); + NetworkItems.Sort((x, y) => y.priority.CompareTo(x.priority)); + // Fortunately, List.Sort() in C# uses a stable sorting algorithm so same priority remains in the same order and new items are added to the end + // [2-a, 1-a, 1-b, 0-a, 0-b, 0-c] + [1-c] => [2-a, 1-a, 1-b, 1-c, 0-a, 0-b, 0-c] + } + + /// Removes a network entity from the collection based on the item reference only. + /// The network item to remove. + public static void RemoveNetworkEntity(INetworkedItem item) { + NetworkItems.RemoveAll(entry => entry.item.Equals(item)); + } + + + /// + /// Runs the BeforeNetworkUpdate method on each network item in priority order. + /// This method is intended to perform any necessary setup or pre-update logic before the main network updates are processed. + /// + /// The number of ticks since the last update. + /// The time elapsed since the last update in seconds. + public static void RunBeforeNetworkUpdates(int deltaTicks, float deltaTime) { + foreach (var (priority, item) in NetworkItems) { + item.BeforeNetworkUpdate(deltaTicks, deltaTime); + } + } + + + /// + /// Runs the OnNetworkUpdate method on each network item in priority order. + /// This method executes the main network update logic for each item, handling any core updates needed for the network state or entity positions. + /// + /// The number of ticks since the last update. + /// The time elapsed since the last update in seconds. + public static void RunNetworkUpdates(int deltaTicks, float deltaTime) { + foreach (var (priority, item) in NetworkItems) { + item.OnNetworkUpdate(deltaTicks, deltaTime); + } + } + + + /// + /// Runs the AfterNetworkUpdate method on each network item in priority order. + /// This method is intended for any necessary cleanup or post-update logic following the main network updates. + /// + /// The number of ticks since the last update. + /// The time elapsed since the last update in seconds. + public static void RunAfterNetworkUpdates(int deltaTicks, float deltaTime) { + foreach (var (priority, item) in NetworkItems) { + item.AfterNetworkUpdate(deltaTicks, deltaTime); + } + } + } +} \ No newline at end of file diff --git a/Assets/Mirror/Core/TickManager/NetworkPhysicsEntity.cs.meta b/Assets/Mirror/Core/TickManager/NetworkPhysicsEntity.cs.meta new file mode 100644 index 000000000..7d5484a56 --- /dev/null +++ b/Assets/Mirror/Core/TickManager/NetworkPhysicsEntity.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 8b08c8ad7b0840fdb96784accdc66787 +timeCreated: 1730400815 \ No newline at end of file diff --git a/Assets/Mirror/Core/TickManager/NetworkTick.cs b/Assets/Mirror/Core/TickManager/NetworkTick.cs new file mode 100644 index 000000000..f8d9722c4 --- /dev/null +++ b/Assets/Mirror/Core/TickManager/NetworkTick.cs @@ -0,0 +1,245 @@ +using System; + +namespace Mirror{ + public class NetworkTick{ + /*** Private Definitions ***/ + + #region Private Definitions + + // Current state flags + private static bool _isServer = false; + private static bool _isSynchronizing = false; + private static bool _isSynchronized = false; + private static bool _isReconciling = false; + + // Internal tick counters + private static int _clientTick = 0; + private static int _serverTick = 0; + private static int _absoluteClientTick = 0; + private static int _absoluteServerTick = 0; + + // Packet loss compensation ticks + private static int _clientToServerPacketLossCompensation = 0; + private static int _serverToClientPacketLossCompensation = 0; + + #endregion + + /*** CLIENT ONLY METHODS ***/ + + #region CLIENT ONLY METHODS + + /// Gets the client-to-server packet loss compensation ticks. Client-only: This cant be accessed on the server. + /// Thrown if accessed on the server. + public static int ClientToServerPacketLossCompensation { + get { + if (_isServer) throw new InvalidOperationException("ClientToServerPacketLossCompensation is client-only and cannot be accessed on the server."); + return _clientToServerPacketLossCompensation; + } + } + + /// Gets the server-to-client packet loss compensation ticks. Client-only: This cant be accessed on the server. + /// Thrown if accessed on the server. + public static int ServerToClientPacketLossCompensation { + get { + if (_isServer) throw new InvalidOperationException("ServerToClientPacketLossCompensation is client-only and cannot be accessed on the server."); + return _serverToClientPacketLossCompensation; + } + } + + /// + /// Sets the client-to-server packet loss compensation ticks, allowing the client to define the number of compensation ticks based on detected packet loss. + /// Client-only: This method should not be called on the server. + /// + /// The number of compensation ticks to set. + public void SetClientToServerPacketLossCompensation(int compensationTicks) { + if (_isServer) throw new InvalidOperationException("SetClientToServerPacketLossCompensation is client-only and cannot be accessed on the server."); + _clientToServerPacketLossCompensation = compensationTicks; + } + + /// + /// Sets the server-to-client packet loss compensation ticks, allowing the client to define the number of compensation ticks based on detected packet loss. + /// Client-only: This method should not be called on the server. + /// + /// The number of compensation ticks to set. + public void SetServerToClientPacketLossCompensation(int compensationTicks) { + if (_isServer) throw new InvalidOperationException("SetServerToClientPacketLossCompensation is client-only and cannot be accessed on the server."); + _serverToClientPacketLossCompensation = compensationTicks; + } + + #endregion + + /*** Static Status Getters ***/ + + #region Static Status Getters + + /// Gets a value indicating whether the current instance is a server. + public static bool IsServer => _isServer; + + /// Gets a value indicating whether the client is synchronizing with the server. + public static bool IsSynchronizing => _isSynchronizing; + + /// Gets a value indicating whether the client is synchronized with the server. + public static bool IsSynchronized => _isSynchronized; + + /// Gets a value indicating whether the system is reconciling ticks. + public static bool IsReconciling => _isReconciling; + + #endregion + + /*** Static Tick Getters ***/ + + #region Static Tick Getters + + /// Gets the current tick count based on whether the instance is a server or client. + public static int CurrentTick => _isServer ? _serverTick : _clientTick; + + /// Gets the current absolute tick count based on whether the instance is a server or client. + public static int CurrentAbsoluteTick => _isServer ? _absoluteServerTick : _absoluteClientTick; + + /// Gets the client tick count. + public static int ClientTick => _clientTick; + + /// Gets the client absolute tick count. + public static int ClientAbsoluteTick => _absoluteClientTick; + + /// Gets the server tick count. + public static int ServerTick => _serverTick; + + /// Gets the server tick count. + public static int ServerAbsoluteTick => _absoluteServerTick; + + #endregion + + /*** Instance Getters ***/ + + #region Instance Getters + + /// Checks if the client is in the process of synchronizing with the server. + public bool GetIsSynchronizing() => _isSynchronizing; + + /// Checks if the client is currently synchronized with the server. + public bool GetIsSynchronized() => _isSynchronized; + + /// Gets the current client tick value. + public int GetClientTick() => _clientTick; + + /// Gets the absolute tick value for the client. + public int GetClientAbsoluteTick() => _absoluteClientTick; + + /// Gets the current client tick value. + public int GetServerTick() => _serverTick; + + /// Gets the absolute tick value for the server. + public int GetServerAbsoluteTick() => _absoluteServerTick; + + #endregion + + /*** Instance Status Setters ***/ + + #region Instance Status Setters + + /// Sets the server status of the current instance. + public void SetIsServer(bool isServer) => _isServer = isServer; + + /// Sets the synchronization status between client and server. + public void SetSynchronized(bool isSynchronized) => _isSynchronized = isSynchronized; + + /// Sets the synchronization status between client and server. + public void SetSynchronizing(bool isSynchronizing) => _isSynchronizing = isSynchronizing; + + /// Sets the reconciling status. + public void SetReconciling(bool reconciling) => _isReconciling = reconciling; + + #endregion + + /*** Instance Tick Setters ***/ + + #region Instance Tick Setters + + /// Sets a new tick value for the client. + public void SetClientTick(int newTick) => _clientTick = newTick; + + /// Sets a new absolute tick value for the client. + public void SetClientAbsoluteTick(int newAbsoluteTick) => _absoluteClientTick = newAbsoluteTick; + + /// Sets a new tick value for the server. + public void SetServerTick(int newTick) => _serverTick = newTick; + + /// Sets a new absolute tick value for the server. + public void SetServerAbsoluteTick(int newAbsoluteTick) => _absoluteServerTick = newAbsoluteTick; + + #endregion + + /*** Instance Tick Modifiers ***/ + + #region Instance Tick Modifiers + + /// Increments the client tick by a specified amount, wrapping to 11 bits. + public void IncrementClientTick(int increment) => _clientTick = (_clientTick + increment) & 0b11111111111; + + /// Increments the client's absolute tick by a specified amount. + public void IncrementClientAbsoluteTick(int increment) => _absoluteClientTick += increment; + + /// Increments the server tick by a specified amount, wrapping to 11 bits. + public void IncrementServerTick(int increment) => _serverTick = (_serverTick + increment) & 0b11111111111; + + /// Increments the server's absolute tick by a specified amount. + public void IncrementServerAbsoluteTick(int increment) => _absoluteServerTick += increment; + + #endregion + + /*** Useful Bitwise Functions ***/ + + #region Useful Bitwise Functions + + /// + /// Combines a fiveBits and tick counter into a single value. This is used to optimize network traffic by packing two values into one. + /// + /// The fiveBits value (should be within 5 bits). + /// The tick counter value (should be within 11 bits). + /// A combined containing both the fiveBits and tick counter. + public static ushort CombineBitsTick(int fiveBits, int tick) { + // Ensure the fiveBits is within 5 bits and tickCounter within 11 bits + fiveBits &= 0x1F; // Mask to keep only the lowest 5 bits + tick &= 0x7FF; // Mask to keep only the lowest 11 bits + return (ushort)((fiveBits << 11) | tick); // Shift fiveBits left by 11 bits and combine with tickCounter + } + + /// + /// Splits a combined fiveBits and tick counter value back into its individual components. + /// + /// The combined value. + /// A tuple containing the fiveBits and tick counter. + public static (int fiveBits, int tickCounter) SplitCombinedBitsTick(ushort combined) { + var fiveBits = (combined >> 11) & 0x1F; // Extract the 5-bit fiveBits by shifting right and masking + var tickCounter = combined & 0x7FF; // Extract the 11-bit tick counter by masking the lower 11 bits + return (fiveBits, tickCounter); + } + + /// + /// Calculates the minimal difference between two ticks, accounting for wraparound (ex: SubtractTicks(2040, 2) => 10). + /// This helps in correctly comparing tick counts in a circular tick range. + /// + /// The first tick value. + /// The second tick value. + /// The minimal difference between the two ticks. + public static int SubtractTicks(int tickOne, int tickTwo) { + var delta = (tickOne - tickTwo + 2048) % 2048; + if (delta >= 1024) delta -= 2048; + return delta; + } + + /// + /// Increments a tick value by a specified amount, wrapping around within a 2048 tick range. + /// This function ensures that tick values stay within a defined range by handling wraparound correctly. + /// + /// The initial tick value. + /// The amount to increment the tick by. + /// The incremented tick value, wrapped within the 2048 range. + public static int IncrementTick(int tick, int increment) { + return (tick + increment) & 0b11111111111; + } + + #endregion + } +} \ No newline at end of file diff --git a/Assets/Mirror/Core/TickManager/NetworkTick.cs.meta b/Assets/Mirror/Core/TickManager/NetworkTick.cs.meta new file mode 100644 index 000000000..402d11bc2 --- /dev/null +++ b/Assets/Mirror/Core/TickManager/NetworkTick.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e4b45d54032d4018af1450222e8e7eef +timeCreated: 1730317658 \ No newline at end of file diff --git a/Assets/Mirror/Core/TickManager/NetworkTickManager.cs b/Assets/Mirror/Core/TickManager/NetworkTickManager.cs new file mode 100644 index 000000000..ddf53bda8 --- /dev/null +++ b/Assets/Mirror/Core/TickManager/NetworkTickManager.cs @@ -0,0 +1,698 @@ +using System; +using UnityEngine; +using System.Collections.Generic; + +namespace Mirror{ + /// + /// Represents data sent from the client to track its current state. + /// + public struct ClientData{ + public int ClientNonce; + public int ClientTick; + public PacketLossTracker RemoteClientLoss; + public int SentPackets; + } + + /// + /// Represents a server response that includes synchronization data for the client tick and packet loss information. + /// + public struct ServerPong{ + public ushort ServerTickWithNonce; // 5 bits for nonce + 11 bits for the tick + public ushort ClientTickWithLoss; // 5 bits for packet loss value + 11 bits for the tick + } + + /// + /// Represents a server response with an absolute tick count to help the client synchronize with the server more accurately. + /// + public struct AbsoluteServerPong{ + public ushort ServerTickWithNonce; // 5 bits for nonce + 11 bits for the tick + public ushort ClientTickWithLoss; // 5 bits for packet loss value + 11 bits for the tick + public int AbsoluteServerTick; // Absolute server tick count to sync the client + } + + /// + /// Represents a client request sent to the server, including the client tick and a unique nonce for tracking. + /// + public struct ClientPing{ + public ushort ClientTickWithNonce; // 5 bits for nonce + 11 bits for the tick + } + + // Ensure we run first and that there is only one instance present. + [DefaultExecutionOrder(-10)] + [DisallowMultipleComponent] + [AddComponentMenu("Network/Network Tick Manager")] + public class NetworkTickManager : NetworkBehaviour{ + /*** Public Definitions ***/ + + #region Public Definitions + + [Header("Client Prediction Settings")] + [Min(1)] + [Tooltip("The minimum tick difference required between the client and server when the client's tick data arrives on the server." + + "If the difference is less than this value, the client must adjust to stay synchronized.")] + public int minClientRunaway = 1; + + [Min(1)] + [Tooltip( + "The allowable tick difference range added on top of the minimum client runaway." + + "This defines how much further ahead the client can be from the server beyond the minimum before needing adjustment.")] + public int acceptableClientRunawayRange = 1; + + [Header("Server State Replay Settings")] + [Min(1)] + [Tooltip( + "The minimum tick difference required between the server and client when the client replays server states." + + "If the difference is less than this value, the client must adjust the server replay tick.")] + public int minServerRunaway = 0; + + [Min(1)] + [Tooltip("The allowable tick difference range added on top of the minimum server runaway." + + "Defines how much further ahead the received server state can be from the server replay beyond the minimum before needing adjustment.")] + public int acceptableServerRunawayRange = 1; + + [Header("Deviation measurements and timings:")] [Tooltip("The amount of samples to use for packet loss calculation")] [Min(25)] + public int packetLossSamples = 100; + + [Min(1)] + [Tooltip("The duration in seconds over which deviation data is collected before adjusting the client or server states to acceptable runaway values.")] + public int calculationSeconds = 2; + + [Min(10)] + [Tooltip("The longer duration in seconds over which deviation data is collected before adjusting the client or server states to minimum runaway values.")] + public int longCalculationSeconds = 30; + + [Tooltip("Tick compensation when packet loss is present: \ncompensation = loss percent / factor")] [Range(1, 30)] + public int packetLossCompensationFactor = 10; + + [Header("Absolute tick sync settings:")] + [Min(1)] + [Tooltip("How often to verify absolute tick between the server and the client (this can happen if desync is larger than 1024 ticks)")] + public int absoluteTickSyncIntervalSeconds = 10; + + [Min(10)] [Tooltip("How many ticks to send the absolute server tick for the clients to sync. Cant be 1 because this packet can get lost in transit.")] + public int absoluteTickSyncHandshakeTicks = 10; + + [Header("Physics Controller:")] public NetworkPhysicsController physicsController; + + #endregion + + /*** Private Definitions ***/ + + #region Private Definitions + + // Local clients list on the server for efficient communication + private readonly Dictionary _clients = new Dictionary(); + + // Instance of NetworkTick used for managing and changing the tick counters + private readonly NetworkTick _networkTick = new NetworkTick(); + + // Actual runaway metrics - adjusted based on network conditions ( aka packet losses ) + private int _internalClientRunaway = 0; + private int _internalServerRunaway = 0; + private int _internalMinClientRunaway = 0; + + private int _internalMinServerRunaway = 0; + + // Running minimum counters used to avoid adjusting too fast and oscillating back and forth + private RunningMin _clientRunningMin; + private RunningMin _serverRunningMin; + private RunningMin _clientLongRunningMin; + private RunningMin _serverLongRunningMin; + + // Client side packet loss tracker for packets from the server + private PacketLossTracker _receivePacketLoss; + + // Server and Client last nonce values - used to dettect packet losses + private int _serverNonce = 0; + private int _clientNonce = 0; + + // Client side last client tick received from the server to avoid lengthy adjustments + private int _lastRemoteClientTick = 0; + + // Absolute tick synch status + private bool _isAbsoluteTickSynced = false; + + // Modulus ( % modulus == 0 -> send absolute tick to clients ) based on absoluteTickSyncIntervalSeconds and tick rate + private int _absoluteServerTickModulus = 0; + + // Adjustment flags and variables used to ensure only one adjustment is taking place at a time. + private bool _capturedOffsets = false; + private bool _isAdjusting = false; + private int _adjustmentEndTick = 0; + + // Make sure NetworkPhysicsController is attached to the tick manager + protected new virtual void OnValidate() { + base.OnValidate(); + physicsController = GetComponent(); + if (physicsController == null) { + throw new ArgumentException("Missing NetworkPhysicsController! please attach it to this entity"); + } + } + + public static NetworkTickManager singleton; + + void Awake() { + if (singleton != null && singleton != this) { + Destroy(gameObject); + return; + } + singleton = this; + } + + #endregion + + /*** Server Startup and Setup ***/ + + #region Server Startup and Setup + + /// + /// Called when the server starts. Registers callbacks for client connection and disconnection events, + /// allowing the server to handle these events appropriately. + /// + [Server] + public override void OnStartServer() { + // Register callback for when clients connect/disconnect + NetworkServer.OnConnectedEvent += OnClientConnected; + NetworkServer.OnDisconnectedEvent += OnClientDisconnected; + base.OnStartServer(); + } + + /// + /// Called when the server stops. Unregisters callbacks for client connection and disconnection events + /// to ensure cleanup and avoid unintended event handling after the server has stopped. + /// + [Server] + public override void OnStopServer() { + // Unregister callbacks when server stops + NetworkServer.OnConnectedEvent -= OnClientConnected; + NetworkServer.OnDisconnectedEvent -= OnClientDisconnected; + base.OnStopServer(); + } + + /// + /// Called when a client connects to the server. Initializes a new entry in the _clients dictionary for the connected client, + /// storing initial client data such as nonce, tick count, packet loss tracker, and sent packets. + /// + /// The network connection for the connected client. + [Server] + private void OnClientConnected(NetworkConnection conn) => _clients[conn] = new ClientData() + { ClientNonce = 0, ClientTick = 0, RemoteClientLoss = new PacketLossTracker(packetLossSamples), SentPackets = 0 }; + + /// + /// Called when a client disconnects from the server. Removes the client entry from the _clients dictionary + /// to free up resources and maintain an accurate list of active clients. + /// + /// The network connection for the disconnected client. + [Server] + private void OnClientDisconnected(NetworkConnection conn) => _clients.Remove(conn); + + #endregion + + /*** Network Tick Start and Tick Initialization ***/ + + #region Network Tick Start and Tick Initialization + + /// Initializes server-specific settings when the server starts. + [Server] + private void StartServer() { + _absoluteServerTickModulus = Mathf.RoundToInt(absoluteTickSyncIntervalSeconds / Time.fixedDeltaTime); + _networkTick.SetSynchronized(true); + _networkTick.SetSynchronizing(false); + } + + /// Initializes client-specific settings when the client starts. + [Client] + private void StartClient() { + _internalMinClientRunaway = minClientRunaway; + _internalMinServerRunaway = minServerRunaway; + _internalClientRunaway = acceptableClientRunawayRange; + _internalServerRunaway = acceptableServerRunawayRange; + _clientRunningMin = new RunningMin(Mathf.RoundToInt(calculationSeconds / Time.fixedDeltaTime)); + _serverRunningMin = new RunningMin(Mathf.RoundToInt(calculationSeconds / Time.fixedDeltaTime)); + _clientLongRunningMin = new RunningMin(Mathf.RoundToInt(longCalculationSeconds / Time.fixedDeltaTime)); + _serverLongRunningMin = new RunningMin(Mathf.RoundToInt(longCalculationSeconds / Time.fixedDeltaTime)); + _receivePacketLoss = new PacketLossTracker(packetLossSamples); + } + + /// Advances the client's tick counters by the specified number of ticks. + /// Number of ticks to advance. + [Client] + private void OnTickForwardClient(int deltaTicks) { + _networkTick.IncrementClientTick(deltaTicks); + _networkTick.IncrementClientAbsoluteTick(deltaTicks); + _networkTick.IncrementServerTick(deltaTicks); + _networkTick.IncrementServerAbsoluteTick(deltaTicks); + } + + /// Advances the server's tick counters by the specified number of ticks. + /// Number of ticks to advance. + [Server] + private void OnTickForwardServer(int deltaTicks) { + _networkTick.IncrementServerTick(deltaTicks); + _networkTick.IncrementServerAbsoluteTick(deltaTicks); + } + + /// Initializes the tick manager and sets up the physics controller based on whether it is running on the server or client. + private void Start() { + _networkTick.SetIsServer(isServer); + if (isServer) + StartServer(); + else + StartClient(); + + // Allow the physics to position all the items in the scene - we run 1 tick for this then wait for sync + physicsController.TickForwardCallback = isServer ? OnTickForwardServer : OnTickForwardClient; + physicsController.RunSimulate(1); + } + + #endregion + + /*** Packet Loss Calculations ***/ + + #region Packet Loss Calculations + + /// Updates the client's packet loss compensation values based on the server's nonce and reported packet loss. + /// The nonce received from the server to detect packet loss. + /// The packet loss percentage reported by the server. + [Client] + private void UpdatePacketLossCompensation(int serverNonce, int sendPacketLoss) { + _receivePacketLoss.AddPacket(_serverNonce > 0 && NextNonce(_serverNonce) != serverNonce); + _serverNonce = serverNonce; + + // Calculate adjustments based on server and client packet loss factors + var sendCompensationTicks = CalculateTickCompensation(sendPacketLoss); + var receiveCompensationTicks = CalculateTickCompensation(_receivePacketLoss.Loss); + + // Update NetworkTick with the compensation values for users to integrate compensations if needed + _networkTick.SetClientToServerPacketLossCompensation(sendCompensationTicks); + _networkTick.SetServerToClientPacketLossCompensation(receiveCompensationTicks); + + // Adjust internal tick min and max runaway values to compensate for packet losses + _internalMinClientRunaway = minClientRunaway + sendCompensationTicks; + _internalClientRunaway = acceptableClientRunawayRange + _internalMinClientRunaway + sendCompensationTicks; + _internalMinServerRunaway = minServerRunaway + receiveCompensationTicks; + _internalServerRunaway = acceptableServerRunawayRange + _internalMinServerRunaway + receiveCompensationTicks; + } + + #endregion + + /*** Synchronization functions ***/ + + #region Synchronization functions + + /// Sets or adjusts the client's absolute server tick values to maintain synchronization with the server. + /// The absolute tick count provided by the server. + /// The server's current tick value. + [Client] + private void SetAbsoluteTicks(int absoluteServerTick, int serverTick) { + if (!_isAbsoluteTickSynced) { + _isAbsoluteTickSynced = true; + _networkTick.SetServerTick(serverTick); + _networkTick.SetServerAbsoluteTick(absoluteServerTick); + return; + } + + var proposedServerAbsoluteTick = absoluteServerTick - NetworkTick.SubtractTicks(serverTick, _networkTick.GetServerTick()); + var absoluteTickDiff = proposedServerAbsoluteTick - _networkTick.GetServerAbsoluteTick(); + if (absoluteTickDiff != 0) { + _networkTick.IncrementServerAbsoluteTick(absoluteTickDiff); + _networkTick.IncrementClientAbsoluteTick(absoluteTickDiff); + } + } + + /// Initiates the synchronization process by aligning the client's and server's ticks. + /// The current tick value from the server. + /// The client's tick value as received by the server. + [Client] + private void SynchronizeStart(int serverTick, int clientTick) { + var oldServerTick = _networkTick.GetServerTick(); + _networkTick.IncrementServerTick(-_internalServerRunaway); + var newServerTick = _networkTick.GetServerTick(); + _networkTick.IncrementServerAbsoluteTick(NetworkTick.SubtractTicks(newServerTick, oldServerTick)); + _networkTick.SetClientTick(NetworkTick.IncrementTick(serverTick, + NetworkTick.SubtractTicks(_networkTick.GetClientTick(), clientTick) + _internalClientRunaway)); + _networkTick.SetClientAbsoluteTick(_networkTick.GetServerAbsoluteTick() + NetworkTick.SubtractTicks(_networkTick.GetClientTick(), newServerTick)); + _networkTick.SetSynchronizing(true); + SetAdjusting(_networkTick.GetClientTick()); + } + + /// Synchronizes the client's and server's ticks by applying necessary adjustments. + [Client] + private void Synchronize() { + var serverTickAdjustment = GetServerAdjustment(true); + var clientTickAdjustment = GetServerAdjustment(true); + // Apply adjustments on current tick counters + _networkTick.IncrementClientTick(clientTickAdjustment); + _networkTick.IncrementClientAbsoluteTick(clientTickAdjustment); + _networkTick.IncrementServerTick(-serverTickAdjustment); + _networkTick.IncrementServerAbsoluteTick(-serverTickAdjustment); + // Set status to synchronized + _networkTick.SetSynchronized(true); + _networkTick.SetSynchronizing(false); + + SetAdjusting(_networkTick.GetClientTick()); + } + + #endregion + + /*** Handling Message from the Server ***/ + + #region Handling Message from the Server + + /// + /// Handles the server's pong response to maintain and adjust tick synchronization between the client and server. + /// This method updates packet loss compensation, ensures packets are processed in order, + /// and manages the client's synchronization state based on the received ticks. + /// Depending on whether the client is synchronized, in the process of synchronizing, or not yet synchronized, + /// it calculates necessary offsets and adjusts ticks to align with the server. + /// + /// Nonce value from the server for packet loss detection. + /// Current tick count from the server. + /// Packet loss percentage reported by the server. + /// Client's tick count as received by the server. + [Client] + private void HandleServerPong(int serverNonce, int serverTick, int sendLoss, int clientTick) { + _capturedOffsets = false; + UpdatePacketLossCompensation(serverNonce, sendLoss); + + // We want to avoid handling the same client tick from the server to improve accuracy otherwise we risk of repeating adjustments + if (!IsValidPacket(clientTick)) return; + + if (_networkTick.GetIsSynchronized()) { + if (_isAdjusting && NetworkTick.SubtractTicks(clientTick, _adjustmentEndTick) > 0) { + _isAdjusting = false; + ResetRunningMins(); + } + + // Calculate and deviations using the server info + CalculateOffsets(serverTick, clientTick); + return; + } + + if (_networkTick.GetIsSynchronizing()) { + // Since client tick is not yet synchronized and the server sends old tick we need to compare to server tick rather than client tick + if (NetworkTick.SubtractTicks(serverTick, _adjustmentEndTick) > 0) { + // We are not worried about being too much ahead at this point, we only care about being behind the execution on client or server + // So we calculate the minimum values and run the adjustment again + CalculateOffsets(serverTick, clientTick); + Synchronize(); + ResetRunningMins(); + } + + return; + } + + // Wait until we receive positive tick from server before starting the initial 2 step sync + if (clientTick > 0) { + SynchronizeStart(serverTick, clientTick); + ResetRunningMins(); + } + } + + #endregion + + /*** Tick Adjustment Calculations ***/ + + #region Tick Adjustment Calculations + + /// Calculates the tick offsets between client and server, updating running minimums for synchronization adjustments. + /// The current tick value from the server. + /// The client's tick value as received by the server. + [Client] + private void CalculateOffsets(int serverTick, int clientTick) { + _capturedOffsets = true; + var clientTickOffset = NetworkTick.SubtractTicks(clientTick, serverTick); + var serverTickOffset = NetworkTick.SubtractTicks(serverTick, _networkTick.GetServerTick()); + _clientRunningMin.Add(clientTickOffset); + _clientLongRunningMin.Add(clientTickOffset); + _serverRunningMin.Add(serverTickOffset); + _serverLongRunningMin.Add(serverTickOffset); + } + + /// Determines the necessary tick adjustment for the client to maintain synchronization with the server. + /// If true, applies the full adjustment needed; otherwise, applies a minimal step. + /// The number of ticks to adjust the client's tick by. + [Client] + private int GetClientAdjustment(bool absolute = false) { + // If the server received client predicted tick bellow min thresh hold we need to adjust ourselves forward otherwise risking server not receiving inputs + if (_clientRunningMin.CurrentMin < _internalMinClientRunaway) + return -(_clientRunningMin.CurrentMin - _internalMinClientRunaway); + + // If the server received client predicted tick is too far into the future we want to slow down the client to reduce perceived latency + if (_clientRunningMin.IsFull && _clientRunningMin.CurrentMin > _internalClientRunaway) + return absolute ? -_clientRunningMin.CurrentMin : -1; + + // If the server received client predicted tick is stable but above the min requirement we can slow down the client to reduce perceived latency + if (_clientLongRunningMin.IsFull && _clientLongRunningMin.CurrentMin > _internalMinClientRunaway) + return absolute ? -_clientLongRunningMin.CurrentMin : -1; + return 0; + } + + /// Determines the necessary tick adjustment for the server to maintain synchronization with the client. + /// If true, applies the full adjustment needed; otherwise, applies a minimal step. + /// The number of ticks to adjust the server's tick by. + [Client] + private int GetServerAdjustment(bool absolute = false) { + // If the received server tick is behind the expected minimum we need to adjust our tick backwards + if (_serverRunningMin.CurrentMin < _internalMinServerRunaway) + return _internalMinServerRunaway - _serverRunningMin.CurrentMin; + + // If the received server tick is too far forward we need to reduce it to reduce latency + if (_serverRunningMin.IsFull && _serverRunningMin.CurrentMin > _internalServerRunaway) + return absolute ? -_serverRunningMin.CurrentMin : -1; + + // If the received server tick is more than the minimum for an extended period of time its safe to reduce it to reduce latency + if (_serverLongRunningMin.IsFull && _serverLongRunningMin.CurrentMin > _internalMinServerRunaway) + return absolute ? -_serverLongRunningMin.CurrentMin : -1; + return 0; + } + + /// Calculates adjusted tick values for synchronization, applying any necessary client or server tick adjustments. + /// The base number of ticks to advance. + /// The adjusted number of ticks to use for simulation. + [Client] + private int GetAdjustedTicks(int deltaTicks) { + int clientAdjustment = GetClientAdjustment(); + int serverAdjustment = GetServerAdjustment(); + if (serverAdjustment != 0) { + _networkTick.IncrementServerTick(-serverAdjustment); + _networkTick.IncrementServerAbsoluteTick(-serverAdjustment); + } + + // If client or server are adjusting we need to wait for confirmation to avoid oscillating adjusments + if (clientAdjustment != 0 || serverAdjustment != 0) + SetAdjusting(NetworkTick.IncrementTick(_networkTick.GetClientTick(), deltaTicks + clientAdjustment)); + + return deltaTicks + clientAdjustment; + } + + #endregion + + /*** Tick Simulation Functions ***/ + + #region Tick Simulation Functions + + /// Checks for any required reconciliation due to state discrepancies and resimulates physics accordingly. + /// The number of ticks advanced since the last update. + [Client] + private void CheckReconcile(int deltaTicks) { + var reconcileStartTick = physicsController.GetReconcileStartTick(); + if (reconcileStartTick > 0) { + var reconcileTicks = _networkTick.GetClientTick() - reconcileStartTick + deltaTicks; + OnTickForwardClient(-reconcileTicks); + _networkTick.SetReconciling(true); + physicsController.RunSimulate(reconcileTicks); + physicsController.ResetReconcile(); + _networkTick.SetReconciling(false); + } + } + + /// Updates the client's state each tick, handling synchronization, reconciliation, and physics simulation. + /// The number of ticks to advance. + [Client] + private void UpdateClient(int deltaTicks) { + // Check if need reconciling - if yes reconcile before executing the next ticks + CheckReconcile(deltaTicks); + + // Adjust the delta ticks if not waiting for adjustment confirmation + var adjustedTicks = _capturedOffsets && !_isAdjusting ? GetAdjustedTicks(deltaTicks) : deltaTicks; + + // fix discrepancies cause by client tick adjustment + _networkTick.IncrementServerTick(deltaTicks - adjustedTicks); + _networkTick.IncrementServerAbsoluteTick(deltaTicks - adjustedTicks); + + // Simulate ticks or skip if pause was requested + if (adjustedTicks > 0) + physicsController.RunSimulate(adjustedTicks); + } + + /// Handles physics simulation and synchronization updates on both the server and client each fixed frame. + public void FixedUpdate() { + // Handle FixedUpdate for deltaTicks + if (isServer) { + physicsController.RunSimulate(1); + SendUpdatesToAllClients(); + } + else { + // Keep pushing the tick counters forward until the client is synced with the server + if (!_networkTick.GetIsSynchronized()) + OnTickForwardClient(1); + else + UpdateClient(1); + ClientSendPing(); + } + } + + #endregion + + /*** Communication Functions ***/ + + #region Communication Functions + + /// Sends a ping to the server with the client's current tick and a nonce for packet loss detection. + [Client] + private void ClientSendPing() { + // Increase nonce by 1 but keep withing 5 bits of data [0-31] + _clientNonce = NextNonce(_clientNonce); + CmdPingServer(new ClientPing() { ClientTickWithNonce = NetworkTick.CombineBitsTick(_clientNonce, _networkTick.GetClientTick()) }); + } + + /// Sends synchronization updates to all connected clients, including tick counts and packet loss information. + [Server] + private void SendUpdatesToAllClients() { + // Increase nonce by 1 but keep withing 5 bits of data [0-31] + _serverNonce = NextNonce(_serverNonce); + var absoluteServerTick = _networkTick.GetServerAbsoluteTick(); + var isSendAbsolute = absoluteServerTick % _absoluteServerTickModulus == 0; + var serverTickWithNonce = NetworkTick.CombineBitsTick(_serverNonce, _networkTick.GetServerTick()); + foreach (NetworkConnectionToClient conn in NetworkServer.connections.Values) { + // If connection is on the same machine as the server we skip it. + if (conn == NetworkServer.localConnection) continue; + + if (_clients.TryGetValue(conn, out ClientData clientData)) { + // 0-30 % are reported regularly but 31 or higher are aggregated as just 31 ( mor ethan 30% packet loss is extreme! ) + int compressedLoss = Math.Min(31, (int)Math.Ceiling(clientData.RemoteClientLoss.Loss)); + + if (isSendAbsolute || clientData.SentPackets < absoluteTickSyncHandshakeTicks) + // If requested by interval or during hand shake send absolute tick alongside tick information + RpcAbsoluteServerPong(conn, new AbsoluteServerPong() { + AbsoluteServerTick = absoluteServerTick, + ServerTickWithNonce = serverTickWithNonce, + ClientTickWithLoss = NetworkTick.CombineBitsTick( + compressedLoss, + clientData.ClientTick) + }); + else + // Send tick information with nonce and loss + RpcServerPong(conn, new ServerPong() { + ServerTickWithNonce = serverTickWithNonce, + ClientTickWithLoss = NetworkTick.CombineBitsTick( + compressedLoss, + clientData.ClientTick) + }); + + // Count how many packets were sent + clientData.SentPackets += 1; + _clients[conn] = clientData; + } + } + } + + #endregion + + /*** Target RPC and Command callbacks ***/ + + #region Target RPC and Command callbacks + + /// Handles the server's response containing absolute tick synchronization data. + /// The client connection receiving the response. + /// The server's pong message with tick and nonce data. + [TargetRpc(channel = Channels.Unreliable)] + private void RpcAbsoluteServerPong(NetworkConnectionToClient target, AbsoluteServerPong serverPong) { + var (serverNonce, serverTick) = NetworkTick.SplitCombinedBitsTick(serverPong.ServerTickWithNonce); + var (sendLoss, clientTick) = NetworkTick.SplitCombinedBitsTick(serverPong.ClientTickWithLoss); + SetAbsoluteTicks(serverPong.AbsoluteServerTick, serverTick); + HandleServerPong(serverNonce, serverTick, sendLoss, clientTick); + } + + /// Handles the server's standard response containing synchronization data. + /// The client connection receiving the response. + /// The server's pong message with tick and nonce data. + [TargetRpc(channel = Channels.Unreliable)] + private void RpcServerPong(NetworkConnectionToClient target, ServerPong serverPong) { + var (serverNonce, serverTick) = NetworkTick.SplitCombinedBitsTick(serverPong.ServerTickWithNonce); + var (sendLoss, clientTick) = NetworkTick.SplitCombinedBitsTick(serverPong.ClientTickWithLoss); + HandleServerPong(serverNonce, serverTick, sendLoss, clientTick); + } + + /// Receives ping messages from clients and updates their data on the server. + /// The ping message containing the client's tick and nonce. + /// The connection to the client sending the ping. + [Command(requiresAuthority = false, channel = Channels.Unreliable)] + private void CmdPingServer(ClientPing clientPing, NetworkConnectionToClient connectionToClient = null) { + if (connectionToClient == null) return; + var (nonce, clientTick) = NetworkTick.SplitCombinedBitsTick(clientPing.ClientTickWithNonce); + _clients[connectionToClient].RemoteClientLoss.AddPacket(NextNonce(_clients[connectionToClient].ClientNonce) != nonce); + _clients[connectionToClient] = new ClientData() { + ClientTick = clientTick, + ClientNonce = nonce, + RemoteClientLoss = _clients[connectionToClient].RemoteClientLoss, + SentPackets = _clients[connectionToClient].SentPackets, + }; + } + + #endregion + + /*** Helper Functions ***/ + + #region Helper Functions + + /// Validates whether the incoming packet is newer than the last processed one to prevent out-of-order processing. + /// The tick value from the incoming packet. + /// True if the packet is valid and should be processed; otherwise, false. + [Client] + private bool IsValidPacket(int clientTick) { + var isValid = NetworkTick.SubtractTicks(clientTick, _lastRemoteClientTick) > 0; + _lastRemoteClientTick = clientTick; + return isValid; + } + + /// Marks the start of an adjustment period, during which tick synchronization adjustments are applied. + /// The client tick value when adjustments should stop. + [Client] + private void SetAdjusting(int adjustmentEndTick) { + _capturedOffsets = false; + _isAdjusting = true; + _adjustmentEndTick = adjustmentEndTick; + } + + /// Resets the running minimums used for calculating tick adjustments, clearing any accumulated data. + [Client] + private void ResetRunningMins() { + _capturedOffsets = false; + _clientRunningMin.Reset(); + _clientLongRunningMin.Reset(); + _serverRunningMin.Reset(); + _serverLongRunningMin.Reset(); + } + + /// + /// Generates the next nonce value by incrementing the current nonce. + /// The nonce is a looping 5-bit variable (0-31), ensuring it wraps around correctly when reaching 31. + /// + /// The starting nonce value to increment. + /// The next nonce value, wrapped to stay within the 5-bit range. + private static int NextNonce(int startNonce) => (startNonce + 1) & 0b11111; + + /// + /// Calculates the tick compensation based on packet loss and a predefined compensation factor. + /// This is used to adjust for lost packets, smoothing gameplay experience based on the + /// `packetLossCompensationFactor`. + /// + /// The packet loss percentage used to calculate compensation ticks. + /// The number of ticks to compensate based on the provided packet loss. + private int CalculateTickCompensation(float loss) => Mathf.FloorToInt((loss + packetLossCompensationFactor - 0.01f) / packetLossCompensationFactor); + + #endregion + } +} \ No newline at end of file diff --git a/Assets/Mirror/Core/TickManager/NetworkTickManager.cs.meta b/Assets/Mirror/Core/TickManager/NetworkTickManager.cs.meta new file mode 100644 index 000000000..953b434a8 --- /dev/null +++ b/Assets/Mirror/Core/TickManager/NetworkTickManager.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: eb2e49dca4a74e7baa4b2fc290e20730 +timeCreated: 1730317270 \ No newline at end of file diff --git a/Assets/Mirror/Core/TickManager/PacketLossTracker.cs b/Assets/Mirror/Core/TickManager/PacketLossTracker.cs new file mode 100644 index 000000000..65f20be36 --- /dev/null +++ b/Assets/Mirror/Core/TickManager/PacketLossTracker.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; + +namespace Mirror{ + /// + /// A class for tracking packet loss over a sliding window of packets. + /// It calculates the packet loss rate as a fraction based on a fixed sample size, + /// and provides both a floating-point representation and a byte-scaled value for the loss. + /// + public class PacketLossTracker{ + // A queue to store recent packet loss values (1 for loss, 0 for received) + private Queue _packetLossWindow; + + // The current packet loss rate as a float (between 0 and 1) + private float _loss = 0; + + // Sum of packet loss values in the current window + private int _packetLossSum = 0; + + // Number of packets in the sample window for calculating loss + private readonly int _samplePackets; + + /// Initializes a new instance of the PacketLossTracker class with a specified sample size. + /// The number of packets to track for calculating the loss rate. + /// Thrown when samplePackets is zero or negative. + public PacketLossTracker(int samplePackets) { + _samplePackets = samplePackets > 0 ? samplePackets : throw new ArgumentException("Sample packets must be greater than zero."); + _packetLossWindow = new Queue(_samplePackets); + } + + /// + /// Adds a packet result to the tracker, indicating whether the packet was lost or received. + /// Updates the loss rate based on the latest window of packet results. + /// + /// True if the packet was lost; false if the packet was received. + public void AddPacket(bool isLost) { + int lossValue = isLost ? 1 : 0; + _packetLossWindow.Enqueue(lossValue); + _packetLossSum += lossValue; + + // Remove the oldest packet when the queue exceeds the max size + if (_packetLossWindow.Count > _samplePackets) + _packetLossSum -= _packetLossWindow.Dequeue(); + + _loss = (float)_packetLossSum / _samplePackets; + } + + /// Gets the current packet loss rate as a floating-point value between 0 and 100. + public float Loss => _loss * 100; + } +} \ No newline at end of file diff --git a/Assets/Mirror/Core/TickManager/PacketLossTracker.cs.meta b/Assets/Mirror/Core/TickManager/PacketLossTracker.cs.meta new file mode 100644 index 000000000..e5fae0278 --- /dev/null +++ b/Assets/Mirror/Core/TickManager/PacketLossTracker.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 44fd6008a95941a1997ed153caf41f5e +timeCreated: 1730577723 \ No newline at end of file diff --git a/Assets/Mirror/Core/TickManager/RunningMin.cs b/Assets/Mirror/Core/TickManager/RunningMin.cs new file mode 100644 index 000000000..1df2c3933 --- /dev/null +++ b/Assets/Mirror/Core/TickManager/RunningMin.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; + +namespace Mirror{ + /// + /// A class that maintains a running minimum over a fixed-size sliding window of integers. + /// Provides efficient tracking of the minimum value as elements are added and removed from the window. + /// + public class RunningMin{ + // The fixed size of the sliding window + private readonly int _windowSize; + + // Queue to store the values in the sliding window + private readonly Queue _values; + + // Stores the current minimum value in the window + private int _currentMin; + + /// Gets the current minimum value in the sliding window. + public int CurrentMin => _currentMin; + + /// Gets the current count of elements in the sliding window. + public int Count => _values.Count; + + /// Checks if the sliding window is full. + public bool IsFull => _values.Count == _windowSize; + + /// Returns last added value. + public int Last => _values.ToArray()[_values.Count - 1]; + + /// Initializes a new instance of the class with a specified window size. + /// The maximum number of elements in the sliding window. + public RunningMin(int windowSize = 100) { + _windowSize = windowSize > 0 ? windowSize : throw new ArgumentException("Sample packets must be greater than zero."); + _values = new Queue(windowSize); + _currentMin = int.MaxValue; + } + + /// Resets the values and current minimum. + public void Reset() { + _currentMin = int.MaxValue; + _values.Clear(); + } + + /// Recalculates the current minimum by iterating through the queue. Only called when necessary to avoid performance overhead. + private void UpdateCurrentMin() { + _currentMin = int.MaxValue; + foreach (int value in _values) + if (value < _currentMin) + _currentMin = value; + } + + /// Adds a value to the sliding window. Updates the current minimum as needed. + /// The new value to add to the window. + public void Add(int value) { + _values.Enqueue(value); + if (value < _currentMin) + _currentMin = value; + // Check if exceeding the window size, if so then remove oldest item + if (_values.Count > _windowSize) { + int removedValue = _values.Dequeue(); + // Check oldest value is equal to minimum and is not equal to the new value we need to calculate the current minimum + if (removedValue == _currentMin && removedValue != value) + UpdateCurrentMin(); + } + } + } +} \ No newline at end of file diff --git a/Assets/Mirror/Core/TickManager/RunningMin.cs.meta b/Assets/Mirror/Core/TickManager/RunningMin.cs.meta new file mode 100644 index 000000000..3d86e20d5 --- /dev/null +++ b/Assets/Mirror/Core/TickManager/RunningMin.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 9671059f9b45471abcf30dd42fa2018e +timeCreated: 1730577873 \ No newline at end of file