Phone to Browser HTML5 Gaming Using Node.js and Socket.io

Posted by | Development | 10 Replies.

ss

With the advent of new Chrome Experiments such as Maze and RollIt, we’ve been discovering a new way of HTML5 gaming – using our smartphones as controllers to play browser games.

While this seems quite magical up front, the inner workings are actually quite simple.  In this blog, I’ll be showing how to set up this type of connection using a few different technologies:

  • Node.js – To serve our app
  • Express - Web App Framework for Node.js
  • Socket.io - To create the websocket connection between our browser and phone
  • HexGL - The open-source HTML5/WebGL racing game by Thibaut Despoulain.  We will add the phone-controller feature.

This tutorial assumes an Android/iOS device, and CentOS as our base environment. It will also assume we are running Chrome or Firefox browsers to play the game.

If you’d like to skip ahead, the demo is available here.
The full source code is also available on GitHub

Table of contents:

  1. Set Up Environment
  2. Create the HTTP Server
  3. Create the serverside websocket interface
  4. Create the clientside websocket interface
  5. Implementation and Conclusion

Set Up Environment

We begin by setting up Node.js :

git clone https://github.com/joyent/node.git
cd node
git checkout v0.10.12-release
./configure
make
sudo make install

Notes:

  • We git checkout the latest release branch.  All branches can be found by doing git branch -a
  • If you get an error while trying to execute ./configure, you probably need Python 2.7 installed
  • There are also Windows and Mac installers located here

Once installed, we now have access to npm, the Node.js package manager, and can install Socket.io:

npm install socket.io

We are now ready to set up our project.

Let’s go ahead and download HexGL:

git clone https://github.com/BKcore/HexGL.git

Navigate into HexGL and create a “server” directory.
Create a file named “package.json”. This will serve as the config/metadata for our project so Node.js can serve it.

HexGL/server/package.json

{
    "name": "HexGLPhoneController",
    "version": "0.0.1",
    "description": "Control HexGL with phone tilt",
    "dependencies": {
        "socket.io": "latest",
        "express": "3.x"
    },
    "author": "Robert Devitt, rdevitt@artandlogic.com"
}

The important things here are the dependencies. Socket.io will establish the phone-to-browser connection, and Express is a web app framework that will allow us to serve our app easily.

Now, while in the server directory, have Node.js initialize the project:

npm install

This will create a “node_modules” folder which contains the dependencies.

We are now ready to begin coding!

Create the HTTP Server

First, we’ll set up the HTTP Server. Create a file named “server.js”:
HexGL/server/server.js

// Dependencies
var express = require('express');
var http = require('http');
var io = require('socket.io')
var crypto = require('crypto');

// Set up our app with Express framework
var app = express();

// Create our HTTP server
var server = http.createServer(app);

// Configure the app's document root to be HexGl/
app.configure(function() {
   app.use(
      "/",
      express.static("../")
   );
});

// Tell Socket.io to pay attention
io.listen(server);

// Tell HTTP Server to begin listening for connections on port 3250
server.listen(3250);

Now would be a good time to test if our server is working! Start the server by simply executing:

node server.js

You should see a simple message:
info – socket.io started

Ensure that the HexGL game comes up by opening http://yourserver.com:3250/ in your browser

Create the serverside websocket interface

We’ll now set up the websocket part of our Node.js server. We have several requirements:

  • Identify client types (is this client the browser “game” or the phone “controller”?)
  • Connect the controller client to the game client
  • Send controller data to the game client

To link the browser and the phone, the user will need to first open the game in the browser, which will generate a code that the user will then enter into their phone.  Once authenticated, the game will automatically start.

The Maze Chrome Experiment uses a fancier version of this, which automatically appends the code into the URL, and using Chrome tab-sync the user can open the same tab on their phone without having to enter a code.  For the purposes of this tutorial though, we’ll keep it simple.

The server-side socket conversation looks like this:
Client: Connect
Server: Welcome!
______________________
Client: My device type is 'game'
Server: Ok, here is your game code, show it to the user.
______________________
Client: My device type is 'controller' and my game code is ####
Server: Ok, your game code is valid, begin playing!
Client: Accelerate!
Server: Telling the game client to accelerate!
...

After a client is connected, only the controller client will need to send commands. The game client will only receive.

Working in the same HexGL/server/server.js file:

// Sockets object to save game code -> socked associations
var socketCodes = {};

// When a client connects...
io.sockets.on('connection', function(socket)
{
   // Confirm the connection
   socket.emit("welcome", {});

   // Receive the client device type
   socket.on("device", function(device)
   {
      // if client is a browser game
      if(device.type == "game")
      {
         // Generate a code
         var gameCode = crypto.randomBytes(3).toString('hex');

         // Ensure uniqueness
         while(gameCode in socketCodes)
         {
            gameCode = crypto.randomBytes(3).toString('hex');
         }

         // Store game code -> socket association
         socketCodes[gameCode] = io.sockets.sockets[socket.id];
         socket.gameCode = gameCode

         // Tell game client to initialize
         //  and show the game code to the user
         socket.emit("initialize", gameCode);
      }
      // if client is a phone controller
      else if(device.type == "controller")
      {
         // if game code is valid...
         if(device.gameCode in socketCodes)
         {
            // save the game code for controller commands
            socket.gameCode = device.gameCode

            // initialize the controller
            socket.emit("connected", {});

            // start the game
            socketCodes[device.gameCode].emit("connected", {});
         }
         // else game code is invalid,
         //  send fail message and disconnect
         else
         {
            socket.emit("fail", {});
            socket.disconnect();
         }
      }
   });

   // send accelerate command to game client
   socket.on("accelerate", function(data)
   {
      var bAccelerate = data.accelerate;
      if(socket.gameCode && socket.gameCode in socketCodes)
      {
         socketCodes[socket.gameCode].emit("accelerate",
          bAccelerate);
      }
   });

   // send turn command to game client
   socket.on("turn", function(degrees)
   {
      if(socket.gameCode && socket.gameCode in socketCodes)
      {
         socketCodes[socket.gameCode].emit("turn", degrees);
      }
   });
});

// When a client disconnects...
io.sockets.on('disconnect', function(socket)
{
   // remove game code -> socket association on disconnect
   if(socket.gameCode && socket.gameCode in socketCodes)
   {
      delete socketCodes[socket.gameCode];
   }
});

Now that we’ve set up the server-side socket event handling, we’ll need to set up the client-side.

Create the clientside websocket interface

To keep this tutorial concise, I’ll explain the Socket.io part of the code. If you’d like to view other code that updates the phone interface and emulates controls, you may view that code in the full source.

Create a phoneController.js file to hold our client-side logic -
HexGL/js/phoneController.js

var server = 'http://yourserver.com:3250';

var initPhoneController = function()
{
   var controller = $("#controller");
   var gameConnect = $("#gameConnect");
   var wheel = $("#wheel");
   var status = $("#status");

   // If client is a mobile device
   if(  /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent) )
   {
      // Show the controller ui with gamecode input
      controller.show();

      // When connect is pushed, establish socket connection
      $("#connect").click(function()
      {
         var gameCode = $("#socket input").val();
         var socket = io.connect(server);

         // When server replies with initial welcome...
         socket.on('welcome', function(data)
         {
            // Send 'controller' device type with our entered game code
            socket.emit("device", {"type":"controller", "gameCode":gameCode});
         });

         // When game code is validated, we can begin playing...
         socket.on("connected", function(data)
         {
            // Hide game code input, and show the vehicle wheel UI
            $("#socket").hide();
            wheel.show();

            // If user touches the screen, accelerate
            document.addEventListener("touchstart", function(event){
               socket.emit("accelerate", {'accelerate':true});
            }, false);

            // Stop accelerating if user stops touching screen
            document.addEventListener("touchend", function(event){
               socket.emit("accelerate", {'accelerate':false});
            }, false);

            // Prevent touchmove event from cancelling the 'touchend' event above
            document.addEventListener("touchmove", function(event){
               event.preventDefault();
            }, false);

            // Steer the vehicle based on the phone orientation
            window.addEventListener('deviceorientation', function(event) {
               var a = event.alpha; // "direction"
               var b = event.beta; // left/right 'tilt'
               var g = event.gamma; // forward/back 'tilt'

               // Tell game to turn the vehicle
               socket.emit("turn", b);
            }, false);
         });

         socket.on("fail", function()
         {
            status.html("Failed to connect");
         });
      });

   }
   // continued below ...

A note about the HTML5 “deviceorientation” event above. For more information on this event and its properties, see here

   // ... continued from above
   // If client is browser game
   else
   {
      var socket = io.connect(server);

      // When initial welcome message, reply with 'game' device type
      socket.on('welcome', function(data)
      {
         socket.emit("device", {"type":"game"});
      });

      // When we receive our game code, show the user
      socket.on("initialize", function(gameCode)
      {
         $("#gameConnect").show();
         $("#gameCode").html(gameCode);
      });

      // When the user inputs the code into the phone client,
      //  we become 'connected'.  Start the game.
      socket.on("connected", function(data)
      {
         $("#gameConnect").hide();
         $("#status").hide();

         // Start HexGL
         init();
      });

      // When the phone is turned, turn the vehicle
      socket.on('turn', function(turn)
      {
         // Emulate turn controls
      });

      // When the phone is touched, accelerate the vehicle
      socket.on("accelerate", function(accelerate)
      {
         // Emulate accelerate controls
      });
   }
};

Implementation and Conclusion

Finally we can implement our code into HexGL. For details on how this is done, see the following files:

Also make sure you include socket.io.js to have access to Socket.io clientside:

<script type="text/javascript" src="/socket.io/socket.io.js"></script>

In conclusion, the magic is actually websocket work in combination with new HTML5 phone event support like DeviceOrientation.

Node.js will generate a unique id that is associated with a socket connection to the browser game.  Once entered into your phone, a “route” is made through the server and all commands from your phone are sent to the browser game.

Node.js and Socket.io make this very easy to do.

I hope this tutorial was clear and opened your mind to more ideas and possibilities. Post questions or comments below!