Creating a Multiplayer Drawing Game with Phaser and MongoDB
Rate this article
When it comes to MongoDB, an often overlooked industry that it works amazingly well in is gaming. It works great in gaming because of its performance, but more importantly its ability to store whatever complex data the game throws at it.
Let's say you wanted to create a drawing game like . I know what you're thinking: why would I ever want to create a Pictionary game with MongoDB integration? Well, what if you wanted to be able to play with friends remotely? In this scenario, you could store your brushstrokes in MongoDB and load those brushstrokes on your friend's device. These brushstrokes can be pretty much anything. They could be images, vector data, or something else entirely.
A drawing game is just one of many possible games that would pair well with MongoDB.
Take the following animated image for example:
In the above example, I have my MacBook as well as my iOS device in the display. I'm drawing on my iOS device, on the right, and after the brushstrokes are considered complete, they are sent to MongoDB and the other clients, such as the MacBook. This is why the strokes are not instantly available as the strokes are in progress.
There are a few requirements that must be met prior to starting this tutorial:
There's no account requirement or downloads necessary when it comes to building Phaser games. These games are both web and mobile compatible.
When it comes to Phaser, you can do everything within a single HTML file. This file must be served rather than opened from the local filesystem, but nothing extravagant needs to be done with the project.
Let's start by creating a project somewhere on your computer with an index.html file and a game.js file. We're going to add some boilerplate code to our index.html prior to adding our game logic to the game.js file.
Within the index.html file, add the following:
In the above HTML, we've added scripts for both Phaser and MongoDB Realm. We've also defined an HTML container
<div>element, as seen by the
gameid, to hold our game when the time comes.
We could add all of our Phaser and MongoDB logic into the unused
<script>tag, but this is where it makes sense to have most of our logic in the game.js file.
Open the game.js file and include the following boilerplate code:
Gameclass is responsible for rendering, interactions between the user and the game, and interactions between the game and the database. For now, we're not going to worry about our database.
constructormethod, we take information provided from the user to configure Phaser. Some of the configuration fields have default values in case the user decides not to provide them when instantiating the class. This data will come from the developer rather than the gamer. When it comes to the , the
initScenewill be responsible for initializing our game variables, the
createScenewill be responsible for synchronizing with the server when the game first loads and the
updateScenewill be responsible for ongoing interactions with the game.
Before we jump into each of the functions, let's go back into the index.html file and instantiate our class. This can be done in the
<script>tag that currently exists:
In the above code, we're defining that our game should be rendered in the HTML element with the
gameid. We're also saying that it should take the full width and height that's available to us in the browser. This full width and height works for both computers and mobile devices.
Now we can take a look at each of our scenes in the game.js file, starting with the
For now, the
initScenefunction will remain short. This is because we are not going to worry about initializing any database information yet. When it comes to
strokes, this will represent independent collections of points. A brushstroke is just a series of connected points, so we want to maintain them. We need to be able to determine when a stroke starts and finishes, so we can use
isDrawingto determine if we've lifted our cursor or pencil.
Now let's have a look at the
Like with the
initScene, this function will change as we add the database functionality. For now, we're initializing the graphics layer in our scene and defining the line size and color that should be rendered. This is a simple game so all lines will be 4 pixels in size and the color green.
This brings us into the most extravagant of the scenes. Let's take a look at the
updateScenefunction is responsible for continuously rendering things to the screen. It is constantly run, unlike the
createScenewhich is only ran once. When updating, we want to check to see if we are either drawing or not drawing.
activePointeris not down, it means we are not drawing. If we are not drawing, we probably want to indicate so with the
isDrawingvariable. This condition will get more advanced when we start adding database logic.
activePointeris down, it means we are drawing. In Phaser, to draw a line, we need a starting point and then a series of points we can render as a path. If we're starting the brushstroke, we should probably create a new path. Because we set our line to be 4 pixels, if we want the line to draw at the center of our cursor, we need to use half the size for the x and y position.
We're not ever clearing the canvas, so we don't actually need to draw the path unless the pointer is active. When the pointer is active, whatever was previously drawn will stay on the screen. This saves us some processing resources.
We're almost at a point where we can test our offline game!
The scenes are good, even though we haven't added MongoDB logic to them. We need to actually create the game so the scenes can be used. Within the game.js file, update the following function:
The above code will take the Phaser configuration that we had set in the
constructormethod and start the
defaultscene. As of right now we aren't passing any data to our scenes, but we will in the future.
createGamefunction available, we need to make use of it. Within the index.html file, add the following line to your
<script>block below the instantiation of the
You did it! You have a game with a black screen where you can draw lines with your cursor. As of right now, you can only ever have one game and the pictures you draw won't save or sync anywhere. We'll get to that next!
So what's the goal for the next step of this tutorial?
We are currently able to draw lines on the canvas. It works great, but we can only ever have a single game, and if we refresh, those lines disappear. In addition, if we wanted a friend to see our image, they wouldn't be able to.
So we're going to do the following with MongoDB Atlas and MongoDB Realm:
- Create documents for the person who is drawing and store the brushstrokes.
- Watch for changes to the document and render them on the screen.
Rather than starting in our code, we're going to get things ready to go in Atlas and Realm first. We need to get a database and collection ready as well as the data rules for who can access and manipulate the data.
Assuming you've already created an Atlas cluster, create a
mongo-drawsdatabase with a
gamecollection. We're not going to add any documents manually, but when we start adding them, they're going to look like this:
strokesarray will be populated from whatever Phaser determines to be a path, as created in the earlier part of this tutorial. The
_idwill represent a game room and the
owner_idwill represent the host or owner of that particular room. Essentially, the plan is to have only the host able to add new brushstrokes in a game room while the other participants watch.
You can enable anonymous authentication by clicking the Providers tab in the Users area of the Realm dashboard.
With the anonymous authentication enabled, now we can define the read and write rules of our documents.
Within the Rules area of the Realm dashboard, you're going to want to create two rules for the game collection. The first rule, for the document owner, you'll want to give them read and write ability. For non-owners, since we want them to spectate the game, we will give them read access. During the process of creating rules, make sure to specify that the
owner_idof the document is to be mapped to the document owner.
For more insight, the rules should look something like this:
MongoDB Atlas and Realm are ready to be used in our game. Don't forget to deploy your changes in Realm if you haven't already.
Open the project's game.js file and add the following to the
We want to configure how we plan to use MongoDB Realm when we instantiate our
Gameclass. This means that we need to change what we're doing in the index.html file slightly:
You'll notice that we have three new configuration fields to be fed into our Realm configuration.
collectionNamefields should match the name of your database and collection. The
realmAppIdcan be found within your Realm web dashboard towards the top left of the screen.
Before we try to create or join an existing game, we need to authenticate with MongoDB Realm. Within the game.js file, update the following function:
Remember, we're doing anonymous authentication, so nothing too fancy. We're not going to try to authenticate with this function quite yet.
So now what we need to do is figure out how to either create a new game room document, or use one that already exists. We already have a
createGamefunction, but we're going to need to improve upon it to include database functionality. Within the
createGamefunction of the game.js file, change it to the following:
Remember, previously we were just creating a game and starting the scene collection. Now we're creating a new document based on the passed game id and owner id, then passing some of that information to the scene itself.
So why are we passing this information?
We could update the index.html file to use the new
createGamefunction, but we know we're going to want to create or join, not just create. With this in mind, let's update another function in the game.js file:
Rather than inserting a new document, we are trying to find one based on the game id that was provided. If no data comes back, it means no game exists. However, if a game does exist, we're going to initialize the game scene with the stored information.
createGamefunctions available, let's update a wrapper function to eliminate some of the work.
joinOrCreateGamefunction, we're first authenticating with MongoDB Realm. Once we've authenticated, we attempt to join a game. If the join is not successful because no document exists, then we try to create a game.
So let's update the index.html file to use our
joinOrCreateGamefunction instead of our
Since we're working with game documents, let's update the
initScenefunction in the game.js file. The one where we are passing game information to when we do a create or a join:
Alright, so we have a mechanism for creating and joining games. We can do better though. We probably don't want to hard-code the game id when trying to join or create a game. Let's add some more HTML elements to help us out.
Open the index.html file and make it look like the following:
The above code has a little more going on now, but don't forget to use your own application ids, database names, and collections. You'll start by probably noticing the following markup:
Not all of it was absolutely necessary, but it does give our game a better look and feel. Essentially now we have an input field. When the input field is submitted, whether that be with keypress or click, the
joinOrCreateGamefunction is called The
keyCode == 13represents that the enter key was pressed. The function isn't called directly, but the wrapper functions call it. The game id is extracted from the input, and the HTML components are transformed based on the information about the game.
To summarize what happens, the user submits a game id. The game id floats on top of the game scene as well as information regarding if you're the owner of the game or not.
The markup looks worse than it is.
Now that we can create or join games both from a UX perspective and a logic perspective, we need to change what happens when it comes to interacting with the game itself. We need to be able to store our brush strokes in MongoDB. To do this, we're going to revisit the
Remember, this time around we have access to the game id and the owner id information. It was passed into the scene when we created or joined a game.
When it comes to actually drawing, nothing is going to change. However, when we aren't drawing, we want to update the game document to push our new strokes. Phaser makes it easy to convert our line information to JSON which inserts very easily into MongoDB. Remember earlier when I said accepting flexible data was a huge benefit for gaming?
So we are pushing these brushstrokes to MongoDB. We need to be able to load them from MongoDB.
Let's update our
createScenefunction executes, we are taking the
strokesarray that was provided by the
joinGamefunctions and looping over it. Remember, in the
updateScenefunction we are storing the exact path. This means we can load the exact path and draw it.
Let's update our
createScenefunction once more:
We're now watching our collection for documents that have an
_idfield that matches our game id. Remember, we're in a game, we don't need to watch documents that are not our game. When a new document comes in, we can look at the updated fields and render the new strokes to the scene.
So why are we not using
pathlike all the other areas of the code?
You don't know when new strokes are going to come in. If you're using the same global variable between the active drawing canvas and the change stream, there's a potential for the strokes to merge together given certain race conditions. It's just easier to let the change stream make its own path.
At this point in time, assuming your cluster is available and the configurations were made correctly, any drawing you do will be added to MongoDB and essentially synchronized to other computers and devices watching the document.
You just saw how to make a simple drawing game with Phaser and MongoDB. Given the nature of Phaser, this game is compatible on desktops as well as mobile devices, and, given the nature of MongoDB and Realm, anything you add to the game will sync across devices and platforms as well.
This is just one of many possible gaming examples that could use MongoDB, and these interactive applications don't even need to be a game. You could be creating the next Photoshop application and you want every brushstroke, every layer, etc., to be synchronized to MongoDB. What you can do is limitless.
ORLANDO, FLORIDA, US | IN-PERSON
Orlando Code Camp
Mar 25, 2023 | 12:00 PM - 9:00 PM UTC