Our website is made possible by displaying online advertisements to our visitors. Please consider supporting us by disabling your ad blocker.

Manage Game User Profiles with MongoDB, Phaser, and JavaScript

TwitterFacebookRedditLinkedInHacker News

When it comes to game development, you’re almost always going to need to store information about your player. This information could be around how many health points you currently have in the game or it can extend beyond the game-play experience and into details such as the billing information for the person playing the game. When we talk about this type of data, we’re talking about a user profile store.

The user profile has everything about the user or player and doesn’t end at health points or billing information.

In this tutorial, we’re going to look at creating user profiles in a game that leverages the Phaser game development framework, JavaScript, and MongoDB.

To get a better idea of what we’re going to accomplish, take a look at the following animated image:

Phaser User Profile Store with MongoDB

Alright, so we’re not making a very exciting game. However, many modern games let you design your character either before the game starts or at some point as you progress through the game. These customizations that you add to your character are stored within the user profile. In our example, we’re going to customize our player and then send the information to our back end for storing in MongoDB.

The Requirements

There are a few components to this game, but we’ll be leveraging a JavaScript stack throughout. To be successful with this tutorial, you’ll need the following:

  • Node.js 12+
  • A properly configured MongoDB cluster with a gamedev database and a profiles collection

We’ll be using Node.js to create a back end API that will communicate with our MongoDB cluster. The game, which will be our front end, will communicate with our Node.js back end. We won’t go into the details on how to configure and deploy a MongoDB instance, but you’ll need one with a database and collection available for use.

If you want to quickly get up and running with MongoDB, consider deploying a MongoDB Atlas M0 cluster, which is FREE forever!

If you need help with configuring MongoDB, check out this previous tutorial on the subject.

To create a Phaser game, you don’t need anything beyond basic HTML, CSS, and JavaScript, so there aren’t any prior requirements for the game component.

Defining a Data Model for a User Profile Store in a Phaser Game

Before we get into the code, it is probably a good idea to explore what our user profile should contain. These aren’t blanket rules for all user profiles because often, games are not the same.

Based on the animated image toward the beginning of this tutorial, we know we have the following JSON:

{
    "_id": "nraboy",
    "cosmetics": {
        "eyes": "player-eyes-1",
        "mouth": "player-mouth-1"
    },
    "hp": 100,
    "mp": 30
}

The depth of the above profile is hardly anything, but we know for this particular player that they’ve decided to give their character a certain appearance and that character has a particular set of attributes such as health points.

If you want to see a more aggressive user profile, check out my previous tutorial, which is focused on a Unity game that I’m building with Adrienne Tacke.

You might consider adding a hashed password, player position within the game, stored items, or any other relevant field to your user profile.

What makes MongoDB particularly powerful when it comes to gaming and user profiles is how flexible the data model can be. If we release an update to our game that includes customizations to the color of the diamond, we could easily add that field to our NoSQL documents. Adding and removing fields requires no special migrations or coding, which translates to faster development and better code.

Developing the User Profile Store Back End API with Express Framework and MongoDB

We have a rough idea of what our user profile should look like for this particular game. Now we need to be able to add it to MongoDB and query for it. To do this, we’re going to create a back end with two simple API endpoints: an endpoint for creating a document with the profile and an endpoint for retrieving it.

For this particular tutorial, we are not going to worry about authentication. In other words, we are only going to be creating user profiles and not caring who they truly belong to.

Within a new directory on your computer, execute the following commands:

npm init -y
npm install cors express body-parser mongodb --save

The above commands will create a new package.json file and download the dependencies that we plan to use. We’re using the cors package because we plan to run our game locally within a web browser. If we don’t manage cross-origin resource sharing (CORS), we’re going to get errors. The express and body-parser packages will be for our development framework and the mongodb package will be for interaction with MongoDB.

Within the same project, create a main.js file with the following code:

const { MongoClient, ObjectID } = require("mongodb");
const Express = require("express");
const BodyParser = require("body-parser");
const Cors = require("cors");

const server = Express();

server.use(BodyParser.json());
server.use(BodyParser.urlencoded({ extended: true }));
server.use(Cors());

const client = new MongoClient(process.env["ATLAS_URI"]);

