"Vector3.Lerp" might not be transforming smoothly at 30fps (on phone/tablet devices)

I have a bezier curve drawn in my game and I’m moving an object (called “blind”) around the curve, in the form of an ellipsoid, like in this picture:


I have a total of 40 points (0-39) that make up the ellipsoid, and the points are gathered in a list called “fullPathWayPoints”. Since I don’t know of a way to smoothly interpolate between two points in a path that is not linear (this method will be ideal though), what I’m doing is linearly interpolating between each point, one by one, until the object (blind) reaches the target point. This works fine in the editor, in a PC build, and in a Mac build, although one of the drawbacks of using this method is that I cannot make the object go from point A to point B in a specific time that I can set up. Thus, the way is done is by speed instead: constant speed through all the points until the object reaches its destination. The code is the following:

 private IEnumerator MoveBlind(int index, int finalPos)
    {
        tParam = 0;

        if (index >= fullPathWayPoints.Count - 1) //this one resets the list so it doesn't go beyond the count limits
        {
            while (tParam < 1)
            {
                tParam += Time.deltaTime * _speed;


                _objectPosition = Vector3.Lerp(_fullPathWayPoints[index], fullPathWayPoints[0], tParam);
                transform.position = _objectPosition;
                _currentPos = transform.position;

                yield return null;
            }
            index = -1;
        }
        else
        {
            while (tParam < 1)
            {
                tParam += Time.deltaTime * _speed;


                _objectPosition = Vector3.Lerp(fullPathWayPoints[index], fullPathWayPoints[index + 1], tParam);
                transform.position = _objectPosition;
                _currentPos = transform.position;

                yield return null;
            }

        }

        index += 1;
        if (index != finalPos)
        {
            StartCoroutine(MoveBlind(index, finalPos));
        }
        else if (index == finalPos)
        {
            Debug.Log("Finished");
        
        }

    }

Code is not perfect, but at least get the job done (dirtily). As a side question, if someone knows of a way to interpolate between two points in a way that I can set up a time from point A to point B, and also that describes the path marked by the points, it’d be super useful.

So, the thing is that in the devices I tested (iPad, Xiaomi tablet, and iPhone), the blind (object) stutters while moving across the path. All devices are capped at 30 fps, so at first, I thought that that might be the reason. But everything else moves smoothly even at 30fps, which makes me now think that it might do something to do with the code (it’s not the first time that I experienced stutters while using Lerping methods (in other projects, though)). Any help is highly appreciated.

EDIT: I capped the game on the editor at 30fps and the stutter becomes more obvious.

All of the .Lerp() methods, without exception, know absolutely NOTHING about framerate.

All .Lerp() methods are simple mathematic constructs, nothing else.

Obviously they also know absolutely nothing about what platform they are running on.

Look to your inputs / framerates / Time.deltaTime values.

Mathematically speaking, nobody knows that in the case of an ellipse or a Bezier curve. Smooth interpolation is easy for both, but since there is no exact formula for their length, there is also no exact way to interpolate in a way that the speed is constant. Of course, there are ways to approximate this better than a linear interpolation, so I’d have a look at that.

As Kurt said above, it’s just a basic math function for a Vector3. Here’s the source. You can look for the platform/render-dependent fault there if you wish. :wink: Does sound like one of your previous post TBH.

I’m not sure if it’ll help but I replied to a user a while back about moving between “waypoints” at a constant speed here. It’s not exactly what you want but might trigger some insights. I’ve attached the simple test project too.

8627598–1159380–Waypoints.zip (157 KB)

Thank you for the replies. I guess I explained myself wrong. What I should have said is that I noticed the stutters at 30 fps, not that the fps themselves are affecting the Lerp function. I wanted to point to something like this issue:
8627928--1159428--upload_2022-12-1_15-40-50.png

Which I experienced myself in other projects.

I was thinking there might be a known issue (not by me though) that affects smooth transformations when building or using the editor, therefore I thought I’d ask just to make sure before moving on to investigate other possible causes.

Since the above mentioned issue happens, afaik, only on the editor and I’m experiencing stutters on both build and editor, I’ll assume the cause is something different: I’ll have a look then at

My first hunch is that it might have something to do with the fact that I’m lerping small lines that make up a curve, and the lower the framerate, the more noticeable the steps (each linear path) that make up the ellipsoid. Although I should double-check and see if there are noticeable stutters on each step that define the curve.

Regarding the side question:

Thank you, this pointed me in the right direction.
I calculated the desired speed (the one that travels from point A to the nearest point in the desired time), and then multiplied that speed by the total waypoints the object should travel along, and it works nicely: It takes the object the same time to travel to any waypoint of the ellipsoid. Something like:

//calculate speed
        int speed = 2; //this is the desired speed in my case
        int multiplier;
        if(targetRoutePos > _currentRoutePos)
        {
            multiplier = targetRoutePos - _currentRoutePos;
        }
        else
        {
            multiplier = (40 - _currentRoutePos) + (targetRoutePos - 1);
        }
        _speed = speed * multiplier;

One small issue notice in your code is that tParam in your loop will go past one. This could lead to jitters at low framerate as the overshoot will increase with Time.deltaTime. You could replace the while(tParam<1) statement with a for loop. Something like for(float tParam=0;tParam<1;tParam+=Time.deltaTime * _speed)

Like this?

for (tParam = 0; tParam < 1; tParam += Time.deltaTime * _speed)
            {
                //tParam += Time.deltaTime * _speed;

                _objectPosition = Vector3.Lerp(fullPathWayPoints[index], fullPathWayPoints[0], tParam);
                transform.position = _objectPosition;
                _currentPos = transform.position;

                yield return null;
            }

Yes, that should do it.

Thank you. Would you mind telling me why the While-Loop leads to jitters whereas the For-Loop does not?

Hi guys. Sorry for necroing, but I noticed something wrong with this method.

The thing is that the lower the framerate, the longer it takes the object to travel along the curve to the target point.

I don’t understand why since I’m calculating it with deltaTime, as mentioned here by @digipaul

Each update, you look at a variable. If the variable is less than 1, you add to it and yield. Otherwise, you start a new coroutine to move to the next point.

Let’s say that it takes 1 second to move from one point to the next. So, after 3 seconds, we should be at the end of the third coroutine.

Let’s also say that our framerate is…1. So, at each update, the following sequence happens:

Frame 0: t=0, so we run the loop. Add 1 to t.
Frame 1: t=1, so we don’t loop. Start a new coroutine.
Frame 2: t=0, so we run the loop. Add 1 to t.
Frame 3: t=1, so we don’t loop. Start a new coroutine.

It takes us two updates to start a new coroutine, even though we should be starting a new one every single frame.

One solution would be to check if t >= 1 at the end of the loop, and to immediately break out of it if that’s true. That way, we proceed immediately after we reach the end.

However, that’s not the only issue!

Suppose our framerate is 2.5, and that we’ve fixed the previous issue. Here’s a new sequence:

Frame 0: t=0, so we run the loop. Add 0.4 to t.
Frame 1: t=0.4, so we run the loop. Add 0.4 to t.
Frame 2: t=0.8, so we run the loop. Add 0.4 to t. t >= 1, so we break the loop. Start a new coroutine.
Frame 3: t=0, so we run the loop. Add 0.4 to t.
Frame 4: t=0.4, so we run the loop. Add 0.4 to t.

Two seconds have passed, but we’re only at t=0.8.

Each time we start the coroutine, we “lose” any extra time we accumulated. As an extreme example, imagine if we had a framerate of 0.1: we’d go WAY past t=1, but only move forward one point!

My suggestion: Don’t start a new coroutine. Instead, do something a bit like this:

index = 0;
t = 0;

while (true) {
  t += Time.deltaTime;

  while (t >= 1) {
    index += 1;
    t -= 1;
  }

  if (index >= points.Count - 1)
    break;

  target.position = Vector3.Lerp(points[index], points[index+1], t);
  yield return null;
}

target.position = points[^1];

This prevents each step from “keeping the change”: if you get to t=1.1, then after switching indices, you’ll be at t=0.1.

Once the index gets too high, it sets the target’s position to the final point (since, otherwise, you’d be left a bit short), and then the coroutine exits.

foo[^1] is shorthand for foo[foo.Count-1], btw.

This makes complete sense. The fact that you noticed it is amazing. Thank you!
I think I could fiddle your solution into the previous code and fix it, but I was thinking of giving a try to this method you suggested:

Although I don’t understand it perfectly. I haven’t used the while(true) method yet, so I’m not sure about how it works. How am I supposed to tell it if it’s true or false, where do I establish its condition?
Edit: I think I figured it out myself while writing the post, but I’ll keep it so I can get a confirmation. So, what I think is that the condition will be true forever until that “if statement” that breaks the while(true). I assume that will be equivalent to false, then get out of the “while(true)” function and continue to perform the code inside the coroutine till the end of it. Please let me know if I didn’t get it right.

Another thing is, the starting index can be whatever point (from 0 to 39), and when it reaches 39, it should reset itself back to 0, to continue its way along the curve. However, I noticed that this

if (index >= points.Count - 1)
    break;

will get out of the while (true), and position the object to the end of the points (39), but it should continue if the target position is other than 39.

So, I think I’m almost done, but there’s only one thing I can’t figure out right now:

