Thursday, August 4, 2016

Taming WebRTC with PeerJS: Making a Simple P2P Web Game

WebRTC is a technology that enables real-time communication between web browsers. It is relatively new, and the API definition is still considered to be a draft. Coupled with the fact that WebRTC is not supported by all major web browsers yet, (and among the ones that do, some of them do not support every feature of this technology), this makes it relatively difficult to use WebRTC for any mission critical applications. Or so you would think!
Connect Four over WebRTC using PeerJS: Look ma, no server!Connect Four over WebRTC using PeerJS: Look ma, no server!
Since it was first introduced by Google in May 2011, WebRTC has been used in many modern web applications. Being a core feature of many modern web browsers, web applications can seamlessly take advantage of this technology to deliver improved user experience in many ways. Video streaming or conferencing applications that don’t require bloated browser plugins, and can take advantage of peer-to-peer (P2P) networks (while not transmitting every bit of data through some server) is only a part of all the amazing things that can be achieved with WebRTC.
In this article, we will take a look at how WebRTC can be used to make a simple P2P web game of Connect Four. To work around the various rough edges and implementation differences of WebRTC, we will use an amazing JavaScript library: PeerJS.

Data over WebRTC

Before we start, it is important to understand that WebRTC is not all about transmitting audio and video streams. It also provides support for P2P data channels. These channels come in two variations: reliable and unreliable. As one may guess, reliable data channels guarantee that messages are delivered and they are delivered in order, while unreliable channels provide no such guarantees.
WebRTC infrastructure - an ocean of acronymsWebRTC infrastructure - an ocean of acronyms
Moreover, WebRTC data channels require no special infrastructure setup, other than what is needed by a typical WebRTC peer connection: a signaling server to coordinate the connection between peers, a STUN server to figure out public identity of the peers, and optionally a TURN server to route messages between peers if a direct connection between peers cannot be established (for example when both peers are behind NATs). If these acronyms sound familiar, it is because WebRTC repurposes existing technologies wherever possible.
This opens the door to a lot more use cases of WebRTC, including but not limited to multiplayer games, content delivery, and file sharing. Again, all without the need of any intermediary server and hence with lower latencies.
In our simple web game, we will use a data channel between two web browsers to communicate player moves back-and-forth.

Meet PeerJS

PeerJS takes the implementation of WebRTC in your browser and wraps a simple, consistent, and elegant APIaround it. It plugs various holes in WebRTC implementation of earlier browsers. For example, in Chrome 30 or older, only unreliable data channels were available. PeerJS, if configured to use reliable data channels, would use a shim for those older browsers. Although this wouldn’t be as performant as native implementation of reliable channels, it would still work.
With PeerJS, identifying peers is even simpler. Every peer is identified using nothing but an ID. A string that the peer can choose itself, or have a server generate one. Although WebRTC promises peer-to-peer communication, you still need a server anyway to act as a connection broker and handle signaling. PeerJS provides an open source implementation of this connection broker server PeerJS Server (written in Node.js), in case you do not want to use their cloud-hosted version (which is free right now, and comes with some limitations).

Connect Four Goes P2P

Now that we have a source of confidence for working with WebRTC, i.e PeerJS, let us start by creating a simple Node.js/Express application.
npm init
npm install express --save
npm install jade --save
npm install peer --save
We will use this only to host PeerJS Server, and serve a page and front-end assets. We will need to serve only a single page, and this will contain two sections: a plain main menu, and a 7-by-6 Connect Four grid.

PeerJS Server

Hosting our own PeerJS Server is really easy. The official repository on GitHub even has a one-click button to deploy an instance of PeerJS Server to Heroku.
In our case, we just want to create an instance of ExpressPeerServer in our Node.js application, and serve it at “/peerjs”:
var express = require('express')
var app = express()
// … Configure Express, and register necessary route handlers
srv = app.listen(process.env.PORT)
app.use('/peerjs', require('peer').ExpressPeerServer(srv, {
 debug: true
}))

PeerJS Client

With PeerJS Server up and running, we move on to the client side. As discussed earlier, PeerJS identifies peers with unique IDs. These IDs can be generated by PeerServer for every peer automatically, or we can pick one for every peer while instantiating Peer objects.
var peer = new Peer(id, options)
Here, id can be omitted altogether if we want the server to generate one for us. In our case, that is what we will want to do. PeerServer will ensure that the IDs it gives out are unique. The second argument, options, is usually an object containing key (the API key, if you are using cloud-hosted PeerServer, or hostportpath, etc in case you are hosting the PeerServer yourself).
var peer = new Peer({
 host: location.hostname,
 port: location.port || (location.protocol === 'https:' ? 443 : 80),
 path: '/peerjs'
})
In order to establish a connection between two PeerJS peers, one of the peers must know the ID of the other peer. For the sake of keeping things simple, in our implementation of Connect Four over WebRTC, we will require the player starting the game to share his peer ID with his opponent. With the destination peer ID known, a simple call to peer.connect(destId) is all that we will need:
var conn = peer.connect(destId)
Both the Peer object and the DataConnection object returned by peer.connect(destId) emit some really useful events that are worth listening on. For the purposes of this tutorial, we are particularly interested about the ‘data’ event of DataConnection object and ‘error’ events of both objects.
In order to send data to the other end of the connection, simply invoke conn.send(data):
conn.send('hello')
Although a bit overkill for our needs here, PeerJS transmits data between peers after encoding them in BinaryPack format. This allows peers to communicate strings, numbers, arrays, objects, and even blobs.
To receive incoming data, simply listen for ‘data’ event on conn:
conn.on(‘data’, function(data) {
 // data === 'hello'
})
And that is pretty much all we need!

