All chapters · Chapter 5/9

The ball

Starting

Now there's a game. The ball bounces off the walls, bounces off the paddle, and if it gets past you it serves again from the middle.

A ball is a position and a velocity

let velocityX = BALL_SPEED * 0.55;
let velocityY = -BALL_SPEED;

Two numbers: how far it travels per second horizontally and vertically. velocityY starts negative because (chapter 2) subtracting from Y means going up.

And moving it is the same dt as always, twice:

ball.transform.x += velocityX * dt;
ball.transform.y += velocityY * dt;

Bouncing is flipping a sign

Against the left wall:

if (ball.transform.x <= 0) {
    ball.transform.x = 0;
    velocityX = Math.abs(velocityX);
}

Two things, and both matter.

Putting it back on the edge (x = 0). The ball moves in jumps, so it rarely lands exactly on 0: a frame typically leaves it at −3. Without the correction it sits slightly outside.

Forcing the sign with Math.abs rather than velocityX = -velocityX. Here's the classic bug in this game, and it deserves a paragraph.

If the ball stays pinned to the edge, the x <= 0 condition is true again on the next frame. With -velocityX you flip it again, and again, and again. The ball shivers against the wall, trapped. With Math.abs you say what you actually mean: from now on, rightwards, whatever state it was in. Repeating that does no harm.

The paddle: box collision

const onPaddle =
    velocityY > 0 &&
    ball.transform.y + BALL_SIZE >= PADDLE_Y &&
    ball.transform.y <= PADDLE_Y + PADDLE_H &&
    ball.transform.x + BALL_SIZE >= paddle.transform.x &&
    ball.transform.x <= paddle.transform.x + PADDLE_W;

Four comparisons asking whether two rectangles overlap. It's called AABB (axis-aligned bounding box) and it's the simplest collision there is. With rectangles that never rotate, it's plenty.

The fifth condition (the first one) heads off another nasty bug: velocityY > 0 means only if the ball is falling. Without it, a ball clipping the paddle's side on the way up would bounce upwards from below it, and get stuck under the paddle's bottom edge.

And when it does touch, the same thing happens as with the walls: the ball is put back right on top (ball.transform.y = PADDLE_Y - BALL_SIZE) before it is sent upwards, so it is never drawn a single frame inside the paddle.

The hit is not always the same

const offset = (ball.transform.x + BALL_SIZE / 2 - paddle.transform.x) / PADDLE_W - 0.5;
velocityX = offset * BALL_SPEED * 1.6;

This is what turns it into a game. offset measures where you hit it, as a number between −0.5 (the paddle's left end) and +0.5 (its right). The centre gives 0.

That measurement becomes the horizontal speed on the way out. Catch it on the left tip and it flies left; catch it dead centre and it goes almost straight up.

Without this line the ball would always leave at the same angle, and you'd have no way to aim. A brick-breaker you can't aim in is a screensaver.

What does `&&` mean?

&& is "and". A condition using && is true only if all its parts are:

if (imHungry && thereIsFood) eat();

Here there are five parts chained together, one per line so they can be read. The ball touches the paddle only if all five hold at once: it's falling, and its bottom edge reaches the paddle, and it hasn't gone past, and its right edge reaches the paddle's left, and its left edge hasn't gone past the right.

There's also ||, which is "or" and needs only one part to hold.

Your turn

Change the 1.6 at the end to 3 and watch how wild the angle gets. Set it to 0 and you'll see the screensaver I mentioned.

And try velocityX = -velocityX instead of Math.abs(velocityX) on the side bounces. It may take a while to happen, but eventually it'll shiver.