
Block breaker
You move a paddle along the edge of the screen to bounce a ball into the level's blocks and break them all, without letting the ball get past you.
Also known as: block breaking, brick breaker, Breakout clone, Arkanoid clone
Defining mechanics
- A paddle that moves along a single axis with direct control (mouse, rotary knob or finger).
- A ball that bounces on its own. The angle it leaves at depends on where it hits the paddle, and that is what lets you aim.
- A wall of bricks with different strengths, sometimes with unbreakable blocks, laid out as a level.
- Lives. Letting the ball past costs one, and the ball speeds up as you keep hitting it.
- Since Arkanoid, power-ups that drop from bricks (longer paddle, multiple balls, cannons) and must be caught.
Gameplay loop
Follow the ball, get under it, decide where to send it back, watch it break bricks and repeat until the wall is clear.
Our demo
Controls. Move the paddle with the mouse, your finger or the arrow keys (or A/D). Click, tap or space to launch the ball.
Open full screenTwo levels of bricks with different strengths, a paddle that follows the pointer and a ball whose bounce depends on where you hit it. Just enough to feel what defines the genre, which is aiming with the paddle.
Made with Kaplay, in about 174 lines of JavaScript.
How it's made
Setup and controls
The virtual buttons left, right and launch are defined. On touch screens they appear as buttons, although dragging your finger is more comfortable. The paddle size, ball speed and maximum bounce angle constants are the ones that most change how the game feels.
const k = kaplay({ width: 640, height: 360, letterbox: true, pixelDensity, background: [20, 22, 48], buttons: { left: { keyboard: ["left", "a"] }, right: { keyboard: ["right", "d"] }, launch: { keyboard: ["space", "up", "enter"] }, }, }); // On touch screens the paddle follows your finger; the buttons are an extra. touchControls(k, [ { button: "left", label: "◀" }, { button: "right", label: "▶" }, { button: "launch", label: "●", side: "right" }, ]); const PADDLE_W = 80; const PADDLE_Y = 330; const PADDLE_SPEED = 420; const BALL_R = 6; const BALL_SPEED = 300; // starting speed, in pixels per second const MAX_BALL_SPEED = 480; const MAX_ANGLE = 60; // degrees from vertical when the ball hits a paddle edge const LIVES = 3;Levels as text
Each level is a list of twelve-character strings. The digit says how many hits the brick can take (1, 2 or 3) and a dot leaves a gap. Each brick stores its remaining hits in a hits property, and its colour depends on it. Designing a new level means drawing it.
// Each level is a text drawing. The digit is how many hits a brick takes; // a dot is an empty space. Twelve columns fit across the screen. const LEVELS = [ [ "............", "111111111111", "122222222221", "111111111111", "..11....11..", ], [ "333333333333", "2..2.22.2..2", ".1111111111.", "22.2.33.2.22", "1.1.1..1.1.1", ], ]; const BRICK_W = 48; const BRICK_H = 16; const COLORS = { 1: [110, 200, 140], 2: [240, 190, 80], 3: [230, 90, 90] }; function buildBricks(map) { map.forEach((row, r) => [...row].forEach((c, col) => { if (c === ".") return; const hits = Number(c); add([rect(BRICK_W - 4, BRICK_H - 4), pos(34 + col * BRICK_W, 40 + r * BRICK_H), color(...COLORS[hits]), { hits }, "brick"]); }), ); }The paddle
The paddle copies the pointer's x (mouse or finger) and does not move on y. The arrow keys move it at a constant speed, and in both cases it is clamped so it cannot leave the screen. Direct control, with no momentum, is what makes the genre feel precise.
// The paddle follows the pointer (mouse or finger) on the x axis only, // and the arrow keys move it too. It never leaves the screen. const paddle = add([rect(PADDLE_W, 10, { radius: 4 }), pos(width() / 2, PADDLE_Y), anchor("center"), color(140, 200, 255), "paddle"]); const clampPaddle = () => (paddle.pos.x = Math.min(Math.max(paddle.pos.x, PADDLE_W / 2), width() - PADDLE_W / 2)); onMouseMove(() => { paddle.pos.x = mousePos().x; clampPaddle(); }); onButtonDown("left", () => { paddle.pos.x -= PADDLE_SPEED * dt(); clampPaddle(); }); onButtonDown("right", () => { paddle.pos.x += PADDLE_SPEED * dt(); clampPaddle(); });The ball and the angle
The ball does not use the physics engine. Its position moves forward each frame according to its velocity. The key mechanic is bounceOffPaddle, where the distance between the impact point and the paddle's centre (from -1 to 1) becomes an angle of up to 60°. Hitting it with the centre sends it straight up and with an edge sends it diagonally. Without this the ball would just bounce like a mirror and the player could not aim.
// The ball is moved by hand (no physics engine): position += velocity * dt. // Until it is launched it sits on the paddle. const ball = add([circle(BALL_R), pos(0, 0), anchor("center"), color(255, 255, 255), { vel: vec2(0, 0), speed: BALL_SPEED, stuck: true }, "ball"]); const launch = () => { if (!ball.stuck) return; ball.stuck = false; ball.vel = vec2(rand(-0.4, 0.4), -1).unit().scale(ball.speed); }; onButtonPress("launch", launch); onMousePress(launch); // Hitting the paddle: the further from the centre, the wider the angle. // This is what gives the player aim instead of a ball that only bounces. function bounceOffPaddle() { const offset = (ball.pos.x - paddle.pos.x) / (PADDLE_W / 2); // -1 … 1 const angle = (Math.max(-1, Math.min(1, offset)) * MAX_ANGLE * Math.PI) / 180; ball.vel = vec2(Math.sin(angle), -Math.cos(angle)).scale(ball.speed); }Bricks and collision
The ball moves first on x and checks whether it touches a brick, then does the same on y. The axis where the hit appeared is the one whose velocity flips, a simple way to tell whether the ball hit a side or the top or bottom face. Each hit takes away strength, adds points and speeds the ball up a little, as in Breakout.
// Axis by axis: move in x and check for bricks, then the same in y. // Whichever axis produced the overlap is the one whose velocity flips. const overlaps = (b) => ball.pos.x + BALL_R > b.pos.x && ball.pos.x - BALL_R < b.pos.x + b.width && ball.pos.y + BALL_R > b.pos.y && ball.pos.y - BALL_R < b.pos.y + b.height; function hitBrick(b) { b.hits--; score += 10; ball.speed = Math.min(MAX_BALL_SPEED, ball.speed + 4); // each hit speeds the ball up if (b.hits <= 0) destroy(b); else b.color = rgb(...COLORS[b.hits]); // the colour shows how many hits are left updateHud(); } function moveAxis(axis) { ball.pos[axis] += ball.vel[axis] * dt(); const brick = get("brick").find(overlaps); if (!brick) return; ball.pos[axis] -= ball.vel[axis] * dt(); // step back out of the brick ball.vel[axis] = -ball.vel[axis]; hitBrick(brick); }Walls, lives and end of level
Every frame the ball bounces off the side walls and the ceiling, and off the paddle only when it is coming down. If it falls below the screen you lose a life and it returns to the paddle at its starting speed; with no lives left, the game ends. When no bricks remain you move on to the next level, keeping your score and lives.
// Every frame: move the ball, then apply the rules of the screen's edges. ball.onUpdate(() => { if (ball.stuck) { ball.pos = vec2(paddle.pos.x, PADDLE_Y - 5 - BALL_R); return; } moveAxis("x"); moveAxis("y"); // Walls on the left, right and top. if (ball.pos.x < BALL_R || ball.pos.x > width() - BALL_R) { ball.pos.x = Math.min(Math.max(ball.pos.x, BALL_R), width() - BALL_R); ball.vel.x = -ball.vel.x; } if (ball.pos.y < 30 + BALL_R) { ball.pos.y = 30 + BALL_R; ball.vel.y = Math.abs(ball.vel.y); } // The paddle: only when the ball is coming down onto it. const onPaddle = Math.abs(ball.pos.x - paddle.pos.x) < PADDLE_W / 2 + BALL_R && Math.abs(ball.pos.y - PADDLE_Y) < 5 + BALL_R; if (onPaddle && ball.vel.y > 0) bounceOffPaddle(); // Missing the ball costs a life; with none left the game is over. if (ball.pos.y > height() + 20) { lives--; updateHud(); if (lives <= 0) return go("end", false, score); ball.stuck = true; ball.speed = BALL_SPEED; } // No bricks left: next level, or the end if it was the last one. if (get("brick").length === 0) { if (levelIndex + 1 < LEVELS.length) go("game", levelIndex + 1, score, lives); else go("end", true, score); } });Scenes
There are three scenes, title, game and end. The game scene receives the level number, the score and the lives, so go("game", 1, score, lives) builds the next level without cleaning anything up by hand.
function screen(title, subtitle, next) { add([text(title, { size: 34, font: FONT, align: "center" }), pos(center().sub(0, 40)), anchor("center")]); add([ text(subtitle, { size: 16, font: FONT, align: "center", width: 600 }), pos(center().add(0, 30)), anchor("center"), color(200, 210, 255), ]); let ready = false; wait(0.4, () => (ready = true)); // so the last click of a game does not skip this screen const start = () => ready && next(); onButtonPress("launch", start); onClick(start); } scene("title", () => screen( t({ es: "Rompebloques", en: "Block breaker" }), t({ es: "Mueve la paleta con el ratón, el dedo o las flechas. Clic o espacio para lanzar la pelota.\nDonde golpea la paleta decide el ángulo. Rompe todos los ladrillos.\nPulsa espacio o toca para empezar.", en: "Move the paddle with the mouse, your finger or the arrows. Click or space to launch the ball.\nWhere it hits the paddle decides the angle. Break every brick.\nPress space or tap to start.", }), () => go("game"), ), ); scene("end", (won, score) => screen( won ? t({ es: "¡Muro despejado!", en: "Wall cleared!" }) : t({ es: "Sin vidas", en: "Out of lives" }), t({ es: `Puntos: ${score}. Pulsa espacio o toca para jugar otra vez.`, en: `Score: ${score}. Press space or tap to play again.`, }), () => go("game"), ), ); go("title");
What we simplified
- Everything is rectangles and a circle; there is no sound and no effect when a brick breaks.
- There are no power-ups (long paddle, multiple balls, cannons) and no enemies, which are what Arkanoid added.
- Collision treats the ball as a square and checks one axis at a time; at much higher speeds each step would have to be split into several smaller ones.
To modify it
- Change MAX_ANGLE to 20 and then to 80. Which one lets you aim best? Which one becomes unfair?
- Draw a third level in LEVELS and add a
- Make some bricks drop a capsule that, if you catch it with the paddle, makes it longer for ten seconds.
Full code
// Block breaker demo
// Engine: Kaplay 3001 (https://kaplayjs.com). MIT licence.
import kaplay from "../../vendor/kaplay.mjs";
import { t, FONT, touchControls, pixelDensity } from "../common.js";
// #region setup
const k = kaplay({
width: 640,
height: 360,
letterbox: true, pixelDensity,
background: [20, 22, 48],
buttons: {
left: { keyboard: ["left", "a"] },
right: { keyboard: ["right", "d"] },
launch: { keyboard: ["space", "up", "enter"] },
},
});
// On touch screens the paddle follows your finger; the buttons are an extra.
touchControls(k, [
{ button: "left", label: "◀" },
{ button: "right", label: "▶" },
{ button: "launch", label: "●", side: "right" },
]);
const PADDLE_W = 80;
const PADDLE_Y = 330;
const PADDLE_SPEED = 420;
const BALL_R = 6;
const BALL_SPEED = 300; // starting speed, in pixels per second
const MAX_BALL_SPEED = 480;
const MAX_ANGLE = 60; // degrees from vertical when the ball hits a paddle edge
const LIVES = 3;
// #endregion
// #region levels
// Each level is a text drawing. The digit is how many hits a brick takes;
// a dot is an empty space. Twelve columns fit across the screen.
const LEVELS = [
[
"............",
"111111111111",
"122222222221",
"111111111111",
"..11....11..",
],
[
"333333333333",
"2..2.22.2..2",
".1111111111.",
"22.2.33.2.22",
"1.1.1..1.1.1",
],
];
const BRICK_W = 48;
const BRICK_H = 16;
const COLORS = { 1: [110, 200, 140], 2: [240, 190, 80], 3: [230, 90, 90] };
function buildBricks(map) {
map.forEach((row, r) =>
[...row].forEach((c, col) => {
if (c === ".") return;
const hits = Number(c);
add([rect(BRICK_W - 4, BRICK_H - 4), pos(34 + col * BRICK_W, 40 + r * BRICK_H), color(...COLORS[hits]), { hits }, "brick"]);
}),
);
}
// #endregion
scene("game", (levelIndex = 0, score = 0, lives = LIVES) => {
buildBricks(LEVELS[levelIndex]);
const hud = add([text("", { size: 16, font: FONT }), pos(12, 10), z(10)]);
const updateHud = () =>
(hud.text = t({
es: `Nivel ${levelIndex + 1} Puntos: ${score} Vidas: ${lives}`,
en: `Level ${levelIndex + 1} Score: ${score} Lives: ${lives}`,
}));
updateHud();
// #region paddle
// The paddle follows the pointer (mouse or finger) on the x axis only,
// and the arrow keys move it too. It never leaves the screen.
const paddle = add([rect(PADDLE_W, 10, { radius: 4 }), pos(width() / 2, PADDLE_Y), anchor("center"), color(140, 200, 255), "paddle"]);
const clampPaddle = () => (paddle.pos.x = Math.min(Math.max(paddle.pos.x, PADDLE_W / 2), width() - PADDLE_W / 2));
onMouseMove(() => {
paddle.pos.x = mousePos().x;
clampPaddle();
});
onButtonDown("left", () => {
paddle.pos.x -= PADDLE_SPEED * dt();
clampPaddle();
});
onButtonDown("right", () => {
paddle.pos.x += PADDLE_SPEED * dt();
clampPaddle();
});
// #endregion
// #region ball
// The ball is moved by hand (no physics engine): position += velocity * dt.
// Until it is launched it sits on the paddle.
const ball = add([circle(BALL_R), pos(0, 0), anchor("center"), color(255, 255, 255), { vel: vec2(0, 0), speed: BALL_SPEED, stuck: true }, "ball"]);
const launch = () => {
if (!ball.stuck) return;
ball.stuck = false;
ball.vel = vec2(rand(-0.4, 0.4), -1).unit().scale(ball.speed);
};
onButtonPress("launch", launch);
onMousePress(launch);
// Hitting the paddle: the further from the centre, the wider the angle.
// This is what gives the player aim instead of a ball that only bounces.
function bounceOffPaddle() {
const offset = (ball.pos.x - paddle.pos.x) / (PADDLE_W / 2); // -1 … 1
const angle = (Math.max(-1, Math.min(1, offset)) * MAX_ANGLE * Math.PI) / 180;
ball.vel = vec2(Math.sin(angle), -Math.cos(angle)).scale(ball.speed);
}
// #endregion
// #region bricks
// Axis by axis: move in x and check for bricks, then the same in y.
// Whichever axis produced the overlap is the one whose velocity flips.
const overlaps = (b) =>
ball.pos.x + BALL_R > b.pos.x && ball.pos.x - BALL_R < b.pos.x + b.width &&
ball.pos.y + BALL_R > b.pos.y && ball.pos.y - BALL_R < b.pos.y + b.height;
function hitBrick(b) {
b.hits--;
score += 10;
ball.speed = Math.min(MAX_BALL_SPEED, ball.speed + 4); // each hit speeds the ball up
if (b.hits <= 0) destroy(b);
else b.color = rgb(...COLORS[b.hits]); // the colour shows how many hits are left
updateHud();
}
function moveAxis(axis) {
ball.pos[axis] += ball.vel[axis] * dt();
const brick = get("brick").find(overlaps);
if (!brick) return;
ball.pos[axis] -= ball.vel[axis] * dt(); // step back out of the brick
ball.vel[axis] = -ball.vel[axis];
hitBrick(brick);
}
// #endregion
// #region each-frame
// Every frame: move the ball, then apply the rules of the screen's edges.
ball.onUpdate(() => {
if (ball.stuck) {
ball.pos = vec2(paddle.pos.x, PADDLE_Y - 5 - BALL_R);
return;
}
moveAxis("x");
moveAxis("y");
// Walls on the left, right and top.
if (ball.pos.x < BALL_R || ball.pos.x > width() - BALL_R) {
ball.pos.x = Math.min(Math.max(ball.pos.x, BALL_R), width() - BALL_R);
ball.vel.x = -ball.vel.x;
}
if (ball.pos.y < 30 + BALL_R) {
ball.pos.y = 30 + BALL_R;
ball.vel.y = Math.abs(ball.vel.y);
}
// The paddle: only when the ball is coming down onto it.
const onPaddle = Math.abs(ball.pos.x - paddle.pos.x) < PADDLE_W / 2 + BALL_R && Math.abs(ball.pos.y - PADDLE_Y) < 5 + BALL_R;
if (onPaddle && ball.vel.y > 0) bounceOffPaddle();
// Missing the ball costs a life; with none left the game is over.
if (ball.pos.y > height() + 20) {
lives--;
updateHud();
if (lives <= 0) return go("end", false, score);
ball.stuck = true;
ball.speed = BALL_SPEED;
}
// No bricks left: next level, or the end if it was the last one.
if (get("brick").length === 0) {
if (levelIndex + 1 < LEVELS.length) go("game", levelIndex + 1, score, lives);
else go("end", true, score);
}
});
// #endregion
});
// #region scenes
function screen(title, subtitle, next) {
add([text(title, { size: 34, font: FONT, align: "center" }), pos(center().sub(0, 40)), anchor("center")]);
add([
text(subtitle, { size: 16, font: FONT, align: "center", width: 600 }),
pos(center().add(0, 30)),
anchor("center"),
color(200, 210, 255),
]);
let ready = false;
wait(0.4, () => (ready = true)); // so the last click of a game does not skip this screen
const start = () => ready && next();
onButtonPress("launch", start);
onClick(start);
}
scene("title", () =>
screen(
t({ es: "Rompebloques", en: "Block breaker" }),
t({
es: "Mueve la paleta con el ratón, el dedo o las flechas. Clic o espacio para lanzar la pelota.\nDonde golpea la paleta decide el ángulo. Rompe todos los ladrillos.\nPulsa espacio o toca para empezar.",
en: "Move the paddle with the mouse, your finger or the arrows. Click or space to launch the ball.\nWhere it hits the paddle decides the angle. Break every brick.\nPress space or tap to start.",
}),
() => go("game"),
),
);
scene("end", (won, score) =>
screen(
won ? t({ es: "¡Muro despejado!", en: "Wall cleared!" }) : t({ es: "Sin vidas", en: "Out of lives" }),
t({
es: `Puntos: ${score}. Pulsa espacio o toca para jugar otra vez.`,
en: `Score: ${score}. Press space or tap to play again.`,
}),
() => go("game"),
),
);
go("title");
// #endregionDesign challenges
- Reliable collisions between ball and bricks. You need to know which face was hit and keep the ball from passing through a block when it moves fast.
- Tuning the angle rule. Too little angle and you cannot aim; too much and the ball bounces almost horizontally and takes forever to come back.
- The last brick. When only a few remain, hitting them can become slow and boring, and you need power-ups, speed-ups or level layouts that avoid it.
- Designing walls with gaps and tough bricks that create interesting routes, for example opening a tunnel so the ball bounces behind the wall.
History
- 1976OriginBreakout
Breakout (Atari, 1976) created the genre and gave its name to the clones; Clean Sweep (Ramtek, 1974) already had a similar idea. In Japan the genre is called block kuzushi.
Breakout (video game) (Wikipedia) - 1978Super Breakout
Super Breakout, Atari's sequel, puts several balls in play at once in two of its three modes.
Super Breakout (Wikipedia) - 1986Arkanoid
Taito's Arkanoid refreshes the Breakout formula with power-ups and enemies, and spawns its own wave of imitators.
Breakout (video game) (Wikipedia) - 1999Brick Breaker
Ali Asaria's Brick Breaker comes preloaded on some BlackBerry phones and takes the genre to mobile.
Brick Breaker (Wikipedia) - 2009Shatter
Shatter adds physics. Besides moving the paddle, you can pull or blow the ball and the blocks to bend their path.
Shatter (Wikipedia) - 2013
Google turns the «atari breakout» image search into a playable game, with the thumbnails as bricks.
Breakout (video game) (Wikipedia) - 2018Breakout Hero
Krystian Majewski releases Breakout Hero, a 15-level block breaker for the PICO-8 fantasy console that plays in the browser.
Breakout Hero (Indie Retro News)
Reference games
1976
Breakout
With a paddle you bounce a ball into eight rows of coloured bricks. The upper rows are worth more, the ball speeds up as you hit it and the paddle shrinks once the ball reaches the top wall. Nolan Bushnell and Steve Bristow designed it, and it gave the genre its name.
1978
Super Breakout
A sequel to Breakout with three modes. In Double you control two paddles and two balls at once; in Cavity you free balls trapped in the wall; in Progressive the wall slowly moves down.
1986
Arkanoid
You control the Vaus ship with a rotary knob. Some bricks drop capsules that lengthen the paddle, slow the ball, split it or give you cannons, and there are floating enemies and a final boss, Doh.
1999
Brick Breaker
A block breaker that came preinstalled on some BlackBerry phones, and that gathered a cult following of players competing for the highest score.
2009
Shatter
A physics-based block breaker in which, besides moving the paddle, you can pull or blow the ball and loose blocks to bend their path.
2018
Breakout Hero
A Breakout- and Arkanoid-style block breaker for the PICO-8 fantasy console, with 15 levels, power-ups that drop from bricks and a high-score table. Its author made it alongside a video tutorial series and released it for free, playable in the browser.
Exercise
In the demo, change MAX_ANGLE and PADDLE_W and discuss how the feel of control changes. Then draw a level in the text map that rewards opening a tunnel behind the wall. Finally, play Breakout Hero and note which effects (shakes, particles, sound) make breaking a brick feel good and are missing from the demo.