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:
- No ghost frame. The brick never gets drawn one last time while already "dead".
- No double hit. The rest of the frame carries on normally, but the brick is already out
of the
brickslist, so nothing can hit it again.
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.