Lobby and room system

The concepts of game rooms and a lobby are central to multiplayer gaming. In order to understand how it works, think about the game server as a building in which people go in order to play games together.

Before entering the building, a player may stand in front of the building and enjoy the beauty of the outside walls. In our metaphor, staring at the front of the building would be the equivalent of being greeted by a splash screen that introduces the game.

Upon entering the building, the player may or may not see some options from which to make a choice, such as a listing of the available floor to which he or she may want to go. In some games, you can choose the type of game to play as well as a difficulty level. Think of this as taking an elevator to a specific floor.

Finally, you arrive at a lobby. Similar to the way a lobby works in real life, in multiplayer games, the lobby is a special room that multiple players go to before entering a specific room where the playing takes place. In the lobby, you can see what the available rooms are and then choose one to join.

Once you have decided which room you'd like to join, you can now enter that room and participate in an existing game with other players. Alternatively, you can join an empty room and wait for others to join you there.

Typically, there is never an empty room in multiplayer games. Every room has at least one player in it, and every player can belong to one room at a time. Once all players have left the room, the game server would delete the room and release the associated resources.

Implementing the lobby

With the basic understanding of a lobby, we can implement it in a number of ways. Generally speaking, a lobby is actually a special room that all players join before they end up at a room where they'll play a particular game.

One way to implement this is to keep track of all socket connections in your server as an array. For all practical purposes, that array of sockets is your lobby. Once a player connects to the lobby (in other words, once a player has connected to your server), he or she can communicate with other players and possibly be an observing participant in a conversation between other players in the lobby.

In our case, the lobby is simple and to the point. A player is assigned to the lobby automatically upon starting the game. Once in the lobby, the player queries the server for a list of available rooms. From there, the player can issue a socket command to join an existing room or create a new one:

// ch3/snake-ch3/server/app.js

var Game = require('./../share/game.js'),
var gameEvents = require('./../share/events.js'),
var Room = require('./room.js'),

// ...

/** @type {Array.<Room>} */
var rooms = [];

module.exports = {
    newRoom: function(maxWidth, maxHeight){
        var room = new Room(FPS, maxWidth, maxHeight);
        rooms.push(room);
        return rooms.length - 1;
    },

    listRooms: function(){
        return rooms.map(function(room, index) {
            return {
                roomId: index,
                players: room.players.map(function(player){
                    return {
                        id: player.snake.id,
                        x: player.snake.head.x,
                        y: player.snake.head.y,
                        color: player.snake.color
                    };
                })
            };
        });
    },

    joinRoom: function(roomId, socket, playerId, playerX, playerY, playerColor) {
        var room = rooms[roomId];
        var snake = new Snake(playerId, playerX, playerY, playerColor, 1, 1);
        room.join(snake, socket);

        socket.emit(gameEvents.client_roomJoined, {roomId: roomId});
    },
};

Remember that our main server script exposed an interface that sockets could use to communicate with the game server. The previously mentioned script is the backend service with which the interface communicated. The actual sockets connected to the server are stored in and managed by Socket.io.

The list of available rooms is implemented as an array of Room objects, which we'll look at in detail in the next section. Note that every room will need at least two things. First, a room will need a way to group players and run the game with those same players. Second, a room will need a way for both the client and server to uniquely identify each individual room.

The two simple approaches to identify the rooms individually are to ensure that each room object has an ID property, which would need to be unique across the entire game space, or we could use the array index where the room is stored.

For simplicity, we've chosen the second. Keep in mind that, should we delete a room and splice it off the rooms array, the room ID that some players have may now point to the wrong room.

For example, suppose there are three rooms in the array so that the room ID for the rooms are 0, 1, and 2 respectively. Suppose that each of these rooms have several players participating in a game there. Finally, imagine that all the players in room ID 0 leave the game. If we splice that first room off the array (stored at index 0), then the room that used to be the second element in the array (formerly stored at index 1) would be shifted down to the front of the array (index 0). The third element in the array would also change and would be stored at index 1 instead of index 2. Thus, players who used to be in rooms 1 and 2 respectively will now report back to the game server with those same room IDs, but the server will report the first room as the second one, and the second room will not exist. Therefore, we must avoid deleting empty rooms by splicing them off the rooms array. Remember that the largest integer that JavaScript can represent is 2^53 (which equals 9,007,199,254,740,992), so we will not run out of slots in the array if we simply add new rooms to the end of the rooms array.

Implementing the rooms

The game room is a module that implements the game class and runs the game loop. This module looks fairly similar to the client game as it has references to the player and fruit objects and updates the game state at each game tick.

One difference you will notice is that there is no render phase in the server. In addition, the room will need to expose a few methods so that the server application can managed it as needed. Since each room has references to all the players in it and every player in the server is represented by a socket, the room can contact every player who is connected to it:

// ch3/snake-ch3/server/room.js

var Game = require('./../share/game.js'),
var Snake = require('./../share/snake.js'),
var Fruit = require('./../share/fruit.js'),
var keys = require('./../share/keyboard.js'),
var gameEvents = require('./../share/events.js'),

/** @type {Game} game */
var game = null, gameUpdateRate = 1, gameUpdates = 0;
var players = [], fruits = [], fruitColor = '#c00';
var fruitDelay = 1500, lastFruit = 0, fruitDelta = 0;

var Room = function (fps, worldWidth, worldHeight) {
    var self = this;
    game = new Game(fps);

    game.onUpdate = function (delta) {
        var now = process.hrtime()[1];
        if (fruits.length < 1) {
            fruitDelta = now - lastFruit;

            if (fruitDelta >= fruitDelay) {
                var pos = {
                    x: parseInt(Math.random() * worldWidth, 10),
                    y: parseInt(Math.random() * worldHeight, 10)
                };

                self.addFruit(pos);
                players.map(function(player){
                    player.socket.emit(gameEvents.client_newFruit, pos);
                });
            }
        }

        players.map(function (player) {
            player.snake.update(delta);
            player.snake.checkCollision();

            if (player.snake.head.x < 0) {
                player.snake.head.x = worldWidth;
            }

            if (player.snake.head.x > worldWidth) {
                player.snake.head.x = 0;
            }

            if (player.snake.head.y < 0) {
                player.snake.head.y = worldHeight;
            }

            if (player.snake.head.y > worldHeight) {
                player.snake.head.y = 0;
            }

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

        if (++gameUpdates % gameUpdateRate === 0) {
            gameUpdates = 0;
            var data = players.map(function(player){
                return player.snake;
            });
            players.map(function(player){
                player.socket.emit(gameEvents.client_playerState, data);
            });

            lastFruit = now;
        }
    };
};

Room.prototype.start = function () {
    game.start();
};

Room.prototype.addFruit = function (pos) {
    fruits[0] = new Fruit(pos.x, pos.y, fruitColor, 1, 1);
};

Room.prototype.join = function (snake, socket) {
    if (players.indexOf(snake.id) < 0) {
        players.push({
            snake: snake,
            socket: socket
        });
    }
};

Room.prototype.getPlayers = function(){
    return players;
};

module.exports = Room;

Note that the players array holds a list of object literals that contain a reference to a snake object as well as the actual socket. This way both resources are together in the same logical place. Whenever we need to ping every player in the room, we can simply map over the player's array and then access the socket through player.socket.emit.

In addition, note that the sync loop is placed inside the main game loop, but we only trigger the logic inside the sync loop whenever a certain amount of frames have elapsed. The goal is to only synchronize all the clients every so often.

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

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