October 2017

Volume 32 Number 10

[Game Development]

Multiplayer Networked Physics for Web Game Development

By Gary Weiss | October 2017

Web games have a shoddy reputation. Well-written games are available on high-end game consoles, and for computers equipped with modern graphics processors. Even mobile gaming has come a long way since the canonical “Snake.” But Web games, like Rodney Dangerfield, get no respect. That said, recent advances in modern browsers have the potential to bring quality gaming experiences to the browser. The most important technology in this respect is WebGL, which brings direct access to hardware-accelerated graphics.

In this article, I’ll investigate Web gaming from a specific point of view: implementation of a simple multiplayer networked-physics game. This exercise investigates which technologies and game development tools are available for the task, and stress tests modern browsers for networking performance, computation efficiency, and rendering. This article can be viewed as a small investigation that is part of a much larger theme: Can a professional game studio really write a high-end game in a browser?

The source code for the game is freely available at bit.ly/2toc4h4, and the game is playable online at sprocketleague.lance.gg.

The Game

The game I’ll model involves cars driving around in a stadium. There’s a large ball in the stadium that can be bounced into the goals. This concept is inspired by “Rocket League,” a popular e-sports title by Psyonix. Therefore, the game is assembled from the following objects: an arena (including the goals), cars and a ball, as you can see in Figure 1. The player can move the car forward or in reverse and turn the wheels.

Cars Playing Soccer in Your Browser

Figure 1 Cars Playing Soccer in Your Browser

The Architecture of the Game

The game implementation uses the authoritative server model, whereby each client forwards user inputs to a central server. Clients won’t wait for the network before they apply the inputs locally; rather, the clients will perform what’s commonly referred to as client-side prediction. This technique allows the client to continue running the game logic and game physics. The catch, of course, is that when updates to positions and velocities are broadcast from the authoritative server, each client must make its own incremental corrections so that the game preserves a smooth visual experience.

Rendering The main goal in rendering is accessing the GPU. There are several tools that can fill this slot by simplifying the interface to WebGL. Babylon.js and Three.js have been used for many advanced Web games. For this game I used A-Frame, a relatively new library that allows you to build a 3D scene using HTML elements. A-Frame defines new HTML elements that you can use as building blocks. Also, A-Frame does a good job of modeling the objects using an Entity-Component-System design pattern. Another interesting advantage of A-Frame is that it was designed for virtual reality (VR), so the game can be easily made into a VR experience.

Physics Cannon.js is a relatively easy-to-use physics engine. It provides rigid-body mechanics with collision detection. It was written in JavaScript from scratch, which means the library is much smaller than transpiled engines such as ammo.js.

Object Serialization and Networking For the server to broadcast object positions and velocities, game objects need to be serialized. For this purpose I use the Lance game server (Lance.gg), an open source multiplayer engine. Game objects that derive the Lance GameObject base class will be serialized and forwarded to clients. Advanced game engines use UDP sockets for better performance, while Lance still uses TCP sockets. This is because of the complexity of WebRTC for client-server communication (as opposed to peer-to-peer). This is one area where Web gaming significantly lags behind other platforms. However, in my experience, the resulting game provides a smooth experience even at network lags of 150ms, which is typical within the continental United States or Europe.

Client-Side Prediction Here, too, I relied on Lance to perform the extrapolation of positions and velocities. Lance implements the client-side collection of server broadcasts, calculates the necessary corrections for each position and orientation, and applies the correction incrementally over the progress of the rendering loop. Position corrections are applied by calculating the vector difference, and dividing this difference into increments. Orientation corrections, in contrast, are applied by calculating the quaternion difference. The correction quaternion  *qΔ * for a server quaternion  *qs * and a client quaternion  qc  is given by  qΔ = qs o qc -1. This quaternion can be represented as a rotation about a single axis, and then divided into increments about that axis.

