Friends, food, and flourishing

Async

I’ve been wanting to learn about async/await processing in Unity, without using coroutines. Why?

  • Coroutines depend on MonoBehavior, so only work in Unity’s component context. Using new() to make a class not descending from MonoBehavior is Not Supported.  Async/await doesn’t depend on MonoBehavior. It’s easy to create as many objects using async/await as you want, without MonoBehavior overhead.
  • It’s easier to pass data betwixt callers and task runners. This is harder with Coroutines.


This blog post summarizes what I’ve learned so far, through examples. There are seven examples at the moment.

  • Wait for me: a simple program that uses async to wait for short time.
  • A non-MonoBehavior class: same, but with a second class that isn’t derived from MonoBehavior.
  • Parameters: passing parameters from the caller to the async processing method, and back again.
  • A gamey async: a demon and a ghost race each other. The game manager class uses async to wait for a winner. Shows how to cancel async tasks.
  • Fetching JSON (basic): Fetch one chunk of JSON from the web.
  • Fetching JSON (multiple files): fetching several chunks. Returns could overlap, that is, ask the server for file 1 and then file 2, but file 2 arrives before file 1 (maybe it’s smaller). No worries, since we can make a new JSON grabber object for each JSON chunk we want.
  • Fetching JSON (multiple files with error control): same, but with error control. 404, timeout, whatevs.


Please comment if you see errors, or to suggest additions.

Wait for me

The simplest async Unity program. Start() waits for Waiter() to complete. Waiter() waits for Task.Delay() to complete.

Notice Waiter() returns a Task, according to the method’s signature. Err, but there’s no return statement. WTF? C#’s async magic handles that in the background.

This threw me for a bit. In JS, you might get a Promise, something passed back to the caller. C# does that, too, but it’s hidden. This makes some things easier, and other things more difficult, like cancelling tasks.

				
					using UnityEngine;
using System.Threading.Tasks;

public class ManagerController : MonoBehaviour
{
    /// <summary>
    /// Wait for a short time after start. 
    /// </summary>
    async void Start()
    {
        Debug.Log("start start");
        await Waiter();
        Debug.Log("end start");
    }
    
    async Task Waiter() {
        Debug.Log("start waiter");
        await Task.Delay(510);
        Debug.Log("end waiter");
    }
}

				
			

A non-MonoBehavior class

This version has two classes:

  • One derived from MonoBehavior, for doing GameObject stuff.
  • A stand-alone class with the async code.

Debug.Log() is part of MonoBehavior, so I implement a Report() method in the MonoBehavior class, and pass a reference to the non-MonoBehavior class. IReporter helps.

				
					namespace Custom.Scripts
{
    public interface IReporter
    {
        public void Report(string textIn);
    }
}

// ------------ CUT SCREEN HERE -------------

using System.Threading.Tasks;
using UnityEngine;

namespace Custom.Scripts.WaiterNonUnityClass
{
    /// <summary>
    /// Wait for a short time after start. 
    /// </summary>
    public class WaiterNonUnityClassController : MonoBehaviour, IReporter
    {
        async void Start()
        {
            Debug.Log("start start");
            Waiter waiter = new Waiter();
            await waiter.WaitForIt(this);
            Debug.Log("end start");
        }

        public void Report(string textIn)
        {
            Debug.Log(textIn);
        }
    }
}

// ------------ CUT SCREEN HERE -------------

using System.Threading.Tasks;

namespace Custom.Scripts.WaiterNonUnityClass
{
    public class Waiter
    {
        public async Task WaitForIt(IReporter reporter) {
            reporter.Report("start waiter");
            await Task.Delay(510);
            reporter.Report("end waiter");
        }
    
    }
}

				
			

Parameters

Now pass parameters, from the controller to the waiter, and back again. The generic Task<T> makes this work well.

				
					using System.Threading.Tasks;
using UnityEngine;

namespace Custom.Scripts.SimpleParams
{
    public class SimpleParamsController : MonoBehaviour, IReporter
    {
        async void Start()
        {
            Debug.Log("start start");
            Waiter waiter = new Waiter();
            string taskResult = await waiter.WaitForIt(this, "Doggos are the best!");
            Debug.Log($"Got: {taskResult}");
            Debug.Log("end start");
        }

