dan

dan

groovin'

Get This Boilerplate Out of My Way!

Our game will be written in JavaScript, but in order to create our basic webpage, we need to write a little HTML. Let's give our canvas a width and height of 500 pixels. Now that we have a canvas, we can modify it with two css properties so that it will be neatly centered on the page. I added a border so that you can see the canvas is centered before we draw on it, but I chose to remove the border later.

<html>
<head>
  <title>Breakout</title>
  <style>
    canvas {
      display: block;
      margin: auto;
      border: solid;
    }
  </style>
</head>
<body>
  <canvas width="500" height="500"></canvas>
  <script src="breakout.js"></script>
</body>
</html>
breakout.html

Make a file called "breakout.js" in the same directory as your html file, and let's get started on the game!

Grabbing the Canvas Element

Let's grab a reference to our canvas element, get a rendering context, it's dimensions, and it's width and height. We declare these variables as const because the should never change. Some developers will write constant variables in ALL CAPS, but that is not my personal style. I know we need a player and a ball, so let's go ahead and define those variables here, too.

const canvas             = document.querySelector("canvas");
const ctx                = canvas.getContext("2d");
const canvasBoundingRect = canvas.getBoundingClientRect();
const width              = Number(canvas.width);
const height             = Number(canvas.height);
 
let player;
let ball;

Meet Our Lifecycle Methods

setup

This function initializes all game state. It will be called when the game both starts and restarts.

update

Upon each frame of the game, it considers the delta (ie. difference) between the previous frame and the current frame to perform time-based modelling of the game state. For example, if the player moves the mouse to control the paddle, the update function will adjust the paddle's position to move towards the mouse. If the delta between the last frame and the current frame is small, it will move the paddle a little bit. If the delta is large, it will move the paddle more.

Why do we care about the delta?

Quite honestly, you don't have to, and many games don't. If we implemented this game without time-based modelling and we set a fast computer next to a slow computer, the ball would move around the scene at a slower speed on the slow computer. So players with slow computers will have a drastically different experience than someone with a fast computer. If we implement time-based modelling, both computers will have the ball moving at the same speed at any given time. Sometimes, even good computers experience a drop in framerate, and in that case, we rest assured that our simulation of the ball travelling around the screen will still run at the same pace.

render

This function will use the canvas API to draw our game state to the screen. All of the drawing code should live in this function with no exceptions. If we decide to draw our game using WebGL or SVG later, we will only have to replace this render function.

main

The main function orchestrates the game loop and calculates the delta that gets passed to the update function. It also calls the render function.

Creating the Main Function

The very first thing the main function should do is call requestAnimationFrame to ask the browser to call the main function at the best refresh rate. When we use requestAnimationFrame, it calls our function and passes in a timeStamp. If we keep track of the previous timeStamp, we can calculate the difference between them to find the length of time that elapsed between frames of animation. We can use this value to perform our time-based modelling, later.

let lastTimeStamp = 0;
 
function main(timeStamp) {
  requestAnimationFrame(main);
 
  // timeElapsed is the time between the previous and current frame.
  // The timestamps are in units of milliseconds. Divide by 1000 to get into units of seconds.
  let dt = (timeStamp - lastTimeStamp) / 1000;
  lastTimeStamp = timeStamp;
 
  update(dt);
  render();
}
breakout.js

We need a way to start the loop. Let's work on our startup function. If the game has never been started, we could call requestAnimationFrame(main); to initialize the game loop; however, the initial page load will delay the first frame and cause a significant delta. To avoid this, we can write a simple function initAnimation that will simply set the lastTimeStamp and call requestAnimationFrame(main);. By calling requestAnimationFrame(initAnimation); from startup, we send the timestamp to the function initAnimation.

This logic really comes in handy for games that don't start until the user presses "play". In that situation, it could take anywhere between several seconds up to a few minutes for the player to start the game, resulting in a huge delta for the first frame. In our case, calling requestAnimationFrame(initAnimation); to initialize the game means that the first frame of animation has a normal delta.

let initialized = false;
 
function setup() {
  if(!initialized)
    requestAnimationFrame(initAnimation);
}
 
