Added a copy of working NetworkTickManager

This commit is contained in:
NiftyliuS 2024-11-12 21:15:48 +02:00
parent 1187a59b18
commit fcde679c43
12 changed files with 1269 additions and 0 deletions

View File

@ -0,0 +1,96 @@
using UnityEngine;
using System;
namespace Mirror{
[DefaultExecutionOrder(-10)]
[DisallowMultipleComponent]
[AddComponentMenu("Network/Network Physics Controller")]
public class NetworkPhysicsController : MonoBehaviour{
/// <summary>
/// Callback action to handle tick-forwarding logic.
/// Allows external classes to define custom behavior when the tick advances.
/// </summary>
public Action<int> TickForwardCallback;
private static int _reconcileStartTick = 0;
/// <summary>
/// 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.
/// </summary>
/// <param name="deltaTicks">The number of ticks to forward.</param>
public virtual void TickForward(int deltaTicks) {
TickForwardCallback?.Invoke(deltaTicks);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="deltaTime">The time interval to simulate physics for.</param>
public virtual void PhysicsTick(float deltaTime) {
Physics.Simulate(deltaTime); // Using Unity's built-in physics engine.
}
/// <summary>
/// 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.
/// </summary>
/// <param name="deltaTicks">The number of ticks to simulate forward.</param>
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);
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="deltaTicks">The number of ticks to simulate forward in one batch.</param>
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);
}
/// <summary>
/// Requests the reconciliation process to start from a specific tick.
/// Stores the earliest requested tick before reconciliation is executed.
/// </summary>
/// <param name="reconcileStartTick">The tick from which to start reconciliation.</param>
public static void RequestReconcileFromTick(int reconcileStartTick) {
if (_reconcileStartTick > reconcileStartTick || _reconcileStartTick == 0) {
_reconcileStartTick = reconcileStartTick; // the +1 is important to include the faulty tick
}
}
/// <summary>
/// Retrieves the tick number from which reconciliation should start.
/// </summary>
/// <returns>The tick number from which to start reconciliation.</returns>
public int GetReconcileStartTick() {
return _reconcileStartTick;
}
/// <summary>
/// Resets the reconciliation counter, marking the reconciliation process as complete.
/// Sets _ticksToReconcile to 0, indicating no further reconciliation is required.
/// </summary>
public void ResetReconcile() {
_reconcileStartTick = 0;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: b36faa4b9565404b95ba6539a10fc47f
timeCreated: 1730317284

View File

@ -0,0 +1,93 @@
using System.Collections.Generic;
namespace Mirror{
/// <summary>
/// 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.
/// </summary>
public interface INetworkedItem{
/// <summary>
/// Called before the main network update, allowing the item to perform any necessary preparation or pre-update logic.
/// </summary>
/// <param name="deltaTicks">The number of ticks since the last update.</param>
/// <param name="deltaTime">The time elapsed since the last update in seconds.</param>
void BeforeNetworkUpdate(int deltaTicks, float deltaTime);
/// <summary>
/// Called during the main network update, allowing the item to handle core updates related to network state, physics, or entity positioning.
/// </summary>
/// <param name="deltaTicks">The number of ticks since the last update.</param>
/// <param name="deltaTime">The time elapsed since the last update in seconds.</param>
void OnNetworkUpdate(int deltaTicks, float deltaTime);
/// <summary>
/// Called after the main network update, allowing the item to perform any necessary cleanup or post-update logic.
/// </summary>
/// <param name="deltaTicks">The number of ticks since the last update.</param>
/// <param name="deltaTime">The time elapsed since the last update in seconds.</param>
void AfterNetworkUpdate(int deltaTicks, float deltaTime);
}
/// <summary>
/// Manages network update sequences for entities requiring tick-based adjustments.
/// </summary>
public class NetworkPhysicsEntity{
/// <summary> Stores items requiring updates on each tick, as a list of tuples with priority and item. </summary>
private static readonly List<(int priority, INetworkedItem item)> NetworkItems = new List<(int, INetworkedItem)>();
/// <summary> Adds a network entity to the collection for updates and sorts by priority. </summary>
/// <param name="item">The network item implementing <see cref="INetworkedItem"/> that requires tick updates.</param>
/// <param name="priority">The priority for the entity, with lower numbers indicating higher priority.</param>
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]
}
/// <summary> Removes a network entity from the collection based on the item reference only. </summary>
/// <param name="item">The network item to remove.</param>
public static void RemoveNetworkEntity(INetworkedItem item) {
NetworkItems.RemoveAll(entry => entry.item.Equals(item));
}
/// <summary>
/// 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.
/// </summary>
/// <param name="deltaTicks">The number of ticks since the last update.</param>
/// <param name="deltaTime">The time elapsed since the last update in seconds.</param>
public static void RunBeforeNetworkUpdates(int deltaTicks, float deltaTime) {
foreach (var (priority, item) in NetworkItems) {
item.BeforeNetworkUpdate(deltaTicks, deltaTime);
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="deltaTicks">The number of ticks since the last update.</param>
/// <param name="deltaTime">The time elapsed since the last update in seconds.</param>
public static void RunNetworkUpdates(int deltaTicks, float deltaTime) {
foreach (var (priority, item) in NetworkItems) {
item.OnNetworkUpdate(deltaTicks, deltaTime);
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="deltaTicks">The number of ticks since the last update.</param>
/// <param name="deltaTime">The time elapsed since the last update in seconds.</param>
public static void RunAfterNetworkUpdates(int deltaTicks, float deltaTime) {
foreach (var (priority, item) in NetworkItems) {
item.AfterNetworkUpdate(deltaTicks, deltaTime);
}
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 8b08c8ad7b0840fdb96784accdc66787
timeCreated: 1730400815

View File

@ -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
/// <summary> Gets the client-to-server packet loss compensation ticks. <para><b>Client-only:</b> This cant be accessed on the server.</para></summary>
/// <exception cref="InvalidOperationException">Thrown if accessed on the server.</exception>
public static int ClientToServerPacketLossCompensation {
get {
if (_isServer) throw new InvalidOperationException("ClientToServerPacketLossCompensation is client-only and cannot be accessed on the server.");
return _clientToServerPacketLossCompensation;
}
}
/// <summary> Gets the server-to-client packet loss compensation ticks. <para><b>Client-only:</b> This cant be accessed on the server.</para></summary>
/// <exception cref="InvalidOperationException">Thrown if accessed on the server.</exception>
public static int ServerToClientPacketLossCompensation {
get {
if (_isServer) throw new InvalidOperationException("ServerToClientPacketLossCompensation is client-only and cannot be accessed on the server.");
return _serverToClientPacketLossCompensation;
}
}
/// <summary>
/// Sets the client-to-server packet loss compensation ticks, allowing the client to define the number of compensation ticks based on detected packet loss.
/// <para><b>Client-only:</b> This method should not be called on the server.</para>
/// </summary>
/// <param name="compensationTicks">The number of compensation ticks to set.</param>
public void SetClientToServerPacketLossCompensation(int compensationTicks) {
if (_isServer) throw new InvalidOperationException("SetClientToServerPacketLossCompensation is client-only and cannot be accessed on the server.");
_clientToServerPacketLossCompensation = compensationTicks;
}
/// <summary>
/// Sets the server-to-client packet loss compensation ticks, allowing the client to define the number of compensation ticks based on detected packet loss.
/// <para><b>Client-only:</b> This method should not be called on the server.</para>
/// </summary>
/// <param name="compensationTicks">The number of compensation ticks to set.</param>
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
/// <summary> Gets a value indicating whether the current instance is a server. </summary>
public static bool IsServer => _isServer;
/// <summary> Gets a value indicating whether the client is synchronizing with the server. </summary>
public static bool IsSynchronizing => _isSynchronizing;
/// <summary> Gets a value indicating whether the client is synchronized with the server. </summary>
public static bool IsSynchronized => _isSynchronized;
/// <summary> Gets a value indicating whether the system is reconciling ticks. </summary>
public static bool IsReconciling => _isReconciling;
#endregion
/*** Static Tick Getters ***/
#region Static Tick Getters
/// <summary> Gets the current tick count based on whether the instance is a server or client. </summary>
public static int CurrentTick => _isServer ? _serverTick : _clientTick;
/// <summary> Gets the current absolute tick count based on whether the instance is a server or client. </summary>
public static int CurrentAbsoluteTick => _isServer ? _absoluteServerTick : _absoluteClientTick;
/// <summary> Gets the client tick count. </summary>
public static int ClientTick => _clientTick;
/// <summary> Gets the client absolute tick count. </summary>
public static int ClientAbsoluteTick => _absoluteClientTick;
/// <summary> Gets the server tick count. </summary>
public static int ServerTick => _serverTick;
/// <summary> Gets the server tick count. </summary>
public static int ServerAbsoluteTick => _absoluteServerTick;
#endregion
/*** Instance Getters ***/
#region Instance Getters
/// <summary> Checks if the client is in the process of synchronizing with the server. </summary>
public bool GetIsSynchronizing() => _isSynchronizing;
/// <summary> Checks if the client is currently synchronized with the server. </summary>
public bool GetIsSynchronized() => _isSynchronized;
/// <summary>Gets the current client tick value.</summary>
public int GetClientTick() => _clientTick;
/// <summary>Gets the absolute tick value for the client.</summary>
public int GetClientAbsoluteTick() => _absoluteClientTick;
/// <summary>Gets the current client tick value.</summary>
public int GetServerTick() => _serverTick;
/// <summary>Gets the absolute tick value for the server.</summary>
public int GetServerAbsoluteTick() => _absoluteServerTick;
#endregion
/*** Instance Status Setters ***/
#region Instance Status Setters
/// <summary> Sets the server status of the current instance. </summary>
public void SetIsServer(bool isServer) => _isServer = isServer;
/// <summary> Sets the synchronization status between client and server. </summary>
public void SetSynchronized(bool isSynchronized) => _isSynchronized = isSynchronized;
/// <summary> Sets the synchronization status between client and server. </summary>
public void SetSynchronizing(bool isSynchronizing) => _isSynchronizing = isSynchronizing;
/// <summary> Sets the reconciling status. </summary>
public void SetReconciling(bool reconciling) => _isReconciling = reconciling;
#endregion
/*** Instance Tick Setters ***/
#region Instance Tick Setters
/// <summary>Sets a new tick value for the client.</summary>
public void SetClientTick(int newTick) => _clientTick = newTick;
/// <summary>Sets a new absolute tick value for the client.</summary>
public void SetClientAbsoluteTick(int newAbsoluteTick) => _absoluteClientTick = newAbsoluteTick;
/// <summary>Sets a new tick value for the server.</summary>
public void SetServerTick(int newTick) => _serverTick = newTick;
/// <summary>Sets a new absolute tick value for the server.</summary>
public void SetServerAbsoluteTick(int newAbsoluteTick) => _absoluteServerTick = newAbsoluteTick;
#endregion
/*** Instance Tick Modifiers ***/
#region Instance Tick Modifiers
/// <summary>Increments the client tick by a specified amount, wrapping to 11 bits.</summary>
public void IncrementClientTick(int increment) => _clientTick = (_clientTick + increment) & 0b11111111111;
/// <summary>Increments the client's absolute tick by a specified amount.</summary>
public void IncrementClientAbsoluteTick(int increment) => _absoluteClientTick += increment;
/// <summary>Increments the server tick by a specified amount, wrapping to 11 bits.</summary>
public void IncrementServerTick(int increment) => _serverTick = (_serverTick + increment) & 0b11111111111;
/// <summary>Increments the server's absolute tick by a specified amount.</summary>
public void IncrementServerAbsoluteTick(int increment) => _absoluteServerTick += increment;
#endregion
/*** Useful Bitwise Functions ***/
#region Useful Bitwise Functions
/// <summary>
/// Combines a fiveBits and tick counter into a single <see cref="ushort"/> value. This is used to optimize network traffic by packing two values into one.
/// </summary>
/// <param name="fiveBits">The fiveBits value (should be within 5 bits).</param>
/// <param name="tick">The tick counter value (should be within 11 bits).</param>
/// <returns>A combined <see cref="ushort"/> containing both the fiveBits and tick counter.</returns>
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
}
/// <summary>
/// Splits a combined fiveBits and tick counter value back into its individual components.
/// </summary>
/// <param name="combined">The combined <see cref="ushort"/> value.</param>
/// <returns>A tuple containing the fiveBits and tick counter.</returns>
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);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="tickOne">The first tick value.</param>
/// <param name="tickTwo">The second tick value.</param>
/// <returns>The minimal difference between the two ticks.</returns>
public static int SubtractTicks(int tickOne, int tickTwo) {
var delta = (tickOne - tickTwo + 2048) % 2048;
if (delta >= 1024) delta -= 2048;
return delta;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="tick">The initial tick value.</param>
/// <param name="increment">The amount to increment the tick by.</param>
/// <returns>The incremented tick value, wrapped within the 2048 range.</returns>
public static int IncrementTick(int tick, int increment) {
return (tick + increment) & 0b11111111111;
}
#endregion
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: e4b45d54032d4018af1450222e8e7eef
timeCreated: 1730317658

View File

@ -0,0 +1,698 @@
using System;
using UnityEngine;
using System.Collections.Generic;
namespace Mirror{
/// <summary>
/// Represents data sent from the client to track its current state.
/// </summary>
public struct ClientData{
public int ClientNonce;
public int ClientTick;
public PacketLossTracker RemoteClientLoss;
public int SentPackets;
}
/// <summary>
/// Represents a server response that includes synchronization data for the client tick and packet loss information.
/// </summary>
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
}
/// <summary>
/// Represents a server response with an absolute tick count to help the client synchronize with the server more accurately.
/// </summary>
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
}
/// <summary>
/// Represents a client request sent to the server, including the client tick and a unique nonce for tracking.
/// </summary>
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<NetworkConnection, ClientData> _clients = new Dictionary<NetworkConnection, ClientData>();
// 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<NetworkPhysicsController>();
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
/// <summary>
/// Called when the server starts. Registers callbacks for client connection and disconnection events,
/// allowing the server to handle these events appropriately.
/// </summary>
[Server]
public override void OnStartServer() {
// Register callback for when clients connect/disconnect
NetworkServer.OnConnectedEvent += OnClientConnected;
NetworkServer.OnDisconnectedEvent += OnClientDisconnected;
base.OnStartServer();
}
/// <summary>
/// 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.
/// </summary>
[Server]
public override void OnStopServer() {
// Unregister callbacks when server stops
NetworkServer.OnConnectedEvent -= OnClientConnected;
NetworkServer.OnDisconnectedEvent -= OnClientDisconnected;
base.OnStopServer();
}
/// <summary>
/// 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.
/// </summary>
/// <param name="conn">The network connection for the connected client.</param>
[Server]
private void OnClientConnected(NetworkConnection conn) => _clients[conn] = new ClientData()
{ ClientNonce = 0, ClientTick = 0, RemoteClientLoss = new PacketLossTracker(packetLossSamples), SentPackets = 0 };
/// <summary>
/// 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.
/// </summary>
/// <param name="conn">The network connection for the disconnected client.</param>
[Server]
private void OnClientDisconnected(NetworkConnection conn) => _clients.Remove(conn);
#endregion
/*** Network Tick Start and Tick Initialization ***/
#region Network Tick Start and Tick Initialization
/// <summary> Initializes server-specific settings when the server starts. </summary>
[Server]
private void StartServer() {
_absoluteServerTickModulus = Mathf.RoundToInt(absoluteTickSyncIntervalSeconds / Time.fixedDeltaTime);
_networkTick.SetSynchronized(true);
_networkTick.SetSynchronizing(false);
}
/// <summary> Initializes client-specific settings when the client starts. </summary>
[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);
}
/// <summary> Advances the client's tick counters by the specified number of ticks. </summary>
/// <param name="deltaTicks">Number of ticks to advance.</param>
[Client]
private void OnTickForwardClient(int deltaTicks) {
_networkTick.IncrementClientTick(deltaTicks);
_networkTick.IncrementClientAbsoluteTick(deltaTicks);
_networkTick.IncrementServerTick(deltaTicks);
_networkTick.IncrementServerAbsoluteTick(deltaTicks);
}
/// <summary> Advances the server's tick counters by the specified number of ticks. </summary>
/// <param name="deltaTicks">Number of ticks to advance.</param>
[Server]
private void OnTickForwardServer(int deltaTicks) {
_networkTick.IncrementServerTick(deltaTicks);
_networkTick.IncrementServerAbsoluteTick(deltaTicks);
}
/// <summary> Initializes the tick manager and sets up the physics controller based on whether it is running on the server or client. </summary>
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
/// <summary> Updates the client's packet loss compensation values based on the server's nonce and reported packet loss. </summary>
/// <param name="serverNonce">The nonce received from the server to detect packet loss.</param>
/// <param name="sendPacketLoss">The packet loss percentage reported by the server.</param>
[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
/// <summary> Sets or adjusts the client's absolute server tick values to maintain synchronization with the server. </summary>
/// <param name="absoluteServerTick">The absolute tick count provided by the server.</param>
/// <param name="serverTick">The server's current tick value.</param>
[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);
}
}
/// <summary> Initiates the synchronization process by aligning the client's and server's ticks. </summary>
/// <param name="serverTick">The current tick value from the server.</param>
/// <param name="clientTick">The client's tick value as received by the server.</param>
[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());
}
/// <summary> Synchronizes the client's and server's ticks by applying necessary adjustments. </summary>
[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
/// <summary>
/// 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.
/// </summary>
/// <param name="serverNonce">Nonce value from the server for packet loss detection.</param>
/// <param name="serverTick">Current tick count from the server.</param>
/// <param name="sendLoss">Packet loss percentage reported by the server.</param>
/// <param name="clientTick">Client's tick count as received by the server.</param>
[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
/// <summary> Calculates the tick offsets between client and server, updating running minimums for synchronization adjustments. </summary>
/// <param name="serverTick">The current tick value from the server.</param>
/// <param name="clientTick">The client's tick value as received by the server.</param>
[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);
}
/// <summary> Determines the necessary tick adjustment for the client to maintain synchronization with the server. </summary>
/// <param name="absolute">If true, applies the full adjustment needed; otherwise, applies a minimal step.</param>
/// <returns>The number of ticks to adjust the client's tick by.</returns>
[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;
}
/// <summary> Determines the necessary tick adjustment for the server to maintain synchronization with the client. </summary>
/// <param name="absolute">If true, applies the full adjustment needed; otherwise, applies a minimal step.</param>
/// <returns>The number of ticks to adjust the server's tick by.</returns>
[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;
}
/// <summary> Calculates adjusted tick values for synchronization, applying any necessary client or server tick adjustments. </summary>
/// <param name="deltaTicks">The base number of ticks to advance.</param>
/// <returns>The adjusted number of ticks to use for simulation.</returns>
[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
/// <summary> Checks for any required reconciliation due to state discrepancies and resimulates physics accordingly. </summary>
/// <param name="deltaTicks">The number of ticks advanced since the last update.</param>
[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);
}
}
/// <summary> Updates the client's state each tick, handling synchronization, reconciliation, and physics simulation. </summary>
/// <param name="deltaTicks">The number of ticks to advance.</param>
[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);
}
/// <summary> Handles physics simulation and synchronization updates on both the server and client each fixed frame. </summary>
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
/// <summary> Sends a ping to the server with the client's current tick and a nonce for packet loss detection. </summary>
[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()) });
}
/// <summary> Sends synchronization updates to all connected clients, including tick counts and packet loss information. </summary>
[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
/// <summary> Handles the server's response containing absolute tick synchronization data. </summary>
/// <param name="target">The client connection receiving the response.</param>
/// <param name="serverPong">The server's pong message with tick and nonce data.</param>
[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);
}
/// <summary> Handles the server's standard response containing synchronization data. </summary>
/// <param name="target">The client connection receiving the response.</param>
/// <param name="serverPong">The server's pong message with tick and nonce data.</param>
[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);
}
/// <summary> Receives ping messages from clients and updates their data on the server. </summary>
/// <param name="clientPing">The ping message containing the client's tick and nonce.</param>
/// <param name="connectionToClient">The connection to the client sending the ping.</param>
[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
/// <summary> Validates whether the incoming packet is newer than the last processed one to prevent out-of-order processing. </summary>
/// <param name="clientTick">The tick value from the incoming packet.</param>
/// <returns>True if the packet is valid and should be processed; otherwise, false.</returns>
[Client]
private bool IsValidPacket(int clientTick) {
var isValid = NetworkTick.SubtractTicks(clientTick, _lastRemoteClientTick) > 0;
_lastRemoteClientTick = clientTick;
return isValid;
}
/// <summary> Marks the start of an adjustment period, during which tick synchronization adjustments are applied. </summary>
/// <param name="adjustmentEndTick">The client tick value when adjustments should stop.</param>
[Client]
private void SetAdjusting(int adjustmentEndTick) {
_capturedOffsets = false;
_isAdjusting = true;
_adjustmentEndTick = adjustmentEndTick;
}
/// <summary> Resets the running minimums used for calculating tick adjustments, clearing any accumulated data. </summary>
[Client]
private void ResetRunningMins() {
_capturedOffsets = false;
_clientRunningMin.Reset();
_clientLongRunningMin.Reset();
_serverRunningMin.Reset();
_serverLongRunningMin.Reset();
}
/// <summary>
/// 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.
/// </summary>
/// <param name="startNonce">The starting nonce value to increment.</param>
/// <returns>The next nonce value, wrapped to stay within the 5-bit range.</returns>
private static int NextNonce(int startNonce) => (startNonce + 1) & 0b11111;
/// <summary>
/// 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`.
/// </summary>
/// <param name="loss">The packet loss percentage used to calculate compensation ticks.</param>
/// <returns>The number of ticks to compensate based on the provided packet loss.</returns>
private int CalculateTickCompensation(float loss) => Mathf.FloorToInt((loss + packetLossCompensationFactor - 0.01f) / packetLossCompensationFactor);
#endregion
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: eb2e49dca4a74e7baa4b2fc290e20730
timeCreated: 1730317270

View File

@ -0,0 +1,51 @@
using System;
using System.Collections.Generic;
namespace Mirror{
/// <summary>
/// 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.
/// </summary>
public class PacketLossTracker{
// A queue to store recent packet loss values (1 for loss, 0 for received)
private Queue<int> _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;
/// <summary> Initializes a new instance of the PacketLossTracker class with a specified sample size. </summary>
/// <param name="samplePackets">The number of packets to track for calculating the loss rate.</param>
/// <exception cref="ArgumentException">Thrown when samplePackets is zero or negative.</exception>
public PacketLossTracker(int samplePackets) {
_samplePackets = samplePackets > 0 ? samplePackets : throw new ArgumentException("Sample packets must be greater than zero.");
_packetLossWindow = new Queue<int>(_samplePackets);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="isLost">True if the packet was lost; false if the packet was received.</param>
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;
}
/// <summary> Gets the current packet loss rate as a floating-point value between 0 and 100. </summary>
public float Loss => _loss * 100;
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 44fd6008a95941a1997ed153caf41f5e
timeCreated: 1730577723

View File

@ -0,0 +1,68 @@
using System;
using System.Collections.Generic;
namespace Mirror{
/// <summary>
/// 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.
/// </summary>
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<int> _values;
// Stores the current minimum value in the window
private int _currentMin;
/// <summary> Gets the current minimum value in the sliding window. </summary>
public int CurrentMin => _currentMin;
/// <summary> Gets the current count of elements in the sliding window. </summary>
public int Count => _values.Count;
/// <summary> Checks if the sliding window is full. </summary>
public bool IsFull => _values.Count == _windowSize;
/// <summary> Returns last added value. </summary>
public int Last => _values.ToArray()[_values.Count - 1];
/// <summary> Initializes a new instance of the <see cref="RunningMin"/> class with a specified window size. </summary>
/// <param name="windowSize">The maximum number of elements in the sliding window.</param>
public RunningMin(int windowSize = 100) {
_windowSize = windowSize > 0 ? windowSize : throw new ArgumentException("Sample packets must be greater than zero.");
_values = new Queue<int>(windowSize);
_currentMin = int.MaxValue;
}
/// <summary>Resets the values and current minimum.</summary>
public void Reset() {
_currentMin = int.MaxValue;
_values.Clear();
}
/// <summary> Recalculates the current minimum by iterating through the queue. Only called when necessary to avoid performance overhead. </summary>
private void UpdateCurrentMin() {
_currentMin = int.MaxValue;
foreach (int value in _values)
if (value < _currentMin)
_currentMin = value;
}
/// <summary> Adds a value to the sliding window. Updates the current minimum as needed. </summary>
/// <param name="value">The new value to add to the window.</param>
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();
}
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 9671059f9b45471abcf30dd42fa2018e
timeCreated: 1730577873