© CRC Press.

Used with Permission.

DanNagle.com
@NagleCode
HTML5GameEnginesBook.com

Source Code

HTML5GameEnginesBook.com

Or clone from GitHub:


						git clone https://github.com/dannagle/HTML5GameEngines.git
					

HTML5 Pong

Hello World!


	<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Hello World</title>
</head>
<script>
function onload() {
	var canvas=document.getElementById("canvas");
	var context=canvas.getContext("2d");
    context.font = 'bold 30px Times';
 	context.fillText("Hello World!", canvas.width / 3, canvas.height / 3);
}
</script>
<body onload="onload()">
    <canvas id="canvas" width="320" height="480">Get a better browser!</canvas>
</body>
</html>
	

Demo

Better Hello World!


	<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Hello World</title>
</head>
<script>
function onload() {
	var canvas=document.getElementById("canvas");
	var context=canvas.getContext("2d");
    context.font = 'bold 30px Times';
	context.rotate(Math.PI / 4);
 	context.fillText("Hello World!", canvas.width / 4, 0);
	context.restore();
}
</script>
<body onload="onload()">
    <canvas id="canvas" width="320" height="480">Get a better browser!</canvas>
</body>
</html>
	

Demo

HTML5 Pong Architecture


	  function pongGame() {
    
    keybardEvents();
    computerAI();
    drawBackground();
    drawTopPaddle();
    drawBottomPaddle();
    drawBall();
    hitDetect();
    drawScore();
  }
  initGameObjects();
  window.setInterval(pongGame, 1000 /  GAME_FPS); //start game loop

}
	

HTML5 Pong Final


	<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>HTML5 Pong</title>
<script>

function onload() {
  
  var leftArrowHit = false;
  var rightArrowHit = false;
  
  document.onkeydown = function(event){
    console.log(event.keyCode); //what key did I press?
    if(event.keyCode == 39) //right arrow
    {
      rightArrowHit = true;
    }
    if(event.keyCode == 37) //left arrow
    {
      leftArrowHit = true;
      
    }
    if(event.keyCode == 32) //space
    {
      spaceBarHit = true;
     
    }
  }
  
  var GAME_FPS = 60;  
  var BACKGROUND_COLOR = '#dbdbdb';
  var PADDLE_WIDTH = 100;  
  var PADDLE_HEIGHT = 10;  
  var PADDLE_COLOR = '#000000';  
  var BALL_COLOR = '#000000';
  var BALL_RADIUS = 10;  
  
  var ball = new Object();
  var topPaddle = new Object();
  var bottomPaddle = new Object();
  
  
  var pointsPlayer = 0;
  var pointsComputer = 0;
  
    
  function initGameObjects()
  {
    ball['x'] = 40;
    ball['y'] = 240;
    ball['xspeed'] = 1;
    ball['yspeed'] = 3;
    
    topPaddle['x'] =200 + Math.round(Math.random() * 20);
    topPaddle['y'] =10; + Math.round(Math.random() * 20);
    bottomPaddle['x'] =100;
    bottomPaddle['y'] =460;
    
  }

  
  var canvas=document.getElementById("canvas");
  var context=canvas.getContext("2d");
  
  function drawScore()
  {
    if(pointsPlayer > 0 || pointsComputer > 0)
    {
      context.font = 'bold 15px Times';
      context.fillText("You:" + pointsPlayer +"  CPU:" + pointsComputer, 5, 12);
    }
  }
  
  function computerAI()
  {
    if(ball.yspeed < 0)
    {
      if(ball.x < (topPaddle.x + PADDLE_WIDTH / 2))
      {
        topPaddle.x--;
      } else {
        topPaddle.x++;
      }
    }
    
    if(topPaddle.x <= 0)
    {
      topPaddle.x = 0;
    }
    if(topPaddle.x >= (canvas.width - PADDLE_WIDTH))
    {
      topPaddle.x = canvas.width - PADDLE_WIDTH;
    }
    
  }
  
  function keybardEvents()
  {
    if(leftArrowHit)
    {
      bottomPaddle.x -= 3;
      leftArrowHit = false;
    }
    if(rightArrowHit)
    {
      bottomPaddle.x += 3;
      rightArrowHit = false;
    }
    if(bottomPaddle.x <= 0)
    {
      bottomPaddle.x = 0;
    }
    if(bottomPaddle.x >= (canvas.width - PADDLE_WIDTH))
    {
      bottomPaddle.x = canvas.width - PADDLE_WIDTH;
    }
 }
  
  function hitDetect()
  {
    
    if((ball.y +BALL_RADIUS)  >= (bottomPaddle.y))
    {
      if(bottomPaddle.x <= ball.x && ball.x <= (bottomPaddle.x + PADDLE_WIDTH))
      {
        console.log("bottomPaddle hit", ball.x, ball.y, bottomPaddle.x, bottomPaddle.y);
        ball.yspeed = ball.yspeed * -1;
        ball.y = bottomPaddle.y - BALL_RADIUS;
        return;
      }
      
    }


    if((ball.y -BALL_RADIUS) <= (topPaddle.y+PADDLE_HEIGHT))
    {
      if(topPaddle.x <= ball.x && ball.x <= (topPaddle.x + PADDLE_WIDTH))
      {
        console.log("topPaddle hit", ball.x, ball.y, topPaddle.x, topPaddle.y);
        ball.yspeed = ball.yspeed * -1;
        ball.y = topPaddle.y + BALL_RADIUS+PADDLE_HEIGHT;
        return;
      }
      
    }

    if((ball.x + BALL_RADIUS) >= canvas.width || (ball.x - BALL_RADIUS) <= 0 )
    {
      ball.xspeed = ball.xspeed * -1;
    }

    
    if(ball.y > (canvas.height + BALL_RADIUS))
    {
      pointsComputer++;
      initGameObjects();
      console.log("point for computer",pointsComputer);
    }
      
    if(ball.y < (0 -  BALL_RADIUS) )
    {
      pointsPlayer++;
      initGameObjects();
      console.log("point for player", pointsPlayer);
     
    }
    
  }

  
  function drawBackground()
  {
    context.fillStyle = BACKGROUND_COLOR;  //color for rectangle
    context.fillRect(0, 0, canvas.width, canvas.height); //draw rectangle
  }
  
  function drawBall()
  {
    context.strokeStyle = BALL_COLOR;  //color for ball
    context.beginPath();  //start a draw path
    ball.x += (1 * ball.xspeed);
    ball.y += (1 * ball.yspeed);
    context.arc(ball.x,ball.y,BALL_RADIUS,0,Math.PI*2,true); // draw ball
    context.fill(); //close path and fill in the shape
    
    
  }
  
  function drawTopPaddle()
  {
    context.fillStyle = PADDLE_COLOR;  //color for inside shapes
    context.fillRect(topPaddle.x,topPaddle.y,PADDLE_WIDTH,PADDLE_HEIGHT); // draw top paddle 
  }  
  
  function drawBottomPaddle()
  {
    context.fillStyle = PADDLE_COLOR;  //color for inside shapes
    context.fillRect(bottomPaddle.x,bottomPaddle.y,PADDLE_WIDTH,PADDLE_HEIGHT); // draw top paddle 
  }
  
  function pongGame() {
    
    keybardEvents();
    computerAI();
    drawBackground();
    drawTopPaddle();
    drawBottomPaddle();
    drawBall();
    hitDetect();
    drawScore();
  }
  initGameObjects();
  window.setInterval(pongGame, 1000 /  GAME_FPS); //start game loop

}


</script>
</head>

<body onload="onload()">
    <canvas id="canvas" width="320" height="480">Get a better browser!</canvas>
</body>
</html>
	

Demo

Missing in HTML5 Pong

  • Audio
  • Graphics
  • Touch
  • Mouse
 
 

Touch != Mouse

Chrome Firefox Mac Firefox Windows Safari IE
 

Audio Formats

Ogg MP3 WAV
FirefoxYes WindowsYes
Chrome Yes Yes Yes
Safari Yes Yes
Opera Yes Yes
IE Yes

jQuery Browser Detect


	// Do not do this!
if ($.browser.webkit) {
	alert( "This is Chrome or Safari!" );
	//We are WebKit
}
if ($.browser.mozilla) {
	alert( "this is Firefox!" );
	//We are Firefox
}
if ($.browser.msie) {
	alert( "this is IE!" );
	//We are IE
}	

Not supported in 1.9+

Modernizr


	if(Modernizr.canvas && Modernizr.canvastext) {
    // Continue loading canvas
    if(Modernizr.audio) {
        // Which audio format to use?
        if (Modernizr.audio.wav) {
            //Use wav
        }
        if (Modernizr.audio.mp3) {
            //Use mp3
        }
        if (Modernizr.audio.ogg) {
            //Use ogg
        }
        //No audio format worked? Perhaps use no audio.
    } else {
        // Audio not supported. Perhaps we should use the old <embed> method.
    }
} else {
    // Degrade gracefully. Perhaps use DOM or check for Flash?
}	

HTML5 Audio Test


	<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>HTML5 Audio Test</title>
  <script src="modernizr.custom.html5audio.js"></script>
  <script>
function loadAudio() {
    
  var audio = document.getElementById("modernizrload"); 
  var audiochoice = document.getElementById("modernizrchoice"); 
  if(Modernizr.audio) { 
    // Which audio format to use?
    if (Modernizr.audio.ogg) {
      //Use ogg
	  audio.innerHTML = "<audio src='illuminations-dannagle.ogg' controls ></audio>";
      audiochoice.innerHTML = "Loaded Ogg";
      return;
    } 
    if (Modernizr.audio.mp3) {
      //Use mp3
      audio.innerHTML = "<audio src='illuminations-dannagle.mp3' controls ></audio>";
      audiochoice.innerHTML = "Loaded MP3";
      return;
    } 
    //No audio format worked?
    audio.innerHTML = "Your browser does not support Ogg/MP3.";
    return;
  } else {
   // Audio not supported.
    audio.innerHTML = "Your browser does not support HTML5 Audio.";
    return;
  }
}
  </script>
<script>
</script>
<body onload="loadAudio()">

<br>Play 5s Wav<br>
<audio src="5seconds.wav" controls >
No Wav Support
</audio>

<br><hr><br>
<br>Play 10s MP3<br>
<audio src="10seconds.mp3" controls >
  No MP3 Support
</audio>

<br><hr><br>
<br>Play 15s Ogg<br>
<audio src="15seconds.ogg" controls >
  No Ogg Support
</audio>

<br><hr><br>
<br>Let browser choose preferred format to play (10s MP3 or 15s Ogg)<br>
<audio 	controls >
   <source src="15seconds.ogg" >
   <source src="10seconds.mp3" >
   No Ogg/MP3 Support
</audio>


<br><hr><br>
<br>Dynamically load supported format using Modernizr<br>
<div id="modernizrload"></div>
<div id="modernizrchoice"></div>

</body>
</html>
	

Demo

Crafty Pong

Crafty Game Engine

  • Lightweight
  • Syntax similar to jQuery
  • Complete engine: sound, graphics, hit detection, etc.
  • MIT or GPL license.

Hello Crafty


	<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Hello World</title>
</head>
<script src="modernizr.custom.52029.js"></script>
<script src="crafty-min_v0.5.3.js"></script>
<script>
function onload() {
    if(Modernizr.canvas && Modernizr.canvastext) {
        Crafty.init(320, 480);
        Crafty.background('#dbdbdb');
        Crafty.e("HelloWord, Canvas, 2D, Text")
            .attr({ x: 5, y: 5, w: 100, h: 20})
            .text("Hello Crafty!");
    } else {
        var yes = confirm("Download a better browser?");
        if(yes)
        {
            window.location = "http://google.com/chrome";
        }
    }
}
</script>
<style>
#cr-stage { border:1px solid black; margin:5px auto; }
</style>
<body onload="onload()" />
</html>
	

Demo

Crafty Engine

  • Entity
    • Paddles
    • Ball
    • Score
    • Wall
  • Components
    • SpriteAnimation
    • Collision
    • Mouse/Touch
  • Events
    • onHit
    • EnterFrame

Crafty Entities


	function onload() {

  var BACKGROUND_COLOR = '#dbdbdb';
  var PADDLE_WIDTH = 64;  
  var PADDLE_HEIGHT = 16; 
  var BALL_COLOR = '#000000';
  var BALL_RADIUS = 16;
  var PADDLE_COLOR = "#f0ff00"; 
  
  
  if(Modernizr.canvas && Modernizr.canvastext) {
    Crafty.init(320, 480);
    Crafty.background(BACKGROUND_COLOR);

      
    Crafty.load(["pong_sprites.png"
               ], function() {

        console.log("assets loaded");
        Crafty.scene("main"); //game loop
    });

 
  Crafty.sprite(16,"pong_sprites.png", {

      floor0: [0,0,1,1], //location=320,64, height=1, width=1
      floor1: [0,1,1,1],
      floor2: [1,1,1,1], 
      wall1: [6,0,1,1], 
      wall2: [7,0,1,1], 
      ball0: [2,1,1,1],
      toppaddle:    [0,2,4,1],  
      bottompaddle: [0,3,4,1]
  });
  
  Crafty.scene("main", function() {

    Crafty.e("2D, Canvas, wall, wall1").attr({x: 0, y: 0});
    Crafty.e("2D, Canvas, wall, wall2").attr({x: 320-16, y:0 });
    Crafty.e("2D, Canvas, floor0")
                .attr({x: 16, y: 16, z: -2});
            
    Crafty.e("topPaddle, 2D, Canvas, toppaddle")
                .attr({x: 100, y: 10, w: PADDLE_WIDTH, h: PADDLE_HEIGHT});

    Crafty.e("bottomPaddle, 2D, Canvas, bottompaddle")
                .attr({x: 100, y: 460, w: PADDLE_WIDTH, h: PADDLE_HEIGHT});
                
    Crafty.e("mouseTracking, 2D, Mouse, Touch, Canvas")
                .attr({ w:320, h:480, x:0, y:0 })
                .bind("MouseMove", function(e) 
                {
                    var bottomPaddle = Crafty("bottomPaddle"); //get bottomPaddle
                    bottomPaddle.x = Crafty.mousePos.x - bottomPaddle.w/2;
                });                 

    Crafty.e("scoreValue, 2D, Canvas, Text")
                .attr({x: 20, y: 0, w: PADDLE_WIDTH, h: PADDLE_HEIGHT,
                      pointsPlayer:0, pointsComputer:0
                      })
                .textColor('#FFFFFF');
                
   Crafty.e("gameBall, 2D, Canvas, Collision, SpriteAnimation, ball0")
                .attr({x: 100, y: 100, w: BALL_RADIUS, h: BALL_RADIUS,
                      xspeed: 2, yspeed: 4
                      })
                .animate('BallBlinking', 2,1,4) //setup animation
                .animate('BallBlinking', 5, -1); //start animation
  });


            
  } else {
    var yes = confirm("Download a better browser?");
    if(yes)
    {
      window.location = "http://google.com/chrome";
    }
  }
}
	

Demo

Crafty Pong Audio


	      
  Crafty.load(["pong_sprites.png",
               "lose_effect.mp3",
               "lose_effect.ogg",
               "win_effect.mp3",
               "win_effect.ogg",
               "hit.mp3",
               "hit.ogg",
               "hit2.mp3",
               "hit2.ogg"
               ], function() {
    
    Crafty.scene("main");
  });


  Crafty.scene("main", function() {

    Crafty.audio.add("hit", [
      "hit.mp3",
      "hit.ogg"
      ]);
    Crafty.audio.add("lose", [
      "lose_effect.mp3",
      "lose_effect.ogg"
      ]);
    Crafty.audio.add("win", [
      "win_effect.mp3",
      "win_effect.ogg"
      ]);
    Crafty.audio.add("hit2", [
      "hit2.mp3",
      "hit2.ogg"
      ]);
        ; 
                
    Crafty.e("gameBall, 2D, Canvas, Collision, SpriteAnimation, ball0")
                .attr({x: 100, y: 100, w: BALL_RADIUS, h: BALL_RADIUS,
                    xspeed: 2, yspeed: 4
                })
                .animate('BallBlinking', 2,1,4) //setup animation
                .animate('BallBlinking', 5, -1) //start animation
                .onHit('bottomPaddle', function () {
                    this.yspeed *= -1;
                    Crafty.audio.play("hit");
                    this.y = 460 - BALL_RADIUS;
                });
  });

	

onHit & EnterFrame


	            
    Crafty.e("topPaddle, 2D, Canvas, toppaddle")
                .attr({x: 100, y: 10, w: PADDLE_WIDTH, h: PADDLE_HEIGHT}) 
                .bind('EnterFrame', function () {
                    var gameBall = Crafty("gameBall"); //get gameBall
                    if(gameBall.yspeed < 0)
                    {
                      if(gameBall.x < (this.x + PADDLE_WIDTH / 2))
                      {
                        this.x--;
                      } else {
                        this.x++;
                      }
                    }
                    if(this.x <= 0)
                    {
                      this.x = 0;
                    }
                    if(this.x >= (320 - PADDLE_WIDTH))
                    {
                      this.x = 320 - PADDLE_WIDTH;
                    }
                }); 
                
    Crafty.e("mouseTracking, 2D, Mouse, Touch, Canvas")
              .attr({ w:320, h:480, x:0, y:0 })
              .bind("MouseMove", function(e) 
              {
                    var bottomPaddle = Crafty("bottomPaddle"); //get bottomPaddle
                    bottomPaddle.x = Crafty.mousePos.x - bottomPaddle.w/2;
                });                 

    Crafty.e("scoreValue, 2D, Canvas, Text")
                .attr({x: 20, y: 0, w: PADDLE_WIDTH, h: PADDLE_HEIGHT,
                      pointsPlayer:0, pointsComputer:0
                      })
                .textColor('#FFFFFF')
                .bind('EnterFrame', function () {
                      this.text("You:" + this.pointsPlayer +
                                "  CPU:" + this.pointsComputer);
                });
                
   Crafty.e("gameBall, 2D, Canvas, Collision, SpriteAnimation, ball0")
                .attr({x: 100, y: 100, w: BALL_RADIUS, h: BALL_RADIUS,
                      xspeed: 2, yspeed: 4
                      })
				.reel('BallBlinking', 100, [[2,1], [3,1], [4,1], [5,1]])  
                .animate('BallBlinking', -1) //start animation
                .bind('EnterFrame', function () {
                  
                  this.x += this.xspeed;
                  this.y += this.yspeed;

                 if(this.y > (480 + BALL_RADIUS))
                 {
                    var scoreValue = Crafty("scoreValue"); 
                    scoreValue.pointsComputer++;
                    Crafty.audio.play("lose");
                    this.x = 20 + Math.round(Math.random() * 100);
                    this.y = 100;
                    this.xspeed = 0 -this.xspeed;
                   
                 }
                   
                 if(this.y < 0 )
                 {
                    var scoreValue = Crafty("scoreValue"); 
                    scoreValue.pointsPlayer++;
                    Crafty.audio.play("win");
                    this.x = 20 + Math.round(Math.random() * 100);
                    this.y = 300;
                    this.xspeed = 0 -this.xspeed;
                 }
                                   
                })
              .onHit('bottomPaddle', function () {
                  this.yspeed *= -1;
                  Crafty.audio.play("hit");
                  this.y = 460 - BALL_RADIUS;
              })
              .onHit('topPaddle', function () {
                  Crafty.audio.play("hit");
                  this.yspeed *= -1;
                  this.y = 10+BALL_RADIUS;
              })
              .onHit('wall', function () {
                  Crafty.audio.play("hit2");
                  this.xspeed *= -1;
              });
  
	

Crafty Pong


	function onload() {

  var BACKGROUND_COLOR = '#dbdbdb';
  var PADDLE_WIDTH = 64;  
  var PADDLE_HEIGHT = 16; 
  var BALL_COLOR = '#000000';
  var BALL_RADIUS = 16;


  
  if(Modernizr.canvas && Modernizr.canvastext) {
    Crafty.init(320, 480);
    Crafty.background(BACKGROUND_COLOR);

//Crafty wants to center itself. Do not allow this.
var craftycanvas = document.getElementById("cr-stage");
craftycanvas.style.position="absolute";
craftycanvas.style.top="0px";

      
  Crafty.load(["pong_sprites.png",
               "lose_effect.mp3",
               "lose_effect.ogg",
               "win_effect.mp3",
               "win_effect.ogg",
               "hit.mp3",
               "hit.ogg",
               "hit2.mp3",
               "hit2.ogg"
               ], function() {
    console.log("assets loaded");

    //hack for IE9+
    window.setTimeout(function() {Crafty.scene("main");}, 500);
  });

 
  Crafty.sprite(16,"pong_sprites.png", {

      floor0: [0,0,1,1], //location=320,64, height=1, width=1
      floor1: [0,1,1,1],
      floor2: [1,1,1,1], 
      wall1: [6,0,1,1], 
      wall2: [7,0,1,1], 
      ball0: [2,1,1,1],
      toppaddle:    [0,2,4,1],  
      bottompaddle: [0,3,4,1]
  });
  
  Crafty.scene("main", function() {

    Crafty.audio.add("hit", [
      "hit.mp3",
      "hit.ogg"
      ]);
    Crafty.audio.add("lose", [
      "lose_effect.mp3",
      "lose_effect.ogg"
      ]);
    Crafty.audio.add("win", [
      "win_effect.mp3",
      "win_effect.ogg"
      ]);
    Crafty.audio.add("hit2", [
      "hit2.mp3",
      "hit2.ogg"
      ]);
        
   for(var ytile = 0; ytile < 32; ytile++) {
      for(var xtile = 0; xtile < 20; xtile++) {
          //console.log(xtile * 16, ytile * 16);
          var usefloor = (xtile%2);
          if(xtile % Math.round(Math.random()*10))
          {
            usefloor = 2;
          }
          if(xtile == 19)
          {
            Crafty.e("2D, Canvas, wall, wall1")
              .attr({x: xtile * 16, y: ytile * 16, z: -2});
            
          } else if(xtile == 0){
            Crafty.e("2D, Canvas, wall,  wall2")
                .attr({x: xtile * 16, y: ytile * 16, z: -2});
            
          } else {
            Crafty.e("2D, Canvas, floor"+usefloor)
                .attr({x: xtile * 16, y: ytile * 16, z: -2});
          }
      }
    }
            
    Crafty.e("topPaddle, 2D, Canvas, toppaddle")
                .attr({x: 100, y: 10, w: PADDLE_WIDTH, h: PADDLE_HEIGHT}) 
                .bind('EnterFrame', function () {
                    var gameBall = Crafty("gameBall"); //get gameBall
                    if(gameBall.yspeed < 0)
                    {
                      if(gameBall.x < (this.x + PADDLE_WIDTH / 2))
                      {
                        this.x--;
                      } else {
                        this.x++;
                      }
                    }
                    if(this.x <= 0)
                    {
                      this.x = 0;
                    }
                    if(this.x >= (320 - PADDLE_WIDTH))
                    {
                      this.x = 320 - PADDLE_WIDTH;
                    }
                }); 

    Crafty.e("bottomPaddle, 2D, Canvas, bottompaddle")
                .attr({x: 100, y: 460, w: PADDLE_WIDTH, h: PADDLE_HEIGHT});
                
    Crafty.e("mouseTracking, 2D, Mouse, Touch, Canvas")
              .attr({ w:320, h:480, x:0, y:0 })
              .bind("MouseMove", function(e) 
              {
                    //console.log("MouseDown:"+ Crafty.mousePos.x +", "+ Crafty.mousePos.y);
                    var bottomPaddle = Crafty("bottomPaddle"); //get bottomPaddle
                    bottomPaddle.x = Crafty.mousePos.x - bottomPaddle.w/2;
                    //console.log("new pos:"+ bottomPaddle.x);
                    //console.log("would have been:"+ Crafty.mousePos.x );
                });                 

    Crafty.e("scoreValue, 2D, Canvas, Text")
                .attr({x: 20, y: 0, w: PADDLE_WIDTH, h: PADDLE_HEIGHT,
                      pointsPlayer:0, pointsComputer:0
                      })
                .textColor('#FFFFFF')
                .bind('EnterFrame', function () {
                      this.text("You:" + this.pointsPlayer +
                                "  CPU:" + this.pointsComputer);
                });
                
   Crafty.e("gameBall, 2D, Canvas, Collision, SpriteAnimation, ball0")
                .attr({x: 100, y: 100, w: BALL_RADIUS, h: BALL_RADIUS,
                      xspeed: 2, yspeed: 4
                      })
                .animate('BallBlinking', 2,1,4) //setup animation
                .animate('BallBlinking', 5, -1) //start animation
                .bind('EnterFrame', function () {
                  
                  this.x += this.xspeed;
                  this.y += this.yspeed;

                 if(this.y > (480 + BALL_RADIUS))
                 {
                    var scoreValue = Crafty("scoreValue"); 
                    scoreValue.pointsComputer++;
                    Crafty.audio.play("lose");
                    this.x = 20 + Math.round(Math.random() * 100);
                    this.y = 100;
                    this.xspeed = 0 -this.xspeed;
                   
                 }
                   
                 if(this.y < 0 )
                 {
                    var scoreValue = Crafty("scoreValue"); 
                    scoreValue.pointsPlayer++;
                    Crafty.audio.play("win");
                    this.x = 20 + Math.round(Math.random() * 100);
                    this.y = 300;
                    this.xspeed = 0 -this.xspeed;
                 }
                                   
                })
              .onHit('bottomPaddle', function () {
                  this.yspeed *= -1;
                  Crafty.audio.play("hit");
                  this.y = 460 - BALL_RADIUS;
              })
              .onHit('topPaddle', function () {
                  Crafty.audio.play("hit");
                  this.yspeed *= -1;
                  this.y = 10+BALL_RADIUS;
              })
              .onHit('wall', function () {
                  Crafty.audio.play("hit2");
                  this.xspeed *= -1;
              });
  });


            
  } else {
    var yes = confirm("Download a better browser?");
    if(yes)
    {
      window.location = "http://google.com/chrome";
    }
  }
}
	

