JavaScript Canvas Fun: Pong
Earlier this week I rediscovered some old games I'd written, and I realized that I had not yet done a JavaScript version of Pong. I did versions of Tetris and Snake, perennial favorites of mine to implement, but somehow I'd forgotten about Pong. I think Pong was probably the first game I ever tried to copy, and it has a special place in my early-programmer's memory.
So I set out last night to put together a JavaScript canvas version of Pong, which you can play below:
Score: 0 - 0
Source code
Common = window.Common || {};
(function(exports) {
// Key-codes.
var UP = 38, DOWN = 40;
if (typeof exports.Events === 'undefined') {
exports.Events = {'emit': function() {}};
}
function Paddle(game, leftOrRight, offset) {
this.game = game;
// Distance between paddle and the outer edge.
this.offset = offset || 4;
// The paddle width and height are proportional to the size
// of the game board.
this.width = Math.ceil(game.width / 100);
this.height = Math.ceil((game.height / 10) * game.difficulty);
// Position the paddle, centering it vertically.
this.leftOrRight = leftOrRight;
if (leftOrRight == 'left') {
this.x = this.offset;
} else {
this.x = this.game.width - this.width - this.offset;
}
this.y = Math.ceil((this.game.height - this.height) / 2);
this.startY = this.y;
// The speed at which the paddle moves is proportional to the
// size of the paddle (1/10th of a paddle per move).
this.dy = Math.ceil(this.height / 10);
}
Paddle.prototype.reset = function() {
this.y = this.startY;
}
Paddle.prototype.draw = function(ctx) {
ctx.fillRect(this.x, this.y, this.width, this.height);
}
Paddle.prototype.move = function(direction) {
// Move the paddle up or down.
if (direction > 0) {
this.y += this.dy;
if (this.y + this.height > this.game.height) {
this.y = this.game.height - this.height;
}
} else if (direction < 0) {
this.y -= this.dy;
if (this.y < 0) { this.y = 0; }
}
}
Paddle.prototype.autoMove = function(ball) {
var movingAway = (
(ball.dx > 0 && this.leftOrRight == 'left') ||
(ball.dx < 0 && this.leftOrRight != 'left'));
if (movingAway) {
// Move the paddle
if (this.y < (this.startY - this.dy)) {
this.y += this.dy;
} else if (this.y > this.startY) {
this.y -= this.dy;
}
} else {
// Anticipate the position of the ball.
var fakeBall = new Ball(this.game);
fakeBall.x = ball.x;
fakeBall.y = ball.y;
fakeBall.dx = ball.dx;
fakeBall.dy = ball.dy;
while (Math.abs(fakeBall.x - this.x) > fakeBall.size) {
fakeBall.move();
}
var destY = Math.ceil(fakeBall.y - (this.height / 2));
if (this.y < destY) {
this.y += this.dy;
} else if (this.y > (destY + this.dy)) {
this.y -= this.dy;
}
}
}
function Ball(game) {
this.game = game;
// The size of the ball is 1% of the screen (same as the
// width of the paddle).
this.size = Math.ceil(game.width / 100);
// The ball always starts in the center of the screen.
this.startX = Math.ceil((this.game.width - this.size) / 2);
this.startY = Math.ceil((this.game.height - this.size) / 2);
// Initialize the ball-speed as 0, 0.
this.dx = 0;
this.dy = 0;
// The maximum speed at which the ball can move in any
// direction is equal to 1% of the screen width.
this.maxD = this.game.width / 100;
// Initialize the ball.
this.reset();
}
Ball.prototype.reset = function(direction) {
direction = direction || -1;
// Center the ball.
this.x = this.startX;
this.y = this.startY;
// The ball starts out at half the max speed.
this.dx = (this.maxD / 2) * direction;
// The vertical velocity is random.
this.dy = Math.max(
parseInt(Math.random() * this.maxD),
this.maxD / 4);
this.dy *= (Math.random() < .5)? 1 : -1;
}
Ball.prototype.move = function() {
// Move the ball, bouncing it off horizontal walls and
// triggering an event if the ball hits a vertical wall.
this.x += this.dx;
this.y += this.dy;
if (this.x <= 0) {
this.game.rightWins();
} else if (this.x + this.size >= this.game.width) {
this.game.leftWins();
}
if (this.y <= 0) {
this.dy *= -1;
this.y = 0;
} else if (this.y + this.size >= this.game.height) {
this.dy *= -1;
this.y = this.game.height - this.size;
}
}
Ball.prototype.draw = function(ctx) {
ctx.fillRect(this.x, this.y, this.size, this.size);
}
Ball.prototype.collisionTest = function(paddle) {
var bounds;
if (this.dx < 0) {
bounds = [
paddle.x, paddle.x + paddle.width,
paddle.y - this.size, paddle.y + paddle.height];
} else {
bounds = [
paddle.x - this.size, paddle.x,
paddle.y - this.size, paddle.y + paddle.height];
}
return (
(this.x >= bounds[0] && this.x <= bounds[1]) &&
(this.y >= bounds[2] && this.y <= bounds[3]));
}
Ball.prototype.reflectOnCollision = function(paddle) {
if (this.collisionTest(paddle)) {
var verticalOffset = Math.abs(this.y - paddle.y);
if (verticalOffset < (paddle.height * .4)) {
this.dy -= 1;
} else if (verticalOffset >= (paddle.height * .6)) {
this.dy += 1;
}
if ((Math.abs(this.dx) < this.maxD) && (Math.random() < .3)) {
if (this.dx < 0) {
this.dx -= 1;
} else {
this.dx += 1;
}
}
if (this.dx < 0) {
this.x = paddle.x + paddle.width;
} else {
this.x = paddle.x - this.size;
}
this.dx *= -1;
}
}
function Pong(canvasId, config) {
this.canvas = document.getElementById(canvasId);
// Read the width and height directly from the HTML canvas.
this.width = this.canvas.width;
this.height = this.canvas.height;
this.config = config? config : {};
this.interval = this.config.interval || 20;
// 0.5 == hard, 1.0 == normal, 1.5 == easy
this.difficulty = this.config.difficulty || 1.0;
this.color = this.config.color || '#0ce3ac';
this.background = this.config.background || '#000000';
// Initialize game, binding event handlers, etc.
this.initialize();
}
Pong.prototype.initialize = function() {
if (this.canvas.getContext) {
this.ctx = this.canvas.getContext('2d');
} else {
return false;
}
// Create paddles for the player and computer.
var playerSide = this.config.playerSide || 'left';
this.player = new Paddle(this, playerSide);
this.computer = new Paddle(this, playerSide == 'left' ? 'right': 'left');
if (playerSide == 'left') {
this.left = this.player;
this.right = this.computer;
} else {
this.left = this.computer;
this.right = this.player;
}
// Create the game ball.
this.ball = new Ball(this);
this.score = {'left': 0, 'right': 0};
this.keyState = {};
var self = this;
var makeHandler = function(state) {
return function(e) {
var keyCode = (e || window.event).keyCode;
if (state) {
self.keyState[keyCode] = true;
} else {
delete self.keyState[keyCode];
}
if (keyCode == UP || keyCode == DOWN) {
return false;
} else {
return true;
}
}
}
document.onkeydown = makeHandler(true);
document.onkeyup = makeHandler(false);
this.newGame();
this.gameLoop();
return true;
}
Pong.prototype.newGame = function() {
this.ball.reset();
this.player.reset();
this.computer.reset();
}
Pong.prototype.rightWins = function() {
this.score['right'] += 1;
exports.Events.emit('right-score', this.score);
this.newGame();
}
Pong.prototype.leftWins = function() {
this.score['left'] += 1;
exports.Events.emit('left-score', this.score);
this.newGame();
}
Pong.prototype.drawElements = function() {
// Draw a black background.
this.ctx.fillStyle = this.background;
this.ctx.fillRect(0, 0, this.width, this.height);
this.ctx.fillStyle = this.color;
this.player.draw(this.ctx);
this.computer.draw(this.ctx);
this.ball.draw(this.ctx);
}
Pong.prototype.gameLoop = function() {
var self = this;
this.ball.move();
if (this.keyState[DOWN]) {
this.player.move(1);
} else if (this.keyState[UP]) {
this.player.move(-1);
}
this.computer.autoMove(this.ball);
if (this.ball.dx > 0) {
this.ball.reflectOnCollision(this.right);
} else if (this.ball.dx < 0) {
this.ball.reflectOnCollision(this.left);
}
this.drawElements();
setTimeout(function() { self.gameLoop(); }, this.interval);
}
exports.Pong = Pong;
})(Common);
Comments (0)
Commenting has been closed.