Building Firebase Quickdraw Multiplayer Banner
Building Firebase Quickdraw Multiplayer Banner

Build a multiplayer game with Firebase

Visitors to this year's Google I/O and Adventure and Firebase Summit 2021 got the chance to play our demo app: Firebase Quickdraw. This web app features an AI bot that tries to guess what the user is drawing based on a given prompt. The original app was built to help us understand the challenges that app developers face when they build their own apps. This year, we wanted to expand on that experience by adding a multiplayer mode. We learnt a ton during the development process and we wanted to share what we discovered.

Our hope is that this article will help you:

  • Add multiplayer functionality to your web app with Realtime Database
  • Use Cloud Functions to clean up app data, call APIs, and keep your app secure
  • Host your static assets on Firebase Hostings global CDN
  • Monitor the performance and stability of your app with Google Analytics and Firebase Performance Monitoring

Let's dive right in!

Adding Multiplayer to the existing app

The original app began as a collaborative hackathon project inspired by Google QuickDraw, and was built from scratch with Firebase. Here’s a quick rundown of how the app works:

  1. When the game starts, the app selects a random object from a list and a timer begins.
  2. After the player begins drawing the object, the AI bot (which is unaware of the object's identity), attempts to guess what the user is drawing.
  3. The bot either guesses correctly and the game allocates points to the player based on time remaining, or the timer runs out and the player is awarded no points.
  4. The game repeats this process for 6 rounds.

Once the game is completed, the app calculates the player score. Players are awarded a ranking on the daily leaderboard and offered the chance to play again.

Firebase added multiplayer functionality to Quickdraw, giving players the ability to create shareable game rooms and invite up to four players to play Quickdraw in a room. Multiplayer mode works like single player mode, while letting all players view each other's progress. As each player completes their game, their scores are calculated and a winner is declared.

How did we build it?

We leveraged several Firebase products to build this app. Here is an architecture diagram showing how we used each product for different features in the app:

Architecture diagram showing how each product was used for different features in the app
Architecture diagram showing how each product was used for different features in the app

Let's break down product usage:

  • We host our static assets on Firebase Hosting. The combination of small js bundle size and Hosting’s global content delivery network (CDN) gives the app a short initial load time.
  • To classify images, we use a deployed TensorFlow model. To learn more about deploying AI models, check out Google's Vertex AI.
  • Realtime Database is the core of the multiplayer game mode. Besides storing game and player statuses, Realtime Database's efficient, low-latency characteristics ensure that game state data is synchronized across clients.
  • To clean up app data and maintain security, we used Cloud Functions for a variety of use cases, including the following:
    • To call the HandWriting API, where we deployed a machine learning model to recognize the drawn objects.
    • To run cron jobs that reset the daily leaderboard.
    • To allow players in multiplayer mode to join the game securely, while also regularly cleaning up finished games.

Multiplayer implementation challenges

Setting up the project

Although we inherited the Quickdraw app, this year's entire development team is new to both Firebase AND Google. This scenario gave us the unique opportunity to tackle the project from the same perspective as many of our Firebase customers who are newer to the platform, and to learn more about Firebase as a product.

The first step was to set up a development environment for our project using Firebase on our local machines. We were astonished at how fast we were able to accomplish this task. Here are the steps we followed:

  1. Clone the code repo and npm install.
  2. Install the Firebase CLI and link to the existing Firebase project.
  3. Download the web configuration to connect the game with the project.
  4. Run the emulator suite and start coding!

The only difficulty we encountered involved installing the emulator suite. It turned out that we needed to install the correct versions of Java dependencies on our local machine to get everything working. Thankfully, the CLI messages were clear about what needed to be done, guiding us to install the right dependencies and quickly move on to the development phase.

Designing a multiplayer database structure

Realtime Database supports low latency synchronization between clients, which is great. Now it's our job to design an effective database structure to ensure that:

  • Players can create, join and leave a game room.
  • Players can see other players' scores and statuses in real time.

Due to time constraints, we needed the structure to be as simple as possible. As it turned out, Realtime Database easily supported the structure we developed:

rooms: [
  room1_id: {
    players: {
      player1_id: {
        … player metadata (scores, name)}
    }
    … other room metadata (game status, game start timestamp)},

  room2_id: {},]

Our database contains a list of rooms, where each room stores all game data and player information for a maximum of 4 players. Note that we only allow one player to join one room at a time by verifying their identity with anonymous auth. Each combination of players and rooms are independent from each other. This is important because all room updates are self-contained in an object, helping us to avoid writing complex logic to synchronize the states between clients.

Developing the game lobby

To support multiple players actively engaging in a coordinated quickdraw game, we needed to create a new multiplayer lobby page. This new view lets users create a multiplayer game and get a link that they can share with their friends. The multiplayer lobby page also guarantees that everyone is ready to play the game before the game starts. The following sections cover in detail how we created the new view.

Synchronizing game states between players

As Firebase newbies, we found the Firebase API Reference documentation was a great resource to learn how to read and write to Realtime Database from the web client, and get started writing simple code. Realtime Database also supports complex use cases. Since we need the game to be synchronized between players, we use transactions to ensure we don't have a race condition leading to data corruption.

Although it was easy to get started and use Realtime Database to match our use cases, implementing game logic correctly on the first try was challenging. We encountered serious bugs along the way, and even broke the single-player game mode occasionally as we adapted it for multiplayer mode. However, the Firebase Emulator Suite came to the rescue, helping us to debug logic errors and ensure a correct implementation. To learn more, see Debugging the app.

Using Cloud Functions

We transformed legacy functions to use functions.https.onCall, replacing the legacy functions.https.onRequest. While the legacy method still works well, the new interface is cleaner and simplifies development by handling cross-origin resource sharing (CORS), serializing and deserializing request payload, and authenticating user tokens.

In addition, we added two new Cloud Functions:

  1. A function that lets new players join the room. This is needed for security reasons since we only let players within the room modify the room object. Since Cloud Functions can bypass security rules, we used a function to validate players' identities and allow them to join the room.
  2. A PubSub schedule function that runs every two hours to clean up the finished games.

Detecting disconnecting clients

During the development and testing of our new multiplayer functionality, we encountered issues when a user navigated away from the game or closed their browser or tab while participating in a multiplayer lobby. At first we thought we needed to implement a heartbeat mechanism with a scheduled Cloud Function to clean up stale user data on games that hadn't started yet. Fortunately, we discovered a valuable feature included with the Realtime Database client SDK: OnDisconnect. Any time a client connects to your Realtime Database, you can establish OnDisconnect operations that you want executed against your structure in the event the client disconnects (cleanly or not). If the player disconnects, OnDisconnect lets us remove a player from a game that hasn’t started yet. Because this update is synchronized across clients, OnDisconnect lets all other players see that another player has left the room.

Our initial solution would have required a lot of code and complex testing, taking time away from working on other features of our application. Thanks to OnDisconnect, we were able to solve this problem in three lines of code:

onDisconnect(
     ref(db, `/rooms/${this.currentRoom.roomId}/players/${this.currentPlayer.playerId}`))
     .remove();

Debugging the app

We used a combination of Chrome DevTools and the Firebase Emulator Suite to help us debug and support app development, including:

  • The ability to host the app locally and provide live reload.
  • Realtime Database emulation so we could inspect the correctness of read and write operations to RTDB.
  • Cloud Functions emulation that enabled us to inspect Cloud Function logs and ensure our functions work as expected.

With the Firebase Emulator Suite, local, hermetic instances reduced the risk that we would impact other developers working on the app simultaneously, a feature not supported by shared dev environments.

Note that when you run these emulator tools locally on your workstation, they are very fast because they don't communicate with any back end services. When you deploy your app and use real back-ends for the first time, you might be surprised to find your app not behaving the way you expected when you were testing it locally.

Keeping things secure

You put a lot of care and effort into building your app and it's important to protect your users and your investment from malicious actors. Firebase has powerful Security Rules to protect your app. We combined Firebase Authentication with custom security rules to protect critical services like the Realtime Database.

While security may not seem important in an app that anyone can play, we at Firebase know that security is crucial to app success. We implemented Firebase Security Rules to ensure that only people playing our app game could modify our database, and to prevent players from curling in the command line and spoiling everyone's fun.

It's a best practice to think about security from the beginning of development, because the way you structure your database can make it easier or harder to implement Security Rules. A good rule of thumb is to structure your data according to who will need to read or write the data. Since our initial focus was on getting our prototype up and running, we didn't think about security initially. We then realized we needed to prevent anonymous users from being able to freely edit any data in our database; and we knew we had to add security rules retroactively. The problem was, the ability to freely edit the data was how users were able to join in the first place! Our solution was to move all of the logic around joining a game to a Cloud Function (as previously mentioned). This approach let us implement the following Security Rules:

{
 "rules": {
   "rooms": {
     "$roomid": {
       // Any player in the room can read all data
       ".read": "data.child('players').child(auth.uid).exists()",
       // Any signed in user can write to a new room if they are creator
       // Any player in the room can write any data
       ".write": "(auth.uid != null && !data.exists() && newData.child('creatorId').val() === auth.uid) || data.child('players').child(auth.uid).exists()",
     }
   }
 }
}

With these rules in place, any user could initially create a game, but joining it would require calling a Cloud Function. Assuming the game lobby wasn’t already full, and once a player was added to the game, they were free to edit the game data, for example, letting clients update status codes and scores. Some security issues remained – for example, players could edit their rivals scores, making themselves the winner even if they were bad at drawing– but this still serves as a useful example.

Performance and stability

We use Firebase Performance Monitoring to monitor our app’s load time and HandWriting API's response time as more and more users started playing. This monitoring was made possible by real-time data processing performed by Firebase Performance Monitoring.

With the integration of the Firebase SDK for Google Analytics, we created custom events to measure how many players start a game versus how many finish a game. We can also view other interesting insights like the demographics of our players via the Analytics dashboard in the Firebase console, or in the Google Analytics console directly.

What we learned

Firebase removed a lot of the hassle associated with creating an app. Each of the products we used saved us a ton of time and let us focus on giving our users a better experience. With Firebase Hosting, it was easy to make our assets available globally by running a single command. Realtime Database let us save, update, and use data quickly and efficiently. Cloud Functions was there for us to clean up app data, call external API’s when triggered, and help keep our app secure. And finally, Firebase Performance Monitoring and Google Analytics for Firebase gave us insights into how users were experiencing the app, which let us address any issues with our app and improve the experience for our users.

This combination of complementary products and services made creating a multiplayer game a fun journey for our team. Most importantly, working on this app helped us understand our customers' experience when they use Firebase to build their own apps and games. We hope that all of you who attended Google I/O 2022 and played our game had fun, too. We look forward to seeing what you create with Firebase!