images

Chapter 9

A Real-Time Multiplayer Game Using WebSockets

In this chapter, we will walk through the creation of a simple multiplayer game. We will use the HTML5 canvas to display graphics to the user, WebSockets to facilitate communication with our server, and node.js to create the server.

WebSockets are truly a boon to game development on the web. Before the advent of WebSockets, game clients had to speak to the game's server by polling with XMLHttpRequest or by using Flash. The former has many drawbacks, —particularly that it is very wasteful of bandwidth, and the latter is something that we, as HTML5 developers, should like to avoid. WebSockets, on the other hand, are native to the browser, with a simple javascriptJavaScript interface. Data sent over WebSockets doesn't carry HTTP headers, so it is much more efficient than XMLHttpRequest.

The WebSocket protocol has gone through several iterations since 2010, but now appears to be converging to a final version. Browser support for WebSockets has been notoriously spotty, but the September 2011 releases of Chrome 14 and Firefox 7, both supporting the same version (“hybi-10”) of the WebSocket protocol, are a good sign that things are settling down. Internet Explorer 10, not yet released at the time of this writing, will also implement that version. Browser support will only improve from here.

WebSockets are easy to implement and extremely useful. Let us begin, then, to develop an online bumper car game!

Philosophy of netcode

In a multiplayer game, nothing ruins the experience quite like somebody cheating. Unfortunately, there will always be somebody who tries to cheat at your game. Even more unfortunately, “cheating” in a javascriptJavaScript game, such as the one we will soon construct, is extremely simple. JavascriptJavaScript is not even compiled! The raw source code is available to anybody playing your game. Using Chrome's developer tools, the Firebug browser addonadd-on, etc.and so forth, they can even change the code of the game and the contents of variables as the game runs. All of a sudden, that puny starter weapon jumps from doing 5 damage per hit to 50,000 damage per hit. Uh oh.

For a single player game, this is arguably not an issue. If the player wants to hack around with the game and change it, then it's his fault if it breaks and it affects no one else. Obviously, though, a multiplayer game needs some sort of protection against this abuse. One way to have that protection is to make the server authoritative. In other words:

Tip Never trust the user! Always expect users to be malicious, and therefore do all important processing on the server side.

The game's logic should be executed on the server which is, presumably, impervious to corruption by a would-be cheater. In a sense, the server will be the only computer running the “real” copy of the game. Periodically, it tells the players what's going on, and that is what shows up on their screens. Of course, the players can also supply input (to control their characters, etc.) whichthat the server processes. In this scheme, the cheater fires his ultra-powerful gun, but the server doesn't know or care that the cheater intends for it to do 50,000 damage. All the server receives is a command to fire, and then it fires the regular old 5 damage bullet.

This sounds like an airtight solution, but can it be put to practical use in a game? Yes it can, with some caveats. To illustrate one shortcoming, imagine we make a game where we execute the game logic and then draw the game's graphics to a JPEG file, all on the server side. Then, the server simply sends out this JPEG to all the players and the game's “client” just draws that JPEG and processes user input. This is the epitome of simplicity, but unfortunately it would be borderline unplayable—it would be very choppy. Even supposing the server sends the game state (i.e., the JPEG) to the players ten times per second, the animation on the player's computer will run at just ten frames per second (even less if we consider network latency). We can make it smoother by sending out game state more often, but bandwidth costs will shoot up (even if we're not being silly and sending an image file).

It's really not necessary, though, to have the server telling the client 1000 times per second what's going on. The conditions in your game are probably not changing on such a fine timescale. Ten updates per second, or something on that order of magnitude, is usually enough to accurately portray to the players the correct state of the game. In between these updates, we can have the game client execute its own copy of the game logic, so that things continue to move in predictable, non-choppy ways, and the game doesn't feel unresponsive. The next time an update arrives from the server, it will replace whatever the local game client has done, because the server is authoritative. If the time between server updates is short, then the client's depiction and the server's depiction of the game won't differ much, and the gameplay will be smooth. This simple idea, simulating the game locally while the real game is being played on the server, is sometimes called “dead reckoning.”.

