How to write a C# backend for a Unity game using Firebase and Google’s Cloud Run
Using Firebase with Unity for mobile development has overall been a super easy integration for me, but Cloud Functions have always been a little more challenging. Cloud functions alone are easy to work with, but I’m not as comfortable in JavaScript as I am in languages like C#. I also often need to do server-side validation of player actions, and if I can duplicate game logic on the client and server without rewriting it, I can greatly reduce my potential bug surface. Fundamental differences in each language, such as how they each handle numbers, can cause non-intuitive bugs to crop up in some of the least expected places.
Google Cloud recently launched Cloud Run as a way for you to deploy stateless containers as a cloud backend. Given that these are built on top of Kubernetes, you can use any combination of frameworks and languages you want (as long as you can Dockerize it). You just need to listen to the port specified by the $PORT
environment variable and, for our purposes, you need to respond within 60 seconds.
Since I get my run of languages, I immediately decided to try to get C# running so I can use the same language in Unity and my backend. After some research, I decided to settle on .NET Core and Kestrel for my setup.
Project Setup
As of this writing, Cloud Run is beta. After installing the Cloud SDK, you’ll have to install the beta components by typing:
gcloud components install beta
gcloud components update
If you have an existing Firebase project, you should have a corresponding cloud project at console.cloud.google.com by default. If you don’t see your project under “Recent”, try checking the “All” tab or just search for it by name.
You can now add Cloud Run to your project by selecting it in the side menu and clicking “Start Using Cloud Run”. You will need billing enabled, but the console will walk you through turning that on if you haven’t done so already.
Now we’re ready to start setting up your project! Move to an empty directory where you want to work and type:
gcloud init
to make sure you’re using your account and to select your cloud project.
Once you have your gcloud account setup, you should create a quick project to upload. Since you’re going to be using .NET Core, you should install the corresponding SDK on your computer. Now, cd into an empty directory then type into your console:
dotnet new web
to create a project and:
dotnet run
to verify that it works. If you open your browser to the port indicated, you should see “Hello World.”
Container Time!
It’s time to get this ready for use in Knative now. The generated code is nearly ready, we just need to read the $PORT
environment variable for this all to work. To do this, change:
to:
Now that your web app is ready to work with Knative, you need to create a Dockerfile to containerize it. Create a file in your project directory named Dockerfile
. Then type the following:
Change "Backend.dll"
to the name of your project’s artifact. If you’re unsure what that is, it should match the name of your csproj
file.
I’ll break this down a little bit. This uses Microsoft’s provided dotnet build environment and runtime with:
FROM microsoft/dotnet:sdk AS build-env
and:
FROM microsoft/dotnet:aspnetcore-runtime
In the build-env
, the first COPY
instruction and dotnet restore
make sure you have all the proper dependencies. Then we COPY
in our entire project and dotnet publish
it. Finally you copy the /out
directory into the runtime environment and execute the command dotnet Backend.dll
when it’s time for your container to run.
If you’ve done everything correct up until this point, you will be able to use gcloud to build your project by typing:
gcloud builds submit --tag gcr.io/$PROJECT_NAME/$CONTAINER_NAME
In my case, my $PROJECT_NAME
is flappyfirebird
and I want my Cloud Run container to be named backend
. So I typed:
gcloud builds submit --tag gcr.io/flappyfirebird/backend
If you don’t see any errors, it’s time to upload the built container! Type:
gcloud beta run deploy --image gcr.io/$PROJECT_NAME/$CONTAINER_NAME
so, again, my command is:
gcloud beta run deploy --image gcr.io/flappyfirebird/backend
Right now the only available region is us-central1
, so don’t be worried if you see that as your only region to deploy to.
When asked for your service name, enter whatever you want. I left mine as the default.
When asked to allow unauthenticated invocations, answer y
for yes (this is NOT the default).
Allow unauthenticated invocations to new service [backend]?(y/N)? y
It should take a little while, but you should eventually see the text Done
, and a link to your service. Test it out by following the URL provided:
Firebase Hosting
Congratulations, you’re now running C# in a Cloud Run container! Now it’s time to setup Firebase Hosting.
Navigate over to the Firebase Console, and select your project. If you created your project in the Google Cloud Console first, you’ll have to click “Add project” and select the corresponding project from the dropdown.
Your project must be the Blaze “pay as you go” plan since you enabled billing in the Google Cloud project.
Now that you have a project setup, open your terminal and navigate to an empty directory for your Firebase project. Make sure that you have the command line tools installed, then type the command:
firebase init
Use this menu to initialize Hosting, and optionally any other Firebase products you want to use:
After selecting the Firebase project you’re configuring, you can leave everything else as the defaults. You can type:
firebase deploy
to make sure everything is working.
The final piece of this puzzle is to point some paths over to our Cloud Run instance. Open up firebase.json
and add a new entry under "hosting"
called "rewrites"
. I’ll start by routing "/helloworld"
to my Cloud Run instance:
✔ Deploy complete!Project Console: https://console.firebase.google.com/project/flappyfirebird/overviewHosting URL: https://flappyfirebird.web.app
If you execute firebase deploy
, you can verify that it’s working by appending /helloworld
to the provided URL. In my case, this means that I type https://flappyfirebird.web.app/helloworld into my address bar.
Unity Integration
For bonus points, I’ll quickly integrate all of this into Unity. I wrote a really quick test MonoBehaviour called TestCloudRun.cs
:
As expected, it logged:
Response: Hello World!UnityEngine.Debug:Log(Object)<RunTest>d__1:MoveNext() (at Assets/Scripts/TestCloudRun.cs:18)UnityEngine.SetupCoroutine:InvokeMoveNext(IEnumerator, IntPtr)
Adding Custom Logic
Now that we have code that we can read in Unity, let’s make something a little bit more dynamic. Rather than a simple “helloworld”, let’s log the time you hit the server. To get started, we need to add a new route to our server. Add a map to the endpoint "/time"
to point to a HandleTime
function:
Then implement a function that just returns the current time:
You can test this with dotnet run
, going to localhost:5000/time
. Then just build and deploy with gcloud builds submit
and gcloud beta run
. Note that you still can’t access it by going to /time
on your Firebase Hosting site like you can with /helloworld
.
We can open firebase.json
and add /time
as another path to our Cloud Run container, but instead we’ll use a wildcard to catch all requests and direct them:
Although this may look like I’m redirecting all traffic to my Cloud Run container, thus mostly negating the use of Firebase Hosting, this isn’t as all-encompassing as you may think. Notably:
- Static assets always take precedence over rewrites, so you’ll still be able to get to your index.html.
- Rewrites are applied in order. If you had a few that went to a different container or a Cloud Function, they’ll be hit instead of ”/*” if they match an incoming request first.
If you go to your site’s time
endpoint (for me this was https://flappyfirebird.web.app/time), you should see the current server time! If you refresh the page you see… the same time as before. Why is this? If you run curl -vs https://flappyfirebird.web.app/time
you’ll see the entry cache-control: max-age=3600
. This means that you’ll have to wait an hour before seeing a new time!
Controlling Firebase’s Content Delivery Network
Firebase Hosting will cache whatever you put in your cache control header, so let’s do that for our time endpoint. Kestrel has the concept of Middleware, which is a system with which you can augment or modify the response for an incoming request. You can modify how long Firebase caches the response to a user’s request by modifying the CacheControl header with a simple piece of custom middleware (which I’ve listed below). I opted to put this in the HandleTime
function, since I don’t mind caching the text “Hello World” for an hour, but where you put it is up to the needs of your service.
Where do you go from here?
My goal is to build a simple cloud backend for a small Unity game I’m building, but you might have other needs. To tie into other Firebase services, you should look into the C# admin SDK and the Firebase REST API. There are also a number of excellent open source community projects available on NuGet to bridge the gap between the official admin SDK and REST. Note that even though we provide a Unity C# SDK, this is intended for clients and will probably not work in your server environment. The stable client SDKs are only fully implemented in iOS and Android, where it relies on native features of each OS to improve the end user experience.