Multiplayer: Peer to Peer (P2P)

(return)  (springs & pucks)
data flow
Figure 1. Socket.io connections via a local Node.js server

data flow
Figure 2. Direct P2P connections established with a remote
Node.js server

A P2P layer has been added to the multiplayer functionality for the demos. The socket.io connectivity described on the original multiplayer page is still in place and facilitates P2P and also acts as a backup if P2P connections can't be established.

P2P offers only a minor performance advantage when compared to the local Node.js server configuration represented in Figure 1. The time consumed in sending mouse and keyboard data through a middle-man is small when all computers are on the same local network.

A key drawback with this configuration is the required installation of a Node.js server onto the host computer. It's a simple install but any install can be a barrier to the recruiting process for a multiplayer game.

Prior to now, the demo page, in multiplayer mode, ran with the default Node.js server being a remote one, at Heroku (like Figure 1, but with the yellow circle far outside the local network). No install needed. But the time-of-flight lag, back and forth to Heroku, is quite noticeable especially in mouse movements projected to the host. This lag (latency) is a key drawback of the non-local node server. Even more so, this lag is a hindrance to any kind of synchronized play between users on different networks.

An alternative to synchronizing two physics engines is to stream (P2P) the rendered result from the host's canvas out to a video element on the client page. Client mouse movements (over the video element) and keyboard events are sent to the host, interacting with the host physics engine. Using only one engine, two users can play in different physical rooms, networks, states, countries...

As the educational (classroom) applications of this site are considered, the advantages of P2P connections become clear:

And so began a time of wandering through the online documentation for WebRTC, a P2P protocol that is native now in most browsers.

WebRTC

The diagram in Figure 2 shows the current default configuration for multiplayer. The thin lines represent the socket.io connections that continue to support the chat feature and now the signaling process for the WebRTC connections.

Signaling is the process by which the clients and host exchange their network configuration data needed for NAT traversal, etc. It depends on having a server that is accessible from all computers involved. That means a public server, like the default Heroku server used here, or a computer inside your local network.

Figure 2 shows an example where host and clients are on the same local network. This reflects the same-physical-room setup that has been discussed here; everyone looking at the same display, maybe a projection. Video-stream functionality has been recently added (using the P2P connection of WebRTC). This allows players to be in different rooms, states, countries.

The Node server is shown at a distant location in Figure 2. Since its role now is mainly to establish the P2P connection, its location can be anywhere without reducing performance.

The Heroku server has Node.js running on a free account. By default, the host and client pages use this distant Heroku server for signaling activities. If the default server is used, which is done by leaving the "server" field set to timetocode.herokuapp.com, there is no installation task needed to try the P2P and multiplayer features here. If your network environment/security blocks the WebRTC signaling process, you can try installing the node-server code on a local server following the instructions here.

The following ping-test result is along the P2P connection (local WiFi) to the u6 client. Fast! Even a little faster than the result from a local node server.

ping

A few useful things to know:

All the network magic is contained in three files:

An outline of the P2P connection process as commented in the code (hostAndClient.js):

   Host: 
      starting with the "Create" button, runs "connect_and_listen" to establish socket.io connection with server. 
      io.connect and init_socket_listeners are run
   Server: 
      on connection, listeners are initialized on the server for that connection
      server automatically sends to "connect" listener on host
   Host: 
      from "connect" listener, host sends to "roomJoin" listener on server with room name as indicated on the host's page
   Server: 
      from "roomJoin" listener, server sets up the room, adds the host to the room as host, 
      and sends to "room-joining-message" listener on host: (1) you joined room, (2) you are host
   Client (and Server): 
      Starting with the "Connect" button, the client makes a similar (like the host's above) exchange with the server to establish 
      the socket.io connection and join the room as a member (not as host).
      The server sends to the "your name is" listener on the client where the rtc object is instantiated and openDataChannel is run.
      openDataChannel( false, clientName); // client run this as NOT the initiator 
         the guts of the WebRTC is instantiated ----> new RTCPeerConnection
         onicecandidate event handler established which uses the "signaling message" listeners
         ondatachannel handler is defined (responds to data channel initiation by the host)
   Server: 
      At the end of the client connection process an additional step is done in the server's "roomJoin" listener. 
      The server sends the new client name to the "new-game-client" listener on the host. 
   Host: 
      "new-game-client" listener instantiates a new Client object where the host-side of each P2P connection, for each client, exists:
         gW.createNetworkClient(...)
      Then these two calls start the P2P connection process:
         openDataChannel( true, clientName); // host opens as the initiator
            the guts of the WebRTC is instantiated ----> new RTCPeerConnection
            onicecandidate event handler established
               handler will send ICE info to the server's "signaling message" listener, then relayed to the client's listener.
            createDataChannel: starts the datachannel on the host; client responds with its ondatachannel handler
         createOffer
            an offer is forwarded to the client using the "signaling message" listeners
   Client:
      "signaling message" listener
         handleOffer
            cl.rtc.pc.setRemoteDescription
            cl.rtc.pc.createAnswer
            cl.rtc.pc.setLocalDescription( answer);
            an answer is sent to the Host via "signaling message" listeners
   Host:
      "signaling message" listener
         handleAnswer
            cl.rtc.pc.setRemoteDescription( answer)

Resources that were helpful: