An Interruptible YieldInstruction
Coroutines are a fundamental building block of Unity scripting. In 5.3, we got a new class to make them more powerful: CustomYieldInstruction
. Today we’ll look at it and see if we can make an arbitrarily-interruptible YieldInstruction
so our coroutines can abort the things they yield
. Read on to see how and to compare against the old 5.2 way!
Today’s article is inspired by a comment asking just this question: how can I interrupt a yield instruction? For example, if you yield return new WaitForSeconds(60)
then can you stop the yield
after only 15 seconds? Do you have to wait for the whole minute to get control back to your coroutine function?
Strictly speaking, the answer is “no”. Once you’ve yielded then there’s no taking it back without stopping the whole coroutine. However, you can design a replacement for classes like YieldInstruction
that can be interrupted. In 5.2, you’d do something like this:
public static class WaitForSecondsIterator { public static IEnumerable Run(float numSeconds) { var startTime = Time.time; while (Time.time - startTime < numSeconds) { yield return null; } } } IEnumerator Coroutine() { foreach (var cur in WaitForSecondsIterator.Run(3)) { if (Input.GetMouseButtonDown(0)) { break; } yield return cur; } }
Now the coroutine only yields null
instead of a WaitForSeconds
. This means that Unity will resume the coroutine the very next frame rather than waiting for the specified number of seconds. We can capitalize on this opportunity by performing whatever logic we want to on each frame. In this case, WaitForSecondsIterator.Run
checks the Time.time
whenever it’s resumed. If it hasn’t been long enough, it yields. Otherwise, it stops.
The loop over WaitForSecondsIterator.Run
also gets an opportunity to perform some logic. Each iteration it checks to see if the mouse button is down. If it is, it stops yielding by breaking out of the loop. Otherwise, it keeps yielding.
This is a lot more code than a one-liner yield return new WaitForSeconds(60)
, but we’ve got custom control now. It really didn’t grow by much more than the extra logic we wanted to add (the if
check), so it’s definitely manageable. We also got a reusable WaitForSecondsIterator.Run
function that we can use any time we want an interruptible version of WaitForSeconds
.
Enter Unity 5.3. Now we have a CustomYieldInstruction
class where all we need to do is override the keepWaiting
property. Does this allow us to simplify the code to solve this problem? Let’s start with a straightforward implementation and see:
public class WaitForSecondsOrMouseButton : CustomYieldInstruction { private float numSeconds; private float startTime; public WaitForSecondsOrMouseButton(float numSeconds) { startTime = Time.time; this.numSeconds = numSeconds; } public override bool keepWaiting { get { return Time.time - startTime < numSeconds && Input.GetMouseButtonDown(0) == false; } } } IEnumerator Coroutine() { yield return new WaitForSecondsOrMouseButton(3); }
This version radically simplified the coroutine code! Now it’s just one line like the original, uninterruptible version. That’s ideal for the coroutine, but the WaitForSecondsOrMouseButton
is no longer very reusable. That’s because we’ve moved the mouse button-checking logic into the same class that checks for the time. Two very different checks are now bound together into one bundled package.
So how can we split those up to return some customization to the coroutine? Well, we can make an InterruptibleYieldInstruction
class that is interruptible by arbitrary logic. This class won’t know about mouse button presses or time, so it should be reusable by a whole variety of custom, interruptible yield instructions. Here’s what it looks like:
public class InterruptibleYieldInstruction : CustomYieldInstruction { private bool stop; public event Action<InterruptibleYieldInstruction> OnKeepWaiting; public void Stop(bool condition) { if (condition) { stop = true; } } public override bool keepWaiting { get { if (stop) { return false; } if (OnKeepWaiting == null) { return true; } OnKeepWaiting(this); return stop == false; } } }
To use it, add event listeners to OnKeepWaiting
to do your custom logic. They’ll be passed the InterruptibleYieldInstruction
instance and you can call Stop
on it with your condition. It’s similar to an assert function.
Now let’s see how WaitForSeconds
would be ported to be a InterruptibleYieldInstruction
:
public class InterruptibleWaitForSeconds : InterruptibleYieldInstruction { public InterruptibleWaitForSeconds(float numSeconds) { var startTime = Time.time; OnKeepWaiting += i => i.Stop(Time.time - startTime >= numSeconds); } }
That’s a pretty simple implementation! It’s about as simple as the WaitForSecondsIterator.Run
function was at the start of the article. But how hard is it to use in the coroutine? Let’s see:
IEnumerator Coroutine() { var waitForSeconds = new InterruptibleWaitForSeconds(3); waitForSeconds.OnKeepWaiting += i => i.Stop(Input.GetMouseButtonDown(0)); yield return waitForSeconds; }
The one-liner has expanded to three lines of code, but we’ve regained the reusability. InterruptibleWaitForSeconds
does the time check and the coroutine’s own lambda does the mouse button check. If we wanted, we could go even further and make a class that does both checks so the coroutine would be a one-liner again:
public class InterruptibleWaitForSecondsOrMouseButton : InterruptibleWaitForSeconds { public InterruptibleWaitForSecondsOrMouseButton(float numSeconds) : base(numSeconds) { OnKeepWaiting += i => i.Stop(Input.GetMouseButtonDown(0)); } } IEnumerator Coroutine() { yield return new InterruptibleWaitForSecondsOrMouseButton(3); }
So the flexibility is there to split out the interruption checks with class inheritance, lambdas in the coroutine itself, or even collections of arbitrary functions.
What do you think of InterruptibleYieldInstruction
? Do you prefer the CustomYieldInstruction
way in 5.3 or the iterator function way in 5.2? Let me know in the comments!
#1 by Timo Neu on September 6th, 2018 ·
Hey Jackson!
Thanks a lot for this one! This was exactly what I was searching for. This solution is perfectly fitin’ into my Dialog System which should wait for a given amount of seconds or an input of the user.
Best regards
Timo