Set Up A Node.Js Server To Add A Leaderboard To Your Game
Posted on June 22nd, 2016 by foxcode
Let your players show off their skills by adding a global leaderboard to your game. We will use a simple server setup to share data between users.
READ MORE

Set me up a server! Lets make a global leaderboard that anyone playing our game can submit scores to.

Single player games are fun, but showing your Tetris prowess to the world requires a little more technology. We are going to set up a basic server with a simple database. This allows us to permanently store any game or player data we want and make it easily accessible.

Node.js is a Javascript based server technology. This makes it ideally suited to HTML5 games and apps, as you can use Javascript for both the client and server parts of your project.

Installing Node.js is easy. Open a browser and navigate to https://nodejs.org. From here you can download the current version of node. During installation, you may be prompted about adding a variable to your PATHS. If this happens, accept it, this simply allows you to access Node from anywhere. To verify your install, open a command prompt window, and type node -version

Our server is great, but to store data permanently we need a database. We will use MongoDB. Mongo stores data as JSON objects (Javascript Object Notation). This is great as we can easily convert between JSON and our game objects. You can install mongo from https://www.mongodb.com/. You now have the two key things to create your server.

First we will create our server in a file called server.js. We want the ability to retrieve the current high scores and add our own. We can do this by defining a submitScore route and a highScores route which we will fill in later.

	var express = require('express');           // Used to easily direct requests 
	var mongojs = require('mongojs');           // Javascript interface to mongodb
	var bodyParser = require('body-parser');    // Parses post request data for us
	var app = express();

	// Create mongo connection
	app.db = mongojs('game_server', ['scores']);

	// Tell the app to parse the body of incoming requests
	app.use(bodyParser.urlencoded({extended:false}));
	app.use(bodyParser.json());

	// This will be called when a user submits a new score
	app.post("/submitScore", function(req, res)
	{
		console.log("Submit Score Called");
		res.send("What's up");
	});

	// This will be called when a user requests a list of scores
	app.post("/highScores", function(req, res)
	{
		console.log("Highscores Called");
		res.send("What's up");
	});

	// Start the server
	var server = app.listen(2222, function()
	{ 
		console.log('Listening on port %d', server.address().port);
	});
        

Note the first 3 require statements. These allow us to include extra node modules that will be very useful, but they must first be installed. Luckily we can do this without leaving our command window. That node.js installation you just completed included the node package manager, or NPM. Open a command prompt in the same directory as server.js. From there type the following 3 lines one after the other.

    npm install express
    npm install mongojs
    npm install body-parser
        

Your console window should look something like this

This will create a node_modules folder in the directory, which in turn will contain any modules you install using the install command. This makes it trivial to add modules to your project.

Now for some fun. Create a data folder to store your database somewhere, for example c:\db. To start the database, open a command window in the directory where you installed the binaries of MongoDB. On a Windows PC this may be something like C:\Program Files\MongoDB\Server\3.2\bin. Type mongod --dbpath c:\db . You may use a path other than c:\db however it must exist, Mongo will not create it for you. In a new command window. Type "mongo game_server". This opens a connection to the database that we can see in the command window (so this is a client that is connected to the database server), it also verifies that Mongo is running correctly.

We can finally start the node.js server. Open one final command window in the directory of your server.js file. Simply type "node server". If everything has worked correctly, you should now be looking at a command window that says "Listening on port 2222".

We have our server running, but we have no application to make use of it. Let's make a quick detour and program a very simple app that can use our server. This app will simply generate a score, send it to the server and retrieve a list of scores. Open WADE and create a new project.

For simplicity, we will add only a single text sprite and write all our code in app.js. Create a text sprite, name its scene object "score" and set the text colour to white. You should have something that looks like this

Now double click on app.js to open it, and copy the following code into it

    App = function()
    {
        var setTextSprite = function(textString)
        {
            wade.getSceneObject("score").getSprite().setText(textString);
        }
        
        var generateRandomScore = function()
        {
            var score = Math.floor(Math.random()*1000 + 1);
            setTextSprite(score);
            return score;
        };
        
        var submitScore = function(score, callback)
        {
            $.post("http://localhost:2222/submitScore", {score:score}, function(data)
            {
                if(!data)
                {
                    console.log("Server Communication Error");
                    callback && callback(false);
                    return;
                }
                if(data.error)
                {
                    console.log("Server Error: " + data.error);
                    callback && callback(false);
                    return;
                }
                callback && callback(true);
            });
        };
        
        var getScores = function(callback)
        {
            $.post("http://localhost:2222/highScores", null, function(data)
            {
                if(!data)
                {
                    console.log("Server Communication Error");
                    callback && callback(false);
                    return;
                }
                if(data.error)
                {
                    console.log("Server Error: " + data.error);
                    callback && callback(false);
                    return;
                }
                callback && callback(data.scores);
            });
        };
        
        // this is where the WADE app is initialized
        this.init = function()
        {
            // load a scene
            wade.loadScene('scene1.wsc', true, function()
            {
                // Wait a bit then generate a score
                setTimeout(function() 
                {
                    var score = generateRandomScore();
                    submitScore(score, function(success) // Submit the score
                    {
                        if(!success) // If score submission failed, exit
                        {
                            return;
                        }
                        setTextSprite("" + score + " Submitted Successfully");
                        // Wait a bit more then get scores from server
                        setTimeout(function() 
                        {
                            getScores(function(scores) // Get scores
                            {
                                var scoreString = "";
                                for(var i=0; i<scores.length; i++)
                                {
                                    scoreString += (i+1) + ". " + scores[i];
                                    if(i<scores.length-1)
                                    {
                                        scoreString += "   ";
                                    }
                                }
                                setTextSprite(scoreString);
                            });
                        },1000);
                    });
                }, 1000);
            });
        };
    };

        

