1. So, what is this about?

I want to show you a simple life hack: porting your favorite JavaScript game to your Arduino board by using PHPoC shield. Just a few steps, and you can enjoy any games on your board.

There are lots of JavaScript games available on the Internet, open and free for you to download. And you know what? Since PHPoC shield for Arduino can let your IoT board become embedded Web server, you can host any JavaScript Web game on your IoT board. Note that if the file size is too large, you may need to store your files on an online CDN instead of uploading it to your board.

Let's have "fun" with the Floppy bird this time (Yep, struggling yourself with that annoying bird is really funny).

Basically, I found an open-source JavaScript code for Floppy bird. The remaining job is quite easy. Since the size of the code is not too large (< 250 KB), I can upload them to PHPoC shield. A little bit modification needed here, I add an event listener on JS code so that the game can be processed upon receiving WebSocket data from the server.

On Arduino side, I just make it send data to WebSocket client if a button is pressed. That's how I made a game console for this game. If you continue reading this post, you can see more detail in the latter part.

And actually you can choose any hardware component. For example, a touch sensor may be valid for this game. And for another game? It's totally up to you. Overall, the hardware part for game console won't be hard for you.



2. Things used in this project

Arduino UNO × 1

Button × 1

Grove expansion board for Arduino x1

PHPoC WiFi Shield for Arduino × 1


3. Demo





4. Wiring
  • Stack PHPoC shield on Arduino.
  • Connect pin GND, VCC, and SIG of button to GND, 5V and D2 of Arduino, respectively. Or even easier, I just used Grove board for Arduino.
Wiring

5. Data flow


Arduino ---> PHPoC Shield ---> Web browser

User interacts with a button. The input state of the button is used to control the bird.

Arduino with PHPoC shield act as a WebSocket server, and the browser is a client.

I just use a simple button as a game console. Just a basic digital IO stuff, if the button is pressed, Arduino will send the signal to PHPoC shield.

When receiving the value, PHPoC Shield sends it to Web browser via WebSocket. I modified JS source code to make the game updated upon having incoming WS data from the server (means button pressed).


6. Things to do
  • Set up WiFi connection for PHPoC shield (SSID and password)
  • Upload new UI to PHPoC shield
  • Upload Arduino code

Set up WiFi connection for PHPoC Shield

See this instruction.

Upload new Web User Interface to PHPoC Shield
  • Download PHPoC source code remote_racing_game.php.

Upload Arduino Code
  • Upload Arduino code to Arduino
And Finally

  • Click serial button on Arduino IDE to get the IP address.
  • Open web browser, type http://relace_ip_address_here/floppy_bird.php
  • Use the button to start and enjoy the game. You can also use touch or keyboard, mouse in this game, since they are originally supported.

7. Source code


Floppy_Bird.ino : Arduino code

Code:
#include <Phpoc.h>
#include <PhpocClient.h>
#include <PhpocServer.h>


const int buttonPin = 2;
int buttonState = 0; 

PhpocServer server(80);

void setup() {
  // put your setup code here, to run once:
    pinMode(buttonPin, INPUT); 
    Serial.begin(9600);
    while(!Serial)
        ;

    Phpoc.begin(PF_LOG_SPI | PF_LOG_NET);

    server.beginWebSocket("game");

    Serial.print("WebSocket server address : ");
    Serial.println(Phpoc.localIP());

}

void loop() {
  // put your main code here, to run repeatedly:
    buttonState = digitalRead(buttonPin);
    if (buttonState == HIGH) {

      server.write("Press", 5);
      delay(300);    
    }
}
Basically the modifications I made are in two files:

main.js : JS code to handle interacting event, need to be loaded to PHPoC shield

Code:
/*
   Modified by Homer
   https://www.hackster.io/Homer
   floppybird - main.js

   Thanks to the work of Nebez Briefkani
   http://github.com/nebez/floppybird/

*/

var debugmode = false;

var states = Object.freeze({
   SplashScreen: 0,
   GameScreen: 1,
   ScoreScreen: 2
});

var currentstate;

var gravity = 0.25;
var velocity = 0;
var position = 180;
var rotation = 0;
var jump = -4.6;
var flyArea = $("#flyarea").height();

var score = 0;
var highscore = 0;

var pipeheight = 90;
var pipewidth = 52;
var pipes = new Array();

var replayclickable = false;

//sounds
var volume = 30;
var soundJump = new buzz.sound("sfx_wing.ogg");
var soundScore = new buzz.sound("sfx_point.ogg");
var soundHit = new buzz.sound("sfx_hit.ogg");
var soundDie = new buzz.sound("sfx_die.ogg");
var soundSwoosh = new buzz.sound("sfx_swooshing.ogg");
buzz.all().setVolume(volume);

//loops
var loopGameloop;
var loopPipeloop;

$(document).ready(function() {
   if(window.location.search == "?debug")
      debugmode = true;
   if(window.location.search == "?easy")
      pipeheight = 200;

   //get the highscore
   var savedscore = getCookie("highscore");
   if(savedscore != "")
      highscore = parseInt(savedscore);

   //start with the splash screen
   showSplash();
});

function getCookie(cname)
{
   var name = cname + "=";
   var ca = document.cookie.split(';');
   for(var i=0; i<ca.length; i++) 
   {
      var c = ca[i].trim();
      if (c.indexOf(name)==0) return c.substring(name.length,c.length);
   }
   return "";
}

function setCookie(cname,cvalue,exdays)
{
   var d = new Date();
   d.setTime(d.getTime()+(exdays*24*60*60*1000));
   var expires = "expires="+d.toGMTString();
   document.cookie = cname + "=" + cvalue + "; " + expires;
}

function showSplash()
{
   currentstate = states.SplashScreen;

   //set the defaults (again)
   velocity = 0;
   position = 180;
   rotation = 0;
   score = 0;

   //update the player in preparation for the next game
   $("#player").css({ y: 0, x: 0});
   updatePlayer($("#player"));

   soundSwoosh.stop();
   soundSwoosh.play();

   //clear out all the pipes if there are any
   $(".pipe").remove();
   pipes = new Array();

   //make everything animated again
   $(".animated").css('animation-play-state', 'running');
   $(".animated").css('-webkit-animation-play-state', 'running');

   //fade in the splash
   $("#splash").transition({ opacity: 1 }, 2000, 'ease');
}

function startGame()
{
   currentstate = states.GameScreen;

   //fade out the splash
   $("#splash").stop();
   $("#splash").transition({ opacity: 0 }, 500, 'ease');

   //update the big score
   setBigScore();

   //debug mode?
   if(debugmode)
   {
      //show the bounding boxes
      $(".boundingbox").show();
   }

   //start up our loops
   var updaterate = 1000.0 / 60.0 ; //60 times a second
   loopGameloop = setInterval(gameloop, updaterate);
   loopPipeloop = setInterval(updatePipes, 1400);

   //jump from the start!
   playerJump();
}

function updatePlayer(player)
{
   //rotation
   rotation = Math.min((velocity / 10) * 90, 90);

   //apply rotation and position
   $(player).css({ rotate: rotation, top: position });
}

function gameloop() {
   var player = $("#player");

   //update the player speed/position
   velocity += gravity;
   position += velocity;

   //update the player
   updatePlayer(player);

   //create the bounding box
   var box = document.getElementById('player').getBoundingClien  tRect();
   var origwidth = 34.0;
   var origheight = 24.0;

   var boxwidth = origwidth - (Math.sin(Math.abs(rotation) / 90) * 8);
   var boxheight = (origheight + box.height) / 2;
   var boxleft = ((box.width - boxwidth) / 2) + box.left;
   var boxtop = ((box.height - boxheight) / 2) + box.top;
   var boxright = boxleft + boxwidth;
   var boxbottom = boxtop + boxheight;

   //if we're in debug mode, draw the bounding box
   if(debugmode)
   {
      var boundingbox = $("#playerbox");
      boundingbox.css('left', boxleft);
      boundingbox.css('top', boxtop);
      boundingbox.css('height', boxheight);
      boundingbox.css('width', boxwidth);
   }

   //did we hit the ground?
   if(box.bottom >= $("#land").offset().top)
   {
      playerDead();
      return;
   }

   //have they tried to escape through the ceiling? :o
   var ceiling = $("#ceiling");
   if(boxtop <= (ceiling.offset().top + ceiling.height()))
      position = 0;

   //we can't go any further without a pipe
   if(pipes[0] == null)
      return;

   //determine the bounding box of the next pipes inner area
   var nextpipe = pipes[0];
   var nextpipeupper = nextpipe.children(".pipe_upper");

   var pipetop = nextpipeupper.offset().top + nextpipeupper.height();
   var pipeleft = nextpipeupper.offset().left - 2; // for some reason it starts at the inner pipes offset, not the outer pipes.
   var piperight = pipeleft + pipewidth;
   var pipebottom = pipetop + pipeheight;

   if(debugmode)
   {
      var boundingbox = $("#pipebox");
      boundingbox.css('left', pipeleft);
      boundingbox.css('top', pipetop);
      boundingbox.css('height', pipeheight);
      boundingbox.css('width', pipewidth);
   }

   //have we gotten inside the pipe yet?
   if(boxright > pipeleft)
   {
      //we're within the pipe, have we passed between upper and lower pipes?
      if(boxtop > pipetop && boxbottom < pipebottom)
      {
         //yeah! we're within bounds

      }
      else
      {
         //no! we touched the pipe
         playerDead();
         return;
      }
   }


   //have we passed the imminent danger?
   if(boxleft > piperight)
   {
      //yes, remove it
      pipes.splice(0, 1);

      //and score a point
      playerScore();
   }
}

//Handle space bar
$(document).keydown(function(e){
   //space bar!
   if(e.keyCode == 32)
   {
      //in ScoreScreen, hitting space should click the "replay" button. else it's just a regular spacebar hit
      if(currentstate == states.ScoreScreen)
         $("#replay").click();
      else
         screenClick();
   }
});

//Handle mouse down OR touch start
if("ontouchstart" in window)
   $(document).on("touchstart", screenClick);
else
   $(document).on("mousedown", screenClick);


function screenClick()
{
   if(currentstate == states.GameScreen)
   {
      playerJump();
   }
   else if(currentstate == states.SplashScreen)
   {
       startGame();
       //replayGame();
   }
    else
    {
        replayGame();
    }
}

function playerJump()
{
   velocity = jump;
   //play jump sound
   soundJump.stop();
   soundJump.play();
}

function setBigScore(erase)
{
   var elemscore = $("#bigscore");
   elemscore.empty();

   if(erase)
      return;

   var digits = score.toString().split('');
   for(var i = 0; i < digits.length; i++)
      elemscore.append("<img src='font_big_" + digits[i] + ".png' alt='" + digits[i] + "'>");
}

function setSmallScore()
{
   var elemscore = $("#currentscore");
   elemscore.empty();

   var digits = score.toString().split('');
   for(var i = 0; i < digits.length; i++)
      elemscore.append("<img src='font_small_" + digits[i] + ".png' alt='" + digits[i] + "'>");
}

function setHighScore()
{
   var elemscore = $("#highscore");
   elemscore.empty();

   var digits = highscore.toString().split('');
   for(var i = 0; i < digits.length; i++)
      elemscore.append("<img src='font_small_" + digits[i] + ".png' alt='" + digits[i] + "'>");
}

function setMedal()
{
   var elemmedal = $("#medal");
   elemmedal.empty();

   if(score < 10)
      //signal that no medal has been won
      return false;

   if(score >= 10)
      medal = "bronze";
   if(score >= 20)
      medal = "silver";
   if(score >= 30)
      medal = "gold";
   if(score >= 40)
      medal = "platinum";

   elemmedal.append('<img src="medal_' + medal +'.png" alt="' + medal +'">');

   //signal that a medal has been won
   return true;
}

function playerDead()
{
   //stop animating everything!
   $(".animated").css('animation-play-state', 'paused');
   $(".animated").css('-webkit-animation-play-state', 'paused');

   //drop the bird to the floor
   var playerbottom = $("#player").position().top + $("#player").width(); //we use width because he'll be rotated 90 deg
   var floor = flyArea;
   var movey = Math.max(0, floor - playerbottom);
   $("#player").transition({ y: movey + 'px', rotate: 90}, 1000, 'easeInOutCubic');

   //it's time to change states. as of now we're considered ScoreScreen to disable left click/flying
   currentstate = states.ScoreScreen;

   //destroy our gameloops
   clearInterval(loopGameloop);
   clearInterval(loopPipeloop);
   loopGameloop = null;
   loopPipeloop = null;

   //mobile browsers don't support buzz bindOnce event
   if(isIncompatible.any())
   {
      //skip right to showing score
      showScore();
   }
   else
   {
      //play the hit sound (then the dead sound) and then show score
      soundHit.play().bindOnce("ended", function() {
         soundDie.play().bindOnce("ended", function() {
            showScore();
         });
      });
   }
}

function showScore()
{
   //unhide us
   $("#scoreboard").css("display", "block");

   //remove the big score
   setBigScore(true);

   //have they beaten their high score?
   if(score > highscore)
   {
      //yeah!
      highscore = score;
      //save it!
      setCookie("highscore", highscore, 999);
   }

   //update the scoreboard
   setSmallScore();
   setHighScore();
   var wonmedal = setMedal();

   //SWOOSH!
   soundSwoosh.stop();
   soundSwoosh.play();

   //show the scoreboard
   $("#scoreboard").css({ y: '40px', opacity: 0 }); //move it down so we can slide it up
   $("#replay").css({ y: '40px', opacity: 0 });
   $("#scoreboard").transition({ y: '0px', opacity: 1}, 600, 'ease', function() {
      //When the animation is done, animate in the replay button and SWOOSH!
      soundSwoosh.stop();
      soundSwoosh.play();
      $("#replay").transition({ y: '0px', opacity: 1}, 600, 'ease');

      //also animate in the MEDAL! WOO!
      if(wonmedal)
      {
         $("#medal").css({ scale: 2, opacity: 0 });
         $("#medal").transition({ opacity: 1, scale: 1 }, 1200, 'ease');
      }
   });

   //make the replay button clickable
   replayclickable = true;
}

$("#replay").click(function() {
    replayGame();
});

function replayGame()
{
    //make sure we can only click once
    if(!replayclickable)
        return;
    replayclickable = false;
   //SWOOSH!
   soundSwoosh.stop();
   soundSwoosh.play();

   //fade out the scoreboard
   $("#scoreboard").transition({ y: '-40px', opacity: 0}, 1000, 'ease', function() {
      //when that's done, display us back to nothing
      $("#scoreboard").css("display", "none");

      //start the game over!
      showSplash();
   });
}




function playerScore()
{
   score += 1;
   //play score sound
   soundScore.stop();
   soundScore.play();
   setBigScore();
}

function updatePipes()
{
   //Do any pipes need removal?
   $(".pipe").filter(function() { return $(this).position().left <= -100; }).remove()

   //add a new pipe (top height + bottom height  + pipeheight == flyArea) and put it in our tracker
   var padding = 80;
   var constraint = flyArea - pipeheight - (padding * 2); //double padding (for top and bottom)
   var topheight = Math.floor((Math.random()*constraint) + padding); //add lower padding
   var bottomheight = (flyArea - pipeheight) - topheight;
   var newpipe = $('<div class="pipe animated"><div class="pipe_upper" style="height: ' + topheight + 'px;"></div><div class="pipe_lower" style="height: ' + bottomheight + 'px;"></div></div>');
   $("#flyarea").append(newpipe);
   pipes.push(newpipe);
}