Ideally, we can share the code for the game logic between the client side (for responsiveness) and the server side (for actually running the game). That way, we don't need to have two separate copies of complicated code whichthat perform very similar tasks. It's not always true that this can be done. In fact, JavascriptJavaScript has been, until recently, totally confined to users' web browsers. Only now, with JavascriptJavaScript interpreters like Google's V8 (http://code.google.com/p/v8/) and Mozilla's Rhino (www.mozilla.org/rhino/), can JavascriptJavaScript code be run outside the context of any browser. In this chapter, we will write our game logic once, in JavascriptJavaScript, and then use it for both the game client and for the game server, which will run on node.js, built atop V8.

Designing the bumper cars game

Now let's make a game! The game will be simple in concept: each player controls a bumper car. He can move his car around and bounce off of other players' cars and off of the rigid walls. For simplicity of physics, we'll take the cars to be circular. We'll split up the program into a few parts with well-defined purposes.

  • game.js: This file will contain the game logic, i.e. the collision detection. This file will be used by both the client (the player in his web browser) and the server (in node.js) to run their game loops. game.js will have a routine RunGameFrame(Cars) whichthat will be called over and over again to progress the game's status. The argument, Cars, will be a collection of data about all of the cars' positions, velocities, etc.and so forth, so that they may be processed. game.js will also have a set of game options controlling things like the size of the game environment, strength of friction, etc.
  • client.js: This file will run a game loop and draw the game on an HTML canvas. We will first create a “local” game client (client-local.js) whichthat will operate entirely within the browser, and then upgrade it to the real game client (client-multiplayer.js) whichthat will connect to the WebSocket server and communicate with it (sending user input and receiving game state).
  • server.js: This file, to be executed under node.js, will run the “real” game loop, keeping a list of the players and their car data. server.js will establish a WebSocket server and communicate with the clients (receiving user input and sending game state).
  • bumper.htm: This file will be a bare-bones HTML page serving as the game client. It will refer to game.js, client.js, and have an HTML canvas.

The game logic

The logic in our game is all related to detecting and handling collisions. Because the focus of this chapter is netcode, the explanation of the physics involved will be cursory. Conceptually, it is simple: the bumper cars will collide elastically (conserving both momentum and energy) with each other and with the walls. We will take the cars to be circular (of fixed radius) to maximize symmetry. The collision detection presented here is very good, but not perfect. For our purposes, it more than suffices.

With a little bit of clairvoyance, we begin the game engine, game.js, with the list of game properties that we'll need (see Listing 9-1). These will, in the final product, apply simultaneously to the client and the server. There is certainly virtue in not having two such lists maintained separately.

Listing 9-1. Global Properties of the Game

var GP =
        {
                GameWidth: 700,               // In pixels.
                GameHeight: 400,              // In pixels.
                GameFrameTime: 20,            // In milliseconds.
                CarRadius: 25,                // In pixels.
                FrictionMultiplier: 0.97,     // Unitless
                MaxSpeed: 6,                  // In pixels per game frame.
                TurnSpeed: 0.1,               // In radians per game frame.
                Acceleration: 0.3             // In (pixels per game frame) per game frame.
        };

Now, in Listing 9-2, let's outline the code for the game loop method, RunGameFrame(Cars). The argument, Cars, will be a collection of the car data, namely their positions and velocities. Those two pieces of information, in some sense, are the car: they are all we need, internally, to keep track of. We'll store cars as simple JavascriptJavaScript objects: { X, Y, VX, VY }. We will add some properties later on, but these are the only essentials. The position will be in pixels and the velocity will be in pixels per “game frame.”. A game frame is the basic unit of timing that we use. It means, simply, one iteration of the game loop.

Tip In general, timing for logic in your game should be done by counting game frames and not by using built-in timing functions such as JavascriptJavaScript's setInterval. Those functions will be irregular depending on CPU load, browser throttling, etcand so forth. While that is not a bad thing in and of itself, you will have a hard time giving a consistent game experience for different users unless you do your timing by counting game frames.

In each game frame, we have to

  • update the cars' positions
  • check for collisions after they're in their new positions
  • react to those collisions

Listing 9-2. Outline of RunGameFrame

function RunGameFrame(Cars)
{
        // Move the cars and collect impulses due to collisions.

        // Apply impulses.

        // Enforce speed limit and apply friction.
}

This type of collision detection, where we move the cars regardless of environment and then check for collisions, has some drawbacks. The only serious one whichthat we will consider here is that two cars might pass each other in one time step with no collision registered. Say one car at x=0 is moving to the right at 100 pixels per game frame and the other car, at x=50, is sitting still. One game frame later, the first car has passed the second car but we didn't account for it! A simple way to correct many instances of this is to enforce a maximum speed, small in comparison to the diameter of the bumper cars, so the collision will be registered during some time step.

The result of every collision is to change the momentum of the involved cars (momentum is equivalent to velocity in this formulation — all cars will have unit mass). If a car incurs multiple collisions in one game frame, the collisions can be treated independently and the changes in momentum from each (called “impulses” in physics parlance) simply add up. Therefore, we will look at each car, enumerate its collisions in that game frame (i.e., is its center sufficiently close to any other car or to the walls?), and then compute and store the impulses from those collisions. Afterwards, we will add those impulses onto the car's velocity.

images

Figure 9-1. A glancing collision and the trajectories resulting from it

When car i and car j, with positions ri and rj and velocities vi and vj, collide elastically, they receive the impulses shown in Figure 9-2.

images

Figure 9-2. The impulses received by the colliding cars. All quantities are vectors and the dot is the vector dot product.

The first block of code in RunGameFrame will beis shown in Listing 9-3.

Listing 9-3. Collision Detection Implementation

// Move the cars and collect impulses due to collisions.
 
// Each impulse will be an array in the format
// [ Index of first car, Index of second car, X impulse, Y impulse ].
var Impulses = [];
for (var i = 0; i < Cars.length; i++)
{
     // Move the cars. X and Y are the coordinates of the center of the car.
     Cars[i].X += Cars[i].VX;
     Cars[i].Y += Cars[i].VY;

     // Check for proximity to the left and right walls.
     if (Cars[i].X <= GP.CarRadius || Cars[i].X >= GP.GameWidth - GP.CarRadius)
     {
          // If we are going towards the wall, then give an impulse. Note that, in the
          // game frame following a collision with the wall, the car may still be in close
          // proximity to the wall but will have velocity pointing away from it. We should
          // not treat that as a new collision. That is the reason for this code.
          if ((Cars[i].X <= GP.CarRadius && Cars[i].VX <= 0)
               || (Cars[i].X >= GP.GameWidth - GP.CarRadius && Cars[i].VX >= 0))
          {
               Impulses.push([i, null, 2 * Cars[i].VX, 0]); // Turn the car around.
          }
 
          // Make the walls truly rigid. If the car pushed into the wall, push it back out.
          if (Cars[i].X <= GP.CarRadius) Cars[i].X = GP.CarRadius;
          if (Cars[i].X >= GP.GameWidth - GP.CarRadius)
               Cars[i].X = GP.GameWidth ñ GP.CarRadius;
     }
 
     // Same as above, but now for the top and bottom walls.
     if (Cars[i].Y <= GP.CarRadius || Cars[i].Y >= GP.GameHeight - GP.CarRadius)
     {
          if ((Cars[i].Y <= GP.CarRadius && Cars[i].VY <= 0)
               || (Cars[i].Y >= GP.GameHeight - GP.CarRadius && Cars[i].VY >= 0))
          {
               Impulses.push([i, null, 0, 2 * Cars[i].VY]);
          }

          if (Cars[i].Y <= GP.CarRadius) Cars[i].Y = GP.CarRadius;
          if (Cars[i].Y >= GP.GameHeight - GP.CarRadius)
                  Cars[i].Y = GP.GameHeight ñ GP.CarRadius;
     }

     // Now that collisions with walls have been counted, check for collisions between
     // cars. Two cars have collided if their centers are within 2 * GP.CarRadius, i.e.
     // if they overlap at all.
     // Note the bounds of this for loop. We don't need to check all the cars.
     for (var j = i + 1; j < Cars.length; j++)
     {
          // Euclidean distance between the centers of the two cars.
          var DistSqr = (Cars[i].X - Cars[j].X) * (Cars[i].X - Cars[j].X)
                        + (Cars[i].Y - Cars[j].Y) * (Cars[i].Y ñ Cars[j].Y);
            
          if (Math.sqrt(DistSqr) <= 2 * GP.CarRadius)
          {
               // The impulses from a two dimensional elastic collision.
               // Delta = (r_j - r_i) . (v_i - v_j) / |r_j - r_i|^2.
               // Impulse 1 = -Delta * [ DX, DY ].
               // Impulse 2 = Delta * [ DX, DY ].
               var DX = Cars[j].X - Cars[i].X;
               var DY = Cars[j].Y - Cars[i].Y;

               var Delta = (DX * (Cars[i].VX - Cars[j].VX)
                                 + DY * (Cars[i].VY - Cars[j].VY)) / (DX * DX + DY * DY);

               // If they're proceeding away from the collision,
               // (r_j - r_i) . (v_i - v_j) <= 0,
               // then we already dealt with the collision. This is similar to the
               // consideration we made for collisions at the wall.
               if (Delta <= 0) continue;

               Impulses.push([i, j, Delta * DX, Delta * DY]);
          }
     }
}

This is the meat of the game's physics. Every collision, even with walls, gives rise to an impulse which that will change the cars' velocities. Now, applying the impulses is very straightforward:, as shown in Listing 9-4.

Listing 9-4. Continuation of RunGameFrame

// Apply impulses.
for (var i = 0; i < Impulses.length; i++)
{
     // Wall collisions specify null for one of the car indices, because there is no
     // second car involved. Therefore we are careful not to refer to an index which
     // doesn't belong to the Cars array.
     if (Impulses[i][0] in Cars)
     {
          Cars[Impulses[i][0]].VX -= Impulses[i][2];
          Cars[Impulses[i][0]].VY -= Impulses[i][3];
     }

     if (Impulses[i][1] in Cars)
     {
          Cars[Impulses[i][1]].VX += Impulses[i][2];
          Cars[Impulses[i][1]].VY += Impulses[i][3];
     }
}

Finally, we can't let things get too crazy, so we enforce the speed limit mentioned earlier. We also throw in friction for good measure, as shown in Listing 9-5.

Listing 9-5. Continuation of RunGameFrame

// Enforce speed limit and apply friction.
for (var i = 0; i < Cars.length; i++)
{
// Scale down the car's speed if it's breaking the speed limit.
var Speed = Math.sqrt(Cars[i].VX * Cars[i].VX + Cars[i].VY * Cars[i].VY);
if (Speed >= GP.MaxSpeed)
{
     Cars[i].VX *= GP.MaxSpeed / Speed;
     Cars[i].VY *= GP.MaxSpeed / Speed;
}

// Friction will act on the cars at all times, eventually bringing them to rest.
Cars[i].VX *= GP.FrictionMultiplier;
Cars[i].VY *= GP.FrictionMultiplier;
}

That's all for the RunGameFrame method. In fact, that is all of the game logic, and there wasn't very much! We complete game.js by adding the following lines: in Listing 9-6.

Listing 9-6. Code Required for node.js Compatibility.

if (typeof exports !== "undefined")
{
     exports.GP = GP;
     exports.RunGameFrame = RunGameFrame;
}

These lines will allow us to use game.js within node.js when we write our server in a later section.

The game client, Part 1

The game client—what shows up in the user's browser—is slightly more complicated than game.js. We need to

  • open a WebSocket connection
  • transmit user input to the server
  • receive game state from the server
  • draw the game

Before diving into the multiplayer stuff, let's just get the game client working on its own. First, make a simple HTML file, bumper.htm, to house the game, as shown in Listing 9-7.

Listing 9-7. bumper.htm

<!DOCTYPE html>
<html>

<head>
<title>Bumper Cars</title>
<script language="javascriptJavaScript" src="game.js"></script>
<script language="javascriptJavaScript" src="client-local.js"></script>
</head>

<body style="text-align: center">
<canvas id="BumperCanvas" style="border: 1px solid black">
Your browser does not support HTML canvas!
</canvas>
</body>

</html>

Note This HTML file will be changed to point to client-multiplayer.js instead of client-local.js when we revisit the game client in a later section.

Now, in client.js, we'll define a few variables specific to the game client, as shown in Listing 9-8.

Listing 9-8. Global Variables in client.js

var GraphicsContext;
var Cars = [];
var MyCar = null;
var KeysPressed = 0; // Bit 0: up. Bit 1: left. Bit 2: right.

GraphicsContext will refer to the HTML canvas's drawing context, Cars will contain all of the data for the cars whichthat we'll display to the user, and MyCar will be a reference to that element of Cars which that the player is in control of. The most interesting of these is KeyPressed, and we will discuss it in a moment.

We'll be displaying a little graphic, car.png, and we load it here (see Listing 9-9) so that the browser caches it.

Listing 9-9. Image Preloading Code

var CarImage = new Image();
CarImage.src = "car.png";

The player will control the car with his arrow keys. We'll make it so that left and right turn the car, while up makes the car accelerate. In that case, it's necessary to store an orientation for the car, augmenting the short list of car properties from the previous section. This will be an angle which that the car makes with some arbitrary axis. Pressing left or right increases or decreases the orientation.

In order to process multiple keys at once, we have to think a bit outside the box—simply responding to keydown events is not sufficient here. What we need to know during a given game frame is whether those three keys are pressed down or not. The KeysPressed variable will serve that purpose. We'll use its bits as flags, setting bit #0 if the up key is pressed down, bit #1 if the left key is pressed down, and bit #2 if the right key is pressed down. If this is unfamiliar to you, imagine we have 3 three light switches, each going on when a certain key is pressed down and going off when that key is released. The KeysPressed variable keeps track of which lights are on. The event handlers to achieve this functionality will be:are as shown in Listing 9-10.

Listing 9-10. Handling Keyboard Input

document.addEventListener("keydown",
     function(E)
     {
          if (E.which == 38 && (KeysPressed & 1) == 0) KeysPressed |= 1; // Up.
          else if (E.which == 37 && (KeysPressed & 2) == 0) KeysPressed |= 2; // Left.
          else if (E.which == 39 && (KeysPressed & 4) == 0) KeysPressed |= 4; // Right.
     }
     );

document.addEventListener("keyup",
     function(E)
     {
          if (E.which == 38) KeysPressed &= ~1; // Up.
          else if (E.which == 37) KeysPressed &= ~2; // Left.
          else if (E.which == 39) KeysPressed &= ~4; // Right.
     }
     );

The & and | are the “bitwise and” and “bitwise or” operators, respectively. Once the page loads, we get our drawing context from the canvas and then proceed into the game loop, as shown in Listing 9-11.

Listing 9-11. Starting the Game Loop on Window Load

window.addEventListener("load",
     function()
     {
          var BumperCanvas = document.getElementById("BumperCanvas");
          BumperCanvas.width = GP.GameWidth;
          BumperCanvas.height = GP.GameHeight;
          GraphicsContext = BumperCanvas.getContext("2d");

          // Set up game loop.
          setInterval(
               function()
               {
                    if (MyCar)
                    {
                         if (KeysPressed & 2) MyCar.OR -= GP.TurnSpeed; // Turn left.
                         if (KeysPressed & 4) MyCar.OR += GP.TurnSpeed; // Turn right.
                         if (KeysPressed & 1) // Accelerate.
                         {
                              MyCar.VX += GP.Acceleration * Math.sin(MyCar.OR);
                              MyCar.VY -= GP.Acceleration * Math.cos(MyCar.OR);
                         }
                    }

                    RunGameFrame(Cars);
                    DrawGame();
               },
               GP.GameFrameTime);
     }
     );.

What you see above in Listing 9-11 is the entire game loop. Every 20 milliseconds (this value is stored in GP.GameFrameTime), we process the input captured as shown in Listing 9-11above (the OR property of a car is its orientation), run a game frame (that method belongs to game.js), and then draw the game. When the up key is pressed, the car accelerates along the car's current orientation. The factors of sine and cosine can be understood with a little bit of geometry, taking into account the wonky HTML canvas coordinates where the positive y direction is down and angles are measured clockwise.

Now, the only thing missing from this non-multiplayer client is to draw the game. This Listing 9-12 is a straightforward exercise in using the canvas.

Listing 9-12. DrawGame method

function DrawGame()
{
     // Clear the screen
     GraphicsContext.clearRect(0, 0, GP.GameWidth, GP.GameHeight);

     for (var i = 0; i < Cars.length; i++)
     {
          GraphicsContext.save();
          GraphicsContext.translate(Cars[i].X | 0, Cars[i].Y | 0);
          GraphicsContext.rotate(Cars[i].OR);
          GraphicsContext.drawImage(CarImage, -CarImage.width / 2 | 0,images
 -CarImage.height / 2 | 0);
          GraphicsContext.restore();
     }
}

There are a few interesting points to be raised here:

  • The x | 0 trick takes the integer part of the number x (and it's much faster than Math.round, etc.).
  • In current implementations of canvas, content may be drawn either aliased or anti-aliased depending on whether the command specifies integer or fractional pixel values. It is a good idea to only draw at integer values so that the moving car's appearance doesn't change as it moves through non-integer coordinates.
  • Saving and restoring canvas state is very useful, and a good habit to have.

The client is done! Load it up, and it does... nothing. Well, there's there are no cars. So, let's let's put inadd some cars to test it out.

Listing 9-13. Initializing the Local Client with Some Cars

Cars.push({ X: 200, Y: 200, VX: 0, VY: 0, OR: 0 });
Cars.push({ X: 100, Y: 100, VX: 5, VY: 0, OR: 0 });
Cars.push({ X: 300, Y: 300, VX: -1, VY: -1, OR: Math.PI });
MyCar = Cars[0];

This code can go anywhere that's not in a function, so put it at the end of the file. Next we'll implement the server using node.js and then revisit the game client in order to bring it online.

images

Figure 9-3. The offline game client created in this section

The game server

To set up the game server, you will need access to some computer running node.js (www.nodejs.org). This can be your own computer (there is a Windows port of node.js) or some remote server. We are going to run a WebSocket server on node.js. A WebSocket server can be written in a number of different languages, but using node.js is the most convenient for us because we can share our JavascriptJavaScript game logic, with no modifications whatsoever, between the client and the server.

At the time of this writing, there are a few node.js addonadd-on modules for creating WebSocket servers, but the one we will use is called WebSocket-Node (https://github.com/Worlize/WebSocket-Node). First install npm, the node.js package manager, and then issue the command npm install websocket to get the module just mentioned. More explicit instructions for getting node.js up and running are found in this chapter's appendix.

Note The WebSocket server addonadd-on modules are sure to change in the future, especially once the WebSocket standard is finalized, but the code we write here will be largely agnostic of the module used. It should be easily adapted to any other module fulfilling the same purpose.

Our server.js will need to

  • establish a WebSocket server
  • run a game loop, maintaining the authoritative state of all the players' cars
  • receive and act on player input
  • periodically send out game state to all the players

We'll begin by importing the modules that we need (similar to using #include in C), as shown in Listing 9-14.

Listing 9-14. Importing node.js Modules

var System = require("util");
var HTTP = require("http");
var WebSocketServer = require("/path/to/websocket").server;
var Game = require("/path/to/game");

The sys and http modules come with node.js, but the websocket module is what we downloaded a moment ago, and the game module is actually just our game.js. You can either put these files (the websocket module and game.js) where node.js knows to look for them or you can specify the full paths explicitly (if game.js is located at /home/user123/bumper/game.js, you will do require(“/home/user123/bumper/game”)). The exports code we added at the end of game.js is important here: only those variables exported by game.js will be accessible to server.js. Now, as shown in Listing 9-15, some global variables that we'll use throughout server.js.

Listing 9-15. Global Variables for the Server

var Frame = 0;
var FramesPerGameStateTransmission = 3;
var MaxConnections = 10;
var Connections = {};

Frame will count game frames, though we won't do any intricate timing in this example game. FramesPerGameStateTransmission will control how often game state is broadcasted to players; in this case it's after every three game frames. In game.js, we specified GameFrameTime to be 20 milliseconds, so updates will be sent out about every 60 ms, or approximately 17 times per second. MaxConnections will limit the number of connections we allow on our server. Connections will contain data about the players, including their car state but also stuff about the WebSocket connection: IP address, etcand so forth.

At this point we can immediately accomplish our first goal. It is very simple to set up a WebSocket server in the environment we've chosen (see Listing 9-16). A WebSocket server piggybacks on a regular old HTTP server, and node.js can create an HTTP server for us in one line.

Listing 9-16. Setting Up the WebSocket Server

// Creates an HTTP server that will respond with a simple blank page when accessed.
var HTTPServer = HTTP.createServer(
               function(Request, Response)
               {
                    Response.writeHead(200, { "Content-Type": "text/plain" });
                    Response.end();
               }
               );

// Starts the HTTP server on port 9001.
HTTPServer.listen(9001, function() { System.log("Listening for connections on port 9001"); });

// Creates a WebSocketServer using the HTTP server just created.
var Server = new WebSocketServer(
               {
                    httpServer: HTTPServer,
                    closeTimeout: 2000
               }
               );

Tip Generally, ports 0–1023 are reserved for system processes, but you are free to choose any higher numbered port for your WebSocket server. In this case, we arbitrarily choose 9001. Make sure that the port you choose is not being blocked by a firewall.

It's really that simple (on our end—of course there is some heavy stuff going on behind the scenes). If we were to run this code, we would have a working WebSocket server at this point. It wouldn't do anything, but it would be there. The next thing we do is subscribe to the WebSocket server's request event, which is raised when somebody connects to the server. When that happens, we want to add that client to the Connections object and then throw them into the fray with a bumper car. Actually, we'll put off giving the new player a car until he sends a handshake message with an in-game handle, but more on that in a minute.

Listing 9-17. Reacting to Clients Connecting to WebSocket Server

// When a client connects...
Server.on("request",
     function(Request)
     {
          if (ObjectSize(Connections) >= MaxConnections)
          {
               Request.reject();
               return;
          }

          var Connection = Request.accept(null, Request.origin);
          Connection.IP = Request.remoteAddress;

          // Assign a random ID that hasn't already been taken.
          do
               {
                    Connection.ID = Math.floor(Math.random() * 100000)
               } while (Connection.ID in Connections);

          Connections[Connection.ID] = Connection;

          Connection.on("message",
               function(Message)
               {
                    // All of our messages will be transmitted as unicode text.
                    if (Message.type == "utf8")
                         HandleClientMessage(Connection.ID, Message.utf8Data);
               }
               );

          Connection.on("close",
               function()
               {
                    HandleClientClosure(Connection.ID);
               }
               );

          System.log("Logged in " + Connection.IP + "; currently " +
                     ObjectSize(Connections) + " users.");
     }
     );

Note that the Server object (our WebSocket server) registers event listeners using on(), in the same way that addEventListener is used for DOM objects in the browser. The method is relatively straightforward. In summary, we do the following:

  • Check that we're not running afoul of the connection limit (this connection limit is not needed, but it is here to illustrate rejecting a connection).
  • Call accept (as opposed to reject) on the Request object.
  • Store the client's IP address and assign a unique ID number to the client for internal use.
  • Attach event listeners to this player's Connection object (close is raised when the connection is closed and message is raised whenever that player sends us data).

You may have noticed ObjectSize being applied to Connections. We can't get the size of Connections using Connections.length because it's an object rather than an array. So, we implement this simple counting routine ourselves, as shown in Listing 9-18.

Listing 9-18. Generic Method to Count Size of JavascriptJavaScript Objects

function ObjectSize(Obj)
{
     var Size = 0;
     for (var Key in Obj)
          if (Obj.hasOwnProperty(Key))
               Size++;

     return Size;
}

The event handler for the player disconnecting is very simple. Notice above that in Listing 9-18 we directed that event to a function called HandleClientClosure: (see Listing 9-19).

Listing 9-19. HandleClientClosure method

function HandleClientClosure(ID)
{
     if (ID in Connections)
     {
          System.log("Disconnect from " + Connections[ID].IP);
          delete Connections[ID];
     }
}

Likewise, when we receive data from a player, we process it in HandleClientMessage. Let's outline the behavior of that function here:in Listing 9-20.

Listing 9-20. HandleClientMessage outline

function HandleClientMessage(ID, Message)
{
     // Check that we know this client ID and that the message is in a format we expect.

     // Handle the different types of messages we expect:
     //  - Handshake message where the player tells us his name
     //     --> Create his car object and send everybody the good news.
     //  - Key was pressed.
     //     --> Update the player's personal KeyPressed bitfield.
     //  - Key was released.
     //     --> Ditto.
}

It is completely up to us to choose a message format. Using JavascriptJavaScript, a natural choice is JSON whichthat is nothing more than a standardized way of encoding JavascriptJavaScript variable data. We could easily make our messages shorter or more obscure, but there is no benefit to that in the present case. JSON support is ubiquitous, with all modern browsers (and node.js) supporting the routines JSON.stringify() for encoding and JSON.parse() for decoding. Therefore, we will choose our server-bound messages to be JSON-encoded JavascriptJavaScript objects in the format { Type: ..., Data: ... }. The three types of messages will be "HI" for handshake, "U" for key up, and "D" for key down. Now, in Listing 9-21, let's fill out HandleClientMessage.

Listing 9-21. HandleClientMessage Method

function HandleClientMessage(ID, Message)
{
     // Check that we know this client ID and that the message is in a format we expect.
     if (!(ID in Connections)) return;

     try { Message = JSON.parse(Message); }
     catch (Err) { return; }
     if (!("Type" in Message && "Data" in Message)) return;

     // Handle the different types of messages we expect.
     var C = Connections[ID];
     switch (Message.Type)
     {
          // Handshake.
          case "HI":
               // If this player already has a car, abort.
               if (C.Car) break;

               // Create the player's car with random initial position.
               C.Car =
                   {
                    X: Game.GP.CarRadius + Math.random() * (Game.GP.GameWidthimages
 - 2 * Game.GP.CarRadius),
                    Y: Game.GP.CarRadius + Math.random() * (Game.GP.GameHeightimages
 - 2 * Game.GP.CarRadius),
                    VX: 0,
                    VY: 0,
                    OR: 0,
                    // Put a reasonable length restriction on usernames.
                    // Usernames will be displayed to all players.
                    Name: Message.Data.toString().substring(0, 10)
                   };

               // Initialize the input bitfield.
               C.KeysPressed = 0;
               System.log(C.Car.Name + " spawned a car!");

               SendGameState();
               break;

          // Key up.
          case "U":
               if (typeof C.KeysPressed === "undefined") break;

               if (Message.Data == 37) C.KeysPressed &= ~2; // Left
               else if (Message.Data == 39) C.KeysPressed &= ~4; // Right
               else if (Message.Data == 38) C.KeysPressed &= ~1; // Up
               break;

          // Key down.
          case "D":
               if (typeof C.KeysPressed === "undefined") break;

               if (Message.Data == 37) C.KeysPressed |= 2; // Left
               else if (Message.Data == 39) C.KeysPressed |= 4; // Right
               else if (Message.Data == 38) C.KeysPressed |= 1; // Up
               break;
     }
}

Notice that we again augmented, for the final time, the list of data stored per car. We're now keeping track of the user's handle (username) and it will be displayed on the cars once we revisit the game client.

Caution Always perform strict data validation on any input received from users! It is very simple for a user to change the game client and send messages out of order or send messages that the server does not understand. If you are not careful, these can crash your server or worse.

Of course, the method SendGameState, called after the handshake, is all-important. It will broadcast all of the game data to all of the players several times per second. In a more complicated game, especially one with many users, this will be something that you want to optimize for speed and bandwidth efficiency. In our case, these aren't important concerns. We proceed pretty naively, then: in Listing 9-22.

Listing 9-22. SendGameState Method

function SendGameState()
{
     var CarData = [];
     var Indices = {};

     // Collect all the car objects to be sent out to the clients
     for (var ID in Connections)
     {
          // Some users may not have Car objects yet (if they haven't done the handshake)
          var C = Connections[ID];
          if (!C.Car) continue;

          CarData.push(C.Car);

          // Each user will be sent the same list of car objects, but needs to be able to pick
          // out his car from the pack. Here we take note of the index that belongs to him.
          Indices[ID] = CarData.length - 1;
     }

     // Go through all of the connections and send them personalized messages. Each user gets
     // the list of all the cars, but also the index of his car in that list.
     for (var ID in Connections)
          Connections[ID].sendUTF(JSON.stringify({ MyIndex: Indices[ID], Cars: CarData }));
}

The message we send out is a JSON-encoded string made from the object { MyIndex: ..., Cars: ... }. Each user will get a personalized message like this, with all the car information (including his own car), as well as the index of his car in the group. The way we send data is very simple: each element of Connections has the method sendUTF() (part of WebSocket-Node's type, WebSocketConnection) which that accepts a string as input.

The only remaining piece of server.js is the game loop (see Listing 9-23). We already constructed a game loop in client.js, and this one will be very similar.

Listing 9-23. Setting up the Game Loop on the Server

// Set up game loop.
setInterval(function()
     {
          // Make a copy of the car data suitable for RunGameFrame.
          var Cars = [];
          for (var ID in Connections)
          {
               var C = Connections[ID];
               if (!C.Car) continue;

               Cars.push(C.Car);

               if (C.KeysPressed & 2) C.Car.OR -= Game.GP.TurnSpeed;
               if (C.KeysPressed & 4) C.Car.OR += Game.GP.TurnSpeed;
               if (C.KeysPressed & 1)
               {
                    C.Car.VX += Game.GP.Acceleration * Math.sin(C.Car.OR);
                    C.Car.VY -= Game.GP.Acceleration * Math.cos(C.Car.OR);
               }
          }

          Game.RunGameFrame(Cars);

          // Increment the game frame, which is only used to time the SendGameState calls.
          Frame = (Frame + 1) % FramesPerGameStateTransmission;
          if (Frame == 0) SendGameState();
     },
     Game.GP.GameFrameTime
     );

The variables exported from game.js are referenced as Game.RunGameFrame and Game.GP, where Game was defined all the way at the top of server.js when we called require().

Now all of server.js is put together, but our game client is stuck in the stone Stone ageAge. Time to bring it online!

The game client, Part 2

The game client is going to assume a subordinate role now that we have an authoritative server. Every time the server calls SendGameState(), the game client will receive the list of car data and overwrite whatever it had previously. We still have a game loop and still respond to keyboard input, but only to make the game experience smoother as we discussed in an earlier section. The main client changes, therefore, are to establish a WebSocket connection and then handle messages from the server, and to pass along keyboard data to the server.

As shown in Listing 9-24, wWe'll keep two global variables more than we had in the old game client:.

Listing 9-24. New Global Variables for the Client

var Socket = null;
var GameTimer = null;

The first is our WebSocket and the second will be used to stop the game loop if the WebSocket connection is aborted. When the page loads, we'll ask the player for a handle and then connect to the WebSocket server. So, in our window-load event handler, we add the following code: as shown in Listing 9-25.

Listing 9-25. Modification of Window Load Event Handler in the Client

...
GraphicsContext = BumperCanvas.getContext("2d");

var Name = prompt("What is your username?", "Anonymous");
GraphicsContext.textAlign = "center";
GraphicsContext.fillText("Connecting...", GP.GameWidth / 2, GP.GameHeight / 2);

try
{
     if (typeof MozWebSocket !== "undefined")
          Socket = new MozWebSocket("ws://SERVERIP:9001");
     else if (typeof WebSocket !== "undefined")
          Socket = new WebSocket("ws://SERVERIP:9001");
     else
     {
          Socket = null;
          alert("Your browser does not support websockets. We recommend that you useimages
 an up-to-date version of Google Chrome or Mozilla Firefox.");
          return false;
     }
}
catch (E) { Socket = null; return false; }

At the time of this writing, Firefox calls its WebSocket type MozWebSocket. That may change in the future. For the time being, the code above in Listing 9-25 is compatible with both Firefox and Chrome. Of course, SERVERIP should be replaced by the IP address of the server running server.js, be it 127.0.0.1 or the IP address of a remote server.

Next, in Listing 9-26, we listen for events raised by the WebSocket, and then we will be done with the window-load event handler.

Listing 9-26. WebSocket Event Handlers

Socket.onerror = function(E) { alert("WebSocket error: " + JSON.stringify(E)); };

Socket.onclose = function (E)
     {
          // Shut down the game loop.
          if (GameTimer) clearInterval(GameTimer);
          GameTimer = null;
     };

Socket.onopen = function()
     {
          // Send a handshake message.

          // Set up game loop.
     };

Socket.onmessage = function(E)
     {
          // Parse the car data from the server.
     };

The latter two event handlers require some more detail. When the WebSocket connection succeeds in opening, the open event is raised, meaning that it is ready to send and receive data. At that point, we want to send a handshake message to the server telling it the player's name. Then, we'll begin a game loop.

Listing 9-27. WebSocket “open” Event Handler

Socket.onopen = function()
     {
          // Send a handshake message.
          Socket.send(JSON.stringify({ Type: "HI", Data: Name.substring(0, 10) }));

          // Set up game loop.
          GameTimer = setInterval(
               function()
               {
                    // Supposing MyCar is not null, which it shouldn't be if we're
                    // participating in the game and communicating with the server.
                    if (MyCar)
                    {
                         // Turn and accelerate the car locally, while we wait for the
                         // server to respond to the key presses we transmit to it.
                         if (KeysPressed & 2) MyCar.OR -= GP.TurnSpeed;
                         if (KeysPressed & 4) MyCar.OR += GP.TurnSpeed;
                         if (KeysPressed & 1)
                         {
                              MyCar.VX += GP.Acceleration * Math.sin(MyCar.OR);
                              MyCar.VY -= GP.Acceleration * Math.cos(MyCar.OR);
                         }
                    }

                    RunGameFrame(Cars);
                    DrawGame();
               },
               GP.GameFrameTime);
     };

There is not much new in this game loop besides the fact, which must be stressed again, that the game loop is only being run for a smooth user experience. The actual game logic is being computed on the server and sent to us. Now we receive and handle the server's game state messages:, as shown in Listing 9-28.

Listing 9-28. WebSocket “message” Event Handler

Socket.onmessage = function(E)
     {
          var Message;

          // Check that the message is in the format we expect.
          try { Message = JSON.parse(E.data); }
          catch (Err) { return; }
          if (!("MyIndex" in Message && "Cars" in Message)) return;

          // Overwrite our old Cars array with the new data sent from the server.
          Cars = Message.Cars;
          if (Message.MyIndex in Cars) MyCar = Cars[Message.MyIndex];
     };

Keyboard input from the user should now send a message to the server. It's not necessary to send a message every time a keydown event is fired—if the key is held down there will be a ton of those events. So, we implement this in the following way:as shown in Listing 9-29.

Listing 9-29. New Multiplayer-Enabled Keyboard Handlers

document.addEventListener("keydown",
     function(E)
     {

          var Transmit = true;
          if (E.which == 38 && (KeysPressed & 1) == 0) KeysPressed |= 1; // Up.
          else if (E.which == 37 && (KeysPressed & 2) == 0) KeysPressed |= 2; // Left.
          else if (E.which == 39 && (KeysPressed & 4) == 0) KeysPressed |= 4; // Right.
          else Transmit = false;

          // Only send to the server if the key is one of the three we care about, and only
          // if this key press wasn't already reflected in the KeyPressed bitfield.
          if (Transmit && Socket && Socket.readyState == 1)
               Socket.send(JSON.stringify({ Type: "D", Data: E.which }));
     }
     );

document.addEventListener("keyup",
     function(E)
     {

          var Transmit = true;
          if (E.which == 38) KeysPressed &= ~1; // Up.
          else if (E.which == 37) KeysPressed &= ~2; // Left.
          else if (E.which == 39) KeysPressed &= ~4; // Right.
          else Transmit = false;

          // For "keyup", we just have to check that it's one of the keys we care about.
          if (Transmit && Socket && Socket.readyState == 1)
               Socket.send(JSON.stringify({ Type: "U", Data: E.which }));
     }
     );

Sending the data is as simple as calling the send() method on the WebSocket object. The WebSocket's readyState property has the value 1 when it is open and functioning correctly. With those changes, we're totally online-enabled! One last tweak to make is to draw player names above their cars. The DrawGame method becomes:is shown in Listing 9-30.

Listing 9-30. Modification of DrawGame Method in the Client

function DrawGame()
{
     // Clear the screen
     GraphicsContext.clearRect(0, 0, GP.GameWidth, GP.GameHeight);

     GraphicsContext.save();
     GraphicsContext.font = "12pt Arial";
     GraphicsContext.fillStyle = "black";
     GraphicsContext.textAlign = "center";
     for (var i = 0; i < Cars.length; i++)
     {
          GraphicsContext.save();
          GraphicsContext.translate(Cars[i].X | 0, Cars[i].Y | 0);
          GraphicsContext.rotate(Cars[i].OR);
          GraphicsContext.drawImage(CarImage, -CarImage.width / 2 | 0,images
 -CarImage.height / 2 | 0);
          GraphicsContext.restore();

          if (Cars[i].Name)
          {
               GraphicsContext.fillText(
                    (Cars[i] == MyCar ? "Me" : Cars[i].Name.substring(0, 10)),
                    Cars[i].X | 0,
                    (Cars[i].Y - GP.CarRadius - 12) | 0
                    );
          }
     }
     GraphicsContext.restore();
}

And we are ready to play. Invite all your friends!

images

Figure 9-4. The final game client, with three players online

Conclusion

WebSockets and the HTML5 canvas open up all sorts of possibilities to developers. These technologies are still brand new, but we should expect to see them really take off in the years ahead. In this chapter, we've created a prototype game and hopefully you will agree that it wasn't exceedingly difficult. Nobody can argue the fact that this was a walk in the park compared to making the same bumper car demo as a standalone application in a traditional language like C++.

You may point out that the ease of development owes to offloading all of the heavy lifting to the web browser. This is certainly true, but in the context of many games and applications, the benefits outweigh the downsides. The performance hit you take, relying on a browser rather than optimizing your own standalone application, is getting smaller every day (not to say that it will ever be non-existent). Web browsers are continually improving thanks to intense competition for market share.

On the other hand, you get portability and high-level access to sockets, graphics, sound, user input, etcand so forth,. all with comparatively minimal effort on your part. Clearly, this is an option worth considering for development of any small or mid-sized games. I hope to see yours online soon!

Appendix: Setting up node.js

Let's walk through getting node.js up and running. We'll do it first on Windows and then make some comments about doing it on a UNIX-based system.

Windows

Download the latest Windows version of node.js from www.nodejs.org/. As of this writing, the version is 0.6.2. The installer unpacks node.exe (the thing whichthat will run our javascriptJavaScript code) and appends the appropriate directory (for me, C:Program Files odejs) to the environment path variable. You should find that directory (if you are having trouble, search your hard drive for node.exe) and make a note of it.

On Windows, there is no Node Package Manager (npm) utility to automate installation of node.js libraries, so we'll have to install the websocket library manually. To do that, visit the project's web page, https://github.com/Worlize/WebSocket-Node, and find the option to download the repository as a zip file. Extract that zip file to the same directory as node.exe. Alongside node.exe, there should now be a directory with a name like Worlize-WebSocket-Node-0d04b73 (the string you find at the end may be different). Rename that directory to “websocket”. That's it! The library is installed.

Now, you should place all of the source files associated with the bumper car game in a directory of your choosing. I put mine in c:umper, so we'll use that for what follows. We need to update server.js so that it knows where to find our libraries. Open up server.js and find the lines beginning “var WebSocketServer = …” and “var Game = …”. Replace those with:

var WebSocketServer = require("c:/program files/nodejs/websocket").server;
var Game = require("c:/bumper/game");

where the directories correspond to those on your system. Notice that the slashes are backwards from normal Windows paths.

Next, in client-multiplayer.js, find the two instances of “ws://SERVERIP:9001”. If your port 9001 is open to external traffic, then you can replace SERVERIP by your IP address. If not, and you just want to try it out locally, then replace SERVERIP by localhost (or, equivalently, 127.0.0.1). Go to the command line and execute

node c:umperserver.js

If you get a complaint that it can't find node, then navigate to the directory containing node.exe and try executing it again. Finally, update bumper.htm to make sure that it refers to client-multiplayer.js rather than client-local.js, and then load up bumper.htm in a web browser (or two)!

UNIX

To install node.js on a UNIX-based operating system, you should first check if there is a pre-compiled package available for you, following the instructions herelocated at: https://github.com/joyent/node/wiki/Installing-Node.js-via-package-manager. If not, you will have to build node.js from its source code. There are comprehensive instructions for doing that available at https://github.com/joyent/node/wiki/Installation.

After installing node.js, you will want to install the Node Package Manager (npm), by executing the following command: curl http://npmjs.org/install.sh | sh and then install the websocket module by running npm:

npm install websocket

If you have issues installing or using npm, you can perform a manual installation instead, as described in the Windows walkthrough above. At this point, node.js should be all set up and it remains just to direct server.js, client-multiplayer.js, and bumper.htm to the correct places.

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

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