Pong 2 (Single Player) Project


Prior to reading the walkthrough of my code, play the game I created first >>   Play Pong 2 Game


Use the left arrow and right arrow keys for paddle movement, or even the mouse.



Pong is one of the earliest arcade video games and the very first sports arcade video game. The game was originally manufactured by Atari, which released it in 1972. It was a table tennis sports game featuring simple two-dimensional graphics. Pong was one of the first video games to reach mainstream popularity. The aim was to defeat an opponent in a simulated table-tennis game by earning a higher score.


This version of Pong 2 is written in HTML5 and Javascript.


Initially I wanted to make the game as close to the original Atari version as possible. However I soon decided to pursue ideas from my childhood memories of how I had wanted to improve Pong.



Project Files

Click here to download the project files. You will find the following in the zip file:




Organization of the game




Pong2.html

  
<html>
    <head>
        <title>Pong 2</title>
        <meta charset="UTF-8" />
        <style>
        canvas { background: Black; display: block; margin: 0 auto; }
        </style>   
    </head>
  <body>
  <canvas id="myCanvas" width="960" height="480"></canvas>
 <script type="text/javascript" src="js/game.js"></script> 
  </body>
</html>  


game.js


Canvas Basics and Declaring Variables

To actually be able to render graphics on the <canvas> element, first we have to grab a reference to it in JavaScript.

 
var canvas = document.getElementById("myCanvas");    // the canvas element
var context = canvas.getContext("2d");

Next follow variables defining properties of the paddle, bricks, the ball. The variable paddleY determines the height and location of the paddle vertically, and paddleX determines the width and location of the paddle horizontally.

 
var paddleHeight = 14;
var paddleWidth = 75;
var paddleColor = "#0095DD";
var spacer = 10;  //space beneath the paddle
var paddleY = canvas.height-(paddleHeight+spacer);
var paddleX = (canvas.width-paddleWidth)/2;

The x and y control at this point the starting position of the ball and then represents its location as it moves around the screen.

 
var x = canvas.width/2;  
var y = canvas.height-(paddleHeight+spacer+10);   
var x2 = canvas.width/2;  
var y2 = canvas.height-(paddleHeight+spacer+10);
var dx = 3;     
var dy = -3;
var dx2 = -4;
var dy2 = 4;

The dx and dy variables control the ball speed and moves the ball by incrementing x and y after every frame has been drawn to make it appear that the ball is moving. Incrementing x and y are updated at the end of the Draw() function.

 
x += dx;
y += dy;
x2 += dx2;
y2 += dy2;


Draw Balls

The balls are painted with these functions:

 
function drawBall() {
    context.beginPath();
    context.arc(x, y, ballRadius, 0, Math.PI*2);  
    context.fillStyle = cr;   
    context.fill();
    context.closePath();
}

function drawBall2() {
   context.beginPath();
   context.arc(x2, y2, ballRadius2, 0, Math.PI*2);
   context.fillStyle = cr2;   
   context.fill();
   context.closePath();
} 


Ball Collision Detection

The ball can deflect off the vertical sides of the canvas, the top, and the paddle. To detect collision check whether the ball is touching the wall, top or paddle and change it's direction accordingly dx = -dx; or dy = -dy; . The variable ballRadius holds the radius of the drawn circle and is used for calculations. The following code is located in the Draw() function and is repeated for each ball. Only the first ball is shown.


When the ball bounces of the bottom of the screen, logic determines if the games is over. This is discussed below.


Also note the variable rect which defines the paddle so that a specific collision between ball and the paddle object can be detected.

// BALL 1
//ball bounces off screen sides
    if(x + dx > canvas.width-ballRadius || x + dx < ballRadius) {  
      dx = -dx;
      calcColors();
      drawBall();  
    }
    
//ball bounces off top of screen
    if(y + dy < ballRadius) {  
      dy = -dy;
      calcColors();
      drawBall(); 
    }
    
//ball bounces off paddle
    var rect = {paddleX, paddleY, paddleWidth, paddleHeight};
    if(x > rect.paddleX && x < rect.paddleX+rect.paddleWidth && y > rect.paddleY && y < rect.paddleY+rect.paddleHeight) {
      dy = -dy;
      calcColors();
      drawBall();
    }
    
//ball bounces off bottom of screen
    if( y + dy >  canvas.height ) {  
      lives--;
        if(!lives) {
          //game over
          drawGameoverScreen();
        } else {
          dx = 3;   //set above initially
          dy = -3;
         }
     }


Keyboard Controls

First we start with two variables initialized with boolean values.

 
var rightPressed = false;
var leftPressed = false;

The event listeners are set up to detect when a key is pressed and released, or mouse movement.

 
document.addEventListener("keydown", keyDownHandler, false);
document.addEventListener("keyup", keyUpHandler, false);
document.addEventListener("mousemove", mouseMoveHandler, false);