var collection;

server.post("/profile", async (request, response, next) => {});
server.get("/profile/:username", async (request, response, next) => {});

server.listen("3000", async () => {
    try {
        await client.connect();
        collection = client.db("gamedev").collection("profiles");
        console.log("Listening at :3000...");
    } catch (e) {
        console.error(e);
    }
});

The core of what is happening in the above code is we are laying the foundation to our Express Framework application by importing the dependencies and initializing the framework. When we start listening for connections, then we connect to MongoDB and define which database and collection we want to use. In this example, we are using a gamedev database and a profiles collection. You can use whatever makes sense to you.

The actual connection information for MongoDB is defined with an ATLAS_URI environment variable. You could easily use a configuration file or hard-code this value, but environment variables tend to work out the best for security and deployment reasons.

The connection string to be used would look something like this:

mongodb+srv://<username>:<password>@plummeting-us-east-1.hrrxc.mongodb.net/<dbname>

You can gather the information from your MongoDB Atlas dashboard after creating the appropriate user credentials for your database and collection.

When I add an environment variable, I generally do something like the following:

export ATLAS_URI="mongodb+srv://<username>:<password>@plummeting-us-east-1.hrrxc.mongodb.net/<dbname>"

There are numerous ways to do this and it may vary depending on your operating system.

So let’s focus on those endpoints.

When it comes to creating a user profile within MongoDB, we might have an endpoint that looks like the following:

server.post("/profile", async (request, response, next) => {
    try {
        let result = await collection.insertOne(request.body);
        response.send(result);
    } catch (e) {
        response.status(500).send({ message: e.message });
    }
});

The above endpoint is probably the most basic endpoint you can create. We are accepting a payload from the front end and we are inserting it into the database. Then we are returning the response that the database gives us.

In a production game, you’re probably going to want to validate your data to prevent cheating or other malicious activity. You can look into Schema Validation with MongoDB or use a client-facing package like joi instead.

With data going into MongoDB, now we can create our endpoint for retrieving it:

server.get("/profile/:username", async (request, response, next) => {
    try {
        let result = await collection.findOne({ "_id": request.params.username });
        response.send(result);
    } catch (e) {
        response.status(500).send({ message: e.message });
    }
});

After supplying this endpoint a username, which in our example is the _id field, we use it to find one particular document. That one document is returned to the game or whatever tried to retrieve it.

Again, we are not using any data validation or authentication for this tutorial as it is outside the scope of what we want to accomplish.

We now have an API to work with.

Building a Character Creation Game with Phaser and JavaScript

With the API in place, we can focus on the game itself. Remember, the depth of our game is quite shallow, but it offers a feature that a lot of games offer. That feature is customizing the character.

I’m not going to share my graphic assets because I want you to use some imagination for creating your own. Will you create something better? Probably, but it’s on you!

Create an index.html file on your computer with the following HTML markup:

<!DOCTYPE html>
<html>
    <body>
        <div id="game"></div>
        <script src="//cdn.jsdelivr.net/npm/phaser@3.51.0/dist/phaser.min.js"></script>
        <script>

            const phaserConfig = {
                type: Phaser.AUTO,
                parent: "game",
                width: 1280,
                height: 720,
                scene: {
                    init: initScene,
                    preload: preloadScene,
                    create: createScene
                }
            };

            const game = new Phaser.Game(phaserConfig);

            function initScene() {}
            function preloadScene() {}
            function createScene() {}

        </script>
    </body>
</html>

The above markup is standard Phaser boilerplate. We define the container where the game should be loaded, import the JavaScript dependency, and initialize the game while telling it to use the container.

The real magic is going to happen in the scene lifecycle functions. The initScene function is where we’ll initialize our variables, the preloadScene function is where we’ll load our media assets, and the createScene function is where we’ll do our first render.

Starting with the initScene function, add the following:

function initScene() {

    this.player = {
        "_id": "nraboy",
        "cosmetics": {
            "eyes": "",
            "mouth": ""
        },
        "hp": 100,
        "mp": 30
    }
    this.eyes = [];
    this.mouth = [];

}

