using FishNet.Connection; using FishNet.Serializing; using FishNet.Transporting; using GameKit.Dependencies.Utilities; using System; using System.Runtime.CompilerServices; using UnityEngine; using SystemStopwatch = System.Diagnostics.Stopwatch; 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 } /// /// When OnUpdate is performed. /// private enum UpdateOrder : byte { BeforeTick = 0, AfterTick = 1, } #endregion #region Public. /// /// NetworkManager used with this. /// public NetworkManager NetworkManager { get; private set; } /// /// 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; /// /// How many ticks must pass to update timing. /// internal uint TimingTickInterval => _tickRate; /// /// RoundTripTime in milliseconds. This value includes latency from the tick rate. /// public long RoundTripTime { get; private set; } /// /// Returns half value of RoundTripTime rounded to nearest whole. /// public long HalfRoundTripTime => (long)Math.Round((double)RoundTripTime / 2d); /// /// 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 EstimatedTick LastPacketTick { get; internal set; } = new(); /// /// 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 = true; /// /// 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 = 3; /// /// /// [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. /// public byte PingInterval => _pingInterval; /// /// /// [Tooltip("How to perform physics.")] [SerializeField] private PhysicsMode _physicsMode = PhysicsMode.Unity; /// /// How to perform physics. /// public PhysicsMode PhysicsMode => _physicsMode; #endregion #region Private. /// /// /// 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.IsServerStarted) ? Tick : _localTick; private set => _localTick = value; } /// /// Stopwatch used for pings. /// private SystemStopwatch _pingStopwatch = new(); /// /// Ticks passed since last ping. /// private uint _pingTicks; /// /// MovingAverage instance used to calculate mean ping. /// private MovingAverage _pingAverage = new(5); /// /// Accumulating frame time to determine when to increase tick. /// private double _elapsedTickTime; /// /// Internal deltaTime for clients. Controlled by the server. /// private double _adjustedTickDelta; /// /// 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; /// /// True if FixedUpdate called this frame and using Unity physics mode. /// private bool _fixedUpdateTimeStep; /// /// /// private float _physicsTimeScale = 1f; /// /// Gets the current physics time scale. /// /// public float GetPhysicsTimeScale() => _physicsTimeScale; /// /// Sets the physics time scale. /// This is not automatically synchronized. /// /// New value. public void SetPhysicsTimeScale(float value) { value = Mathf.Clamp(value, 0f, float.PositiveInfinity); _physicsTimeScale = value; } #endregion #region Const. /// /// Value for a tick that is invalid. /// public const uint UNSET_TICK = 0; /// /// 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) { /* If fixedUpdateTimeStep then that means * FixedUpdate already called for this frame, which * means a post physics should also be called. * This can only happen if a FixedUpdate occurs * multiple times per frame. */ if (_fixedUpdateTimeStep) OnPostPhysicsSimulation?.Invoke(Time.fixedDeltaTime); _fixedUpdateTimeStep = true; OnPrePhysicsSimulation?.Invoke(Time.fixedDeltaTime); } } /// /// Called when Update ticks. This is called before any other script. /// internal void TickUpdate() { if (NetworkManager.IsServerStarted) ServerUptime += Time.deltaTime; if (NetworkManager.IsClientStarted) 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 && _fixedUpdateTimeStep) { _fixedUpdateTimeStep = false; 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; LastPacketTick.Initialize(networkManager.TimeManager); 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) { if (!NetworkManager.IsServerStarted) LastPacketTick.ResetTicks(); _pingStopwatch.Stop(); ClientUptime = 0f; //Only reset ticks if also not server. if (!NetworkManager.IsServerStarted) { LocalTick = 0; Tick = 0; SetTickRate(TickRate); } } //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.IsAnyServerStarted()) { LastPacketTick.ResetTicks(); 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() { SetAutomaticPhysicsSimulation(true); float simulationTime = PlayerPrefs.GetFloat(SAVED_FIXED_TIME_TEXT, float.MinValue); if (simulationTime != float.MinValue) Time.fixedDeltaTime = simulationTime; } /// /// Sets automatic physics simulation mode. /// /// private void SetAutomaticPhysicsSimulation(bool automatic) { #if UNITY_2022_1_OR_NEWER if (automatic) { Physics.simulationMode = SimulationMode.FixedUpdate; Physics2D.simulationMode = SimulationMode2D.FixedUpdate; } else { Physics.simulationMode = SimulationMode.Script; Physics2D.simulationMode = SimulationMode2D.Script; } #else Physics.autoSimulation = automatic; if (automatic) Physics2D.simulationMode = SimulationMode2D.FixedUpdate; else Physics2D.simulationMode = SimulationMode2D.Script; #endif } /// /// 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) SetAutomaticPhysicsSimulation(false); //Automatically simulate. else SetAutomaticPhysicsSimulation(true); } #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; PooledWriter writer = WriterPool.Retrieve(); writer.WritePacketIdUnpacked(PacketId.PingPong); writer.WriteTickUnpacked(tick); NetworkManager.TransportManager.SendToServer((byte)Channel.Unreliable, writer.GetArraySegment()); writer.Store(); } /// /// Sends a pong to a client. /// internal void SendPong(NetworkConnection conn, uint clientTick) { if (!conn.IsActive || !conn.IsAuthenticated) return; PooledWriter writer = WriterPool.Retrieve(); writer.WritePacketIdUnpacked(PacketId.PingPong); writer.WriteTickUnpacked(clientTick); conn.SendToClient((byte)Channel.Unreliable, writer.GetArraySegment()); writer.Store(); } #endregion /// /// Increases the tick based on simulation rate. /// private void IncreaseTick() { bool isClient = NetworkManager.IsClientStarted; bool isServer = NetworkManager.IsServerStarted; double timePerSimulation = (isServer) ? TickDelta : _adjustedTickDelta; if (timePerSimulation == 0d) { NetworkManagerExtensions.LogWarning($"Simulation delta cannot be 0. Network timing will not continue."); return; } 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) { //If ticks require dropping. Set exactly to maximum ticks. if (ticksCount > _maximumFrameTicks) _elapsedTickTime = (timePerSimulation * (double)_maximumFrameTicks); } bool variableTiming = (_timingType == TimingType.Variable); bool frameTicked = FrameTicked; float tickDelta = ((float)TickDelta * GetPhysicsTimeScale()); do { if (frameTicked) 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) { //Tell predicted objecs to reconcile before OnTick. NetworkManager.PredictionManager.ReconcileToStates(); OnTick?.Invoke(); if (PhysicsMode == PhysicsMode.TimeManager && tickDelta > 0f) { OnPrePhysicsSimulation?.Invoke(tickDelta); Physics.Simulate(tickDelta); Physics2D.Simulate(tickDelta); OnPostPhysicsSimulation?.Invoke(tickDelta); } OnPostTick?.Invoke(); //After post tick send states. NetworkManager.PredictionManager.SendStateUpdate(); /* If isClient this is the * last tick during this loop. */ bool lastTick = (_elapsedTickTime < (timePerSimulation * 2d)); if (isClient && lastTick) TrySendPing(LocalTick + 1); if (NetworkManager.IsServerStarted) SendTimingAdjustment(); } //Send out data. if (frameTicked || variableTiming) TryIterateData(false); if (frameTicked) { _elapsedTickTime -= timePerSimulation; Tick++; LocalTick++; } } while (_elapsedTickTime >= timePerSimulation); } #region Tick conversions. /// /// Returns the percentage of how far the TimeManager is into the next tick as a double. /// Value will return between 0d and 1d. /// /// public double GetTickPercentAsDouble() { if (NetworkManager == null) return 0d; double percent = (_elapsedTickTime / TickDelta); return percent; } /// /// Returns the current elapsed amount for the next tick. /// /// public double GetTickElapsedAsDouble() => _elapsedTickTime; /// /// Returns the percentage of how far the TimeManager is into the next tick. /// Value will return between 0 and 100. /// public byte GetTickPercentAsByte() { double result = GetTickPercentAsDouble(); return (byte)(result * 100d); } /// /// Converts a 0 to 100 byte value to a 0d to 1d percent value. /// This does not check for excessive byte values, such as anything over 100. /// public static double GetTickPercentAsDouble(byte value) { return (value / 100d); } /// /// Returns a PreciseTick. /// /// Tick to set within the returned PreciseTick. /// public PreciseTick GetPreciseTick(uint tick) { if (NetworkManager == null) return default; double delta = (NetworkManager.IsServerStarted) ? TickDelta : _adjustedTickDelta; double percent = (_elapsedTickTime / delta); return new(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.LastRemoteTick); } else { NetworkManager.LogError($"TickType {tickType.ToString()} is unhandled."); return default; } } /// /// Converts current ticks to time. /// /// TickType to compare against. /// 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.LastRemoteTick); } else { NetworkManager.LogError($"TickType {tickType} is unhandled."); return 0d; } } /// /// Converts a PreciseTick to time. /// /// PreciseTick to convert. /// public double TicksToTime(PreciseTick pt) { double tickTime = TicksToTime(pt.Tick); double percentTime = (pt.PercentAsDouble * TickDelta); return (tickTime + percentTime); } /// /// 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. /// 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. /// public double TimePassed(PreciseTick preciseTick, bool allowNegative = false) { PreciseTick currentPt = GetPreciseTick(TickType.Tick); long tickDifference = ((long)currentPt.Tick - (long)preciseTick.Tick); double percentDifference = (currentPt.PercentAsDouble - preciseTick.PercentAsDouble); /* 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 percentTime = (percentDifference * 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. /// 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 as decimal. /// 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); } /// /// Converts time to ticks. /// /// Time to convert as whole (milliseconds) /// public uint TimeToTicks(long time, TickRounding rounding = TickRounding.RoundNearest) { double dTime = ((double)time / 1000d); return TimeToTicks(dTime, rounding); } /// /// Converts time to a PreciseTick. /// /// Time to convert. /// public PreciseTick TimeToPreciseTick(double time) => time.AsPreciseTick(TickDelta); /// /// 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.IsServerStarted) return tick; long difference = (Tick - tick); 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.IsServerStarted) return localTick; long difference = (LocalTick - localTick); 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 due to * there possibly being multiple ticks per frame. */ int frameCount = Time.frameCount; if (frameCount == _lastIncomingIterationFrame) return; _lastIncomingIterationFrame = frameCount; NetworkManager.TransportManager.IterateIncoming(asServer: true); NetworkManager.TransportManager.IterateIncoming(asServer: false); } else { NetworkManager.TransportManager.IterateOutgoing(asServer: true); NetworkManager.TransportManager.IterateOutgoing(asServer: false); } } #region Timing adjusting. /// /// Changes the adjustedTickDelta, increasing or decreasing it. /// /// Amount to multiply expected change by. This can be used to make larger or smaller changes. internal void ChangeAdjustedTickDelta(bool speedUp, double additionalMultiplier = 1d) { double share = (TickDelta * 0.01d) * additionalMultiplier; if (speedUp) _adjustedTickDelta -= share; else _adjustedTickDelta += share; } /// /// Sends a TimingUpdate packet to clients. /// private void SendTimingAdjustment() { //Send every second. if (LocalTick % TimingTickInterval == 0) { //Now send using a packetId. PooledWriter writer = WriterPool.Retrieve(); foreach (NetworkConnection item in NetworkManager.ServerManager.Clients.Values) { if (!item.IsAuthenticated) continue; writer.WritePacketIdUnpacked(PacketId.TimingUpdate); writer.WriteTickUnpacked(item.PacketTick.Value()); item.SendToClient((byte)Channel.Unreliable, writer.GetArraySegment()); writer.Clear(); } writer.Store(); } } /// /// Called on client when server sends a timing update. /// /// internal void ParseTimingUpdate(Reader reader) { uint clientTick = reader.ReadTickUnpacked(); //Don't adjust timing on server. if (NetworkManager.IsServerStarted) return; /* This should never be possible since the server is sending a tick back * that the client previously sent. In other words, the value returned should * always be in the past. */ if (LocalTick < clientTick) return; /* Use the last ordered remote tick rather than * lastPacketTick. This will help with out of order * packets where the timing update sent before * the remote tick but arrived after. By using ordered * remote tick we are comparing against however many * ticks really passed rather than the difference * between the out of order/late packet. */ uint lastPacketTick = LastPacketTick.RemoteTick; //Set Tick based on difference between localTick and clientTick, added onto lastPacketTick. uint prevTick = Tick; //Added ticks for delay in reading packet. const uint socketReadDelay = 1; uint nextTick = ((LocalTick - clientTick) / 2) + lastPacketTick + socketReadDelay; long difference = ((long)nextTick - (long)prevTick); Tick = nextTick; //Maximum difference allowed before resetting values. const int maximumDifference = 4; //Difference is extreme, reset to default timings. Client probably had an issue. if (Mathf.Abs(difference) > maximumDifference) { _adjustedTickDelta = TickDelta; } //Otherwise adjust the delta marginally. else if (difference != 0) { /* A negative tickDifference indicates the client is * moving too fast, while positive indicates too slow. */ bool speedUp = (difference > 0); ChangeAdjustedTickDelta(speedUp); } } #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; } #region UNITY_EDITOR private void OnValidate() { SetInitialValues(); } #endregion } }