The keyDownHandler(), keyUpHandler(), mouseMoveHandler() functions take an event as a parameter, represented by the e variable. The key code 37 is the left cursor key and 39 is the right cursor. If the left cursor is pressed, then the leftPressed variable is set to true, and when it is released the leftPressed variable is set to false. There is also key code 38 which is used to unpause the game after a win or loss, but we will cover that later.

 
function keyDownHandler(e) {
    if(e.keyCode == 39) {
        rightPressed = true;
    }
    else if(e.keyCode == 37) {
        leftPressed = true;
    }
    else if(e.keyCode == 38 && gamePaused == true){  //if up arrow key pressed unpause game after a win or loss
    document.location.reload();
    }
}

function keyUpHandler(e) {
    if(e.keyCode == 39) {
        rightPressed = false;
    }
    else if(e.keyCode == 37) {
        leftPressed = false;
    }
}

function mouseMoveHandler(e) {
    var relativeX = e.clientX - canvas.offsetLeft;
    if(relativeX > 0 && relativeX < canvas.width) {
        paddleX = relativeX - paddleWidth/2;
    }
}


Paddle Movement

If the left cursor is pressed, the paddle will move 7 pixels to the left, and if the right cursor is pressed, the paddle will move 7 pixels to the right.

 
if(rightPressed && paddleX < canvas.width-paddleWidth) {
        paddleX += 7;
    }
    else if(leftPressed && paddleX > 0) {
        paddleX -= 7;
    }


Game Over

When the ball hits the bottom of the canvas, then the number of lives is decremented by one. If there are no lives left the drawGameoverScreen() function is called and a message displayed, and the game is paused until the player hits the up arrow key.

 
//ball bounces off bottom of screen
    if( y2 + dy2 >  canvas.height ) {  
      lives--;
        if(!lives) {
          //game over
          drawGameoverScreen();
        } else {
          dx2 = 4;   //set above initially
          dy2 = -4;
         }
     }
 
function drawGameoverScreen(){
   var canvas = document.getElementById('myCanvas');
   var context = canvas.getContext('2d');

   //Draw background
   context.fillStyle = '#000000';
   context.fillRect(0,0,canvas.width,canvas.height);

   //Draw red text
   context.fillStyle = '#E50000';
   context.font = 'bold 80px impact';
   context.fillText("GAME OVER", 300, 230);
   context.font = '30px impact';
   context.fillText("Press UP arrow to retry", 350, 270);
   gamePaused = true;
   pauseGame();
}


Game Win

If there are no bricks left to break then the drawGamewinScreen() function is called from the collisionDetection() function.

function collisionDetection() {
    for(c=0; c<brickColumnCount; c++) {
        for(r=0; r<brickRowCount; r++) {
            var b = bricks[c][r];
            if(b.status == 1) {
            //ball1
            
                if(x > b.x && x < b.x+brickWidth && y > b.y && y < b.y+brickHeight) {
                    dy = -dy;
                    b.status = 0;
                    score++;
                    if(score == brickRowCount*brickColumnCount) {
                        //You win
                        drawGamewinScreen()
                    }
                }
               
               //ball2
               if(x2 > b.x2 && x2 < b.x2 + brickWidth && y2 > b.y2 && y2 < b.y2 + brickHeight) {
                   dy2 = -dy2;
                    b.status = 0;
                    score++;
                    if(score == brickRowCount*brickColumnCount) {
                        //You win
                        drawGamewinScreen()
                    }
                }
             }
        }
    }
}

A message is displayed to the user, and the game is paused until the player hits the up arrow key.

Refer to the Game Paused section below.

function drawGamewinScreen(){
   var canvas = document.getElementById('myCanvas');
   var context = canvas.getContext('2d');

   //Draw background
   context.fillStyle = '#000000';
   context.fillRect(0,0,canvas.width,canvas.height);

   //Draw green text
   context.fillStyle = '#00CC00';
   context.font = 'bold 80px STCaiyun';
   context.fillText("YOU WIN!!", 300, 230);
   context.font = '30px impact';
   context.fillText("Press UP arrow to retry", 350, 270);
   gamePaused = true;
   pauseGame();
}



Game Paused

After each win or loss the game is paused, and if the user wants to play again is directed to press the up arrow key.

In both the drawGamewinScreen() and drawGameoverScreen() functions (see above) check if the gamePaused variable is set to true. Next the pauseGame() function is called which temporarily suspends the game.

function pauseGame() {
  if (gamePaused == true) {
    game = clearTimeout(game);
  } else if (gamePaused == false) {
    game = setTimeout(gameLoop, 3000);
    gamePaused = false;
  }
}

An event listener watches for the up arrow to be pressed (keyCode 38) and if pressed checks if the gamePaused variable equals true and if so, reloads the screen.