The game architecture, then, consists of server logic, client logic and shared logic, as shown in Figure 2. The client logic must handle all aspects that involve the user, including the collection of inputs and the rendering work. The server logic is simpler by comparison, but it does need to take some authoritative steps, like accepting connections, creating game objects and deciding who scored.

The Architecture of a Multiplayer Game

Figure 2 The Architecture of a Multiplayer Game

The shared logic is the most interesting part of the implementation, and is discussed in a separate section. It includes the main game logic, which must run on the authoritative server, but must also run on each client as the game progresses in between server broadcasts. Let’s start with the nuts and bolts of multiplayer games: the communication layer.

Client-Server Communication

A multiplayer game with an authoritative server requires communication code. This code has many responsibilities.  Initially, it must manage the bring-up of the servers, which listen on TCP/IP ports for incoming data. Next, there’s a handshake process when a client connects to the server. This handshake registers the new client at the server, and establishes a dedicated socket for the client. Some games may require an authentication or a matchmaking process, as well. The connection of a new client, and disconnection of an existing client, have implications for the game. The game needs to be notified so it can create a new car for the new client and assign the player to a team.

While the game executes, the server sends periodic broadcasts to all clients. The broadcasts describe the game’s current state. A state includes the position, velocity, orientation (quaternion) and angular velocity of each game object. Additionally, game objects may well have other non-physical attributes, such as strength, shield power, spell timeout and so forth. Here, the server needs to be very efficient and send as little data as is absolutely required. For example, an object that hasn’t moved or changed shouldn’t be broadcast. On the other hand, if a new player has just connected, that player does need a full description of all the game objects to bootstrap the game state.

Last, the client-server communication will need to emit and capture out-of-band events, which describe game events that don’t fit as part of the normal game progress. For example, the server may want to communicate to a specific player that he’s reached a special achievement. This mechanism can also be used to report a message to all players.

Here’s the implementation. On the server side, I bring up an express node.js server and a socket.io service. The code shown in Figure 3 configures the HTTP server on port 3000, initializes the server engine and the game engine classes, and routes all HTTP requests to the dist sub-directory where all the HTML and assets are placed. This means that the node.js server has two roles. In the first role, it serves as an HTML server that serves HTML content, including CSS files, images, audio and the 3D models in GLTF format. In the second role, the HTTP server acts as a game server that accepts incoming connections on socket.io. Note that best practice is to cache all static assets behind a content-delivery network (CDN) for good performance. This configuration isn’t shown in the example in Figure 3, but the use of a CDN dramatically increased the game bring-up performance, while reducing the unnecessary load from the game server. Finally, at the last line of the code, the game is started.

Figure 3 Server Entry Point

const express = require('express'); 
const socketIO = require('socket.io'); 
 
// Constants 
const PORT = process.env.PORT || 3000; 
const INDEX = path.join(__dirname, './dist/index.html'); 
 
 
// Network servers 
const server = express(); 
const requestHandler = server.listen(PORT, () => console.log(`Listening on ${PORT}`)); 
const io = socketIO(requestHandler); 
 
 
// Get game classes 
const SLServerEngine = require('./src/server/SLServerEngine.js'); 
const SLGameEngine = require('./src/common/SLGameEngine.js'); 
 
 
// Create instances 
const gameEngine = new SLGameEngine({ traceLevel: 0 }); 
const serverEngine = 
  new SLServerEngine(io, gameEngine, { debug: {}, updateRate: 6, timeoutInterval: 20 }); 
 
 
// HTTP routes 
server.get('/', (req, res) => { res.sendFile(INDEX); }); 
server.use('/', express.static(path.join(__dirname, './dist/'))); 
 
// Start the game 
serverEngine.start();

The server has started, but has yet to handle new connections. The base ServerEngine class declares a handler method for new connections called onPlayerConnected and a corresponding onPlayerDisconnected handler. This is the place where the author­itative server subclass methods can instruct the game engine to create a new car or remove an existing car. The code snippet in Figure 4 from the base class shows how socket.io is used to ensure those handlers are called when a new player has connected.

