
Shoot 'em up
Juego de disparos en el que pilotas una nave o un personaje por un plano visto de lado o desde arriba, disparando sin parar a oleadas de enemigos mientras esquivas sus proyectiles.
También se llama: shmup, matamarcianos
Mecánicas que lo definen
- Movimiento libre en dos ejes dentro de la pantalla y disparo casi continuo; las dos cosas pasan a la vez.
- Oleadas de enemigos que entran con trayectorias y formaciones fijas, aprendibles de una partida a otra.
- Proyectiles enemigos con patrones legibles (en línea, en abanico, apuntados) que convierten la pantalla en un problema de espacio.
- Mejoras de arma, bombas y vidas como recursos; la puntuación premia arriesgarse.
- Desplazamiento automático (scroll) que empuja al jugador hacia adelante, o una pantalla fija.
Bucle de juego
Leer el patrón que entra, colocarte en un hueco seguro desde el que puedas disparar, derribar la oleada y prepararte para la siguiente.
Demo propia
Controles. Flechas o WASD para moverte y espacio o Z (mantenido) para disparar. Con ratón o dedo, mantén pulsado y la nave sigue al puntero mientras dispara. En móvil también hay botones en pantalla.
Abrir en pantalla completaUna nave en un campo de estrellas que avanza solo, cinco oleadas de enemigos con trayectorias fijas y dos patrones de balas enemigas. Lo justo para sentir lo que define al género, que es disparar sin parar mientras lees y esquivas lo que viene.
Hecha con Kaplay, en unas 192 líneas de JavaScript.
Cómo está hecha
Configuración y controles
Se crea el juego con un tamaño fijo y botones virtuales para las cuatro direcciones y el disparo, así el mismo código sirve con teclado o con pantalla táctil. Las constantes de velocidad y cadencia definen cuánta potencia sientes. El color de las balas enemigas se reserva para ellas, porque en el género lo primero es distinguir lo que te mata.
const k = kaplay({ width: 640, height: 360, letterbox: true, pixelDensity, background: [10, 12, 36], buttons: { up: { keyboard: ["up", "w"] }, down: { keyboard: ["down", "s"] }, left: { keyboard: ["left", "a"] }, right: { keyboard: ["right", "d"] }, fire: { keyboard: ["space", "z", "j"] }, }, }); touchControls(k, [ { button: "left", label: "◀" }, { button: "up", label: "▲" }, { button: "down", label: "▼" }, { button: "right", label: "▶" }, { button: "fire", label: "✦", side: "right" }, ]); const SHIP_SPEED = 240; const BULLET_SPEED = 560; const FIRE_RATE = 0.12; // seconds between your own shots const ENEMY_BULLET_COLOR = rgb(255, 80, 200); // a colour used only by enemy bulletsEl fondo que avanza
Las estrellas se mueven hacia la izquierda a distintas velocidades y vuelven a entrar por la derecha. Ese efecto de paralaje da la sensación de volar hacia adelante, como el desplazamiento automático de los shoot 'em up laterales, sin mover la cámara.
// Stars drifting left give the feeling of moving forward without moving the // camera, like the auto-scrolling of side-scrolling shoot 'em ups. function starfield() { for (let i = 0; i < 50; i++) { const speed = rand(30, 140); add([rect(2, 2), pos(rand(0, width()), rand(0, height())), color(160, 170, 220), opacity(speed / 140), { speed }, "star"]); } onUpdate("star", (s) => { s.pos.x -= s.speed * dt(); if (s.pos.x < 0) s.pos.x = width(); }); }La nave y su zona de impacto
La nave se mueve libre en dos ejes sin salir de la pantalla. Su dibujo es grande, pero el área que recibe daño es un cuadrado de 6 píxeles marcado con un punto blanco. Esa diferencia es intencional en el género, porque permite rozar balas y que esquivar por poco se sienta justo.
// The drawing is a big arrow, but the hitbox is a 6 px square in the // centre (the white dot). So you can graze bullets without dying. const ship = add([ polygon([vec2(-14, -10), vec2(16, 0), vec2(-14, 10), vec2(-8, 0)]), color(90, 200, 255), pos(80, height() / 2), area({ shape: new Rect(vec2(-3, -3), 6, 6) }), opacity(1), { invulnerable: 0 }, "ship", ]); ship.add([circle(3), color(255, 255, 255)]); ship.onUpdate(() => { let dir = vec2(0, 0); if (isButtonDown("left")) dir.x -= 1; if (isButtonDown("right")) dir.x += 1; if (isButtonDown("up")) dir.y -= 1; if (isButtonDown("down")) dir.y += 1; // With a mouse or finger: the ship heads to the pointer while you hold it down. if (isMouseDown()) dir = mousePos().sub(ship.pos).scale(1 / 40); if (dir.len() > 1) dir = dir.unit(); ship.move(dir.scale(SHIP_SPEED)); ship.pos.x = clamp(ship.pos.x, 16, width() - 16); ship.pos.y = clamp(ship.pos.y, 16, height() - 16); // Blink while the invulnerability after a hit lasts. ship.invulnerable = Math.max(0, ship.invulnerable - dt()); ship.opacity = ship.invulnerable > 0 && Math.floor(time() * 12) % 2 ? 0.2 : 1; });Disparo continuo
Mantener el botón dispara una ráfaga a ritmo fijo (la cadencia). Como disparar no exige atención, la cabeza queda libre para moverse, y esa doble tarea de disparar y esquivar a la vez es el corazón del shoot 'em up. Cada enemigo resiste dos impactos.
// Holding the button fires a continuous stream: the player shoots and moves // at the same time, and that double task is the heart of the genre. let cooldown = 0; onUpdate(() => { cooldown -= dt(); if ((isButtonDown("fire") || isMouseDown()) && cooldown <= 0) { cooldown = FIRE_RATE; add([rect(14, 4), color(255, 230, 90), pos(ship.pos.add(14, 0)), anchor("center"), area(), move(RIGHT, BULLET_SPEED), offscreen({ destroy: true }), "bullet"]); } }); onCollide("bullet", "enemy", (b, e) => { destroy(b); e.hp--; if (e.hp <= 0) { destroy(e); score += 100; updateHud(); } });Trayectorias de los enemigos
Cada enemigo sigue un patrón fijo: recto, en onda o en picada (entra, se detiene un instante y se lanza hacia ti). Que sean fijos los vuelve aprendibles, y la pausa antes de la picada es un aviso que te da tiempo de reaccionar.
// Each enemy follows a fixed path given by its pattern. Being fixed is // what makes it learnable: the second time you know where it comes from. const PATTERNS = { straight: (e) => vec2(-e.speed, 0), sine: (e) => vec2(-e.speed, Math.cos(e.age * 3) * 120), // It enters, stops for a moment (the warning) and charges at where the ship is. dive: (e) => { if (e.age < 1) return vec2(-e.speed, 0); if (e.age < 1.6) return vec2(0, 0); e.heading ??= ship.pos.sub(e.pos).unit(); return e.heading.scale(e.speed * 2.2); }, }; function spawnEnemy(pattern, shot, y, speed) { const e = add([ rect(26, 22), color(240, 110, 70), pos(width() + 20, y), anchor("center"), area(), offscreen({ destroy: true, distance: 60 }), { pattern, shot, speed, age: 0, hp: 2, cooldown: rand(0.8, 1.4), heading: null }, "enemy", ]); e.onUpdate(() => { e.age += dt(); e.move(PATTERNS[e.pattern](e)); enemyFire(e); }); }Patrones de balas
Hay dos patrones, la bala apuntada, que va hacia donde estás, y el abanico de cinco balas abiertas. Las balas son lentas, redondas y de un color propio. Un buen patrón se lee de un vistazo y obliga a moverse de una forma concreta. La apuntada se esquiva con un paso al lado y el abanico, buscando el hueco entre balas.
// Two bullet patterns: "aimed" (flies at the ship) and "fan" (five spread // bullets). They are slow, big and have their own colour so you can read them in time. function enemyBullet(origin, direction) { add([circle(5), color(ENEMY_BULLET_COLOR), pos(origin), anchor("center"), area(), move(direction, 150), offscreen({ destroy: true }), "enemy-bullet"]); } function enemyFire(e) { e.cooldown -= dt(); if (e.shot === "none" || e.cooldown > 0 || e.pos.x > width() - 10) return; e.cooldown = 1.6; const towardShip = ship.pos.sub(e.pos).angle(); if (e.shot === "aimed") enemyBullet(e.pos, Vec2.fromAngle(towardShip)); if (e.shot === "fan") for (let i = -2; i <= 2; i++) enemyBullet(e.pos, Vec2.fromAngle(180 + i * 18)); }Oleadas como datos
El nivel es una lista de oleadas, cada una con su patrón, su tipo de disparo, su número de naves y sus alturas de entrada. Cuando cae o se va la última nave, empieza la siguiente. Al terminar la lista vuelve a empezar más rápido. Diseñar con datos permite ajustar el ritmo de presión y respiro sin tocar la lógica.
// The level is a list of waves. When the last ship of one falls, the next begins. // It is the same trick as the platformer's text map: designing with data. const WAVES = [ { pattern: "straight", shot: "none", n: 5, ys: [60, 120, 180, 240, 300] }, { pattern: "sine", shot: "aimed", n: 6, ys: [120] }, { pattern: "dive", shot: "none", n: 4, ys: [60, 300, 120, 240] }, { pattern: "straight", shot: "fan", n: 3, ys: [90, 180, 270] }, { pattern: "sine", shot: "fan", n: 6, ys: [100, 260] }, ]; let waveNumber = 0; function launchWave() { const w = WAVES[waveNumber % WAVES.length]; const lap = Math.floor(waveNumber / WAVES.length); waveNumber++; banner.text = t({ es: `Oleada ${waveNumber}`, en: `Wave ${waveNumber}` }); wait(1, () => (banner.text = "")); for (let i = 0; i < w.n; i++) { // Each full loop through the list makes the enemies faster. wait(0.8 + i * 0.45, () => spawnEnemy(w.pattern, w.shot, w.ys[i % w.ys.length], 120 + lap * 30)); } wait(0.8 + w.n * 0.45 + 0.5, () => (waiting = false)); } const banner = add([text("", { size: 26, font: FONT }), pos(center()), anchor("center"), color(200, 210, 255)]); let waiting = true; launchWave(); onUpdate(() => { if (!waiting && get("enemy").length === 0) { waiting = true; wait(0.8, launchWave); } });Vidas, invulnerabilidad y puntos
Tocar una bala o una nave enemiga cuesta una vida, sacude la pantalla y deja la nave parpadeando, invulnerable un segundo y medio, para que no pierdas varias vidas de golpe. Cada enemigo derribado da 100 puntos, el registro de lo bien que jugaste.
// Touching a bullet or an enemy ship costs a life and gives a moment of // invulnerability so you do not lose several at once. function takeHit() { if (ship.invulnerable > 0) return; lives--; updateHud(); shake(8); ship.invulnerable = 1.5; if (lives <= 0) go("end", score); } ship.onCollide("enemy-bullet", (b) => { destroy(b); takeHit(); }); ship.onCollide("enemy", takeHit);Escenas
El juego tiene tres escenas (title, game y end) y go() salta de una a otra. Reiniciar la partida es volver a entrar en la escena de juego, que crea todo de nuevo.
function screen(title, subtitle, next) { starfield(); 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: 560 }), pos(center().add(0, 30)), anchor("center"), color(200, 210, 255), ]); onButtonPress("fire", next); onKeyPress("enter", next); onClick(next); } scene("title", () => screen( t({ es: "Shoot 'em up", en: "Shoot 'em up" }), t({ es: "Flechas o WASD para moverte, mantén espacio o Z para disparar.\nCon ratón o dedo: mantén pulsado y la nave sigue al puntero disparando.\nEsquiva las balas rosas: solo el punto blanco de tu nave recibe daño.\nPulsa espacio o haz clic para empezar.", en: "Arrows or WASD to move, hold space or Z to fire.\nWith a mouse or finger: hold down and the ship follows the pointer while firing.\nDodge the pink bullets: only the white dot on your ship takes damage.\nPress space or click to start.", }), () => go("game"), ), ); scene("end", (score) => screen( t({ es: "Fin de la partida", en: "Game over" }), t({ es: `Puntos: ${score}. Pulsa espacio o haz clic para repetir.`, en: `Score: ${score}. Press space or click to play again.` }), () => go("game"), ), ); go("title");
Qué simplificamos
- Naves, enemigos y balas son formas simples; no hay sonido, explosiones, mejoras de arma ni bombas.
- No hay jefe ni final; las cinco oleadas se repiten cada vez más rápido hasta que pierdes las tres vidas.
- El desplazamiento es solo visual (las estrellas se mueven); no hay un escenario con obstáculos.
Para modificarla
- Añade un tercer patrón de bala, por ejemplo un anillo de ocho balas en todas direcciones, y crea una oleada que lo use.
- Haz que los enemigos derribados suelten a veces una mejora que dispare dos balas en paralelo durante unos segundos.
- Programa un jefe al final de las cinco oleadas, con mucha vida y dos fases de disparo distintas.
- Añade un modo lento (por ejemplo con Mayúsculas) que reduzca la velocidad de la nave para esquivar con precisión, como en los juegos de lluvia de balas.
Código completo
// Shoot 'em up 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: [10, 12, 36],
buttons: {
up: { keyboard: ["up", "w"] },
down: { keyboard: ["down", "s"] },
left: { keyboard: ["left", "a"] },
right: { keyboard: ["right", "d"] },
fire: { keyboard: ["space", "z", "j"] },
},
});
touchControls(k, [
{ button: "left", label: "◀" },
{ button: "up", label: "▲" },
{ button: "down", label: "▼" },
{ button: "right", label: "▶" },
{ button: "fire", label: "✦", side: "right" },
]);
const SHIP_SPEED = 240;
const BULLET_SPEED = 560;
const FIRE_RATE = 0.12; // seconds between your own shots
const ENEMY_BULLET_COLOR = rgb(255, 80, 200); // a colour used only by enemy bullets
// #endregion
// #region background
// Stars drifting left give the feeling of moving forward without moving the
// camera, like the auto-scrolling of side-scrolling shoot 'em ups.
function starfield() {
for (let i = 0; i < 50; i++) {
const speed = rand(30, 140);
add([rect(2, 2), pos(rand(0, width()), rand(0, height())), color(160, 170, 220), opacity(speed / 140), { speed }, "star"]);
}
onUpdate("star", (s) => {
s.pos.x -= s.speed * dt();
if (s.pos.x < 0) s.pos.x = width();
});
}
// #endregion
scene("game", () => {
starfield();
let score = 0;
let lives = 3;
const hud = add([text("", { size: 16, font: FONT }), pos(12, 10), z(10)]);
const updateHud = () =>
(hud.text = t({ es: `Puntos: ${score} Vidas: ${lives}`, en: `Score: ${score} Lives: ${lives}` }));
updateHud();
// #region ship
// The drawing is a big arrow, but the hitbox is a 6 px square in the
// centre (the white dot). So you can graze bullets without dying.
const ship = add([
polygon([vec2(-14, -10), vec2(16, 0), vec2(-14, 10), vec2(-8, 0)]),
color(90, 200, 255),
pos(80, height() / 2),
area({ shape: new Rect(vec2(-3, -3), 6, 6) }),
opacity(1),
{ invulnerable: 0 },
"ship",
]);
ship.add([circle(3), color(255, 255, 255)]);
ship.onUpdate(() => {
let dir = vec2(0, 0);
if (isButtonDown("left")) dir.x -= 1;
if (isButtonDown("right")) dir.x += 1;
if (isButtonDown("up")) dir.y -= 1;
if (isButtonDown("down")) dir.y += 1;
// With a mouse or finger: the ship heads to the pointer while you hold it down.
if (isMouseDown()) dir = mousePos().sub(ship.pos).scale(1 / 40);
if (dir.len() > 1) dir = dir.unit();
ship.move(dir.scale(SHIP_SPEED));
ship.pos.x = clamp(ship.pos.x, 16, width() - 16);
ship.pos.y = clamp(ship.pos.y, 16, height() - 16);
// Blink while the invulnerability after a hit lasts.
ship.invulnerable = Math.max(0, ship.invulnerable - dt());
ship.opacity = ship.invulnerable > 0 && Math.floor(time() * 12) % 2 ? 0.2 : 1;
});
// #endregion
// #region shooting
// Holding the button fires a continuous stream: the player shoots and moves
// at the same time, and that double task is the heart of the genre.
let cooldown = 0;
onUpdate(() => {
cooldown -= dt();
if ((isButtonDown("fire") || isMouseDown()) && cooldown <= 0) {
cooldown = FIRE_RATE;
add([rect(14, 4), color(255, 230, 90), pos(ship.pos.add(14, 0)), anchor("center"), area(), move(RIGHT, BULLET_SPEED), offscreen({ destroy: true }), "bullet"]);
}
});
onCollide("bullet", "enemy", (b, e) => {
destroy(b);
e.hp--;
if (e.hp <= 0) {
destroy(e);
score += 100;
updateHud();
}
});
// #endregion
// #region enemy-patterns
// Each enemy follows a fixed path given by its pattern. Being fixed is
// what makes it learnable: the second time you know where it comes from.
const PATTERNS = {
straight: (e) => vec2(-e.speed, 0),
sine: (e) => vec2(-e.speed, Math.cos(e.age * 3) * 120),
// It enters, stops for a moment (the warning) and charges at where the ship is.
dive: (e) => {
if (e.age < 1) return vec2(-e.speed, 0);
if (e.age < 1.6) return vec2(0, 0);
e.heading ??= ship.pos.sub(e.pos).unit();
return e.heading.scale(e.speed * 2.2);
},
};
function spawnEnemy(pattern, shot, y, speed) {
const e = add([
rect(26, 22), color(240, 110, 70), pos(width() + 20, y), anchor("center"), area(),
offscreen({ destroy: true, distance: 60 }),
{ pattern, shot, speed, age: 0, hp: 2, cooldown: rand(0.8, 1.4), heading: null },
"enemy",
]);
e.onUpdate(() => {
e.age += dt();
e.move(PATTERNS[e.pattern](e));
enemyFire(e);
});
}
// #endregion
// #region enemy-bullets
// Two bullet patterns: "aimed" (flies at the ship) and "fan" (five spread
// bullets). They are slow, big and have their own colour so you can read them in time.
function enemyBullet(origin, direction) {
add([circle(5), color(ENEMY_BULLET_COLOR), pos(origin), anchor("center"), area(), move(direction, 150), offscreen({ destroy: true }), "enemy-bullet"]);
}
function enemyFire(e) {
e.cooldown -= dt();
if (e.shot === "none" || e.cooldown > 0 || e.pos.x > width() - 10) return;
e.cooldown = 1.6;
const towardShip = ship.pos.sub(e.pos).angle();
if (e.shot === "aimed") enemyBullet(e.pos, Vec2.fromAngle(towardShip));
if (e.shot === "fan") for (let i = -2; i <= 2; i++) enemyBullet(e.pos, Vec2.fromAngle(180 + i * 18));
}
// #endregion
// #region waves
// The level is a list of waves. When the last ship of one falls, the next begins.
// It is the same trick as the platformer's text map: designing with data.
const WAVES = [
{ pattern: "straight", shot: "none", n: 5, ys: [60, 120, 180, 240, 300] },
{ pattern: "sine", shot: "aimed", n: 6, ys: [120] },
{ pattern: "dive", shot: "none", n: 4, ys: [60, 300, 120, 240] },
{ pattern: "straight", shot: "fan", n: 3, ys: [90, 180, 270] },
{ pattern: "sine", shot: "fan", n: 6, ys: [100, 260] },
];
let waveNumber = 0;
function launchWave() {
const w = WAVES[waveNumber % WAVES.length];
const lap = Math.floor(waveNumber / WAVES.length);
waveNumber++;
banner.text = t({ es: `Oleada ${waveNumber}`, en: `Wave ${waveNumber}` });
wait(1, () => (banner.text = ""));
for (let i = 0; i < w.n; i++) {
// Each full loop through the list makes the enemies faster.
wait(0.8 + i * 0.45, () => spawnEnemy(w.pattern, w.shot, w.ys[i % w.ys.length], 120 + lap * 30));
}
wait(0.8 + w.n * 0.45 + 0.5, () => (waiting = false));
}
const banner = add([text("", { size: 26, font: FONT }), pos(center()), anchor("center"), color(200, 210, 255)]);
let waiting = true;
launchWave();
onUpdate(() => {
if (!waiting && get("enemy").length === 0) {
waiting = true;
wait(0.8, launchWave);
}
});
// #endregion
// #region lives-score
// Touching a bullet or an enemy ship costs a life and gives a moment of
// invulnerability so you do not lose several at once.
function takeHit() {
if (ship.invulnerable > 0) return;
lives--;
updateHud();
shake(8);
ship.invulnerable = 1.5;
if (lives <= 0) go("end", score);
}
ship.onCollide("enemy-bullet", (b) => {
destroy(b);
takeHit();
});
ship.onCollide("enemy", takeHit);
// #endregion
});
// #region scenes
function screen(title, subtitle, next) {
starfield();
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: 560 }),
pos(center().add(0, 30)),
anchor("center"),
color(200, 210, 255),
]);
onButtonPress("fire", next);
onKeyPress("enter", next);
onClick(next);
}
scene("title", () =>
screen(
t({ es: "Shoot 'em up", en: "Shoot 'em up" }),
t({
es: "Flechas o WASD para moverte, mantén espacio o Z para disparar.\nCon ratón o dedo: mantén pulsado y la nave sigue al puntero disparando.\nEsquiva las balas rosas: solo el punto blanco de tu nave recibe daño.\nPulsa espacio o haz clic para empezar.",
en: "Arrows or WASD to move, hold space or Z to fire.\nWith a mouse or finger: hold down and the ship follows the pointer while firing.\nDodge the pink bullets: only the white dot on your ship takes damage.\nPress space or click to start.",
}),
() => go("game"),
),
);
scene("end", (score) =>
screen(
t({ es: "Fin de la partida", en: "Game over" }),
t({ es: `Puntos: ${score}. Pulsa espacio o haz clic para repetir.`, en: `Score: ${score}. Press space or click to play again.` }),
() => go("game"),
),
);
go("title");
// #endregionRetos de diseño
- Que los disparos enemigos se distingan siempre del fondo y de los propios, por color, forma y tamaño.
- Diseñar patrones que se lean a tiempo; si el jugador no puede ver venir la bala, perder se siente injusto.
- Ajustar la zona de impacto (hitbox) del jugador, a menudo más pequeña que su dibujo, para que esquivar por poco sea posible.
- Ritmo de oleadas con momentos de presión y de respiro, y jefes que resumen lo aprendido.
- Equilibrar la potencia del arma contra la densidad de enemigos para que mejorar se note sin romper el reto.
Historia
- 1978OrigenSpace Invaders
Space Invaders es el título más citado como el primero del género y fijó su plantilla. Algunos autores remontan el género a Spacewar! (1962).
Shoot 'em up (Wikipedia) - 1979Galaxian
Galaxian trae gráficos a color y enemigos que rompen la formación para lanzarse en picada contra el jugador.
Galaxian (Wikipedia) - 1981Scramble
Scramble es el primer shoot 'em up de desplazamiento lateral forzado con varios niveles distintos.
Scramble (Wikipedia) - 1985Gradius
Gradius define el shoot 'em up lateral y su barra de mejoras deja que el jugador elija cómo potenciar la nave.
Gradius (Wikipedia) - 1987R-Type
R-Type añade el disparo cargado y la Force, un módulo que sirve a la vez de arma y de escudo.
R-Type (Wikipedia) - 1993Batsugun
Batsugun llena la pantalla de patrones de balas y abre el camino a la «lluvia de balas» (bullet hell).
Batsugun (Wikipedia) - 2001Ikaruga
Ikaruga hace que la nave cambie entre blanco y negro para absorber las balas de su color, y todo el diseño gira en torno a esa regla.
Ikaruga (Wikipedia) - 2013Radius Raid
Radius Raid gana la competencia js13kGames con un shoot 'em up espacial para el navegador que pesa menos de 13 kilobytes.
Js13kGames (Wikipedia)
Juegos de referencia
1978
Space Invaders
Mueves un cañón por la base de la pantalla y disparas a filas de invasores que bajan cada vez más rápido, protegido por búnkeres que se desgastan. Fijó la plantilla del shoot 'em up.
1979
Galaxian
Una formación de alienígenas a color de la que salen naves en picada, disparando, contra el jugador. Convirtió a los enemigos en patrones de ataque que hay que leer.
1981
Scramble
Un avión que avanza solo sobre un terreno con scroll lateral, dispara y suelta bombas, y gasta combustible que recupera destruyendo depósitos a lo largo de seis tramos.
1985
Gradius
Shoot 'em up lateral en el que cada cápsula recogida avanza una barra de mejoras y tú decides cuándo activar la elegida. En Norteamérica y Europa se llamó Nemesis.
1987
R-Type
Shoot 'em up lateral con un disparo que se carga al mantener el botón y la Force, un módulo que se acopla delante o detrás de la nave o se suelta para usarlo como escudo.
1993
Batsugun
Shoot 'em up vertical con oleadas complejas, patrones de balas densos y mucha potencia de fuego. Se le considera el primer «manic shooter», antecedente de la lluvia de balas.
2001
Ikaruga
La nave cambia entre blanco y negro cuando quieres. Absorbe las balas de su color para cargar un láser y muere con las del color contrario, así que esquivar se vuelve también elegir.
2011
Z-Type
Un matamarcianos en el que en lugar de disparar escribes: cada nave enemiga lleva una palabra y la destruyes al teclearla. Cuantos menos errores cometes, más puntos sumas.
2013
Radius Raid
Shoot 'em up espacial en el que te mueves con el teclado y apuntas con el ratón contra 13 tipos de enemigos. Ganó js13kGames 2013 y su autor lo publicó jugable en el navegador y con código abierto.
Ejercicio
Juega la demo y anota qué oleada te cuesta más y por qué. Luego cambia el patrón de disparo de un enemigo (más balas en el abanico, o balas apuntadas) y prueba si sigue siendo legible. Por último, agranda la hitbox del jugador y comenta cómo cambia la sensación de justicia.
Subgéneros
- Bullet hellUn shoot 'em up que llena la pantalla de cientos de proyectiles en patrones intrincados; tu nave solo es vulnerable en un punto diminuto y el reto es colarte entre las balas.
- Disparos con doble stickCon un stick mueves a tu personaje y con el otro disparas en cualquier dirección, sin importar hacia dónde caminas, mientras sobrevives a hordas que te rodean vistas desde arriba.
- Run and gunAvanzas a pie por niveles con desplazamiento, corriendo, saltando y disparando sin parar a enemigos que llegan desde todos lados.