Demo

No Engine vs Engine

HTML5 Pong Crafty Pong
Line Count 210 207
AI Yes Yes
Graphics Yes Yes
Sprites No Yes
Sound No Yes
Touch No Yes
Mouse No Yes

MechaJet

Demo

Impact Game Engine

  • Very OOP.
  • Advanced libraries for physics, sprites, audio, etc.
  • $99. No royalties. Includes the source code.
  • Optimized for iOS with Ejecta.
  • Weltmeister Level Editor. Includes the source code.
  • Built-in debug tools

Impact Setup

Demo

Web server with PHP required.

Impact Engine

  • entities
    • spritesheet, animation state
    • health, damage, collision properties
    • position, speed, acceleraton, friction, gravity
    • sound, inputs, timers, frame update
  • levels
    • Tile Map, Collision Map
  • main.js
    • init
    • HUD
    • Screen scrolling
    • key bindings
  • media

player.js


	ig.module(
    'game.entities.player'
)

.requires(
	'impact.entity'
)
.defines(function() {

EntityPlayerBullet = ig.Entity.extend({

    size: {x:16, y:16},
    collides: ig.Entity.COLLIDES.PASSIVE,
	type: ig.Entity.TYPE.A,
	gravityFactor: 0,
	flip:false,
    checkAgainst: ig.Entity.TYPE.B,


	animSheet: new ig.AnimationSheet( 'media/bullet.png', 8, 8 ),
	
    check: function( other ) {
    	other.receiveDamage( 10, this );
		this.kill();
    },	
	 
    update: function() {

        this.parent();
		if( this.vel.x == 0)
    	{
			this.kill();
    	}
    },
	
	init: function( x, y, settings ) {
		this.parent( x, y, settings );
		if(settings.flip)
		{
			this.vel.x = -100;
			
		} else {
			this.vel.x = 100;
		}
       
		this.addAnim( 'idle', 0.1, [0,1] );

	}
	
});
   
   
EntityPlayer = ig.Entity.extend({
   
    size: {x:16, y:16},
	type: ig.Entity.TYPE.A,
    checkAgainst: ig.Entity.TYPE.B,
    collides: ig.Entity.COLLIDES.PASSIVE,
	gravityFactor: 7,
	maxVel: {x: 100, y: 200},
	friction: {x: 0, y: 0},
	health: 100,
	maxHealth: 100,
	startXY: null,
	leftButtonDown: false,
	rightButtonDown: false,
	upButtonDown: false,
	downButtonDown: false,
	xButtonDown: false,
	restartSound: new ig.Sound( 'media/restart.*' ),
	shootSound: new ig.Sound( 'media/shoot.*' ),
	fireburnSound: new ig.Sound( 'media/fireburn.*' ),
	killTimer:null,
	dead: false,
	killcallback:null,
	flip: false, //track flip
   
   
	kill: function(){
		//this.parent();
		ig.log("player killed!");
		ig.game.startXY = this.startXY;
		this.killTimer = new ig.Timer();
		this.killTimer.reset();
		this.killcallback = this.parent;
		this.dead = true;
		this.collides = ig.Entity.COLLIDES.NONE;
	},
	
	receiveDamage: function(amount, from){
		if(this.dead)
			return;
		this.parent(amount, from);
	},
    update: function() {

		if(this.dead)
		{
			this.vel.x = 0;
			this.vel.y = 0;
			this.currentAnim = this.anims.death;
			this.currentAnim.flip.x = flip;
			if( this.killTimer.delta() > 2 ) {
				this.killcallback();
				this.restartSound.play();
				ig.game.spawnEntity( EntityPlayer, ig.game.startXY.x, ig.game.startXY.y);
			}
			
		} else {
			
				
			//ig.log(this.friction.x, this.vel.x)
			if( ig.input.state('left') || this.leftButtonDown) {
				this.accel.x = -100;
				if(!this.vel.y)
				{
					this.friction.x = 35;
					this.currentAnim = this.anims.roll;
				} else {
					this.friction.x = 0;
					this.currentAnim = this.anims.idle;
				}
				this.currentAnim.flip.x = true;
				flip = true;
	
			}
			else if( ig.input.state('right') || this.rightButtonDown ) {
				this.accel.x = 100;
				if(!this.vel.y)
				{
					this.friction.x = 35;
					this.currentAnim = this.anims.roll;
				} else {
					this.friction.x = 0;
					this.currentAnim = this.anims.idle;
				}
				this.currentAnim.flip.x = false;
				flip = false;
			} else {
				this.accel.x = 0;
			}
			
	   
			if( ig.input.state('up')  || this.upButtonDown ) {
				this.vel.y = -100;
				this.currentAnim = this.anims.fly;
				this.fireburnSound.play();
				this.currentAnim.flip.x = flip;
			}
			else if( ig.input.state('down')  || this.downButtonDown ) {
				this.vel.y = 100;
				this.currentAnim = this.anims.idle;
				this.currentAnim.flip.x = flip;
			}
			else {
				this.vel.y = 0;
			}
			
			if(!this.vel.y && !this.vel.x)
			{
				this.currentAnim = this.anims.idle;
				this.currentAnim.flip.x = flip;
				
			}
			
			if( ig.input.state('xkey')  || ig.input.state('space') || this.xButtonDown ) {
				var bulletsettings = {flip:this.currentAnim.flip.x};
				//this forces 1 bullet at a time
				var alreadythere = ig.game.getEntitiesByType( EntityPlayerBullet )[0];
				if( !alreadythere ) {
					
					this.shootSound.play();
					
					ig.game.spawnEntity( EntityPlayerBullet, this.pos.x, this.pos.y, bulletsettings);
				}
			}
			
			
		}
		
        this.parent();
    },
    
	animSheet: new ig.AnimationSheet( 'media/robot.png', 16, 16 ),
	
	
	init: function( x, y, settings ) {
		this.parent( x, y, settings );
		flip = false;
        
		this.startXY = {x:x,y:y}; // track start location
		
		this.addAnim( 'idle', 0.1, [0,0] );
		this.addAnim( 'roll', 0.1, [0,1] );
		this.addAnim( 'fly', 0.1, [12,13] );
		this.addAnim( 'death', 0.7, [24, 25,26], true );
	
	}
});
    
});	

