a pong battle to the death on an HTML5 canvas

June 19th, 2010 § 7

Download the full source.

The first (and only) HTML5 element we need is a canvas to draw our stuff on.

<canvas id="game" width="500" height="350">
	<p>Your browser doesn't support canvas.</p>
</canvas>

Using  jQuery for simplicity we get a reference to the canvas element and define constants and variables for the game.

var canvas = $('#game');
var ctx =  canvas[0].getContext('2d');

const game_width = $("#game").width();
const game_height = $("#game").height();

const paddle_height = 60;
const paddle_width = 10;
const paddle_speed=8;
const ball_max_speed = 30;

var ball_x;
var ball_y;
var ball_x_speed;
var ball_y_speed;

var hits = 0;

var p1_paddle_x = 20;
var p1_paddle_y = game_height/2-paddle_height/2;
var p2_paddle_x = game_width-paddle_width-20;
var p2_paddle_y = game_height/2-paddle_height/2;

var p1_score=0;
var p2_score=0;

Now to draw the game background

//define the fill color
ctx.fillStyle = "#fb2";
//start drawing
ctx.beginPath();
//draw a rectangle
ctx.rect(0,0,game_width,game_height);
//this is not necessary as the path will close before fill()
ctx.closePath();
//do the fill
ctx.fill();

The above javascript  gives the following output

To draw the ball and the paddles:

//draw the ball
ctx.fillStyle = "#f55";
ctx.beginPath();
ctx.arc(ball_x, ball_y, 10, 0, Math.PI*2, true);
ctx.closePath();
ctx.fill();
//draw the left paddle
ctx.beginPath();
ctx.rect(p1_paddle_x,p1_paddle_y,paddle_width,paddle_height);
ctx.closePath();
ctx.fill();
//draw the right paddle
ctx.beginPath();
ctx.rect(p2_paddle_x,p2_paddle_y,paddle_width,paddle_height);
ctx.closePath();
ctx.fill();
//draw a line to separate the playing field
ctx.fillStyle = "#ddd";
ctx.beginPath();
ctx.rect(game_width/2-1,0,2,game_height);
ctx.closePath();
ctx.fill();

Each player’s score can be drawn with the fillText function.

ctx.textBaseline = "top";
ctx.fillStyle = "#000";
ctx.font = "bold 13px sans-serif";
ctx.fillText("( "+p1_score+" )", 40, 5);
ctx.fillText("( "+p2_score+" )", game_width-60, 5);

To make it look a little bit better we can use a drop shadow on the paddles and ball

ctx.shadowOffsetX = 2;
ctx.shadowOffsetY = 5;
ctx.shadowBlur = 10;

Now in order to make things move we need to clear the canvas and redraw the updated positions of the paddles and the ball. This can be achieved using a setInterval calling a draw function every 10 milliseconds.

setInterval(draw, 10);

function draw() {
   ctx.clearRect(0,0,game_width,game_height);
   ...
   calculate and draw stuff
   ...
   ...
}

Making the ball move. We need the ball to bounce off the top and bottom , bounce off the paddles and when it leaves the stage to keep score and move to center with a random direction

// bounce off the paddles
// first we check the x coordinate to see if the ball
// is in the paddle area.
// Then if the y coordinate of the ball is within the
// paddle dimensions reverse the x speed
// we also keep track of the bounces so that we can
// increase the ball speed
if (ball_x+ball_x_speed>p2_paddle_x)
	if (ball_y+ball_y_speed>=p2_paddle_y && ball_y+ball_y_speed<p2_paddle_y+paddle_height) {
		ball_x_speed *= -1;
		hits++;
	}

if (ball_x+ball_x_speed<=p1_paddle_x+paddle_width)
	if (ball_y+ball_y_speed>p1_paddle_y && ball_y+ball_y_speed<p1_paddle_y+paddle_height) {
		ball_x_speed *= -1;
		hits++;
	}
// to bounce off the top and bottom simply check
// the y of the ball and reverse the vertical speed
if (ball_y + ball_y_speed > game_height || ball_y + ball_y_speed < 5)
  ball_y_speed = -ball_y_speed;

// the ball has gone beyond the game_width
// so increase p1's score and reposition ball
if (ball_x + ball_x_speed > game_width) {
	p1_score++;
	new_ball()
}
// the ball has gone off the lest side. point for player 2
if (ball_x - ball_x_speed < 0 ) {
	p2_score++;
	new_ball()
}
// for every 3 bounces of the paddles
// we increase the ball speed, up to maximum
if (hits == 3) {
	if (Math.abs(ball_x_speed) < ball_max_speed && Math.abs(ball_y_speed) < ball_max_speed) {
		ball_x_speed += 1.3;
		ball_y_speed *= 1.3;
		hits = 0;
	}
}
//calculate the ball's position and draw it
ball_x += ball_x_speed;
ball_y += ball_y_speed;
ctx.fillStyle = "#f55";
ctx.beginPath();
ctx.arc(ball_x, ball_y, 10, 0, Math.PI*2, true);
ctx.closePath();
ctx.fill();

