Modelling the brick¶
All bricks should have the same width and height, and border-width, but they should all have different x and y positions. I declared w
, h
, and borderWidth
as static fields on the Brick
class, since we only need one instance for each of those properties.
Now that we know what a brick will look like, let's create a function that will create as many bricks as we specify. I find it simple to specify bricksAcross
and bricksDown
so that the function will generate a rectangle of bricks. We can make our function accept and offset from the wall in the x
and y
direction so that we can position the bricks wherever we want. Our function will return a matrix (two-dimensional array) of bricks.
Let's use this function in setup
to create some bricks. I intentionally made the canvas width of 500 easily divisible by the width of the bricks: 50. So in our case, we can easily get the number of bricks needed to fill the screen by dividing the canvas width by the bricks width. I chose to create 6 rows of bricks, and gave the bricks an offset of 50 in the y
direction so that we have some room at the top of the screen for the score. Also, it's fun to bounce off the top of the ceiling into the top of the bricks. We can pass this information to our function createBricks
and save the returned matrix to the bricks
variable. Multiplying the matrix's number of columns by the number of rows will give us the total number of bricks.
Now, let's render the bricks so we can see the fruits of our labor! First, we loop through the bricks and test whether or not the brick is defined. Soon, when we break the brick, it will appear as undefined in our matrix. We fill the brick with a light blue color.
I See a Big Blue Rectangle!
This is a good sign that all of the bricks are rendering, but we need to create a border around the bricks!
Adding a Border¶
After drawing the rectangle, let's draw a 3D-looking border around the brick. We can accomplish this by using the path API to draw two polygons on top of the rectangle. In the top-left, it will be a lighter blue, and in the bottom-right it will be a darker blue to create the effect we want. We use the borderWidth
property along with the x
, y
, w
, and h
properties to draw the pattern which looks like this:
Looking Good!
Now it's time to break those bricks.
Collision Detection Between a Circle and a Rectangle¶
All of the code we are about to write should go at the bottom of the update function. First, we loop through our two-dimensional array of bricks, and for each brick, we will test if the ball has collided with it.
Many games approximate collisions with complex shapes. Pretending that our ball is a square for the sake of collision detection would be much faster, but it would not be as high-fidelity. Since the circle-to-rectangle collision algorithm is actually pretty simple, we're going to implement it!
-
Find which side of the brick is closest to the center of the ball - assuming that the center of the ball is outside of the brick.
-
Then, use those sides to determine which vertex of the brick is closest to the ball. If no vertex is closest to the ball, it's because the ball is inside the brick, so we default the vertex's position to the center of the ball.
-
When we calculate the distance between that vertex and the center of the ball, if the radius is smaller than the distance, there has been a collision! Otherwise, there is no collision and we can carry on to the next iteration of the loop.
Collision Handling¶
Detecting a collision is not enough - we must figure out how to handle it! The first thing we know is that we just broke this brick - we are trying to breakout after all. Remember how we check if (!brick)
in our renderer and before our collision detection code that we just wrote? Well, 0
always evaluates to false
in JavaScript. You could set the brick to false
or undefined
if you prefer. So instead of this element in the array holding our Brick
object, it now holds 0
.
Let's continue inside this if
statement. If the number of brokenBricks
is equal to the number of totalBricks
, we should set gameOver
to true
and not bother continuing on in the update
function.
If the game isn't over - and it usually won't be - we want to bounce the ball off the brick, right? Let's take a crack at a simple algorithm for that. If the ball has hit the top or bottom of the brick, we can reverse the velocity in the y
direction. If the ball hit the right or left side, we should reverse the x
component of the velocity.
If you play the game like this for any length of time, you will see some pretty crazy behavior! Try to hit the corners. Currently, we are not taking the velocity into account when reversing the velocity, so if the ball hits the top-left corner with some velocity carrying it upward, we wouldn't expect the ball to turn around and fall downward, but that's one example of how this can go wrong. Also, we need to consider whether the brick has any neighbors to determine how the corner should behave. So let's throw the above code away and start all over again.
Let's first see if there are any neightbors in the cardinal directions. The first condition in these if
statements test to see if we are about to check outside the bounds of the array. If we aren't checking out of bounds, then we can index into that neighboring element to see if there is a brick there, or if it is empty.
Now it's time for some logic. We consider the corner that was hit: let's take the bottom-right for example. I drew this diagram to explain how the neighbors and velocity factor into our calculations. A picture is worth a thousand words, right? The teal brick is the one we are considering the collision on. The white brick is acting as a neighboring brick.
Let's translate this logic into code. If the x
velocity is negative and there isn't a neighbor to the right, we can reverse the velocity. the same is applicable for the y
velocity, only we are considering the bottom neighbor.
I'll extend the logic for the remaining corners without drawing a diagram to explain it, but I did add comments with + and - signs to signify the velocity direction. Now is also a good time to add our previous logic that we threw away in the case that only a single edge - not a corner - is touching.
Now that we handled the collision, we can break
out of both for
loops so that only one collision can happen per frame. The easiest way to do this is to name our outer loop "loop", and supply that name in the break statement like so: break loop;
. We just wrote a ton of code! So I'm going to put the entire ball-hits-brick collision detection and handling logic here to make sure we are on the same page.
Performance Consideration
What if we have a low framerate and the center of the ball ends up inside a brick? Our collision detection algorithm will catch that case, but we won't be able to determine which direction to send the ball. We will tackle this issue and add polish in the next chapter.