Async & Await
Async & Await is a model for asynchronous programming which is heavily used by the engine.
It allows you to do things in parallel and wait for them asynchronously while still keeping the logical flow of the code in one sequence (thus avoiding the notorious callback hell which javascript developers know all too well).
Before getting into the nitty gritty, let's look at a few examples.
cHero.Walk(100,100);
await cHero.WalkAsync(100,100);
Those 2 lines have the same logical meaning, the character will walk to (100,100), in terms of the user there's no difference.
The key to understanding the benefit for the async model, is that we don't have to await
the walk right away.
The await cHero.WalkAsync
is actually 2 parts: the WalkAsync
starts the walk and the await
waits for it to complete.
So we can split it like this:
Task walking = cHero.WalkAsync(100,100);
//We can do stuff here, while the character is walking
//Doing some more stuff
//Ok, done, now we can wait for the walk to complete before moving on
await walking;
//Now we can do more stuff, after the character finished walking
The WalkAsync
method returns a task, an asynchronous operation which we can wait for (or not), whenever we please. We can also benefit from the rich API the dot net task provides us, including ContinueWith
for chaining tasks, and waiting for multiple tasks together, either with Task.WhenAll
which will wait for all tasks to complete, or with Task.WhenAny
which will wait for the first task to complete.
We can think of a real game scenario which can be relatively difficult to code with the "classic" AGS, but trivial to code using async/await: A guard which walks in circles endlessly in the background:
private async Task guardWalk()
{
while (someConditionApplies())
{
await cGuard.WalkAsync(100,100);
await cGuard.WalkAsync(200,100);
if (cHero.X < 200)
{
await cGuard.SayAsync("Go away!!");
}
await cGuard.WalkAsync(200,200);
await cGuard.WalkAsync(100,200);
}
}
Now think of what it takes to code something like this with 'classic' AGS. You have to add and track state on each tick, remember where you were and where you're going, etc. Async & Await helps you execute things in parallel with great ease.
The Nitty Gritty
The async
keyword is used to mark a method that might run asynchronously, and the await
keyword is used to "asynchronously wait" for an asynchronous method to complete.
If I'm awaiting an asynchronous method to complete, it means that my method is also asynchronous and should therefore be marked with async
.
If I'm an asynchronous method, I can either return void
(i.e nothing), a Task
or a Task<TResult>
. If I return void
it means that my method cannot be awaited by another method, making my method a fire-and-forget method. It can be useful in some scenarios, but usually it's not desired.
So usually the method will return Task
which lets other methods await
it, or Task<TResult>
which allows the method to return an actual result (asynchronously):
private readonly HttpClient _httpClient = new HttpClient();
var text = await _httpClient.GetStringAsync(url); //Will download text from a url asynchronously and return the text
One important gotcha here, is that you might be tempted to synchronously wait (i.e block) on an asynchronous method. This can technically be done, for example:
cHero.WalkAsync(100,100).Wait();
//or:
var text = _httpClient.GetStringAsync(url).Result;
However due to how async/await is implemented behind the scene (a complicated state machine) this might lead to deadlocks (the computer hanging) in some scenarios, especially if you try doing this from the rendering thread. Therefore it is not recommended blocking on an asynchronous method unless you really know what you're doing.
The implication of this, is that usually once you go async, the entire calling chain should go async (everything calling you will go async, and so on until the end of the calling chain).
The end of the calling chain, for MonoAGS
is usually subscribing to an event. The IEvent
method allows you to subscribe to an asynchronous callback (SubscribeToAsync
) which you should use to "end" the chain.
When subscribing to an event, if you suspect you might need it to be asynchronous in the future, you can subscribe to the async version even if you are not currently async:
oBottle.Interactions.OnInteract(AGSInteractions.Look).SubscribeToAsync(onLookBottle);
private Task onLookBottle(ObjectEventArgs args)
{
cHero.Say("It's a bottle.");
return Task.CompletedTask;
}
By returning Task.CompletedTask
, I'm "faking" an asynchronous result so other methods can still await
my method, without knowing it isn't really asynchronous.
Later on, when I add my asynchronous stuff, I'll remove it:
private async Task onLookBottle(ObjectEventArgs args)
{
await cHero.SayAsync("It's a bottle.");
}