CHAPTER 12

image

Real-Time Multiplayer Network Programming

Jason Gauci, Research Scientist, Apple

Many of the most popular games have a multiplayer component. All but one of the top ten most popular games on Steam, a digital distribution and communications platform for PCs, are either designed for a multiplayer experience or contain support for multiplayer (see Table 12-1). As you will discover in this chapter, adding multiplayer support to a game increases the range of experiences that a player can have by introducing the elements of human psychology and social interaction to the game agents. However, adding real-time multiplayer can be rather tricky to implement correctly. Although it may seem daunting, by following some principled methods, you can add a new, exciting dimension to your game.

Table 12-1. Most Popular Games by Player Count (at time of writing)

Game Name

Multiplayer

Dota 2

Yes

Team Fortress 2

Yes

Terraria

Yes

Civilization V

Yes

Counter-Strike: Global Offensive

Yes

Garry’s Mod

Yes

Path of Exile

Yes

The Elder Scrolls V: Skyrim

No

Batman: Arkham Origins

Yes

Total War: Rome II

Yes

This chapter will begin by explaining real-time multiplayer network programming and then discuss what makes this area of software engineering so challenging. The chapter will cover two powerful techniques for implementing real-time multiplayer network programming and offer tips and tricks to make the process as simple as possible. The chapter concludes with a case study, including links to source code and documentation.

Introduction

Clearly, the Internet and HTML5 were designed with multiple clients in mind. According to a survey by Netcraft, as of March 2012 there were more than 600 million active web sites,1 most designed to serve multiple agents simultaneously. The unique challenge with real-time multiplayer network programming is at the intersection of the term’s component parts: “real-time,” “multiplayer,” “network programming.” To illustrate, note how removing one of these terms reduces the complexity substantially.

“Real-time” suggests that the game requires simultaneous, precise coordination among players (either for cooperative or competitive goals). Players must have an accurate, up-to-date description of the shared game state at all times. Providing this description quickly is challenging because of latency (for more information, see the section “Latency”). For games designed to be played gradually (e.g., chess), or games in which only one person is actively modifying the game state at any given time (e.g., poker), latency is not an issue. Implementing these games is actually no different from creating a modern web site. (For more information on building a web platform that can push data from the server to clients when latency is not an issue, check out ShareJS or DerbyJS, which itself is built on ShareJS).

Multiplayer games contain several human agents, who work together or against one another in the same game instance. Note that simply having a global leaderboard in an otherwise single-player game does not make the game multiplayer, because one player’s high score does not affect the current game of another player. Also note that most social network games, such as FarmVille, are not considered multiplayer in this context, because each player is operating within his or her own sandbox, and sharing content between these sandboxes is limited and not real time. For these and other single-player games, the clients and server can exchange data at their leisure, without time-sensitive synchronization (for more information, see the section “Synchronization”).

Network programming in this context involves a constant stream of communication from each single client to all other clients. This is in contrast to many web applications, in which most of the data flow from the server to each client, in sparse intervals. If several people are taking turns on one keyboard or playing at the same computer with different joysticks, the effects of all player actions can be known immediately, and this local multiplayer does not constitute a networked game.

Although this area of game programming is rather specific and presents a unique set of challenges, it also affords an unparalleled level of engagement for your players. The next sections discuss the core challenges of real-time multiplayer network programming.

Challenges

The three main challenges in real-time multiplayer network programming are bandwidth, latency, and synchronization. The following sections cover each of these in detail.

Bandwidth

One of the challenges in real-time multiplayer network programming is bandwidth. Unlike many web applications, networked games require a significant stream of data from each client to every other client. This means that the amount of data each client must transmit to the others increases linearly with the number of players. To illustrate, suppose a player action consumes 256B of data, and a player can make 60 actions a second. In a 2-player game, approximately 15KB will be received per player per second (256 × 60). However, in a 100-player game, approximately 150KB of data per second will be received by each client, which is a significant amount of data for most home Internet connections. This is why massively multiplayer online (MMO) games typically have shards (clones of the game that are isolated from one another) and instances (areas of the game in which a small subset of the players are isolated from the rest of the shard). By isolating smaller groups of players, the actions of these players do not need to be shared with the rest of the world in real time. Also, much of the data do not need to be shared at all. For example, the real-time location of your character in an instance does not need to be known by other players of your guild who aren’t in the instance.