        public void Report(string textIn)
        {
            Debug.Log(textIn);
        }        
    }
}

// ------------ CUT SCREEN HERE -------------

using System.Threading.Tasks;

namespace Custom.Scripts.SimpleParams
{
    public class Waiter
    {
        public async Task<string> WaitForIt(IReporter reporter, string messageIn) {
            reporter.Report("start waiter");
            await Task.Delay(500);
            reporter.Report(messageIn);
            await Task.Delay(500);
            reporter.Report("end waiter");
            return "Burt is a good doggo.";
        }
    
    }
}


				
			

A gamey async

Now a gamey thing. A race between a demon, and a ghost.

Race

Each one has a NavMeshAgent. The speed of the ghost is slightly higher than the demon. There are two destination objects near the white pillars, one for each racer. You can see them in the Hierarchy.

The Manager is the thing that controls the race. It creates a Task for each character, and hangs around until the first one finishes. It cancels the other task after that.

The Manager, destinations, and racers all have scripts. Here’s the code, with comments showing alternative implementations I found in various tutorials.

				
					using System.Threading;
using System.Threading.Tasks;
using UnityEngine;

namespace Custom.Scripts.NavigatingCharacters
{
    /// <summary>
    /// Manage a race betwixt a demon and a ghost.
    /// </summary>
    public class NavigatingCharactersController : MonoBehaviour
    {
        [SerializeField]
        private Destination demonDestination;
        [SerializeField]
        private Destination ghostDestination;
        async void Start()
        {
            // Create a cancellation token needed to cancel tasks. Here, we're using one token source
            // source for all tasks.
            CancellationTokenSource cancelTokenSource = new CancellationTokenSource();
            // Or we could use a separate token for each task...
            // CancellationTokenSource demonCancelTokenSource = new CancellationTokenSource();
            // CancellationTokenSource ghostCancelTokenSource = new CancellationTokenSource();
            
            // Start the tasks, putting them in an array, so we can easily wait for one
            // to complete.
            Task<string>[] tasks = new Task<string>[2];
            tasks[0] = demonDestination.WaitForArrival(cancelTokenSource);
            tasks[1] = ghostDestination.WaitForArrival(cancelTokenSource);
            // Or, if using separate token sources...
            // tasks[0] = demonDestination.WaitForArrival(demonCancelTokenSource);
            // tasks[1] = ghostDestination.WaitForArrival(ghostCancelTokenSource);
            
            // Wait for one of the tasks could complete, returning the one that did.
            // Note: we could send both tasks are individual params of WhenAny...
            // await Task.WhenAny(demonTask, ghostTask);
            // ... but the array way is more general. We could also use a list, instead
            // of an array.
            // Wait for one of the tasks to complete. Cancel the others later.  
            Task<string> winnerTask = await Task.WhenAny(tasks);
            // We could use a try/catch to watch for exceptions, like file read failures, or whatevs.
            // See the code in Destination for more deets on throwing exceptions.
            // Task<string> winnerTask = null;
            // try
            // {
            //     winnerTask = await Task.WhenAny(tasks);
            // }
            // catch (OperationCanceledException e)
            // {
            //     Debug.Log("Task was canceled.");
            // }
            // One of the tasks returned. Get the returned string.
            string winnerName = winnerTask.Result;
            // Report the winner.
            Debug.Log($"We have a winner: {winnerName}");
            // Cancel the tasks that are still running.
            cancelTokenSource.Cancel();
            // If using separate tokens for each task, cancel both.
            // demonCancelTokenSource.Cancel();
            // ghostCancelTokenSource.Cancel();
            cancelTokenSource.Dispose();
            // Dispose of both, if you have two.
        }
        
    }
}

// ------------ CUT SCREEN HERE -------------

using System;
using UnityEngine;
using UnityEngine.AI;

namespace Custom.Scripts.NavigatingCharacters
{
    /// <summary>
    /// A racer.
    /// </summary>
    public class Character : MonoBehaviour
    {
        [Tooltip("Where the character is racing to.")]
        [SerializeField]
        private Destination destination;
        // The agent controlling the racer's movement.
        private NavMeshAgent _navMeshAgent;

