Search Unity

Network.Time in 2017.3 workaround

Discussion in 'Multiplayer' started by dnnkeeper, Mar 24, 2018.

  1. dnnkeeper

    dnnkeeper

    Joined:
    Jul 7, 2013
    Posts:
    84
    Due to old RakNet unity network being removed in new versions I run into need of a Network.Time replacement so I can continue to use reliable timestamps for network states interpolation.
    Long story short: I wrote a script sending Time.realtimeSinceStartup form server to clients and managed to correct network delay using NetworkTransport.GetRemoteDelayTimeMS and substracting some predicted average error.

    My estimation is following:
    Reported server time is late by delay measured with NetworkTransport.GetRemoteDelayTimeMS and probably less than that by (time wasted on local frame update + 1/serverFPS) and by average error

    I'm opened to hear your suggestions. Is my estimation correct? Planning to update this project with buffered network transform example for testing.

    So
    Code (CSharp):
    1. double predictedServerTime = syncTimeMessage.Time + delay - (Time.deltaTime + 1/60f) - expectedTimeMissmatchAvg;
    To get it working just add this component to your NetworkManager game object

    Code (CSharp):
    1.  
    2. //
    3. // NetworkTime - helper class for UNET server time synchronization
    4. //
    5. // Copyright (C) 2018 Boris Novikov @ dnnkeeper
    6. //
    7. // Permission is hereby granted, free of charge, to any person obtaining a copy
    8. // of this software and associated documentation files (the "Software"), to deal
    9. // in the Software without restriction, including without limitation the rights
    10. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    11. // copies of the Software, and to permit persons to whom the Software is
    12. // furnished to do so, subject to the following conditions:
    13. //
    14. // The above copyright notice and this permission notice shall be included in
    15. // all copies or substantial portions of the Software.
    16. //
    17. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    18. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    19. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    20. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    21. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    22. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    23. // THE SOFTWARE.
    24. //
    25.  
    26. using System.Collections;
    27. using System.Collections.Generic;
    28. using UnityEngine;
    29. using UnityEngine.Networking;
    30. using UnityEngine.SceneManagement;
    31.  
    32.  
    33. public class CustomMsgType
    34. {
    35.     public const short SyncTimeMsgType = MsgType.Highest + 1;
    36. }
    37.  
    38. public class SyncTimeMessage : MessageBase
    39. {
    40.     public int timestamp;
    41.     public double Time;
    42. }
    43.  
    44. [RequireComponent(typeof(NetworkManager))]
    45. public class NetworkTime : MonoBehaviour
    46. {
    47.     public static NetworkTime singleton;
    48.  
    49.     //time offset of this client relative to the server. Always 0 on server itself
    50.     public static double serverTimeOffset;
    51.  
    52.     //if above zero - this amount of sync messages will be sent when new client connects with 0.1f delay
    53.     public int syncMessageCount = 0;
    54.  
    55.     //if above zero - will be sending sync messages every period of time
    56.     public float syncPeriod = 2.0f;
    57.  
    58.     //blinks with main camera background color if enabled and shows GUI
    59.     public bool debug;
    60.  
    61.     //last missmatch between expected servertime and servertime in arrived message
    62.     float expectedTimeMissmatch;
    63.  
    64.     //avearge error
    65.     float expectedTimeMissmatchAvg;
    66.  
    67.     //last delay of arrived message in MS
    68.     float lastTimeDelayMS;
    69.  
    70.     bool init;
    71.  
    72.     bool isClientActive;
    73.  
    74.     bool isServerActive;
    75.  
    76.     //keeps track of all network connections passed initial synchronization
    77.     HashSet<NetworkConnection> syncedConnections = new HashSet<NetworkConnection>();
    78.  
    79.     public static double ServerTime
    80.     {
    81.         get
    82.         {
    83.             return Time.realtimeSinceStartup + serverTimeOffset;
    84.         }
    85.     }
    86.  
    87.     void OnEnable()
    88.     {
    89.         SceneManager.sceneLoaded += OnLevelFinishedLoading;
    90.     }
    91.  
    92.     void OnDisable()
    93.     {
    94.         SceneManager.sceneLoaded -= OnLevelFinishedLoading;
    95.     }
    96.  
    97.     void Awake()
    98.     {
    99.         if (singleton == null)
    100.         {
    101.             if (transform.parent == null)
    102.                 DontDestroyOnLoad(this);
    103.             singleton = this;
    104.         }
    105.         else
    106.         {
    107.             Destroy(this);
    108.         }
    109.     }
    110.  
    111.     void Update()
    112.     {
    113.         isClientActive = NetworkClient.active;
    114.  
    115.         isServerActive = NetworkServer.active;
    116.  
    117.         if (isClientActive || isServerActive)
    118.         {
    119.             if (!init)
    120.             {
    121.                 RegisterHandler();
    122.                 init = true;
    123.             }
    124.         }
    125.         else
    126.         {
    127.             init = false;
    128.         }
    129.  
    130.         //Start time synchronization for every new connection
    131.         foreach (NetworkConnection connectionToClient in NetworkServer.connections)
    132.         {
    133.             if (connectionToClient != null)
    134.             {
    135.                 if (connectionToClient.isConnected && !syncedConnections.Contains(connectionToClient))
    136.                 {
    137.                     if (syncPeriod > 0f && syncMessageCount == 0f )
    138.                     {
    139.                         StartCoroutine(SyncTimeRoutine(connectionToClient, syncPeriod, x => { syncedConnections.Remove(connectionToClient); }));
    140.                     }
    141.                     else
    142.                     {
    143.                         if (syncMessageCount > 0)
    144.                         {
    145.                             StartCoroutine(SendTimeSync(connectionToClient, 0.1f, syncMessageCount));
    146.                         }
    147.                         else
    148.                             SendTimeSync(connectionToClient);
    149.                     }
    150.                    
    151.                     syncedConnections.Add(connectionToClient);
    152.                 }
    153.             }
    154.         }
    155.        
    156.         //clear invalid connections
    157.         syncedConnections.RemoveWhere(conn => conn.address.Length == 0);
    158.  
    159.         //blink
    160.         if (debug)
    161.         {
    162.             if ((ServerTime % 1f) <= 1f/30f)
    163.             {
    164.                 Camera.main.backgroundColor = Color.grey;
    165.             }
    166.             else
    167.             {
    168.                 Camera.main.backgroundColor = Color.black;
    169.             }
    170.         }
    171.     }
    172.  
    173.     void RegisterHandler()
    174.     {
    175.         if (isServerActive)
    176.         {
    177.             if (debug)
    178.                 Debug.Log("Register ServerTimeRequest handler");
    179.             NetworkServer.RegisterHandler(CustomMsgType.SyncTimeMsgType, ServerTimeRequest);
    180.         }
    181.         else if (isClientActive)
    182.         {
    183.             if (debug)
    184.                 Debug.Log("Register OnReceiveSyncTime handler");
    185.             NetworkClient.allClients[0].RegisterHandler(CustomMsgType.SyncTimeMsgType, SyncTimeRequest);
    186.         }
    187.     }
    188.  
    189.     void ServerTimeRequest(NetworkMessage msg)
    190.     {
    191.         if (debug)
    192.             Debug.Log("Time request from "+msg.conn.address);
    193.         SendTimeSync(msg.conn);
    194.     }
    195.  
    196.  
    197.  
    198.     void SyncTimeRequest(NetworkMessage msg)
    199.     {
    200.         var syncTimeMessage = msg.ReadMessage<SyncTimeMessage>();
    201.  
    202.         byte error;
    203.         lastTimeDelayMS = NetworkTransport.GetRemoteDelayTimeMS(msg.conn.hostId, msg.conn.connectionId, syncTimeMessage.timestamp, out error);
    204.  
    205.         //how much did we miss this time
    206.         expectedTimeMissmatch = (float)(ServerTime - syncTimeMessage.Time);
    207.  
    208.         //check if it was first sync so we don't interpolate and set it later
    209.         if (expectedTimeMissmatch > 0.1f)
    210.             expectedTimeMissmatchAvg = 0f;
    211.         else
    212.             expectedTimeMissmatchAvg = Mathf.Lerp(expectedTimeMissmatchAvg, expectedTimeMissmatch, 0.1f); //error averaging
    213.  
    214.         float delay = lastTimeDelayMS * 0.001f;
    215.    
    216.         //Reported server time is late by delay and probably less than that by (time wasted on local frame update + 1/serverFPS) and by average error
    217.         double predictedServerTime = syncTimeMessage.Time + delay - (Time.deltaTime + 1/60f) - expectedTimeMissmatchAvg;
    218.    
    219.         float newTimeOffset = (float)(predictedServerTime - Time.realtimeSinceStartup);
    220.        
    221.         //Debug.Log("syncTimeMessage.Time = " + syncTimeMessage.Time.ToString("F6") + "; delay = " + delay.ToString("F6"));
    222.  
    223.         //Debug.Log("Local Time = " + Time.realtimeSinceStartup.ToString("F6"));
    224.  
    225.         //Debug.Log("timeOffset = "+timeOffset +" + " + (newTimeOffset-timeOffset).ToString("F6")+" = "+ newTimeOffset.ToString("F6"));
    226.  
    227.         serverTimeOffset = newTimeOffset;
    228.    
    229.         if (debug)
    230.             Debug.Log("ServerTime = " + ServerTime.ToString("F6") + "; delay = " + (delay).ToString("F3")+";  ");
    231.     }
    232.  
    233.     public void SendTimeSyncToAll()
    234.     {
    235.         //Debug.Log("SYNC");
    236.  
    237.         SyncTimeMessage msg = new SyncTimeMessage();
    238.  
    239.         msg.timestamp = NetworkTransport.GetNetworkTimestamp();
    240.  
    241.         msg.Time = Time.realtimeSinceStartup;
    242.  
    243.         foreach (NetworkConnection connectionToClient in NetworkServer.connections)
    244.         {
    245.             if (connectionToClient != null)
    246.                 connectionToClient.Send(CustomMsgType.SyncTimeMsgType, msg);
    247.         }
    248.     }
    249.  
    250.     public void SendTimeSync(NetworkConnection connectionToClient)
    251.     {
    252.         SyncTimeMessage msg = new SyncTimeMessage();
    253.  
    254.         msg.timestamp = NetworkTransport.GetNetworkTimestamp();
    255.  
    256.         msg.Time = Time.realtimeSinceStartup;
    257.        
    258.         connectionToClient.Send(CustomMsgType.SyncTimeMsgType, msg);
    259.     }
    260.  
    261.     public void RequestServerTime()
    262.     {
    263.         if (debug)
    264.             Debug.Log("RequestServerTime");
    265.  
    266.         SyncTimeMessage msg = new SyncTimeMessage();
    267.  
    268.         msg.timestamp = NetworkTransport.GetNetworkTimestamp();
    269.  
    270.         msg.Time = Time.realtimeSinceStartup;
    271.  
    272.         NetworkClient.allClients[0].connection.Send(CustomMsgType.SyncTimeMsgType, msg);
    273.     }
    274.  
    275.     public IEnumerator SendTimeSync(NetworkConnection connectionToClient, float delay, int count)
    276.     {
    277.         for (int i = count; i > 0; i--)
    278.         {
    279.             SendTimeSync(connectionToClient);
    280.             yield return new WaitForSecondsRealtime(delay);
    281.         }
    282.     }
    283.  
    284.     public IEnumerator syncTimeAllRoutine()
    285.     {
    286.         while (NetworkServer.active)
    287.         {
    288.             SendTimeSyncToAll();
    289.             yield return new WaitForSecondsRealtime(syncPeriod);
    290.         }
    291.     }
    292.  
    293.     public IEnumerator SyncTimeRoutine(NetworkConnection connectionToClient, float delay, System.Action<bool> Callback = null)
    294.     {
    295.         while (connectionToClient != null && connectionToClient.address.Length > 0)
    296.         {
    297.             SendTimeSync(connectionToClient);
    298.  
    299.             yield return new WaitForSecondsRealtime(delay);
    300.         }
    301.         if (Callback != null)
    302.             Callback(true);
    303.     }
    304.  
    305.     private void OnConnectedToServer()
    306.     {
    307.         if (debug)
    308.             Debug.Log("OnConnectedToServer!");
    309.         RequestServerTime();
    310.     }
    311.  
    312.     void OnLevelFinishedLoading(Scene scene, LoadSceneMode mode)
    313.     {
    314.         if (isClientActive)
    315.         {
    316.             RequestServerTime();
    317.         }
    318.     }
    319.  
    320.     public float GetLastTimeDelay()
    321.     {
    322.         return lastTimeDelayMS;
    323.     }
    324.  
    325.     private void OnGUI()
    326.     {
    327.         if (!debug)
    328.             return;
    329.  
    330.         GUILayout.Space(200);
    331.  
    332.         GUILayout.Label("CorrectedServerTime = " + ServerTime.ToString("F6")+"s");
    333.  
    334.         if (NetworkClient.active)
    335.         {
    336.             GUILayout.Label("lastTimeDelay = " + lastTimeDelayMS+"ms");
    337.  
    338.             GUILayout.Label("expectedTimeMissmatch = " + (expectedTimeMissmatch).ToString("F6"));
    339.  
    340.             if (GUILayout.Button("Request Server Time"))
    341.             {
    342.                 RequestServerTime();
    343.             }
    344.  
    345.         }
    346.         else if (NetworkServer.active)
    347.         {
    348.             if (GUILayout.Button("Sync Time"))
    349.             {
    350.                 SendTimeSyncToAll();
    351.             }
    352.  
    353.             GUILayout.Label("NetworkServer.connections:");
    354.  
    355.  
    356.             foreach (NetworkConnection c in NetworkServer.connections)
    357.             {
    358.                 if (c == null)
    359.                     GUILayout.Label("Null connection");
    360.                 else
    361.                     GUILayout.Label(c.address);
    362.             }
    363.        
    364.             GUILayout.Space(20);
    365.  
    366.             GUILayout.Label("syncedConnections:");
    367.        
    368.             foreach (NetworkConnection c in syncedConnections)
    369.             {
    370.                 if (c == null)
    371.                     GUILayout.Label("Null connection");
    372.                 else
    373.                 {
    374.                     GUILayout.Label(c.address + " " + c.isConnected + " " + c.connectionId);
    375.                 }
    376.             }
    377.        
    378.         }
    379.     }
    380. }
    381.  

    Result is about ~5ms error on local machine and very similar on any remote client with stable ping.
    NetTime.jpg

    demo Project on bitbucket:
    https://bitbucket.org/dnnkeeper/networktime