At the dawn of real-time multiplayer network programming, network engineers established peer-to-peer connections among players, whereby all players (peers) are connected to each other and can send their actions to each other directly. Although this reduces the overall network traffic, connecting all peers is problematic, owing to one-way firewalls and routers, which have become commonplace among today’s Internet users. As a result, there will be a subset of peers between whom a connection cannot be made, and the server will have to route messages for these peers, regardless. Thus, most modern games use a client-server model, whereby all traffic passes through a central server. This server is typically a dedicated machine with low packet latency and high bandwidth. Because data are flowing through the server, clients need to send their actions to only one destination, dramatically reducing upload bandwidth, but at the expense of increased latency (see Figure 12-1).

9781430266976_Fig12-01.jpg

Figure 12-1. Graphical model of peer-to-peer and client-server network topologies (courtesy of Wikimedia)

Latency

Latency is the measure of time delay experienced by a system. Typically, two types of latency concern network programmers.

The first is input latency, the time between when the user requests an action (e.g., by pressing a button) and when that action appears to take place. In games such as WarCraft III, an early real-time strategy (RTS) game designed when many of the players were on high-latency Internet connections, the game plays an acknowledgment sound immediately after the user takes an action. This audio cue indicates to the user that his or her action has been received, even though the input has not taken effect. This trick and other visual and audio illusions can simulate lower levels of input latency, without making changes to the game engine.

The second form of latency is state latency (also called simply latency), which measures the time between when a local action is taken and when that action is received by all the remote clients. This is the true measure of latency in a system, and there are few ways to reduce it. However, it is possible to hide state latency through client-side prediction (for more information, see the section “Client-Side Prediction”).

Synchronization

Synchronization is the most challenging problem facing network programmers. When a new client joins a game in progress, or a new game begins, the server must perform an initial sync, whereby the complete state of the game, including any custom assets, or server-specific settings, are sent to the new client. After the initial sync, the server can strictly route client actions and assume that all clients executing the same actions will maintain exactly the same game state. This is known as the lockstep method. The server can also continue to send the complete game state at regular intervals, along with all client actions. This is known as the state broadcast method. When two clients are playing on the same server but contain different game states because of a problem in the network code, they are said to be out of sync. Two out-of-sync clients may each perceive that he or she is winning the game, when in fact the client’s opponent is moving, based on his or her own divergent game state. When two clients are experiencing a different game because of a permanent out-of-sync condition, this is called a desync and results in frustrated players and a bad player experience.

These sections have covered several important problems in network programming. The following section discusses two very different approaches that deal with these problems in different ways.

State Broadcast vs. Lockstep

The most intuitive approach to network programming is the Lockstep method, whereby the server performs the initial sync and then broadcasts only player actions, with the expectation that all players will simulate exactly the same game. Although the lockstep method is the most bandwidth-efficient approach, it suffers from many issues. First, it requires that each client processes the same actions at the same time. This means that any packet loss or out-of-order packets are unacceptable. Second, the game engine must be entirely deterministic, with no randomness (but note that it is possible to use pseudorandom numbers, so long as all clients share the same seed, or the random numbers are presented in advance from the server). Third, the use of floating point in these systems is problematic, as floating-point computations vary slightly from one machine to another, and these differences can accumulate until the clients are out of sync. Note that the use of floating-point numbers outside the game engine is fine; so long as they have no effect on the game, they can be used in displaying graphics, audio processing, and so on with no problem.

Another approach is the state broadcast method, iwhereby the server broadcasts the complete game state to all clients, and each client replaces his or her own game state with the server copy periodically. Although this approach ensures that the server automatically resolves any synchronization issues, it also dramatically increases the amount of bandwidth that the server must use. For example, if the game state is 1KB, and there are 32 players in the game, the server would have to upload 320KB per second to send the ten updates per second necessary to create a smooth experience. For games such as Minecraft, which has a game state on the order of megabytes, this approach is intractable. Note that, even when applying the state broadcast method, clients still need to share actions so that they can fast-forward (for more information, see the section “Fast-Forwarding the Game State”).

Dealing with Latency

