I spent some time yesterday reading an excellent article on a simple Canvas-based version of the old Snake game: Create a mobile version of Snake with HTML5 canvas and JavaScript. This article, by Eoin McGrath, does a great job explaining how he used Canvas to animate the game. (If you've never played Snake before, think Tron light cycles, single player, and not as cool.) I've been meaning to work on some simple games with Canvas, and while there are some very cool frameworks out there (EaselJS and Impact for example), I wanted to play around a bit with the raw code before I started punting some of the grunt work to other libraries. What follows is a series of experiments based on McGrath's core code set. Be gentle - this is my first time.
I began by creating a ball that would bounce around. I know - not rocket science. McGrath's original code animated the snake and had it die as soon as it hit a wall. For my logic I needed to simply make the ball bounce. Here's the code for version 1:
<head>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"></script>
<script>
$(document).ready(function() { var canvas = $("#gamearea")[0];
canvas.width = 400;
canvas.height = 400;
var ctx = canvas.getContext("2d"); var draw = {
clear: function () {
ctx.clearRect(0, 0, canvas.width, canvas.height);
}, rect: function (x, y, w, h, col) {
ctx.fillStyle = col;
ctx.fillRect(x, y, w, h);
}, circle: function (x, y, radius, col) {
ctx.fillStyle = col;
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI*2, true);
ctx.closePath();
ctx.fill();
}, text: function (str, x, y, size, col) {
ctx.font = 'bold ' + size + 'px monospace';
ctx.fillStyle = col;
ctx.fillText(str, x, y);
}
}; var ballOb = function() { this.init = function() {
this.speed = 8;
this.x = 19;
this.y = 89;
this.w = this.h = 10;
this.col = "darkgreen";
this.xdir = this.ydir = 1;
} this.move = function() { if ((this.x-this.w) < 0 || this.x > (canvas.width - this.w)) this.xdir*=-1;
if ((this.y-this.w) < 0 || this.y > (canvas.height - this.h)) this.ydir*=-1; this.x += (this.xdir * this.speed);
this.y += (this.ydir * this.speed); } this.draw = function () {
draw.circle(this.x, this.y, this.w, this.col);
} } var ball = new ballOb();
ball.init(); function loop() {
draw.clear();
ball.move();
ball.draw();
} setInterval(loop, 30);
})
</script>
</head> <body>
<canvas id="gamearea" style="background-color:red"></canvas> </body> </html>
<!DOCTYPE HTML>
<html>
You can demo this here: http://www.coldfusionjedi.com/demos/2011/nov/15_2/index.html. Notice the wall hit isn't exactly perfect. I made it a bit better as I went on. I apologize for the horrible color schemes. I wanted something very clear to see.
In the next iteration, I added a paddle object and added event listeners so I could move the paddle:
<head>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"></script>
<script>
var ball;
var paddle; $(document).ready(function() { var canvas = $("#gamearea")[0];
canvas.width = 400;
canvas.height = 400;
var ctx = canvas.getContext("2d");
var input = {
left: false,
right: false
}; $(window).keydown(function(e) {
switch (e.keyCode) {
case 37: input.left = true; break; $(window).keyup(function(e) {
switch (e.keyCode) {
case 37: input.left = false; break; var draw = {
clear: function () {
ctx.clearRect(0, 0, canvas.width, canvas.height);
}, rect: function (x, y, w, h, col) {
ctx.fillStyle = col;
ctx.fillRect(x, y, w, h);
}, circle: function (x, y, radius, col) {
ctx.fillStyle = col;
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI*2, true);
ctx.closePath();
ctx.fill();
}, text: function (str, x, y, size, col) {
ctx.font = 'bold ' + size + 'px monospace';
ctx.fillStyle = col;
ctx.fillText(str, x, y);
}
}; var ballOb = function() { this.init = function() {
this.speed = 8;
this.x = 19;
this.y = 89;
this.w = this.h = 10;
this.col = "darkgreen";
this.xdir = this.ydir = 1;
} this.move = function() { if (this.x < 0 || this.x > (canvas.width-this.w)) this.xdir*=-1;
if (this.y < 0 || this.y > (canvas.height-this.h)) this.ydir*=-1; this.x += (this.xdir * this.speed);
this.y += (this.ydir * this.speed); //handle hitting the edge
if(this.x-this.w < 0) { this.x = 0+this.w; this.xdir=1 }
if(this.x+this.w > canvas.width) { this.x = canvas.width-this.w; this.xdir= -1 }
if(this.y-this.w < 0) { this.y = 0+this.w; this.ydir=1 }
if(this.y+this.w > canvas.height) { this.y = canvas.height-this.w; this.ydir=-1 }
} this.draw = function () {
draw.circle(this.x, this.y, this.w, this.col);
} } var paddleOb = function() { this.init = function() {
this.speed = 8;
this.w = 0.25 * canvas.width;
this.h = 20;
this.x = 10;
this.y = canvas.height - this.h - 10;
this.col = "white";
//this.xdir = this.ydir = 1;
} this.move = function() {
if(input.left) {
this.x -= this.speed;
if(this.x < 0) this.x=0;
}
if(input.right) {
this.x += this.speed;
if((this.x+this.w) > canvas.width) this.x=canvas.width-this.w;
}
} this.draw = function () {
draw.rect(this.x, this.y, this.w, this.h,this.col);
} } ball = new ballOb();
ball.init(); paddle = new paddleOb();
paddle.init(); function loop() {
draw.clear();
ball.move();
ball.draw();
paddle.draw();
paddle.move();
} setInterval(loop, 30);
})
</script>
</head> <body>
<canvas id="gamearea" style="background-color:red"></canvas> </body> </html>
<!DOCTYPE HTML>
<html>
case 39: input.right = true; break;
}
});
case 39: input.right = false; break;
}
});
And here is the demo: http://www.coldfusionjedi.com/demos/2011/nov/15_2/index2.html. Note that this version has better detection for hitting the walls.
So next I added support for noticing when I hit the paddle and keeping score. Nothing too crazy - just a call to the collides method McGrath created for this own game.
<head>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"></script>
<script>
var ball;
var paddle; $(document).ready(function() { var canvas = $("#gamearea")[0];
canvas.width = 400;
canvas.height = 400;
var score = 0; var ctx = canvas.getContext("2d");
var input = {
left: false,
right: false
}; $(window).keydown(function(e) {
switch (e.keyCode) {
case 37: input.left = true; break; $(window).keyup(function(e) {
switch (e.keyCode) {
case 37: input.left = false; break; var draw = {
clear: function () {
ctx.clearRect(0, 0, canvas.width, canvas.height);
}, rect: function (x, y, w, h, col) {
ctx.fillStyle = col;
ctx.fillRect(x, y, w, h);
}, circle: function (x, y, radius, col) {
ctx.fillStyle = col;
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI*2, true);
ctx.closePath();
ctx.fill();
}, text: function (str, x, y, size, col) {
ctx.font = 'bold ' + size + 'px monospace';
ctx.fillStyle = col;
ctx.fillText(str, x, y);
}
}; var ballOb = function() { this.init = function() {
this.speed = 8;
this.x = 19;
this.y = 89;
this.w = this.h = 10;
this.col = "darkgreen";
this.xdir = this.ydir = 1;
} this.move = function() { if (this.x < 0 || this.x > (canvas.width-this.w)) this.xdir*=-1;
if (this.y < 0 || this.y > (canvas.height-this.h)) this.ydir*=-1; this.x += (this.xdir * this.speed);
this.y += (this.ydir * this.speed); //handle hitting the edge
if(this.x-this.w < 0) { this.x = 0+this.w; this.xdir=1 }
if(this.x+this.w > canvas.width) { this.x = canvas.width-this.w; this.xdir= -1 }
if(this.y-this.w < 0) { this.y = 0+this.w; this.ydir=1 }
if(this.y+this.w > canvas.height) { this.y = canvas.height-this.w; this.ydir=-1; score++ } //handle hitting paddle
if(this.collides(paddle)) this.ydir = -1;
} this.draw = function () {
draw.circle(this.x, this.y, this.w, this.col);
} this.collides = function(obj) { // this sprite's rectangle
this.left = this.x;
this.right = this.x + this.w;
this.top = this.y;
this.bottom = this.y + this.h; // other object's rectangle
// note: we assume that obj has w, h, w & y properties
obj.left = obj.x;
obj.right = obj.x + obj.w;
obj.top = obj.y;
obj.bottom = obj.y + obj.h; // determine if not intersecting
if (this.bottom < obj.top) { return false; }
if (this.top > obj.bottom) { return false; } if (this.right < obj.left) { return false; }
if (this.left > obj.right) { return false; } // otherwise, it's a hit
return true;
}; } var paddleOb = function() { this.init = function() {
this.speed = 8;
this.w = 0.25 * canvas.width;
this.h = 20;
this.x = 10;
this.y = canvas.height - this.h - 10;
this.col = "white";
//this.xdir = this.ydir = 1;
} this.move = function() {
if(input.left) {
this.x -= this.speed;
if(this.x < 0) this.x=0;
}
if(input.right) {
this.x += this.speed;
if((this.x+this.w) > canvas.width) this.x=canvas.width-this.w;
}
} this.draw = function () {
draw.rect(this.x, this.y, this.w, this.h,this.col);
} } ball = new ballOb();
ball.init(); paddle = new paddleOb();
paddle.init(); function loop() {
draw.clear();
ball.move();
ball.draw();
paddle.draw();
paddle.move();
draw.text("Score: "+score, 10, 20, 20);
} setInterval(loop, 30);
})
</script>
</head> <body>
<canvas id="gamearea" style="background-color:red"></canvas> </body> </html>
<!DOCTYPE HTML>
<html>
case 39: input.right = true; break;
}
});
case 39: input.right = false; break;
}
});
And this demo may be found here: http://www.coldfusionjedi.com/demos/2011/nov/15_2/index3.html
I then got mean and built this one: http://www.coldfusionjedi.com/demos/2011/nov/15_2/index4.html Don't click. Seriously. I warned you.
Finally I went the extra step of doing a Google search and making the graphics not quite so ugly.
<head>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"></script>
<script>
var ball;
var paddle; $(document).ready(function() { var canvas = $("#gamearea")[0];
canvas.width = 400;
canvas.height = 400;
var score = 0; var ctx = canvas.getContext("2d");
var input = {
left: false,
right: false
}; $(window).keydown(function(e) {
switch (e.keyCode) {
case 37: input.left = true; break; $(window).keyup(function(e) {
switch (e.keyCode) {
case 37: input.left = false; break; var draw = {
clear: function () {
ctx.clearRect(0, 0, canvas.width, canvas.height);
}, rect: function (x, y, w, h, col) {
ctx.fillStyle = col;
ctx.fillRect(x, y, w, h);
}, circle: function (x, y, radius, col) {
ctx.fillStyle = col;
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI*2, true);
ctx.closePath();
ctx.fill();
}, text: function (str, x, y, size, col) {
ctx.font = 'bold ' + size + 'px monospace';
ctx.fillStyle = col;
ctx.fillText(str, x, y);
}
}; var ballOb = function() { this.init = function() {
this.speed = 10;
this.x = 19;
this.y = 89;
this.w = this.h = 10;
this.col = "green";
this.xdir = this.ydir = 1;
} this.move = function() { if (this.x < 0 || this.x > (canvas.width-this.w)) this.xdir*=-1;
if (this.y < 0 || this.y > (canvas.height-this.h)) this.ydir*=-1; this.x += (this.xdir * this.speed);
this.y += (this.ydir * this.speed); //handle hitting the edge
if(this.x-this.w < 0) { this.x = 0+this.w; this.xdir=1 }
if(this.x+this.w > canvas.width) { this.x = canvas.width-this.w; this.xdir= -1 }
if(this.y-this.w < 0) { this.y = 0+this.w; this.ydir=1 }
if(this.y+this.w > canvas.height) { this.y = canvas.height-this.w; this.ydir=-1; score++ } //handle hitting paddle
if(this.collides(paddle)) this.ydir = -1;
} this.draw = function () {
draw.circle(this.x, this.y, this.w, this.col);
} this.collides = function(obj) { // this sprite's rectangle
this.left = this.x;
this.right = this.x + this.w;
this.top = this.y;
this.bottom = this.y + this.h; // other object's rectangle
// note: we assume that obj has w, h, w & y properties
obj.left = obj.x;
obj.right = obj.x + obj.w;
obj.top = obj.y;
obj.bottom = obj.y + obj.h; // determine if not intersecting
if (this.bottom < obj.top) { return false; }
if (this.top > obj.bottom) { return false; } if (this.right < obj.left) { return false; }
if (this.left > obj.right) { return false; } // otherwise, it's a hit
return true;
}; } var paddleOb = function() { this.init = function() {
this.speed = 8;
this.w = 0.25 * canvas.width;
this.h = 20;
this.x = 10;
this.y = canvas.height - this.h - 10;
this.col = "white";
//this.xdir = this.ydir = 1;
} this.move = function() {
if(input.left) {
this.x -= this.speed;
if(this.x < 0) this.x=0;
}
if(input.right) {
this.x += this.speed;
if((this.x+this.w) > canvas.width) this.x=canvas.width-this.w;
}
} this.draw = function () {
draw.rect(this.x, this.y, this.w, this.h,this.col);
} } ball = new ballOb();
ball.init(); paddle = new paddleOb();
paddle.init(); function loop() {
draw.clear();
ball.move();
ball.draw();
paddle.draw();
paddle.move();
draw.text("Score: "+score, 10, 20, 20);
} setInterval(loop, 30);
})
</script>
</head> <body>
<canvas id="gamearea" style="background:url(brick.jpg)"></canvas>
<p>
Brick wall picture credit: <a href="http://www.flickr.com/photos/richard_wasserman/5510266273/">Nutch Bicer</a>
</p>
</body> </html>
<!DOCTYPE HTML>
<html>
case 39: input.right = true; break;
}
});
case 39: input.right = false; break;
}
});
You can find this final demo here: http://www.coldfusionjedi.com/demos/2011/nov/15_2/index5.html.
Enjoy. I've got an idea for a simple zombie game name. Yes - I love my job. I really love my job.