buzzard.js


	ig.module(
    'game.entities.buzzard'
)

.requires(
	'impact.entity'
)
.defines(function() {
  
EntityBuzzardBoom = ig.Entity.extend({
 
    size: {x:16, y:16},
    collides: ig.Entity.COLLIDES.NONE,
	killTimer:null,
 
	animSheet: new ig.AnimationSheet( 'media/buzzardbaddie.png', 16, 16 ),
	
    update: function() {
		this.parent();
		if( this.killTimer.delta() > 1 ) {
			this.kill();
		}
	},
	
	init: function( x, y, settings ) {
		this.parent( x, y, settings );
		this.killTimer = new ig.Timer();
		this.killTimer.reset();
		this.addAnim( 'idle', 0.5, [2,3] );
	}
});
   
EntityBuzzard = ig.Entity.extend({
   
    size: {x:16, y:16},
    gravityFactor: 0,
    collides: ig.Entity.COLLIDES.PASSIVE,
    type: ig.Entity.TYPE.B,
    checkAgainst: ig.Entity.TYPE.A,
	flip:false,
   
    update: function() {

		if( this.vel.x == 0)
    	{
			if(this.flip)
			{
				this.flip = false;
				this.vel.x = -10;
			} else {
				this.flip = true;
				this.vel.x = 10;
			}
    	}
		
		if(this.vel.x < 0)
		{
			this.currentAnim.flip.x = false;
		} else {
			this.currentAnim.flip.x = true;
		}

		
		
        this.parent();
    },
    
	animSheet: new ig.AnimationSheet( 'media/buzzardbaddie.png', 16, 16 ),

    check: function( other ) {
    	other.receiveDamage( 20, this );
    },	
	
	
    kill: function(  ) {
		ig.game.spawnEntity( EntityBuzzardBoom, this.pos.x, this.pos.y);
		this.parent();
    },	
	
	
	init: function( x, y, settings ) {
		this.parent( x, y, settings );
        
		this.addAnim( 'idle', 0.4, [0,1] );
		this.vel.x = -20;
	}
});
    
});	

Weltmeister

main.js


	ig.module( 
	'game.main' 
)
.requires(
	'impact.game',
	'game.levels.mechajetlevel1',
	'game.levels.mechajetlevel2',
//	'impact.debug.debug',
	'game.entities.player'
)
.defines(function(){
	


MyGame = ig.Game.extend({
	
	gravity: 300,

	leftButton: {x:0,y:320/2-32,l:32},
	rightButton: {x:32,y:320/2-32,l:32},
	upButton: {x:16,y:320/2-64,l:32},
	downButton: {x:480/2-32,y:320/2-32,l:32},
	xButton: {x:480/2-32,y:320/2-64,l:32},
	
	 
    collisionDetect: function (object1, object2)
    {
        var ax1 = object1.x;
        var ay1 = object1.y;
        var ax2 = object1.x + object1.l;
        var ay2 = object1.y + object1.l;

        var bx1 = object2.x;
        var by1= object2.y;
        var bx2= bx1 + 1;
        var by2= by1 + 1;
     
        if (ax1 <= bx2 && ax2 >= bx1 &&
                ay1 <= by2 && ay2 >= by1)
        {
            return true;
        } else {
            
            return false;
        }
        
    },

	buttonCheck: function(theButton) {
		
		if(!ig.input.state("CanvasTouch"))
        {
			return false;
		}
		return this.collisionDetect(theButton, ig.input.mouse);
	},
	
	
	hudSheet: new ig.Image( 'media/fadedarrow_half.png'),
	healthSheet: new ig.Image( 'media/health.png'),
	
	
	
	init: function() {
		
		ig.input.bind( ig.KEY.UP_ARROW, 'up' );
		ig.input.bind( ig.KEY.DOWN_ARROW, 'down' );
		ig.input.bind( ig.KEY.LEFT_ARROW, 'left' );
		ig.input.bind( ig.KEY.RIGHT_ARROW, 'right' );
		ig.input.bind( ig.KEY.MOUSE1, "CanvasTouch" );
		ig.input.bind( ig.KEY.SPACE, "space" );
		ig.input.bind( ig.KEY.X, "xkey" );
		this.loadLevel(LevelMechajetlevel1);
	},
	
	update: function() {
		// Update all entities and backgroundMaps
		this.parent();
			
		// screen follows the player
		var player = this.getEntitiesByType( EntityPlayer )[0];
		if( player ) {
			this.screen.x = player.pos.x - ig.system.width/2;
			this.screen.y = player.pos.y - ig.system.height/2;

			//track button pushes
			player.leftButtonDown = this.buttonCheck(this.leftButton);
			player.rightButtonDown = this.buttonCheck(this.rightButton);
			player.upButtonDown = this.buttonCheck(this.upButton);
			player.downButtonDown = this.buttonCheck(this.downButton);
			player.xButtonDown = this.buttonCheck(this.xButton);
		
		}
		
		
	},
	
	draw: function() {
		// Draw all entities and backgroundMaps
		this.parent();
		
		//Draw arrow keys
		this.hudSheet.drawTile( this.leftButton.x, this.leftButton.y, 4, 32 );//left
		this.hudSheet.drawTile(this.rightButton.x, this.rightButton.y, 1, 32 ); //right
		this.hudSheet.drawTile( this.upButton.x, this.upButton.y, 0, 32 ); //up
		this.hudSheet.drawTile( this.downButton.x, this.downButton.y, 3, 32 ); //down
		this.hudSheet.drawTile( this.xButton.x, this.xButton.y, 2, 32 ); //fire
				
		//Draw health
		var player = this.getEntitiesByType( EntityPlayer )[0];
		
		if( player ) {
			if(player.health >= player.maxHealth)
			{
				
	//			ig.log("full", player.health, player.maxHealth);
				this.healthSheet.drawTile( 0, 0, 0, 32);
			} else if (player.maxHealth * 3 / 4 <= player.health)
			{
		//		ig.log("3/4", player.health, player.maxHealth);
				this.healthSheet.drawTile( 0, 0, 1, 32);
				
			} else if (player.maxHealth / 2 <= player.health)
			{
			//	ig.log("1/2", player.health, player.maxHealth);
				this.healthSheet.drawTile( 0, 0, 2, 32);
				
			} else {
				//ig.log("empty", player.health, player.maxHealth);
				this.healthSheet.drawTile( 0, 0, 3, 32);
			}
		} else {
				this.healthSheet.drawTile( 0, 0, 3, 32);

		}

		
	
	}
});

if( ig.ua.mobile ) {
    // Disable sound for all mobile devices
    ig.Sound.enabled = false;
}


// Start the Game with 60fps, a resolution of 320x240, scaled
// up by a factor of 2
ig.main( '#canvas', MyGame, 60,480/2 ,320/2 , 2 );

});
	