Regardless of which method is used, the problem of latency has to be addressed. Note that a single player’s latency is the amount of time it takes for data to travel from his or her computer to the server. For a client-server architecture, the state latency is the time between when a player requests an action on his or her machine and when that action reaches the player with the most latent connection. When a player takes an action, the server delays the action by the state latency. The expectation is that the client’s action reaches all other players in time for everyone to execute the action at exactly the same time. If the server delays the action by too much, the game will not feel responsive, making it hard for the player to time his or her actions precisely. If the server delays the action by too little, the game is frozen until players can receive overdue messages, and this can cause stuttering, throwing off the rhythm of the game.

Client-Side Prediction

One of the challenges in real-time multiplayer network programming is latency prediction, estimating the latency among all machines in the game. When the prediction is not accurate, latency can cause stuttering in the game play or intermittent pausing. Even with accurate latency prediction, the delay between physically pressing a button and seeing the effect of that press can be disorienting. To address these problems, one can add client-side prediction. With client-side prediction, each client stores two copies of the game state: one copy that contains completely accurate information but that is delayed because of latency and another that is current but that assumes that no other clients have sent any new actions. As the client receives new actions or game states from the server, the client updates his or her delayed copy of the game state, replaces the current copy with a copy of the delayed game state, and fast-forwards the newly copied state to the present time (see Figure 12-2). This technique gives the illusion of responding to player actions immediately, while also correcting the state as it goes out of sync.

9781430266976_Fig12-02.jpg

Figure 12-2. Client-side prediction and correction; note that care should be taken when writing the graphics engine to smoothly correct for these transitions (car image courtesy of Wikimedia)

When using client-side prediction with correction, clients can make new assumptions about when an action should be processed. Without client-side prediction, clients must delay their inputs, basing them on the time it is expected the input will take to reach all other clients. With client-side prediction, clients can reduce input delay to an arbitrary number. As client delay decreases, the number of fast-forwards increases, and the game will feel more responsive, but it will also contain more stuttering. As the delay increases, the number of fast-forwards decreases, but the input latency will increase. The important point to note here is that adding client-side prediction removes the latency constraint on the system and gives the developer more freedom. Adding client-side prediction also keeps the entire game from halting when a particular user experiences packet loss. Note how in RTS games, such as the first Starcraft, the entire game must slow down when a single player’s connection quality decreases (i.e., when, during play, a box pops up with the heading “Waiting for Players”). This is because many RTSs do not have client-side prediction. The reason they do not is that their game state is so large and that reversing actions is difficult.

Synchronized Time Among Clients

