Creating a Multiplayer Game with WebRTC
Tags: webrtc, javascript
Published: 2017-07-10 20:13:33

---
title: Creating a Multiplayer Game with WebRTC
published: true
description: Peer-to-peer gaming in the browser.
tags: WebRTC, javascript
---

My goal with this project was to develop an online multiplayer game which would use as little server resources as possible.  I was hoping to be able to run the game server in one users browser and allow the other players to connect to it.  I also wanted to keep the project simple enough to be explained in a blog post.

**Technology**

Pretty soon into my search for Peer to Peer web technology I discovered [WebRTC](https://webrtc.org/), and it seemed perfect for my project.  WebRTC is a new web standard that provides browsers with Real-Time Communication capabilities.  Most examples I saw for WebRTC was setting up video or audio streams, but you can also transmit arbitrary data.  In my case, I could use the data channel to transmit user input to the host and the game state to the players.

However, WebRTC does not completely eliminate the need for a server.  In order to establish a connection, two browsers must exchange a small amount of information.  Once the connection is established, the communication is completely peer-to-peer.

**Libraries**

The [WebRTC API](https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API) is pretty complex, so I looked for a library to simplify it.  The most full featured one I came across was [PeerJS](https://github.com/peers/peerjs), but it had not been updated in two years.  I quickly ran into some major bugs that forced me to abandon it.  I settled upon using [simple-peer](https://github.com/feross/simple-peer), which provides a simple API for connecting and communicating using WebRTC.  From their documentation:

```javascript
var SimplePeer = require('simple-peer')

var peer1 = new SimplePeer({ initiator: true })
var peer2 = new SimplePeer()

peer1.on('signal', function (data) {
  // when peer1 has signaling data, give it to peer2 somehow
  peer2.signal(data)
})

peer2.on('signal', function (data) {
  // when peer2 has signaling data, give it to peer1 somehow
  peer1.signal(data)
})

peer1.on('connect', function () {
  // wait for 'connect' event before using the data channel
  peer1.send('hey peer2, how is it going?')
})

peer2.on('data', function (data) {
  // got a data channel message
  console.log('got a message from peer1: ' + data)
})
```

**Establishing a Connection**

In order to establish the connection between two browsers, I needed to exchange about 2 kb of signaling data.  I opted to use Firebase Realtime Database, as it allowed me to easily sync data between two browsers, and the free tier offers plenty of storage.

From the users perspective, the host gives the players a four letter code which they use to connect to the game.  From the browsers perspective, the process is only slightly more complicated.  For reference, my database rules look like this:

```javascript
{
  "rules": {
    "rooms": {
      // 4 Digit room code used to connect players
      "$room_code": {
        "host": {
           "$player": {
             "$data": {
               "data": {
                 // Data from the host for the player
               }
             }
           }
        },
        "players": {
          "$player": {
            "$data": {
              "data": {
                // Data from the player for the host
              }
            }
          }
        },
        "createdAt": {
          // Timestamp set by host when room is created
        }
      }
    }
  }
}
```

**Hosting a Room**

In order to host a room, the host first generates a code by randomly trying 4 character codes until it finds a room that is not in use.  Rooms are considered not in use if they don’t exist in the database, or if the room was created over 30 minutes ago.  The host should delete the room when the game starts, but I wanted to be sure to avoid zombie rooms.  When the host finds an open room, the host's browser adds itself as the room's host and listens for players.

```javascript
function getOpenRoom(database){
 return new Promise((resolve, reject) => {
   const code = generateRoomCode();
   const room = database.ref('rooms/'+code);
   room.once('value').then((snapshot) => {
     const roomData = snapshot.val();
     if (roomData == null) {
       // Room does not exist
       createRoom(room).then(resolve(code));
     } else {
       const roomTimeout = 1800000; // 30 min
       const now = Date.now();
       const msSinceCreated = now - roomData.createdAt;
       if (msSinceCreated > roomTimeout) {
         // It is an old room so wipe it and create a new one
         room.remove().then(() => createRoom(room)).then(resolve(code));
       } else {
         // The room is in use so try a different code
         resolve(getOpenRoom(database));
       }
     }
   })
 });
}
```

**Joining a Game**

A player joins a game by entering the room code and their username.  The player's browser notifies the host by adding an entry in the route `rooms/[code]/players`.  When the player gets their signaling data, the data to the database in the route `rooms/[code]/players/[name]`.

```javascript
// code and name are entered by user
const peer = new SimplePeer({initiator: true});
this.peer = peer;
this.setState({host: peer});

// Sending signaling data from player
peer.on('signal', (signalData) => {
  const nameRef = database.ref('/rooms/'+code+'/players/'+name);
  const newSignalDataRef = nameRef.push();
  newSignalDataRef.set({
    data: JSON.stringify(signalData)
  });
});

// Listen for signaling data from host for me
const hostSignalRef = database.ref('/rooms/'+code+'/host/'+name);
hostSignalRef.on('child_added', (res) => {
  peer.signal(JSON.parse(res.val().data));
});
```

The host listens for new players being added.  When a new player is connected, the host consumes the signals they send and replys with its own signals on the route `rooms/[code]/host/[name]`.

```javascript
// Listen for new players
playersRef.on('child_added', (res) => {
  const playerName = res.key;

  // Create Peer channel
  const peer = new SimplePeer();

  // Listen for signaling data from specific player
  playerRef.on('child_added', (res) => peer.signal(JSON.parse(res.val().data)));

  // Upload signaling data from host
  const signalDataRef = database.ref('/rooms/'+code+'/host/'+playerName);
  peer.on('signal', (signalData) => {
    const newSignalDataRef = signalDataRef.push();
    newSignalDataRef.set({
      data: JSON.stringify(signalData)
    });
  });
});
```

From this point forward, the host and the player can communicate using `peer.on(‘data’, cb)` and `peer.send(data)`.  The player's machine terminates its firebase connection once connected with the host, and the host does the same when the game starts.

And that’s it!  At this point I had bidirectional communication between the host and all of the players, just like I would with a traditional server, so all that was left was to make the game and pass data between the players.

**Getting User Input**

User input is sent as a JSON object whenever the key changes state.  Example: `{ up: true }`

The host keeps track of each player's input states and uses them to move the players each frame.

**Sharing the Game State**

In order to keep the game development simple, I wanted to use the 2D game framework [Phaser](https://github.com/photonstorm/phaser-ce).  The game runs on the host machine, and handles things like physics and collisions.  Each frame, the position and size of every sprite is serialized and sent to each player.  To make things easy, I simply used the sprite data to redraw the whole game in the player's browser each frame.  Because my game only uses a handful of sprite the process works fine, but a more complex game would likely require a more efficient process for sharing the game state.


**Gameplay**

The game I made to test all of this is a simple side scroller.  Platforms randomly appear, and the last player remaining on a platform wins.  Apologies if you encounter any impossible gaps, I didn't spend much time polishing it.

**Notes**

Because the game server is running on one of the player’s machine, it is potentially vulnerable to manipulation by that player.  This system should work fine for playing games with friends though, as long as your friends aren’t cheaters!

**Conclusion**

I was able to set up a peer-to-peer multiplayer game that uses only ~2kb of server bandwidth per player.  I should be able to support 500,000 players per month on the Firebase free tier!  As a bonus, I was able to keep my code concise enough to fit most of it in this post.  I think WebRTC is a neat technology, and I am excited to see what other projects will be built with it.

Play the game [here](https://rynobax.github.io/jump-game/), and check out the source code [here](https://github.com/rynobax/jump-game)!