Firebase and Tasks
The Firebase Unity SDK makes judicious use of asynchronous logic for many of its calls. Unity itself isn’t super resilient to threaded logic, with most of the classes and functions in the UnityEngine
namespace just flat out throwing exceptions if invoked off of the main Unity thread. My goal with this post is to provide you the tools you need to not only safely use Firebase’s asynchronous function calls, but to do so in a way that best suits your own programming style and preferences. Ideally even giving you more confidence to thread other parts of your game to provide your players with the smooth and responsive gameplay they expect from a modern video game.
Let’s get started with a very innocent looking demo script:
using Firebase;
using Firebase.Auth;
using UnityEngine;
using UnityEngine.Assertions;
public class FirebaseContinueWith : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
Debug.Log("Checking Dependencies");
FirebaseApp.CheckAndFixDependenciesAsync().ContinueWith(fixTask =>
{
Assert.IsNull(fixTask.Exception);
Debug.Log("Authenticating");
var auth = FirebaseAuth.DefaultInstance;
auth.SignInAnonymouslyAsync().ContinueWith(authTask =>
{
Assert.IsNull(authTask.Exception);
Debug.Log("Signed in!");
var successes = PlayerPrefs.GetInt("Successes", 0);
PlayerPrefs.SetInt("Successes", ++successes);
Debug.Log($"Successes: {successes}");
auth.SignOut();
Debug.Log("Signed Out");
});
});
}
}
The first thing I do is ensure that Firebase’s dependencies are available on the player’s device with CheckAndFixDependenciesAsync
. Note that I’m not really handling any failure cases in this example. This shouldn’t be an issue for this post, but you’ll want to do more than assert in your own games.
Next I use ContinueWith
to create a continuation and I start signing in anonymously with SignInAnonymouslyAsync
.
When sign-in completes, I figure out how many times this script has run successfully before by reading PlayerPrefs. Then I increment this value, and write it back out before logging the new number of successes.
This is all super straightforward. I run it and… I just see the log “Signed In!” then nothing. What happened?
Threads
Firebase does a lot of work that’s dependent on I/O. This can either be out to disk, or even out to the network. Since you don’t want your game to lock up for potentially many seconds for network latency, Firebase uses Tasks to perform much of this I/O work in the background.
Whenever you continue from this work, you have to be careful to come back into your game in a graceful manner. I’ve done none of that here, and have just charged right into a shared resource managed by the UnityEngine in the form of a call to PlayerPrefs
. This most likely raised an exception, but it even got lost in the background thread! What can you do to fix it?
Scheduling with Continuations
C# has the concept of a TaskScheduler. When you say ContinueWith
, rather than just letting it continue on whatever thread the task completed on, you can use a TaskScheduler to force it onto a specific thread. So, I can modify the example to cache the TaskScheduler
on which Start()
was called. Then I pass that into the ContinueWith
statement to be able to safely change the state of objects in my game:
Debug.Log("Checking Dependencies");
var taskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
FirebaseApp.CheckAndFixDependenciesAsync().ContinueWith(fixTask =>
{
Assert.IsNull(fixTask.Exception);
Debug.Log("Authenticating");
var auth = FirebaseAuth.DefaultInstance;
auth.SignInAnonymouslyAsync().ContinueWith(authTask =>
{
Assert.IsNull(authTask.Exception);
Debug.Log("Signed in!");
var successes = PlayerPrefs.GetInt("Successes", 0);
PlayerPrefs.SetInt("Successes", ++successes);
Debug.Log($"Successes: {successes}");
auth.SignOut();
Debug.Log("Signed Out");
}, taskScheduler);
});
Since Start executes on the Unity main thread, I grab the scheduler with TaskScheduler.FromCurrentSynchronizationContext()
. This way I can get back to the main thread later by passing the scheduler into my second ContinueWith
statement. Now whatever work I do in that ContinueWith
block will be done in sequence with the game rather than in parallel with it, preventing any threading issues.
When I run the script, I can see that I finally have one success (and that this script hasn’t succeeded before).
This pattern is really common so Firebase provides an extension method named ContinueWithOnMainThread
that does all of that hard work for you. If you’re using a newer version of the Firebase Unity SDK, you can write the above as simply:
Debug.Log("Checking Dependencies");
FirebaseApp.CheckAndFixDependenciesAsync().ContinueWith(fixTask =>
{
Assert.IsNull(fixTask.Exception);
Debug.Log("Authenticating");
var auth = FirebaseAuth.DefaultInstance;
auth.SignInAnonymouslyAsync().ContinueWithOnMainThread(authTask =>
{
Assert.IsNull(authTask.Exception);
Debug.Log("Signed in!");
var successes = PlayerPrefs.GetInt("Successes", 0);
PlayerPrefs.SetInt("Successes", ++successes);
Debug.Log($"Successes: {successes}");
auth.SignOut();
Debug.Log("Signed Out");
});
});
A word of caution as well. In C#, you can safely assume that anything in your ContinueWith
block hasn’t been garbage collected. The same doesn’t hold true with Unity’s design. If you were to access any fields of this MonoBehaviour
or its encompassing GameObject
after OnDestroy
is invoked, you would want to check that this
hasn’t become null. Due to the way Unity implemented this as well, you cannot do so with the ??
operator.
Hopefully I’ve shed a little light on what’s happening in these tasks and continuations in Unity. You may also be a little frustrated now. What should be a simple block of code where we fix dependencies, sign on, then do work has become this ugly mess of nested statements that just becomes harder to read as we chain more steps into the logic. If only there were a better way!
Async/Await
Although the goal of tasks is to perform operations in parallel, so much logic in programming is sequential. Since continuations get hard to read, C# provides a mechanism in async/await syntax to represent this sequential logic. To use this mechanism, I’ll rewrite the Start
method like this:
async void Start()
{
Debug.Log("Checking Dependencies");
await FirebaseApp.CheckAndFixDependenciesAsync();
Debug.Log("Authenticating");
var auth = FirebaseAuth.DefaultInstance;
await auth.SignInAnonymouslyAsync();
Debug.Log("Signed in!");
var successes = PlayerPrefs.GetInt("Successes", 0);
PlayerPrefs.SetInt("Successes", ++successes);
Debug.Log($"Successes: {successes}");
auth.SignOut();
Debug.Log("Signed Out");
}
The first thing you’ll notice is that I denote Start
as async. This tells the C# compiler “this function will perform work in the background. Do something else whilst it finishes up.”
Then, I replace ContinueWith
with the await
keyword. If I were doing anything with the Task
’s result, I could store the result in a variable.
This reads much better, but why doesn’t my code break like the very first sample? It turns out that async
functions will always return to the thread they’re await
ed on. This way you don’t have to be as careful about thread safety in functions where you do this. In fact, by default, async
functions that are await
ed will typically execute on the thread that called them unless the developer explicitly did something else.
There is one downside compared to the ContinueWith
sample though: the code following CheckAndFixDependenciesAsync
will execute on the Unity main thread rather than potentially running on a background thread. In practice, this won’t be much of an issue. It could be a behaviour of note if you’re doing some significant amount of work between calls to await
. Be aware as well that this code is very similar to the continuation example above. Just like how Unity may clean up your underlying MonoBehaviour
before ContinueWith
executes, Unity may clean it up when the call to await
completes. If you access any member fields after a call to await
, you should check to ensure this
is not yet null.
The Unity Way
Unity has the concept of coroutines, which used to be the preferred method of performing asynchronous work across multiple frames. The interesting bit about Coroutines is that they’re not really asynchronous, behind the scenes they simply generate IEnumerators which are evaluated on the main thread.
Unity has some special yield instructions such as WaitForEndOfFrame and WaitForSeconds, allowing you to jump around to different moments in your game’s time. I choose to implement a new CustomYieldInstruction to wait for a task to complete. I’ve even seen some developers convert something like this into an extension method on the Task
class itself!
using System.Threading.Tasks;
using UnityEngine;
public class YieldTask : CustomYieldInstruction
{
public YieldTask(Task task)
{
Task = task;
}
public override bool keepWaiting => !Task.IsCompleted;
public Task Task { get; }
}
I can now use yield return
on a Task
, such as the Task
s typically used for Firebase, to make my asynchronous logic read sequentially. Inside it, I wait for a task to complete with a standard continuation. If I were actually doing something with the result of a Task, I’d have to build this class out a little more. For the time being, this will work to illustrate my basic point.
I can then reimplement my async/await logic using coroutines like this:
using System.Collections;
using System.Collections.Generic;
using Firebase;
using Firebase.Auth;
using UnityEngine;
public class FirebaseCoroutine : MonoBehaviour
{
void Start()
{
StartCoroutine(DoWork());
}
private IEnumerator DoWork()
{
Debug.Log("Checking Dependencies");
yield return new YieldTask(FirebaseApp.CheckAndFixDependenciesAsync());
Debug.Log("Authenticating");
var auth = FirebaseAuth.DefaultInstance;
yield return new YieldTask(auth.SignInAnonymouslyAsync());
Debug.Log("Signed in!");
var successes = PlayerPrefs.GetInt("Successes", 0);
PlayerPrefs.SetInt("Successes", ++successes);
Debug.Log($"Successes: {successes}");
auth.SignOut();
Debug.Log("Signed Out");
}
}
My Start
function now just immediately calls a function called DoWork
, which is my coroutine.
Since I’m not doing anything with the return value of the async function calls, I just allocate temporary YieldTask
objects and return them in the DoWork
coroutine.
Now there are some important pros and cons to consider if you use this type of logic. This will have a performance hit as not only does the work between each yield return
call execute on the main thread, but the property keepWaiting
is queried every frame. On the other hand coroutines only exist for as long as a MonoBehaviour
hasn’t been destroyed. This means that those caveats I mentioned above with having to check for null after an await
or inside a ContinueWith
don’t apply to coroutines!
Queues, Queues Everywhere!
Sometimes the performance characteristics of coroutines don’t match up exactly to what you want. Remembering that a CustomYieldInstruction is queried every frame, you may end up in a state where Unity is performing many checks against against the keepWaiting
property. In this case, it may be beneficial to queue these actions on Unity thread manually by adding work to a queue when it’s ready to be processed. Note that this is effectively how ContinueWithOnMainThread
works and you should use that method when possible.
With that in mind, let’s look at an example of how I’ve implemented an action queue:
using System;
using System.Collections.Generic;
using System.Linq;
using Firebase;
using Firebase.Auth;
using UnityEngine;
using UnityEngine.Assertions;
public class FirebaseQueue : MonoBehaviour
{
private Queue<Action> _actionQueue = new Queue<Action>();
void Start()
{
Debug.Log("Checking Dependencies");
FirebaseApp.CheckAndFixDependenciesAsync().ContinueWith(fixTask =>
{
Assert.IsNull(fixTask.Exception);
Debug.Log("Authenticating");
var auth = FirebaseAuth.DefaultInstance;
auth.SignInAnonymouslyAsync().ContinueWith(authTask =>
{
EnqueueAction(() =>
{
Assert.IsNull(authTask.Exception);
Debug.Log("Signed in!");
var successes = PlayerPrefs.GetInt("Successes", 0);
PlayerPrefs.SetInt("Successes", ++successes);
Debug.Log($"Successes: {successes}");
auth.SignOut();
Debug.Log("Signed Out");
});
});
});
}
public void EnqueueAction(Action action)
{
lock (_actionQueue)
{
_actionQueue.Enqueue(action);
}
}
void Update()
{
while (_actionQueue.Any())
{
Action action;
lock (_actionQueue)
{
action = _actionQueue.Dequeue();
}
action();
}
}
}
This starts much like all of my continuation based examples. Unlike those, I call EnqueueAction
to perform work on the main thread. I would highly recommend breaking this into two different MonBehaviour
s so you don’t forget to call EnqueueAction, but I’m compressing this for illustrative reasons.
The EnqueueAction
function locks the Queue and adds some nugget of logic in the form of a C# Action into a list of logic to be executed later. If you get really clever, you may be able to just replace all of this with a lockless thread safe queue.
Finally, in Update
, I execute every enqueued Action
. It is very important to NOT execute the action with the _actionQueue
locked. If the Action
itself enqueues another Action
, you’ll end up in a deadlock.
Similar to the coroutine, this does involve checking whether or not the queue is empty every frame. Using what you’ve learned above about task schedulers and coroutines, I’m confident that you could reduce this burden with little effort if this becomes an issue.
Prescription: Unity! UniRx
Finally, there is a hip concept running around many programming circles known as reactive programming. Game developers I talk to tend to either love this or hate it, and there is enough public discourse that I won’t spend this post trying to turn you for or against this paradigm.
Reactive programming tends to favor logic that can come in streams — that is logic that you would typically register for an event or query something every frame for — and where you’ll perform functional operations on the streams as they flow through your game. For the purpose of staying consistent with the rest of this post, I’ll use it with the current example with the note that I’m not giving reactive programming its chance to shine.
So, with all that said, first I import UniRx from the Unity Asset Store. Then I have to make sure that there’s a MainThreadDispatcher
in my scene:
Now I can write my logic in UniRx form:
using Firebase;
using Firebase.Auth;
using UniRx;
using UnityEngine;
public class FirebaseRx : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
Debug.Log("Checking Dependencies");
FirebaseApp.CheckAndFixDependenciesAsync().ToObservable().Subscribe(status =>
{
Debug.Log("Authenticating");
var auth = FirebaseAuth.DefaultInstance;
auth.SignInAnonymouslyAsync().ToObservable().ObserveOnMainThread().Subscribe(user =>
{
Debug.Log("Signed in!");
var successes = PlayerPrefs.GetInt("Successes", 0);
PlayerPrefs.SetInt("Successes", ++successes);
Debug.Log($"Successes: {successes}");
auth.SignOut();
Debug.Log("Signed Out");
});
});
}
}
What’s interesting about UniRx is that I can compose behaviour to form complex interactions. A brief example of this is the call ObserveOnMainThread
, which guarantees that the following Subscribe
executes on the main thread.
For an example like this one, I would not pull in the complexities of UniRx but it’s useful to put it on your radar. If you were instead trying to build game logic around realtime database updates or periodically invoking cloud functions based on streams of events in game, you could do worse than combining UniRx and Zenject to quickly build a robust system around asynchronous logic.
Conclusion
I hope that I’ve not only given you some tools to help understand Firebase’s asynchronous API, but have empowered you to deal with them in a way that best suits your own game and coding style. I would strongly encourage you to create a small project using each of the techniques I’ve outlined here to really get a feel for the shape of each solution, and encourage you to think about ways you might improve your game’s performance using threads elsewhere. I’ve personally gotten some great mileage out of background tasks when dealing with peer to peer communication in games as well as processing and annotating screenshots without halting gameplay. I’ve even found that sometimes things like enemy AI doesn’t actually need to finish processing every frame, and it can sometimes be perfectly fine to let it run for a bit in the background over the course of a few frames.