Maintaining a Geolocation Specific Game Leaderboard with Phaser and MongoDB
Rate this tutorial
When it comes to game development, an often forgotten component comes in the form of a database for storing gameplay information. The database can contribute to numerous roles, such as storing user profile information, game state, and so much more.
To get an idea of what we want to accomplish, take a look at the following animated image:
The above game has three different screens, which are referred to as scenes in Phaser. The first screen (first few seconds of gif) accepts user input for a username and also gathers geolocation information about the player. The second screen is where you actually play the game and attempt to accumulate points (by collecting leaves) while avoiding bombs! Finally, the third screen is where your score and location information is submitted. You'll also see the all-time top scores as well as the top scores near your location, all of which can be queried easily using MongoDB!
There aren't many requirements that must be met in order to be successful with this tutorial. Here's what you'll need:
Before we jump into the game development side of things (with Phaser), we should take care of our backend API. This backend API will be responsible for the direct interaction with our database. It will accept requests from our game to store data as well as requests to fetch data.
Create a new project directory on your computer and from within that directory, execute the following commands:
There are a few lines that I want to bring attention to, starting with the creation of the
In this example,
ATLAS_URIis an environment variable on my computer and Node.js is reading from that variable. For context, the variable looks something like this:
Regardless on how you wish to create the
MongoClient, make sure your MongoDB Atlas connection URL contains the correct username and password information that you defined within the MongoDB Atlas dashboard.
The next thing I want to bring attention to is in the following two lines:
In this example, my database is
gamedevand my collection is
scores. If you want to use your own naming, just swap what I have with your own database and collection names. Also, since we plan to use geospatial queries, we'll need a geospatial index. That's where the
createIndexcommand comes in. When we launch our backend, the index will be created for us on the
locationfield of our documents, just as we've specified in our command.
We don't need any documents created at this point, but when documents get inserted, they'll look something like this:
locationfield is a special, formatted GeoJSON compliant object. The formatting is important when it comes to the geospatial queries and the index itself.
With the base of our backend created, let's start creating each of the endpoint functions.
Since we don't have any data to work with, let's start with the
When the client makes a POST request, we take the
locationfrom the payload and insert it into MongoDB as a new document. The resulting
_idwill be returned to the user when successful. Data validation is out of the scope of this tutorial, but it is important to note that we're not validating any of the data coming from the user.
Now that we can create scores, we'll need a way to query for them. Looking at the
getendpoint, we can do the following:
In the above code, we are using the
findmethod on our collection with no filter. This means we'll be attempting to retrieve all documents in the collection. However, we're also using a
limit, which says that we only want three documents in descending order.
With this endpoint configured this way, we can treat it as a global function that gets the top three scores from any location. Perfect for a leaderboard!
Now we can narrow down our results. Let's take a look at the
Here, we also use the
findmethod, but utilize a geospatial query as our filter using the
$nearoperator. When we pass a latitude and longitude position as query parameters in the request, we build a geospatial query that returns any document with a location that's within 25,000 meters of the provided position. You can play around with the numbers to get the results that you need.
The backend should be ready to go. Make sure you are running the Node.js application before you try to play the game that we create in the next step.
When we create our game we're going to want to create a new project directory. On your computer create a new project directory and in it, create an index.html file with the following HTML markup:
The above code shouldn't run, but it is the starting point to our Phaser game. Let's break it down.
<head>you'll notice the following
At this point in the tutorial, the most important chunk of information is in here:
In the above configuration, we are defining the game canvas, but we are also enabling the physics engine as well as DOM element embedding.
The DOM element embedding allows us to embed a user input field into our game so users can enter their name. The physics engine allows us to handle collisions between the player and the reward as well as the player and the obstacle. There are numerous physics engines available with Phaser, but we'll use the
arcadephysics option as it's the easiest to use.
Because we plan to keep scores for players based on their name and location, we'll need to enable location tracking. Within the
<script>tag that contains the
phaserConfig, modify it to the following:
The above code will leverage the location tracking of the web browser. When the game starts, the browser will prompt the user to enable location tracking. If the location cannot be determined, zero values will be used. If location tracking is not available in the browser, an error will be shown in the logs and the game will fail to configure.
As of right now nothing is done with the location, but after the user accepts, the game will start. However, since we don't have any scenes created yet, nothing will happen.
It's important to note that between getting the location and starting the game, it could take a few seconds to a few minutes. This is dependent on how quickly the browser can detect your location.
With the configuration out of the way, let's create the first scene that the player sees.
The first scene the player sees will prompt them to enter a username. This username will be sent to our backend and stored in MongoDB with a score.
If you haven't already, create an information-scene.js file and include the following code:
The above class represents our initial scene. In it, you'll see the four lifecycle events that Phaser uses to compose a scene. Because this scene takes user input, we need to create another HTML file that contains our form.
Create a form.html file within the game project and add the following HTML markup:
There's nothing particularly fancy happening in the above HTML. However, take note of the
nameattribute on the
<input>tag. We'll be referencing it later within our information-scene.js file.
Jumping back into the information-scene.js file, we need to load the HTML file that we just created in the
With the HTML form loaded in our scene, we can now work towards displaying it and capturing any data entered into it. This can be done from the
createfunction like so:
preloadfunction, we've referenced the HTML file as
form, which will be added as a DOM element on the Phaser canvas. We want to listen for events on a particular keystroke, in this case the enter key, and when the enter key is pressed, we want to get the data from the
usernameelement. Remember the
nameattribute on the
<input>tag? That is the value we're using in the
At this point, we technically have a working scene even though nothing happens with the user input. Let's edit the index.html file so we can switch to it:
If you're wondering what changed, first take a look at the
scenefield of the
phaserConfigobject. Notice that we've included the
InformationSceneclass to the array. Next, take a look at what happens after we get the geolocation information from the browser:
These changes let us start the
InformationScenescene and pass the location information we receive from the browser into it. To maintain my privacy for the example, I'm setting the decimal precision of the latitude and longitude to only a single decimal. This way, it shows my general location, but not exactly where I live.
The location information is formatted as appropriate GeoJSON since that is what MongoDB will depend on later.
So if we're passing information into our scene, how do we make use of it?
Open the information-scene.js file and change the
initfunction to look like this:
We're taking the data that was passed and are storing it in a local variable to the class. When we switch from this scene to another scene in the future, we'll pass the variable again.
Before we start working on the next scene, let's display the location information underneath the text input. Within the
createfunction of the information-scene.js file, add the following:
The above code renders whatever is in the location variable. It should be centered below the input field in this particular scene of the game.
Even though the next scene doesn't exist yet, let's get the switching logic in place. Let's change the logic that happens when the enter key is pressed after typing a username:
We'll be calling the next scene
MainSceneand we're going to pass into it the username that the user provided, the location from the browser, and an initial score.
We have a username and some location data to work with. Now it's time to create the game that the user can actually play.
Create a main-scene.js file in your project if you haven't already and include the following code:
You'll notice that we are accepting the data passed in from the previous scene in the
initfunction. Before we start modifying the other lifecycle functions, let's add this scene to the
phaserConfigfound in the index.html file:
MainScenerepresents our gameplay scene, we need to preload our game assets.
You can download my assets below or use your own images.
The actual images are not very important as long as something exists. Depending on the resolution of your images, you may need to change the scaling that we do later in the tutorial.
With the image files in your project, change the
preloadfunction of the main-scene.js file to look like the following:
We're only ever going to have a single box to represent our player, but we're going to have many of the
bombgame objects. This means that we'll need to create two object pools and one single sprite for the player.
If you're new to the concept of object pools, they are common when it comes to game development. The idea behind them is that instead of creating and destroying game objects as needed, which is bad for performance, a specific number of objects are created up front and these objects exist inactive and invisible until needed. When the object is no longer needed, it is deactivated and made invisible, hence going back into the pool to be used again in the future.
Remember the score and username information that was passed from the previous scene? We're attaching this data to the player (lines 5-6).
bombGroup, we are creating object pools. These object pools have thirty objects each and are inactive and invisible by default. This is convenient as we don't want to display or activate them until we're ready to use them.
The score is being tracked on the player, but it's probably a good idea to show it as well. Within the
createfunction of the main-scene.js function, add the following:
In the above code, we are initializing our text to be rendered with specific formatting. We're going to change it later as the score increases.
Even though we're not currently using our object pools, let's define some collision logic for when they do collide with our player.
createfunction of the main-scene.js file, add the following:
We have two colliders in the above code. One with logic to determine what happens when a leaf touches the player and one for when a bomb touches the player. When the leaf touches the player, we need to first make sure the leaf was active. When an object is active, certain game and scene logic is able to be applied. If leaf was active, we need to get the current score from the
playerobject, increase it, set the new score to the
playerobject and update the rendered text. We also need to add the leaf back to the pool so it can be used again.
When the bomb touches the player, we need to make sure the bomb is active and if it is, add it back to the pool and change the scene. We'll work on the logic for changing the scene soon.
Now is a good time to pull objects from both of our object pools. Within the
In the above code, we are creating a repeating timer. Every time the timer triggers, we pull a leaf and a bomb from the object pools and place them at a random position on the x-axis. We also activate that particular object and make it visible.
Don't try to pull more objects than exist in the pool, otherwise you'll get errors if the pool is empty.
The timer is pulling objects, but those objects are not yet moving. We need to move them in the
updatefunction of the main-scene.js file. Change the function to look like the following:
For both the
bombGroup, we are increasing the position on the y-axis. We are doing this for every object in those object pools. Even though we're changing the position of the entire pool, you'll only see and interact with the active and visible objects.
To prevent us from running out of objects in the pool, we can loop through each pool and see if any objects have moved beyond the screen. If they have, add them back into the pool.
So as of right now we have objects falling down the screen in our scene. If our player touches any of the leaf objects our score increases, otherwise the bombs will end the scene. The problem is that we can't actually control our player yet. This is an easy fix though.
updatefunction of the main-scene.js file, add the following:
Now when the pointer is down, whether it be on mobile or desktop, the x-axis position of the player will be updated to wherever the pointer is. For this particular game we won't bother updating the y-axis position.
With the exception of changing from this current scene to the next scene, we have a playable game.
Let's jump back into the collider logic for the bomb:
Even though we haven't created a
GameOverScenescene, we've assumed that we have. So if we collide with a bomb, the
GameOverScenewill start and we'll pass the username, score, and location information to the next scene.
The final scene is where we actually include MongoDB into our game. We've defined our information, played our game, and now we need to send it to MongoDB for storage.
If you haven't already, create a gameover-scene.js file in your project directory with the following code:
The above class should look familiar as it was used as the basis for the previous two classes. In the
initfunction we accept the username, score, and location data from the previous scene. Before we start defining our scene functionality, let's add the scene to the index.html file in the
Since we do not have any game assets, we don't need to use the
preloadfunction within the gameover-scene.js file. Instead, let's take a look at the
In the above
createfunction, we're doing three requests. First we are taking the player information provided in the previous scene and we're sending it to our backend via the
createendpoint. After we send our score data, we do two requests, the first for the top three global scores, and the second for the top three scores near my latitude and longitude.
The next step is to render the results from these requests on the screen as text.
The above code might look messy, but the reality is that we're just rendering text to the screen. We're looping through both of the scores results and rending the results and we're also rendering the current score.
To replay the game, we create a text that is clickable:
When clicking on the button, the
MainSceneis started and the current player information is sent.
You just saw how to work with leaderboard information using MongoDB within a game. In this particular example we saw two different types of leaderboards, one being global and one being geospatial. Another possibility that we didn't explore could be in the realm of platform such as mobile or desktop.
Let’s Give Your Realm-Powered Ionic Web App the Native Treatment on iOS and Android!
Sep 16, 2022