function initAnimation(timeStamp) {
  lastTimeStamp = timeStamp;
  initialized = true;
  requestAnimationFrame(main);
};
 

We'll come back to setup shortly, because we need to create some entities for setup to intialize. Let's start with the player. In breakout, the player controls a paddle, which is really just a rectangle floating at the bottom of the screen. I'm going to use a class, which is modern JavaScript's updated version of a constructor function to model our entities.

I arbitrarily chose the player's width and height to be what looked good to me. The x position uses the canvas width to center the player on the canvas, and the y position is offset by 25 pixels from the bottom of the canvas.

class Player {
  constructor() {
    this.w = 75;
    this.h = 10;
    this.x = width / 2 - this.w / 2;
    this.y = height - 25;
    this.lives = 3;
  }
}

Now in our setup function, we need to initialize the player. At the bottom of the file, call setup so that when the script loads, the game will automatically start.

function setup() {
  player = new Player();
  // ...
}
 
// At the bottom of the file
setup();

Let's get this rendered on the screen as soon as possible so you can see the paddle! First, fill the canvas with a black rectangle and then draw the paddle rectangle on top.

function render() {
  // background
  ctx.fillStyle = "black";
  ctx.fillRect(0, 0, width, height);
 
  // paddle
  ctx.fillStyle = "white";
  ctx.fillRect(player.x, player.y, player.w, player.h);
}

Go ahead and add an empty update function that we will flesh out later.

function update(dt) {
  
}

Paddle Screenshot

We're Off to a Good Start!

This paddle is just begging to be allowed to move around the screen. Let's work on getting the player input squared away, next.

Player Input

The player will use the mouse to control the paddle. If you're not familiar, I'll be using arrow functions to define an inline callback from each event listener. Lets start by creating an object to store the mouse state.

let mouse = {
  x: width / 2,
  y: 0,
  click: false,
};

Clicking (mousedown) will shoot the ball, so we need to keep track of whether the mouse is being clicked or not. The mousedown event has a property which that tells us which mouse button has been clicked. If the player left-clicks, it will return 1, so that's what we should check for in our event handlers.

canvas.addEventListener("mousedown", e => {
  if (e.which == 1) mouse.click = true;
});
 
canvas.addEventListener("mouseup", e => {
  if (e.which == 1) mouse.click = false;
});

Moving the cursor around the screen (mousemove) will cause the paddle to follow on the x axis. We use the canvasBoundingRect to find the canvas' offset from the left and top of the screen. By taking the x and y positions from the mousemove event and subtracting by the offset, we convert x and y coordinates relative to the webpage into coordinates relative to the canvas.

canvas.addEventListener("mousemove", e => {
  mouse.x = e.clientX - canvasBoundingRect.x;
  mouse.y = e.clientY - canvasBoundingRect.y;
});

To make the paddle follow the mouse, I started by centering the player around the mouse's x position.

function update(dt) {
  player.x = mouse.x - player.w / 2;
}

This could be good enough for our game; however, I want the paddle to move with a set speed. That way, the player could have trouble reaching the ball if the paddle is on the opposite side of the screen. Otherwise, I'm afraid the game might be too easy.

class Player {
  constructor() {
    // ...
    this.speed = 400;
  }
}

I know that we're going to be getting the center of the player a lot, so let's go ahead and create a utility function for it. I put this at the bottom of the file.

function getCenter(x, w) {
  return x + w / 2;
}

In order to move the player by the fixed speed we defined, we must first determine which side of the paddle the mouse is on. Then, we can move towards the mouse. Additionally, we can keep the paddle inside the canvas by not letting the left side (player.x) go lower than 0, and not letting the right side (player.x + player.w) exceed the width of the canvas.

function update(dt) {
  let playerCenter = getCenter(player.x, player.w);
 
  // Move player to the right
  if (mouse.x > playerCenter) {
    player.x += player.speed * dt;
  }
  // Move player to the left
  else if(mouse.x < playerCenter) {
    player.x -= player.speed * dt;
  }
 
  // Bound the player
  if (player.x < 0)
    player.x = 0;
  else if(player.x + player.w > width)
    player.x = width - player.w;
}

