Bye-bye callbacks, hello async-await

01/03/2018 08:00
Recently Unity introduced the option to switch to newer .NET and C# versions in our projects. We didn't dare to upgrade our project at first, but then we said... Well, what the hell, why not? What could possibly go wrong...? Right...?

how to change the target framework?

It is pretty easy. Unity explains it in their Introducing Unity 2017 article, but if you are too lazy to look it up, here's how it's done.

Open Player Settings from the Build Settings popup (File > Build Settings).
Under the Configuration section change the Scripting Runtime Version to Experimental (.NET 4.6 Equivalent).
Unity will then restart and tada! You're done.

If you prefer to use Visual Studio for coding (like I do), you need to make sure your Visual Studio Tools for Unity is also updated. I spent a good few hours trying to figure out why VS wouldn't compile my code and is insisting that I am using an older C# version. Silly me...

The Callbacks

Back to the interesting stuff! What's the deal with callbacks? How did we use them and why?

There are certain functions in our framework where we need something to finish first and only then do we want to continue with the rest of the script. Say for example there is a bucket on the floor in our scene. When the player clicks on it and commands Johnny to investigate, we want Johnny to to the following:

1. Walk to the bucket
2. Say "It's a bucket."

Obviously, we only want Johnny to say anything about the bucket, once he has arrived at the bucket. So we have to wait for the Walk function to finish, then continue with talking.

Take a look at this code snippet of our old WalkTo function:
Please note that this is not the full code, and I have omitted most parts of the walking logic as it is not relevant here!

What is happening here?
In the WalkTo() call, we set the target for the character to walk to. Then we subscribe to an event we defined: OnArrived. This will fire when the character has reached his destination. So in our event handler, first of all we unsubscribe from the event as we won't need that anymore. Then if there was an action defined in the continueWith parameter, we continue with executing the action. In each frame, we move the character closer to his destination, and if in the current FixedUpdate he arrives, we stop and fire the OnArrived event.

This is all very cool and handy, right? (The answer is: yes, it is.)

A bit of side note here. This is one of the reasons we switched from Ink to our own implementation many moons ago. With this code structure we are able to do many things at the same time. Characters in the background can walk, talk, etc in parallel without having to interrupt each other. With Ink there was no way for 2 characters to speak at the same time.

So what's the problem with this then?

One of the most used methods in our framework is the one that makes the characters speak and displays the subtitles. This one, just like WalkTo has a continueWith callback, as we want to wait for the character to finish a line before displaying the next one. The code for a long conversation looks like this...
And this is just displaying line one after another. Image if there are more complicated things to do in the meantime...

It was not the end of the world for us, because most of this ugly code was generated anyway, so we didn't have to write anything like this manually. But still, it could get complicated enough for anyone to lose their way in it.

On top of all that, you really have to be on your toes when you start defining, subscribing to and firing events like there is no tomorrow. You have to make sure you clean up after yourself. You've probably heard all about it in C# school, but in case you haven't: if you don't unsubscribe from events you don't need anymore, it can cause all sorts of mayhem. Like memory leaks. Or unnecessary lines of execution (e.g. in WalkTo example above, once you reach the current destination, you will never be interested in the character reaching any other destinations). And so on...

Async-await to the rescue!

There are numerous features in the newer C# versions which I missed sorely from Unity. Most of which I don't remember at the moment, but the async and await keywords were the first one we started using right away.

What is this async-await thing then?
It's really simple (...not!)

First of all you need to know what a Task is. Task is a C# class that defines an asynchronous operation. You can read all about in detail here. To make it simple, think about it this way. All the things that may take some time to finish, but they can happily run in the background without interrupting the main flow of your code, you can put inside a Task. Like our WalkTo logic from the example above. You can get notified once a task is finished (or cancelled) and then react to its completion. Similar to what we did with the events above.

Here comes the sorcery...
There are these two keywords in C#: async, await. If you have a Task which you want to execute and wait until it's finished and then do some things, you can use await to simplify your code a lot: it will look like any old synchronous code, but in fact, when the execution gets to the part where you "await" a Task's completion, it will start executing the Task itself, lets your main thread go about it's business as usual, and only continue with the rest of your method once the Task you are "awaiting" is completed.

Now, let's take a look at our WalkTo method from above, now ready to be used with await.
What changed? Instead of the event, we now have a TaskCompletionSource that will help us manage the Task of walking about. The WalkTo method itself now returns a Task, instead of void. And finally, in FixedUpdate we set the result of the TaskCompletionSource, instead of firing the event.

Let's see the new WalkTo in action!
When this code is executed, the character will first walk to "Point A" and only then walk to "Point B".
All you need to make sure is that you mark you methods that use await, with async. But the compiler will warn you about it anyway.

Neat, huh? Once you get your head around Tasks and how to bend async methods to your will, you just won't get enough of them!

Finally, just for fun, here is the long conversation from before, now with async-await pattern.