
Rompebloques
Mueves una paleta por el borde de la pantalla para hacer rebotar una pelota contra los bloques del nivel y romperlos todos, sin dejar que la pelota pase de largo.
También se llama: rompeladrillos, clon de Breakout, tipo Arkanoid, block kuzushi
Mecánicas que lo definen
- Una paleta que se mueve en un solo eje con control directo (ratón, perilla giratoria o dedo).
- Una pelota que rebota sola. El ángulo con el que sale depende del punto de la paleta donde golpea, y eso es lo que te deja apuntar.
- Un muro de ladrillos con distinta resistencia, a veces con bloques irrompibles, dibujado como un nivel.
- Vidas. Dejar pasar la pelota cuesta una, y la pelota se acelera a medida que la golpeas.
- Desde Arkanoid, potenciadores que caen de los ladrillos (paleta larga, varias pelotas, cañones) y que hay que atrapar.
Bucle de juego
Seguir la pelota, ponerte debajo, decidir hacia dónde devolverla, verla romper ladrillos y repetir hasta despejar el muro.
Demo propia
Controles. Mueve la paleta con el ratón, con el dedo o con las flechas (o A/D). Clic, toque o espacio para lanzar la pelota.
Abrir en pantalla completaDos niveles de ladrillos con distinta resistencia, una paleta que sigue al puntero y una pelota cuyo rebote depende de dónde la golpeas. Lo justo para sentir lo que define al género, que es apuntar con la paleta.
Hecha con Kaplay, en unas 174 líneas de JavaScript.
Cómo está hecha
Configuración y controles
Se definen los botones virtuales left, right y launch. En pantallas táctiles aparecen como botones, aunque lo más cómodo es arrastrar el dedo. Las constantes del tamaño de la paleta, la velocidad de la pelota y el ángulo máximo de rebote son las que más cambian la sensación del juego.
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;Los niveles como texto
Cada nivel es una lista de cadenas de doce caracteres. El dígito dice cuántos golpes aguanta el ladrillo (1, 2 o 3) y el punto deja un hueco. Cada ladrillo guarda sus golpes restantes en una propiedad hits, y el color depende de ella. Diseñar un nivel nuevo es dibujarlo.
// 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"]); }), ); }La paleta
La paleta copia la x del puntero (ratón o dedo) y no se mueve en y. Las flechas la mueven a velocidad constante, y en los dos casos se limita para que no salga de la pantalla. Que el control sea directo, sin inercia, es lo que hace que el género se sienta preciso.
// 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(); });La pelota y el ángulo
La pelota no usa el motor de física. Su posición avanza cada fotograma según su velocidad. La mecánica clave es bounceOffPaddle, donde la distancia entre el punto de impacto y el centro de la paleta (de -1 a 1) se convierte en un ángulo de hasta 60°. Golpear con el centro la manda recta hacia arriba y con un borde la manda en diagonal. Sin esto, la pelota solo rebotaría como un espejo y el jugador no podría apuntar.
// 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); }Ladrillos y colisión
La pelota se mueve primero en x y se mira si toca un ladrillo; luego lo mismo en y. El eje en el que apareció el choque es el que invierte su velocidad, una forma sencilla de saber si la pelota golpeó un lado o la cara de arriba o de abajo. Cada golpe resta resistencia, suma puntos y acelera un poco la pelota, como en 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); }Paredes, vidas y fin de nivel
Cada fotograma la pelota rebota en las paredes laterales y en el techo, y en la paleta solo si viene bajando. Si cae por debajo de la pantalla pierdes una vida y vuelve a la paleta a su velocidad inicial; sin vidas, la partida termina. Cuando no quedan ladrillos se pasa al siguiente nivel conservando los puntos y las vidas.
// 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); } });Escenas
Hay tres escenas (title, game y end). La escena game recibe el número de nivel, los puntos y las vidas, así go("game", 1, score, lives) arma el nivel siguiente sin limpiar nada a mano.
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");
Qué simplificamos
- Todo son rectángulos y un círculo; no hay sonido ni efectos al romper un ladrillo.
- No hay potenciadores (paleta larga, varias pelotas, cañones) ni enemigos, que son lo que añadió Arkanoid.
- La colisión trata la pelota como un cuadrado y revisa un eje cada vez; a velocidades mucho mayores haría falta dividir cada paso en varios más pequeños.
Para modificarla
- Cambia MAX_ANGLE a 20 y luego a 80. ¿Con cuál puedes apuntar mejor? ¿Con cuál se vuelve injusto?
- Dibuja un tercer nivel en LEVELS y añade un carácter
- Haz que algunos ladrillos suelten una cápsula que, si la atrapas con la paleta, la alargue durante diez segundos.
Código completo
// 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");
// #endregionRetos de diseño
- Colisiones fiables entre la pelota y los ladrillos. Hay que saber qué cara se golpeó y evitar que la pelota atraviese un bloque cuando va rápido.
- Ajustar la regla del ángulo. Con poco ángulo no se puede apuntar, y con demasiado la pelota rebota casi en horizontal y tarda una eternidad en volver.
- El último ladrillo. Cuando quedan pocos, acertarles puede volverse lento y aburrido, y hacen falta potenciadores, aceleración o diseños de nivel que lo eviten.
- Diseñar muros con huecos y ladrillos resistentes que creen rutas interesantes, por ejemplo abrir un túnel para que la pelota rebote detrás del muro.
Historia
- 1976OrigenBreakout
Breakout (Atari, 1976) creó el género y dio nombre a sus clones; Clean Sweep (Ramtek, 1974) ya tenía una idea parecida. En Japón el género se llama block kuzushi.
Breakout (video game) (Wikipedia) - 1978Super Breakout
Super Breakout, la secuela de Atari, pone varias pelotas en juego a la vez en dos de sus tres modos.
Super Breakout (Wikipedia) - 1986Arkanoid
Arkanoid, de Taito, renueva la fórmula de Breakout con potenciadores y enemigos, y genera su propia ola de imitadores.
Breakout (video game) (Wikipedia) - 1999Brick Breaker
Brick Breaker, de Ali Asaria, viene preinstalado en algunos teléfonos BlackBerry y lleva el género al móvil.
Brick Breaker (Wikipedia) - 2009Shatter
Shatter añade física. Además de mover la paleta, puedes atraer o soplar la pelota y los bloques para desviar su trayectoria.
Shatter (Wikipedia) - 2013
Google convierte la búsqueda de imágenes «atari breakout» en una partida jugable, con las miniaturas como ladrillos.
Breakout (video game) (Wikipedia) - 2018Breakout Hero
Krystian Majewski publica Breakout Hero, un rompebloques de 15 niveles para la consola de fantasía PICO-8 que se juega en el navegador.
Breakout Hero (Indie Retro News)
Juegos de referencia
1976
Breakout
Con una paleta haces rebotar una pelota contra ocho filas de ladrillos de colores. Las filas altas valen más, la pelota se acelera con los golpes y la paleta se encoge al tocar el techo. Lo diseñaron Nolan Bushnell y Steve Bristow, y dio nombre al género.
1978
Super Breakout
Secuela de Breakout con tres modos. En Double controlas dos paletas y dos pelotas a la vez; en Cavity liberas pelotas atrapadas en el muro; en Progressive el muro baja poco a poco.
1986
Arkanoid
Controlas la nave Vaus con una perilla giratoria. Algunos ladrillos sueltan cápsulas que alargan la paleta, frenan la pelota, la multiplican o te dan cañones, y hay enemigos flotantes y un jefe final, Doh.
1999
Brick Breaker
Rompebloques que venía preinstalado en algunos teléfonos BlackBerry, y que reunió un público fiel de jugadores que compiten por la puntuación más alta.
2009
Shatter
Rompebloques con física en el que, además de mover la paleta, puedes atraer o soplar la pelota y los bloques sueltos para desviar su trayectoria.
2018
Breakout Hero
Rompebloques al estilo de Breakout y Arkanoid para la consola de fantasía PICO-8, con 15 niveles, potenciadores que caen de los ladrillos y tabla de récords. Su autor lo hizo a la par de una serie de videotutoriales y lo publicó gratis, jugable en el navegador.
Ejercicio
En la demo, cambia MAX_ANGLE y PADDLE_W y comenta cómo cambia la sensación de control. Luego dibuja en el mapa de texto un nivel que premie abrir un túnel hasta detrás del muro. Por último, juega Breakout Hero y anota qué efectos (sacudidas, partículas, sonido) hacen que romper un ladrillo se sienta bien y que la demo no tiene.