Understanding the game loop

The next set of changes that we'll need to make on the original game code is in the game class. As you'll remember, this class defines a basic game life cycle, exposing the functions update and render, which get implemented by whoever uses it.

Since the core of the game loop defined in this class (found in Game.prototype.loop) uses window.requestAnimationFrame, we'll need to get rid of that call since it will not be available in Node.js (or in any other environment outside the browser).

One technique that is commonly used to allow us the flexibility to write a single module that is used in both the browser and the server is to wrap the browser- and server-specific functions in a custom module.

Using Browserify, we can write two separate modules that wrap the environment-specific functionality but only reference a single one in the code. By configuring Browserify property, we can tell it to compile a different module whenever it sees a require statement for the custom wrapper module. For simplicity, we have only mentioned this capability here, but we will not get into it in this book. Instead, we will write a single component that automatically detects the environment it's under at runtime and responds accordingly.

// ch3/snake-ch3/share/tick.js
var tick = function () {
    var ticks = 0;
    var timer;

    if (typeof requestAnimationFrame === 'undefined') {
        timer = function (cb) {
            setTimeout(function () {
                cb(++ticks);
            }, 0);
        }
    } else {
        timer = window.requestAnimationFrame;
    }

    return function (cb) {
        return timer(cb);
    }
};

module.exports = tick();

The tick component is made up of a function that returns one of the two functions, depending on the availability of window.requestAnimationFrame. This pattern might look somewhat confusing at first, but it offers the benefit that it only detects the environment once and then makes the environment-specific functionality every time after the initial setup.

Note that what we export from this module is a call to tick and not a mere reference. This way, when we require the module, what ends up being referenced in the client code is the function returned by tick. In a browser, this will be a reference to window.requestAnimationFrame, and in node, it'll be a function that calls setTimeout, by passing an incrementing number to it, similar to how the browser version of tick would.

Game client's game loop

Now that the abstract game loop class is ready for use in any environment, let's take a look at how we could refactor the existing client implementation so that it can be driven by sockets connected to the authoritative server.

Note how we no longer determine when a new fruit should be generated. All that we check for on the client is how we might move the player's character. We could let the server tell us where the snake is at each frame, but that would overload the application. We could also only render the main snake when the server syncs its state, but that would make the entire game seem really slow.

What we do instead is just copy the entire logic here and ignore what the server says about it when we sync. Later, we'll talk about client prediction; at that point, we'll add some logic here to correct any discrepancies that we find when the server syncs with us.

// ch3/snake-ch3/share/app.client.js

game.onUpdate = function (delta) {
    // The client no longer checks if the player has eaten a fruit.
    // This task has now become the server's jurisdiction.
    player.update(delta);
    player.checkCollision();

    if (player.head.x < 0) {
        player.head.x = parseInt(renderer.canvas.width / player.width, 10);
    }

    if (player.head.x > parseInt(renderer.canvas.width / player.width, 10)) {
        player.head.x = 0;
    }

    if (player.head.y < 0) {
        player.head.y = parseInt(renderer.canvas.height / player.height, 10);
    }

    if (player.head.y > parseInt(renderer.canvas.height / player.height, 10)) {
        player.head.y = 0;
    }

    if (fruits.length > 0) {
        if (player.head.x === fruits[0].x && player.head.y === fruits[0].y) {
            fruits = [];
            player.grow();
        }
    }
};

Game server's game loop

This is where things get exciting. Before we implement the game loop for the server-side code, we'll first need to implement an API that the client will use to query the server and issue other commands.

One of the benefits of using express in this project is that it works so well with Socket.io. Without stealing any thunder from the section later in this chapter that is dedicated to Socket.io, this is how our main server script will look like:

// ch3/snake-ch3/app.js

// …

var io = require('socket.io')();
var gameEvents = require('./share/events.js'),
var game = require('./server/app.js'),

var app = express();
app.io = io;

// …

io.on('connection', function(socket){
    // when a client requests a new room, create one, and assign
    // that client to this new room immediately.
    socket.on(gameEvents.server_newRoom, function(data){
        var roomId = game.newRoom(data.maxWidth, data.maxHeight);
        game.joinRoom(roomId, this, data.id, data.x, data.y, data.color);
    });

    // when a client requests to join an existing room, assign that
    // client to the room whose roomId is provided.
    socket.on(gameEvents.server_joinRoom, function(data){
        game.joinRoom(data.roomId, this, data.playerId, data.playerX, data.playerY, data.playerColor);
    });

    // when a client wishes to know what all the available rooms are,
    // send back a list of roomIds, along with how many active players
    // are in each room.
    socket.on(gameEvents.server_listRooms, function(){
        var rooms = game.listRooms();
        socket.emit(gameEvents.client_roomsList, rooms);
    });
});

Adding to the default Express app.js script, we import Socket.io, the game events module that we defined earlier, and the game application that we will discuss throughout the rest of the chapter.

Next, after we've finished setting up Express, we set up our socket communication with the clients. The first step is to wait until a connection has been established, which will give us access to an individual socket that is bound to an individual client.

Once we have a live socket, we configure all the events we care about by registering custom event listeners to each event. You will notice that some of the sample event listeners mentioned previously also emit events back to the requesting socket, while others simply call methods on the game object. The difference between the two scenarios is that when we only need to talk to a single client (the requesting client), we contact that socket directly from the event listener. There are situations, however, when we might wish to talk to all the sockets connected to the same room. When this is the case, we must let the game object alert all the players that it needs since it will know who all are the clients that belong to a given room.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.191.74.66