Breakout game: Breakout.html

<!DOCTYPE html>

<html>
<head>

<title>Comp 86 example Breakout</title>
<!-- Based on: https://developer.mozilla.org/en-US/docs/Games/Tutorials/2D_Breakout_game_pure_JavaScript -->

<style>
	#myCanvas {
		background: #eee;
	}

	#buttonPanel {
		max-width: 480px;
		margin-top: 12px;
		text-align: center;
		background: #0095DD;
	}

	button {
		margin: 4px;
		color: #0095DD;
	}

	#scorePanel {
		max-width: 480px;
		background: #eee;
	}

	p.score {
		font-family: sans-serif;
		font-size: 24px;
		color: #0095DD;
	}
</style>

<script>
"use strict";

/*
 **************************************************
 * Main class 
 **************************************************
 */
class Main {
	constructor () {
		// Save this, for convenience
		this._ctx = myCanvas.getContext("2d");

		// Instantiate the bricks
		this._bricks = [] //*5 Instantiate the bricks
		for (let c=0; c<Brick.columnCount; c++) { //*5
			for (let r=0; r<Brick.rowCount; r++) { //*5
				this._bricks.push (new Brick (r, c)) //*5
			}
		}

		// Instantiate our main objects and connect them up
		this._paddle = new Paddle () //*18 Instantiate our main objects and connect them up
		this._rules = new Rules (this._paddle, this._bricks) //*18
		this._ball = new Ball (this._paddle, this._rules) //*18
		this._rules.init (this._ball) //*19 Delayed initialization
	}

	// Update everything and redraw for animation loop
	loop () { //*17 Animation: update then redraw
		if (this._rules.playing) { //*20 Pause/resume uses a state variable
			// Updates
			this._paddle.update() //*17
			this._ball.update() //*17
			this._rules.detectCollision(); //*17

			// Clear background
			this._ctx.clearRect(0, 0, myCanvas.width, myCanvas.height);

			// Draw
			this._ball.draw(this._ctx); //*17
			this._paddle.draw(this._ctx); //*17
			for (let b of this._bricks) b.draw(this._ctx) //*17
			this._rules.draw(); //*17
		}

		// Set up for next frame, use arrow function to override "this"
		requestAnimationFrame(() => this.loop()); //*17
	}
}

/*
 **************************************************
 * Ball class  
 **************************************************
 */ 
class Ball { //*1 Ball class, like Square
	constructor (paddle, rules) { //*1
		// Stash args
		this._paddle = paddle //*1
		this._rules = rules //*1

		// Our own ivars
		this._radius = 10; //*2 Maintains info
		this._x = myCanvas.width/2; //*2
		this._y = myCanvas.height-30; //*2
		this._dx = 2; //*2
		this._dy = -2; //*2
	}

	// Full pedantic style with private ivars, but with getter syntax here
	get x () { return this._x; } //*8 Use getter function
	get y () { return this._y; }

	draw (ctx) { //*3 Knows how to draw itself when told
		ctx.beginPath(); //*3
		ctx.arc(this._x, this._y, this._radius, 0, Math.PI*2); //*3
		ctx.fillStyle = "#0095DD"; //*3
		ctx.fill(); //*3
	}

	update () { //*4 Knows how to update itself when told
		// Normal update
		this._x += this._dx; //*4
		this._y += this._dy; //*4

		// U-turn ball at sides
		if (this._x + this._dx > myCanvas.width-this._radius || this._x + this._dx < this._radius) { //*13 Ball hits side or top = bounce back
			this._dx = -this._dx; //*13
		}

		// U-turn ball at top
		if (this._y + this._dy < this._radius) { //*13
			this._dy = -this._dy; //*13
		}

		// Ball hit bottom of screen
		if (this._y + this._dy > myCanvas.height-this._radius) { //*14 Ball hits paddle = bounce up
			if (this._paddle.collide (this)) { //*14
				// Hit paddle = turn around
				this._dy = -this._dy; //*14
			}
			else { //*15 Ball hits bottom = use  a life
				// Falls off screen = use a life
				this._rules.nextLife() //*15
			}
		}
	}

	// Ball hits a brick
	reverseY () {
		this._dy = -this._dy;
	}

	// Ball resets and speeds up
	nextLife() {
		this._x = myCanvas.width/2;
		this._y = myCanvas.height-30;
		this._dx = 4;
		this._dy = -4;
	}
}

/*
 **************************************************
 * Brick class
 **************************************************
 */
class Brick {
	constructor (row, col) {
		// Initialize our ivars
		this._x = (row * (Brick.width+Brick.padding)) + Brick.offsetLeft; //*6 Calculate Brick's x, y from r,c subscripts
		this._y = (col * (Brick.height+Brick.padding)) + Brick.offsetTop; //*6
		this._visible = true //*7 Use visible flag
	}

	remove () { //*7
		this._visible = false; //*7
	}

	draw (ctx) {
		if(this._visible) { //*7
			ctx.beginPath();
			ctx.rect (this._x, this._y, Brick.width, Brick.height);
			ctx.fillStyle = "#0095DD";
			ctx.fill();
		}
	}