Debugger


	ig.module( 
	'game.main' 
)
.requires(
	'impact.game',
	'game.levels.mechajetlevel1',
	'game.levels.mechajetlevel2',
//	'impact.debug.debug',
	'game.entities.player'
)
.defines(function(){
	


MyGame = ig.Game.extend({
	
	gravity: 300,

	leftButton: {x:0,y:320/2-32,l:32},
	rightButton: {x:32,y:320/2-32,l:32},
	upButton: {x:16,y:320/2-64,l:32},
	downButton: {x:480/2-32,y:320/2-32,l:32},
	xButton: {x:480/2-32,y:320/2-64,l:32},
	
	 
    collisionDetect: function (object1, object2)
    {
        var ax1 = object1.x;
        var ay1 = object1.y;
        var ax2 = object1.x + object1.l;
        var ay2 = object1.y + object1.l;

        var bx1 = object2.x;
        var by1= object2.y;
        var bx2= bx1 + 1;
        var by2= by1 + 1;
     
        if (ax1 <= bx2 && ax2 >= bx1 &&
                ay1 <= by2 && ay2 >= by1)
        {
            return true;
        } else {
            
            return false;
        }
        
    },

	buttonCheck: function(theButton) {
		
		if(!ig.input.state("CanvasTouch"))
        {
			return false;
		}
		return this.collisionDetect(theButton, ig.input.mouse);
	},
	
	
	hudSheet: new ig.Image( 'media/fadedarrow_half.png'),
	healthSheet: new ig.Image( 'media/health.png'),
	
	
	
	init: function() {
		
		ig.input.bind( ig.KEY.UP_ARROW, 'up' );
		ig.input.bind( ig.KEY.DOWN_ARROW, 'down' );
		ig.input.bind( ig.KEY.LEFT_ARROW, 'left' );
		ig.input.bind( ig.KEY.RIGHT_ARROW, 'right' );
		ig.input.bind( ig.KEY.MOUSE1, "CanvasTouch" );
		ig.input.bind( ig.KEY.SPACE, "space" );
		ig.input.bind( ig.KEY.X, "xkey" );
		this.loadLevel(LevelMechajetlevel1);
	},
	
	update: function() {
		// Update all entities and backgroundMaps
		this.parent();
			
		// screen follows the player
		var player = this.getEntitiesByType( EntityPlayer )[0];
		if( player ) {
			this.screen.x = player.pos.x - ig.system.width/2;
			this.screen.y = player.pos.y - ig.system.height/2;

			//track button pushes
			player.leftButtonDown = this.buttonCheck(this.leftButton);
			player.rightButtonDown = this.buttonCheck(this.rightButton);
			player.upButtonDown = this.buttonCheck(this.upButton);
			player.downButtonDown = this.buttonCheck(this.downButton);
			player.xButtonDown = this.buttonCheck(this.xButton);
		
		}
		
		
	},
	
	draw: function() {
		// Draw all entities and backgroundMaps
		this.parent();
		
		//Draw arrow keys
		this.hudSheet.drawTile( this.leftButton.x, this.leftButton.y, 4, 32 );//left
		this.hudSheet.drawTile(this.rightButton.x, this.rightButton.y, 1, 32 ); //right
		this.hudSheet.drawTile( this.upButton.x, this.upButton.y, 0, 32 ); //up
		this.hudSheet.drawTile( this.downButton.x, this.downButton.y, 3, 32 ); //down
		this.hudSheet.drawTile( this.xButton.x, this.xButton.y, 2, 32 ); //fire
				
		//Draw health
		var player = this.getEntitiesByType( EntityPlayer )[0];
		
		if( player ) {
			if(player.health >= player.maxHealth)
			{
				
	//			ig.log("full", player.health, player.maxHealth);
				this.healthSheet.drawTile( 0, 0, 0, 32);
			} else if (player.maxHealth * 3 / 4 <= player.health)
			{
		//		ig.log("3/4", player.health, player.maxHealth);
				this.healthSheet.drawTile( 0, 0, 1, 32);
				
			} else if (player.maxHealth / 2 <= player.health)
			{
			//	ig.log("1/2", player.health, player.maxHealth);
				this.healthSheet.drawTile( 0, 0, 2, 32);
				
			} else {
				//ig.log("empty", player.health, player.maxHealth);
				this.healthSheet.drawTile( 0, 0, 3, 32);
			}
		} else {
				this.healthSheet.drawTile( 0, 0, 3, 32);

		}

		
	
	}
});

if( ig.ua.mobile ) {
    // Disable sound for all mobile devices
    ig.Sound.enabled = false;
}


// Start the Game with 60fps, a resolution of 320x240, scaled
// up by a factor of 2
ig.main( '#canvas', MyGame, 60,480/2 ,320/2 , 2 );

});
	
  • FPS and draw counts.
  • Toggles.
  • Vectors and Timelines.

