Async Programming

Complex code made simple.

Async programming is a compiler feature of the C# language that rewrites what appears to be linear code into a state machine. This makes it much easier to write and understand complex gameplay logic.

In async programming, state is tracked using the natural idioms of the language. We design the workflow based on the natural primitives of the language - we insert the await prefix in places where we want to stop execution, and our application resumes execution on the tick where it left off.

ToKey see this in action, we override the Update method in Actor, as illustrated by the following code:

protected override async Task Update (CancellationToken token)
{
	
}

A behavior that uses async styles might look like this:

protected override async Task Update (CancellationToken token)
{
    while (Health > 0){
	// chase and shoot
	while (Target.Visible && Ammo > 0){
	    await MoveTo (ComputeApproach (Target));
            await AnimateAsync ("shoot");
            await ShootAt (Target);
            await AnimateAsync ("reload");
	}

	// out of ammo, try to reload
        if (Ammo == 0){
	    // Shows Task.WaitAny to await for multiple operations:
	    // either finds a recharge station, or try to use a 
            // teleport to quickly escape
            await Task.WaitAny (FindRechargeStation (), FindTeleportStation ())
        }
        await MoveTo (ComputePatrolPoint ());
    }
    //
    // We get out of the loop, now run a new loop to show
    // blinking before we vanish
    //
    var original = TintColor;
    for (int i = 0; i < 3; i++){
        TintColor = Yellow;
        await Task.Delay (200);
        TintColor = original;
        await Task.Delay (200);
    }

    // Play the final animation, and then die
    await AnimateAsync ("destroy");
    Destroy ();
}

This code snippet controls an actor's weapon shooting and animations. You can see how the await keyword is useful here: in the first while loop, you can see a sort of state machine described--first, the Actor attempts to MoveTo() its target. When that is done, a shooting animation is started with AnimateAsync(), followed by a shooting behavior through ShootAt(). Once the shooting is done, a reload animation is started. These states are looped until the target is no longer visible or the actor runs out of ammo. All of these states are traversed across a large number of frames, instead of each method being called every tick.

Low-Level Details: TickSynchronizationContext

During the execution of the Update method, the thread's synchronization context is replaced with a game-specific synchronization context that will resume execution on the next Tick.

Any operation that is queued inside the Update method will not be dispatched by default into the default synchronization context (which would typically have been just an idle handler on the main loops).

Instead, the operations are only resumed and executed when Unreal invokes the ReceiveTick event. This ensures that our code runs in the context that Unreal expects it to run.

If we need to queue tasks from the Update method to be executed in a different synchronization context, we need to manually request that.