Search Unity

Expecting too much from network movement interpolation?

Discussion in 'Multiplayer' started by HiddenMonk, Sep 20, 2017.

  1. HiddenMonk

    HiddenMonk

    Joined:
    Dec 19, 2014
    Posts:
    987
    I am working on interpolating movement and am displeased by the results I am getting.
    The results are alright I guess? I think it isnt so great if I am spectating someone though...
    (tested only on localhost so far)

    Here is an example of how I am doing things.
    Code (CSharp):
    1.      
    2.         float updateRate = 20;
    3.         //...
    4.  
    5.         public void Update()
    6.         {
    7.             Vector3 prevPos = transform.position;
    8.  
    9.             currentInterpolateTime += (Time.deltaTime * updateRate);
    10.  
    11.             transform.position = Vector3.Lerp(startPosition, targetPosition, currentInterpolateTime);
    12.            
    13.             //Check to see if we ever move backwards.
    14.             Debug.DrawRay(transform.position, (Vector3.up + ExtVector3.Direction(prevPos, transform.position)) * 2f, Color.yellow, 1f);
    15.             //Check to see our movement consistency.
    16.             Debug.DrawRay(transform.position, Vector3.up * (Vector3.Distance(prevPos, transform.position) / Time.deltaTime), Color.green, 2f);
    17.         }
    18.        
    19.         //This is called before Update
    20.         protected override void OnAfterDeserialize()
    21.         {
    22.             startPosition = transform.position;
    23.             targetPosition = position.value;
    24.             currentInterpolateTime = 0;
    25.         }

    Here is an image example
    Note that I am just moving at a consistent rate on the server side, and this is the result on the client side.
    NetworkInterpolationConsistency.png

    As you can see by the image, basically each time we receive an updated position packet, the distance is different, which seems to cause inconsistencies in the movement between each update position packet.
    This may be due to the other side not exactly sending every so and so time since its running at 60fps and the sendrate is 20 times a second, so there can be a 16ms difference in distance etc...(remember, this is just localhost so far).

    What I want to ask is, is this to be expected? Is it just not possible to really lerp smoothly across all movement update packets with the current info I am sending?
    Is there anything I can do to improve this?

    For example, I see people talk about valves article on entity interpolation and what not with buffered states, but I am confused on a few things. Some source codes of interpolation I see people just store the currentTime of their own client when they receive the packet and what not, while in other source codes I see people having the server send the time the packet was sent and then using that as the time for when calculating the interpolation.
    I have tried the client time way, but not the server network time way.
    Is using the network time the way to get proper smoothed movement over the network?

    Am I meant to also include the velocity and what not to try and lerp smoothly somehow?
    For right now I am not worrying about extrapolation or anything, so is sending the velocity not needed?

    Any insight is appreciated.
    I have read multiple articles on this, but I guess either my expectations are too high, or I am just not sending/using enough data to properly just guess the new position instead of just relying on lerp?
     
  2. HiddenMonk

    HiddenMonk

    Joined:
    Dec 19, 2014
    Posts:
    987
    I may have found something decent for now.
    I wanted to avoid having to deal with syncing the times over the network, so instead I had the sender send a timestamp just so I could use it to help calculate the duration to be used for interpolation. I then did the whole buffering of states and what not similar to the valve article.
    One thing to note is I think that although the valve articles interpolation method tries to keep you at a constant delay offset from the server movement, the code I have I am pretty sure is more so of a max delay offset, meaning its not really known as to what delay behind the senders movement you will be, but it will try to not be more than the max interpolation set.
    Notice I said "try".
    The interpolation delay could go over the "expected" max interpolation since we do not take into account the total stored duration in all the states combined. We could avoid this by puting extra checks in our AddPositionState, but this might not be a problem and might even be a good thing.
    This is only really an issue during large close packet loss, and should fix itself when the packet loss ends and new states come in, but overall it might even be a good thing its dynamic like this.

    I have not really tested this too much, but hopefully its good.


    Heres the code. Theres some debug code in there that I pushed to the left, so ignore that.
    Code (CSharp):
    1.  
    2.     //-Must be used on a sequenced channel of some sort, else the timestep wrapping might cause problems.
    3.     //-The way the interpolation works is it only starts to reach the max interpolation delay when it first starts to miss states (due to packet loss or delays).
    4.     //Then it will keep that delay possibly always or maybe until the states get overwritten by new incoming states.
    5.     //Could potentially reach a larger delay then expected, but thats just during lots of packet loss and can
    6.     //also be avoided if we add extra checks in our AddPositionState to take into account all state durations combined.
    7.     //Overall it shouldnt be a problem and should fix itself as new position states come in.
    8.     public class SyncTransform : SyncVar
    9.     {
    10.         public SyncVector3 position;
    11.         public SyncFloat rotationY;
    12.         public SyncFloat timestamp;
    13.  
    14.         public float sendRate; //How many times a second are we sending the data?
    15.         public float maxInterpolationCount; //How many sendRates do we want to interpolate for. Set to at least 1f or so if in no packet loss environment for smooth movement
    16.         public float snapThresholdDistance = 2f; //How far from our targetPosition do we need to be before we decide to just teleport to it.
    17.  
    18.         Quaternion startRotation;
    19.         Quaternion targetRotation;
    20.         float currentRotationDuration;
    21.         float totalRotationDuration;
    22.         float currentPositionDuration;
    23.  
    24.         const int maxTimestamp = 64;
    25.  
    26.         //The positionStates size probably should not be any larger than maxInterpolationCount + 1,
    27.         //else there might be great delays if theres packet loss.
    28.         //We do +1 for a little extra buffer. Could maybe do Mathf.CeilToInt(maxInterpolationCount) as well / instead of the +1.
    29.         int maxStoredStates {get {return (int)maxInterpolationCount + 1;}}
    30.         float currentTimeWrapped;
    31.         double previousTime;
    32.         bool hasStarted;
    33.  
    34.         List<PositionState> positionStates = new List<PositionState>();
    35.  
    36.         Transform transform;
    37.  
    38.         public SyncTransform(Transform transform)
    39.         {
    40.             this.transform = transform;
    41.             this.sendRate = SyncManager.syncClient.sendRateCount; //sendRateCount currently set to 20
    42.             this.maxInterpolationCount = 2.25f;
    43.         }
    44.         public SyncTransform(Transform transform, float updateRate, float maxInterpolationCount)
    45.         {
    46.             this.transform = transform;
    47.             this.sendRate = updateRate;
    48.             this.maxInterpolationCount = maxInterpolationCount;
    49.         }
    50.  
    51.         protected override IList<ISyncable> SetSyncables()
    52.         {
    53.             //The constructor parameters are (maxValue, byteSize, canBeNegative)
    54.             position = new SyncVector3(1024, 3, true);
    55.             rotationY = new SyncFloat(360, 2, false);
    56.             timestamp = new SyncFloat(maxTimestamp, 2, false); //Since we use only 2 bytes, we need to wrap the value back to 0.
    57.  
    58.             //These values will be sent out every network send update, even if they didnt change.
    59.             position.isAlwaysDirty = true;
    60.             rotationY.isAlwaysDirty = true;
    61.             timestamp.isAlwaysDirty = true;
    62.  
    63.             return new List<ISyncable>() {position, rotationY, timestamp};
    64.         }
    65.  
    66.         //Only receiver should call update
    67.         public void Update()
    68.         {
    69.             if(!hasStarted) return;
    70.  
    71. Vector3 prevPos = transform.position;
    72.  
    73.             currentRotationDuration += Time.deltaTime;
    74.             currentPositionDuration += Time.deltaTime;
    75.  
    76.             HandleRotation();
    77.             HandlePosition();
    78.  
    79. if(Mathf.Approximately(Vector3.Distance(prevPos, transform.position), 0)) { Debug.DrawRay(transform.position, Vector3.up * 3f, Color.white, 1f); }
    80. Vector3 moveDirection = ExtVector3.Direction(prevPos, transform.position);
    81. Color color = (ExtVector3.IsInDirection(Vector3.right, moveDirection)) ? Color.magenta : Color.yellow;
    82. Debug.DrawRay(transform.position, (Vector3.up + moveDirection) * 2f, color, 2f);
    83. Debug.DrawRay(transform.position, Vector3.up * (Vector3.Distance(prevPos, transform.position) / Time.deltaTime), Color.green, 2f);
    84.         }
    85.  
    86.         void HandlePosition()
    87.         {
    88.             if(positionStates.Count > 1)
    89.             {
    90.                 PositionState leftState = positionStates[0];
    91.                 PositionState rightState = positionStates[1];
    92.  
    93.                 float leftOver = currentPositionDuration - rightState.duration;
    94.                 float interpolationPercent = currentPositionDuration / rightState.duration;
    95.  
    96.                 transform.position = Vector3.Lerp(leftState.position, rightState.position, interpolationPercent);
    97.  
    98.                 if(leftOver >= 0)
    99.                 {
    100.                     RemoveFirstPositionState();
    101.                     currentPositionDuration = leftOver;
    102.                     HandlePosition();
    103.                 }
    104.             }
    105.             else
    106.             {
    107.                 currentPositionDuration = 0;
    108.             }
    109.         }
    110.  
    111.         void HandleRotation()
    112.         {
    113.             transform.rotation = Quaternion.Slerp(startRotation, targetRotation, currentRotationDuration / totalRotationDuration);
    114.         }
    115.  
    116.         //The sender calls this
    117.         protected override void OnBeforeSerialize()
    118.         {
    119.             position.SetValue(transform.position);
    120.  
    121.         //We might want to handle rotation better since using eulerAngles for interpolation can cause us to rotate the long way in the wrong direction.
    122.         //Currently we force it to always rotate in shortest path, but that isnt accurate either, but would look normal at least.
    123.             rotationY.SetValue(transform.rotation.eulerAngles.y);
    124.  
    125.             timestamp.SetValue(SetCurrentTime());
    126.  
    127.             //ExtTime.realtimeSinceStartup is a self calculated time using Time.unscaledDeltaTime and stored in a double for longer lasting precision
    128.             previousTime = ExtTime.realtimeSinceStartup;
    129.         }
    130.  
    131. public int lossPercent = 0;
    132.         //The receiver calls this
    133.         protected override void OnAfterDeserialize()
    134.         {
    135.             hasStarted = true;
    136.  
    137. if(UnityEngine.Random.Range(0, 100) < lossPercent)
    138. {
    139.     Debug.DrawRay(transform.position, Vector3.down * 2f, Color.red, 1f);
    140.     return;
    141. }
    142. Debug.DrawRay(transform.position, Vector3.up * 4f, Color.cyan, 2f);
    143.  
    144.             AddPositionState(new PositionState(position.value, timestamp.value));
    145.  
    146.             startRotation = transform.rotation;
    147.             targetRotation = Quaternion.Euler(0f, rotationY.value, 0f);
    148.             totalRotationDuration = positionStates.GetLast().duration;
    149.             currentRotationDuration = 0;
    150.  
    151.             HandleSnapThreshold(); //Make sure to call this after we set the totalRotationDuration since this might clear the positionStates
    152.         }
    153.  
    154.         void AddPositionState(PositionState state)
    155.         {
    156.             if(positionStates.Count > maxStoredStates)
    157.             {
    158.                 RemoveFirstPositionState();
    159.             }
    160.  
    161.             if(positionStates.Count > 0)
    162.             {
    163.                 state.SetDuration(positionStates.GetLast().timestamp, maxTimestamp);
    164.             }
    165.             positionStates.Add(state);
    166.         }
    167.  
    168.         void RemoveFirstPositionState()
    169.         {
    170.             positionStates.RemoveAt(0);
    171.             currentPositionDuration = 0;
    172.         }
    173.  
    174.         void HandleSnapThreshold()
    175.         {
    176.             if(positionStates.Count > 0)
    177.             {
    178.                 Vector3 targetPosition = positionStates.GetLast().position;
    179.                 Vector3 otherPosition = (positionStates.Count > 1) ? positionStates[positionStates.Count - 1].position : transform.position;
    180.                 if(Vector3.Distance(targetPosition, otherPosition) > snapThresholdDistance)
    181.                 {
    182.                     transform.position = targetPosition;
    183.                     positionStates.Clear();
    184.                     currentPositionDuration = 0;
    185.                 }
    186.             }
    187.         }
    188.  
    189.         float SetCurrentTime()
    190.         {
    191.             float elapsedTime = (float)(ExtTime.realtimeSinceStartup - previousTime);
    192.             currentTimeWrapped += elapsedTime;
    193.  
    194.             if(currentTimeWrapped > maxTimestamp)
    195.             {
    196.                 currentTimeWrapped = currentTimeWrapped - maxTimestamp;
    197.                 //If we are still greater than the max time, then something weird happend so just set to 0. (probably disconnected before this would happen)
    198.                 if(currentTimeWrapped > maxTimestamp)
    199.                 {
    200.                     currentTimeWrapped = 0;
    201.                 }
    202.             }
    203.  
    204.             return currentTimeWrapped;
    205.         }
    206.  
    207.         //float GetSendRateTime(float count)
    208.         //{
    209.         //    return (1f / sendRate) * count;
    210.         //}
    211.  
    212.         struct PositionState
    213.         {
    214.             public Vector3 position;
    215.             public float timestamp;
    216.             public float duration;
    217.  
    218.             public PositionState(Vector3 position, float timestamp)
    219.             {
    220.                 this.position = position;
    221.                 this.timestamp = timestamp;
    222.                 duration = 0;
    223.             }
    224.  
    225.             public void SetDuration(float leftStateTimestamp, float maxTimestamp)
    226.             {
    227.                 float unwrappedTimestep = timestamp;
    228.                 if(leftStateTimestamp > timestamp)
    229.                 {
    230.                     unwrappedTimestep = maxTimestamp + timestamp;
    231.                 }
    232.                 duration = Mathf.Max(0f, unwrappedTimestep - leftStateTimestamp);
    233.  
    234.                //I used to do it like this
    235.                //duration = Mathf.Clamp(unwrappedTimestep - leftStateTimestamp, 0f, maxDuration); //maxDuration is calculated as GetSendRateTime(maxInterpolationCount)
    236.                //but since we clamped to a maxDuration, it wasnt as flexible and caused jitter more often.
    237.                //So for now we just dont clamp the duration and rely on old states being overwritten by new states to keep the interpolation delay low.
    238.             }
    239.         }
    240.     }
     
    Last edited: Sep 29, 2017
  3. Vytek

    Vytek

    Joined:
    Apr 29, 2016
    Posts:
    51
    HiddenMonk likes this.
  4. Vytek

    Vytek

    Joined:
    Apr 29, 2016
    Posts:
    51