Pause Coroutine and keep WaitForSeconds the same?

Hey folks!

Been messing around with using Coroutines for things like Melee Attacks, and running into problems when I need to “pause” the animation of the attack, but then have the next part of the animation be triggered with the right timing while also accounting for the possibility of the player pausing during/between the attack.

Here is some sample code for the issue.

    void DoMelee()
    {
        // start the melee attack
        StartCoroutine(MeleeAttack());
    }

    public IEnumerator MeleeAttack()
    {
        // start the state here
        currentMeleeState = MeleeState.Attack;

        // play the attack anim
        meleeAnim.Play("MeleeAttack");

        yield return new WaitForSeconds(ourMeleeInfo.attackTime); // wait for wind up

        // make sure not paused, and wait if we are paused here??
        yield return new WaitWhile(() => paused);

        // then check what we hit here
        bool didHit = CheckMeleeAttack();

        currentMeleeState = MeleeState.Recovery;

        // play the recovery anim
        meleeAnim.Play("MeleeRecovery");

        yield return new WaitForSeconds(ourMeleeInfo.recoveryTime); // wait for recovery

        // make sure not paused, and wait if we are paused here??
        yield return new WaitWhile(() => paused);

        // finish up and bring back to normal
        currentMeleeState = MeleeState.NONE;

    }

I’m pausing the actual animation elsewhere, but if I pause the game after the attack has started, but before the recovery, and wait paused for some time, then when un-paused, it would just go right into the recovery. I also don’t wanna do any timescale shenanigans cause I’m having other things run on normal time scale elsewhere.

It might be easier to just rework the whole system with pausing/stopping in mind from the start. But when I’m reading about CustomYieldInstruction being able to: “also return an object in Current property, that will be processed by Unity’s coroutine scheduler after executing MoveNext() method. So for example if Current returned another object inheriting from IEnumerator, then current enumerator would be suspended until the returned one has completed.” Then I start to wonder if it’s possible.
That page’s “WaitWhile2” example seems like the right way forward, but it doesn’t show any example how I should be using “Current” anywhere.
So hoping to learn something new today instead of just finding a work around. Thanks for any help!

  • Fanttum

Just don’t use WaitForSeconds. Write your own which understands your paused variable.

// Wait for a minimum of 'expire' seconds, not counting any time paused.
IEnumerator WaitForUnpausedSeconds(float expire)
{
    float time = 0f;
    while (time < expire)
    {
        yield return null;
        if (paused)
            continue;
        time += Time.deltaTime;
    }
}

(Code above corrected to avoid endless hang.)

Then yield return WaitForUnpausedSeconds(attackTime);.

If you need to be sure to wait additional time if they’re paused at the end, even if the ‘expire’ is zero, insert this as the last line of the method above.

    while (paused)
        yield return null;
1 Like

While I thought that might be an easy enough way to make it work (although not exactly what I was hoping for), when I pause the game Unity gets stuck! So I’m not sure what “continue” is doing there, or what else could be making your solution crash it.

Yes, his example would make your game hang (not crash) because it has a bug. The key word continue can only be used inside a loop (while, for, foreach, …) and it will immediately stop the current iteration and starts the next one. So it essentially jumps right back to the beginning of your loop. Since there is no yield statement in between, you have an infinite loop that you can never leave and your program will hang. You can either move the yield to the beginning of the loop, so it’s always executed or reverse the condition and guard the increment of the time variable

Solution one

IEnumerator WaitForUnpausedSeconds(float expire)
{
    float time = 0f;
    while (time < expire)
    {
        yield return null;
        if (paused)
            continue;
        time += Time.deltaTime;
    }
}

Solution two

IEnumerator WaitForUnpausedSeconds(float expire)
{
    float time = 0f;
    while (time < expire)
    {
        if (!paused)
            time += Time.deltaTime;
        yield return null;
    }
}

Solution three

IEnumerator WaitForUnpausedSeconds(float expire)
{
    float time = 0f;
    while (time < expire)
    {
        while (paused)
            yield return null;
        time += Time.deltaTime;
        yield return null;
    }
}
2 Likes

