Using Firebase Cloud Storage with Unity to share user generated content

A few easy steps to win the social media zeitgeist

Patrick Martin
Firebase Developers

--

If you’ve been following the indie game scene for the past couple of years, you’ve seen really cool screenshots or even animated gifs shared by fans of these games on various social media channels. As you may have seen in my previous post, I have (what I hope is) a super fun game written in Unity with sword chucks, bats and zombies that I’m sharing for internal beta tests. I really want to drive up the buzz and possibly get more testers in on this. I want some way to enable my game to go viral!

Thanks to Cloud Storage for Firebase, I have the perfect place to get started. Let’s take a look.

I will assume that you have already set up Firebase and have the Unity SDK downloaded.

To review the game, I have armed my players with the coolest and most practical actual real life and totally not made up weapon known to man: the sword chuck! When the player takes any damage, obviously never self inflicted, I take a screenshot to allow for further study and honing of their strategy.

The obvious path forward is to let my players share their high scores, challenge each other, and hopefully get some excitement going about my game.

Configuring Cloud Storage

To kick everything off, I need some place to actually share my high score. Fortunately, Firebase provides an SDK to help you manage binary data in Google Cloud Storage which seems perfect for the screenshots I capture when you lose a round.

I chose the location nam5 since it’s a multi-region location for the United States. This maximizes the availability and integrity of any data I persist to Cloud Storage.

I’ll leave the default rules in place for now:

Default rules as generated by Cloud Storage

But what do they mean?

First, this says that this is a rule for firebase.storage. Then match /b/{bucket}/o matches my storage bucket. For the most part, you should probably always leave these two lines in place.

Then match /{allPaths=**} matches anything in my bucket, storing the path in a variable named allPaths.

allow read, write: if request.auth != null says that a player can read and write anywhere in allPaths as long as they’re authenticated via Firebase Authentication. I’ll cover the bare minimum for authentication in a moment, but I cover Firebase Authentication for Unity in a bit more detail over in this video.

If you want to dig in deeper, you can read about rules here.

Persisting User Data with Unity

I’ll start the way I start anything in Unity, with a MonoBehaviour. I’ll put the full text of this MonoBehaviour at the end of the article for reference.

I designed the entry point of this MonoBehaviour to be the Trigger function:

Excerpt showing the implementation of “Trigger”

This is designed to be hooked up in the Unity Editor in response to any UnityEvent. For now it’s hooked up to a button click:

but I want to leave it open for more complex chains of events if needed in the future.

Trigger checks to see if an upload is in progress with an _uploadCorotuine field that I cache at the top of my file, and starts the actual upload in UploadData. I’ll eventually set this to null when the coroutine is over.

Looking at UploadData. I start out with some logic to pull the DeathData out of _gameOverPanel:

Snippet retrieving DeathData from GameOverPanel

Don’t worry too much about DeathData. For the purposes of the game over scene, it just exposes a few public properties.

There are two things that I want to highlight here:

First, HandleException is my general abstraction for failing out of this coroutine, you’ll see this again.

Second, I make judicious use of yield break; to exit early from a coroutine.

If you remember from the top of this post, my rules require that a player must be logged in to be able to read or write data. For now, I’ll create an anonymous user account to satisfy this requirement — that is I’ll register this installation of a game as a user rather than more sophisticated credentials:

Snippet showing inline anonymous authentication

It’s useful to note two key patterns in this block of code:

First, I check auth.CurrentUser to make sure I’m not already signed in. This is cached locally and persists between runs, so if I didn’t do this SignInAnonymouslyAsync would create a user account every time I uploaded a score! If you’d like to know more about authentication, see this video.

Second, note the yield return new WaitUntil(() => signInTask.IsCompleted);. WaitUntil is a convenience class that lets you turn any small snippet of logic into a coroutine. In this case, I’m using it to wait for a Task’s IsCompleted property to go to true. If you want to learn all about different strategies to work with threads in Unity, check out my previous blog post on the subject!

After I have a user signed in, I need to decide where I’m going to store the images they upload. For this, I define:

Snippet showing the path to the final score being serialized for the Unity editor

Which is how I indicate that scores will go under /finalScores/{UserID}/finalScore.png, restricting each user to one score. For now this means that each user will only ever have one final score posted at a time, which I’ll want to expand upon in the future.

I use FirebaseStorage.GetReference to get a reference to this path and string.Format to use C#’s own in-built string substitution library to substitute in the user id:

Snippet showing how to get a reference in Cloud Storage

With all of this in place, I can write:

Snippet for uploading a file

to upload my screenshot to Cloud Storage.

Augmenting my screenshots

A picture is worth a thousand words, but the words I want are, “that the player scored 20 points over 30 seconds before being struck by their own sword chuck.” Your first consideration might be to store every score in a Realtime Database with the associated textual metadata then referencing a cloud storage node. This is actually how Mecha Hamster stores replays associated with high scores for its leaderboard.