Why are we multiplying the speed by dt?

Remember earlier when I discussed time-based modelling? You can read the equation of speed * dt as "400 pixels per second", since we set the speed to be 400. Without multiplying by the delta-time, we would want to decrease the speed, because the paddle would be moving "400 pixels per frame". Having a simulation run "per second" instead of "per frame" results in a consistent speed on devices with variable framerates.

If you run the game now, the result isn't looking so great. When the paddle gets close to the mouse position, it will start vibrating back-and-forth, since it's overshooting the mouse, and then switching directions on the next frame. To fix this, let's check if the center of the player has overshot the mouse after moving, and if it has, we will set the center of the player equal to the mouse position.

function update(dt) {
  let playerCenter = getCenter(player.x, player.w);
 
  // Move player to the right
  if (mouse.x > playerCenter) {
    player.x += player.speed * dt;
    if (getCenter(player.x, player.w) > mouse.x) {
      player.x = mouse.x - player.w / 2;
    }
  }
  // Move player to the left
  else if(mouse.x < playerCenter) {
    player.x -= player.speed * dt;
    if (getCenter(player.x, player.w) < mouse.x) {
      player.x = mouse.x - player.w / 2;
    }
  }
 
  // Bound the player
  if (player.x < 0)
    player.x = 0;
  else if(player.x + player.w > width)
    player.x = width - player.w;
}

Modelling the Ball

Our ball's x and y position will refer to the center of the ball, and the radius will be the distance from the center (x,y) to the edge of the circle. We are going to need some arcade physics to model the ball bouncing around the screen, so I assigned a speed and velocity with an x and y component. In physics, speed is a scalar, meaning it is simply a numeric quantity, but velocity is a vector, which means it has both direction and magnitude. Our ball will have a speed in both the x and y direction, which is why we define our velocity property to have an x and y value. Since we need to let the player shoot the ball, add a property active to keep track of whether the ball is waiting to be launched, or if it is already in play.

class Ball {
  constructor(x, y) {
    this.r = 5;
    this.x = x;
    this.y = y - this.r;
    this.velocity = { x: 0, y: 0 };
    this.speed = 200;
    this.active = false;
  }
}

Now initialize the ball variable in setup by passing in the desired x and y coordinates. The ball should sit in the center of the paddle, so we call getCenter to find the x position. For the y position, passing in the player.y position will allow the ball to sit on top of the paddle.

function setup() {
  ball = new Ball(getCenter(player.x, player.w), player.y);
  // ...
}

Rendering the Ball

Let's go ahead and render the ball so that we have something to look at when we try to model the ball physics. We will use a few methods in the canvas API that you may not be familiar with, namely: beginPath and arc. The arc function needs our ball's center point, radius, start angle, and end angle. There are 360 degrees in a circle, or 2π radians, so we will use 0 as the start angle and 2π as the end angle.

function render() {
  // ...
  ctx.beginPath();
  ctx.arc(ball.x, ball.y, ball.r, 0, Math.PI * 2);
  ctx.fill();
}
 

Updating the Ball's Position

Let's handle the case where the ball isn't being launched. We want the ball to either sit on top of the paddle if it hasn't been launched, or if it has already been launched, it should move around the screen at it's velocity. We should recalculate the playerCenter again, and check whether or not the ball is active. If the ball is active, we multiply the ball's velocity by dt and add it to the ball's position. To reiterate from earlier, when multiplying dt to a value like velocity, we can say "the ball is travelling at velocity pixels per second." If the ball isn't active, we simply set it's x position equal to the player's center.

function update(dt) {
  // ...
  playerCenter = getCenter(player.x, player.w);
  if (ball.active) {
    ball.x += ball.velocity.x * dt;
    ball.y += ball.velocity.y * dt;
  } else {
    ball.x = playerCenter;
  }
}

Launching the Ball

Now, we need to let the player shoot the ball! We already created mouse controls, and the ball's active state, so if we check whether both the mouse is being clicked, and if the ball has not been activated we can determine whether the ball should be launched or not. If the ball needs to be launched, we should set it's velocity in the negative direction to launch it up the canvas. This may be counter-intuitive, so now is a good time to remind you that the coordinates (0,0) refer to the top-left of the canvas. Since the paddle is sitting at the bottom of the canvas and we want to launch it up, we need a negative velocity to move it's position down towards 0.

function update(dt) {
  // ...
  if (mouse.click && !ball.active) {
    ball.active = true;
    ball.velocity.y = -ball.speed;
  }
}

Bouncing the Ball

We won't get very far if our ball isn't bounded on the left, right, and top of the canvas! The logic here is pretty simple. If the left side of the ball ball.x - ball.r is less than the left side of the canvas 0, than we bound the ball's x position to the left side of the canvas plus the radius of the ball 0 + ball.r, and reverse the velocity ball.velocity.x = ball.velocity.x * -1 so that it travels in the opposite direction of it's collision with the wall. We apply this same logic to the right and top sides of the canvas.

function update(dt) {
  // ...
  // Bound the ball left
  if (ball.x - ball.r < 0) {
    ball.x = ball.r;
    ball.velocity.x *= -1;
  }
  // Bound the ball left
  else if(ball.x + ball.r > width)
  {
    ball.x = width - ball.r;
    ball.velocity.x *= -1;
  }
  // Bound the ball top
  if (ball.y - ball.r < 0) {
    ball.y = ball.r;
    ball.velocity.y *= -1;
  }
}

If the ball is not getting bounded to the top of the screen, we should check to see if it has fallen below the screen. If so, let's subtract one life from the player and if the player has zero lives, we will set the gameOver state to true. Also, we need to recreate the ball so that it re-generates in the center of the player. We haven't set up the gameover variable, so we will do that next.

function update(dt) {
  // ...
  // Falls below player
  else if (ball.y - ball.r > height) {
    --player.lives;
    if (player.lives == 0) {
      gameOver = true;
    }
    ball = new Ball(player.w / 2 + player.x, player.y);
  }
}

I defined the gameOver variable near the top of the file and set it equal to false in our setup function.

let gameOver;
 
function setup() {
  // ...
  gameOver = false;
}

Bouncing off the Paddle

In this section, we will be implementing collision detection and collision handling between the ball and the paddle. For many genres of games - especially those with physics - collision handling is the most important part of the game. Imagine playing breakout without collisions! So to check whether or not the ball has hit the paddle, the ball must overlap with the paddle on the x-axis, and it must have the bottom edge in between the top and bottom of the paddle. This allows the player to hit the ball with the side of the paddle.

function update(dt) {
  // ...
  if (ball.x + ball.r > player.x &&
    ball.x - ball.r < player.x + player.w &&
    ball.y + ball.r > player.y &&
    ball.y + ball.r < player.y + player.h)
  {
    // ...
  }
}

When we detect a collision, we have to decide how to handle it. As with the ball bouncing off the sides of the walls, the ball should reverse velocity when it hits the paddle. When colliding with the walls, we bound the ball's coordinates inside the wall. We apply that same strategy with the paddle by snapping the ball's y position so that the ball rests on top of the paddle for the first frame of bouncing off of it. If we don't do that, we might get the situation where the ball gets stuck inside the paddle, where it changes directions each frame.

It's very common in breakout games to set the x component of the velocity based upon how far from the center that the ball hit the paddle. We find the distance that the ball is from the player's center by subtracting ball.x - playerCenter, and then scale that by multiplying it by some value. I played around with this value and found that a value of 7 "felt" right to me. Sometimes, there's no replacement for intuition when designing games.

{
  ball.velocity.y *= -1;
  ball.y = player.y - ball.r; //snap to top
  ball.velocity.x = (ball.x - playerCenter) * ball.scaleSpeed;
}

Don't forget to go back to the Ball class to set the scaleSpeed value to 7.

class Ball {
	constructor(x, y) {
    // ...
		this.scaleSpeed = 7;
	}
}

Great work!

We got the ball to bounce off the paddle, stay within the bounds of the canvas, and reset when it falls below the paddle. In the next chapter, we will create bricks for the ball to break.