Network Jitter - I'm Desparate?

Hey all,

I have everything working great, almost done the alpha build of my game, but this network jitter is preventing me from releasing anything. I've scouered the boards, but haven't found the resolution and I know its gotta work. I'm using NetworkRigidBody and Unity4, I have Observed set to NetworkRigidBody, with it enabled on both the sending and receiving sides. I've taken time to diagnose the code and I understand it and I'm pretty confident the code looks valid. If my client isn't moving and the remote avatar is moving I notice the jitter, but if we're both traveling in the same direction at the same speed, and I'm very close to the other player, I see the other player jitter very bad. The faster I go the worse I see it. Is there another version of NetworkRigidBody that works better? Might it be an issue with prediction? I'm traveling perfectly straight, no turning, so even prediction should be pretty simple. I even commented out the prediction code thinking it was moving too far forward, then it had to back up and reset, but I still had the jitter. This is a show stopper and I'm not ready to give up. Anyone have any magic for me.... PLEASE!!!!

I'm waiting on a new network capture card, should be here any day. Then I'll upload a video if its not resolved by then.

Comments

  • The problem you're seeing is that the default networkrigidbody uses 50ms updates, however that is not in line with PUNs default 100ms OnSerializePhotonView updates. You'll need to change one of these.
    Try my code (150ms interpolation, with still PUNs default 100ms OnSerialize)


    // Example code from the unity networking examples
    using UnityEngine;
    using System.Collections;

    public class NetworkRigidbody : Photon.MonoBehaviour
    {
    //
    // NOTE: Network interpolation is afffected by the network sendRate.
    // By default this is 10 times/second for OnSerialize. (See PhotonNetwork.sendIntervalOnSerialize)
    // Raise the sendrate if you want to lower the interpolationBackTime (or vice versa)
    //

    public double m_InterpolationBackTime = 0.15; //0.15 = 150ms
    public double m_ExtrapolationLimit = 0.5;

    internal struct State
    {
    internal double timestamp;
    internal Vector3 pos;
    internal Vector3 velocity;
    internal Quaternion rot;
    internal Vector3 angularVelocity;
    }

    // We store twenty states with "playback" information
    State[] m_BufferedState = new State[20];
    // Keep track of what slots are used
    int m_TimestampCount;

    void Awake()
    {
    if (photonView.isMine)
    this.enabled = false;
    }


    void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info)
    {

    // Send data to server
    if (stream.isWriting)
    {
    Vector3 pos = transform.position;
    Quaternion rot = transform.rotation;
    Vector3 velocity = rigidbody.velocity;
    Vector3 angularVelocity = rigidbody.angularVelocity;

    stream.Serialize(ref pos);
    stream.Serialize(ref velocity);
    stream.Serialize(ref rot);
    stream.Serialize(ref angularVelocity);
    }
    // Read data from remote client
    else
    {
    Vector3 pos = Vector3.zero;
    Vector3 velocity = Vector3.zero;
    Quaternion rot = Quaternion.identity;
    Vector3 angularVelocity = Vector3.zero;
    stream.Serialize(ref pos);
    stream.Serialize(ref velocity);
    stream.Serialize(ref rot);
    stream.Serialize(ref angularVelocity);

    // Shift the buffer sideways, deleting state 20
    for (int i = m_BufferedState.Length - 1; i >= 1; i--)
    {
    m_BufferedState = m_BufferedState[i - 1];
    }

    // Record current state in slot 0
    State state;
    state.timestamp = info.timestamp;
    state.pos = pos;
    state.velocity = velocity;
    state.rot = rot;
    state.angularVelocity = angularVelocity;
    m_BufferedState[0] = state;

    // Update used slot count, however never exceed the buffer size
    // Slots aren't actually freed so this just makes sure the buffer is
    // filled up and that uninitalized slots aren't used.
    m_TimestampCount = Mathf.Min(m_TimestampCount + 1, m_BufferedState.Length);

    // Check if states are in order, if it is inconsistent you could reshuffel or
    // drop the out-of-order state. Nothing is done here
    for (int i = 0; i < m_TimestampCount - 1; i++)
    {
    if (m_BufferedState.timestamp < m_BufferedState[i + 1].timestamp)
    Debug.Log("State inconsistent");
    }
    }
    }

    // We have a window of interpolationBackTime where we basically play
    // By having interpolationBackTime the average ping, you will usually use interpolation.
    // And only if no more data arrives we will use extra polation
    void Update()
    {
    // This is the target playback time of the rigid body
    double interpolationTime = PhotonNetwork.time - m_InterpolationBackTime;

    // Use interpolation if the target playback time is present in the buffer
    if (m_BufferedState[0].timestamp > interpolationTime)
    {
    // Go through buffer and find correct state to play back
    for (int i = 0; i < m_TimestampCount; i++)
    {
    if (m_BufferedState.timestamp <= interpolationTime || i == m_TimestampCount - 1)
    {
    // The state one slot newer (<100ms) than the best playback state
    State rhs = m_BufferedState[Mathf.Max(i - 1, 0)];
    // The best playback state (closest to 100 ms old (default time))
    State lhs = m_BufferedState;

    // Use the time between the two slots to determine if interpolation is necessary
    double length = rhs.timestamp - lhs.timestamp;
    float t = 0.0F;
    // As the time difference gets closer to 100 ms t gets closer to 1 in
    // which case rhs is only used
    // Example:
    // Time is 10.000, so sampleTime is 9.900
    // lhs.time is 9.910 rhs.time is 9.980 length is 0.070
    // t is 9.900 - 9.910 / 0.070 = 0.14. So it uses 14% of rhs, 86% of lhs
    if (length > 0.0001)
    {
    t = (float)((interpolationTime - lhs.timestamp) / length);
    }
    // if t=0 => lhs is used directly
    transform.localPosition = Vector3.Lerp(lhs.pos, rhs.pos, t);
    transform.localRotation = Quaternion.Slerp(lhs.rot, rhs.rot, t);
    return;
    }
    }
    }
    // Use extrapolation
    else
    {
    State latest = m_BufferedState[0];

    float extrapolationLength = (float)(interpolationTime - latest.timestamp);
    // Don't extrapolation for more than 500 ms, you would need to do that carefully
    if (extrapolationLength < m_ExtrapolationLimit)
    {
    float axisLength = extrapolationLength * latest.angularVelocity.magnitude * Mathf.Rad2Deg;
    Quaternion angularRotation = Quaternion.AngleAxis(axisLength, latest.angularVelocity);

    transform.position = latest.pos + latest.velocity * extrapolationLength;
    transform.rotation = angularRotation * latest.rot;
    rigidbody.velocity = latest.velocity;
    rigidbody.angularVelocity = latest.angularVelocity;
    }
    }
    }
    }
  • Thanks Leepo but that didn't quite work either. Is the backtime from .1 to .15 the only thing you changes?

    However, I did get some code from Duhprey off the unity forums with a modified piece of code from this http://wiki.unity3d.com/index.php?title ... ition_Sync, but Duhprey's version further added control inputs to help with the prediction and its working well. I'm still working through some tweaks and refinements, but when I'm done, with Duhprey's permission I'll post it.
  • Weird, my script should work fine right away.
    What is your PING to the cloud servers? Are you using the closest region?

    Otherwise I'll wait on your results.
  • Leepo, thanks for the help

    I had several different variations of the original NetworkRigidBody but none worked as advertised. I don't know what I'm doing different. I've tested on both my own server and the Photon cloud server with the exact same results. It's not a ping issue. Avg ping is around 2-3ms because I'm actually over wifi, but its still fine. I finally received a variation of the code from Duhprey on the Unity forums and his code worked great for me. Furthermore, he also passes player control input to help stream line the smoothing based on inputs and also helps the prediction. It's actually pretty cool. I've included the final variation below.
        using UnityEngine;
        using System.Collections;
         
        public class NetworkRigidbody : MonoBehaviour {
           
            public double m_InterpolationBackTime = 0.1;
            public double m_ExtrapolationLimit = 0.5;
            private float interpolationConstant = 0.01f;  		// &lt;---- This was originally .1, but I had to make it .01 to really get rid of the jitter
           
            internal struct  State
            {
                internal double timestamp;
                internal Vector3 pos;
                internal Vector3 velocity;
                internal Quaternion rot;
                internal Vector3 angularVelocity;
                internal float yaw;
                internal float pitch;
                internal float roll;
    			internal float speed;
            }
           
            // We store twenty states with "playback" information
            State&#91;&#93; m_BufferedState = new State&#91;20&#93;;
            // Keep track of what slots are used
            int m_TimestampCount;
    		PhotonView _pv;
    		PlayerControl _pc;
    		ShipEngine _se;
    		float queuedyaw;
    		float queuedpitch;
    		float queuedroll;
    		float lastsentyaw;
    		float lastsentpitch;
    		float lastsentroll;
         
            void Awake () {
    			_pv = (PhotonView)gameObject.GetComponent("PhotonView");		 // &lt;--- used to check if networkView.isMine
    			_pc = (PlayerControl)gameObject.GetComponent("PlayerControl");   // &lt;--- Used to get yaw, potch, roll inputs
    			_se = (ShipEngine)gameObject.GetComponent("ShipEngine");		 // &lt;--- Used to get throttle/speed setting
    
                enabled = !_pv.isMine;
            }
           
            void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info) 
            {
                // Send data to server
                if (stream.isWriting)
                {
                    Vector3 pos = transform.position;
                    Quaternion rot = transform.rotation;
                    Vector3 velocity = rigidbody.velocity;
                    Vector3 angularVelocity = rigidbody.angularVelocity;
                    float yaw = queuedyaw;									// &lt;--- In the FixedUpdate below I keep track of max value between network sends, then send the max
                    float pitch = queuedpitch;
                    float roll = queuedroll;
    				float speed = _se.shipspeed;
    				lastsentyaw = queuedyaw;
    				lastsentpitch = queuedpitch;
    				lastsentroll = queuedroll;
    				
    				queuedyaw = 0;											// &lt;--- Reset
    				queuedpitch = 0;
    				queuedroll = 0;
         
                    stream.Serialize(ref pos);
                    stream.Serialize(ref velocity);
                    stream.Serialize(ref rot);
                    stream.Serialize(ref angularVelocity);
                    stream.Serialize(ref yaw);
                    stream.Serialize(ref pitch);
                    stream.Serialize(ref roll);
                    stream.Serialize(ref speed);
                }
                // Read data from remote client
                else
                {
                    Vector3 pos = Vector3.zero;
                    Vector3 velocity = Vector3.zero;
                    Quaternion rot = Quaternion.identity;
                    Vector3 angularVelocity = Vector3.zero;
                    float yaw = 0f;
                    float pitch = 0f;
                    float roll = (short)0;
    				float speed = 0f;
               
                    stream.Serialize(ref pos);
                    stream.Serialize(ref velocity);
                    stream.Serialize(ref rot);
                    stream.Serialize(ref angularVelocity);
                    stream.Serialize(ref yaw);
                    stream.Serialize(ref pitch);
                    stream.Serialize(ref roll);
                    stream.Serialize(ref speed);
                   
    				// Shift the buffer sideways, deleting state 20
                    for (int i=m_BufferedState.Length-1;i&gt;=1;i--)
                    {
                        m_BufferedState&#91;i&#93; = m_BufferedState&#91;i-1&#93;;
                    }
                   
                    // Record current state in slot 0
                    State state;
                    state.timestamp = info.timestamp;
                    state.pos = pos;
                    state.velocity = velocity;
                    state.rot = rot;
                    state.angularVelocity = angularVelocity;
                    state.yaw = yaw;
                    state.pitch = pitch;
                    state.roll = roll;
                    state.speed = speed;
                    m_BufferedState&#91;0&#93; = state;
         
                    // Update used slot count, however never exceed the buffer size
                    // Slots aren't actually freed so this just makes sure the buffer is
                    // filled up and that uninitalized slots aren't used.
                    m_TimestampCount = Mathf.Min(m_TimestampCount + 1, m_BufferedState.Length);
         
                    // Check if states are in order, if it is inconsistent you could reshuffel or
                    // drop the out-of-order state. Nothing is done here
                    for (int i=0;i&lt;m_TimestampCount-1;i++)
                    {
                        if (m_BufferedState&#91;i&#93;.timestamp &lt; m_BufferedState&#91;i+1&#93;.timestamp)
                            Debug.Log("State inconsistent");
                    }   
                }
            }
           
            // We have a window of interpolationBackTime where we basically play
            // By having interpolationBackTime the average ping, you will usually use interpolation.
            // And only if no more data arrives we will use extra polation
            void FixedUpdate () {
    		
    			// If we're the owning player, not remote, then grab which is the largest for this net period
    			if ( _pv.isMine )
    			{
    				if ( Mathf.Abs(_pc.prevMouseYaw)  &gt; Mathf.Abs( queuedyaw ) )
    		            queuedyaw = _pc.prevMouseYaw;
    				if ( Mathf.Abs(_pc.prevMousePitch) &gt; Mathf.Abs(queuedpitch) )
                		queuedpitch = _pc.prevMousePitch;
    				if ( Mathf.Abs(_pc.prevMouseRoll) &gt; Mathf.Abs(queuedroll) )
        	        	queuedroll = _pc.prevMouseRoll;
    		
    				_pc.prevMouseYaw = 0;
    				_pc.prevMousePitch = 0;
    				_pc.prevMouseRoll = 0;
    			}
         
                if (m_BufferedState&#91;0&#93;.timestamp == 0)
                    return;
    		
                // This is the target playback time of the rigid body
                double interpolationTime = PhotonNetwork.time - m_InterpolationBackTime;
               
                // Use interpolation if the target playback time is present in the buffer
                if (m_BufferedState&#91;0&#93;.timestamp &gt; interpolationTime)
                {
    				//Debug.Log("Performing Smoothing");
    			
                    // Go through buffer and find correct state to play back
                    for (int i=1;i&lt;m_TimestampCount;i++)
                    {
                        if (m_BufferedState&#91;i&#93;.timestamp &lt;= interpolationTime || i == m_TimestampCount-1)
                        {
                            // The state one slot newer (&lt;100ms) than the best playback state
                            State rhs = m_BufferedState&#91;i-1&#93;;
                            // The best playback state (closest to 100 ms old (default time))
                            State lhs = m_BufferedState&#91;i&#93;;
         
                            // Use the time between the two slots to determine if interpolation is necessary
                            double length = rhs.timestamp - lhs.timestamp;
                            float t = 0.0F;
                            // As the time difference gets closer to 100 ms t gets closer to 1 in
                            // which case rhs is only used
                            // Example:
                            // Time is 10.000, so sampleTime is 9.900
                            // lhs.time is 9.910 rhs.time is 9.980 length is 0.070
                            // t is 9.900 - 9.910 / 0.070 = 0.14. So it uses 14% of rhs, 86% of lhs
                            if (length &gt; 0.0001){
                                t = (float)((interpolationTime - lhs.timestamp) / length);
                            }
         
                            // if t=0 =&gt; lhs is used directly
                            transform.localPosition =
                                Vector3.Lerp (transform.localPosition,
                                              Vector3.Lerp (lhs.pos, rhs.pos, t),
                                              interpolationConstant);
                            transform.localRotation =
                                Quaternion.Slerp (transform.localRotation,
                                                  Quaternion.Slerp(lhs.rot, rhs.rot, t),
                                                  interpolationConstant);
                            rigidbody.velocity =
                                Vector3.Lerp (rigidbody.velocity,
                                              Vector3.Lerp (lhs.velocity, rhs.velocity, t),
                                              interpolationConstant);
                            rigidbody.angularVelocity =
                                Vector3.Lerp (rigidbody.angularVelocity,
                                              Vector3.Lerp (lhs.angularVelocity, rhs.angularVelocity, t),
                                              interpolationConstant);
    					
    						// Normally on the remote avatar we wouldn't process controls, but for this to perfectly smooth
    						// we set the controls on the avatar (we don't read them from Input, and allow the controls to update
    						// this is not fully required but helps further improve smoothing
    						_pc.mouseYaw = Mathf.Lerp (lhs.yaw, rhs.yaw, t);
    						_pc.mousePitch = Mathf.Lerp (lhs.pitch, rhs.pitch, t);
    						_pc.mouseRoll = Mathf.Lerp (lhs.roll, rhs.roll, t);
    						_se.shipspeed = Mathf.Lerp (lhs.speed, rhs.speed, t);
                            return;
                        }
                    }
                }
                else
                {
    				//Debug.Log("Performing Prediction");
    			
                    float dt = (float)(PhotonNetwork.time - m_BufferedState&#91;0&#93;.timestamp);
                    Vector3 extra_pos = m_BufferedState&#91;0&#93;.pos + m_BufferedState&#91;0&#93;.velocity * dt;
         
                    float angle = m_BufferedState&#91;0&#93;.angularVelocity.magnitude;
                    Vector3 axis = m_BufferedState&#91;0&#93;.angularVelocity / angle;
                    Quaternion extra_rot = m_BufferedState&#91;0&#93;.rot * Quaternion.AngleAxis (angle * dt, axis);
         
                    transform.localPosition = Vector3.Lerp (transform.localPosition, extra_pos, interpolationConstant);
                    transform.localRotation = Quaternion.Slerp (transform.localRotation, extra_rot, interpolationConstant);
         
                    rigidbody.velocity = Vector3.Lerp (rigidbody.velocity, m_BufferedState&#91;0&#93;.velocity, interpolationConstant);
                    rigidbody.angularVelocity = Vector3.Lerp (rigidbody.angularVelocity, m_BufferedState&#91;0&#93;.angularVelocity, interpolationConstant);
         
    				_pc.mouseYaw = m_BufferedState&#91;0&#93;.yaw;
    				_pc.mousePitch = m_BufferedState&#91;0&#93;.pitch;
    				_pc.mouseRoll = m_BufferedState&#91;0&#93;.roll;
    				_se.shipspeed = m_BufferedState&#91;0&#93;.speed;
                }
            }
        }
    
  • Hi,

    I have tested all these solutions including the original 'ThirdPersonNetwork.cs' which is delivered with the 'DemoWorker' example. The jitter is always there... sometimes its interpolated. But I get never a really smooth movement. I like to use Photon in my 'neXt - CGM rc Heli Simulator'.

    I think, the initial problem must be fixed. The send rate must match the frame rate or (sometimes better) the half of the frame rate.

    I've changed them in 'PhotonNetwork.cs':
    private static int sendInterval = 16;
    private static int sendIntervalOnSerialize = 16;

    Is the failure on my site? 1000 / 16 = 62.5 times per second ?

    All the best,
    Klaus
  • Is the failure on my site? 1000 / 16 = 62.5 times per second ?
    Which failure? What happens?
    We usually check the send queue when OnSerialize was called, so there is less delay due to that. Setting the sendInterval to 16 might not be needed.

    What you attempt is the brute-force approach of networking.

    Even if you send every frame, the delay and potential variance in delivery of the messages won't go away. If the pure network roundtrip takes ~100ms, all actions are still delayed this much on the remote machines, no matter how many updates you write. Worse: Some updates might only take 80ms and some 120ms. If you don't smooth this (or fix this inconsistency otherwise), you still end up with non-linear updates from the other machines.
    If you are this sensitive to timing and updates, you can't assume they will happen consistently every frame (or second frame).

    You need to analyze how you can better anticipate movements of remote clients. Maybe send additional info like velocity and direction. Depending on the game, it might also be easier to send input and replicate the reactions to that, instead of sending the current state in high frequency (cause the amount of input it more or less constant, while an update/frame is more than an update every 10 frames).
  • Klaus,

    You cant send every frame, network technology is no where close to being able to keep up. Multiply that by each player and its even worse. You need to rely on smoothing and prediction for best results. The last example I posted above works perfectly for me so I'd double check your implementation. I have both human and AI all being syncd this way with no more jitter.
  • Thank you very much for your explanations... now I begin to understand.
  • Now I made a video of the problem I have. I used the original example code which is delivered with the 'Photon 20 Free Cloud' license. I'm located in Europe and selected the cloud region 'EU'. The ping is also visible.

    versionPUN = "1.22.1"

    http://www.youtube.com/watch?v=2baEiS93WMU&feature=youtu.be

    The sync problem is every second. I see that also in the 'Worker Demo' example.

    My computers are connected to a high speed cable network and I can play other network games in the same configuration without any problems.

    What's going wrong? Do you have any ideas?
  • Hm, that doesn't look like a networking issue really. At least none I can reproduce. I was just running this in Unity.
    Are you running the unmodified samples?
    You could try to update PUN but I doubt there have been changes that fix anything that could cause it.
    If you got Unity Pro you could try and run the profiler, too.
  • I could be wrong but looking at your sample (I really like how you put that together by the way) it looks like you're running at about 5 fps. Assuming you have a normal frame rate (60+/-) then something is wrong with how the slerp and/or interpolation is being calculated. I'm not sure why you have both interpolation and slerp, as that's what slerp does, but I assume you're just using two different calculations for the same thing. When you interpolate between two points you need to smooth the movement over time. If you're hard coding time instead of the real time that could be a problem. For smoothing you want the time in the packet itself to compare the previous packet time to the current packet time. If the packet took 300 ms to be delivered, then smooth over 300 ms. 300/60 fps = 5 frames to smooth before the next packet should arrive. then if it doesn't arrive you start predicting at the same rate. If it comes sooner you end the smoothing where you're at, and restart from their to the new point.

    I'm not seeing the jitter in your example, at least not the kind of jitter I originally experienced. For me the jitter actually caused the object to move in a way that resembled moving forward+3, backward-1, forward+3, backward-1

    You look like you're continually moving forward, but its just not smooth. Try the lerp first, then worry about prediction later.
  • Thank you very much for coming back to me.

    @Quadgmin: That's not mine... it's an example project which I have downloaded from Unity3D asset store. It's name is 'Photon Unity Networking Free', version 1.22.3, September 06, 2013. This demo is running with 60 fps. You see the naked problem in the upper line (Transformation Sync). And I think it's doesn't make any sense to interpolate that. In the other lines, the problem is interpolated, but still there. It's far away from a perfect movement.

    Now I have double checked everything:
    - deleted all old files of the PUN demo
    - downloaded it again from Unity 3D AssetStore and imported it into an empty project
    - I've added my AppID in the PUN Wizard window, selected the region 'EU' and saved it
    - then I built the App and started it on a second computer

    When I start the App and select there 'Synchronization Demo' I still get the same result :-(

    I use the 20 CCU Photon Demo Account. When I log in and take a look at the settings on your webpage, I see this: 'This app is on the free plan. We recommend you upgrade before using it in production.'
    I don't like to purchase server power while I don't know if this is working as expected?

    @Tobias: I'm on Unity Pro v4.2.1f4 (OSX and Windows), I don't see a problem in the Profiler. Is there anything I have forgotten? Any setup changes in the PUN files?

    I have also checked again all the other hardware I used by testing a network game to make sure it's not my computers, router, service provider, ...

    Of course, when that is running, I'll purchase server power from Photon before I release the next update of my App which brings online meetings.
  • I wouldn't worry about purchasing any server power before things are ready for release, the free version should be fine for this level of testing.

    I haven't played with the Photon Sync demo. When I get some time I'll try and load it and have a look. Are you using all the sample code from the demo? PUN uses an override for the Observed property, just like Unity does. The override points to a user defined class that in my code sample above is called NetworkRigidBody, but can be called anything. Are you using the overridden code, or that native code in the demo? To use this code you must either replace the native demo code, or update the project to change the observed to use this new class/component.
  • I used the sample code as it is. It's only a 8 MB download. There is everything ready for use.

    In my simulator, I've tested your NetworkRigidBody script (without input controls because that doesn't make sense in a simulation). Your script interpolates way better than the methods in the Photon samples. But the jitter which occurres every second is still visible.
  • ahh, when I used Photon they didn't have those samples I don't think.

    So I changed the lerp code just a little. try this and let me know if it's a little better. It's just smoothing, no prediction so you'll have to add that yourself, but I think the smoothing is a little better, at least to my eyes.
    using UnityEngine;
    using System.Collections;
    
    &#91;RequireComponent(typeof(PhotonView))&#93;
    public class CubeLerp : Photon.MonoBehaviour
    {
        Vector3 prevCorrectPos = Vector3.zero;
        Vector3 latestCorrectPos = Vector3.zero;
    	double prevtime=0;
    	double timestamp = 0;
    
        public void Awake()
        {
            if (photonView.isMine)
            {
                this.enabled = false;//Only enable inter/extrapol for remote players
            }
    
            latestCorrectPos = transform.position;
        }
    
        public void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info)
        {
            // Always send transform (depending on reliability of the network view)
            if (stream.isWriting)
            {
                Vector3 pos = transform.localPosition;
                stream.Serialize(ref pos);
            }
            // When receiving, buffer the information
            else
            {
                // Receive latest state information
                Vector3 pos = Vector3.zero;
                stream.Serialize(ref pos);
    			
    			prevCorrectPos = latestCorrectPos;
                latestCorrectPos = pos;
    			
    			prevtime = timestamp;
    			timestamp = info.timestamp;
            }
        }
    
        // This only runs where the component is enabled, which is only on remote peers (server/clients)
        public void Update()
        {
            transform.localPosition =
                   Vector3.Lerp (transform.localPosition,
                                 latestCorrectPos, (float)(timestamp-prevtime));
    	}
    }
    
    
  • You are right... that is looking way better ('Lerp' row) than the original code.

    But I have noticed, the Photon cloud runs faster now. So the 'Interpolation' row looks perfect at the moment.
  • Quadgnim: As this seems to be very helpful for Klaus, would you mind if I updated our code in the sample accordingly?
  • no not at all. I was just playing around, and I also think the update should be a FixedUpdate instead, what do you think?

    Also, take out the PrevCorrectPos, I put it in there, but don't use it.
  • Ok, cool. Then I'll play around with this a bit before the next update.

    About FixedUpdate() :
    Doesn't it do about the same as the regular Update() if you factor-in the time that actually passed since Update()?
    I didn't use FixedUpdate() so far. Maybe I should.
  • The main change I did was to not use Time but instead use the time from the network packet instead, so we need a consistent timing in the Update calls.
  • Hi,

    now I understand the things behind... thank you very much for explaining that. I played with the new 'CubeLerp' code a bit and found out the problem itself.

    When you interpolate by using the time between two movement positions and the start and endpoint, you should never change the starting position during interpolation. When you do this, you'll get always the jerky movement when a object is moving with constant speed.

    I have already added the PrevCorrectPos and a modified counter for the time. I used a send rate of 10 times per second. Now I have to add a stack because the end time is not known and cannot be correct, so I have to shift one step behind. That's mostly no problem... for collision detection we can use the Valve technic (collision boxes without interpolation).

    All the best,
    Klaus
  • Hi,

    I have also noticed, the time which is is used for interpolation varies a lot. So, we always have to use the correct time for interpolating between two states.

    I'll post it when I have solved it before.

    That's the reason why the interpolation quality differs... it's depending to the cloud servers. When the following packages is coming too late, the object stays still for one or two frames which causes the jitter in my video. The duration between the jitters matches the serial send rate.

    BTW: FixedUpdate cannot solve this. That's only used when you are working with physic calculations and add to the rigidbody a force, velocity, ... I have done some basic checks with 1 package per second... then you see it much better.

    All the best,
    Klaus
  • two different things we're talking about.

    1. fixed update is necessary for smoothing not prediction, and should be used IMHO for the interpolation between packets.

    2. the issue of the object pausing when the next packet arrives late is called prediction. My tweak to the Photon sample doesn't provide prediction, but it's easy enough to do. If you don't receive the next packet as expected, then continue interpolating in the same direction, expecting that the player kept moving the same way. If the player stopped or changed direction you'll compensate once the next packet finally arrives.
  • I just took some time to think about this and ... finally understood what made our sample algorithm look so bad.
    :D

    The problem so far:
    While we lerped from our position to the target, we not only increased the fraction (how far we got from A to B) but also moved our A in every update.
    This looked bad: The cube moved faster and slowed down, even if updates were relatively constant (timing wise). This was worse when updates came in before we reached the target.

    A solution:
    When we get an update, we remember "where we are on update" as O and save the "last known position" as B. We also reset how far we got (as our new origin is different).
    In the following frames until next update, we have to somehow count up from 0.0f to 1.0f. This can be realized with Update() and deltaTime or FixedUpdate() and it looks relatively smooth.
    The current position should be ignored while Lerping from O to B.

    This is much better than before.
    The new code is attached and will be in next PUN update.

    Of course, it's not perfect. In this algorithm, we always change speed of the remote cubes to not fall behind the last update too much.
    An alternative would be to fix speed but then maybe never reaching the position we got from last update.