	// It only counts if our visible==true
	collide (ball) { //*16 Check for ball hitting a brick
		return this._visible && ball.x > this._x && ball.x < this._x+Brick.width && ball.y > this._y && ball.y < this._y+Brick.height //*16
	}
}

// Class vars for Brick, handled awkwardly in JS like this
Brick.rowCount = 5;
Brick.columnCount = 3;
Brick.width = 75;
Brick.height = 20;
Brick.padding = 10;
Brick.offsetTop = 30;
Brick.offsetLeft = 30;

/*
 **************************************************
 * Paddle class
 **************************************************
 */
class Paddle {
	constructor () {
		// Initialize our own ivars
		this._height = 10;
		this._width = 75;
		this._x = (myCanvas.width-this._width)/2; //*9 Paddle: maintains X location, draws itself; Rules; draws itself in HTML elements

		// Paddle is also responsible for keyboard and mouse controls
		this._leftPressed = false
		this._rightPressed = false

		// Install callbacks
		document.addEventListener ("keydown", (e) => this.keyDownHandler (e)); //*10 Remember arrow key state, but don't act yet
		document.addEventListener("keyup", (e) => this.keyUpHandler (e)); //*10
		document.addEventListener("mousemove", (e) => this.mouseMoveHandler (e)); //*12 Mouse moves also changes paddle X location
	}

	draw (ctx) { //*9
		ctx.beginPath();
		ctx.fillStyle = "#0095DD";
		ctx.rect(this._x, myCanvas.height-this._height, this._width, this._height); //*9
		ctx.fill();
	}

	collide (ball) {
		return ball.x > this._x && ball.x < this._x + this._width
	}

	// Moves on every frame, while you are holding down a key
	update () { //*11 Update paddle if an arrow key is down 
		if (this._rightPressed && this._x < myCanvas.width-this._width) { //*11
			this._x += 7; //*11
		}
		else if (this._leftPressed && this._x > 0) { //*11
			this._x -= 7; //*11
		}
	}

	// Reset position for next game
	nextLife() {
		this._x = (myCanvas.width-this._width)/2;
	}

	// Keyboard callbacks for arrow keys down and up
	// here we just remember if they are up or down,
	// update() takes the action
	keyDownHandler (e) { //*10
		if(e.keyCode == 39) { //*10
			this._rightPressed = true; //*10
		}
		else if(e.keyCode == 37) {
			this._leftPressed = true;
		}
	}
	
	keyUpHandler (e) { //*10
		if(e.keyCode == 39) { //*10
			this._rightPressed = false; //*10
		}

		else if(e.keyCode == 37) { //*10
			this._leftPressed = false; //*10
		}
	}

	// Mouse callback
	mouseMoveHandler (e) { //*12
		const relativeX = e.clientX - myCanvas.offsetLeft;
		if (relativeX > 0 && relativeX < myCanvas.width) { //*12
			this._x = relativeX - this._width/2; //*12
		}
	}
}

/*
 **************************************************
 * Rules class
 **************************************************
 */
class Rules {
	constructor (paddle, bricks) {
		// Stash
		this._paddle = paddle
		this._bricks = bricks

		// Initialize
		this._score = 0;
		this._lives = 0;
		this._playing = true;
		
		// We will handle the button callbacks
		pauseButton.addEventListener ("click", () => this._pause())
		resumeButton.addEventListener ("click", () => this._resume())
		restartButton.addEventListener ("click", () => this._restart())
	}

	// Delayed initialization cause ball was not available when our constructor was called
	init (ball) { //*19
		this._ball = ball //*19
	}

	draw () {
		score.textContent = this._score //*9
		lives.textContent = this._lives //*9
	}

	// Tests the ball against every (visible) brick
	detectCollision () { //*16
		for (let brick of this._bricks) { //*16
			if(brick.collide (this._ball)) { //*16
				brick.remove () //*16
				this._ball.reverseY() //*16

				this._score++; //*16

				if (this._score == Brick.rowCount*Brick.columnCount) {
					alert ("YOU WIN, CONGRATS!");
					document.location.reload();
				}
			}
		}
	}

	nextLife () {
		this._lives++;
		this._ball.nextLife()
		this._paddle.nextLife()
	}

	get playing () {return this._playing} //*20

	// "Private" methods, denoted by "_"
	_pause() { //*20
		this._playing = false //*20
	}

	_resume() { //*20
		this._playing = true //*20
	}

	_restart() {
		document.location.reload();
	}
}

/*
 **************************************************
 * Initialize
 **************************************************
 */

onload = function () { //*21 Initialize
	// Just instantiate, and loop the first time, which will trigger the next times
	new Main().loop() //*21
}

</script>
</head>

<body>
	<canvas id="myCanvas" width="480" height="320" ></canvas>

	<div id="buttonPanel">
	<button id="pauseButton">Pause</button>
	<button id="resumeButton">Resume</button>
	<button id="restartButton">Restart</button>
	</div>

	<div id="scorePanel">
	<p class="score">Score: <b><span id="score"></span></b></p>
	<p class="score">Lives used: <b><span id="lives"></span></b></p>
	</div>
</body>

</html>
[download file]