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.
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.
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?
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.
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.
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.
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.
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.
Go ahead and add an empty update function that we will flesh out later.
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.
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.
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.
To make the paddle follow the mouse, I started by centering the player around the mouse's x
position.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
I defined the gameOver
variable near the top of the file and set it equal to false in our setup
function.
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.
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.
Don't forget to go back to the Ball
class to set the scaleSpeed
value to 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.