I am a big fan of UniRx.Async. Here I will show various thing we could do with it. But first, get it here : https://github.com/neuecc/UniRx#unirxasync

UniRx.Async provides main thread async

Many associates async keyword with threads. But C#'s async is actually related to returning the control to somewhere earlier. Nothing to do with multithread stuff at all. Go here to learn more.

UniRx.Async gives you UniTask, where await on it results in it resuming at various phase in Unity later. It's like frame delaying, but more granular that you could choose somewhere in that frame. It is Unity-flavored asynchronous. Nothing runs in parallel. It just delay things and you check on them later. It is still very "Unity".

If you already used yield return new WaitForSeconds(__) in a coroutine, it already works the same way. You are waiting frame after frame until that time passed then resume at specific point in that frame over that required time. It is not waiting for exactly that time length.

Because of this UniTask got stripped all weighty ability that are related to multithreading, making it a lightweight Unity-only Task object.

All the magic is thanks to UnityEngine.Experimental.LowLevel

It is quite important to know how this package does its work so nothing seems magical. But first you will have to learn about the reality of Unity's coroutine.

How the magic starts

The bootstrap is possible thanks to RuntimeInitializeOnLoadMethod attribute hidden in PlayerLoopHelper.cs.

[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
static void Init()
{
	// capture default(unity) sync-context.
	unitySynchronizationContetext = SynchronizationContext.Current;
	mainThreadId = Thread.CurrentThread.ManagedThreadId;

	if (runners != null) return; // already initialized

	var playerLoop = PlayerLoop.GetDefaultPlayerLoop();
	Initialize(ref playerLoop);
}

Now! You can notice some caveats. It asks for the default player loop then do surgery on it so various callback points are present on every phase, which the library uses to provide you Unity-flavored async functions.

The surgery, essentially adds a queue per phase. (Precisely 7 queues, since Unity comes with 7 main phases). Everytime the phase comes to these queue it try to dequeue and do things. When you use the API, it adds a "one use" Action to these queues and dequeue at the appropriate time.

Understanding this, you can think of UniRx.Async as a direct upgrade of Unity's coroutine since it can hook up a routine from anywhere without help from any GameObject, because that invisible hand running and managing the waiting is now Unity's player loop. It is also an upgrade because you have 7 choices of running point to choose from!

The big offender is the Unity Entities package where it uses similar tactics to add things to the player loop. Unfortunately when 2 RuntimeInitializeOnLoad clashes it follows alphabet order of the assembly qualified name of the method to determine winner. This one happen to comes before Unity's Entities package's hack, so it got overwritten.

Luckily you can call PlayerLoopHelper.Initialize again to force it. So you now have both ECS loop plus UniRx.Async magic. Remember to take the ECS loop from the package, since PlayerLoop API sucks and could not get the current loop. (It can just get the default loop... wtf)

If you are adding more ECS systems manually to the World and make them loop, remember to initialize UniRx.Async again. Also! This will cancels all running UniRx.Async routines since they now have nowhere to callback to.

Default UniRx.Async timing vs Unity coroutine

When you encountered the first yield return __ in Unity's coroutine, after the requirement is satisfied in a given frame it continues the enumeration IN Update phase, but after all normal MonoBehaviour Update().

await on UniTask.Yield() with no parameter says that it should come back IN Update phase also. The same as coroutine. Who would win? The answer is UniRx.Async's things in Update comes first, then all normal MonoBehaviour Update() then finally coroutine continuation. This is because UniRx.Async inserts callback point at the first thing in each phase.

UniTask.DelayFrame(1) resumes in the next 2 frames

I made a wrong assumption at one point thinking that to wait a frame (Similar to yield return null in the case of Unity coroutine) I must use UniTask.DelayFrame(1).

Actually it meant after yielding the control to caller context and it came back again, wait 1 more frame additionally. The result is we are resuming 2 frames later.

To really do an equivalent of yield return null, use UniTask.Yield(). It is even better than yield return null since you can specify the PlayerLoop timing that it comes back! (Default to in Update phase, before everything else.)

Play mode testing upgrade with C# 7.0

See this glorious pattern I found, made possible with UniRx.Async and C# 7.0 :

[UnityTest]
public IEnumerator AwaitablePlayModeTest()
{
	yield return Task().ToCoroutine(); async UniTask Task()
	{
		await UniTask.Yield(); //Forcing UniTask timing to be BEFORE Update

		//Anything goes after this!
	}
}

Play mode test allows you to start with IEnumerator where it count as a coroutine. (There is a GameObject spawned to run the routine if you look closely on your dummy test scene.) We can exploit this to kick off UniRx.Async magic!

Pattern explanation

I sandwiched an async method returning UniTask that got turned into IEnumerator, so the test runner now know only one thing that is to wait for this one IEnumerator to finish. We then do all the testing in this async method as we wanted.

This UniTask came from local function feature of C# 7.0, crucial to allowing copy-paste to all tests without naming conflict. Then for compact code it is on the same line as the first yield return.

I use this pattern on my own Firebase interfacing tests where it needs to wait several times. (Sign in, get scores, do things, submit scores, etc.) The test would looks like hell but thanks to this async pattern the test looks linear and easy to understand utilizing Task object returned from Firebase Unity SDK + async to peel out the result.

Remember you have [UnitySetUp] and [UnityTearDown] as well, where you could do the same to get async enabled set up and tear down! The timing goes like this UnitySetUp -> NUnit SetUp -> your test -> NUnit TearDown -> UnityTearDown.

Small test update timing caveats

When that Task() run for the first time it will run ahead until it hits the first await where it returns UniTask to be turned into IEnumerator. This first run-ahead has update timing a bit inconsistent, as until the first await the timing is the same as Unity's couroutine. (IN Update, but AFTER everything else.) The test was just kicked off with classic coroutine system, not UniRx.Async yet.

By using await UniTask.Yield() the first thing in all tests, it forces everything after to be IN Update, but BEFORE everything else as discussed before about the difference of UniTask timing and Unity's coroutine timing. I notice something else may throw the timing off again, in that case you can try yielding to restore to predictable timing. Testing consistency is important.

You may choose to not care about this if your test is not sensitive of update timing. But I was testing ECS system that could be in the Update phase. Without yielding first to make the timing right some of my system missed a beat and has to wait 1 extra frame. And when my test use an another await UniTask.Yield()  then Assert the result expecting that it should be here already, it is not here yet until I wait an another frame. It depends if you considered 1 frame late as a failure in your design or not.

You can just copy paste the first await by default even if it matters or not, just in case. (One frame longer test is not going to cost your life!)