Since I don’t have a leaderboard for the Swordchuck Challenge and I don’t want to maintain two databases with the added complexity of keeping them in sync, I’d prefer to somehow annotate a screenshot uploaded to Cloud Storage with the context under which the image was captured. It turns out that that’s a key feature of Cloud Storage called “Custom Metadata”.

I can update my call to PutBytesAsync with a Dictionary<string,string>, to add my relevant metadata directly to the image I upload:

Example metadata to attach to a Cloud Storage reference

After hooking this all into Unity, I can find an uploaded finalScore.png in my Storage console:

As well as its associated custom metadata:

I won’t cover retrieving data from Cloud Storage in detail in this post, but it is a simple call to GetMetadataAsync to pull the metadata back out.

Now I’m ready to share my image on social media. I can see a “Download URL” also associated with my image:

Which I could also get with a call to GetDownloadUrlAsync, assuming that the user is logged in.

Securing my Player Data

For this game right now, I’ll make any high scores a player shares entirely public to drive interest into my game. I also don’t want one user to be able to replace another’s high score. The current rules don’t satisfy either of these requirements.

I’ll go back over my Storage Rules tab:

and drop in this logic:

Updated storage rules

My biggest change is to replace match /{allPaths=**} { with match /finalScores/{userId}/finalScore.png {. By doing this, I effectively lock away every other possible path in Cloud Storage.

I’ve also broken apart the rules a little bit. By saying allow read;, I say that anyone can read what’s in this path. This way, friends can share their scores on social media without requiring their friends to have accounts in my game.

allow write: if request.auth.uid = userId; is where this gets cool. If you look at my match statement again, I pull out /finalScores/{userId}/finalScore.png. This means that whatever happens to be between /finalScores/ and /finalScore.png will become the variable userId.

Then, I check if this userId value matches request.auth.uid. This variable is automatically populated with your Firebase Authentication user id if you’re logged in and using the Unity SDKs for Auth and Storage. Unless a malicious player physically has access to your phone, it won’t be possible for them to overwrite your current finalScore.png. Note that since I’m currently using anonymous authentication, it won’t be possible for a player to remove their own score either after uninstalling the game (although you can do it manually as a developer in the console or with the admin SDK).

As soon as you hit “Publish”, these rules should be live. Before doing that, let’s test these out a little. Click the Simulator button next to your rules, and let’s walk through some basic tests.

First, I’ll try to get a final score anonymously by setting:

  • “Simulation type” to “get”
  • “Location” to /finalScores/32/finalScore.png
  • “Authenticated” to off
Screenshot of the rules simulator. Testing an anonymous get request to /finalScores/32/finalScore.png

When I hit “Run”, I get a green banner saying “Simulated read allowed”.

Now, I’ll try to access any location that isn’t represented in my rules file changing “Location” to /topSecret:

Screenshot of the rules simulator now trying to get what’s at the location /topSecret

When I press “Run”, the banner turns red and indicates “Simulated read denied”.

Next, I’ll try to create a finalScore.png as an anonymous user by setting:

  • “Simulation type” to “create”
  • “Location” to /finalScores/32/finalScore.png

Note that Authentication is still set to false.

Screenshot of the rules simulator showing a failure to write a finalScore.png as an anonymous user

Even though the banner says “Null value error.” rather than “Simulated write denied”, this rule is functioning as expected. This will be reported as any other access denied message when you access it from a client, but in the simulator you’re getting a little more information about why (in this case, request.auth is null so you can’t call .uid).

Finally, let’s try writing a value with a uid that matches the pattern I wrote above by setting:

  • “Authenticated” to true
  • “Provider” to google.com
  • “Firebase UID” to 32 (matching the 32 in “Location”)
Screenshot of the rules simulator writing to user 32’s finalScore.png as user 32

And I get a green banner!

Everything looks fine! I can click publish, and I just need to work out how to share!

Wrap Up

So now, I can take a screenshot whenever a sword chuck aficionado dies. Then I annotate this with some metadata about how well they did up until that point and how they died. Finally I created a nice social media friendly link that I can share with my friends.

My next steps would be to be able to use this Cloud Storage information to provide a customized landing page for each high score that a user shared. I could do this with Firebase Hosting, or even by deep linking into the game on either Android or iOS. This could also be the start of a challenge system, perhaps by also sending a random seed up with the high scores.

I could also actually implement the SDKs for the social media platforms I plan on sharing my game to. I could even use their native SDKs as authentication providers in Firebase Authentication, also removing the reliance on anonymous authentication. Keep an eye out for my next article as I expand more on this game!

Appendix

See the entire UploadGameData.cs

--

--

Patrick Martin
Firebase Developers

I’ve been a software engineer on everything from games to connected toys. I’m now a developer advocate for Google’s Firebase.