Advertisement

Snapshot Interpolation

Started by January 09, 2018 01:28 PM
41 comments, last by Tipotas688 6 years, 6 months ago
4 hours ago, Kylotan said:

So when you say "I think state synchronisation might help my issue but it might also not", that doesn't really make sense.

I'm not sure I said that, the name state synchronisation I used is again from Glenn's page, I didn't mean it in the grammatical sense of it. 

4 hours ago, Kylotan said:

At this stage I think the problem is not so much of getting new ideas, it's about implementation. Most likely all your problems are at the implementation level rather than the conceptual level, but since we've seen no code or gameplay it's hard to know what they would be.

I didn't share it basically because I followed your previous explanations to write up my code so I thought it wouldn't matter but in any case here's my code:

My SetData method basically is what's called when I receive the ball's position from the server:

( the index check at first is to ensure that data arriving late isn't used )


    public void SetData( NetworkBallData data )
    {
        if( m_LatestData.Index >= data.Index )
        {
            return;
        }

        m_PreviousData = m_LatestData;
        m_LatestData = data;


        if( !m_HasFinished )
        {
            m_PreviousData.Position = m_BallTransform.position;
            m_PreviousData.Rotation = m_BallTransform.rotation;
        }

        if( m_PreviousDateTime == DateTime.MinValue )
        {
            m_PreviousDateTime = DateTime.Now;
        }
        else
        {
            DateTime now = DateTime.Now;
            m_NextPositionTime = now.AddMilliseconds( CurrentDelay );
        }
    }

 

Then my Update method which is called every frame is:

( steps are used to manage packet send rate )


    public void Update()
    {
        if( MatchManager.Instance.IsInMatch )
        {
            if( BattleManager.Instance.IsServer )
            {
                m_Step++;
                if( m_Step == MultiplayerManager.PACKET_STEP )
                {
                    m_Step = 0;
                    SendPosition();
                }
            }
            else
            {
                DateTime now = DateTime.Now;
                if( now < m_NextPositionTime )
                {
                    m_HasFinished = false;
                    TimeSpan diff = m_NextPositionTime.Subtract( now );
                    percentage = 1.0f - diff.Milliseconds / CurrentDelay;
                    m_BallTransform.position = Vector3.Lerp( m_PreviousData.Position, m_LatestData.Position, percentage );
                }
                else
                {
                    m_HasFinished = true;
                }
            }
        }
    }

 

You don't want to keep track of whether things have "finished interpolating" or not. Instead, keep track of something like:

"last known good position P0, and timestamp T0 for that position"

"next known good position P1, and timestamp T1 for that position"

Then keep track of current time, minus interpolation offset, T2.

Then, calculate "P2 = P1 + (T2-T1)*(P1-P0)" as the output position.

The interpolation offset can be such that your time T2 is somewhere between T0 and T1, for "past time interpolation," or it can be such that T2 > T1 and you do present-time extrapolation. The formula works either way. You will also establish a maximum delta between T2 and T1, so you don't extrapolate too far into the future.

When you receive a new update, if you interpolate (T2 < T1) set P0 to your current P2 and set P1 to the update position, set T0 to the current T2. If you extrapolate, instead set P1 to (new P1 - prev P1) * ExtrapolateTime.

There's a library that does approximately this, to good effect, called EPIC (entity position update code,) available at http://www.mindcontrol.org/~hplus/epic/

enum Bool { True, False, FileNotFound };
Advertisement

@hplus0603 I'm a bit lost, the reason I'm keeping whether it has finished interpolating is in case a packet arrives before my interpolation to the "present" has finished. If that happens it means my fake delay is too long and I could reduce it.

 

As I understand, in your example P2 is a future potential position so I'm predicting but do I do that in case a packet is late or do I do it regardless?

 

I actually had this epic link and I've already started converting the code to C#

the reason I'm keeping whether it has finished interpolating is in case a packet arrives before my interpolation to the "present" has finished. If that happens it means my fake delay is too long and I could reduce it

You don't need a boolean, just keep the two time stamps, and compare them.

There are two ways of predicting:

  1. Predict from a previous known position to a new known position, so the entity follows a known-taken path, but is "behind"  in time
  2. Predict from a previously predicted position to a new predicted position, so the entity is in "current" time if it moves reasonably regularly, but will be "out of place" if there are fast turns involved

 

enum Bool { True, False, FileNotFound };

Yeap, I understand that part of the concept but the part I don't is where you said:

12 hours ago, hplus0603 said:

When you receive a new update, if you interpolate (T2 < T1) set P0 to your current P2 and set P1 to the update position, set T0 to the current T2. If you extrapolate, instead set P1 to (new P1 - prev P1) * ExtrapolateTime.

but if when receiving the latest data is where I decide if the packet has arrived in good time so I interpolate or it's too slow so I extrapolate, it kinda makes interpolation redundant. Unless you were aiming for something different.

 

As far as I understand your idea to add extrapolation in my interpolation-only solution so far is:

During the update, if it's been too long after the now + delay, I extrapolate if not then I interpolate as I have been. 

When I receive data from the server I update the information as well as the future potential position if the next packet is delayed.

 

That sounds right!

And there really are three points you want to think about:

"old update" position (and perhaps velocity)

"new update" position (and perhaps velocity)

Currently shown object position

The last is important, you need to somehow work that in as your "reference point" else you will "jump around" when receiving new update positions.

enum Bool { True, False, FileNotFound };
Advertisement

@hplus0603 following a few days of reading and testing I got a question:

I did try your logic above but it didn't really work, mostly because T2-T1 wasn't relevant to the positions so somewhere along the way I didn't get correct positions, I implemented the EPIC solution in C# which seems to give me some results but their explanation for some reason doesn't make sense to me: 

Quote

packetTime: The globally synchronized time at which the packet was sent (and thus the data valid).
curTime: The globally synchronized time at which you put the data into the Extrapolator (i e, "now").

The time at which to interpolate the entity. It should 
be greater than the last packetTime, and less than the last curTime 
plus some allowable slop (determined by EstimateFreqency()).

I don't understand why it should be less, if it's less than the last curTime which is basically now as you update the same and calculate the next potential position you'll get a time difference that ends before the interpolation does. 
 

 



        oPos = m_SnapPos + oVel * (float)( forTime - m_SnapTime );

This is the part which I understand the least, time is unit dependent since it's differentiation rather than a % so if I use ticks which are 10000 times a millisecond the number returned is huge. 

One second it can be:

(1.0, -2.7, 0.0) = (1.0, -2.7, 0.0) + (0.0, 0.0, 0.0) * ( 6.36594027695935E+17 - 6.36594027694191E+17 )

and the  next second it's:

(-17777810000.0, -30792030000.0, 0.0) = (1.0, -2.7, 0.0) + (0.0, 0.0, 0.0) *(6.58622968946338E+17 - 6.36594027695935E+17 )

 

Also oVel can a lot of the time be 0 since a user might not move for some time and that returns a zero vector at first since m_SnapPos will also stay zero and that creates a weird problem where the potential extrapolated position is 0,0,0 which is wrong

I made an example script that shows my results:


using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Object = UnityEngine.Object;

public class ExtrapolatorExample : MonoBehaviour
{
    private Extrapolator3 m_Extrapolator3 = new Extrapolator3();

    private DateTime m_PressTime;

    private int m_Step = -1;
    private const int STEP_COUNT = 30;

    private Vector3 m_ClickData = new Vector3();
    
    void Update()
    {
        if( Input.GetMouseButtonDown( 0 ) )
        {
            m_ClickData = Camera.main.ScreenToWorldPoint( Input.mousePosition );
            CreateDebugCube( m_ClickData, Color.blue );

            m_PressTime = DateTime.Now;
            m_Step = STEP_COUNT;
        }
        if( m_Step > 0 )
        {
            m_Step--;
        }
        else if( m_Step == 0 )
        {
            m_Step--;
            bool done = m_Extrapolator3.AddSample( m_PressTime.Ticks, DateTime.Now.Ticks, m_ClickData );
            Debug.Log( done + " " + m_ClickData );

            Vector3 data;

            bool read = m_Extrapolator3.ReadPosition( m_PressTime.Ticks + STEP_COUNT, out data );
            Debug.Log( read + " " + data );
            CreateDebugCube( data, Color.yellow );
        }
    }


    private List<GameObject> m_TempList = new List<GameObject>();

    private void CreateDebugCube( Vector3 data, Color color )
    {
        GameObject temp = GameObject.CreatePrimitive( PrimitiveType.Cube );
        temp.transform.position = new Vector3( data.x, data.y, -5 );
        temp.transform.localScale = Vector3.one * 0.5f;
        temp.name = "Cube " + m_TempList.Count;
        Object.Destroy( temp.GetComponent<BoxCollider>() );
        temp.GetComponent<Renderer>().material.shader = Shader.Find( "VertexLit" );
        temp.GetComponent<Renderer>().material.color = color;
        m_TempList.Add( temp );
    }
}

 

Don't use ticks - measure time in seconds. You don't need sub-millisecond precision and the timer doesn't truly have that level of resolution anyway. Time in seconds stored as a float is adequate for gaming purposes. If you need to use the C# DateTime value, try measuring the time at program start and after that using TimeSpan.TotalSeconds to get the time since the program started. (Similar to Unity's Time.realTimeSinceStartup).

It's not clear why you think that the extrapolation is wrong - if velocity is zero then it would be expected that the extrapolated distance would also be 0,0,0. Added to the previous position, whatever it was, creates a correct new position for a stationary object. I don't know if I've missed a post but since we don't know what each of those variables mean to you, it's hard to say exactly what is wrong here.

I assume that snippet is referring to your C# port of the EPIC code, but it's possible that there could have been bugs introduced in the porting, so it would probably be useful to see that code as well.

This topic is closed to new replies.

Advertisement