bake

  • Minify .js files in to one.
  • Removes debug.
  • Requires PHP.
  • Do not redistribute Impact source.

Android Distribution

CocoonJS

  • Launcher -- develop locally
    • Google Play
    • Apple App Store
    • Amazon App Store
  • Online compiler -- simultaneous targets
  • No dev tools except favorite web editors
  • Still need tools to code sign

  1. Compress.
  2. Send to launcher.
  3. Test.
  4. Repeat.
  5. Upload final version to CooconJS cloud compiler.
  6. Wait a for an email. Download apk.

Sign the APK (Requires JDK)

Create a keystore.


keytool - genkey -v - keystore mykey . keystore \
- alias mykeyalias - keyalg RSA - keysize 2048 \
- validity 10000
					

Sign the package.


jarsigner - verbose - keystore mykey . keystore \
- storepass KEYSTOREPASSWORD - keypass KEYPASSWORD \
unsigned_app . apk mykeyalias
					

Upload to Google Play.

iOS Distribution



  • Framework for Xcode from Impact developer
  • MIT Open Source License
  • Fast "Canvas-only" JavaScript.
  • Supports iAd, IAP, etc.
  • Could instead use CocoonJS

  1. Open Ejecta project.
  2. Add in your Impact library.
  3. Add in your game files.
  4. Compile/Publish like a normal iOS project.


Device testing requires $99/year iOS license.

Windows Distribution

node-webkit


  • Open source MIT license
  • Based on Chromium
  • Based on node.js

node-webkit setup

  • CraftyPong
    • nw.exe
    • *.dll
    • app.nw (compressed)
      • package.json
      • index.html
      • *.* (other game files)

								copy /b nw.exe+app.nw craftypong.exe
							

package.json


	{
  "main": "index.html",
  "name": "com.dannagle.craftynode",
  "description": "Crafty Pong",
  "version": "0.1.0",
  "keywords": [ "crafty", "pong" ],
  "window": {
    "icon": "html5logo.png",
    "toolbar": false,
    "width": 500,
    "height": 500,
    "position": "mouse",
    "min_width": 500,
    "min_height": 500,
    "max_width": 500,
    "max_height": 500
  }
}	

Inno Setup


	; Script generated by the Inno Setup Script Wizard.
#define MyAppName "Crafty Pong"
#define MyAppVerName "Crafty Pong for Windows"
#define MyAppPublisher "Dan Nagle"
#define MyAppURL "http://html5gameenginesbook.com/"
#define MyAppExeName "craftypong.exe"
#define InstallDir "Crafty Pong"
#define GroupName "Crafty Pong"
#define MyDateTimeString GetDateTimeString('yyyy-mm-dd', '', '');

[Setup]
; NOTE: The value of AppId uniquely identifies this application.
AppId={{E7A1A2A9-B9D8-4AC4-9E57-5F81F478D1D5}}
AppName={#MyAppName}
AppVerName={#MyAppVerName}
AppPublisher={#MyAppPublisher}
AppPublisherURL={#MyAppURL}
AppSupportURL={#MyAppURL}
AppUpdatesURL={#MyAppURL}
DefaultDirName={pf}\CraftyPong
DefaultGroupName={#GroupName}
OutputDir=installer
OutputBaseFilename=CraftyPong{#MyDateTimeString}
SetupIconFile=resources\html5logo.ico
LicenseFile=resources\craftyponglicense.rtf

Compression=lzma2/ultra64
AlwaysRestart = "no"

[Languages]
Name: English; MessagesFile: compiler:Default.isl


[Tasks]
Name: desktopicon; Description: {cm:CreateDesktopIcon}; GroupDescription: {cm:AdditionalIcons};

[Files]
Source: resources\*; DestDir: {app}; Flags: ignoreversion


[Icons]
Name: {group}\{#MyAppName}; Filename: {app}\{#MyAppExeName}
Name: {commondesktop}\{#MyAppName}; Filename: {app}\{#MyAppExeName}; Tasks: desktopicon

[Icons]
Name: {group}\{#MyAppName}; Filename: {app}\{#MyAppExeName};
Name: {commondesktop}\{#MyAppName}; Filename: {app}\{#MyAppExeName};  IconFilename: {app}\html5logo.ico; Tasks: desktopicon

[Run]
Filename: {app}\{#MyAppExeName}; Description: {cm:LaunchProgram,{#MyAppName}}; Flags: nowait postinstall skipifsilent



	
  • Open Source MIT-like license
  • Around for 17 years
  • Compiles to single EXE

Crafty Pong Windows

Mac Distribution

node-webkit

Right-click ⇒ show package contents

DMG Creation


								hdiutil convert CraftyPongTemplate.dmg -format UDZO -imagekey \
								 zlib-level=9 -o CraftyPong.dmg
							

Crafty Pong Mac

Summary

  • Pong using pure HTML5. No engine.
  • Pong using Crafty engine.
  • MechaJet (space shooter) using Impact engine.
  • Android Native using CocoonJS.
  • iOS Native using Ejecta.
  • Windows Native using node-webkit + Inno Setup.
  • Mac Native using node-webkit + DMG.

Further Reading

Further Listening

Lostcast Podcast

Questions?

CRCPress.com
DanNagle.com
@NagleCode
HTML5GameEnginesBook.com