Whoops, serves me right for typing while eating. Thanks, Bunny83, sorry Fanttum. Always check for yields in while loops.

1 Like

Thanks for the vocab and debug. #2 deff closest to how I think
Will still be nice to know if ways to do it with a CustomYieldInstruction but if it works, it works!

The CustomYieldInstruction in most cases isn’t worth it. It’s also just a separate coroutine and all you can modify is the keepWaiting property that it calls every frame. See the source of the CustomYieldInstruction.

The main problem you have is accessing your “paused” variable. When using a custom yield instruction you’re inside a separate class while coroutines act like closures and can access variables in the same class.

ps: As far as I remember, when you pause your game by setting Time.timeScale to 0, a WaitForSeconds yield should also be paused automatically. Of course if you only want to pause certain elements, you would need to use your own paused variable.

1 Like

Yeah I think I was having trouble (and having trouble understanding) when accessing the variable. So maybe that’s a wash. And also correct in not wanting to use timeScale = 0, cause some things I don’t want paused :wink:

If you wanted to use a custom yield instruction, you could have it accept a delegate in its constructor, which returns the paused state of the game when executed.

Code:

public sealed class PausableWaitForSeconds : CustomYieldInstruction
{
    private readonly float secondsToWait;
    private readonly Func<bool> isPaused;
    private readonly Stopwatch stopwatch;

    public override bool keepWaiting
    {
        get
        {
            if(isPaused())
            {
                stopwatch.Stop();
            }
            else
            {
                stopwatch.Start();
            }

            if(stopwatch.Elapsed.TotalSeconds >= secondsToWait)
            {
                Reset();
                return false;
            }
          
            return true;
        }
    }

    public PausableWaitForSeconds(float secondsToWait, Func<bool> isPaused)
    {
        this.secondsToWait = secondsToWait;
        this.isPaused = isPaused;
        stopwatch = new();
    }

    public override void Reset() => stopwatch.Reset();
}

Usage:

yield return new PauseableWaitForSeconds(3f, IsGamePaused);

It’s also worth mentioning, that using an approach where Time.deltaTime is added to a float variable every frame isn’t a very accurate way to measure time. With small wait times of only a couple of seconds, it shouldn’t be anything noticeable; however, with a wait time of a few minutes, the small loss of precision with each sum operation can add up to the measurement being off by a couple of seconds.

EDIT: Fixed keepWaiting return value being inverted, as pointed out by @Bunny83 .

1 Like

Well, this is only true when you throw away any fractional left over / overshoot which is the usual trivial approach. However you could simply keep the overshoot and take it into account for the next interval. Of course this doesn’t work when using concealed internal throw away state like a CustomYieldInstruction. Note that you’re usage of a Stopwatch has the exact same problem. Unity only advances in “frames”. So your stopwatch will also overshoot anywhere in between 0 and deltaTime. Since you also just “Reset” your stopwatch you’re loosing that amount as well.

btw, I think you have your booleans somewhat messed up. keepWaiting needs to return true every frame for the time you want to wait. You only return true when the timer expires. So you don’t wait at all. I think you want to flip the boolean values around.

1 Like

Another bug uncovered! The OP should hire you as the QA lead for this thread :smile:

Yeah, it is true that the custom yield instruction isn’t exactly accurate either. But that is an inherent problem with coroutines, them being polling-based instead of event-based. At least it is only during the last frame that some inaccuracy is introduced to the result, instead of inaccuracies piling up every single frame.

But you raise a good point, that since the whole system is inherently inaccurate, it really never should be used for anything where being very accurate is important anyways. You ideally wouldn’t want a speed runner’s finishing time to be off by half-a-second, if there happens to be a big lag spike right as they’re crossing the finishing line and the results screen is being loaded.

Still, using a timestamp based approach could be enough of an improvement in practice, to make a difference between players noticing the lack of accuracy or not in practice. So I would still go with the more accurate approach, unless there’s some reason not to.