<!DOCTYPE html>
<html>
<head>
<title>CS 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 () //*19 Instantiate our main objects and connect them up
this._rules = new Rules (this._paddle, this._bricks) //*19
this._ball = new Ball (this._paddle, this._rules) //*19
this._rules.init (this._ball) //*20 Delayed initialization
}
// Update everything and redraw for animation loop
loop () { //*17 Animation: update then redraw
if (this._rules.playing) { //*21 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; } //*8
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() { //*15
this._x = myCanvas.width/2; //*15
this._y = myCanvas.height-30; //*15
this._dx = 4; //*15
this._dy = -4; //*15
}
}
/*
**************************************************
* Brick class
**************************************************
*/
class Brick { //*7 Use visible flag
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
}
remove () { //*7
this._visible = false; //*7
}
draw (ctx) {//*7
if(this._visible) { //*7
ctx.beginPath(); //*7
ctx.rect (this._x, this._y, Brick.width, Brick.height); //*7
ctx.fillStyle = "#0095DD"; //*7
ctx.fill(); //*7
}
}
// 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 { //*9 Paddle maintains X location, draws itself
constructor () { //*9
// Initialize our own ivars
this._height = 10;
this._width = 75;
this._x = (myCanvas.width-this._width)/2; //*9
// 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) { //*10
this._leftPressed = true; //*10
}
}
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; //*12
if (relativeX > 0 && relativeX < myCanvas.width) { //*12
this._x = relativeX - this._width/2; //*12
}
}
}
/*
**************************************************
* Rules class
**************************************************
*/
class Rules { //*18 Rules draws its text in HTML elements
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) { //*20
this._ball = ball //*20
}
draw () { //*18
score.textContent = this._score //*18
lives.textContent = this._lives //*18
}
// 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} //*21
// "Private" methods, denoted by "_"
_pause() { //*21
this._playing = false //*21
}
_resume() { //*21
this._playing = true //*21
}
_restart() {
document.location.reload();
}
}
/*
**************************************************
* Initialize
**************************************************
*/
onload = function () { //*22 Initialize
// Just instantiate, and loop the first time, which will trigger the next times
new Main().loop() //*22
}
</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>