I really like coroutines. Like with most things, they should be applied in well-considered quantities, but when applied they can really expand what you can achieve in just a few lines of code. Particularly in our frame-bound world.
However there has always been this little subset of extra things, which I would have liked to have seen supported by Unity coroutines out of the box.
“Hang on, weren’t you just there?”
Yep. I was and terrible as the excuse might be, there was always something that seemed shinier or more on fire.
Then at Unite 13, Devin and Matthew of Twisted Oak Studios did an excellent talk about extending coroutines by using extension methods to insert a proxy layer between coroutines and the Unity coroutine-system. Unfortunately I didn’t catch the talk live (I don’t see many talks at conferences), but shortly after I could catch it on YouTube. Technology, yay!
In any case that really ended my supply of excuses, other than work.
Devin and Matthew had specifically implemented the setup to allow coroutines to provide a return value, provide a nicer stopping mechanism, as well as provide a useful way of handling exceptions thrown during the execution of coroutines. This provided an excellent platform to work off and did tick three items off my wishlist, but didn’t quite deliver the whole package.
It is an interesting talk and if you have the time, you should give it a look.
The Whole Package
So I slacked off doing work for a bit and somewhere in Q2 2014 got started writing my own implementation of extended coroutines, with the extra bits and bops on my list.
This here is the list:
- Coroutines providing return values (yay! free!)
- Handle exceptions thrown during coroutine execution (yay! free!)
- Useful stopping behaviour (yay! free!)
- Built-in soft stop.
- Better built-in communication between caller and coroutine.
- Easier way of intermixing threading and coroutines.
- Built-in simple load balancing.
Soft Stop
Straight up stopping a coroutine is fine, but dependent on what it was doing while being stopped, it might leave a mess behind. The idea with a soft stop mechanism is that in stead of directly stopping the coroutine, the external controller in stead requests that the coroutine stops.
This allows the coroutine to monitor the state of the requested stop flag at appropriate points in its execution and terminate with the necessary cleanup in response.
Caller – Coroutine Communications
But why stop the communications at just requesting a nice stop? What if the work produced by the coroutine could have relevance throughout its execution?
By making the result of the coroutine accessible before the completion of the coroutine, we gain the capability of not just reading it early on, but also contributing to its building from outside of the coroutine.
Threading
Sufficiently intense tasks, working on data types not tied to the UnityEngine, API can benefit from threading. Unfortunately, most of the time things aren’t that cleanly cut out, and only part of the task is suitable for threading.
At that point the alternatives are to either forget about partially threading the task or to bridge the two types of concurrency system. Since the latter rarely turns out very pretty, the former is often chosen.
Building threading right into the coroutine system makes the choice easy.
Simple Load Balancing
Finally, heavy tasks which for one reason or another cannot be threaded (perhaps the very nature of the task requires dealing with the UnityEngine API), can in stead be frame sliced inside a coroutine.
However the logic for doing so can be more or less pretty and writing boilerplate is boring. Clearly this would be great to also have built into the base coroutine system.
Usage
So that is the full wish list implemented. Now let’s take a look at some quick example snippets.
Result
The base interface for using the extended coroutines is to start them via the new generic version of MonoBehaviour.StartCoroutine
.
For running regular coroutines, the only other difference is that in stead of yielding directly to the return value of StartCoroutine<T>
, you yield to the .Coroutine member of its CoroutineData<T>
return value.
This is also the reference which gives you access to the result of the coroutine execution.
Exception
Since the coroutine calls are executed by the coroutine system and not the callee, normally exceptions just get swallowed up by the coroutine system and logged out.
The approach of Devin and Matthew, as implemented here, is to swallow the exception, break coroutine execution and then re-throw the same exception when next the coroutine comes into contact with the caller – namely when its result is accessed.
Stop
In stead of relying on the strange, existing methods of stopping coroutines and the limitations they come with (such as only stopping a coroutine initially started by string parameter or stopping all coroutines on a behaviour), the CoroutineData<T>
reference yields another possibility.
Since it effectively represents an instance of an executing coroutine, adding a Stop
method to it becomes much more intuitive.
Soft Stop
Requesting that a coroutine stops, requires a line of communication between the caller and coroutine. This is provided in the form of the instance of CoroutineData<T>
.
However in order to effectively pass this, the calling convention on the coroutine is slightly different. Rather than passing the return value of the coroutine to StartCoroutine<T>
, the coroutine is passed as a delegate, taking CoroutineData<T>
as its only parameter.
The rest of the flow is exactly as with Stop
, except RequestStop
is used by the caller and the coroutine periodically checks CoroutineData<T>.ShouldStop
.
Threading
The interface for threading is really very simple. Two new yield classes have been added WaitForWorkerThread
and WaitForMainThread
. Yielding to an instance of either will do exactly as advertised. Yielding break or the result or throwing an exception will terminate the thread and the coroutine.
When yielding WaitForSeconds
on the worker thread, the thread will sleep for the specified amount of seconds. And finally the CoroutineData<T>
holds a flag accessor to determine the thread state of the coroutine.
Load Balancing
Load balancing could be done in a bunch of different ways and I would encourage you to add your own (I would love to see your ideas as well). So I decided to just do the simplest I could think of.
WaitIfFrameTime
will yield until the next frame if the current frame has taken longer than the specified amount of time. This way it effectively sets down a max frame time while utilising all the time leading up to that cap.
Fin
And that is it! I hope you find some use of these extensions to the extensions and do let me know if you come up with some cool further extensions.