To get a new ball after each point we create a random number from -5 to 5 for both speeds and reposition the ball

function new_ball() {
			var adjustedHigh = (parseFloat(5) - parseFloat(-5)) + 1;
			var numRand=0;
			while (numRand==0) {
				numRand = Math.floor(Math.random()*adjustedHigh) + parseFloat(-5);
			}
			ball_x_speed = numRand;
			ball_y_speed = numRand;
			hits=0;
			ball_x = 250;
			ball_y = 175;
		}

Lastly we need to make the paddles move and try to hit the ball. Here we have two cases. The first case is when the ball is coming towards the paddle. In this case we check the ball position and move the paddle accordingly.

The second case is the ball leaving the paddle and going to the other direction. In this case we slowly bring back the paddle to the middle position. This is the code for the left paddle.

// if the ball is coming towards the paddle
if (ball_x_speed < 0) {
// if ball is higher than paddle move up
	if (ball_y < p1_paddle_y + paddle_height / 2 && p1_paddle_y>0)
		p1_paddle_y -= paddle_speed;
// if ball is lower than paddle move down
	if (ball_y > p1_paddle_y + paddle_height / 2 && p1_paddle_y + paddle_height<game_height)
		p1_paddle_y += paddle_speed;
}
// else slowly go back to the middle
else {
	if (p1_paddle_y<game_height/2-paddle_height/2) p1_paddle_y += 2;
	else if (p1_paddle_y>game_height/2-paddle_height/2) p1_paddle_y -= 2;
}

Here is the full source:

<script type="text/javascript">
	$(document).ready (function () {

		var canvas = $('#game');
	 	var ctx =  canvas[0].getContext('2d');
		const game_width = $("#game").width();
		const game_height = $("#game").height();
		const paddle_height = 60;
			const paddle_width = 10;
		const paddle_speed=8;
		const ball_max_speed = 30;

		var ball_x;
		var ball_y;
		var ball_x_speed;
		var ball_y_speed;

		var hits = 0;

		var p1_paddle_x = 20;
		var p1_paddle_y = game_height/2-paddle_height/2;
		var p2_paddle_x = game_width-paddle_width-20;
		var p2_paddle_y = game_height/2-paddle_height/2;

		var p1_score=0;
		var p2_score=0;

		setInterval(draw, 10);

		function new_ball() {
			var adjustedHigh = (parseFloat(5) - parseFloat(-5)) + 1;
			var numRand=0;
			while (numRand==0) {
				numRand = Math.floor(Math.random()*adjustedHigh) + parseFloat(-5);
			}
			ball_x_speed = numRand;
			ball_y_speed = numRand;
			hits=0;
			ball_x = 250;
			ball_y = 175;
		}
		function draw() {
			if (!ball_x) new_ball();
			ctx.clearRect(0,0,game_width,game_height);
			ctx.fillStyle = "#fb2";
			ctx.beginPath();
				ctx.rect(0,0,game_width,game_height);
				ctx.closePath();
				ctx.fill();

			ctx.shadowOffsetX = 0;
			ctx.shadowOffsetY = 0;
			ctx.shadowBlur = 0;

			ctx.textBaseline = "top";
			ctx.fillStyle = "#000";
			ctx.font = "bold 13px sans-serif";
			ctx.fillText("( "+p1_score+" )", 40, 5);
			ctx.fillText("( "+p2_score+" )", game_width-60, 5);

			ctx.shadowOffsetX = 2;
			ctx.shadowOffsetY = 5;
			ctx.shadowBlur = 10;

			ctx.shadowColor = "black";
			ctx.fillStyle = "#ddd";
			ctx.beginPath();
				ctx.rect(game_width/2-1,0,2,game_height);
				ctx.closePath();
				ctx.fill();

			if (ball_x+ball_x_speed>p2_paddle_x)
				if (ball_y+ball_y_speed>=p2_paddle_y && ball_y+ball_y_speed<p2_paddle_y+paddle_height) {
					ball_x_speed *= -1;
					hits++;
				}
			if (ball_x+ball_x_speed<=p1_paddle_x+paddle_width)
				if (ball_y+ball_y_speed>p1_paddle_y && ball_y+ball_y_speed<p1_paddle_y+paddle_height) {
					ball_x_speed *= -1;
					hits++;
				}
			if (ball_y + ball_y_speed > game_height || ball_y + ball_y_speed < 5)
			  ball_y_speed = -ball_y_speed;
			if (ball_x + ball_x_speed > game_width) {
				p1_score++;
				new_ball()
			}
			if (ball_x - ball_x_speed < 0 ) {
				p2_score++;
				new_ball()
			}
			if (hits == 3) {
				if (Math.abs(ball_x_speed) < ball_max_speed && Math.abs(ball_y_speed) < ball_max_speed) {
					ball_x_speed += 1.3;
					ball_y_speed *= 1.3;
					hits = 0;
				}
			}

		  	ball_x += ball_x_speed;
			ball_y += ball_y_speed;
			if (Math.abs(ball_x_speed)<2.5) ball_x_speed*=5;

			ctx.fillStyle = "#f55";
			ctx.beginPath();
			ctx.arc(ball_x, ball_y, 10, 0, Math.PI*2, true);
			ctx.closePath();
			ctx.fill();

			ctx.fillStyle = "#1e1";

			if (ball_x_speed < 0) {
				if (ball_y < p1_paddle_y + paddle_height / 2 && p1_paddle_y>0)
					p1_paddle_y -= paddle_speed;
				if (ball_y > p1_paddle_y + paddle_height / 2 && p1_paddle_y + paddle_height<game_height)// && act > 4)
					p1_paddle_y += paddle_speed;
			}
			else {
				if (p1_paddle_y<game_height/2-paddle_height/2) p1_paddle_y += 2;
				else if (p1_paddle_y>game_height/2-paddle_height/2) p1_paddle_y -= 2;
			}
			ctx.beginPath();
				ctx.rect(p1_paddle_x,p1_paddle_y,paddle_width,paddle_height);
				ctx.closePath();
				ctx.fill();

			if (ball_x_speed > 0) {
				if (ball_y < p2_paddle_y + paddle_height / 2 && p2_paddle_y>0)
					p2_paddle_y -= paddle_speed;
				if (ball_y > p2_paddle_y + paddle_height / 2 && p2_paddle_y + paddle_height<game_height)// && act > 4)
					p2_paddle_y += paddle_speed;
			}
			else {
				if (p2_paddle_y<game_height/2-paddle_height/2) p2_paddle_y += 2;
				else if (p2_paddle_y>game_height/2-paddle_height/2) p2_paddle_y -= 2;
			}

			ctx.beginPath();
			ctx.rect(p2_paddle_x,p2_paddle_y,paddle_width,paddle_height);
			ctx.closePath();
			ctx.fill();

		}
	})

Download the full source.

This is my first attempt on anything HTML5 so comments and suggestions are welcome.

§ 7 Responses to “a pong battle to the death on an HTML5 canvas”

  • That looks cool. I probably would have abstracted the shiz out of the implementation and handled collision detection differently but that’s another story. :)

    Have you considered treating the paddles and the ball as canvasii of their own instead of rendering them all on the same canvas?

    I suppose this would add some complexity to the code. I’m not sure if this sort of overlaying works with the canvas element but that’s easy enough to find out. Perhaps you just need to tweak the element Z index and it will be just fine.

  • Apparently layering works just the way I imagined. See http://stackoverflow.com/questions/3008635/html5-canvas-element-multiple-layers . Of course in this case you have to tweak the coordinates and size of the canvases a bit. The idea should be valid, though.

    • alex alex says:

      Hi Juho,
      thanks for the comments! I believe your suggestion will increase performance :) I also plan to update the code to redraw only the changed parts of the canvas.

  • Arif Arif says:

    By Rob Larsen January 3, 2013 – 9:57 am1) Thanks. Syntax highlighting to the recuse.2) True. I could have wrapped it in document.addEventListener(“DOMContentLoaded”, etc) and didn’t. I didn’t really think about it. For better or worse, wrapping this in a jQuery ready function is going to be clearer to people who are going to get something out of a basic introduction to the canvas element.

  • Your thinking matches mine – great minds think alike!

  • Muzza Muzza says:

    By January 3, 2013 – 9:50 am1) You’re missing a on line 3 of your seocnd code block. You’ve also got a seocnd ; on line 342) You have this wrapped in a jQuery ready function, however none of the code is jQuery.

  • Your article perfectly shows what I needed to know, thanks!

  • § Leave a Reply

What's this?

You are currently reading a pong battle to the death on an HTML5 canvas at Discorganized.

meta

Partly powered by CleverPlugins.com