Figure 4 Connection Handler Implementation with socket.io

class ServerEngine {

  // The server engine constructor registers a listener on new connections
  constructor(io, gameEngine, options) {
      this.connectedPlayers = {};
      io.on('connection', this.onPlayerConnected.bind(this));
  }

  // Handle new player connection
  onPlayerConnected(socket) {
    let that = this;

    // Save player socket and state
    this.connectedPlayers[socket.id] = {
      socket: socket,
      state: 'new'
    };
    let playerId = socket.playerId = ++this.gameEngine.world.playerCount;
 
    // Save player join time, and emit a `playerJoined` event
    socket.joinTime = (new Date()).getTime();
    this.resetIdleTimeout(socket);
    let playerEvent = { id: socket.id, playerId, 
      joinTime: socket.joinTime, disconnectTime: 0 };
    this.gameEngine.emit('playerJoined', playerEvent);
    socket.emit('playerJoined', playerEvent);

    // Ensure a handler is called when the player disconnects
    socket.on('disconnect', function() {
      playerEvent.disconnectTime = (new Date()).getTime();
      that.onPlayerDisconnected(socket.id, playerId);
      that.gameEngine.emit('playerDisconnected', playerEvent);
    });


  }

  // Every server step starts here
  step() {

    // Run the game engine step
    this.gameEngine.step();

    // Broadcast game state to all players
    if (this.gameEngine.world.stepCount % this.options.updateRate === 0) {
      for (let socketId of Object.keys(this.connectedPlayers)) {
        this.connectedPlayers[socketId].socket.emit(
          'worldUpdate', this.serializeUpdate());
      }
    }
  }
}

When a player connects, this code emits the playerJoined event twice. The first event is emitted on the game engine’s event controller, where other parts of the game code may have registered listeners to this specific event. The second event is sent over the player’s socket. In this case, the event can be captured on the client side of the socket, and acts as an acknowledgement that the server has allowed this player to join the game.

Note the server engine’s step method—here, the game state is broadcast to all connected players. This broadcast only occurs at a fixed interval. In my game I chose to configure the schedule such that 60 steps are executed per second, and a broadcast is sent every sixth step, or 10 times per second.

Now I can implement the methods in the subclass, as they apply specifically to the game. As shown in Figure 5, this code handles new connections by creating a new car and joining that car to either the blue team or the red team. When a player disconnects, I remove that player’s car.

Figure 5 Server Connection Handling

// Game-specific logic for player connections
onPlayerConnected(socket) {
  super.onPlayerConnected(socket);

  let makePlayerCar = (team) => {
    this.gameEngine.makeCar(socket.playerId, team);
  };

  // Handle client restart requests
  socket.on('requestRestart', makePlayerCar);
  socket.on('keepAlive', () => { this.resetIdleTimeout(socket); });
}

// Game-specific logic for player dis-connections
onPlayerDisconnected(socketId, playerId) {
  super.onPlayerDisconnected(socketId, playerId);
  this.gameEngine.removeCar(playerId);
}

Communication can also occur out-of-band, meaning that an event must sometimes be sent to one or more clients asynchronously, or data must be sent that’s not part of the game object states. The following sample shows how socket.io can be used on the server side: 

// ServerEngine: send the event monsterAttack! with data 
// monsterData = {...} to all players
this.io.sockets.emit('monsterAttack!', monsterData);

// ServerEngine: send events to specific connected players
for (let socketId of Object.keys(this.connectedPlayers)) {
  let player = this.connectedPlayers[socketId];
  let playerId = player.socket.playerId;

  let message = `hello player ${playerId}`;
  player.socket.emit('secret', message);
}

On the client side, listening to events is simple:

this.socket.on('monsterAttack!', (e) => {
  console.log(`run for your lives! ${e}`);
});

The Game Logic

