Canvas Game Tutorial Pong
After several posts about Canvas and game code, we are finally ready to put together our first game, Pong in this game tutorial.
For those of you too young to remember 2d games or why it was so important for Marty McFly’s parents to kiss at the dance…. Pong is a basic tennis-style game.
The finished game from this tutorial is far from perfect with some flaws that we will correct later, but for now, this game tutorial should be enough to keep you entertained for a while.
You can see the game yourself at http://games.thejaytray.com/pong.html
So, open Notepad or whatever text editor you are using and we can begin.
Assumptions
I am working off the assumption that you have either a basic (at least) understanding of HTML 5 and Canvas/JavaScript, or that you have been following these posts:
- Canvas Basics 01 – Hello World
- Canvas Basics 02 – Mouse Detection
- Canvas Basics 03 – Moving An Object
- Canvas Basics 04 – Move An Object Using The Keyboard
- Canvas Basics 05 – Limiting Movement To The Canvas
- Canvas Basics 06 – Collision Detection
- Canvas Basics 07 – Random Positions
- Canvas Basics 08 – Bouncing And Deflecting
The code you are about to see will include elements of the previous posts, plus some extra bits that will be explained as we go along.
HTML File
Save a blank file as “pong.html”.
You are going to create this file in Notepad, but view it using your browser (I use Google Chrome).
<!DOCTYPE html> <head> <title>Pong</title> </head> <body> <canvas id="canvas" style="background-color:black"> Your browser doesn't support Canvas, try another or update </canvas> <script src="pong-script.js"></script> </body> </html>
The pong.html file will reference a canvas object and the source for the JavaScript will be a file called “pong-script.js”.
Save the html file and close the text editor.
You can open pong.html in your web browser now (it will be blank for now).
JavaScript File
Open another blank file in your text editor and save it to the same folder as your html file as “pong-script.js”.
We will continuously edit this file, save changes made and then refresh our browser so we can see the effect of those changes.
In the numbered lines following, the code will be in red font and my comments will be in black.
Game Tutorial – Create Basic Game Objects
In this section, we will the canvas area and some of the game objects like the players’ paddles, scores and a “net” going down the middle of the game area.
- var canvas = document.getElementById(‘canvas’);
- var ctx = canvas.getContext(‘2d’);
- canvas.width = 650;
- canvas.height = 400;
- var score1 = 0; We are going to record 2 scores, one for each player
- var score2 = 0; Player 1 will be the left paddle and player 2 will be the right paddle
- function Box(options) { This constructor function will contain all options and defaults for our “Box” object
- this.x = options.x || 10;
- this.y = options.y || 10;
- this.width = options.width || 40;
- this.height = options.height || 50;
- this.color = options.color || ‘#FFFFFF’;
- this.speed = options.speed || 2;
- this.gravity = options.gravity || 2;
- }
- var player1 = new Box({ These are the options for the player 1 paddle
- x: 10,
- y: 200,
- width: 15,
- height: 80,
- color: ‘#FFFFFF’,
- gravity: 2
- });
- var player2 = new Box({ These are the options for the player 2 paddle
- x: 625,
- y: 100,
- width: 15,
- height: 80,
- color: ‘#FFFFFF’,
- gravity: 2
- });
- var midLine = new Box({ The net, or line in the middle will also be a box type object.
- x: (canvas.width/2) – 2.5, I am making it slightly longer than the game area to avoid any gaps.
- y: -1,
- width: 5,
- height: canvas.height+1,
- color: ‘#FFFFFF’
- });
- function drawBox(box) { This function will be called to draw each box stated.
- ctx.fillStyle = box.color;
- ctx.fillRect(box.x, box.y, box.width, box.height);
- }
- function displayScore1() { This gets the player 1 code and displays it onscreen
- ctx.font = “20px Arial”;
- ctx.fillStyle = “rgb(255,255,255)”;
- var str1 = score1;
- ctx.fillText(str1, (canvas.width/2) – 50, 30);
- }
- function displayScore2() { This gets the player 1 code and displays it onscreen
- ctx.font = “20px Arial”;
- ctx.fillStyle = “rgb(255,255,255)”;
- var str2 = score2;
- ctx.fillText(str2, (canvas.width / 2) + 50, 30);
- }
- function draw() { The draw function will clear the canvas area and then draw each object onscreen
- ctx.clearRect(0, 0, canvas.width, canvas.height);
- displayScore1();
- displayScore2();
- drawBox(player1);
- drawBox(player2);
- drawBox(midLine);
- }
- function loop() { The loop function will continuously call the draw function and update our screen
- draw();
- window.requestAnimationFrame(loop);
- }
- loop();
When you enter the code above you end up with 2 scores, 2 paddles and a net/line in the middle.
The paddles don’t move and the score doesn’t change, but this is a good starting point for now.
Game Tutorial – Create A Bouncing Ball
We will add a new box object called “theBall”.
This code will start at line 45, just below where we created the midLine object and the object values will be as follows:
- var theBall = new Box({
- x: (canvas.width / 2),
- y: (canvas.height / 2),
- width: 15,
- height: 15,
- color: ‘#FF0000’,
- speed: 1,
- gravity: 1
- });
We will create 2 more functions, 1 that will make the ball bounce up/down if it hits the top or bottom of the canvas.
This function will be called ballBounce.
The second function will be called ballCollision.
For now, this function will check for collision of theBall with the left and right sides of the canvas areas and change the speed variable to move in the opposite direction.
Later we will change this function to check for collision with the paddles.
This code will go at line 74, below the displayScore2 function.
- function ballBounce() { Check if the ball will hit the top or bottom of the canvas
- if (((theBall.y+theBall.gravity) <= 0) || ((theBall.y+theBall.gravity+theBall.height) >= canvas.height)){
- theBall.gravity = theBall.gravity * -1; If it does, change the gravity value to move in the opposite direction
- theBall.y += theBall.gravity; Move theBall
- theBall.x += theBall.speed;
- } else { If the ball doesn’t hit the top or bottom, then move theBall as normal
- theBall.x += theBall.speed;
- theBall.y += theBall.gravity
- }
- ballCollision(); Call the ballCollision function next
- }
- function ballCollision() { Check if the ball will hit the left or right sides of the canvas
- if (((theBall.x + theBall.speed) <= 0) || ((theBall.x + theBall.speed + theBall.width) >= canvas.width)) {
- theBall.speed = theBall.speed * -1; If it does, change the speed value to move in the opposite direction
- theBall.x += theBall.speed; Move theBall
- theBall.y += theBall.gravity;
- } else { If the ball doesn’t hit the sides, then move theBall as normal
- theBall.x += theBall.speed;
- theBall.y += theBall.gravity;
- }
- draw(); Call the draw function
- }
We have created a new object called theBall.
We now need to update the draw function to include this item. This will be done at line 105 below.
- function draw() {
- ctx.clearRect(0, 0, canvas.width, canvas.height);
- displayScore1();
- displayScore2();
- drawBox(player1);
- drawBox(player2);
- drawBox(midLine);
- drawBox(theBall);
- }
Up to now, the loop function had been calling the draw function.
We are going to change this function to call the ballBounce function, which then calls the ballCollision function and that then calls the draw function.
This change is made to line 109.
- function loop() {
- ballBounce();
- window.requestAnimationFrame(loop);
- }
When you save your js file and refresh your browser, you should see a red (square) ball bouncing off the sides and top and bottom.
The full code as it stands is:
var canvas = document.getElementById('canvas'); var ctx = canvas.getContext('2d'); canvas.width = 650; canvas.height = 400; var score1 = 0; var score2 = 0; function Box(options) { this.x = options.x || 10; this.y = options.y || 10; this.width = options.width || 40; this.height = options.height || 50; this.color = options.color || '#FFFFFF'; this.speed = options.speed || 2; this.gravity = options.gravity || 2; } var player1 = new Box({ x: 10, y: 200, width: 15, height: 80, color: '#FFFFFF', gravity: 2 }); var player2 = new Box({ x: 625, y: 100, width: 15, height: 80, color: '#FFFFFF', gravity: 2 }); var midLine = new Box({ x: (canvas.width/2) - 2.5, y: -1, width: 5, height: canvas.height+1, color: '#FFFFFF' }); var theBall = new Box({ x: (canvas.width / 2), y: (canvas.height / 2), width: 15, height: 15, color: '#FF0000', speed: 1, gravity: 1 }); function drawBox(box) { ctx.fillStyle = box.color; ctx.fillRect(box.x, box.y, box.width, box.height); } function displayScore1() { ctx.font = "20px Arial"; ctx.fillStyle = "rgb(255,255,255)"; var str1 = score1; ctx.fillText(str1, (canvas.width/2) - 50, 30); } function displayScore2() { ctx.font = "20px Arial"; ctx.fillStyle = "rgb(255,255,255)"; var str2 = score2; ctx.fillText(str2, (canvas.width / 2) + 50, 30); } function ballBounce() { if (((theBall.y+theBall.gravity) <= 0) || ((theBall.y+theBall.gravity+theBall.height) >= canvas.height)){ theBall.gravity = theBall.gravity * -1; theBall.y += theBall.gravity; theBall.x += theBall.speed; } else { theBall.x += theBall.speed; theBall.y += theBall.gravity } ballCollision(); } function ballCollision() { if (((theBall.x + theBall.speed) <= 0) || ((theBall.x + theBall.speed + theBall.width) >= canvas.width)) { theBall.speed = theBall.speed * -1; theBall.x += theBall.speed; theBall.y += theBall.gravity; } else { theBall.x += theBall.speed; theBall.y += theBall.gravity; } draw(); } function draw() { ctx.clearRect(0, 0, canvas.width, canvas.height); displayScore1(); displayScore2(); drawBox(player1); drawBox(player2); drawBox(midLine); drawBox(theBall); } function loop() { ballBounce(); window.requestAnimationFrame(loop); } loop();
Game Tutorial – Moving Paddles
Next we will move our two paddles.
Player 1 (on the left) will use the ‘w’ and ‘s’ keys to move up and down, and player 2 (on the right) will use the up and down arrows to move.
We will get this part working before we worry about having theBall bounce off the paddles.
Starting at line 9, after our score variables we will add an array variables for the keys and then our event listeners for when the keys are pressed down and released up.
- var = keys {};
- window.addEventListener(‘keydown’, function (e) {
- keys[e.keyCode] = true;
- e.preventDefault();
- });
- window.addEventListener(‘keyup’, function (e) {
- delete keys[e.keyCode];
- });
At line 66, after we have created all our game objects, we will add an input function.
This will handle changing the paddles’ positions depending on what keys are pressed.
Both paddles can only move either up or down and the input function is as follows:
- function input() {
- if (87 in keys) { If the w key is pressed….
- if (player1.y – player1.gravity > 0) {
- player1.y -= player1.gravity;
- }
- } else if (83 in keys) { If the s key is pressed…..
- if (player1.y + player1.height + player1.gravity < canvas.height) {
- player1.y += player1.gravity;
- }
- }
- if (38 in keys) { If the up arrow key is pressed
- if (player2.y – player2.gravity > 0) {
- player2.y -= player2.gravity;
- }
- } else if (40 in keys) { If the down arrow key is pressed
- if (player2.y + player2.height + player2.gravity < canvas.height) {
- player2.y += player2.gravity;
- }
- }
- }
We will next amend the loop function by calling the input function for both players at lines 142 and 142.
- function loop() {
- ballBounce();
- input(player1);
- input(player2);
- window.requestAnimationFrame(loop);
- }
Save your js file as it is and refresh your browser.
You should be able to move both paddles and watch the ball bouncing.
So far, we have not added any code to make theBall bounce off the paddles.
The updated js code so far is
var canvas = document.getElementById('canvas'); var ctx = canvas.getContext('2d'); canvas.width = 650; canvas.height = 400; var score1 = 0; var score2 = 0; var keys = {}; window.addEventListener('keydown', function (e) { keys[e.keyCode] = true; e.preventDefault(); }); window.addEventListener('keyup', function (e) { delete keys[e.keyCode]; }); function Box(options) { this.x = options.x || 10; this.y = options.y || 10; this.width = options.width || 40; this.height = options.height || 50; this.color = options.color || '#FFFFFF'; this.speed = options.speed || 2; this.gravity = options.gravity || 2; } var player1 = new Box({ x: 10, y: 200, width: 15, height: 80, color: '#FFFFFF', gravity: 2 }); var player2 = new Box({ x: 625, y: 100, width: 15, height: 80, color: '#FFFFFF', gravity: 2 }); var midLine = new Box({ x: (canvas.width/2) - 2.5, y: -1, width: 5, height: canvas.height+1, color: '#FFFFFF' }); var theBall = new Box({ x: (canvas.width / 2), y: (canvas.height / 2), width: 15, height: 15, color: '#FF0000', speed: 1, gravity: 1 }) function input() { if (87 in keys) { if (player1.y - player1.gravity > 0) { player1.y -= player1.gravity; } } else if (83 in keys) { if (player1.y + player1.height + player1.gravity < canvas.height) { player1.y += player1.gravity; } } if (38 in keys) { if (player2.y - player2.gravity > 0) { player2.y -= player2.gravity; } } else if (40 in keys) { if (player2.y + player2.height + player2.gravity < canvas.height) { player2.y += player2.gravity; } } } function drawBox(box) { ctx.fillStyle = box.color; ctx.fillRect(box.x, box.y, box.width, box.height); } function displayScore1() { ctx.font = "20px Arial"; ctx.fillStyle = "rgb(255,255,255)"; var str1 = score1; ctx.fillText(str1, (canvas.width/2) - 50, 30); } function displayScore2() { ctx.font = "20px Arial"; ctx.fillStyle = "rgb(255,255,255)"; var str2 = score2; ctx.fillText(str2, (canvas.width / 2) + 50, 30); } function ballBounce() { if (((theBall.y+theBall.gravity) <= 0) || ((theBall.y+theBall.gravity+theBall.height) >= canvas.height)){ theBall.gravity = theBall.gravity * -1; theBall.y += theBall.gravity; theBall.x += theBall.speed; } else { theBall.x += theBall.speed; theBall.y += theBall.gravity } ballCollision(); } function ballCollision() { if (((theBall.x + theBall.speed) <= 0) || ((theBall.x + theBall.speed + theBall.width) >= canvas.width)) { theBall.speed = theBall.speed * -1; theBall.x += theBall.speed; theBall.y += theBall.gravity; } else { theBall.x += theBall.speed; theBall.y += theBall.gravity; } draw(); } function draw() { ctx.clearRect(0, 0, canvas.width, canvas.height); displayScore1(); displayScore2(); drawBox(player1); drawBox(player2); drawBox(midLine); drawBox(theBall); } function loop() { ballBounce(); input(player1); input(player2); window.requestAnimationFrame(loop); } loop();
Game Tutorial – Bounce Off Paddles And Scores
The last changes we will make to our code for now will be to have theBall bounce off the paddles, and if theBall passes by a paddle, the other player’s score will increase and theBall will be put back in play again.
We will change the ballCollision function.
Before now, this just changed the direction of the speed variable when theBall collided with the left and right walls.
Now we are going to change the direction of theBall if it hits the paddle.
If it passes by the paddles, the other player’s score increases and theBall is put back into play again.
Remember that the red font below is the code and the black font is my comment on what is happening here.
- function ballCollision() {
- if (((theBall.x + theBall.speed <= player1.x + player1.width) && (theBall.y + theBall.gravity > player1.y) && theBall.y + theBall.gravity <= player1.y + player1.height)) If theBall is in the same space as the player 1 paddle (&& means AND) if theBall will be in the same X position as the left paddle (player 1) AND the ball’s Y position is between the player 1 paddles top and bottom Y values, then they have collided
- || ((theBall.x + theBall.width + theBall.speed >= player2.x) && (theBall.y + theBall.gravity > player2.y) && theBall.y + theBall.gravity <= player2.y + player2.height))) { Here we are saying OR (||) and run the same checks against the player 2 paddle on the right
- theBall.speed = theBall.speed * -1; If theBall hits either paddle then change the direction by changing the speed value
- } else if (theBall.x + theBall.speed < player1.x) { If theBall doesn’t hit the left paddle, but goes past it then…
- score2 += 1; Increase player 2’s score by 1
- theBall.speed = theBall.speed * -1; Change the direction of theBall to go to the right
- theBall.x = 100 + theBall.speed; Reposition theBall and move it along the X axis
- theBall.y += theBall.gravity; Reposition theBall and move it along the Y axis
- } else if (theBall.x + theBall.speed > player2.x + player2.width) { This is similar to the above lines of code but increases Player 1’s score and repositions theBall and moves it towards the left
- score1 += 1;
- theBall.speed = theBall.speed * -1;
- theBall.x = 500 + theBall.speed;
- theBall.y += theBall.gravity;
- } else { If theBall doesn’t hit the paddles, or pass either paddle, then we want to move theBall as normal
- theBall.x += theBall.speed;
- theBall.y += theBall.gravity;
- }
- draw(); After recalculating theBall’s position and scores, we call the draw function next
- }
The code we are going to finish with is:
var canvas = document.getElementById('canvas'); var ctx = canvas.getContext('2d'); canvas.width = 650; canvas.height = 400; var score1 = 0; var score2 = 0; var keys = {}; window.addEventListener('keydown', function (e) { keys[e.keyCode] = true; e.preventDefault(); }); window.addEventListener('keyup', function (e) { delete keys[e.keyCode]; }); function Box(options) { this.x = options.x || 10; this.y = options.y || 10; this.width = options.width || 40; this.height = options.height || 50; this.color = options.color || '#FFFFFF'; this.speed = options.speed || 2; this.gravity = options.gravity || 2; } var player1 = new Box({ x: 10, y: 200, width: 15, height: 80, color: '#FFFFFF', gravity: 2 }); var player2 = new Box({ x: 625, y: 100, width: 15, height: 80, color: '#FFFFFF', gravity: 2 }); var midLine = new Box({ x: (canvas.width/2) - 2.5, y: -1, width: 5, height: canvas.height+1, color: '#FFFFFF' }); var theBall = new Box({ x: (canvas.width / 2), y: (canvas.height / 2), width: 15, height: 15, color: '#FF0000', speed: 1, gravity: 1 }) function input() { if (87 in keys) { if (player1.y - player1.gravity > 0) { player1.y -= player1.gravity; } } else if (83 in keys) { if (player1.y + player1.height + player1.gravity < canvas.height) { player1.y += player1.gravity; } } if (38 in keys) { if (player2.y - player2.gravity > 0) { player2.y -= player2.gravity; } } else if (40 in keys) { if (player2.y + player2.height + player2.gravity < canvas.height) { player2.y += player2.gravity; } } } function drawBox(box) { ctx.fillStyle = box.color; ctx.fillRect(box.x, box.y, box.width, box.height); } function displayScore1() { ctx.font = "20px Arial"; ctx.fillStyle = "rgb(255,255,255)"; var str1 = score1; ctx.fillText(str1, (canvas.width/2) - 50, 30); } function displayScore2() { ctx.font = "20px Arial"; ctx.fillStyle = "rgb(255,255,255)"; var str2 = score2; ctx.fillText(str2, (canvas.width / 2) + 50, 30); } function ballBounce() { if (((theBall.y+theBall.gravity) <= 0) || ((theBall.y+theBall.gravity+theBall.height) >= canvas.height)){ theBall.gravity = theBall.gravity * -1; theBall.y += theBall.gravity; theBall.x += theBall.speed; } else { theBall.x += theBall.speed; theBall.y += theBall.gravity } ballCollision(); } function ballCollision() { if (((theBall.x + theBall.speed <= player1.x + player1.width) && (theBall.y + theBall.gravity > player1.y) && (theBall.y + theBall.gravity <= player1.y + player1.height)) || ((theBall.x + theBall.width + theBall.speed >= player2.x) && (theBall.y + theBall.gravity > player2.y) && (theBall.y + theBall.gravity <= player2.y + player2.height))) { theBall.speed = theBall.speed * -1; } else if (theBall.x + theBall.speed < player1.x) { score2 += 1; theBall.speed = theBall.speed * -1; theBall.x = 100 + theBall.speed; theBall.y += theBall.gravity; } else if (theBall.x + theBall.speed > player2.x + player2.width) { score1 += 1; theBall.speed = theBall.speed * -1; theBall.x = 500 + theBall.speed; theBall.y += theBall.gravity; } else { theBall.x += theBall.speed; theBall.y += theBall.gravity; } draw(); } function draw() { ctx.clearRect(0, 0, canvas.width, canvas.height); displayScore1(); displayScore2(); drawBox(player1); drawBox(player2); drawBox(midLine); drawBox(theBall); } function loop() { ballBounce(); input(player1); input(player2); window.requestAnimationFrame(loop); } loop();
Review
Save your js file and refresh your browser.
There are some glitches with the collision detection that you may or may not notice.
As we continue with these posts we will improve on our collision detection.
There is a lot of code in the ballCollision function. Break down this function into smaller pieces to see if you can follow it.
Try recreating the code yourself, and even make changes if you want.
Can you change the speed of theBall?
Can you amend the code so that theBall moves faster the higher the scores go?
Can you change the colors of your objects and canvas area?
In later posts we will also look at ending a game.
In the next post we will talk more about arrays as we move towards making our next game (Breakout).