1
\$\begingroup\$

I have created a progress bar system that lerps the value over a duration to provide some life to the progress bars.

I've done this using a coroutine that gets called each time a value (such as life or exp) gets modified.

The problem with this is, if the coroutine gets called in rapid succession, i.e. before the previous instance of the coroutine is finished, the lerped values inside the progress bar get messed up. They get fixed at the next coroutine call, but it kind of bothers me, so if someone can provide some insight into this, or maybe a workaround or an alternative way of accomplishing this, I would very much appreciate it.

The code for the coroutine I'm using is this:

private IEnumerator FillLerp(int currExp, int expNeed)
{        
    float lerpDuration = 2f;
    float lerpSpeed = 3f;
    float i = 0f;
    float amountToFill = (float)currExp / expNeed;

    while (i < lerpDuration)
    {
        yield return null;
        _experienceBarFill.fillAmount = Mathf.Lerp(_experienceBarFill.fillAmount, amountToFill, Time.deltaTime * lerpSpeed);
        i += Time.deltaTime;
    }
}
\$\endgroup\$
1
  • \$\begingroup\$ I use the DoTween asset. Among many other extension methods (for transform tweening and whatnot) it contains one for the fill amount of ui image. In general I try to minimize depending on assets but DoTween is awesome and makes things surprisingly simple. \$\endgroup\$
    – Nikaas
    Nov 6 '20 at 8:00
0
\$\begingroup\$

I often like to tackle this with the following pattern, where I store the coroutine instance while it's in progress, and stop it if I need a new one.

Coroutine _fillAnimation;

void AnimateFill(float targetFill, float duration) {
    if(_fillAnimation != null)
        StopCoroutine(_fillAnimation);

    _fillAnimation = StartCoroutine(AnimateFillCoroutine(targetFill, duration);
}

IEnumerator AnimateFillCoroutine(float targetFill, float durationForCompleteFill)
{       
    float startFill = _experienceBarFill.fillAmount;

    // We'll scale our lerp speed so the total duration of the animation is
    // proportionate to how much the value has changed.
    float speed = 1.0f / (durationForCompleteFill * Mathf.Abs(targetFill - startFill));
     
    for(float t = 0; t < 1f; t += speed * Time.deltaTime) {
        // I've replaced your exponential ease-out Lerp with a quadratic version that
        // completes in a finite number of steps, and is easier to correct for deltaTime.
        // (The exponential one never quite reaches its target)
        float progress = 1f - t;
        progress = 1f - progress * progress;
        _experienceBarFill.fillAmount = Mathf.Lerp(startFill, targetFill, progress);
        yield return null;
    }

    // Account for any loose change to ensure we end exactly where we want.
    _experienceBarFill.fillAmount = targetFill;
    // Clear the coroutine so we know we don't need to interrupt it next time.
    _fillAnimation = null;
}
\$\endgroup\$
3
  • \$\begingroup\$ This seems to be exactly what I needed! ^_^ My brainstormed solution was to add an if-check that would branch out the execution of the coroutine into reducing or increasing fill based whether the values were added or subtracted, like in my comment on the first answer :) \$\endgroup\$ Nov 5 '20 at 19:57
  • \$\begingroup\$ Another strategy I'll use sometimes is to have the coroutine read straight from the live member variable tracking the state I want to animate - currentXP here - and have it continue moving its local "chase" value until they match. Then, if I change the XP and there's already a coroutine animation running, I don't have to cancel it and start a new one - updating my member variable just moves the goalpost it's moving toward, and it will pick up that change and see it through. \$\endgroup\$
    – DMGregory
    Nov 5 '20 at 20:04
  • \$\begingroup\$ I have a similar thing, where a class wraps a coroutine and knows if it's still running. Upon calling a new start it can choose to stop the current coroutine, ignore the new start or queue the new one after current one finishes. It helped in so many different situations. \$\endgroup\$
    – Nikaas
    Nov 6 '20 at 8:11
0
\$\begingroup\$

Well, there are two main ways you can do this. The first and probably easier way is to not use a coroutine at all and have a central update. Something like.

private void Update() {
    if (desiredFillAmount > _experienceBarFill.fillAmount) {
        _experienceBarFill.fillAmount = Mathf.Min(desiredFillAmount, _experienceBarFill.fillAmount + Time.deltaTime * 0.1f);
    } else if (desiredFillAmount < _experienceBarFill.fillAmount) {
        _experienceBarFill.fillAmount = Mathf.Max(desiredFillAmount, _experienceBarFill.fillAmount - Time.deltaTime * 0.1f);
    }
}

The other way is that you can instead create your own coroutine runner that instead of playing coroutines all at once plays them sequentially by putting them in a queue.

So instead of StartCoroutine(FillLerp()) you'd have something like CoroutineRunner.instance.run(FillLerp()). It may sound hard, but all you really need to do is something like:

public void runCoroutine() {
    if (coroutineQueue.Count > 0) {
        if (!coroutineQueue.Peek().MoveNext()) {
            coroutineQueue.Pop();
        }
    }
}
\$\endgroup\$
1
  • \$\begingroup\$ Coroutine queues are a bit outside my comfort zone :) That being said, while using a central update is not a desirable option for me, your if checks gave me an idea! :) Basically in my example, I would also add a check: while(i < lerpDuration && _experienceBarFill.fillAmount < amountToFill)... This will ensure the previous instances of the same coroutine will end and the new one will take over (so long as the values are going up and not down xDD ) This creates another conundrum to solve, i.e. reversing the check values when they're going down and not up without creating a separate coroutine \$\endgroup\$ Nov 5 '20 at 19:32

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.