using System;
using Unity.Profiling;
namespace Unity.Netcode
{
///
/// is a standalone system which can be used to run a network time simulation.
/// The network time system maintains both a local and a server time. The local time is based on the server time
/// as last received from the server plus an offset based on the current RTT - in other words, it is a best-guess
/// effort at predicting what the server tick will be when a given network action is processed on the server.
///
public class NetworkTimeSystem
{
///
/// This was the original comment when it lived in NetworkManager:
/// todo talk with UX/Product, find good default value for this
///
private const float k_DefaultBufferSizeSec = 0.05f;
///
/// Time synchronization frequency defaults to 1 synchronization message per second
///
private const double k_TimeSyncFrequency = 1.0d;
///
/// The threshold, in seconds, used to force a hard catchup of network time
///
private const double k_HardResetThresholdSeconds = 0.2d;
///
/// Default adjustment ratio
///
private const double k_DefaultAdjustmentRatio = 0.01d;
#if DEVELOPMENT_BUILD || UNITY_EDITOR
private static ProfilerMarker s_SyncTime = new ProfilerMarker($"{nameof(NetworkManager)}.SyncTime");
#endif
private double m_TimeSec;
private double m_CurrentLocalTimeOffset;
private double m_DesiredLocalTimeOffset;
private double m_CurrentServerTimeOffset;
private double m_DesiredServerTimeOffset;
///
/// Gets or sets the amount of time in seconds the server should buffer incoming client messages.
/// This increases the difference between local and server time so that messages arrive earlier on the server.
///
public double LocalBufferSec { get; set; }
///
/// Gets or sets the amount of the time in seconds the client should buffer incoming messages from the server. This increases server time.
/// A higher value increases latency but makes the game look more smooth in bad networking conditions.
/// This value must be higher than the tick length client side.
///
public double ServerBufferSec { get; set; }
///
/// Gets or sets a threshold in seconds used to force a hard catchup of network time.
///
public double HardResetThresholdSec { get; set; }
///
/// Gets or sets the ratio at which the NetworkTimeSystem speeds up or slows down time.
///
public double AdjustmentRatio { get; set; }
///
/// The current local time with the local time offset applied
///
public double LocalTime => m_TimeSec + m_CurrentLocalTimeOffset;
///
/// The current server time with the server time offset applied
///
public double ServerTime => m_TimeSec + m_CurrentServerTimeOffset;
internal double LastSyncedServerTimeSec { get; private set; }
internal double LastSyncedRttSec { get; private set; }
private NetworkConnectionManager m_ConnectionManager;
private NetworkTransport m_NetworkTransport;
private NetworkTickSystem m_NetworkTickSystem;
private NetworkManager m_NetworkManager;
///
///
///
private int m_TimeSyncFrequencyTicks;
///
/// The constructor class for
///
/// The amount of time, in seconds, the server should buffer incoming client messages.
/// The amount of the time in seconds the client should buffer incoming messages from the server.
/// The threshold, in seconds, used to force a hard catchup of network time.
/// The ratio at which the NetworkTimeSystem speeds up or slows down time.
public NetworkTimeSystem(double localBufferSec, double serverBufferSec = k_DefaultBufferSizeSec, double hardResetThresholdSec = k_HardResetThresholdSeconds, double adjustmentRatio = k_DefaultAdjustmentRatio)
{
LocalBufferSec = localBufferSec;
ServerBufferSec = serverBufferSec;
HardResetThresholdSec = hardResetThresholdSec;
AdjustmentRatio = adjustmentRatio;
}
///
/// The primary time system is initialized when a server-host or client is started
///
internal NetworkTickSystem Initialize(NetworkManager networkManager)
{
m_NetworkManager = networkManager;
m_ConnectionManager = networkManager.ConnectionManager;
m_NetworkTransport = networkManager.NetworkConfig.NetworkTransport;
m_TimeSyncFrequencyTicks = (int)(k_TimeSyncFrequency * networkManager.NetworkConfig.TickRate);
m_NetworkTickSystem = new NetworkTickSystem(networkManager.NetworkConfig.TickRate, 0, 0);
// Only the server side needs to register for tick based time synchronization
if (m_ConnectionManager.LocalClient.IsServer)
{
m_NetworkTickSystem.Tick += OnTickSyncTime;
}
return m_NetworkTickSystem;
}
internal void UpdateTime()
{
// As a client wait to run the time system until we are connected.
// As a client or server don't worry about the time system if we are no longer processing messages
if (!m_ConnectionManager.LocalClient.IsServer && !m_ConnectionManager.LocalClient.IsConnected)
{
return;
}
// Only update RTT here, server time is updated by time sync messages
var reset = Advance(m_NetworkManager.RealTimeProvider.UnscaledDeltaTime);
if (reset)
{
m_NetworkTickSystem.Reset(LocalTime, ServerTime);
}
m_NetworkTickSystem.UpdateTick(LocalTime, ServerTime);
if (!m_ConnectionManager.LocalClient.IsServer)
{
Sync(LastSyncedServerTimeSec + m_NetworkManager.RealTimeProvider.UnscaledDeltaTime, m_NetworkTransport.GetCurrentRtt(NetworkManager.ServerClientId) / 1000d);
}
}
///
/// Server-Side:
/// Synchronizes time with clients based on the given .
/// Also:
///
///
/// The default is to send 1 time synchronization message per second
///
private void OnTickSyncTime()
{
#if DEVELOPMENT_BUILD || UNITY_EDITOR
s_SyncTime.Begin();
#endif
// Check if we need to send a time synchronization message, and if so send it
if (m_ConnectionManager.LocalClient.IsServer && m_NetworkTickSystem.ServerTime.Tick % m_TimeSyncFrequencyTicks == 0)
{
var message = new TimeSyncMessage
{
Tick = m_NetworkTickSystem.ServerTime.Tick
};
m_ConnectionManager.SendMessage(ref message, NetworkDelivery.Unreliable, m_ConnectionManager.ConnectedClientIds);
}
#if DEVELOPMENT_BUILD || UNITY_EDITOR
s_SyncTime.End();
#endif
}
///
/// Invoke when shutting down the NetworkManager
///
internal void Shutdown()
{
if (m_ConnectionManager.LocalClient.IsServer)
{
m_NetworkTickSystem.Tick -= OnTickSyncTime;
}
}
///
/// Creates a new instance of the class for a server instance.
/// The server will not apply any buffer values which ensures that local time equals server time.
///
/// The instance.
public static NetworkTimeSystem ServerTimeSystem()
{
return new NetworkTimeSystem(0, 0, double.MaxValue);
}
///
/// Advances the time system by a certain amount of time. Should be called once per frame with Time.unscaledDeltaTime or similar.
///
/// The amount of time to advance. The delta time which passed since Advance was last called.
///
public bool Advance(double deltaTimeSec)
{
m_TimeSec += deltaTimeSec;
if (Math.Abs(m_DesiredLocalTimeOffset - m_CurrentLocalTimeOffset) > HardResetThresholdSec || Math.Abs(m_DesiredServerTimeOffset - m_CurrentServerTimeOffset) > HardResetThresholdSec)
{
m_TimeSec += m_DesiredServerTimeOffset;
m_DesiredLocalTimeOffset -= m_DesiredServerTimeOffset;
m_CurrentLocalTimeOffset = m_DesiredLocalTimeOffset;
m_DesiredServerTimeOffset = 0;
m_CurrentServerTimeOffset = 0;
return true;
}
m_CurrentLocalTimeOffset += deltaTimeSec * (m_DesiredLocalTimeOffset > m_CurrentLocalTimeOffset ? AdjustmentRatio : -AdjustmentRatio);
m_CurrentServerTimeOffset += deltaTimeSec * (m_DesiredServerTimeOffset > m_CurrentServerTimeOffset ? AdjustmentRatio : -AdjustmentRatio);
return false;
}
///
/// Resets the time system to a time based on the given network parameters.
///
/// The most recent server time value received in seconds.
/// The current RTT in seconds. Can be an averaged or a raw value.
public void Reset(double serverTimeSec, double rttSec)
{
Sync(serverTimeSec, rttSec);
Advance(0);
}
///
/// Synchronizes the time system with up-to-date network statistics but does not change any time values or advance the time.
///
/// The most recent server time value received in seconds.
/// The current RTT in seconds. Can be an averaged or a raw value.
public void Sync(double serverTimeSec, double rttSec)
{
LastSyncedRttSec = rttSec;
LastSyncedServerTimeSec = serverTimeSec;
var timeDif = serverTimeSec - m_TimeSec;
m_DesiredServerTimeOffset = timeDif - ServerBufferSec;
m_DesiredLocalTimeOffset = timeDif + rttSec + LocalBufferSec;
}
}
}