Game Logic

The first player, one who starts a game, is shown his peer ID as generated by PeerJS which they can share with their opponent. Once an opponent joins the game using the first player’s peer ID, the first player is allowed to make a move.
Connect Four, being a game of simple rules and mechanics, has only one type of move: each player, in turn, must pick a column and drop a disc into it. This means that all a peer needs to communicate is the column number in which the current player has chosen to drop his disc in. We will transmit this information as an array with two elements: a string ‘move’, and a number - 0-based index of the column from the left.
Every time a player clicks on a column:
if(!turn) {
 // it is not the current player’s turn
 return
}

var i
// i = chosen column index
if(grid[i].length == 6) {
 // the column doesn’t have any more space available
 return
}

// track player’s move locally
grid[i].push(peerId)

// end current player’s turn
turn = false

conn.send(['move', i])
After sending this move data to the opponent, we update the game’s state locally. This includes determining if the current player has won, or if the game has ended in a draw.
On the receiving end of this move data:
if(turn) {
 // ignore incoming move data when it is the current player's turn
 return
}

var i = data[1]
if(grid[i].length == 6) {
 // ignore incoming move data when it is invalid
 return
}

// track opponent’s move locally
grid[i].push(opponent.peerId)

// activate current player’s turn
turn = true
And naturally, after this we update the game’s state locally, determine if the opponent has won or if the game has ended in a draw.
Notice how we need to perform sanity checks on incoming data. This is important since with WebRTC-based games, we do not have intermediary server and server-based game logic validating the move data.
To keep the snippets simple, lines of code that update the UI have been omitted. You can find the full source code for the client-side JavaScript here.

Connecting It All

To combine it all, we create a simple page with two sections. On page load, the section containing the main menu is shown, the section containing the game grid is kept hidden.
section#menu
 div.animated.bounceIn
  div
   h1 Connect Four
   br
   div.no-support(style='display: none;')
    div.alert.alert-warning
     p Unfortunately, your web browser does not <a href="http://iswebrtcreadyyet.com">support WebRTC</a>
   div
    a.btn.btn-primary.btn-lg(href='#start') Start
    | &nbsp;
    a.btn.btn-default.btn-lg(href='#join') Join

section#game(style='display: none;')
 div
  div
   h1 Connect Four
   br
   table.table.grid
    tbody
     for i in [0, 1, 2, 3, 4, 5]
      tr
       for j in [0, 1, 2, 3, 4, 5, 6]
        td
         div.slot
   br
   div.alert.alert-info
    p 
Making these DOM elements look pretty is beyond the scope of this tutorial. Hence we will resort to our trusted companion Bootstrap and do some light styling over it.
As the first player clicks on the “Start” button, the game grid is revealed along with the player’s peer ID. The player can then share this peer ID with their opponent.
Start a new game, and share your peer ID as generated by PeerJS
The second player can click then click on the “Join” button, enter the first player’s peer ID, and begin the game.
Use your opponent's peer ID to join a game

Trying It Out

You can try out this example application at https://arteegee.herokuapp.com.
Or, you can clone the repository from GitHub, install NPM dependencies, and try it out locally:
git clone https://github.com/hjr265/arteegee.git 
cd arteegee
npm install
PORT=5000 npm start
Once the server is running, you can point your web browser to http://localhost:5000, start a game from one tab, and join from another tab (or even a different WebRTC capable web browser) using the peer ID.
You can open your web browser’s console to see some debug information, as in this example application, PeerJS client has been configured to perform verbose logging.

But It Doesn’t Work for Me!

There are two major reasons why this game may not work on your computer.
It is possible that you are using a web browser which doesn’t support the necessary WebRTC APIs yet. If that is the case, you may want to try a different browser - one that supports WebRTC and data channels.
If you are using a modern web browser with WebRTC support, then there is the chance that you are behind some network infrastructure which WebRTC cannot penetrate. Ideally this issue can be easily addressed with a TURN server, but since the example application is not using one, it won’t work when both you and your opponent are behind NATs.
This article was written by Mahmud Ridwan, a Toptal Python developer.

No comments:

Post a Comment