        private void Start()
        {
            _navMeshAgent = GetComponent<NavMeshAgent>();
            // Let's go!
            _navMeshAgent.SetDestination(destination.transform.position);
        }

        // If we don't want to use a collider on the destination, we could
        // use this approach. However, I don't like adding Update()
        // methods without good cause.
        // private void Update()
        // {
        //     // Are we there yet?
        //     if (_navMeshAgent.remainingDistance <= _navMeshAgent.stoppingDistance)
        //     {
        //         // Debug.Log("Stopping");
        //         destination.characterArrived = true;
        //     }
        // }
        
    }
}

// ------------ CUT SCREEN HERE -------------

using System.Threading;
using System.Threading.Tasks;
using UnityEngine;

namespace Custom.Scripts.NavigatingCharacters
{
    public class Destination : MonoBehaviour
    {
        [Tooltip("The character racing for this destination.")]
        [SerializeField]
        private GameObject character;
        
        // Has the character arrived?
        private bool _isCharacterArrived = false;

        public void OnTriggerEnter(Collider other)
        {
            if (other.gameObject.CompareTag("Character"))
            {
                if (character == other.gameObject)
                {
                    _isCharacterArrived = true;
                }
            }
        }

        /// <summary>
        /// The task.
        /// </summary>
        /// <param name="cancellationTokenSource"></param>
        /// <returns>The name of the character who won.</returns>
        public async Task<string> WaitForArrival(CancellationTokenSource cancellationTokenSource)
        {
            // Loop until a character has arrived.
            while (! characterArrived)
            {
                // Has the race manager asked that the task be cancelled?
                if (cancellationTokenSource.IsCancellationRequested)
                {
                    // Yes.
                    Debug.Log($"Canceling {character.gameObject.name} ");
                    // Return outta here.
                    return null;
                    // Or throw an exception.
                    // The throw is special, somehow. It doesn't cause Unity to 
                    // make a log entry when it is thrown.
                    // cancellationTokenSource.Token.ThrowIfCancellationRequested();
                }
                await Task.Yield();
            }
            return character.name;
        }
    }
}
				
			

Here’s what the console shows:

Console

Fetching JSON (basic)

This code fetches one JSON file from the interwebs. It’s a starting point. The JSON format is defined in the Monters class. It isn’t included below, since it doesn’t matter for our purposes.

				
					using UnityEngine;

namespace Custom.Scripts.JsonFetchBasic
{
    public class JsonFetchController : MonoBehaviour
    {
        private async void Start()
        {
            // URL of monster data for CryptidKitchen.
            string url = "https://ck-gamecontent.cryptidkitchen.net/storage/monsters.json";
            // Make a new fetcher. A class I made, not based on MonoBehavior,
            // so new()ing one isn't a problem.
            WebStringFetcher webStringFetcher = new WebStringFetcher();
            // Wait for the fetcher to grab the data.
            string fetched = await webStringFetcher.GetStringFromWeb(url);
            // Deserialize.
            Monsters monstersCollection = JsonUtility.FromJson<Monsters>(fetched);
            Monster[] monsters = monstersCollection.monsters;
            // Show the monsters.
            foreach (Monster monster in monsters)
            {
                Debug.Log(monster.label);
            }
        }
    }
}

// ------------ CUT SCREEN HERE -------------

using System.IO;
using System.Net;
using System.Threading.Tasks;

namespace Custom.Scripts.JsonFetchBasic
{
    public class WebStringFetcher
    {
        /// <summary>
        /// Get a string from a URL.
        /// No error control yet.
        /// Adapted from https://www.red-gate.com/simple-talk/development/dotnet-development/calling-restful-apis-unity3d/
        /// </summary>
        /// <returns>The string</returns>
        public async Task<string> GetStringFromWeb(string url)
        {
            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
            HttpWebResponse response = (HttpWebResponse)(await request.GetResponseAsync());
            StreamReader reader = new StreamReader(response.GetResponseStream());
            string result = reader.ReadToEnd();
            return result;
        }
    }
}

				
			