The fundamental game logic involves the application of user inputs to the game state, as shown in Figure 6. For example, pressing on the gas should apply a forward-directed force on a car. Pressing the brakes should apply a backward-directed force on a car, potentially going in reverse. Turning the steering wheel applies an angular velocity, and so on.

Figure 6 The GameEngine Step Method

// A single Game Engine Step
step() {
  super.step();

  // Car physics
  this.world.forEachObject((id, o) => {
    if (o.class === Car) {
      o.adjustCarMovement();
    }
  });

  // Check if we have a goal
  if (this.ball && this.arena) {

    // Check for ball in Goal 1
    if (this.arena.isObjInGoal1(this.ball)) {
      this.ball.showExplosion();
      this.resetBall();
      this.metaData.teams.red.score++;
    }

    // Check for ball in Goal 2
    if (this.arena.isObjInGoal2(this.ball)) {
      this.ball.showExplosion();
      this.resetBall();
      this.metaData.teams.blue.score++;
    }
  }
}

These operations are implemented as calls to the physics engine, as time progresses. An interesting twist is that applying correct physical forces to a car provides a poor gaming experience. If you apply the correct physical forces just as they work in the real world, the handling and control of the car doesn’t “feel right” in the hands of the player. The resulting game action is too slow. For the game to be enjoyable, I needed to apply an artificial boost when the car accelerates from very low velocities, otherwise the game action was just too slow.

Finally, the game logic must check if the ball passed the goal posts, and update the score.

Client-Side Prediction

Each multiplayer game has different requirements for client-side prediction and needs to be configured correspondingly. Game object types can also have a specific configuration. The code that follows shows a small subset of the game’s configuration. User inputs are delayed by three steps, or 50ms, to better match the time at which the inputs will be applied on the server. This is done by setting delayInputCount to 3. syncOptions.sync is set to “extrapolate” to enable client-side prediction; localObjBending is set to 0.6, indicating that locally controlled object positions should be corrected by 60 percent before the next server broadcast is estimated to arrive; remoteObjBending is set higher, to 80 percent, because those objects are more likely to diverge significantly. Last, I set bendingIncrements to 6, indicating that each correction must not be applied at once, but rather over 6 increments of the render loop:

const options = {
  delayInputCount: 3,
  syncOptions: {
    sync: 'extrapolate',
    localObjBending: 0.6,
    remoteObjBending: 0.8,
    bendingIncrements: 6
  }
};

The ball has its own velocity-bending parameter (not shown) set to zero. This is because when one team scores, the ball gets teleported back to the center of the field, and any attempt to bend the resulting velocity will result in undesired visual effects.

The client-side prediction logic itself is too large to include here, so I presented it as general pseudocode in Figure 7. It’s the secret sauce, which makes a multiplayer game possible. The first step scans the last server broadcast and checks if any new objects have been created. Existing objects remember their current state before they’re synced to the server state. The second step is the reenactment phase, which re-executes the game logic for all the steps that have occurred since the step described in the broadcast. Inputs need to be reapplied, and the game engine step is executed in reenactment mode. In the third step, each object records the required correction, and reverts to the remembered state. Finally, in the last step, objects that are marked for destruction can be removed.

Figure 7 Simplified Client-Side Prediction Pseudocode

applySync(lastSync):

  // Step 1: sync to all server objects in the broadcast
  for each obj in lastSync.objects
    localObj = getLocalObj(obj.id)
    if localObj
      localObj.rememberState()
      localObj.syncTo(obj)
    else
      addLocalObj(obj)

  // Step 2: re-enactment using latest state information
  for step = lastSync.serverStep; step < clientStep; step++
    processInputsForStep(step)
    gameEngine.step(reenactment=true)

  // Step 3: record bending and revert
  for each obj in world.obects
     obj.bendToCurrentState()
     obj.revertToRememberedState()

  // Step 4: remove destroyed objects
  for each obj in world.obects
    objSyncEvents = lastSync.getEvents(obj.id)
    for each event in objSyncEvents
      if event.name == ‘objectDestroy’
        gameEngine.removeObjectFromWorld(obj.id)