index = X /*whatever number is passed in*/;
        t = 0;

        while (true)
        {
            t += Time.deltaTime;
            while (t >= 1)
            {
                index += 1;
                t -= 1;
            }
            if (index >= points.Count - 1)
            {
                index = 0; // I think I should reset the index here, so it can go from 0 to the target Position
                break; // ****** Here I should NOT break, because I will get out of the while(true),
                       // so an alternative is needed *****
            }

            if (index == finalPos)
            {
                //Run whatever code: e.g., trigger events
                break;
            }

            target.position = Vector3.Lerp(points[index], points[index + 1], t);
            yield return null;
        }
        target.position = points[finalPos];

    }

So, in order to prevent the index to go out of bounds, I need to do a check and reset the index if it reaches the points.Count - 1. As you can see in the posted code, I have no idea how to stop the code from continuing while keep staying inside the “while(true)”

You can use the continue keyword to skip the rest of the loop without breaking out.

Also, if you want to smoothly move from point 39 to point 0, then you can do something like this:

int next = (index + 1) % points.Count;

% is the modulo operator. It computes the remainder of integer division. 39 / 40 has a remainder of 39; 40 / 40 has a remainder of 0.

If index is 39 and points.Count is 40, then next will be 0, and you’ll be able to lerp between those two points properly.

Ok, maybe this will do it:

index = X /*whatever number is passed in*/;
        t = 0;

        while (true)
        {
            t += Time.deltaTime;
            while (t >= 1)
            {
                index += 1;
                t -= 1;
            }

            if (index == finalPos)
            {
                //Run whatever code: e.g., trigger events
                break;
            }

            target.position = Vector3.Lerp(points[index], points[index + 1], t);

            if (index >= points.Count - 1)
            {
                index = 0;
                target.position = points[^1];
// not breaking, but instead, positioning the obj to the end of the points and resetting the index
                
            }
            yield return null;
        }
        target.position = points[finalPos];

I forgot about it! thanks

Which one would you recommend, using the

if (index >= points.Count - 1)
            {
                index = 0;
                target.position = points[^1];
             
            }

at the end of the while(true) after the lerp function or before (with the continue;)

PS: I’m trying to figure out how to add the % for smoothing inside the coroutine.

EDIT: I’ve done some testing and it works perfectly! (and frame independently). Thanks again. I noticed the jump from point 39 to 0, as you mentioned. So adding that % is a must for me now. I’ll reach out afterI figure it out.

I figured it out. So, since the % for the points.Count is 0 (40/40), then I just simply change

if (index >= points.Count - 1)

for

if (index > points.Count - 1)

Because there’s no index+1 going out of bounds. Now it’s working nicely.

However, there seems to be one last problem. When the speed for the obj is high and the framerate is low, the object will go past the target point and go for another round. It was going in circles in a loop, never reaching the target point, unless it hits it by chance (for example, at 10 fps sometimes it hits after 20 rounds, sometimes after 50, sometimes after 2).

I believe this issue is caused because the value of:

t += Time.deltaTime * _speed;

is greater than 1 when the delta time is high enough. So the index is added before running the subsequent code.

For instance, you can have an index of 0, so you would hopefully start interpolating from point 0 to point 1 in that frame; then you get the t value for that frame and check if it’s greater than 1 (normally it’d be less). In this case, since the delta and speed are high, the t value surpasses 1 directly and adds 1 to the index. So, for that round, it’s going as index 1 instead of index 0. If this is the case for any frame, then the index always skips a step: (1-3-5-7…etc.).

I’d just check if the index is at least as high as the target point. >= is your friend when you’re worried about skipping something!

But this doesn’t help for my use case. Sometimes you start at the end of the curve, let’s say point 33. And let’s say that the target point is 7. So the path goes: 33-34-35-36-37-38-39-0-1-2-3-4-5-6-7.

If I use the >= operator then it would stop immediately without traveling a single point

Ooh, I see. I was only imagining the basic “start at 0, stop at the last one” use-case.

I’d suggest abstracting a little. Compute a starting point, plus the distance you need to travel:

// start and end are integer indices

int count = points.Count;

// how far start and end are from each other
int delta = end - start; // 7 - 33 = -26

// If start > end, add count to get a positive value!
if (delta < 0)
  delta += count; // -26 + 40 = 14

int index = 0;
float t = 0;

// proceed as usual

To compute the two indices you’re lerping between, you’ll just do:

int i1 = (start + index) % count;
int i2 = (start + index + 1) % count;

Stop when index >= delta.

I came up with a dirty solution that involved calculating the total points to travel and subtracting one point for each index added (checked on each loop). But your solution is so elegant, to be honest, and it seems much more robust. I’ve just tested and it works pretty neatly.

I really appreciate your help. I can finally mark the thread resolved.