using FishNet.Connection; using FishNet.Documenting; using FishNet.Object; using FishNet.Serializing; using FishNet.Serializing.Helping; using FishNet.Transporting; using FishNet.Utility; using FishNet.Utility.Extension; using System; using System.Collections.Generic; using System.Runtime.CompilerServices; using UnityEngine; using SystemStopwatch = System.Diagnostics.Stopwatch; using UnityScene = UnityEngine.SceneManagement.Scene; namespace FishNet.Managing.Timing { /// /// Provides data and actions for network time and tick based systems. /// [DisallowMultipleComponent] [AddComponentMenu("FishNet/Manager/TimeManager")] public sealed partial class TimeManager : MonoBehaviour { #region Types. /// /// How networking timing is performed. /// private enum TimingType { /// /// Send and read data on tick. /// Tick = 0, /// /// Send and read data as soon as possible. This does not include built-in components, which will still run on tick. /// Variable = 1 } private enum UpdateOrder : byte { BeforeTick = 0, AfterTick = 1, } #endregion #region Public. /// /// Called when the local clients ping is updated. /// public event Action OnRoundTripTimeUpdated; /// /// Called right before a tick occurs, as well before data is read. /// public event Action OnPreTick; /// /// Called when a tick occurs. /// public event Action OnTick; /// /// When using TimeManager for physics timing, this is called immediately before physics simulation will occur for the tick. /// While using Unity for physics timing, this is called during FixedUpdate. /// This may be useful if you wish to run physics differently for stacked scenes. /// public event Action OnPrePhysicsSimulation; /// /// When using TimeManager for physics timing, this is called immediately after the physics simulation has occured for the tick. /// While using Unity for physics timing, this is called during Update, only if a physics frame. /// This may be useful if you wish to run physics differently for stacked scenes. /// public event Action OnPostPhysicsSimulation; /// /// Called after a tick occurs; physics would have simulated if using PhysicsMode.TimeManager. /// public event Action OnPostTick; /// /// Called when MonoBehaviours call Update. /// public event Action OnUpdate; /// /// Called when MonoBehaviours call LateUpdate. /// public event Action OnLateUpdate; /// /// Called when MonoBehaviours call FixedUpdate. /// public event Action OnFixedUpdate; /// /// RoundTripTime in milliseconds. This value includes latency from the tick rate. /// public long RoundTripTime { get; private set; } /// /// True if the number of frames per second are less than the number of expected ticks per second. /// internal bool LowFrameRate => ((Time.unscaledTime - _lastMultipleTicksTime) < 1f); /// /// Tick on the last received packet, be it from server or client. /// public uint LastPacketTick { get; internal set; } /// /// Current approximate network tick as it is on server. /// When running as client only this is an approximation to what the server tick is. /// The value of this field may increase and decrease as timing adjusts. /// This value is reset upon disconnecting. /// Tick can be used to get the server time by using TicksToTime(). /// Use LocalTick for values that only increase. /// public uint Tick { get; internal set; } /// /// A fixed deltaTime for TickRate. /// [HideInInspector] public double TickDelta { get; private set; } /// /// True if the TimeManager will or has ticked this frame. /// public bool FrameTicked { get; private set; } /// /// How long the local server has been connected. /// public float ServerUptime { get; private set; } /// /// How long the local client has been connected. /// public float ClientUptime { get; private set; } #endregion #region Serialized. /// /// When to invoke OnUpdate and other Unity callbacks relayed by the TimeManager. /// [Tooltip("When to invoke OnUpdate and other Unity callbacks relayed by the TimeManager.")] [SerializeField] private UpdateOrder _updateOrder = UpdateOrder.BeforeTick; /// /// Timing for sending and receiving data. /// [Tooltip("Timing for sending and receiving data.")] [SerializeField] private TimingType _timingType = TimingType.Tick; /// /// While true clients may drop local ticks if their devices are unable to maintain the tick rate. /// This could result in a temporary desynchronization but will prevent the client falling further behind on ticks by repeatedly running the logic cycle multiple times per frame. /// [Tooltip("While true clients may drop local ticks if their devices are unable to maintain the tick rate. This could result in a temporary desynchronization but will prevent the client falling further behind on ticks by repeatedly running the logic cycle multiple times per frame.")] [SerializeField] private bool _allowTickDropping; /// /// Maximum number of ticks which may occur in a single frame before remainder are dropped for the frame. /// [Tooltip("Maximum number of ticks which may occur in a single frame before remainder are dropped for the frame.")] [Range(1, 25)] [SerializeField] private byte _maximumFrameTicks = 2; /// /// /// [Tooltip("How many times per second the server will simulate. This does not limit server frame rate.")] [Range(1, 240)] [SerializeField] private ushort _tickRate = 30; /// /// How many times per second the server will simulate. This does not limit server frame rate. /// public ushort TickRate { get => _tickRate; private set => _tickRate = value; } /// /// /// [Tooltip("How often in seconds to a connections ping. This is also responsible for approximating server tick. This value does not affect prediction.")] [Range(1, 15)] [SerializeField] private byte _pingInterval = 1; /// /// How often in seconds to a connections ping. This is also responsible for approximating server tick. This value does not affect prediction. /// internal byte PingInterval => _pingInterval; /// /// How often in seconds to update prediction timing. Lower values will result in marginally more accurate timings at the cost of bandwidth. /// [Tooltip("How often in seconds to update prediction timing. Lower values will result in marginally more accurate timings at the cost of bandwidth.")] [Range(1, 15)] [SerializeField] private byte _timingInterval = 2; /// /// /// [Tooltip("How to perform physics.")] [SerializeField] private PhysicsMode _physicsMode = PhysicsMode.Unity; /// /// How to perform physics. /// public PhysicsMode PhysicsMode => _physicsMode; #endregion #region Private. /// /// Ticks that have passed on client since the last time server sent an UpdateTicksBroadcast. /// private uint _clientTicks = 0; /// /// Last Tick the server sent out UpdateTicksBroadcast. /// private uint _lastUpdateTicks = 0; /// /// /// private uint _localTick; /// /// A tick that is not synchronized. This value will only increment. May be used for indexing or Ids with custom logic. /// When called on the server Tick is returned, otherwise LocalTick is returned. /// This value resets upon disconnecting. /// public uint LocalTick { get => (_networkManager.IsServer) ? Tick : _localTick; private set => _localTick = value; } /// /// Stopwatch used for pings. /// SystemStopwatch _pingStopwatch = new SystemStopwatch(); /// /// Ticks passed since last ping. /// private uint _pingTicks; /// /// MovingAverage instance used to calculate mean ping. /// private MovingAverage _pingAverage = new MovingAverage(5); /// /// Accumulating frame time to determine when to increase tick. /// private double _elapsedTickTime; /// /// NetworkManager used with this. /// private NetworkManager _networkManager; /// /// Internal deltaTime for clients. Controlled by the server. /// private double _adjustedTickDelta; /// /// Range which client timing may reside within. /// private double[] _clientTimingRange; /// /// Last frame an iteration occurred for incoming. /// private int _lastIncomingIterationFrame = -1; /// /// True if client received Pong since last ping. /// private bool _receivedPong = true; /// /// Last unscaledTime multiple ticks occurred in a single frame. /// private float _lastMultipleTicksTime; /// /// Number of TimeManagers open which are using manual physics. /// private static uint _manualPhysics; #endregion #region Const. /// /// Maximum percentage timing may vary from SimulationInterval for clients. /// private const float CLIENT_TIMING_PERCENT_RANGE = 0.5f; /// /// Percentage of TickDelta client will adjust when needing to speed up. /// private const double CLIENT_SPEEDUP_PERCENT = 0.003d; /// /// Percentage of TickDelta client will adjust when needing to slow down. /// private const double CLIENT_SLOWDOWN_PERCENT = 0.005d; /// /// When steps to be sent to clients are equal to or higher than this value in either direction a reset steps will be sent. /// private const byte RESET_STEPS_THRESHOLD = 5; /// /// Playerprefs string to load and save user fixed time. /// private const string SAVED_FIXED_TIME_TEXT = "SavedFixedTimeFN"; #endregion #if UNITY_EDITOR private void OnDisable() { //If closing/stopping. if (ApplicationState.IsQuitting()) { _manualPhysics = 0; UnsetSimulationSettings(); } else if (PhysicsMode == PhysicsMode.TimeManager) { _manualPhysics = Math.Max(0, _manualPhysics - 1); } } #endif /// /// Called when FixedUpdate ticks. This is called before any other script. /// internal void TickFixedUpdate() { OnFixedUpdate?.Invoke(); /* Invoke onsimulation if using Unity time. * Otherwise let the tick cycling part invoke. */ if (PhysicsMode == PhysicsMode.Unity) OnPrePhysicsSimulation?.Invoke(Time.fixedDeltaTime); } /// /// Called when Update ticks. This is called before any other script. /// internal void TickUpdate() { if (_networkManager.IsServer) ServerUptime += Time.deltaTime; if (_networkManager.IsClient) ClientUptime += Time.deltaTime; bool beforeTick = (_updateOrder == UpdateOrder.BeforeTick); if (beforeTick) { OnUpdate?.Invoke(); MethodLogic(); } else { MethodLogic(); OnUpdate?.Invoke(); } void MethodLogic() { IncreaseTick(); /* Invoke onsimulation if using Unity time. * Otherwise let the tick cycling part invoke. */ if (PhysicsMode == PhysicsMode.Unity && Time.inFixedTimeStep) OnPostPhysicsSimulation?.Invoke(Time.fixedDeltaTime); } } /// /// Called when LateUpdate ticks. This is called after all other scripts. /// internal void TickLateUpdate() { OnLateUpdate?.Invoke(); } /// /// Initializes this script for use. /// internal void InitializeOnce_Internal(NetworkManager networkManager) { _networkManager = networkManager; SetInitialValues(); _networkManager.ServerManager.OnServerConnectionState += ServerManager_OnServerConnectionState; _networkManager.ClientManager.OnClientConnectionState += ClientManager_OnClientConnectionState; AddNetworkLoops(); } /// /// Adds network loops to gameObject. /// private void AddNetworkLoops() { //Writer. if (!gameObject.TryGetComponent(out _)) gameObject.AddComponent(); //Reader. if (!gameObject.TryGetComponent(out _)) gameObject.AddComponent(); } /// /// Called after the local client connection state changes. /// private void ClientManager_OnClientConnectionState(ClientConnectionStateArgs obj) { if (obj.ConnectionState != LocalConnectionState.Started) { _pingStopwatch.Stop(); ClientUptime = 0f; //Only reset ticks if also not server. if (!_networkManager.IsServer) { LocalTick = 0; Tick = 0; } } //Started. else { _pingStopwatch.Restart(); } } /// /// Called after the local server connection state changes. /// private void ServerManager_OnServerConnectionState(ServerConnectionStateArgs obj) { //If no servers are running. if (!_networkManager.ServerManager.AnyServerStarted()) { ServerUptime = 0f; Tick = 0; } } /// /// Sets values to use based on settings. /// private void SetInitialValues() { SetTickRate(TickRate); InitializePhysicsMode(PhysicsMode); } /// /// Sets simulation settings to Unity defaults. /// private void UnsetSimulationSettings() { Physics.autoSimulation = true; #if !UNITY_2020_2_OR_NEWER Physics2D.autoSimulation = true; #else Physics2D.simulationMode = SimulationMode2D.FixedUpdate; #endif float simulationTime = PlayerPrefs.GetFloat(SAVED_FIXED_TIME_TEXT, float.MinValue); if (simulationTime != float.MinValue) Time.fixedDeltaTime = simulationTime; } /// /// Initializes physics mode when starting. /// /// private void InitializePhysicsMode(PhysicsMode mode) { //Disable. if (mode == PhysicsMode.Disabled) { SetPhysicsMode(mode); } //Do not automatically simulate. else if (mode == PhysicsMode.TimeManager) { #if UNITY_EDITOR //Preserve user tick rate. PlayerPrefs.SetFloat(SAVED_FIXED_TIME_TEXT, Time.fixedDeltaTime); //Let the player know. if (Time.fixedDeltaTime != (float)TickDelta) Debug.LogWarning("Time.fixedDeltaTime is being overriden with TimeManager.TickDelta"); #endif Time.fixedDeltaTime = (float)TickDelta; /* Only check this if network manager * is not null. It would be null via * OnValidate. */ if (_networkManager != null) { //If at least one time manager is already running manual physics. if (_manualPhysics > 0) _networkManager.LogError($"There are multiple TimeManagers instantiated which are using manual physics. Manual physics with multiple TimeManagers is not supported."); _manualPhysics++; } SetPhysicsMode(mode); } //Automatically simulate. else { #if UNITY_EDITOR float savedTime = PlayerPrefs.GetFloat(SAVED_FIXED_TIME_TEXT, float.MinValue); if (savedTime != float.MinValue && Time.fixedDeltaTime != savedTime) { Debug.LogWarning("Time.fixedDeltaTime has been set back to user values."); Time.fixedDeltaTime = savedTime; } PlayerPrefs.DeleteKey(SAVED_FIXED_TIME_TEXT); #endif SetPhysicsMode(mode); } } /// /// Updates physics based on which physics mode to use. /// /// public void SetPhysicsMode(PhysicsMode mode) { _physicsMode = mode; //Disable. if (mode == PhysicsMode.Disabled || mode == PhysicsMode.TimeManager) { Physics.autoSimulation = false; #if !UNITY_2020_2_OR_NEWER Physics2D.autoSimulation = false; #else Physics2D.simulationMode = SimulationMode2D.Script; #endif } //Automatically simulate. else { Physics.autoSimulation = true; #if !UNITY_2020_2_OR_NEWER Physics2D.autoSimulation = true; #else Physics2D.simulationMode = SimulationMode2D.FixedUpdate; #endif } } #region PingPong. /// /// Modifies client ping based on LocalTick and clientTIck. /// /// internal void ModifyPing(uint clientTick) { uint tickDifference = (LocalTick - clientTick); _pingAverage.ComputeAverage(tickDifference); double averageInTime = (_pingAverage.Average * TickDelta * 1000); RoundTripTime = (long)Math.Round(averageInTime); _receivedPong = true; OnRoundTripTimeUpdated?.Invoke(RoundTripTime); } /// /// Sends a ping to the server. /// private void TrySendPing(uint? tickOverride = null) { byte pingInterval = PingInterval; /* How often client may send ping is based on if * the server responded to the last ping. * A response may not be received if the server * believes the client is pinging too fast, or if the * client is having difficulties reaching the server. */ long requiredTime = (pingInterval * 1000); float multiplier = (_receivedPong) ? 1f : 1.5f; requiredTime = (long)(requiredTime * multiplier); uint requiredTicks = TimeToTicks(pingInterval * multiplier); _pingTicks++; /* We cannot just consider time because ticks might run slower * from adjustments. We also cannot only consider ticks because * they might run faster from adjustments. Therefor require both * to have pass checks. */ if (_pingTicks < requiredTicks || _pingStopwatch.ElapsedMilliseconds < requiredTime) return; _pingTicks = 0; _pingStopwatch.Restart(); //Unset receivedPong, wait for new response. _receivedPong = false; uint tick = (tickOverride == null) ? LocalTick : tickOverride.Value; using (PooledWriter writer = WriterPool.GetWriter()) { writer.WritePacketId(PacketId.PingPong); writer.WriteUInt32(tick, AutoPackType.Unpacked); _networkManager.TransportManager.SendToServer((byte)Channel.Unreliable, writer.GetArraySegment()); } } /// /// Sends a pong to a client. /// internal void SendPong(NetworkConnection conn, uint clientTick) { if (!conn.IsActive || !conn.Authenticated) return; using (PooledWriter writer = WriterPool.GetWriter()) { writer.WritePacketId(PacketId.PingPong); writer.WriteUInt32(clientTick, AutoPackType.Unpacked); conn.SendToClient((byte)Channel.Unreliable, writer.GetArraySegment()); } } #endregion /// /// Increases the tick based on simulation rate. /// private void IncreaseTick() { bool isClient = _networkManager.IsClient; bool isServer = _networkManager.IsServer; double tickDelta = TickDelta; double timePerSimulation = (isServer) ? tickDelta : _adjustedTickDelta; double time = Time.unscaledDeltaTime; _elapsedTickTime += time; FrameTicked = (_elapsedTickTime >= timePerSimulation); //Number of ticks to occur this frame. int ticksCount = Mathf.FloorToInt((float)(_elapsedTickTime / timePerSimulation)); if (ticksCount > 1) _lastMultipleTicksTime = Time.unscaledDeltaTime; if (_allowTickDropping && !_networkManager.IsServer) { //If ticks require dropping. Set exactly to maximum ticks. if (ticksCount > _maximumFrameTicks) _elapsedTickTime = (timePerSimulation * (double)_maximumFrameTicks); } bool variableTiming = (_timingType == TimingType.Variable); bool frameTicked = FrameTicked; do { if (frameTicked) { _elapsedTickTime -= timePerSimulation; OnPreTick?.Invoke(); } /* This has to be called inside the loop because * OnPreTick promises data hasn't been read yet. * Therefor iterate must occur after OnPreTick. * Iteration will only run once per frame. */ if (frameTicked || variableTiming) TryIterateData(true); if (frameTicked) { OnTick?.Invoke(); if (PhysicsMode == PhysicsMode.TimeManager) { float tick = (float)TickDelta; OnPrePhysicsSimulation?.Invoke(tick); Physics.Simulate(tick); Physics2D.Simulate(tick); OnPostPhysicsSimulation?.Invoke(tick); } OnPostTick?.Invoke(); /* If isClient this is the * last tick during this loop. */ if (isClient && (_elapsedTickTime < timePerSimulation)) { _networkManager.ClientManager.SendLodUpdate(false); TrySendPing(LocalTick + 1); } if (_networkManager.IsServer) SendTimingAdjustment(); } //Send out data. if (frameTicked || variableTiming) TryIterateData(false); if (frameTicked) { if (_networkManager.IsClient) _clientTicks++; Tick++; LocalTick++; _networkManager.ObserverManager.CalculateLevelOfDetail(LocalTick); } } while (_elapsedTickTime >= timePerSimulation); } #region Tick conversions. /// /// Returns the percentage of how far the TimeManager is into the next tick. /// /// public double GetTickPercent() { if (_networkManager == null) return default; double delta = (_networkManager.IsServer) ? TickDelta : _adjustedTickDelta; double percent = (_elapsedTickTime / delta) * 100d; return percent; } /// /// Returns a PreciseTick. /// /// Tick to set within the returned PreciseTick. /// public PreciseTick GetPreciseTick(uint tick) { if (_networkManager == null) return default; double delta = (_networkManager.IsServer) ? TickDelta : _adjustedTickDelta; double percent = (_elapsedTickTime / delta) * 100; return new PreciseTick(tick, percent); } /// /// Returns a PreciseTick. /// /// Tick to use within PreciseTick. /// public PreciseTick GetPreciseTick(TickType tickType) { if (_networkManager == null) return default; if (tickType == TickType.Tick) { return GetPreciseTick(Tick); } else if (tickType == TickType.LocalTick) { return GetPreciseTick(LocalTick); } else if (tickType == TickType.LastPacketTick) { return GetPreciseTick(LastPacketTick); } else { _networkManager.LogError($"TickType {tickType.ToString()} is unhandled."); return default; } } /// /// Converts current ticks to time. /// /// TickType to compare against. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public double TicksToTime(TickType tickType = TickType.LocalTick) { if (tickType == TickType.LocalTick) { return TicksToTime(LocalTick); } else if (tickType == TickType.Tick) { return TicksToTime(Tick); } else if (tickType == TickType.LastPacketTick) { return TicksToTime(LastPacketTick); } else { _networkManager.LogError($"TickType {tickType} is unhandled."); return 0d; } } /// /// Converts a number ticks to time. /// /// Ticks to convert. /// public double TicksToTime(uint ticks) { return (TickDelta * (double)ticks); } /// /// Gets time passed from currentTick to previousTick. /// /// The current tick. /// The previous tick. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public double TimePassed(uint currentTick, uint previousTick) { double multiplier; double result; if (currentTick >= previousTick) { multiplier = 1f; result = TicksToTime(currentTick - previousTick); } else { multiplier = -1f; result = TicksToTime(previousTick - currentTick); } return (result * multiplier); } /// /// Gets time passed from Tick to preciseTick. /// /// PreciseTick value to compare against. /// True to allow negative values. When false and value would be negative 0 is returned. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public double TimePassed(PreciseTick preciseTick, bool allowNegative = false) { PreciseTick currentPt = GetPreciseTick(TickType.Tick); long tickDifference = (currentPt.Tick - preciseTick.Tick); double percentDifference = (currentPt.Percent - preciseTick.Percent); /* If tickDifference is less than 0 or tickDifference and percentDifference are 0 or less * then the result would be negative. */ bool negativeValue = (tickDifference < 0 || (tickDifference <= 0 && percentDifference <= 0)); if (!allowNegative && negativeValue) return 0d; double tickTime = TimePassed(preciseTick.Tick, true); double percent = (percentDifference / 100); double percentTime = (percent * TickDelta); return (tickTime + percentTime); } /// /// Gets time passed from Tick to previousTick. /// /// The previous tick. /// True to allow negative values. When false and value would be negative 0 is returned. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public double TimePassed(uint previousTick, bool allowNegative = false) { uint currentTick = Tick; //Difference will be positive. if (currentTick >= previousTick) { return TicksToTime(currentTick - previousTick); } //Difference would be negative. else { if (!allowNegative) { return 0d; } else { double difference = TicksToTime(previousTick - currentTick); return (difference * -1d); } } } /// /// Converts time to ticks. /// /// Time to convert. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public uint TimeToTicks(double time, TickRounding rounding = TickRounding.RoundNearest) { double result = (time / TickDelta); if (rounding == TickRounding.RoundNearest) return (uint)Math.Round(result); else if (rounding == TickRounding.RoundDown) return (uint)Math.Floor(result); else return (uint)Math.Ceiling(result); } /// /// Estimatedly converts a synchronized tick to what it would be for the local tick. /// /// Synchronized tick to convert. /// public uint TickToLocalTick(uint tick) { //Server will always have local and tick aligned. if (_networkManager.IsServer) return tick; long difference = (Tick - tick); //If no ticks have passed then return current local tick. if (difference <= 0) return LocalTick; long result = (LocalTick - difference); if (result <= 0) result = 0; return (uint)result; } /// /// Estimatedly converts a local tick to what it would be for the synchronized tick. /// /// Local tick to convert. /// public uint LocalTickToTick(uint localTick) { //Server will always have local and tick aligned. if (_networkManager.IsServer) return localTick; long difference = (LocalTick - localTick); //If no ticks have passed then return current local tick. if (difference <= 0) return Tick; long result = (Tick - difference); if (result <= 0) result = 0; return (uint)result; } #endregion /// /// Tries to iterate incoming or outgoing data. /// /// True to iterate incoming. private void TryIterateData(bool incoming) { if (incoming) { /* It's not possible for data to come in * more than once per frame but there could * be new data going out each tick, since * movement is often based off the tick system. * Because of this don't iterate incoming if * it's the same frame but the outgoing * may iterate multiple times per frame. */ int frameCount = Time.frameCount; if (frameCount == _lastIncomingIterationFrame) return; _lastIncomingIterationFrame = frameCount; _networkManager.TransportManager.IterateIncoming(true); _networkManager.TransportManager.IterateIncoming(false); } else { _networkManager.TransportManager.IterateOutgoing(true); _networkManager.TransportManager.IterateOutgoing(false); } } #region Timing adjusting. /// /// Sends a TimingUpdate packet to clients. /// private void SendTimingAdjustment() { uint requiredTicks = TimeToTicks(_timingInterval); uint tick = Tick; if (tick - _lastUpdateTicks >= requiredTicks) { //Now send using a packetId. PooledWriter writer = WriterPool.GetWriter(); writer.WritePacketId(PacketId.TimingUpdate); _networkManager.TransportManager.SendToClients((byte)Channel.Unreliable, writer.GetArraySegment()); writer.Dispose(); _lastUpdateTicks = tick; } } /// /// Called on client when server sends a timing update. /// /// internal void ParseTimingUpdate() { //Don't adjust timing on server. if (_networkManager.IsServer) return; //Add half of rtt onto tick. uint rttTicks = TimeToTicks((RoundTripTime / 2) / 1000f); Tick = LastPacketTick + rttTicks; uint expected = (uint)(TickRate * _timingInterval); long difference; //If ticking too fast. if (_clientTicks > expected) difference = (long)(_clientTicks - expected); //Not ticking fast enough. else difference = (long)((expected - _clientTicks) * -1); //If difference is unusually off then reset timings. if (Mathf.Abs(difference) >= RESET_STEPS_THRESHOLD) { _adjustedTickDelta = TickDelta; } else { sbyte steps = (sbyte)Mathf.Clamp(difference, sbyte.MinValue, sbyte.MaxValue); double percent = (steps < 0) ? CLIENT_SPEEDUP_PERCENT : CLIENT_SLOWDOWN_PERCENT; double change = (steps * (percent * TickDelta)); _adjustedTickDelta = MathFN.ClampDouble(_adjustedTickDelta + change, _clientTimingRange[0], _clientTimingRange[1]); } _clientTicks = 0; } #endregion /// /// Sets the TickRate to use. This value is not synchronized, it must be set on client and server independently. /// /// New TickRate to use. public void SetTickRate(ushort value) { TickRate = value; TickDelta = (1d / TickRate); _adjustedTickDelta = TickDelta; _clientTimingRange = new double[] { TickDelta * (1f - CLIENT_TIMING_PERCENT_RANGE), TickDelta * (1f + CLIENT_TIMING_PERCENT_RANGE) }; } #region UNITY_EDITOR private void OnValidate() { SetInitialValues(); } #endregion } }