1054 lines
		
	
	
		
			38 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
			
		
		
	
	
			1054 lines
		
	
	
		
			38 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
| using FishNet.Connection;
 | |
| using FishNet.Documenting;
 | |
| using FishNet.Object;
 | |
| using FishNet.Serializing;
 | |
| using FishNet.Serializing.Helping;
 | |
| using FishNet.Transporting;
 | |
| using FishNet.Utility;
 | |
| using FishNet.Utility.Extension;
 | |
| using System;
 | |
| using System.Collections.Generic;
 | |
| using System.Runtime.CompilerServices;
 | |
| using UnityEngine;
 | |
| using SystemStopwatch = System.Diagnostics.Stopwatch;
 | |
| using UnityScene = UnityEngine.SceneManagement.Scene;
 | |
| 
 | |
| namespace FishNet.Managing.Timing
 | |
| {
 | |
| 
 | |
|     /// <summary>
 | |
|     /// Provides data and actions for network time and tick based systems.
 | |
|     /// </summary>
 | |
|     [DisallowMultipleComponent]
 | |
|     [AddComponentMenu("FishNet/Manager/TimeManager")]
 | |
|     public sealed partial class TimeManager : MonoBehaviour
 | |
|     {
 | |
|         #region Types.
 | |
|         /// <summary>
 | |
|         /// How networking timing is performed.
 | |
|         /// </summary>
 | |
|         private enum TimingType
 | |
|         {
 | |
|             /// <summary>
 | |
|             /// Send and read data on tick.
 | |
|             /// </summary>
 | |
|             Tick = 0,
 | |
|             /// <summary>
 | |
|             /// Send and read data as soon as possible. This does not include built-in components, which will still run on tick.
 | |
|             /// </summary>
 | |
|             Variable = 1
 | |
|         }
 | |
|         private enum UpdateOrder : byte
 | |
|         {
 | |
|             BeforeTick = 0,
 | |
|             AfterTick = 1,
 | |
|         }
 | |
|         #endregion
 | |
| 
 | |
|         #region Public.
 | |
|         /// <summary>
 | |
|         /// Called when the local clients ping is updated.
 | |
|         /// </summary>
 | |
|         public event Action<long> OnRoundTripTimeUpdated;
 | |
|         /// <summary>
 | |
|         /// Called right before a tick occurs, as well before data is read.
 | |
|         /// </summary>
 | |
|         public event Action OnPreTick;
 | |
|         /// <summary>
 | |
|         /// Called when a tick occurs.
 | |
|         /// </summary>
 | |
|         public event Action OnTick;
 | |
|         /// <summary>
 | |
|         /// 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.
 | |
|         /// </summary>
 | |
|         public event Action<float> OnPrePhysicsSimulation;
 | |
|         /// <summary>
 | |
|         /// 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.
 | |
|         /// </summary>
 | |
|         public event Action<float> OnPostPhysicsSimulation;
 | |
|         /// <summary>
 | |
|         /// Called after a tick occurs; physics would have simulated if using PhysicsMode.TimeManager.
 | |
|         /// </summary>
 | |
|         public event Action OnPostTick;
 | |
|         /// <summary>
 | |
|         /// Called when MonoBehaviours call Update.
 | |
|         /// </summary>
 | |
|         public event Action OnUpdate;
 | |
|         /// <summary>
 | |
|         /// Called when MonoBehaviours call LateUpdate.
 | |
|         /// </summary>
 | |
|         public event Action OnLateUpdate;
 | |
|         /// <summary>
 | |
|         /// Called when MonoBehaviours call FixedUpdate.
 | |
|         /// </summary>
 | |
|         public event Action OnFixedUpdate;
 | |
|         /// <summary>
 | |
|         /// RoundTripTime in milliseconds. This value includes latency from the tick rate.
 | |
|         /// </summary>
 | |
|         public long RoundTripTime { get; private set; }
 | |
|         /// <summary>
 | |
|         /// True if the number of frames per second are less than the number of expected ticks per second.
 | |
|         /// </summary>
 | |
|         internal bool LowFrameRate => ((Time.unscaledTime - _lastMultipleTicksTime) < 1f);
 | |
|         /// <summary>
 | |
|         /// Tick on the last received packet, be it from server or client.
 | |
|         /// </summary>
 | |
|         public uint LastPacketTick { get; internal set; }
 | |
|         /// <summary>
 | |
|         /// 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.
 | |
|         /// </summary>
 | |
|         public uint Tick { get; internal set; }
 | |
|         /// <summary>
 | |
|         /// A fixed deltaTime for TickRate.
 | |
|         /// </summary>
 | |
|         [HideInInspector]
 | |
|         public double TickDelta { get; private set; }
 | |
|         /// <summary>
 | |
|         /// True if the TimeManager will or has ticked this frame.
 | |
|         /// </summary>
 | |
|         public bool FrameTicked { get; private set; }
 | |
|         /// <summary>
 | |
|         /// How long the local server has been connected.
 | |
|         /// </summary>
 | |
|         public float ServerUptime { get; private set; }
 | |
|         /// <summary>
 | |
|         /// How long the local client has been connected.
 | |
|         /// </summary>
 | |
|         public float ClientUptime { get; private set; }
 | |
|         #endregion
 | |
| 
 | |
|         #region Serialized.
 | |
|         /// <summary>
 | |
|         /// When to invoke OnUpdate and other Unity callbacks relayed by the TimeManager.
 | |
|         /// </summary>
 | |
|         [Tooltip("When to invoke OnUpdate and other Unity callbacks relayed by the TimeManager.")]
 | |
|         [SerializeField]
 | |
|         private UpdateOrder _updateOrder = UpdateOrder.BeforeTick;
 | |
|         /// <summary>
 | |
|         /// Timing for sending and receiving data.
 | |
|         /// </summary>
 | |
|         [Tooltip("Timing for sending and receiving data.")]
 | |
|         [SerializeField]
 | |
|         private TimingType _timingType = TimingType.Tick;
 | |
|         /// <summary>
 | |
|         /// 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.
 | |
|         /// </summary>
 | |
|         [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;
 | |
|         /// <summary>
 | |
|         /// Maximum number of ticks which may occur in a single frame before remainder are dropped for the frame.
 | |
|         /// </summary>
 | |
|         [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;
 | |
|         /// <summary>
 | |
|         /// 
 | |
|         /// </summary>
 | |
|         [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;
 | |
|         /// <summary>
 | |
|         /// How many times per second the server will simulate. This does not limit server frame rate.
 | |
|         /// </summary>
 | |
|         public ushort TickRate { get => _tickRate; private set => _tickRate = value; }
 | |
|         /// <summary>
 | |
|         /// 
 | |
|         /// </summary>        
 | |
|         [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;
 | |
|         /// <summary>
 | |
|         /// How often in seconds to a connections ping. This is also responsible for approximating server tick. This value does not affect prediction.
 | |
|         /// </summary>
 | |
|         internal byte PingInterval => _pingInterval;
 | |
|         /// <summary>
 | |
|         /// How often in seconds to update prediction timing. Lower values will result in marginally more accurate timings at the cost of bandwidth.
 | |
|         /// </summary>        
 | |
|         [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;
 | |
|         /// <summary>
 | |
|         /// 
 | |
|         /// </summary>
 | |
|         [Tooltip("How to perform physics.")]
 | |
|         [SerializeField]
 | |
|         private PhysicsMode _physicsMode = PhysicsMode.Unity;
 | |
|         /// <summary>
 | |
|         /// How to perform physics.
 | |
|         /// </summary>
 | |
|         public PhysicsMode PhysicsMode => _physicsMode;
 | |
|         #endregion
 | |
| 
 | |
|         #region Private.
 | |
|         /// <summary>
 | |
|         /// Ticks that have passed on client since the last time server sent an UpdateTicksBroadcast.
 | |
|         /// </summary>
 | |
|         private uint _clientTicks = 0;
 | |
|         /// <summary>
 | |
|         /// Last Tick the server sent out UpdateTicksBroadcast.
 | |
|         /// </summary>
 | |
|         private uint _lastUpdateTicks = 0;
 | |
|         /// <summary>
 | |
|         /// 
 | |
|         /// </summary>
 | |
|         private uint _localTick;
 | |
|         /// <summary>
 | |
|         /// 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.
 | |
|         /// </summary>
 | |
|         public uint LocalTick
 | |
|         {
 | |
|             get => (_networkManager.IsServer) ? Tick : _localTick;
 | |
|             private set => _localTick = value;
 | |
|         }
 | |
|         /// <summary>
 | |
|         /// Stopwatch used for pings.
 | |
|         /// </summary>
 | |
|         SystemStopwatch _pingStopwatch = new SystemStopwatch();
 | |
|         /// <summary>
 | |
|         /// Ticks passed since last ping.
 | |
|         /// </summary>
 | |
|         private uint _pingTicks;
 | |
|         /// <summary>
 | |
|         /// MovingAverage instance used to calculate mean ping.
 | |
|         /// </summary>
 | |
|         private MovingAverage _pingAverage = new MovingAverage(5);
 | |
|         /// <summary>
 | |
|         /// Accumulating frame time to determine when to increase tick.
 | |
|         /// </summary>
 | |
|         private double _elapsedTickTime;
 | |
|         /// <summary>
 | |
|         /// NetworkManager used with this.
 | |
|         /// </summary>
 | |
|         private NetworkManager _networkManager;
 | |
|         /// <summary>
 | |
|         /// Internal deltaTime for clients. Controlled by the server.
 | |
|         /// </summary>
 | |
|         private double _adjustedTickDelta;
 | |
|         /// <summary>
 | |
|         /// Range which client timing may reside within.
 | |
|         /// </summary>
 | |
|         private double[] _clientTimingRange;
 | |
|         /// <summary>
 | |
|         /// Last frame an iteration occurred for incoming.
 | |
|         /// </summary>
 | |
|         private int _lastIncomingIterationFrame = -1;
 | |
|         /// <summary>
 | |
|         /// True if client received Pong since last ping.
 | |
|         /// </summary>
 | |
|         private bool _receivedPong = true;
 | |
|         /// <summary>
 | |
|         /// Last unscaledTime multiple ticks occurred in a single frame.
 | |
|         /// </summary>
 | |
|         private float _lastMultipleTicksTime;
 | |
|         /// <summary>
 | |
|         /// Number of TimeManagers open which are using manual physics.
 | |
|         /// </summary>
 | |
|         private static uint _manualPhysics;
 | |
|         #endregion
 | |
| 
 | |
|         #region Const.
 | |
|         /// <summary>
 | |
|         /// Maximum percentage timing may vary from SimulationInterval for clients.
 | |
|         /// </summary>
 | |
|         private const float CLIENT_TIMING_PERCENT_RANGE = 0.5f;
 | |
|         /// <summary>
 | |
|         /// Percentage of TickDelta client will adjust when needing to speed up.
 | |
|         /// </summary>
 | |
|         private const double CLIENT_SPEEDUP_PERCENT = 0.003d;
 | |
|         /// <summary>
 | |
|         /// Percentage of TickDelta client will adjust when needing to slow down.
 | |
|         /// </summary>
 | |
|         private const double CLIENT_SLOWDOWN_PERCENT = 0.005d;
 | |
|         /// <summary>
 | |
|         /// When steps to be sent to clients are equal to or higher than this value in either direction a reset steps will be sent.
 | |
|         /// </summary>
 | |
|         private const byte RESET_STEPS_THRESHOLD = 5;
 | |
|         /// <summary>
 | |
|         /// Playerprefs string to load and save user fixed time.
 | |
|         /// </summary>
 | |
|         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
 | |
| 
 | |
|         /// <summary>
 | |
|         /// Called when FixedUpdate ticks. This is called before any other script.
 | |
|         /// </summary>
 | |
|         internal void TickFixedUpdate()
 | |
|         {
 | |
|             OnFixedUpdate?.Invoke();
 | |
|             /* Invoke onsimulation if using Unity time.
 | |
|              * Otherwise let the tick cycling part invoke. */
 | |
|             if (PhysicsMode == PhysicsMode.Unity)
 | |
|                 OnPrePhysicsSimulation?.Invoke(Time.fixedDeltaTime);
 | |
|         }
 | |
| 
 | |
|         /// <summary>
 | |
|         /// Called when Update ticks. This is called before any other script.
 | |
|         /// </summary>
 | |
|         internal void TickUpdate()
 | |
|         {
 | |
|             if (_networkManager.IsServer)
 | |
|                 ServerUptime += Time.deltaTime;
 | |
|             if (_networkManager.IsClient)
 | |
|                 ClientUptime += Time.deltaTime;
 | |
| 
 | |
|             bool beforeTick = (_updateOrder == UpdateOrder.BeforeTick);
 | |
|             if (beforeTick)
 | |
|             {
 | |
|                 OnUpdate?.Invoke();
 | |
|                 MethodLogic();
 | |
|             }
 | |
|             else
 | |
|             {
 | |
|                 MethodLogic();
 | |
|                 OnUpdate?.Invoke();
 | |
|             }
 | |
| 
 | |
|             void MethodLogic()
 | |
|             {
 | |
|                 IncreaseTick();
 | |
|                 /* Invoke onsimulation if using Unity time.
 | |
|                 * Otherwise let the tick cycling part invoke. */
 | |
|                 if (PhysicsMode == PhysicsMode.Unity && Time.inFixedTimeStep)
 | |
|                     OnPostPhysicsSimulation?.Invoke(Time.fixedDeltaTime);
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         /// <summary>
 | |
|         /// Called when LateUpdate ticks. This is called after all other scripts.
 | |
|         /// </summary>
 | |
|         internal void TickLateUpdate()
 | |
|         {
 | |
|             OnLateUpdate?.Invoke();
 | |
|         }
 | |
| 
 | |
| 
 | |
|         /// <summary>
 | |
|         /// Initializes this script for use.
 | |
|         /// </summary>
 | |
|         internal void InitializeOnce_Internal(NetworkManager networkManager)
 | |
|         {
 | |
|             _networkManager = networkManager;
 | |
|             SetInitialValues();
 | |
|             _networkManager.ServerManager.OnServerConnectionState += ServerManager_OnServerConnectionState;
 | |
|             _networkManager.ClientManager.OnClientConnectionState += ClientManager_OnClientConnectionState;
 | |
| 
 | |
|             AddNetworkLoops();
 | |
|         }
 | |
| 
 | |
|         /// <summary>
 | |
|         /// Adds network loops to gameObject.
 | |
|         /// </summary>
 | |
|         private void AddNetworkLoops()
 | |
|         {
 | |
|             //Writer.
 | |
|             if (!gameObject.TryGetComponent<NetworkWriterLoop>(out _))
 | |
|                 gameObject.AddComponent<NetworkWriterLoop>();
 | |
|             //Reader.
 | |
|             if (!gameObject.TryGetComponent<NetworkReaderLoop>(out _))
 | |
|                 gameObject.AddComponent<NetworkReaderLoop>();
 | |
|         }
 | |
| 
 | |
| 
 | |
|         /// <summary>
 | |
|         /// Called after the local client connection state changes.
 | |
|         /// </summary>
 | |
|         private void ClientManager_OnClientConnectionState(ClientConnectionStateArgs obj)
 | |
|         {
 | |
|             if (obj.ConnectionState != LocalConnectionState.Started)
 | |
|             {
 | |
|                 _pingStopwatch.Stop();
 | |
|                 ClientUptime = 0f;
 | |
| 
 | |
|                 //Only reset ticks if also not server.
 | |
|                 if (!_networkManager.IsServer)
 | |
|                 {
 | |
|                     LocalTick = 0;
 | |
|                     Tick = 0;
 | |
|                 }
 | |
|             }
 | |
|             //Started.
 | |
|             else
 | |
|             {
 | |
|                 _pingStopwatch.Restart();
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         /// <summary>
 | |
|         /// Called after the local server connection state changes.
 | |
|         /// </summary>
 | |
|         private void ServerManager_OnServerConnectionState(ServerConnectionStateArgs obj)
 | |
|         {
 | |
|             //If no servers are running.
 | |
|             if (!_networkManager.ServerManager.AnyServerStarted())
 | |
|             {
 | |
|                 ServerUptime = 0f;
 | |
|                 Tick = 0;
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         /// <summary>
 | |
|         /// Sets values to use based on settings.
 | |
|         /// </summary>
 | |
|         private void SetInitialValues()
 | |
|         {
 | |
|             SetTickRate(TickRate);
 | |
|             InitializePhysicsMode(PhysicsMode);
 | |
|         }
 | |
| 
 | |
|         /// <summary>
 | |
|         /// Sets simulation settings to Unity defaults.
 | |
|         /// </summary>
 | |
|         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;
 | |
|         }
 | |
| 
 | |
|         /// <summary>
 | |
|         /// Initializes physics mode when starting.
 | |
|         /// </summary>
 | |
|         /// <param name="automatic"></param>
 | |
|         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);
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         /// <summary>
 | |
|         /// Updates physics based on which physics mode to use.
 | |
|         /// </summary>
 | |
|         /// <param name="enabled"></param>
 | |
|         public void SetPhysicsMode(PhysicsMode mode)
 | |
|         {
 | |
|             _physicsMode = mode;
 | |
| 
 | |
|             //Disable.
 | |
|             if (mode == PhysicsMode.Disabled || mode == PhysicsMode.TimeManager)
 | |
|             {
 | |
|                 Physics.autoSimulation = false;
 | |
| #if !UNITY_2020_2_OR_NEWER
 | |
|                 Physics2D.autoSimulation = false;
 | |
| #else
 | |
|                 Physics2D.simulationMode = SimulationMode2D.Script;
 | |
| #endif
 | |
|             }
 | |
|             //Automatically simulate.
 | |
|             else
 | |
|             {
 | |
|                 Physics.autoSimulation = true;
 | |
| #if !UNITY_2020_2_OR_NEWER
 | |
|                 Physics2D.autoSimulation = true;
 | |
| #else
 | |
|                 Physics2D.simulationMode = SimulationMode2D.FixedUpdate;
 | |
| #endif
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         #region PingPong.
 | |
|         /// <summary>
 | |
|         /// Modifies client ping based on LocalTick and clientTIck.
 | |
|         /// </summary>
 | |
|         /// <param name="clientTick"></param>
 | |
|         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);
 | |
|         }
 | |
| 
 | |
|         /// <summary>
 | |
|         /// Sends a ping to the server.
 | |
|         /// </summary>
 | |
|         private void TrySendPing(uint? tickOverride = null)
 | |
|         {
 | |
|             byte pingInterval = PingInterval;
 | |
| 
 | |
|             /* How often client may send ping is based on if
 | |
|              * the server responded to the last ping.
 | |
|              * A response may not be received if the server
 | |
|              * believes the client is pinging too fast, or if the 
 | |
|              * client is having difficulties reaching the server. */
 | |
|             long requiredTime = (pingInterval * 1000);
 | |
|             float multiplier = (_receivedPong) ? 1f : 1.5f;
 | |
| 
 | |
|             requiredTime = (long)(requiredTime * multiplier);
 | |
|             uint requiredTicks = TimeToTicks(pingInterval * multiplier);
 | |
| 
 | |
|             _pingTicks++;
 | |
|             /* We cannot just consider time because ticks might run slower
 | |
|              * from adjustments. We also cannot only consider ticks because
 | |
|              * they might run faster from adjustments. Therefor require both
 | |
|              * to have pass checks. */
 | |
|             if (_pingTicks < requiredTicks || _pingStopwatch.ElapsedMilliseconds < requiredTime)
 | |
|                 return;
 | |
| 
 | |
|             _pingTicks = 0;
 | |
|             _pingStopwatch.Restart();
 | |
|             //Unset receivedPong, wait for new response.
 | |
|             _receivedPong = false;
 | |
| 
 | |
|             uint tick = (tickOverride == null) ? LocalTick : tickOverride.Value;
 | |
|             using (PooledWriter writer = WriterPool.GetWriter())
 | |
|             {
 | |
|                 writer.WritePacketId(PacketId.PingPong);
 | |
|                 writer.WriteUInt32(tick, AutoPackType.Unpacked);
 | |
|                 _networkManager.TransportManager.SendToServer((byte)Channel.Unreliable, writer.GetArraySegment());
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         /// <summary>
 | |
|         /// Sends a pong to a client.
 | |
|         /// </summary>
 | |
|         internal void SendPong(NetworkConnection conn, uint clientTick)
 | |
|         {
 | |
|             if (!conn.IsActive || !conn.Authenticated)
 | |
|                 return;
 | |
| 
 | |
|             using (PooledWriter writer = WriterPool.GetWriter())
 | |
|             {
 | |
|                 writer.WritePacketId(PacketId.PingPong);
 | |
|                 writer.WriteUInt32(clientTick, AutoPackType.Unpacked);
 | |
|                 conn.SendToClient((byte)Channel.Unreliable, writer.GetArraySegment());
 | |
|             }
 | |
|         }
 | |
|         #endregion
 | |
| 
 | |
|         /// <summary>
 | |
|         /// Increases the tick based on simulation rate.
 | |
|         /// </summary>
 | |
|         private void IncreaseTick()
 | |
|         {
 | |
|             bool isClient = _networkManager.IsClient;
 | |
|             bool isServer = _networkManager.IsServer;
 | |
| 
 | |
|             double tickDelta = TickDelta;
 | |
|             double timePerSimulation = (isServer) ? tickDelta : _adjustedTickDelta;
 | |
|             double time = Time.unscaledDeltaTime;
 | |
|             _elapsedTickTime += time;
 | |
|             FrameTicked = (_elapsedTickTime >= timePerSimulation);
 | |
| 
 | |
|             //Number of ticks to occur this frame.
 | |
|             int ticksCount = Mathf.FloorToInt((float)(_elapsedTickTime / timePerSimulation));
 | |
|             if (ticksCount > 1)
 | |
|                 _lastMultipleTicksTime = Time.unscaledDeltaTime;
 | |
| 
 | |
|             if (_allowTickDropping && !_networkManager.IsServer)
 | |
|             {
 | |
|                 //If ticks require dropping. Set exactly to maximum ticks.
 | |
|                 if (ticksCount > _maximumFrameTicks)
 | |
|                     _elapsedTickTime = (timePerSimulation * (double)_maximumFrameTicks);
 | |
|             }
 | |
| 
 | |
|             bool variableTiming = (_timingType == TimingType.Variable);
 | |
|             bool frameTicked = FrameTicked;
 | |
| 
 | |
|             do
 | |
|             {
 | |
|                 if (frameTicked)
 | |
|                 {
 | |
|                     _elapsedTickTime -= timePerSimulation;
 | |
|                     OnPreTick?.Invoke();
 | |
|                 }
 | |
| 
 | |
|                 /* This has to be called inside the loop because
 | |
|                  * OnPreTick promises data hasn't been read yet.
 | |
|                  * Therefor iterate must occur after OnPreTick.
 | |
|                  * Iteration will only run once per frame. */
 | |
|                 if (frameTicked || variableTiming)
 | |
|                     TryIterateData(true);
 | |
| 
 | |
|                 if (frameTicked)
 | |
|                 {
 | |
|                     OnTick?.Invoke();
 | |
| 
 | |
|                     if (PhysicsMode == PhysicsMode.TimeManager)
 | |
|                     {
 | |
|                         float tick = (float)TickDelta;
 | |
|                         OnPrePhysicsSimulation?.Invoke(tick);
 | |
|                         Physics.Simulate(tick);
 | |
|                         Physics2D.Simulate(tick);
 | |
|                         OnPostPhysicsSimulation?.Invoke(tick);
 | |
|                     }
 | |
| 
 | |
|                     OnPostTick?.Invoke();
 | |
|                     /* If isClient this is the
 | |
|                      * last tick during this loop. */
 | |
|                     if (isClient && (_elapsedTickTime < timePerSimulation))
 | |
|                     {
 | |
|                         _networkManager.ClientManager.SendLodUpdate(false);
 | |
|                         TrySendPing(LocalTick + 1);
 | |
|                     }
 | |
| 
 | |
|                     if (_networkManager.IsServer)
 | |
|                         SendTimingAdjustment();
 | |
|                 }
 | |
| 
 | |
|                 //Send out data.
 | |
|                 if (frameTicked || variableTiming)
 | |
|                     TryIterateData(false);
 | |
| 
 | |
|                 if (frameTicked)
 | |
|                 {
 | |
|                     if (_networkManager.IsClient)
 | |
|                         _clientTicks++;
 | |
| 
 | |
|                     Tick++;
 | |
|                     LocalTick++;
 | |
| 
 | |
|                     _networkManager.ObserverManager.CalculateLevelOfDetail(LocalTick);
 | |
|                 }
 | |
| 
 | |
|             } while (_elapsedTickTime >= timePerSimulation);
 | |
|         }
 | |
| 
 | |
| 
 | |
| 
 | |
|         #region Tick conversions.
 | |
|         /// <summary>
 | |
|         /// Returns the percentage of how far the TimeManager is into the next tick.
 | |
|         /// </summary>
 | |
|         /// <returns></returns>
 | |
|         public double GetTickPercent()
 | |
|         {
 | |
|             if (_networkManager == null)
 | |
|                 return default;
 | |
| 
 | |
|             double delta = (_networkManager.IsServer) ? TickDelta : _adjustedTickDelta;
 | |
|             double percent = (_elapsedTickTime / delta) * 100d;
 | |
|             return percent;
 | |
|         }
 | |
|         /// <summary>
 | |
|         /// Returns a PreciseTick.
 | |
|         /// </summary>
 | |
|         /// <param name="tick">Tick to set within the returned PreciseTick.</param>
 | |
|         /// <returns></returns>
 | |
|         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);
 | |
|         }
 | |
|         /// <summary>
 | |
|         /// Returns a PreciseTick.
 | |
|         /// </summary>
 | |
|         /// <param name="tickType">Tick to use within PreciseTick.</param>
 | |
|         /// <returns></returns>
 | |
|         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;
 | |
|             }
 | |
|         }
 | |
| 
 | |
| 
 | |
|         /// <summary>
 | |
|         /// Converts current ticks to time.
 | |
|         /// </summary>
 | |
|         /// <param name="tickType">TickType to compare against.</param>
 | |
|         /// <returns></returns>
 | |
|         [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;
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         /// <summary>
 | |
|         /// Converts a number ticks to time.
 | |
|         /// </summary>
 | |
|         /// <param name="ticks">Ticks to convert.</param>
 | |
|         /// <returns></returns>
 | |
|         public double TicksToTime(uint ticks)
 | |
|         {
 | |
|             return (TickDelta * (double)ticks);
 | |
|         }
 | |
| 
 | |
|         /// <summary>
 | |
|         /// Gets time passed from currentTick to previousTick.
 | |
|         /// </summary>
 | |
|         /// <param name="currentTick">The current tick.</param>
 | |
|         /// <param name="previousTick">The previous tick.</param>
 | |
|         /// <returns></returns>
 | |
|         [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);
 | |
|         }
 | |
| 
 | |
|         /// <summary>
 | |
|         /// Gets time passed from Tick to preciseTick.
 | |
|         /// </summary>
 | |
|         /// <param name="preciseTick">PreciseTick value to compare against.</param>
 | |
|         /// <param name="allowNegative">True to allow negative values. When false and value would be negative 0 is returned.</param>
 | |
|         /// <returns></returns>
 | |
|         [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);
 | |
|         }
 | |
|         /// <summary>
 | |
|         /// Gets time passed from Tick to previousTick.
 | |
|         /// </summary>
 | |
|         /// <param name="previousTick">The previous tick.</param>
 | |
|         /// <param name="allowNegative">True to allow negative values. When false and value would be negative 0 is returned.</param>
 | |
|         /// <returns></returns>
 | |
|         [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);
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         /// <summary>
 | |
|         /// Converts time to ticks.
 | |
|         /// </summary>
 | |
|         /// <param name="time">Time to convert.</param>
 | |
|         /// <returns></returns>
 | |
|         [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);
 | |
|         }
 | |
| 
 | |
|         /// <summary>
 | |
|         /// Estimatedly converts a synchronized tick to what it would be for the local tick.
 | |
|         /// </summary>
 | |
|         /// <param name="tick">Synchronized tick to convert.</param>
 | |
|         /// <returns></returns>
 | |
|         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;
 | |
|         }
 | |
|         /// <summary>
 | |
|         /// Estimatedly converts a local tick to what it would be for the synchronized tick.
 | |
|         /// </summary>
 | |
|         /// <param name="localTick">Local tick to convert.</param>
 | |
|         /// <returns></returns>
 | |
|         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
 | |
| 
 | |
| 
 | |
|         /// <summary>
 | |
|         /// Tries to iterate incoming or outgoing data.
 | |
|         /// </summary>
 | |
|         /// <param name="incoming">True to iterate incoming.</param>
 | |
|         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.    
 | |
|         /// <summary>
 | |
|         /// Sends a TimingUpdate packet to clients.
 | |
|         /// </summary>
 | |
|         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;
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         /// <summary>
 | |
|         /// Called on client when server sends a timing update.
 | |
|         /// </summary>
 | |
|         /// <param name="ta"></param>
 | |
|         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
 | |
| 
 | |
|         /// <summary>
 | |
|         /// Sets the TickRate to use. This value is not synchronized, it must be set on client and server independently.
 | |
|         /// </summary>
 | |
|         /// <param name="value">New TickRate to use.</param>
 | |
|         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
 | |
| 
 | |
|     }
 | |
| 
 | |
| } |