Implementing an authoritative server

The strategy that we'll use for this server will be to run two game loops for two different purposes. The first loop is the physics update, which we'll run close to the same frequency as the clients' loop. The second loop, which we'll refer to as the client sync loop, is run at a slower pace, and at each tick, it will send the entire game state to every connected client.

At this point, we'll only focus on getting the server working as we've described. The current implementation of the clients will continue to work as it did, managing the entire game logic locally. Any data a client receives from the server (using the game sync loop) will only be rendered. Later in the book, we'll discuss the concept of client prediction, where we'll use the input from the game sync loop as the actual input for the game's logic rather than just rendering it mindlessly.

Game server interface

The first thing to change from the current implementation of the game client will be to break the input and output points so that they can communicate with the socket layer in the middle. We can think of this as a programming interface that specifies how the server and clients will communicate.

For this, let's create a simple module in our project to serve as a poor man's enum since enums aren't available in JavaScript. Though the data in this module will not be immutable, it will give us the advantage since the IDE will automatically suggest values, correct us when we make a typing mistake, and put all of our intents in one place. By convention, any event that starts with the word server_ represent actions for the server. From example, the event named server_newRoom asks the server to create a new room:

// ch3/snake-ch3/share/events.js

module.exports = {
    server_spawnFruit: 'server:spawnFruit',
    server_newRoom: 'server:newRoom',
    server_startRoom: 'server:startRoom',
    server_joinRoom: 'server:joinRoom',
    server_listRooms: 'server:listRooms',
    server_setPlayerKey: 'server:setPlayerKey',

    client_newFruit: 'client:newFruit',
    client_roomJoined: 'client:roomJoined',
    client_roomsList: 'client:roomsList',
    client_playerState: 'client:playerState'
};

We now use the string values defined in this module to register callbacks for and emit events to sockets in a consistent and predictable way between the client and the server. For example, when we emit an event named modules.exports.server_spawnFruit, we know that what is intended is that a message to be received by the server has the action name of spawnFruit. In addition, you'll notice that we'll use socket.io to abstract away the socket communication between the client and the server. If you're curious to get started with socket.io right now, feel free to skip ahead to the end of this chapter and read the Socket.io section.

var gameEvents = require('./share/events.js'),

socket.on(gameEvents.server_spawnFruit, function(data){
   var pos = game.spawnFruit(data.roomId, data.maxWidth, data.maxHeight);

   socket.emit(gameEvents.client_newFruit, pos);
});

In the given example, we first include our module into a gameEvents variable. We then register a callback function whenever a socket receives an server_spawnFruit event. Presumably, this code is in some server code, as indicated by the server keyword at the beginning of the key name. This callback function takes a data argument created by the client (whoever is sending the command on the other end of the socket). This data object has the data that is needed by the specific call to spawn a new fruit object for the game.

Next, we use the input data into the socket event to perform some task (in this case, we generate a random position where a fruit can be added in the game world). With this data on hand, we emit a socket command back to the client to send the position that we just generated.

Updating the game client

The first thing to change in the client code is to add different screens. At a minimum, we need two different screens. One of the screens will be the game board as we've implemented so far. The other is the lobby, which we'll discuss in more detail later. In brief, the lobby is an area where players go before they join a specific room, which we'll also discuss shortly.

Updating the game client

Once in the lobby, the player can choose to join an existing room or create and join a new room with no players in it.

In a perfect world, your game engine would offer great support for multiple screens. Since the sample game we're writing is not written in such a game engine, we'll just use basic HTML and CSS and write every screen along with any supporting props and widgets in the same HTML file that will be served up originally:

// ch3/snake-ch3/views/index.jade

extends layout

block content
  div#lobby
    h1 Snake
    div#roomList

 div#main.hidden
    div#gameArea
      p#scoreA SCORE: <span>000000</span>
      p#gameOver.animated.pulse.hidden Game Over
      canvas#gameCanvas
      div#statsPanel

  script(src='/js/socket.io.js')
  script(src='/js/app.build.js')

There are only three blocks of code in the previous template. First, we have a div element with an ID of lobby inside which we dynamically add a list of available game rooms. Next, there is a div element with an ID of main, initially with a class of hidden, so that this screen is not visible initially. Finally, we include the socket.io library as well as our app.

The simplest way to bind to that HTML structure is to create module-wide global variables that reference each desired node. Once these references are in place, we can attach the necessary event listeners so that the player can interact with the interface:

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

var roomList = document.getElementById('roomList'),
var screens = {
    main: document.getElementById('main'),
    lobby: document.getElementById('lobby')
};

// …

socket.on(gameEvents.client_roomsList, function (rooms) {
    rooms.map(function (room) {
        var roomWidget = document.createElement('div'),
        roomWidget.textContent = room.players.length + ' player';
        roomWidget.textContent += (room.players.length > 1 ? 's' : ''),

        roomWidget.addEventListener('click', function () {
            socket.emit(gameEvents.server_joinRoom, {
                    roomId: room.roomId,
                    playerId: player.id,
                    playerX: player.head.x,
                    playerY: player.head.y,
                    playerColor: player.color
                }
            );
        });

        roomList.appendChild(roomWidget);
    });

    var roomWidget = document.createElement('div'),
    roomWidget.classList.add('newRoomWidget'),
    roomWidget.textContent = 'New Game';

    roomWidget.addEventListener('click', function () {
        socket.emit(gameEvents.server_newRoom, {
            id: player.id,
            x: player.head.x,
            y: player.head.y,
            color: player.color,
            maxWidth: window.innerWidth,
            maxHeight: window.innerHeight
        });
    });

    roomList.appendChild(roomWidget);
});

socket.on(gameEvents.client_roomJoined, function (data) {
    // ...
    screens.lobby.classList.add('hidden'),
    screens.main.classList.remove('hidden'),
});

Since the initial game screen is the lobby, and the markup for the lobby is already visible, we don't do anything else to set it up. We simply register a socket callback to be invoked when we receive a list of available rooms and create individual HTML nodes with event listeners for each, attaching them to the DOM when we're ready.

Inside a different socket callback function, this time the one associated with the roomJoined custom event, we first make the lobby screen invisible, and then we make the main screen visible. We do this by adding and removing a CSS class named hidden, whose definition is shown in the following code snippet:

// ch3/snake-ch3/public/css/style.css

.hidden {
    display: none;
}
..................Content has been hidden....................

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