var isIncompatible = {
   Android: function() {
   return navigator.userAgent.match(/Android/i);
   },
   BlackBerry: function() {
   return navigator.userAgent.match(/BlackBerry/i);
   },
   iOS: function() {
   return navigator.userAgent.match(/iPhone|iPad|iPod/i);
   },
   Opera: function() {
   return navigator.userAgent.match(/Opera Mini/i);
   },
   Safari: function() {
   return (navigator.userAgent.match(/OS X.*Safari/) && ! navigator.userAgent.match(/Chrome/));
   },
   Windows: function() {
   return navigator.userAgent.match(/IEMobile/i);
   },
   any: function() {
   return (isIncompatible.Android() || isIncompatible.BlackBerry() || isIncompatible.iOS() || isIncompatible.Opera() || isIncompatible.Safari() || isIncompatible.Windows());
   }
};


floppy_bird.php: Server code, need to be loaded to PHPoC shield.


Code:
<!DOCTYPE html>
<html lang="en">
<!--
   Modified by Homer
   https://www.hackster.io/Homer
   floppybird - floppy_bird.php

   Thanks to the work of Nebez Briefkani
   http://github.com/nebez/floppybird/
-->

   <head>
      <title>Floppy Bird</title>
      <meta http-equiv="content-type" content="text/html; charset=utf-8" />
      <meta name="author" content="Nebez Briefkani" />
      <meta name="description" content="play floppy bird. a remake of popular game flappy bird using just html/css/js" />
      <meta name="keywords" content="flappybird,flappy,bird,floppybird,floppy,html,html  5,css,css3,js,javascript,jquery,github,nebez,brief  kani,nebezb,open,source,opensource" />
      <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" />

      <!-- Open Graph tags -->
      <meta property="og:title" content="Floppy Bird" />
      <meta property="og:description" content="play floppy bird. a remake of popular game flappy bird using just html/css/js" />
      <meta property="og:type" content="website" />
      <meta property="og:image" content="http://nebez.github.io/floppybird/assets/thumb.png" />
      <meta property="og:url" content="http://nebez.github.io/floppybird/" />
      <meta property="og:site_name" content="Floppy Bird" />

      <!-- Style sheets -->
      <link href="reset.css" rel="stylesheet">
      <link href="main.css" rel="stylesheet">
   </head>
   <body>
      <div id="gamecontainer">
         <div id="gamescreen">
            <div id="sky" class="animated">
               <div id="flyarea">
                  <div id="ceiling" class="animated"></div>
                  <!-- This is the flying and pipe area container -->
                  <div id="player" class="bird animated"></div>

                  <div id="bigscore"></div>

                  <div id="splash"></div>

                  <div id="scoreboard">
                     <div id="medal"></div>
                     <div id="currentscore"></div>
                     <div id="highscore"></div>
                     <div id="replay"><img src="replay.png" alt="replay"></div>
                  </div>

                  <!-- Pipes go here! -->
               </div>
            </div>
            <div id="land" class="animated"><div id="debug"></div></div>
         </div>
      </div>
      <div id="footer">
         <a href="http://www.dotgears.com/">original game/concept/art by dong nguyen</a>
         <a href="http://nebezb.com/">recreated by nebez briefkani</a>
         <a href="http://github.com/nebez/floppybird/">view github project</a>
         <a href="https://www.hackster.io/Homer">mod by Homer</a>
      </div>
      <div class="boundingbox" id="playerbox"></div>
      <div class="boundingbox" id="pipebox"></div>

      <script src="jquery.min.js"></script>
      <script src="jquery.transit.min.js"></script>
      <script src="buzz.min.js"></script>
      <script src="main.js"></script>
      <script>
              var ws_host_addr = "<?echo _SERVER("HTTP_HOST")?>";
            var ws = new WebSocket("ws://" + ws_host_addr + "/game", "text.phpoc");
            ws.onmessage = screenClick;
      </script>

   </body>
</html>

You can download the full source code (POC file) for PHPoC shield here.

Cheers.