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.
In fact, I created a previous tutorial titled Creating a Multiplayer Drawing Game with Phaser and MongoDB. In this drawing example, every brushstroke made was stored in MongoDB.
In this tutorial, we're going to look at a different data component for a game. We're going to explore leaderboards and some of the neat things you can do with them. Like my other tutorial, we'll be using Phaser and JavaScript.
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:
- Node.js 12+
For this example, MongoDB Atlas will store all of our leaderboard information and Node.js will power our backend API. The Phaser game doesn't have any real dependency beyond having something available to serve the project. I use serve to accomplish this, but you can use whatever you're comfortable with.
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:
1 npm init -y 2 npm install express body-parser cors mongodb --save
The above commands will create a new package.json file and install the project dependencies for our backend. We'll be using Express to create our API and the MongoDB Node.js driver for communicating with the database.
For the backend, we'll add all of our code to a main.js file. Create it in your project directory and add the following JavaScript:
1 const { MongoClient, ObjectID } = require("mongodb"); 2 const Express = require("express"); 3 const Cors = require("cors"); 4 const BodyParser = require("body-parser"); 5 const { request } = require("express"); 6 7 const client = new MongoClient(process.env["ATLAS_URI"]); 8 const server = Express(); 9 10 server.use(BodyParser.json()); 11 server.use(BodyParser.urlencoded({ extended: true })); 12 server.use(Cors()); 13 14 var collection; 15 16 server.post("/create", async (request, response) => {}); 17 server.get("/get", async (request, response) => {}); 18 server.get("/getNearLocation", async (request, response) => {}); 19 20 server.listen("3000", async () => { 21 try { 22 await client.connect(); 23 collection = client.db("gamedev").collection("scores"); 24 collection.createIndex({ "location": "2dsphere" }); 25 } catch (e) { 26 console.error(e); 27 } 28 });
The above JavaScript has a lot of boilerplate code that I won't get into the details on. If you'd like to learn how to connect to MongoDB with Node.js, check out Lauren Schaefer's getting started tutorial on the subject.
There are a few lines that I want to bring attention to, starting with the creation of the
MongoClient
object:1 const client = new MongoClient(process.env["ATLAS_URI"]);
In this example,
ATLAS_URI
is an environment variable on my computer and Node.js is reading from that variable. For context, the variable looks something like this:1 mongodb+srv://<username>:<password>@cluster0-yyarb.mongodb.net/<database>?retryWrites=true&w=majority
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:
1 collection = client.db("gamedev").collection("scores"); 2 collection.createIndex({ "location": "2dsphere" });
In this example, my database is
gamedev
and 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 thecreateIndex
command comes in. When we launch our backend, the index will be created for us on the location
field 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:
1 { 2 "_id": "23abcd87ef", 3 "username": "nraboy", 4 "score": 35, 5 "location": { 6 "type": "Point", 7 "coordinates": [ -121, 37 ] 8 } 9 }
The
location
field 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
create
endpoint function:1 server.post("/create", async (request, response) => { 2 try { 3 let result = await collection.insertOne( 4 { 5 "username": request.body.username, 6 "score": request.body.score, 7 "location": request.body.location 8 } 9 ); 10 response.send({ "_id": result.insertedId }); 11 } catch (e) { 12 response.status(500).send({ message: e.message }); 13 } 14 });
When the client makes a POST request, we take the
username
, score
, and location
from the payload and insert it into MongoDB as a new document. The resulting _id
will 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
get
endpoint, we can do the following:1 server.get("/get", async (request, response) => { 2 try { 3 let result = await collection.find({}).sort({ score: -1 }).limit(3).toArray(); 4 response.send(result); 5 } catch (e) { 6 response.status(500).send({ message: e.message }); 7 } 8 });
In the above code, we are using the
find
method 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 sort
and 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
getNearLocation
function:1 server.get("/getNearLocation", async (request, response) => { 2 try { 3 let result = await collection.find({ 4 "location": { 5 "$near": { 6 "$geometry": { 7 "type": "Point", 8 "coordinates": [ 9 parseFloat(request.query.longitude), 10 parseFloat(request.query.latitude) 11 ] 12 }, 13 "$maxDistance": 25000 14 } 15 } 16 }).sort({ score: -1 }).limit(3).toArray(); 17 response.send(result); 18 } catch (e) { 19 response.status(500).send({ message: e.message }); 20 } 21 });
Here, we also use the
find
method, but utilize a geospatial query as our filter using the $near
operator. 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:
1 2 <html> 3 <head> 4 <script src="//cdn.jsdelivr.net/npm/phaser@3.24.1/dist/phaser.min.js"></script> 5 <script src="information-scene.js"></script> 6 <script src="main-scene.js"></script> 7 <script src="gameover-scene.js"></script> 8 </head> 9 <body> 10 <div id="game"></div> 11 <script> 12 13 const phaserConfig = { 14 type: Phaser.AUTO, 15 parent: "game", 16 width: 1280, 17 height: 720, 18 dom: { 19 createContainer: true 20 }, 21 physics: { 22 default: "arcade", 23 arcade: { 24 debug: false 25 } 26 }, 27 scene: [] 28 }; 29 30 const game = new Phaser.Game(phaserConfig); 31 32 </script> 33 </body> 34 </html>
The above code shouldn't run, but it is the starting point to our Phaser game. Let's break it down.
Within the
<head>
you'll notice the following <script>
tags:1 <head> 2 <script src="//cdn.jsdelivr.net/npm/phaser@3.24.1/dist/phaser.min.js"></script> 3 <script src="information-scene.js"></script> 4 <script src="main-scene.js"></script> 5 <script src="gameover-scene.js"></script> 6 </head>
The first JavaScript file is the Phaser framework. The other three are files that we'll be creating. You can create each of the remaining three files now or wait until we get to that step in the tutorial.
At this point in the tutorial, the most important chunk of information is in here:
1 const phaserConfig = { 2 type: Phaser.AUTO, 3 parent: "game", 4 width: 1280, 5 height: 720, 6 dom: { 7 createContainer: true 8 }, 9 physics: { 10 default: "arcade", 11 arcade: { 12 debug: false 13 } 14 }, 15 scene: [] 16 };
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
arcade
physics 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:1 <script> 2 3 // phaserConfig ... 4 5 if (navigator.geolocation) { 6 navigator.geolocation.getCurrentPosition(position => { 7 if(!position.coords || !position.coords.longitude) { 8 position.coords.latitude = 0; 9 position.coords.longitude = 0; 10 } 11 const game = new Phaser.Game(phaserConfig); 12 }); 13 } else { 14 console.error("Geolocation is not supported by this browser!"); 15 } 16 17 </script>
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:
1 var InformationScene = new Phaser.Class({ 2 Extends: Phaser.Scene, 3 initialize: function () { 4 Phaser.Scene.call(this, { key: "InformationScene" }); 5 }, 6 init: function (data) { }, 7 preload: function () { }, 8 create: async function () { }, 9 update: function () { } 10 });
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:
1 2 <html> 3 <head> 4 <style> 5 #input-form { 6 padding: 15px; 7 background-color: #CCCCCC; 8 } 9 #input-form input { 10 padding: 10px; 11 font-size: 20px; 12 width: 400px; 13 } 14 </style> 15 </head> 16 <body> 17 <div id="input-form"> 18 <input type="text" name="username" placeholder="Enter a Name" /> 19 </div> 20 </body> 21 </html>
There's nothing particularly fancy happening in the above HTML. However, take note of the
name
attribute 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
preload
function:1 preload: function () { 2 this.load.html("form", "form.html"); 3 },
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
create
function like so:1 create: async function () { 2 3 this.usernameInput = this.add.dom(640, 360).createFromCache("form"); 4 5 this.returnKey = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.ENTER); 6 7 this.returnKey.on("down", event => { 8 let username = this.usernameInput.getChildByName("username"); 9 if(username.value != "") { 10 // Switch scene ... 11 } 12 }) 13 14 },
In the
preload
function, 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 username
element. Remember the name
attribute on the <input>
tag? That is the value we're using in the getChildByName
function.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:
1 <script> 2 3 const phaserConfig = { 4 type: Phaser.AUTO, 5 parent: "game", 6 width: 1280, 7 height: 720, 8 dom: { 9 createContainer: true 10 }, 11 physics: { 12 default: "arcade", 13 arcade: { 14 debug: false 15 } 16 }, 17 scene: [InformationScene] 18 }; 19 20 if (navigator.geolocation) { 21 navigator.geolocation.getCurrentPosition(position => { 22 if(!position.coords || !position.coords.longitude) { 23 position.coords.latitude = 0; 24 position.coords.longitude = 0; 25 } 26 const game = new Phaser.Game(phaserConfig); 27 game.scene.start("InformationScene", { 28 location: { 29 type: "Point", 30 coordinates: [ 31 parseFloat(position.coords.longitude.toFixed(1)), 32 parseFloat(position.coords.latitude.toFixed(1)) 33 ] 34 } 35 }) 36 }); 37 } else { 38 console.error("Geolocation is not supported by this browser!"); 39 } 40 41 </script>
If you're wondering what changed, first take a look at the
scene
field of the phaserConfig
object. Notice that we've included the InformationScene
class to the array. Next, take a look at what happens after we get the geolocation information from the browser:1 game.scene.start("InformationScene", { 2 location: { 3 type: "Point", 4 coordinates: [ 5 parseFloat(position.coords.longitude.toFixed(1)), 6 parseFloat(position.coords.latitude.toFixed(1)) 7 ] 8 } 9 })
These changes let us start the
InformationScene
scene 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
init
function to look like this:1 init: function (data) { 2 this.location = data.location; 3 },
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
create
function of the information-scene.js file, add the following:1 this.locationText = this.add.text( 2 640, 3 425, 4 `[${this.location.coordinates[1]}, ${this.location.coordinates[0]}]`, 5 { 6 fontSize: 20 7 } 8 ).setOrigin(0.5);
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:
1 this.returnKey.on("down", event => { 2 let username = this.usernameInput.getChildByName("username"); 3 if(username.value != "") { 4 this.scene.start("MainScene", { username: username.value, score: 0, location: this.location }); 5 } 6 })
We'll be calling the next scene
MainScene
and 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:
1 var MainScene = new Phaser.Class({ 2 Extends: Phaser.Scene, 3 initialize: function() { 4 Phaser.Scene.call(this, { key: "MainScene" }); 5 }, 6 init: function(data) { 7 this.username = data.username; 8 this.score = data.score; 9 this.location = data.location; 10 }, 11 preload: function() { }, 12 create: function() { }, 13 update: function() { } 14 });
You'll notice that we are accepting the data passed in from the previous scene in the
init
function. Before we start modifying the other lifecycle functions, let's add this scene to the phaserConfig
found in the index.html file:1 scene: [InformationScene, MainScene]
Since the
MainScene
represents 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
preload
function of the main-scene.js file to look like the following:1 preload: function() { 2 this.load.image("leaf", "leaf.png"); 3 this.load.image("bomb", "bomb.png"); 4 this.load.image("box", "box.png"); 5 },
We're only ever going to have a single box to represent our player, but we're going to have many of the
leaf
and bomb
game 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.
In the
create
function of the main-scene.js file, add the following JavaScript code:1 create: function() { 2 this.player = this.physics.add.sprite(640, 650, "box"); 3 this.player.setScale(0.25); 4 this.player.setDepth(1); 5 this.player.setData("score", this.score); 6 this.player.setData("username", this.username); 7 8 this.leafGroup = this.physics.add.group({ 9 defaultKey: "leaf", 10 maxSize: 30, 11 visible: false, 12 active: false 13 }); 14 15 this.bombGroup = this.physics.add.group({ 16 defaultKey: "bomb", 17 maxSize: 30, 18 visible: false, 19 active: false 20 }); 21 },
Remember the score and username information that was passed from the previous scene? We're attaching this data to the player (lines 5-6).
For the
leafGroup
and 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
create
function of the main-scene.js function, add the following:1 create: function() { 2 3 // Sprite and object pool creation logic ... 4 5 this.scoreText = this.add.text(10, 10, "SCORE: 0", { fontSize: 40, color: '#000000', fontStyle: "bold", backgroundColor: "#FFFFFF", padding: 10 }); 6 this.scoreText.setDepth(1); 7 8 }
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.
Within the
create
function of the main-scene.js file, add the following:1 create: function() { 2 3 // Sprite and object pool creation logic ... 4 // Render text ... 5 6 this.physics.add.collider(this.player, this.leafGroup, (player, leaf) => { 7 if (leaf.active) { 8 this.score = player.getData("score"); 9 this.score++; 10 player.setData("score", this.score); 11 this.scoreText.setText("SCORE: " + this.score); 12 this.leafGroup.killAndHide(leaf); 13 } 14 }); 15 16 this.physics.add.collider(this.player, this.bombGroup, (player, bomb) => { 17 if (bomb.active) { 18 this.bombGroup.killAndHide(bomb); 19 // Change scenes ... 20 } 21 }); 22 23 }
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
player
object, increase it, set the new score to the player
object 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
create
function of the main-scene.js file, add the following JavaScript code:1 create: function() { 2 3 // Sprite and object pool creation logic ... 4 // Render text ... 5 // Collider logic ... 6 7 this.time.addEvent({ 8 delay: 250, 9 loop: true, 10 callback: () => { 11 let leafPositionX = Math.floor(Math.random() * 1280); 12 let bombPositionX = Math.floor(Math.random() * 1280); 13 this.leafGroup.get(leafPositionX, 0) 14 .setScale(0.1) 15 .setActive(true) 16 .setVisible(true); 17 this.bombGroup.get(bombPositionX, 0) 18 .setScale(0.1) 19 .setActive(true) 20 .setVisible(true); 21 } 22 }); 23 24 }
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
update
function of the main-scene.js file. Change the function to look like the following:1 update: function() { 2 this.leafGroup.incY(6); 3 this.leafGroup.getChildren().forEach(leaf => { 4 if (leaf.y > 800) { 5 this.leafGroup.killAndHide(leaf); 6 } 7 }); 8 this.bombGroup.incY(6); 9 this.bombGroup.getChildren().forEach(bomb => { 10 if (bomb.y > 800) { 11 this.bombGroup.killAndHide(bomb); 12 } 13 }); 14 }
For both the
leafGroup
and 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.
Within the
update
function of the main-scene.js file, add the following:1 update: function() { 2 3 // Object pool movement logic ... 4 5 if (this.input.activePointer.isDown) { 6 this.player.x = this.input.activePointer.position.x; 7 } 8 9 }
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:
1 this.physics.add.collider(this.player, this.bombGroup, (player, bomb) => { 2 if (bomb.active) { 3 this.bombGroup.killAndHide(bomb); 4 this.scene.start("GameOverScene", { 5 "username": this.player.getData("username"), 6 "score": this.player.getData("score"), 7 "location": this.location 8 }); 9 } 10 });
Even though we haven't created a
GameOverScene
scene, we've assumed that we have. So if we collide with a bomb, the GameOverScene
will 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:
1 var GameOverScene = new Phaser.Class({ 2 Extends: Phaser.Scene, 3 initialize: function() { 4 Phaser.Scene.call(this, { key: "GameOverScene" }); 5 }, 6 init: function(data) { 7 this.player = data; 8 }, 9 preload: function() {}, 10 create: async function() {}, 11 update: function() {} 12 });
The above class should look familiar as it was used as the basis for the previous two classes. In the
init
function 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 phaserConfig
object:1 scene: [InformationScene, MainScene, GameOverScene]
Since we do not have any game assets, we don't need to use the
preload
function within the gameover-scene.js file. Instead, let's take a look at the create
function:1 create: async function() { 2 try { 3 if (this.player.username && this.player.score) { 4 await fetch("http://localhost:3000/create", { 5 "method": "POST", 6 "headers": { 7 "content-type": "application/json" 8 }, 9 "body": JSON.stringify(this.player) 10 }); 11 } 12 13 this.globalScores = await fetch("http://localhost:3000/get") 14 .then(response => response.json()); 15 16 this.nearbyScores = await fetch(`http://localhost:3000/getNearLocation?latitude=${this.player.location.coordinates[1]}&longitude=${this.player.location.coordinates[0]}`) 17 .then(response => response.json()); 18 } catch (e) { 19 console.error(e); 20 } 21 22 },
In the above
create
function, 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 create
endpoint. 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.
1 create: async function() { 2 try { 3 4 // REST API logic ... 5 6 this.add.text(10, 100, "GLOBAL HIGH SCORES", { fontSize: 40, color: '#000000', fontStyle: "bold", backgroundColor: "#FFFFFF", padding: 10 }); 7 this.add.text(600, 100, "NEARBY HIGH SCORES", { fontSize: 40, color: '#000000', fontStyle: "bold", backgroundColor: "#FFFFFF", padding: 10 }); 8 9 this.add.text(10, 10, "YOUR SCORE: " + this.player.score, { fontSize: 40, color: '#000000', fontStyle: "bold", backgroundColor: "#FFFFFF", padding: 10 }); 10 11 for(let i = 0; i < this.globalScores.length; i++) { 12 this.add.text(10, 100 * (i + 2), `${this.globalScores[i].username}: ${this.globalScores[i].score}`, { fontSize: 40, color: '#000000', fontStyle: "bold", backgroundColor: "#FFFFFF", padding: 10 }); 13 } 14 15 for(let i = 0; i < this.nearbyScores.length; i++) { 16 this.add.text(600, 100 * (i + 2), `${this.nearbyScores[i].username}: ${this.nearbyScores[i].score}`, { fontSize: 40, color: '#000000', fontStyle: "bold", backgroundColor: "#FFFFFF", padding: 10 }); 17 } 18 19 this.retryButton = this.add.text(1125, 640, "RETRY", { fontSize: 40, color: '#000000', fontStyle: "bold", backgroundColor: "#FFFFFF", padding: 10 }); 20 this.retryButton.setInteractive(); 21 22 this.retryButton.on("pointerdown", () => { 23 this.scene.start("MainScene", { username: this.player.username, score: 0, location: this.player.location }); 24 }, this); 25 26 } catch (e) { 27 console.error(e); 28 } 29 },
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:
1 this.retryButton = this.add.text(1125, 640, "RETRY", { fontSize: 40, color: '#000000', fontStyle: "bold", backgroundColor: "#FFFFFF", padding: 10 }); 2 this.retryButton.setInteractive(); 3 4 this.retryButton.on("pointerdown", () => { 5 this.scene.start("MainScene", { username: this.player.username, score: 0, location: this.player.location }); 6 }, this);
When clicking on the button, the
MainScene
is started and the current player information is sent.You just saw how to work with leaderboard information using MongoDB within a Phaser 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.
If you want to see another gaming example with MongoDB, check out my previous tutorial titled Creating a Multiplayer Drawing Game with Phaser and MongoDB.
Related
Tutorial
Integrate MongoDB into Vercel Functions for the Serverless Experience
Sep 09, 2024 | 7 min read
Tutorial
Building AI Graphs With Rivet and MongoDB Atlas Vector Search to Power AI Applications
Sep 18, 2024 | 10 min read