else if(e.keyCode == 38 && gamePaused == true){  //if up arrow key pressed unpause game after a win or loss
    document.location.reload();


The Brick Field

Render bricks with a nested loop using a two-dimensional array. Start with the variables.

var brickRowCount = 11;
var brickColumnCount = 3;
var brickWidth = 75;
var brickHeight = 20;
var brickPadding = 4;
var brickOffsetTop = 30;
var brickOffsetLeft = 30;

The brick field is an array which contains brick columns (c), and brick rows (r), that have an x and y position to paint each brick on the screen. Add the following just below the variables.

var bricks = [];
for(c=0; c<brickColumnCount; c++) {
    bricks[c] = [];
    for(r=0; r<brickRowCount; r++) {
        bricks[c][r] = { x: 0, y: 0, status: 1 };
    }

Add Bricks

Add a function to loop through all the bricks in the array and draw them on the screen.

Loop thru the rows and columns to set the x and y position of each brick, and render a brick on the Canvas with each loop iteration.

function drawBricks() {
    for(c=0; c<brickColumnCount; c++) {
        for(r=0; r<brickRowCount; r++) {
            if(bricks[c][r].status == 1) {
                var brickX = (r*(brickWidth+brickPadding))+brickOffsetLeft;
                var brickY = (c*(brickHeight+brickPadding))+brickOffsetTop;
             
             //ball1
                bricks[c][r].x = brickX;
                bricks[c][r].y = brickY;
              
              //ball2 
              if(x != x2 || y != y2) {
                 bricks[c][r].x2 = brickX;
                 bricks[c][r].y2 = brickY;
                 }
               
                context.beginPath();
                context.rect(brickX, brickY, brickWidth, brickHeight);
                context.fillStyle = "#0095DD";
                context.fill();
                context.closePath();
            }
        }
    }
}

Remove Bricks

To remove bricks that have already been hit by the ball add a status parameter to indicate whether to paint the brick or not.

Print the brick if status equals 1


When the brick field array is created, each brick has status set to 1

var bricks = [];...
bricks[c][r] = { x: 0, y: 0, status: 1 };...

In the collisionDetection() function check if brick exists. If it exists status will equal 1

  if(b.status == 1) {...

If it exists and was hit by the ball, set the brick status to 0

 b.status = 0;...

In the drawBricks() function. If status equals 1 then draw the brick, otherwise it was hit by the ball so do not display the brick on the screen anymore.

if(bricks[c][r].status == 1)...


Add the drawBricks() call somewhere in the draw() function. Preferably at the beginning.



Draw Score

Drawing text on a canvas is pretty much like drawing a shape.

To update the score each time a brick is hit the score variable is incremented in the collisionDetection() function.

     score++;

The score variable is displayed in the drawScore() function.

function drawScore() {
    context.font = "bold 18px Arial";
    context.fillStyle = "#0095DD";
    context.fillText(" Score: "+score, 8, 20);
}


Draw Lives

To update the lives, each time the ball touches the screen bottom the lives variable is decremented.

//ball bounces off bottom of screen
    if( y + dy >  canvas.height ) {  
      lives--;

The lives variable is displayed in the drawLives() function.

function drawLives() {
    context.font = "bold 18px Arial";
    context.fillStyle = "#0095DD";
    context.fillText("Lives: " + lives, canvas.width-85, 20);
}


Changing Ball Color

Whenever the ball bounces off the sides of the screen, the bottom or top of the screen, or the paddle, it changes color with a call to calcColors.

function calcColors() {
    cr = 'rgb('+ Math.floor(Math.random()*256)+','+ Math.floor(Math.random()*256)+','+ Math.floor(Math.random()*256)+')';
    cr2 = 'rgb('+ Math.floor(Math.random()*256)+','+ Math.floor(Math.random()*256)+','+ Math.floor(Math.random()*256)+')';  

      if(cr == '#000000') {
          cr = 'rgb('+ Math.floor(Math.random()*256)+','+ Math.floor(Math.random()*256)+','+ Math.floor(Math.random()*256)+')';
      } 
      else if(cr2 == '#000000') {
          cr2 = 'rgb('+ Math.floor(Math.random()*256)+','+ Math.floor(Math.random()*256)+','+ Math.floor(Math.random()*256)+')'; 
      }
  }

When the ball is repainted the context.fillStyle references the variable cr for ball one and cr2 for ball two. These variables are set in the calcColors() function.

 
function drawBall() {
    context.beginPath();
    context.arc(x, y, ballRadius, 0, Math.PI*2);  
    context.fillStyle = cr;   
    context.fill();
    context.closePath();
}

function drawBall2() {
   context.beginPath();
   context.arc(x2, y2, ballRadius2, 0, Math.PI*2);
   context.fillStyle = cr2;   
   context.fill();
   context.closePath();
} 


Draw Function

At the beginning of the draw() function is where calls are made to the major components of the game.

function draw() {
    context.clearRect(0, 0, canvas.width, canvas.height);
    drawBricks();
    drawBall();
    drawBall2();  
    drawPaddle();
    drawScore();
    drawLives();
    collisionDetection();...

At the very bottom of the draw() function add the following: requestAnimationFrame(draw);

This causes the draw() function to call itself over and over again.