All chapters · Chapter 7/9

Break them

Starting

Now it's a game. Bricks vanish when you hit them.

A brick that knows how to die

Three new things appeared in bricks.ts:

const self = useSelf();
 
const brick: BrickHandle = {
    x,
    y,
    hit: () => {
        bricks.splice(bricks.indexOf(brick), 1);
        destroy(self);
    },
};
 
bricks.push(brick);

useSelf() gives the component its own identity: the object useSpawn made for it. You need it because the engine's objects are plain data with no methods: there is no brick.destroySelf(). There is destroy(something), a free function, and useSelf is how a component gets hold of that something to hand over.

And hit() does both jobs in the right order: first it removes itself from the list of living bricks, then it removes itself from the world.

The engine does not delete it there and then

This is the important part of the chapter, and it's what separates a game that behaves from one with strange bugs that only happen sometimes.

destroy(self) does not remove the brick on the spot. It marks it, and the engine takes it out of the tree at the end of the frame, before anything is drawn. Godot works the same way, for the same two reasons:

If deletion were immediate, any code walking the brick list at that instant would find the floor moving under its feet. Deferring it makes the whole thing boring, and boring is exactly what you want.

The game keeps the list

const bricks: BrickHandle[] = [];
const spawnBrick = useSpawn(Brick);
// ...
spawnBrick({ row, col, bricks });

An ordinary array holding the bricks still alive. It is made inside the scene and each brick is handed it when it is born, along with its row and column. So every game has its own: if the scene starts again, the list starts again empty. Move it out of the scene body, to the top of the file, and it would outlive the scene, and a new game would inherit the last one's bricks.

The engine does not yet have a query along the lines of "give me everything hittable", so the game keeps its own list. For one wall, that's honest enough.

And finding a hit means walking it:

export const brickAt = (bricks, x, y, size) =>
    bricks.find((brick) =>
        x + size > brick.x && x < brick.x + BRICK_W &&
        y + size > brick.y && y < brick.y + BRICK_H);

Same AABB as the previous chapter, with find, which returns the first element matching the condition, or undefined when there is none.

In the main loop:

const brick = brickAt(bricks, ball.transform.x, ball.transform.y, BALL_SIZE);
if (brick) {
    brick.hit();
    const above = ball.transform.y + BALL_SIZE / 2 < brick.y + BRICK_H / 2;
    velocityY = above ? -Math.abs(velocityY) : Math.abs(velocityY);
}

The bounce sends the ball away from the brick: up if its centre is above the brick's centre, down if it is below. It's the Math.abs from the ball chapter again, for the same reason. A ball that comes in right at the seam between two bricks touches both, one on each frame. With velocityY = -velocityY, the second would turn it round again and the ball would carry on upwards, straight through the wall. Saying which way it has to go makes repeating it harmless.

A flaw that's in there, and you know it

The bounce is always vertical, whether you hit the brick from above or from the side. Clip a brick on its flank and the ball should go sideways, not down.

It's like that because fixing it properly costs a lot more code than fits in an introduction, and because in a brick-breaker nearly every hit is from above or below. It's a decision, not an oversight, and in your game you can decide otherwise.

What's `undefined`, and why does `if (brick)` work?

brickAt returns a brick... or undefined, which is how JavaScript says "there's nothing here".

In an if, certain values count as false without being false: undefined, null, 0, the empty string. Everything else is called truthy.

So if (brick) reads as if it found one. It's the short, ordinary way of writing if (brick !== undefined).

Your turn

Break every brick. Nothing happens: no score, no ending, no reward. That's what's next.

Meanwhile: try commenting out the bricks.splice(...) line and leaving only destroy(self). The brick disappears from the screen but stays in the list, so the ball bounces off an invisible brick forever. A great bug to see once.