Fetching JSON (multiple files)

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut elit tellus, luctus nec ullamcorper mattis, pulvinar dapibus leo.

				
					using System.Collections.Generic;
using System.Threading.Tasks;
using UnityEngine;

namespace Custom.Scripts.JsonFetchMultiple
{
    public class JsonFetchMultipleController : MonoBehaviour
    {
        private async void Start()
        {
            // URL of monster data for CryptidKitchen.
            string monstersUrl = "https://ck-gamecontent.cryptidkitchen.net/storage/monsters.json";
            // Jokes URL.
            string catFactsUrl = "https://official-joke-api.appspot.com/random_ten";
            // Make new fetchers, one for each URL.
            WebStringFetcher monstersFetcher = new WebStringFetcher(monstersUrl);
            WebStringFetcher jokesFetcher = new WebStringFetcher(catFactsUrl);
            // Start the tasks, putting them in an array, so we can easily wait for one
            // to complete.
            Task<string>[] tasks = new Task<string>[2];
            tasks[0] = monstersFetcher.GetStringFromWeb();
            tasks[1] = jokesFetcher.GetStringFromWeb();
            // Wait for all to complete.
            string[] fetchedCollection = await Task.WhenAll(tasks);
            // Deserialize monsters.
            Monsters monstersCollection = JsonUtility.FromJson<Monsters>(fetchedCollection[0]);
            Monster[] monsters = monstersCollection.monsters;
            // Show the monsters.
            Debug.Log("Monsters...");
            foreach (Monster monster in monsters)
            {
                Debug.Log(monster.label);
            }
            // Deserialize jokes.
            // We get a serialized array from the server. JsonUtility only works 
            // with objects. So, wrap what we got to make JsonUtility happy.
            string serializedJokes = "{\"jokes\": " + fetchedCollection[1] + "}";
            Jokes jokesCollection = JsonUtility.FromJson<Jokes>(serializedJokes);
            Joke[] jokes = jokesCollection.jokes;
            // Show the jokes.
            Debug.Log("Jokes...");
            foreach (Joke joke in jokes)
            {
                Debug.Log(joke.setup);
                Debug.Log(joke.punchline);
            }
        }
    }
}

// ------------ CUT SCREEN HERE -------------

using System.IO;
using System.Net;
using System.Threading.Tasks;

namespace Custom.Scripts.JsonFetchMultiple
{
    public class WebStringFetcher
    {
        private string _url;
        
        public WebStringFetcher(string urlIn)
        {
            _url = urlIn;
        }
        
        /// <summary>
        /// Get a string from a URL.
        /// No error control yet.
        /// Adapted from https://www.red-gate.com/simple-talk/development/dotnet-development/calling-restful-apis-unity3d/
        /// </summary>
        /// <returns>The string</returns>
        public async Task<string> GetStringFromWeb()
        {
            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(_url);
            HttpWebResponse response = (HttpWebResponse)(await request.GetResponseAsync());
            StreamReader reader = new StreamReader(response.GetResponseStream());
            string result = reader.ReadToEnd();
            return result;
        }
    }
}
				
			

Fetching JSON (multiple files with error control)

This code fetches several JSON files from the interwebs. Each fetch is managed by a separate instance of the fetching class. Each one can report:

  • The string that was fetched, or…
  • An error from the server, like a 404, or…
  • There was a timeout (the server didn’t respond).

The string is interpreted as serialized JSON, but it could be anything.

How complex the error control is depends on how much detail you want about the error. Code could just say “Something bad happened,” or give details about exactly what failed. I’m going for the easy approach here.

The code is mostly the same as the last example. Start() in the main class has a try/catch to handle errors. WebStringFetcher.GetStringFromWeb() adds a timeout to its HttpWebRequest, and throws an exception if it can’t create a StreamReader.

				
					using System;
using System.Threading.Tasks;
using UnityEngine;