I realise this may be a bit daunting, but it's really quite simple once you break it down. The functions setTextSprite and generateRandomScore are fairly straight forward and basically do what their names imply. The wade init function looks a bit mad with setTimeout written all over the place. These timeouts are artificially delaying our program so we can see the text sprite change. The code would still work fine without them, but everything would happen so fast we would not see the process.

This leaves submitScore and getScores. Lets look at submit score first

    var submitScore = function(score, callback)
    {
        $.post("http://localhost:2222/submitScore", {score:score}, function(data)
        {
            if(!data)
            {
                console.log("Server Communication Error");
                callback && callback(false);
                return;
            }
            if(data.error)
            {
                console.log("Server Error: " + data.error);
                callback && callback(false);
                return;
            }
            callback && callback(true);
        });
    };
        
Submit score sends a post request to the server, and includes an object containing our generated score. The function with the parameter data, is a callback function: it will be executed when the post request returns. This happens when the server sends a response back.

When the server does return, we check the data parameter to make sure it actually exists and to see if there are any errors. If there are not, we call our callback that we initially passed into the function, passing true if everything worked well, or false if we had an error.

    var getScores = function(callback)
    {
        $.post("http://localhost:2222/highScores", null, function(data)
        {
            if(!data)
            {
                console.log("Server Communication Error");
                callback && callback(false);
                return;
            }
            if(data.error)
            {
                console.log("Server Error: " + data.error);
                callback && callback(false);
                return;
            }
            callback && callback(data.scores);
        });
    };
        

Get scores is virtually identical. The difference is we are interested in the value that is returned, which is an array of scores. All this means is we need to pass this value to our callback so we can make use of it, in our case displaying it in the text sprite.

If we go ahead and run our app now, it will not quite work. We still have some work to do in server.js - this is where the magic happens. First lets finish submitScore

    app.post("/submitScore", function(req, res)
    {
        if(!req.body.score)
        {
            res.send({error:"No score value was submitted"});
            return;
        }
        var score = parseInt(req.body.score);
        app.db.scores.insert({score:score}, function(err)
        {
            if(err)
            {
                console.log("Failed to insert score: " + err);
                res.send({error:"Internal Server Error"});
                return;
            }
            res.send({success:true});
        });
    });
        

This is the complete submit score, lets take a look. First we check that a score was actually sent in the post request. If there is no score, we return an error. If there is a score, we ensure it is an integer and then insert it into the scores table. Adding extra functionality to the server is easy, just remember to send a response on all conditions, else your client application may get stuck waiting for an answer that will never arrive. In this project we handle that by always returning either error or success/

    app.post("/highScores", function(req, res)
    {
        app.db.scores.find({}, {_id:0}).sort({score:-1}).limit(3, function(err, result)
        {
            if(err)
            {
                console.log("Failed to find scores: " + err);
                res.send({error:"Internal Server Error"});
                return;
            }
            var scores = [];
            for(var i=0; result && i<result.length; i++)
            {
                scores.push(result[i].score);
            }
            res.send({success:true, scores:scores});
        });
    });
        

highScores has a little more to it. We don't need to provide any parameters but we do need to query the database. Lets look at the first line inside the function. The find statement has 2 parameters. The first is to filter our results, this is a blank object in our case as we want all entries in the scores table to be considered. The second is an optional parameter that allows you to include or exclude values from the returned results. There is no reason for the client to need the entry _id for the data (this is just a unique string used by mongo to identify all items in the database), so we exclude that by setting _id:0.

The sort allows us to return results easily in descending order, this just saves us sorting the data on the client side. The limit is fairly simple, we only want to return a maximum of 3 results. This is mainly because in our simple example we cannot fit many more on the single line of one text sprite. The callback function with parameters err and result always goes in the last chained function call in the query. If successful, result should contain an array of objects, but ideally we would prefer a simple array of score values, so we loop over the result array, and create a simple array that we return to the client.

To restart the server, open its command window, hit Ctrl-C to stop it, then either hit up arrow enter, or retype node server.js to start the server. Now that our updated and complete server is running, we can return to our wade app and run it. If everything works, you should see a random number generated, successfully submitted to the server, and then a list of the current top three scores. Obviously you'll need to run it multiple times to see more scores.

There should now be a valid score stored in our database. To see this, go back to the command window we opened earlier using "mongo game_server" and type "show collections". You should be able to see a scores table. Enter db.scores.find(). This will return a list of all scores. If this table becomes cluttered and you want to start from scratch when looking at the example, enter db.scores.drop(), this will clear all entries from the table.

Congratulations. That was a lot of work. Using some basic queries and the mongo documentation, you should easily be able to increase the functionality of this simple example. I realise this is a lot to go through in a single blog post so I have included both the wade app and the server code that I used when creating it. Have fun :)

Post a Comment
Add Attachment
Submit Comment
Please Login to Post a Comment