For our example, the player object will represent our locally stored user profile object. It will look exactly like what you’d find stored in the database. This object could be as simple or as complex as you want to make it depending on your game needs. The _id field must be unique, so if you’d prefer to let MongoDB generate it, remove the field and create a username or similar field instead. It’s totally up to you.

The eyes and mouth arrays will represent all possible options for character customizations. The chosen customization will be stored in the user profile.

The next step is to look at the preloadScene function:

function preloadScene() {

    this.load.image("background", "game-background.png");
    this.load.image("player-base", "player-base.png");
    this.load.image("player-eyes-1", "player-eyes-1.png");
    this.load.image("player-eyes-2", "player-eyes-2.png");
    this.load.image("player-mouth-1", "player-mouth-1.png");
    this.load.image("player-mouth-2", "player-mouth-2.png");

}

In the above function, we are loading our image files and giving them a reference name to be used throughout the game. In the above example, the reference name is a pretty close name to the filename itself, but it doesn’t have to be.

I didn’t provide the image assets, so feel free to be adventurous.

The magic happens in the createScene function:

function createScene() {

    this.add.image(640, 360, "background");
    this.add.image(800, 250, "player-base");
    this.eyesActive = this.add.image(800, 250, "player-eyes-1");
    this.mouthActive = this.add.image(800, 250, "player-mouth-1");

    for (let i = 1; i <= 2; i++) {
        this.eyes.push(
            this.add.image(75, 90 * i, "player-eyes-" + i)
                .setCrop(150, 115, 100, 50)
                .setScale(0.5)
                .setInteractive()
                .on("pointerdown", () => {
                    this.eyesActive.setTexture("player-eyes-" + i);
                })
        );
        this.eyes[i - 1].input.hitArea = new Phaser.Geom.Rectangle(150, 115, 100, 50);
    }
    
    for (let i = 1; i <= 2; i++) {
        this.mouth.push(
            this.add.image(215, 90 * i - 40, "player-mouth-" + i)
                .setCrop(150, 195, 100, 50)
                .setScale(0.5)
                .setInteractive()
                .on("pointerdown", () => {
                    this.mouthActive.setTexture("player-mouth-" + i);
                })
        );
        this.mouth[i - 1].input.hitArea = new Phaser.Geom.Rectangle(150, 195, 100, 50);
    }

    this.add.text(289, 525, "PROFILE:", { fontSize: 36, color: "#FFFFFF", fontStyle: "bold" })
    this.profileData = this.add.text(289, 575, "", { fontSize: 16, color: "#FFFFFF" })

    this.startButton = this.add.text(1125, 640, "START", { fontSize: 40, color: '#000000', fontStyle: "bold", backgroundColor: "#FFFFFF", padding: 10 });
    this.startButton.setInteractive();
    this.startButton.on("pointerdown", () => {
        this.player.cosmetics.mouth = this.mouthActive.texture.key;
        this.player.cosmetics.eyes = this.eyesActive.texture.key;
        fetch("http://localhost:3000/profile", {
            "method": "POST",
            "headers": {
                "content-type": "application/json"
            },
            "body": JSON.stringify(this.player)
        })
        .then(response => response.json())
        .then(response => {
            this.profileData.setText(JSON.stringify(this.player));
        }, error => {
            console.error(error);
        });
    }, this);

}

Let me start by saying a few things first.

  • The game in my example is 1280x720 and the image assets reflect this.
  • The base image for the character and any cosmetic attachment image has the same resolution.

Rather than creating perfectly sized cosmetic attachment images, in my example, I just removed the base image but left the attachment images the same size but with transparency. Trying to figure out how to position small images could turn into more work than I wanted. Layering the images worked better.

With that said, we have the following:

this.add.image(640, 360, "background");
this.add.image(800, 250, "player-base");
this.eyesActive = this.add.image(800, 250, "player-eyes-1");
this.mouthActive = this.add.image(800, 250, "player-mouth-1");

The initialize images are placed on the screen with varying positions. The eyesActive and mouthActive represent the initial attachments for the character. We are only setting those to a variable because we plan to change them later.

Based on the naming convention of the images and the sizes we chose to use, we can display the attachment options for eyes on the screen:

for (let i = 1; i <= 2; i++) {
    this.eyes.push(
        this.add.image(75, 90 * i, "player-eyes-" + i)
            .setCrop(150, 115, 100, 50)
            .setScale(0.5)
            .setInteractive()
            .on("pointerdown", () => {
                this.eyesActive.setTexture("player-eyes-" + i);
            })
    );
    this.eyes[i - 1].input.hitArea = new Phaser.Geom.Rectangle(150, 115, 100, 50);
}

In this example, we have two different possibilities for eyes. When pushing them into our array, we are specifying the position, but we are also changing a few things in regards to the image. For example, we know the images for attachments are mostly transparent. We can crop them and scale them to show only what we want. This is for selection purposes only, not displaying on the actual character.

We also want to respond to click events. When a click event happens, we want the appropriate image to be used as the active image. However, because we scaled and cropped our image, we need to redefine the hitArea, which is the area that should be clickable on the image.

Doing most of these steps is only a requirement if you are using images of the same size and planning to layer them like I am.

We can reproduce our steps for the mouth options:

for (let i = 1; i <= 2; i++) {
    this.mouth.push(
        this.add.image(215, 90 * i - 40, "player-mouth-" + i)
            .setCrop(150, 195, 100, 50)
            .setScale(0.5)
            .setInteractive()
            .on("pointerdown", () => {
                this.mouthActive.setTexture("player-mouth-" + i);
            })
    );
    this.mouth[i - 1].input.hitArea = new Phaser.Geom.Rectangle(150, 195, 100, 50);
}

The steps are the same, but we are using different positions.

Remember, these two steps are for defining the thumbnails of our images and specifying a click region for them. While the active image is what we show on the character, the lead up is related to the thumbnails.

With the thumbnails out of the way, we can add some placeholder text of what profile we want to send:

this.add.text(289, 525, "PROFILE:", { fontSize: 36, color: "#FFFFFF", fontStyle: "bold" })
this.profileData = this.add.text(289, 575, "", { fontSize: 16, color: "#FFFFFF" })

The goal with this text is to only show what our player object looks like on the screen. This will give us a good idea of what was sent to our MongoDB database.

Finally, we have the button that will submit the data to our API:

this.startButton = this.add.text(1125, 640, "START", { fontSize: 40, color: '#000000', fontStyle: "bold", backgroundColor: "#FFFFFF", padding: 10 });
this.startButton.setInteractive();
this.startButton.on("pointerdown", () => {
    this.player.cosmetics.mouth = this.mouthActive.texture.key;
    this.player.cosmetics.eyes = this.eyesActive.texture.key;
    fetch("http://localhost:3000/profile", {
        "method": "POST",
        "headers": {
            "content-type": "application/json"
        },
        "body": JSON.stringify(this.player)
    })
    .then(response => response.json())
    .then(response => {
        this.profileData.setText(JSON.stringify(this.player));
    }, error => {
        console.error(error);
    });
}, this);

When the button is clicked, the image reference is added to the player object. Using a fetch operation, we can send the player object to our back end which will store it in MongoDB. Since we aren’t doing any data validation, this will succeed as long as the _id field is unique.

Phaser User Profiles in MongoDB

If you’re using MongoDB Atlas and you look at the data explorer, the documents should look something like the above image.

Conclusion

You just saw how to store user profiles for a Phaser game as NoSQL documents in MongoDB. Depending on the game you’re creating, the user profile documents could be significantly more complex in comparison to what was seen in this example. At the end of the day, you’re storing information about your game-play experience and that information could include information regarding the user such as password, billing information, or similar, and you’re also storing information about the game, which might include outfit customizations, item inventory, player location, or something else game-related.

If you are interested in seeing more Phaser examples with MongoDB, check out some of my previous tutorials on the subject. If you’re a Unity developer, I also have some content on that subject as well.

Got questions regarding this tutorial or MongoDB? Visit the MongoDB Community Forums and connect with us!

This content first appeared on MongoDB.

Nic Raboy

Nic Raboy

Nic Raboy is an advocate of modern web and mobile development technologies. He has experience in C#, JavaScript, Golang and a variety of frameworks such as Angular, NativeScript, and Unity. Nic writes about his development experiences related to making web and mobile development easier to understand.