namespace Custom.Scripts.JsonFetchMultipleErrorControl
{
    public class JsonFetchMultipleErrorControlController : MonoBehaviour
    {
        private async void Start()
        {
            // URL of monster data for CryptidKitchen.
            string monstersUrl = "https://ck-gamecontent.cryptidkitchen.net/storage/monsters.json";
            // Jokes URL.
            string catFactsUrl = "https://official-joke-api.appspot.com/random_ten";
            string[] fetchedCollections = null;
            // Catch webbish exceptions for all tasks.
            try
            {
                // Make new fetchers, one for each URL.
                WebStringFetcher monstersFetcher = new WebStringFetcher(monstersUrl);
                WebStringFetcher jokesFetcher = new WebStringFetcher(catFactsUrl);
                // Start the tasks, putting them in an array, so we can easily wait for one
                // to complete.
                Task<string>[] tasks = new Task<string>[2];
                tasks[0] = monstersFetcher.GetStringFromWeb();
                tasks[1] = jokesFetcher.GetStringFromWeb();
                // Wait for all to complete.
                fetchedCollections = await Task.WhenAll(tasks);
            }
            catch (Exception e)
            {
                // Not graceful. Make it graceful in a real app.
                Debug.Log($"Argh! Exception! {e.Message}");
                return;
            }
            // Deserialize monsters.
            Monsters monstersCollection = JsonUtility.FromJson<Monsters>(fetchedCollections[0]);
            Monster[] monsters = monstersCollection.monsters;
            // Show the monsters.
            Debug.Log("Monsters...");
            foreach (Monster monster in monsters)
            {
                Debug.Log(monster.label);
            }
            // Deserialize jokes.
            // We get a serialized array from the server. JsonUtility only works 
            // with objects. So, wrap what we got to make JsonUtility happy.
            string serializedJokes = "{\"jokes\": " + fetchedCollections[1] + "}";
            Jokes jokesCollection = JsonUtility.FromJson<Jokes>(serializedJokes);
            Joke[] jokes = jokesCollection.jokes;
            // Show the jokes.
            Debug.Log("Jokes...");
            foreach (Joke joke in jokes)
            {
                Debug.Log(joke.setup);
                Debug.Log(joke.punchline);
            }
        }
    }
}

// ------------ CUT SCREEN HERE -------------

using System;
using System.IO;
using System.Net;
using System.Threading.Tasks;

namespace Custom.Scripts.JsonFetchMultipleErrorControl
{
    public class WebStringFetcher
    {
        // Timeout at 2 minutes.
        private const int TimeoutLengthMs = 120_000;
        private readonly string _url;
        
        /// <summary>
        /// Constructor. Send in a URL.
        /// </summary>
        /// <param name="urlIn"></param>
        public WebStringFetcher(string urlIn)
        {
            _url = urlIn;
        }
        
        /// <summary>
        /// Get a string from a URL.
        /// Adapted from https://www.red-gate.com/simple-talk/development/dotnet-development/calling-restful-apis-unity3d/
        /// </summary>
        /// <exception cref="WebException">A Bad Thing happened.</exception>
        /// <exception cref="InvalidOperationException">Making StreamReader failed.</exception>
        /// <returns>The string</returns>
        public async Task<string> GetStringFromWeb()
        {
            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(_url);
            // Set the timeout. If it is exceeded, request will throw a WebException.
            request.Timeout = TimeoutLengthMs;
            HttpWebResponse response = (HttpWebResponse)(await request.GetResponseAsync());
            StreamReader reader = new StreamReader(response.GetResponseStream() 
                ?? throw new InvalidOperationException());
            string result = await reader.ReadToEndAsync();
            return result;
        }
    }
}

				
			

Maybe async, maybe not

Say your game has some playing code, called Playie, and some data loading code, Loadie. Playie wants to get some data, Settie, and run Playie.DoSomething(Settie).

Playie says:

“Hey, Loadie! Give me data set Settie!”

Then Playie returns to what it was doing. That is, it’s a nonblocking call to Loadie.

Loadie doesn’t have Settie, so it asks the interwebs for it. Eventually, Settie comes down the ol’ internet pipe. Loadie remembers the data, and says to Playie:

“Got Settie, mate!”

Playie stops what it was doing, and runs Playie.DoSomething(Settie).

A few minutes later, Playie says:

“Hey, Loadie! Give me data set Settie!”

Loadie already has it, so immediately it says:

“Got Settie, mate!”

Playie runs Playie.DoSomething(Settie).

So, two calls to Loadie.GetSettie(). The first one goes async. The second is done sync.

