DEV Community

Ryan Baxley
Ryan Baxley

Posted on

Creating a Multiplayer Game with WebRTC

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, 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 is pretty complex, so I looked for a library to simplify it. The most full featured one I came across was 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, which provides a simple API for connecting and communicating using WebRTC. From their documentation:

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)
})
Enter fullscreen mode Exit fullscreen mode

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:

{
  "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
        }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

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));
       }
     }
   })
 });
}
Enter fullscreen mode Exit fullscreen mode

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].

// 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));
});
Enter fullscreen mode Exit fullscreen mode

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].

// 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)
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

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. 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, and check out the source code here!

Top comments (11)

Collapse
 
naviphuc profile image
hoang trong phuc

Can webRTC connect with global network and ios Devices?

Collapse
 
rynobax_7 profile image
Ryan Baxley

iOS 11 is the first iOS version to support WebRTC.

Collapse
 
naviphuc profile image
hoang trong phuc

But I cannot connect with device in external network. I just only connect with same wifi

Thread Thread
 
rynobax_7 profile image
Ryan Baxley

No, you can connect any internet connected device with any other internet connected device, even if they are on different networks.

Collapse
 
lucpattyn profile image
Mukit, Ataul

Actually you meant iOS 11 is the first to support webrttc in its browser (safari). You could develop native apps with webrtc in previous versions too.

Collapse
 
maanshanguider profile image
MaAnShanGuider

cool article.

Collapse
 
janhavi97 profile image
Janhavi Patil

I just wanted to use this game for my project. Can i know how can i access this??

Collapse
 
rynobax_7 profile image
Ryan Baxley

You can find the source code here

Collapse
 
janhavi97 profile image
Janhavi Patil

Sorry i got code but hpw to execute it??

Collapse
 
rynobax_7 profile image
Ryan Baxley

The app was bootstrapped with create-react-app, so you can start it with npm install followed by npm start.

Collapse
 
umesh1134 profile image
Umesh Kumar

hi ryan, how to handle the incoming stream from peer. want to prompt the user to accept/reject before getting the audio/video/data stream.