Coroutine processing faster in final build?

Has anyone ever experienced things behaving faster/slower in the final build? I have this coroutine that shakes a door when player interacts with it but in final build its (almost exactly) 2x slower than in the editor. I haven’t been able to figure out why so I’m wondering whether there is some kind of unity setting that would affect this?

    public IEnumerator Shake(Param.Attack attack) 
    {
        bool toggle = true, moving = true; float time = 0f, speed = 1f, distance = 0.1f;
        Vector3 start = transform.position;
        Vector3 current = transform.position;
        Vector3 destination;

        // figure out whether attacker is in front of or behind door
        Vector3 forward = transform.TransformDirection(Vector3.forward);
        Vector3 attacker = Vector3.Normalize(attack.attacker.position - transform.position);
        if (Vector3.Dot(forward, attacker) < 0) {
            destination = transform.position + transform.forward * distance;
        } else {
            destination = transform.position + ((transform.forward * -1f) * distance);
        }

        while(moving) 
        {
            time += Time.deltaTime * speed;
            
            if (toggle) {
                current = Vector3.Lerp(current, destination, time);
                if (time >= 1f) { time = 1f; }
                if (current == destination) { toggle = false; time = 0f; }
            } else {
                current = Vector3.Lerp(current, start, time);
                if (time >= 1f) { time = 1f; }
                if (current == start) { moving = false; }
            }

            transform.position = current;
            yield return new WaitForEndOfFrame();
        }

        shake = null;
    }

Or is it because of this line here…

yield return new WaitForEndOfFrame();

And the final build is limited to 60 fps and the editor runs with no vsync? If I use Time.deltaTime then FPS shouldn’t matter though right?

V-sync is usually off in the game view unless you turn it on with the option in the resolution dropdown:

You need to work out an interpolation value that is a calculation of the current time / end time.

This is generally how I’ve always lerp-ed based on time:

float start = 0f;
float end = 5f;

while (start < end)
{
	start += Time.deltaTime;
	float t = start / end;
	Vector3 p = Vector3.Lerp(a, b, t);
	yield return null;
}

Worth pointing out, if you have a coroutine you want happening on a per-frame basis, use yield return null instead as you’re just creating tons of allocations for no reason here.

Also you write code in a way that’s really hard to read.

Your code is not framerate independent, because you’re doing this:

current = Lerp(current, target, time);

That gives you an ease out, but the lower the framerate, the faster the ease approaches the end. This should be intuitive if you think about it - if you move 1/30 of the way to the destination (30fps), that moves you further than if you move 1/60 of the way twice (60fps).
A perhaps easier way to understand the same concept is that adding 10% twice is more than adding 20% once, as (1.1 * 1.1 * x) == 1.21 * x, which is not the same as 1.2 * x!

You should use a linear interpolation from start to end instead, and then add an ease function on the time variable. A nice repository of easing functions is easings.net. Here’s your code in a framerate independent manner. I also simplified your check for if the attacker is in front and your ping pong implementation, because both was kinda overly complex.

public IEnumerator Shake(Param.Attack attack)
{
    float time = 0f;
    float speed = 1f;
    float distance = 0.1f;
    Vector3 startPos = transform.position;

    bool attackerInFront = transform.InverseTransformPoint(attack.attacker.position).z > 0;// same as the dot, just more intuitive
    if (attackerInFront)
        distance = -distance;
    Vector3 destination = transform.position + transform.forward * distance;

    var endOfFrame = new WaitForEndOfFrame();

    do
    {
        yield return endOfFrame;
        time += Time.deltaTime * speed;

        var lerpTime = time < 1f ? time : 2f - time; // ping-pong
        lerpTime = 1f - (1 - lerpTime) * (1 - lerpTime); // easeOutQuad

        transform.position = Vector3.Lerp(startPos, destination, lerpTime); // note that Lerp is clamped, so there's no need to clamp time.
    } while (time < 2f);

    shake = null;
}

Note that this is still not completely framerate independent, as you will always overshoot the final time unless you hit the exact float value. But it’s a lot better than what you had, and as good as you can really get it for this case.

2 Likes

Wow thx Baste, looks very clean.

    IEnumerator Shake()
    {
        Vector3 start = transform.position;
        float shakeTime = Time.time + 0.5f; // shake for 0.5 seconds
        while (Time.time < shakeTime)
        {
            transform.position = start + transform.forward * Mathf.Sin((shakeTime - Time.time) * 30) * 0.1f; // 30 is the shake frequency and 0.1 is the shake amplitude
            yield return null;
        }
        transform.position = start;
    }