Sequencing Background Calls in Unity

Yoav Luft
Yoav Luft
Aug 9, 2017 · 4 min read

We’re using Unity to develop Zen Garden. On the one hand, the engine and framework provide a lot of things out of the box, and C# is a pretty decent programming language, at least as far as Enterprise Object Oriented languages go.

On the other hand, programming in Unity feels like going 10 years back in terms of dependencies management (there is none), language feature (it support an old variant of C#) and APIs.

Doing Stuff in the Background

One such API is the Coroutines API for running background tasks. Turns out that working with threads is rather awkward in unity, due to the fact that many APIs can only be called from the main thread. So instead, unity uses a “Coroutine” based concurrency model.

void StartCoroutine(IEnumerator coroutine)

will run the IEnumerator in an asynchronous manner. One common use for coroutines is to do animations in scripting, for example:

IEnumerator Animate(GameObject obj, Vector3 from, Vector3 to, Time time)
{
Time accumulatedTime = 0;
while (accumulatedTime < time)
{
accumulatedTime += Time.deltaTime;
obj.transform.position = Vector3.Lerp(from, to, accumulatedTime / time);
yield return null;
}
}

The yield return null statement will turn any C# function to an iterator (or generator) function. The compiler will automatically transform the function into a function that returns an IEnumerator object, that on each call to its MoveNext() method it will execute the function until it reaches a yield return x statement. The value of its Current field will be set to the value x yielded by the statement.

So by giving StartCoroutine an IEnumerator, we tell it to execute each step when the coroutine gets some time to run. This is a form of cooperative scheduling that works well on single threaded applications.

A Coroutine of Coroutines?

While using Unity’s coroutines we came across two kinds of problems. The first is how to sequence several coroutines together. For example, let us say we want to move an object through several points, how can we reuse Animate to do that?

IEnumerator AnimatePath(GameObject obj, List<Vector3> path, List<Time> timing)
{
for (int i = 0; i < path.Length; i++) // Unity doesn't support IEnumerable.Zip !
{
StartCoroutine(Animate(obj, obj.transform.position, path.Get(i),
timing.Get(i));
yield return null;
}
}

Now, I find the above code very awkward. We call StartCoroutine inside a StartCoroutine ? What if we need to use several functions? Is there a limit to this nesting? What if we need to refactor some, but not all of the code?

Tell Me When You’re Done

Another problem we had is doing something once a coroutine had completed. For example, let’s say that after we finished animating our object from one point to another, we want to perform some arbitrary action required by the calling function. This is impossible to do without rewriting Animate ! But maybe we can improve Animate to make this possible by making it accept a callback? Let's try!

IEnumerator Animate(GameObject obj, Vector3 from, Vector3 to, Time time, Action callback)
{
Time accumulatedTime = 0;
while (accumulatedTime < time)
{
accumulatedTime += Time.deltaTime;
obj.transform.position = Vector3.Lerp(from, to, accumulatedTime / time);
yield return null;
}
callback();
}

The Action type is just any function that doesn't return a value. We should actually parametrize it (to state that it doesn't receive a value, too), but we'll leave that for now.

Now, our callback solution is decent, but slightly too coupled for my taste. Our Animate function now does two, unrelated things: It animates an object from one place to another, and it calls a callback.

And here’s an advanced observation: running 2 or more IEnumerator one after another is almost the same as calling a callback when one IEnumerator completes.

Abstraction, I Choose You!

Maybe we can abstract the two tasks from one another? Lets start by chaining IEnumerators together:

IEnumerator Chain(params IEnumerator[] enumerators)
{
foreach (IEnumerator enumerator in enumerators)
{
while (enumerator.MoveNext()) return yield enumerator.Current;
}
}

Not too complicated! We’re just yielding the values from each enumerator at its turn. When the first one completes (MoveNext() returns false ) we move to the next enumerator until we had consumed them all.

OK, we can use a slightly modified version of the code that accepts IEnumerable<IEnumerator> instead of a varying number of arguments and we can that refactor our AnimatePath function easily.

From Actions To Enumerators

What about callbacks? Let’s have a look at our callback from function again:

IEnumerator Animate(GameObject obj, Vector3 from, Vector3 to, Time time, Action callback)
{
// Do something with yield
Time accumulatedTime = 0;
while (accumulatedTime < time)
{
accumulatedTime += Time.deltaTime;
obj.transform.position = Vector3.Lerp(from, to, accumulatedTime / time);
yield return null;
}
// Call the callback
callback();
}

It looks like we can turn any function into an IEnumerator quite easily by just adding a return yield null to the body of the function! Or maybe, just wrap it in a function that yields:

IEnumerator ActionToIEnumerator(Action action)
{
action();
yield return null;
}

That’s cool! So now we can combine the two things and transform any IEnumerator to an IEnumerator that calls a callback when it completes:

IEnumerator WithCallback(IEnumerator enumerator, Action callback)
{
return Chain(enumerator,
ActionToIEnumerator(callback));
}

Done! We improved our IEnumerators significantly, and we made coroutines a little bit nicer to work with as result!

Luftzig

Functional Programming, Web Dev and Games

    Yoav Luft

    Written by

    Yoav Luft

    Unlearner of Code, Movement, Thought

    Luftzig

    Luftzig

    Functional Programming, Web Dev and Games

    Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
    Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
    Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade