mirror of
https://github.com/MirrorNetworking/Mirror.git
synced 2024-11-18 02:50:32 +00:00
Added a copy of working NetworkTickManager
This commit is contained in:
parent
1187a59b18
commit
fcde679c43
96
Assets/Mirror/Core/TickManager/NetworkPhysicsController.cs
Normal file
96
Assets/Mirror/Core/TickManager/NetworkPhysicsController.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b36faa4b9565404b95ba6539a10fc47f
|
||||
timeCreated: 1730317284
|
93
Assets/Mirror/Core/TickManager/NetworkPhysicsEntity.cs
Normal file
93
Assets/Mirror/Core/TickManager/NetworkPhysicsEntity.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8b08c8ad7b0840fdb96784accdc66787
|
||||
timeCreated: 1730400815
|
245
Assets/Mirror/Core/TickManager/NetworkTick.cs
Normal file
245
Assets/Mirror/Core/TickManager/NetworkTick.cs
Normal 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
|
||||
}
|
||||
}
|
3
Assets/Mirror/Core/TickManager/NetworkTick.cs.meta
Normal file
3
Assets/Mirror/Core/TickManager/NetworkTick.cs.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e4b45d54032d4018af1450222e8e7eef
|
||||
timeCreated: 1730317658
|
698
Assets/Mirror/Core/TickManager/NetworkTickManager.cs
Normal file
698
Assets/Mirror/Core/TickManager/NetworkTickManager.cs
Normal 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
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: eb2e49dca4a74e7baa4b2fc290e20730
|
||||
timeCreated: 1730317270
|
51
Assets/Mirror/Core/TickManager/PacketLossTracker.cs
Normal file
51
Assets/Mirror/Core/TickManager/PacketLossTracker.cs
Normal 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;
|
||||
}
|
||||
}
|
3
Assets/Mirror/Core/TickManager/PacketLossTracker.cs.meta
Normal file
3
Assets/Mirror/Core/TickManager/PacketLossTracker.cs.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 44fd6008a95941a1997ed153caf41f5e
|
||||
timeCreated: 1730577723
|
68
Assets/Mirror/Core/TickManager/RunningMin.cs
Normal file
68
Assets/Mirror/Core/TickManager/RunningMin.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
3
Assets/Mirror/Core/TickManager/RunningMin.cs.meta
Normal file
3
Assets/Mirror/Core/TickManager/RunningMin.cs.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9671059f9b45471abcf30dd42fa2018e
|
||||
timeCreated: 1730577873
|
Loading…
Reference in New Issue
Block a user