To deal with latency effectively, all clients and the server must have the same absolute time. If a client’s operating system clock is different from the server’s clock, the client will either send his or her messages too late or try to simulate too far into the future. In both cases, the client’s experience will be choppy and could affect the other clients in the system. To address this, a Network Time Protocol (NTP) synchronization process can be added to calculate a time that all computers can agree on. Two approaches to NTP are to sync with the server’s time or to sync with a third-party NTP server. One library that implements a simple NTP client and server is Socket-NTP (https://github.com/calvinfo/socket-ntp).

Fast-Forwarding the Game State

With client-side prediction, there can be times when the server sends the client a copy of the game state that is older than the current time. In this case, the client must fast-forward the game state by playing many frames of the game engine in rapid succession. To fast-forward from a past state, it is important that each client keep a history of actions received since the last received game state. During fast-forwarding, the client replays actions experienced in the past.

Figure 12-3 illustrates fast-forwarding. In the first image the client has predicted a car moving in a straight line. The second image depicts a new state from the server, in which the car has made a left turn. Because the left turn event took place before the current time of the predicted state, the client must go back in time, apply the left turn, and then fast-forward in time to catch up to the predicted time. This ensures that the player’s perception of time always moves in one direction. Note that, when rewinding or fast-forwarding, the user interface (i.e., graphics, sound, and so on) are not updated, so the player does not experience the rewind or fast-forward, but only the causal effects of these actions.

9781430266976_Fig12-03.jpg

Figure 12-3. Fast-Forwarding the game state

Tips and Tricks

The following sections illustrate some handy tips and tricks for writing network code.

Keep Client Input Times Monotonically Increasing

If the client predicts that one of his or her inputs will arrive at time t, then the following input from that client should have an expected arrival time greater than t. Note that, because latency between connections changes randomly and as new players arrive and leave, many input delay calculations can cause the time not to be monotonically increasing. In this case, artificially set the input time slightly higher than the previous until the times stabilize. Assuming that inputs from other clients are monotonically increasing in time makes coding the receiving logic simpler and reduces the number of fast-forwards.

Keep the Game State Independent from the Rest of the Game

Consider a hockey game, in which the goalie’s position determines whether he or she blocks a slap shot. The goalie’s position must be included in the game state. It may be tempting to use the model from the graphics engine to determine whether the slap shot is blocked, but this removes the isolation of the game engine from the rest of the system. Removing this isolation makes fast-forwarding problematic (e.g., graphics engines are often capped at a certain number of frames per second [FPS] and will not run any higher). Do not make the game state depend on the state of the graphics or physics engine. If the game logic depends on physics, then the physics engine needs to be part of the game state (i.e., the physics state should be copied into the game state at every frame).

Avoid Floating Point in the Game State

As mentioned previously, floating-point calculations are not deterministic across machines. This is because some processors have special instructions that allow them to do floating-point math with higher precision than others. Also, some compilers are more aware of these special instructions and will use them at different times from other compilers. With JavaScript in particular, all arithmetic is performed in floating point. Although this technically means that no math can be performed on the game state, in practice all operations on small integers (i.e., less than one million) will yield the same result across processors, operating systems, and interpreters. Note that most physics engines involve many floating-point calculations. In this case, expect that clients will be slightly out of sync, use the state broadcast method, and continuously correct the physics engine as new states arrive. To use client-side prediction, the physics engine must be fast enough to support fast-forwarding and be able to serialize/restore the state of the engine to the game state.

Keep the Game Engine Small

The game state should only include the information necessary to run a headless (i.e., graphic-less) version of your game. Images, textures, music files, your site’s URL, and other information that does not directly affect game logic should not be part of the game state. Keeping your game state small has a dramatic effect on the overall bandwidth consumption of the network. Also, maintaining a light game engine will make it easier to fast-forward the game when the client needs to catch up. Separate the game engine from the rest of the system so that it can be updated without updating graphics, sound, or other parts of the game. Physics engines again complicate this, because they often have an unavoidable effect on the game engine. In this case, the physics engine needs to be considered part of the game engine, and the physics world state, part of the game state.

Interpolate Between Game States

Because of bandwidth limitations, the game state can only update ten or fewer times a second, yet most games run at 60 FPS. The way to ensure that your game is smooth is to interpolate the graphics between game states. For example, if the unit is at position 1 in the current game state, and that same unit is known to be at position 2 in the next game state, the graphics engine can slide that unit from 1 to 2 while the game engine is in between states. Interpolation also lessens the effect of server correction by smoothly sliding units into their correct positions.

Do not Assume a Particular Sequence of Game States

Many graphical and sound effects depend on a particular game-state change, such as scoring a point. Note that, because the server is updating the client’s game state, and the client is trying to predict future game states that may not be accurate, any subsequent graphical or sound effects may be invalid. Care must be taken to ensure that the graphics and sounds are still smooth, despite jumps in game state. One method for handling this is only to play notification sounds when the server and the client agree on an event.

Send Checksums of the Game State

Because bugs in the network engine typically do not crash the game, it is important to send checksums of the game state frequently. A checksum is a small sequence of characters intended to describe a larger block of data. Although it is possible, in theory, for two clients to have a different game state with the same checksum, it is highly unlikely. Adding and verifying checksums will allow you to catch errors as soon as such errors cause the game state to go out of sync. Checksums make the debugging process much simpler by isolating exactly where there can be a bug in the code. One common method of generating a checksum in JavaScript is to stringify the game-state object and then use a library, such as CryptoJS (https://code.google.com/p/crypto-js/), to hash the string. This small hash can then be passed around as the checksum.

Case Study: FrightCycle

FrightCycle is a Halloween-themed light-cycle racing game, in which players try to trap each other with the trail of their light cycle, while also avoiding collision with other trails, their own trail, and the sides of the map. FrightCycle fits the description of a real-time multiplayer network game because all players make decisions simultaneously, timing is of importance, and several players can play a game across the Internet. To view the repository for FrightCycle, visit the following URL: https://github.com/MisterTea/JSRealtimeNetworking.

The next sections begin with an overview of the technology used in FrightCycle and then discuss the specifics of implementing a state broadcast system with time synchronization and client-side prediction.

Getting Started

Although much of the technology in FrightCycle is not specific to real-time network programming and is thus outside the scope of this chapter, it is important to spend some time explaining the code base. First, let’s look at the dependencies:

  • Browserify allows the same “require” syntax (known as CommonJS requires) in node.js to work in browser-based JavaScript. Browserify can also obfuscate and compress client-side JavaScript.
  • Socket.IO is an event-based WebSocket client and server library. This library handles the real-time communication in FrightCycle.
  • Express is a node.js web application framework. It deals with the server-side routing and HTML generation.
  • AngularJS is a client-side web application framework. It handles client-side routing and HTML generation.
  • Clone deep copies objects.
  • Underscore allows object merging, among many other useful functions.
  • Fabric.js provides a scene-graph library on top of HTML5 Canvas.
  • NTPClient lets the clients and the server agree on a common time.

Next, let’s cover the files at a high level. First, the server files:

  • server/app.js contains the main server loop and event handlers for client events.
  • server/GameManager.js handles spawning/releasing games and registering players to games.

Now, the client files:

  • public/js/main.js initializes the network engine that is the entry point for FrightCycle.
  • common/ai.js offers some simple logic for artificial intelligence (AI) players. Note that the AI players do not use the network engine and are queried for commands at every tick.
  • common/game.js is the main loop for the client code. It drives the game, network, input, and rendering engines.
  • common/geometry.js holds several functions specific to the FrightCycle game logic.
  • common/globals.js contains many important constants and global parameters.
  • common/input.js provides code for converting player key presses to in-game commands.
  • common/network.js is the interface between client and server.
  • common/renderer.js has the rendering engine.

Before diving into the code, let’s run the game. First, you need the code. You can download a snapshot of the code from the Source Code/Download area of the Apress web site (www.apress.com), or open a shell, and enter the following command for the most recent copy:

git clonehttps://github.com/MisterTea/JSRealtimeNetworking.git

Next, you have to compile the client-side code into a single bundle with Browserify. To do this, enter the JSRealtimeNetworking directory, and run the following command:

node build.js

This creates a file called bundle.js in the public/js folder. The bundle.js file contains the entire client-side source code for the project and is referenced by the playgame HTML fragment, which is in server/views/playgame.ejs:

<!DOCTYPE html>
<html xmlns:ng="http://angularjs.org">
    <head>
        <script src="js/bundle.js"></script>
    </head>
    <body>
        <div ng-controller="WelcomeCtrl">
            <p>Welcome <%= playerId %></p>
        </div>
        <canvas id="c" width="800" height="600"
                    style="border:1px solid #000000;">
        </canvas>
    </body>
</html>

Note that the bundle.js file can be minified and obfuscated for added performance and security. The Browserify options are located in the build.js file. Once the client-side code is compiled, start the server with the following command:

node server/app.js

You should see some logging that ends with this code:

Express server listening on port 3000

The server is now running. To create a new game, enter the following URL in any modern web browser:

http://localhost:3000/playgame?playerid=Player1&gameid=1234

This command creates a new game room with the ID 1234 and adds player “Player1” to the room. After the initial handshake and state transfer, the game will start, and you can direct your light cycle with the W, S, A, and D keys. To test the networking capability, open a second browser tab, and visit the following URL:

http://localhost:3000/playgame?playerid=Player2&gameid=1234

Separate the tabs on your screen so that you can see both at the same time. Note that each tab controls a different player but that the changes from one player are propagated to the other player in rea -time. The remainder of the chapter is dedicated to explaining how this demo application works.

The Game State

As described previously, it is important to keep the game state as small as possible. The game state for FrightCycle contains the following elements:

  • Tick: An integer that is incremented at every game engine update.
  • GameStartTick: This contains the tick when the game begins (i.e., when the light cycles begin moving); if less than the current tick, the game is active.
  • Players: An associative array that holds objects for each light cycle (where it is, the trails it has left behind, who is controlling it,and so on).
  • ConnectedClients: A list of player IDs that were connected to the game server during this state.

Communication Between the Server and Client

The client begins by handshaking with the server. As part of this process, the client and server exchange a token ID that uniquely identifies the browser session with a game Id/player Id pair. As a result, a player can participate in two games simultaneously on two browser sessions but can also reenter the same game in case there is a disconnect. After the handshaking, the server sends the entire state and command histories to the client. Note that, with 100 states generated every second, these objects are rather large. Sending the current state is not sufficient, because a new client may need to reverse time in order to handle input from a lagging client. Even so, a potential optimization would be to send only recent states and ensure a maximum latency on a single input. The following code describes this initial process:

On Client (network.js):
var socket = io.connect('http://' + window.location.hostname + ':' + window.location.port);
this.socket = socket;
ntp.init(socket);
socket.emit('clientinit', {
  gameid: gameId,
  tokenid: token
});
 
On Server (app.js):
ntp.sync(socket);
socket.on('clientinit', function(data) {
  setTimeout(function() {
    var handler = gameManager.getGameHandler(data.gameid);
    var playerId = handler.tokenPlayerMap[data.tokenid];
    socket.set('playerId', playerId, function() {
      socket.set('gameId', data.gameid, function() {
        var game = handler.game;
        var firstCommandMs = (game.tick * g.MS_PER_TICK) + g.LATENCY_MS;
        gameManager.registerRemotePlayer(
          data.gameid,
          data.tokenid,
          firstCommandMs);
        console.log("Sending join command");
        socket.emit('serverinit', {
          startTime: game.startTime,
          tick: game.tick,
          stateHistory: clone(game.stateHistory),
          commandHistory: clone(game.commandHistory)
        });
 
        var command = {};
        command[playerId] = [g.PLAYER_JOIN];
        applyServerCommand(data.gameid, command);
      });
    });
 // Wait 3 seconds before accepting the client so the ntp
  // accuracy can improve.
  }, 3000);
});

Once this initial phase is complete, the client and the server begin communicating back and forth in real time. The server sends states, player commands, and server commands. The client sends local player commands to be echoed to other clients. The following code snippets walk us through an input from the keyboard to the other clients:

Receiving inputs from the keyboard (input.js):
 
keys: {},
keysToDelete: [],
 
init: function() {
  if (typeof window === 'undefined') {
    // Only runs client-side
    return;
  }
 
  var that = this;
  window.addEventListener(
    "keydown",
    function(e) {
      that.keys[e.keyCode] = e.keyCode;
    },
    false);
 
  window.addEventListener(
    'keyup',
    function(e) {
      // Delay releasing keys to allow the game a chance to read a
      // keydown/keyup event, even if the duration is short.
      that.keysToDelete.push(e.keyCode);
    },
    false);
},
 
getCommands: function(game) {
  commands = [];
  if ('87' in this.keys) {
    commands.push(g.MOVE_UP);
  }
  if ('68' in this.keys) {
    commands.push(g.MOVE_RIGHT);
  }
  if ('65' in this.keys) {
    commands.push(g.MOVE_LEFT);
  }
  if ('83' in this.keys) {
    commands.push(g.MOVE_DOWN);
  }
  for (var i = 0; i < this.keysToDelete.length; i++) {
    delete this.keys[this.keysToDelete[i]];
  }
  this.keysToDelete.length = 0;
 
  return commands;
}
 
Sending command to the server (network.js):
 
lastProcessMs: -1,
sendCommands: function(tick, commands) {
  var processMs = (tick * g.MS_PER_TICK) + g.LATENCY_MS;
 
  if (this.lastProcessMs >= processMs) {
    // This command is scheduled to run before/during the previous
    // command. Move it just ahead of the previous command.
 
    // Note that, because of this, some commands may get dropped
    // because they are sandwiched between two other commands in
    // time. As long as all clients drop the same commands, this
    // isn't a problem.
    processMs = this.lastProcessMs + 1;
  }
  this.lastProcessMs = processMs;
 
  this.socket.emit('clientinput', {
    ms: processMs,
    commands: commands
  });
}
 
Receiving the command on the server (app.js):
 
var applyCommand = function(gameId, playerId, ms, commandlist) {
  var handler = gameManager.getGameHandler(gameId);
  var game = handler.game;
  var playerServerData = handler.playerServerData[playerId];
 
  if (game.tick * g.MS_PER_TICK >= ms) {
    // This command came in too late, we have to adjust the time
    // and apply the command later than expected.
 
    // Note that this makes life difficult. Now the command
    // will execute at a different time than was expected by the
    // client when the client sent the command.
    ms = (game.tick * g.MS_PER_TICK) + 1 + g.intdiv(g.LATENCY_MS, 2);
  }
  if (playerServerData.nextInputMs >= ms) {
    // Note that although we are trying to prevent this scenario
    // client-side, it can still happen because of the if
    // statement above, so we have to be prepared to deal with
    // it. If this happens on the server side, move the command
    // up to accomodate. Note that, as above, the ms of the
    // command has to be modified from the client's original
    // intent.
    ms = playerServerData.nextInputMs + 1;
  }
  playerServerData.nextInputMs = ms;
 
  // Apply the commandlist on the server
  game.addCommand(ms, playerId, commandlist);
 
  // Broadcast the commandlist to the other clients
  io.sockets.in(game.id).emit("serverinput", {
    ms: ms,
    commands: commandlist,
    playerId: playerId
  });
};
 
Receiving commands from the server (game.js):
 
addCommand: function(ms, playerId, commandList) {
  this.getCommands(ms)[playerId] = commandList;
  this.updateLastCommandMs(playerId, ms);
  var newTick = g.intdiv(ms, g.MS_PER_TICK);
  if (this.tick > newTick) {
    // A change in the past has happend, we need to rewind.
    this.tick = newTick;
  }
}

Synchronizing Time

As part of the initial handshaking, the NTP client is initialized and given a few seconds to settle. Note that the NTP client calculates the difference in time between the client and the server. The NTP client does not make any assumption about the server time’s correctness and shouldn’t be used as a true estimate for the current world clock. With the NTP client library, the client knows the time offset between itself and the server. This offset is important, because the server and all clients should process the same frame of the game at roughly the same time. The NTP client prevents a client from getting too far ahead of or behind the server or the other clients. The getNetworkTime() function in network.js returns the current time, as agreed on by the client and server. The game engine (game.js) uses this function to throttle the game engine Throttling means to put the processor to sleep in between frames so that the game moves at a rate that feels realistic. The following code throttles updates so that all clients are processing the same frame at about the same moment in time:

Throttling code (in game.js):
if (g.MS_PER_TICK > 0 && this.tick > 0) {
  // clockTick contains the tick that should be processed
  // based on the current time. In the case of a
  // fractional tick, the code rounds up.
  var clockTick = g.intdiv(this.network.getNetworkTime() - this.startTime, g.MS_PER_TICK);
  if (this.tick > clockTick) {
    // This means that the game time is ahead of real
    // time, don't process more frames.
    break;
  }
}

Server Commands

A server command is a special command that must be processed at the time it is issued in order for the game to progress beyond that time. As mentioned earlier, commands from clients may be dropped if the client is catching up and processing more FPS than the server. To force all clients to process all server commands, clients will not process a game update until the server command list for that tick has been received, and the server will produce exactly one command for every tick. This is similar to the lockstep model described previously but only applies to server commands. To prevent server commands from causing the client to stutter (the main drawback with the lockstep model), an artificially high latency is given to all server commands. This causes a delay in server commands, but this delay is acceptable, because server commands are not time sensitive.

Client-Side Prediction

In FrightCycle, client-side prediction is handled by the CLIENT_SIDE_PREDICTION variable in globals.js. Try setting this variable to false, and see the effect. Now, clients will wait until they have inputs from all other clients before processing the next frame. If you have a fast connection to the server, you may not notice the difference, but with a slow connection, you will notice sporadic delays; the game, however, will never be in an inconsistent state.

Conclusion

As FrightCycle demonstrates, the complexity of implementing a real-time network multiplayer game is trading off latency for correctness of the current state. At one extreme, a lockstep model without client-side prediction will ensure that all users have complete state information at the expense of high latency. The additional latency makes the game less responsive and can make the game difficult to play. At the other extreme, users submit actions that are effective immediately. This provides instant user feedback but causes jittering, as the game engine is constantly rolled back to apply user commands retroactively and then fast-forwarded to the present time with the additional user inputs.

________________________

1Julie Bort, “How Many Web Sites Are There?,” http://www.businessinsider.com/how-many-web-sites-are-are-there-2012-3, March 8, 2012.

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

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