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!