The Devil in the Details

There’s much more work in this game than appears at first glance. I haven’t touched on camera movement tracking the car, differences in input between mobile devices and desktops (in mobile devices car movement is controlled by tilting the device), and topics like matchmaking and debugging. All these topics are outside the scope of this article. However, it’s important to mention that some of these entail significant road bumps in Web gaming. For example, a Web game can run on a desktop or on a tablet or on a mobile device—so sometimes a GPU is available and sometimes it’s not, while the likelihood of low networking bandwidth increases.

Yet another challenge is the workflow required for generating compatible 3D models from existing standards. The game development toolsets for Web platforms aren’t as mature as their counterparts on other platforms. Last, Web games don’t have local storage in the same sense that other platforms provide. So care must be taken to load assets frugally. An impressive achievement in this area is the online game “Urban Galaxy” (bit.ly/2uf3iWB).

These issues present new challenges to game developers on the Web platform. However, there are some distinct advantages to the Web platform that must be mentioned. The debugging environment on browsers is very advanced. The dynamic nature of Web programming leads to very fast prototyping, and continuous delivery of patches. The community of Web developers is much larger.

Probably the most significant advantage for Web games is the way they’re started. A Web game can be accessed by simply clicking on a Web link and doesn’t require pre-downloading the entire set of game assets on the client device before it starts. By downloading assets only as they’re needed, the game play experience can start within seconds, instead of minutes, and where possible, offline support can also be leveraged to improve subsequent game play.

For example, visitors logging into the NBA Web site might find themselves playing a basketball game against players from all over the world, right in the browser.

Web Technologies for Game Development

In the past couple of years, there has been significant progress in browser ability. The primary advance is the development of WebGL, which has brought direct access to GPU functionality in the browser. WebGL is a very low-level interface, so game developers typically rely on higher-level interfaces from libraries like Babylon.js, three.js and A-Frame to build complex scenes.

Other recent Web technologies include WebAudio, which provides advanced audio capabilities; Media Source Extensions (MSE), which optimize video access and allow real-time video manipulation; and WebVR, which is relatively new and provides an interface to virtual reality (VR) hardware including VR headsets and VR controllers. Using WebVR together with WebAudio, it’s possible to implement ambisonic 3D audio experiences in a Web-driven VR scene.

Even the JavaScript language has been updated in useful ways with the latest ECMA-262 6th Edition. The JavaScript ecosystem is wide, providing many frameworks for developers. In fact, there are so many frameworks and methodologies that the abundance sometimes leads to confusion.

On the network front, alongside the HTTP protocol, WebSockets and WebRTC are two standards that are available for applications. WebSockets are simple bi-directional data streams, while WebRTC provides a more complex mechanism for peer-to-peer communication. For multiplayer game development, Lance.gg is an open source library that handles the networking and synchronization of game objects.

Another interesting technology is WebAssembly, which has recently reached cross-browser consensus, and has the potential to deliver a substantial performance advantage by running compiled binary format in a browser.

You can easily find more information about all of these technologies on the Web.

Wrapping Up

Building a multiplayer game for the Web platform is different. The Web is naturally directed at a wide array of devices and this presents a significant challenge to game development. The absence of simple UDP networking in the browser impacts multiplayer response times. However, recent advances in Web technologies have made it possible to write a multiplayer physics demo in a relatively short time. Libraries and workflows used in this exercise are making fast progress and can already be used today. With additional resources and time, it’s possible to develop a quality gaming experience in a browser.

I want to thank the co-author of the game discussed in the article, Opher Vishnia, a co-founder of Lance and a multidisciplinary creator involved with game development, programming, design and music composition.


Gary Weiss* is a software architect and co-founder of Lance, the group that develops the open source project Lance.gg. Reach him at garyweiss74@gmail.com*

Thanks to the following Microsoft technical expert for reviewing this article:  Raanan Weber


Discuss this article in the MSDN Magazine forum