using FishNet.Connection; using FishNet.Documenting; using FishNet.Managing.Logging; 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. //public enum BufferPurgeType //{ // /// // /// Run an additional input per tick when buffered inputs are higher than normal. // /// This prevents clients from sending excessive inputs but may briefly disrupt clients synchronization if their timing is drastically off. // /// Use this option for more secure prediction. // /// // Discard = 0, // /// // /// Run an additional input per tick when buffered inputs are higher than normal. // /// This is useful for keeping the client synchronized with the server by processing inputs that would normally be discarded. // /// However, by running extra buffered inputs the client has a better opportunity to cheat. // /// // Run = 1 //} //#endregion #region Public. /// /// Called before performing a reconcile on NetworkBehaviour. /// public event Action OnPreReconcile; /// /// Called after performing a reconcile on a NetworkBehaviour. /// public event Action OnPostReconcile; /// /// Called before physics is simulated when replaying a replicate method. /// Contains the PhysicsScene and PhysicsScene2D which was simulated. /// public event Action OnPreReplicateReplay; /// /// Called after physics is simulated when replaying a replicate method. /// Contains the PhysicsScene and PhysicsScene2D which was simulated. /// public event Action OnPostReplicateReplay; /// /// 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 OnPhysicsSimulation; /// /// 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. /// 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 - _lastMultipleTicks) < 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; } /// /// Percentage as 0-100 of how much into next tick the time is. /// [Obsolete("Use GetPreciseTick or GetTickPercent instead.")] //Remove on 2023/01/01 public byte TickPercent { get { if (_networkManager == null) return 0; double delta = (_networkManager.IsServer) ? TickDelta : _adjustedTickDelta; double percent = (_elapsedTickTime / delta) * 100; return (byte)Mathf.Clamp((float)percent, 0, 100); } } /// /// 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; } /// /// /// private bool _isReplaying; /// /// Returns if any prediction is replaying. /// /// public bool IsReplaying() => _isReplaying; /// /// Returns if scene is replaying. /// /// /// public bool IsReplaying(UnityScene scene) => _replayingScenes.Contains(scene); /// /// True if any predictions are replaying. /// #endregion #region Serialized. /// /// /// [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; /// /// /// [Tooltip("Maximum number of buffered inputs which will be accepted from client before old inputs are discarded.")] [Range(1, 100)] [SerializeField] private byte _maximumBufferedInputs = 15; /// /// Maximum number of buffered inputs which will be accepted from client before old inputs are discarded. /// public byte MaximumBufferedInputs => _maximumBufferedInputs; #endregion #region Private. /// /// Scenes which are currently replaying prediction. /// private HashSet _replayingScenes = new HashSet(new SceneHandleEqualityComparer()); /// /// 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); /// /// Time elapsed after ticks. This is extra time beyond the simulation rate. /// 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 _lastMultipleTicks; /// /// 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 private void OnEnable() { UnityEngine.SceneManagement.SceneManager.sceneUnloaded += SceneManager_sceneUnloaded; } #if UNITY_EDITOR private void OnDisable() { UnityEngine.SceneManagement.SceneManager.sceneUnloaded -= SceneManager_sceneUnloaded; //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) OnPhysicsSimulation?.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; IncreaseTick(); OnUpdate?.Invoke(); } /// /// Called when LateUpdate ticks. This is called after all other scripts. /// internal void TickLateUpdate() { OnLateUpdate?.Invoke(); } /// /// Initializes this script for use. /// internal void InitializeOnceInternal(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 when a scene unloads. /// /// private void SceneManager_sceneUnloaded(UnityScene s) { _replayingScenes.Remove(s); } /// /// Called after the local client connection state changes. /// private void ClientManager_OnClientConnectionState(ClientConnectionStateArgs obj) { if (obj.ConnectionState != LocalConnectionState.Started) { _replayingScenes.Clear(); _pingStopwatch.Stop(); ClientUptime = 0f; LocalTick = 0; //Also reset Tick if not running as host. if (!_networkManager.IsServer) 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; } } /// /// Invokes OnPre/PostReconcile events. /// Internal use. /// [APIExclude] [CodegenMakePublic] //To internal. public void InvokeOnReconcile(NetworkBehaviour nb, bool before) { nb.IsReconciling = before; if (before) OnPreReconcile?.Invoke(nb); else OnPostReconcile?.Invoke(nb); } /// /// Invokes OnReplicateReplay. /// Internal use. /// [APIExclude] [CodegenMakePublic] //To internal. public void InvokeOnReplicateReplay(UnityScene scene, PhysicsScene ps, PhysicsScene2D ps2d, bool before) { _isReplaying = before; if (before) { _replayingScenes.Add(scene); OnPreReplicateReplay?.Invoke(ps, ps2d); } else { _replayingScenes.Remove(scene); OnPostReplicateReplay?.Invoke(ps, ps2d); } } /// /// 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) { if (_networkManager.CanLog(LoggingType.Error)) Debug.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) { //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; } /// /// 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.WriteUInt16((ushort)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.WriteUInt16((ushort)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; double timePerSimulation = (_networkManager.IsServer) ? TickDelta : _adjustedTickDelta; double time = Time.unscaledDeltaTime; _elapsedTickTime += time; FrameTicked = (_elapsedTickTime >= timePerSimulation); //Multiple ticks will occur this frame. if (_elapsedTickTime > (timePerSimulation * 2d)) _lastMultipleTicks = Time.unscaledTime; while (_elapsedTickTime >= timePerSimulation) { _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. */ TryIterateData(true); OnTick?.Invoke(); if (PhysicsMode == PhysicsMode.TimeManager) { float tick = (float)TickDelta; OnPhysicsSimulation?.Invoke(tick); Physics.Simulate(tick); Physics2D.Simulate(tick); } OnPostTick?.Invoke(); /* If isClient this is the * last tick during this loop. */ if (isClient && (_elapsedTickTime < timePerSimulation)) TrySendPing(LocalTick + 1); if (_networkManager.IsServer) SendTimingAdjustment(); //Send out data. TryIterateData(false); if (_networkManager.IsClient) _clientTicks++; Tick++; LocalTick++; } } #region TicksToTime. /// /// 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 { if (_networkManager.CanLog(LoggingType.Error)) Debug.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 { if (_networkManager != null && _networkManager.CanLog(LoggingType.Error)) Debug.LogError($"TickType {tickType} is unhandled."); return 0d; } } /// /// Converts current ticks to time. /// /// True to use the LocalTick, false to use Tick. /// [Obsolete("Use TicksToTime(TickType) instead.")] [MethodImpl(MethodImplOptions.AggressiveInlining)] //Remove on 2023/01/01 public double TicksToTime(bool useLocalTick = true) { if (useLocalTick) return TicksToTime(LocalTick); else return TicksToTime(Tick); } /// /// Converts a number ticks to time. /// /// Ticks to convert. /// public double TicksToTime(uint ticks) { return (TickDelta * (double)ticks); } /// /// Converts time passed from currentTick to previous. Value will be negative if previousTick is larger than currentTick. /// /// The current tick. /// The previous tick. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public double TicksToTime(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); } } } #endregion [MethodImpl(MethodImplOptions.AggressiveInlining)] /// /// Converts time to ticks. /// /// Time to convert. /// 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); } /// /// 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 } }