One way to do this is for Loadie, not Playie, to call Playie.DoSomething(Settie). If Loadie has the data, it calls Playie.DoSomething(Settie) immediately. If not, it sends an interwebs request, waits, and calls Playie.DoSomething(Settie) when the data is ready.

In the code below, the role of Playie is played by SpeakingObjectController. The role of Loadie is played by InitializationDataLoader. puntato.json is our guest star, playing Settie, a bunch of puns about potatoes, in a JSON data set called Speeches. (Plus a supporting cast of interfaces.)

				
					
public interface ISpeakingObject
{
    ...
    /// <summary>
    /// Called when a Speeches file has been loaded from the ether.
    /// </summary>
    /// <param name="fileName">The name of the file.</param>
    public void SpeechesLoaded(string fileName);
}

public interface ICKInteractable
{

    /// <summary>
    /// The player wants to interact with an object.
    /// </summary>
    public void InteractWithPlayer();
}

// Playie. Attached to an object the player can interact with.
public class SpeakingObjectController : MonoBehaviour, 
    ICKInteractable, ISpeakingObject
{
    ...
    // Reference to Loadie.
    private DataLoader _dataLoader;
    ...
    /// <summary>
    /// Player wants to see a speech (e.g., a pun about potatoes).
    /// </summary>
    public virtual void InteractWithPlayer()
    {
        ...
        // Load the speech file. 
        // fileName is the string "puntato.json" in this example.
        _dataLoader.LoadSpeeches(fileName, this);
        // When _dataLoader is done, it will call back.
    }
    
    /// <summary>
    /// DataLoader finished loading a Speeches file.
    /// </summary>
    /// <param name="fileNameSpeechIsFrom"></param>
    /// <param name="speech"></param>
    /// <exception cref="CKException"></exception>
    public void SpeechesLoaded(string fileNameSpeechIsFrom)
    {
    ...
    }
}

// Loadie. Loads data from the interwebs.
public class DataLoader
{
    ...
    /// <summary>
    /// Load a JSON Speeches file from the ether.
    /// </summary>
    public void LoadSpeeches(string fileName, ISpeakingObject caller)
    {
        ...
        // _speechesMap is a Dictionary remembering speeches 
        // loaded from the interwebs.
        // Speech already loaded?
        if (_speechesMap.ContainsKey(fileName))
        {
            // Speech set is loaded. Sync call to Playie.
            caller.SpeechesLoaded(fileName);
        }
        else
        {
            // Speech set is not loaded.
            // Get it. LoadJsonSpeechesFile calls the caller
            // when it's done.
            LoadJsonSpeechesFile(fileName, caller);
        }
    }
    
    /// <summary>
    /// Load a JSON Speeches file from the ether.
    /// </summary>
    /// <param name="jsonFileName">Name</param>
    /// <param name="caller">Who to tell when it's done.</param>
    private async void LoadJsonSpeechesFile(
        string jsonFileName,
        ISpeakingObject caller)
    {
        // Make a URL.
        string speechesUrl = $"{Utilities.GameContentFilesUrlStart}{JsonsUrlFolderName}{jsonFileName}.json";
        // Make a fetchy thing.
        WebStringFetcher speechesFetcher = new WebStringFetcher(speechesUrl);
        // Hey, fetchy! Get it! I'll wait...
        string jsonString = await speechesFetcher.GetJsonStringFromWeb();
        // Parse what fetchy returned.
        Speeches speeches = JsonUtility.FromJson<Speeches>(jsonString);
        // Remember it.
        _speechesMap[jsonFileName] = speeches;
        // Hey, caller! Got yer data!
        caller.SpeechesLoaded(jsonFileName);
    }
    ...
}

				
			

Error control is missing. Loadie should start a timer, and forgedaboud its interweb task when the timer fires, or (forgedaboud the timer when the task completes). That will handle interweb errors.

Playie should also start a timer, and forgedaboud its call to Loadie after a while (or forgedaboud the timer if Loadie returns). That will take care of other problems that hang Loadie. In case there are any. Which there shouldn’t be, but… you know.

Leave a Reply

Your email address will not be published. Required fields are marked *

css.php