using FishNet.Connection;
using FishNet.Documenting;
using FishNet.Managing.Transporting;
using FishNet.Object;
using FishNet.Serializing;
using FishNet.Serializing.Helping;
using FishNet.Transporting;
using FishNet.Utility;
using FishNet.Utility.Extension;
using GameKit.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
}
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; private set; }
///
/// Last packet tick which did not arrive out of order.
///
internal uint LastOrderedPacketTick;
///
/// Sets LastPacketTick and LastOrderedPacketTick.
///
///
internal void SetLastPacketTick(uint tick)
{
if (tick > LastPacketTick)
LastOrderedPacketTick = tick;
LastPacketTick = tick;
}
///
/// 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.
///
public 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;
///
/// Number of times the client had sent too fast in a row.
///
private float _timingTooFastCount;
///
/// True if FixedUpdate called this frame and using Unity physics mode.
///
private bool _fixedUpdateTimeStep;
#endregion
#region Const.
///
/// How often to send timing updates to clients.
///
internal const float TIMING_INTERVAL = 1f;
///
/// Value for a tick that is invalid.
///
public const uint UNSET_TICK = 0;
///
/// Maximum percentage timing may vary from TickDelta 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_VALUE = 0.035d;
///
/// Percentage of TickDelta client will adjust when needing to slow down.
///
private const double CLIENT_SLOWDOWN_VALUE = 0.02d;
///
/// When steps to be sent to clients are equal to or higher than this value in either direction a reset steps will be sent.
///
internal byte RESET_ADJUSTMENT_THRESHOLD => (byte)Mathf.Max(3, TickRate / 3);
///
/// 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.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 && _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;
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;
SetTickRate(TickRate);
_timingTooFastCount = 0f;
}
}
//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()
{
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;
}
#elif UNITY_2020_1_OR_NEWER
Physics.autoSimulation = automatic;
if (automatic)
Physics2D.simulationMode = SimulationMode2D.FixedUpdate;
else
Physics2D.simulationMode = SimulationMode2D.Script;
#elif UNITY_2019_1_OR_NEWER
Physics.autoSimulation = automatic;
Physics2D.autoSimulation = automatic;
#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.WritePacketId(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.Authenticated)
return;
PooledWriter writer = WriterPool.Retrieve();
writer.WritePacketId(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.IsClient;
bool isServer = _networkManager.IsServer;
double tickDelta = TickDelta;
double timePerSimulation = (isServer) ? tickDelta : _adjustedTickDelta;
if (timePerSimulation == 0d)
{
Debug.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 && !_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.TrySendLodUpdate(LocalTick, 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()
{
double percent = (_elapsedTickTime / TickDelta) * 100d;
return percent;
}
///
/// Returns a PreciseTick.
///
/// Tick to set within the returned PreciseTick.
///
public PreciseTick GetPreciseTick(uint tick)
{
double percent = (_elapsedTickTime / TickDelta) * 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 PreciseTick to time.
///
/// PreciseTick to convert.
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public double TicksToTime(PreciseTick pt)
{
double tickTime = TicksToTime(pt.Tick);
double percentTime = ((pt.Percent / 100) * 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.
///
[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(TIMING_INTERVAL);
uint tick = Tick;
if (tick - _lastUpdateTicks >= requiredTicks)
{
//Now send using a packetId.
PooledWriter writer = WriterPool.Retrieve();
foreach (NetworkConnection item in _networkManager.ServerManager.Clients.Values)
{
if (!item.Authenticated)
continue;
writer.Reset();
writer.WritePacketId(PacketId.TimingUpdate);
//Write the highest number of replicates the client had for the latest tick.
ushort highestQueueCount = item.GetAndResetAverageQueueCount();
writer.WriteUInt16(highestQueueCount);
item.SendToClient((byte)Channel.Unreliable, writer.GetArraySegment());
}
//writer.WritePacketId(PacketId.TimingUpdate);
//_networkManager.TransportManager.SendToClients((byte)Channel.Unreliable, writer.GetArraySegment());
writer.Store();
_lastUpdateTicks = tick;
}
}
private enum TimingUpdateChange : int
{
JustRight = 0,
TooFast = 1,
TooSlow = -1,
}
private TimingUpdateChange _timingUpdateChange = TimingUpdateChange.JustRight;
private float _updateChangeMultiplier = 1f;
///
/// Called on client when server sends a timing update.
///
///
internal void ParseTimingUpdate(PooledReader reader)
{
ushort targetQueuedInputs = _networkManager.PredictionManager.QueuedInputs;
/* The amount of inputs which are over or below
* the targeted queued inputs. If over target the
* difference will be positive. Negative values
* are ignored because the client may not send
* inputs if idle but values over the target
* need to slow down the client. */
ushort queuedInputs = reader.ReadUInt16();
//Don't adjust timing on server.
if (_networkManager.IsServer)
return;
UpdateTick();
//If over target set to overage. Otherwise set to 0.
ushort inputsOverTargetQueued = (queuedInputs > targetQueuedInputs) ? (ushort)(queuedInputs - targetQueuedInputs) : (ushort)0;
//Number of ticks expected for the tick rate.
uint expectedClientTicks = (uint)(TickRate * TIMING_INTERVAL);
//Ticks iterated since last update.
uint clientTicks = _clientTicks;
//Reset client ticks.
_clientTicks = 0;
/* Multiplier to apply towards tickrate to
* adjust for misaligned timing. */
double adjustment;
/* Number of ticks which exceed expected
* ticks. If positive the client is sending too fast,
* if negative too slow. If the value is 0, juuuust right. */
long tickDifference;
/* If queuedInputDifference is 0 then no replicates were
* performed for the tick or there were not enough to meet
* the target queue count. If that is the case then the client
* could not be sending replicates, such as idle, or not
* sending fast enough. We do not really know unless we sent
* packets every tick to keep track but that's a bit wasteful.
* When this occurs calculate based off local ticks vs expected. */
if (inputsOverTargetQueued == 0)
{
/* If no replicates were in queue at the time of the update
* then base timing on local ticks. This can happen due to the
* idle/not replicating mentioned above. */
if (queuedInputs == 0)
tickDifference = ((long)clientTicks - (long)expectedClientTicks);
//If there were queued inputs then assume the client is behind target queue.
else
tickDifference = -(targetQueuedInputs - queuedInputs);
}
//If the server confirmed client is sending too fast.
else
{
tickDifference = inputsOverTargetQueued;
}
TimingUpdateChange timingUpdateChange;
if (tickDifference == 0)
timingUpdateChange = TimingUpdateChange.JustRight;
else if (tickDifference > 0)
timingUpdateChange = TimingUpdateChange.TooFast;
else
timingUpdateChange = TimingUpdateChange.TooSlow;
const float updateChangeModifier = 0.1f;
if (timingUpdateChange != _timingUpdateChange)
{
if (_updateChangeMultiplier > updateChangeModifier)
_updateChangeMultiplier -= updateChangeModifier;
}
else
{
if (_updateChangeMultiplier < 1)
_updateChangeMultiplier += (updateChangeModifier * 0.25f);
}
_timingUpdateChange = timingUpdateChange;
float newTickDifference = ((float)tickDifference * _updateChangeMultiplier);
tickDifference = (int)newTickDifference;
//Debug.Log($"ChangeMultiplier {_updateChangeMultiplier}. ClientTicks {clientTicks}. Queued {queuedInputs}. Over target queued {inputsOverTargetQueued}. Difference {tickDifference}. TooFastCount {_timingTooFastCount}.");
//If over the reset limitation then set difference to 0 forcing use of normal tickdelta.
if (Mathf.Abs(tickDifference) >= RESET_ADJUSTMENT_THRESHOLD)
tickDifference = 0;
double multiplierValue = (tickDifference > 0) ? CLIENT_SLOWDOWN_VALUE : CLIENT_SPEEDUP_VALUE;
adjustment = TickDelta * ((double)tickDifference * multiplierValue);
//Set adjustedTickValue to contain adjustment.
_adjustedTickDelta = TickDelta + adjustment;
/* If client was sending too fast last update
* then add more slowdown to the adjusted delta based on
* number of times client was too fast. */
_adjustedTickDelta += (TickDelta * (CLIENT_SLOWDOWN_VALUE * _timingTooFastCount));
//Lerp between new and old adjusted value to blend them so the change isn't sudden.
//Clamp adjusted tick delta so it cannot be unreasonably fast or slow.
_adjustedTickDelta = Maths.ClampDouble(_adjustedTickDelta, _clientTimingRange[0], _clientTimingRange[1]);
const float tooFastModifier = 0.5f;
//Increase too fast count if needed.
if (tickDifference > 0)
_timingTooFastCount += tooFastModifier;
//Otherwise reduce it by 1 but not below 0.
else if (_timingTooFastCount >= tooFastModifier)
_timingTooFastCount -= tooFastModifier;
else
_timingTooFastCount = 0f;
//Updates synchronized tick.
void UpdateTick()
{
uint rttTicks = TimeToTicks((RoundTripTime / 2) / 1000f);
Tick = LastPacketTick + rttTicks;
}
}
#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
}
}