
Defensa de torres
Juego de estrategia en el que proteges un punto contra oleadas de enemigos que avanzan por un recorrido; casi no los controlas a ellos ni a tus defensas, sino que decides qué torres comprar y dónde ponerlas con un dinero limitado.
También se llama: tower defense, TD
Mecánicas que lo definen
- Enemigos que siguen un camino fijo, o un laberinto que el propio jugador forma con sus torres.
- Torres que disparan solas a lo que entra en su alcance; el jugador solo elige el tipo y la casilla.
- Una economía cerrada, en la que cada enemigo derribado da monedas que pagan la siguiente torre o una mejora.
- Oleadas que crecen en número, vida y velocidad, con pausas para construir entre una y otra.
- Vidas que se pierden cuando un enemigo llega al final del recorrido.
Bucle de juego
Mirar la oleada que viene, gastar el dinero en torres o mejoras, ver cómo resiste la defensa, cobrar las bajas y corregir los puntos débiles antes de la siguiente.
Demo propia
Controles. Elige la torre en los botones de arriba y haz clic o toca una casilla verde para construirla. Las teclas 1 y 2 también eligen torre, y espacio adelanta la oleada.
Abrir en pantalla completaUn camino fijo con varias curvas, dos tipos de torre y ocho oleadas de enemigos cada vez más numerosos, resistentes y rápidos. Lo justo para sentir las dos decisiones del género, que son en qué gastar y dónde construir.
Hecha con Kaplay, en unas 198 líneas de JavaScript.
Cómo está hecha
Configuración
Se crea el juego con un tamaño fijo y se definen la rejilla (tamaño de casilla, filas y columnas) y las reglas de la partida, como el número de oleadas, las vidas y el dinero inicial. Cambiar estas constantes es la forma más rápida de ajustar la dificultad.
kaplay({ width: 640, height: 360, letterbox: true, pixelDensity, background: [20, 26, 34], // Everything is played with the mouse or a finger; the keyboard only adds shortcuts. buttons: { start: { keyboard: ["space", "enter"] }, tower1: { keyboard: ["1"] }, tower2: { keyboard: ["2"] }, }, }); const TILE = 32; const TOP = 40; // height of the top bar const COLS = 20; const ROWS = 10; const WAVES = 8; const LIVES = 10; const STARTING_COINS = 100; const tileCenter = (col, row) => vec2(col * TILE + TILE / 2, TOP + row * TILE + TILE / 2);El camino
El recorrido es una lista de esquinas. A partir de ella se calculan las casillas del camino, donde no se puede construir, y la ruta en píxeles que siguen los enemigos. Un camino largo y con curvas da más tiempo a las torres y crea lugares mejores que otros.
// The path is fixed: a list of corners (column, row) that enemies walk // through in order. Path tiles cannot hold towers. const CORNERS = [[-1, 2], [4, 2], [4, 7], [9, 7], [9, 1], [14, 1], [14, 8], [20, 8]]; const ROUTE = CORNERS.map(([col, row]) => tileCenter(col, row)); const onPath = new Set(); for (let i = 0; i < CORNERS.length - 1; i++) { let [col, row] = CORNERS[i]; const [col2, row2] = CORNERS[i + 1]; for (;;) { onPath.add(`${col},${row}`); if (col === col2 && row === row2) break; col += Math.sign(col2 - col); row += Math.sign(row2 - row); } }Tipos de torre
Cada torre es un objeto de datos con coste, alcance, cadencia, daño y área. Las dos se complementan: el arquero es barato y rápido contra un enemigo, y el cañón es caro y lento pero daña a un grupo. Si una fuera mejor en todo, no habría decisión.
// Two towers with different roles: a cheap, fast one against a single enemy, // and an expensive, slow one with area damage. Choosing which and where is the strategy. const TOWERS = [ { name: t({ es: "Arquero", en: "Archer" }), cost: 40, range: 90, fireRate: 0.45, damage: 1, splash: 0, color: [110, 190, 255] }, { name: t({ es: "Cañón", en: "Cannon" }), cost: 70, range: 70, fireRate: 1.3, damage: 3, splash: 40, color: [240, 150, 70] }, ];Economía y marcador
La barra superior muestra monedas, vidas y oleada, y tiene los botones para elegir torre. El dinero es el recurso que conecta todo, porque matar enemigos da monedas y las monedas compran torres que matan más enemigos.
// Top bar: coins, lives, wave and the buttons to pick a tower. for (let row = 0; row < ROWS; row++) for (let col = 0; col < COLS; col++) { const path = onPath.has(`${col},${row}`); add([rect(TILE - 2, TILE - 2), pos(col * TILE + 1, TOP + row * TILE + 1), color(path ? rgb(150, 122, 84) : rgb(50, 84, 60))]); } add([rect(width(), TOP), color(12, 16, 22)]); const hud = add([text("", { size: 15, font: FONT }), pos(10, 12)]); const notice = add([text("", { size: 16, font: FONT }), pos(width() / 2, TOP + 14), anchor("center"), z(20)]); const updateHud = () => (hud.text = `${t({ es: "Monedas", en: "Coins" })}: ${coins} ${t({ es: "Vidas", en: "Lives" })}: ${lives} ${t({ es: "Oleada", en: "Wave" })}: ${wave}/${WAVES}`); const buttons = TOWERS.map((type, i) => { const b = add([rect(116, 28, { radius: 4 }), pos(384 + i * 126, 6), color(40, 48, 60), outline(2, rgb(...type.color)), area()]); b.add([text(`${i + 1}. ${type.name} (${type.cost})`, { size: 13, font: FONT }), pos(58, 14), anchor("center")]); b.onClick(() => select(i)); return b; }); function select(i) { selected = i; buttons.forEach((b, j) => (b.color = j === i ? rgb(84, 100, 130) : rgb(40, 48, 60))); } onButtonPress("tower1", () => select(0)); onButtonPress("tower2", () => select(1)); select(0); updateHud();Colocar torres
Al hacer clic se convierte la posición del ratón en una casilla y se comprueba que esté libre, que no sea camino y que alcance el dinero. Bajo el cursor se dibuja el alcance en verde o en rojo, para que el jugador vea antes de gastar si ese lugar cubre bien el camino.
// Click a free tile: if you can afford it, the selected tower is built. const tileAt = (m) => [Math.floor(m.x / TILE), Math.floor((m.y - TOP) / TILE)]; const isFree = (col, row) => row >= 0 && row < ROWS && col >= 0 && col < COLS && !onPath.has(`${col},${row}`) && !occupied.has(`${col},${row}`); onMousePress(() => { const [col, row] = tileAt(mousePos()); const type = TOWERS[selected]; if (!isFree(col, row) || coins < type.cost) return; coins -= type.cost; occupied.add(`${col},${row}`); updateHud(); addTower(type, tileCenter(col, row)); }); // Range preview under the cursor: green if you can build there, red if not. onDraw(() => { const m = mousePos(); if (m.y < TOP) return; const [col, row] = tileAt(m); const type = TOWERS[selected]; const ok = isFree(col, row) && coins >= type.cost; drawCircle({ pos: tileCenter(col, row), radius: type.range, color: ok ? rgb(120, 255, 140) : rgb(255, 90, 90), opacity: 0.15 }); }); function addTower(type, p) { const tower = add([circle(11), pos(p), anchor("center"), color(...type.color), outline(2, rgb(20, 20, 20)), { cooldown: 0 }]); tower.onUpdate(() => { tower.cooldown -= dt(); if (tower.cooldown > 0) return; const target = pickTarget(p, type.range); if (!target) return; tower.cooldown = type.fireRate; shoot(type, p, target); }); }Apuntar y disparar
Cada torre busca, entre los enemigos dentro de su alcance, el que más ha avanzado, porque es el que está más cerca de escapar. La bala persigue a su objetivo; el cañón además daña a todos los que estén cerca del impacto. Cada baja suma su premio a las monedas.
// Classic targeting rule: of the enemies in range, the one furthest along, // that is, the one closest to escaping. function pickTarget(p, range) { let best = null; for (const e of get("enemy")) if (e.pos.dist(p) <= range && (!best || e.progress > best.progress)) best = e; return best; } function shoot(type, from, target) { const bullet = add([circle(type.splash ? 5 : 3), pos(from), anchor("center"), color(255, 240, 180)]); bullet.onUpdate(() => { if (!target.exists()) return destroy(bullet); const dir = target.pos.sub(bullet.pos); if (dir.len() < 8) { destroy(bullet); hit(type, target); } else bullet.move(dir.unit().scale(340)); }); } function hit(type, target) { if (!type.splash) return hurt(target, type.damage); add([circle(type.splash), pos(target.pos), anchor("center"), color(255, 180, 90), opacity(0.4), lifespan(0.25, { fade: 0.2 })]); for (const e of get("enemy")) if (e.pos.dist(target.pos) <= type.splash) hurt(e, type.damage); } function hurt(e, damage) { if (!e.exists()) return; e.health -= damage; if (e.health > 0) return; destroy(e); coins += e.bounty; // every kill pays for the next tower updateHud(); }Enemigos
Los enemigos van de esquina en esquina y acumulan la distancia recorrida, que es lo que usan las torres para elegir objetivo. Su vida, velocidad y premio dependen del número de oleada. Si uno llega al final, se pierde una vida.
// Each enemy walks from corner to corner and accumulates `progress` (distance // travelled). If it reaches the end, you lose a life. function addEnemy(n) { const maxHealth = 2 + n * 2; const e = add([rect(18, 18), pos(ROUTE[0]), anchor("center"), color(200, 70, 90), "enemy", { health: maxHealth, maxHealth, speed: 42 + n * 5, leg: 1, progress: 0, bounty: 3 + n }]); e.onUpdate(() => { const step = e.speed * dt(); const dir = ROUTE[e.leg].sub(e.pos); if (dir.len() <= step) e.pos = ROUTE[e.leg++].clone(); else e.pos = e.pos.add(dir.unit().scale(step)); e.progress += step; if (e.leg < ROUTE.length) return; destroy(e); lives--; updateHud(); shake(4); if (lives <= 0) go("end", false, wave); }); e.onDraw(() => drawRect({ pos: vec2(-10, -16), width: (20 * e.health) / e.maxHealth, height: 3, color: rgb(120, 230, 120) })); }La curva de oleadas
Cada oleada trae más enemigos, con más vida y más seguidos, y entre una y otra hay unos segundos para construir. Esta curva, comparada con lo que el jugador gana por bajas, es lo que define si el juego se siente justo, fácil o imposible.
// The difficulty curve: each wave brings more enemies, with more health, // faster and closer together. Between waves there is a break to build. function startWave() { wave++; toSpawn = 4 + wave * 2; updateHud(); loop(Math.max(0.35, 1 - wave * 0.08), () => { addEnemy(wave); toSpawn--; }, toSpawn); } onButtonPress("start", () => (breakTime = 0)); // call the next wave early onUpdate(() => { if (toSpawn > 0 || get("enemy").length > 0) return (notice.text = ""); if (wave === WAVES) return go("end", true, wave); breakTime -= dt(); notice.text = t({ es: `Oleada ${wave + 1} en ${Math.ceil(breakTime)} s (espacio: ya)`, en: `Wave ${wave + 1} in ${Math.ceil(breakTime)} s (space: now)` }); if (breakTime <= 0) { breakTime = 6; startWave(); } });Escenas
El juego tiene tres escenas (title, game y end) y go() salta de una a otra. Reiniciar la escena de juego vuelve a poner el dinero, las vidas y el mapa como al principio.
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: 540 }), pos(center().add(0, 30)), anchor("center"), color(200, 210, 255), ]); onButtonPress("start", next); onClick(next); } scene("title", () => screen( t({ es: "Defensa de torres", en: "Tower defense" }), t({ es: "Elige una torre arriba y haz clic (o toca) en una casilla verde para construirla.\nQue ningún enemigo llegue al final del camino. Sobrevive 8 oleadas.\nHaz clic o pulsa espacio para empezar.", en: "Pick a tower at the top and click (or tap) a green tile to build it.\nDon't let any enemy reach the end of the path. Survive 8 waves.\nClick or press space to start.", }), () => go("game"), ), ); scene("end", (won, wave) => screen( won ? t({ es: "¡Resististe!", en: "You held out!" }) : t({ es: "Cayó la defensa", en: "The defense fell" }), t({ es: `Llegaste a la oleada ${wave}. Haz clic para repetir.`, en: `You reached wave ${wave}. Click to play again.` }), () => go("game"), ), ); go("title");
Qué simplificamos
- Las torres no se mejoran ni se venden, y solo hay un tipo de enemigo que se hace más fuerte con cada oleada.
- Torres, enemigos y balas son círculos y cuadrados, sin animaciones ni sonido.
- Un solo mapa con un camino fijo; no hay laberinto que el jugador pueda alargar.
Para modificarla
- Añade una tercera torre que no haga daño pero ralentice a los enemigos durante unos segundos, y piensa dónde conviene ponerla.
- Permite mejorar una torre haciendo clic sobre ella (más daño o alcance a cambio de monedas).
- Crea un enemigo rápido con poca vida y otro lento con mucha, y mézclalos en las oleadas.
- Cambia la regla de apuntado al enemigo con más vida o al más cercano, y compara cuántas vidas pierdes.
Código completo
// Tower defense demo
// Engine: Kaplay 3001 (https://kaplayjs.com). MIT licence.
import kaplay from "../../vendor/kaplay.mjs";
import { t, FONT, pixelDensity } from "../common.js";
// #region setup
kaplay({
width: 640,
height: 360,
letterbox: true, pixelDensity,
background: [20, 26, 34],
// Everything is played with the mouse or a finger; the keyboard only adds shortcuts.
buttons: {
start: { keyboard: ["space", "enter"] },
tower1: { keyboard: ["1"] },
tower2: { keyboard: ["2"] },
},
});
const TILE = 32;
const TOP = 40; // height of the top bar
const COLS = 20;
const ROWS = 10;
const WAVES = 8;
const LIVES = 10;
const STARTING_COINS = 100;
const tileCenter = (col, row) => vec2(col * TILE + TILE / 2, TOP + row * TILE + TILE / 2);
// #endregion
// #region path
// The path is fixed: a list of corners (column, row) that enemies walk
// through in order. Path tiles cannot hold towers.
const CORNERS = [[-1, 2], [4, 2], [4, 7], [9, 7], [9, 1], [14, 1], [14, 8], [20, 8]];
const ROUTE = CORNERS.map(([col, row]) => tileCenter(col, row));
const onPath = new Set();
for (let i = 0; i < CORNERS.length - 1; i++) {
let [col, row] = CORNERS[i];
const [col2, row2] = CORNERS[i + 1];
for (;;) {
onPath.add(`${col},${row}`);
if (col === col2 && row === row2) break;
col += Math.sign(col2 - col);
row += Math.sign(row2 - row);
}
}
// #endregion
// #region tower-types
// Two towers with different roles: a cheap, fast one against a single enemy,
// and an expensive, slow one with area damage. Choosing which and where is the strategy.
const TOWERS = [
{ name: t({ es: "Arquero", en: "Archer" }), cost: 40, range: 90, fireRate: 0.45, damage: 1, splash: 0, color: [110, 190, 255] },
{ name: t({ es: "Cañón", en: "Cannon" }), cost: 70, range: 70, fireRate: 1.3, damage: 3, splash: 40, color: [240, 150, 70] },
];
// #endregion
scene("game", () => {
let coins = STARTING_COINS;
let lives = LIVES;
let wave = 0;
let toSpawn = 0; // enemies of this wave still waiting to come out
let breakTime = 4; // seconds until the next wave
let selected = 0;
const occupied = new Set();
// #region economy
// Top bar: coins, lives, wave and the buttons to pick a tower.
for (let row = 0; row < ROWS; row++)
for (let col = 0; col < COLS; col++) {
const path = onPath.has(`${col},${row}`);
add([rect(TILE - 2, TILE - 2), pos(col * TILE + 1, TOP + row * TILE + 1), color(path ? rgb(150, 122, 84) : rgb(50, 84, 60))]);
}
add([rect(width(), TOP), color(12, 16, 22)]);
const hud = add([text("", { size: 15, font: FONT }), pos(10, 12)]);
const notice = add([text("", { size: 16, font: FONT }), pos(width() / 2, TOP + 14), anchor("center"), z(20)]);
const updateHud = () =>
(hud.text = `${t({ es: "Monedas", en: "Coins" })}: ${coins} ${t({ es: "Vidas", en: "Lives" })}: ${lives} ${t({ es: "Oleada", en: "Wave" })}: ${wave}/${WAVES}`);
const buttons = TOWERS.map((type, i) => {
const b = add([rect(116, 28, { radius: 4 }), pos(384 + i * 126, 6), color(40, 48, 60), outline(2, rgb(...type.color)), area()]);
b.add([text(`${i + 1}. ${type.name} (${type.cost})`, { size: 13, font: FONT }), pos(58, 14), anchor("center")]);
b.onClick(() => select(i));
return b;
});
function select(i) {
selected = i;
buttons.forEach((b, j) => (b.color = j === i ? rgb(84, 100, 130) : rgb(40, 48, 60)));
}
onButtonPress("tower1", () => select(0));
onButtonPress("tower2", () => select(1));
select(0);
updateHud();
// #endregion
// #region placing-towers
// Click a free tile: if you can afford it, the selected tower is built.
const tileAt = (m) => [Math.floor(m.x / TILE), Math.floor((m.y - TOP) / TILE)];
const isFree = (col, row) => row >= 0 && row < ROWS && col >= 0 && col < COLS && !onPath.has(`${col},${row}`) && !occupied.has(`${col},${row}`);
onMousePress(() => {
const [col, row] = tileAt(mousePos());
const type = TOWERS[selected];
if (!isFree(col, row) || coins < type.cost) return;
coins -= type.cost;
occupied.add(`${col},${row}`);
updateHud();
addTower(type, tileCenter(col, row));
});
// Range preview under the cursor: green if you can build there, red if not.
onDraw(() => {
const m = mousePos();
if (m.y < TOP) return;
const [col, row] = tileAt(m);
const type = TOWERS[selected];
const ok = isFree(col, row) && coins >= type.cost;
drawCircle({ pos: tileCenter(col, row), radius: type.range, color: ok ? rgb(120, 255, 140) : rgb(255, 90, 90), opacity: 0.15 });
});
function addTower(type, p) {
const tower = add([circle(11), pos(p), anchor("center"), color(...type.color), outline(2, rgb(20, 20, 20)), { cooldown: 0 }]);
tower.onUpdate(() => {
tower.cooldown -= dt();
if (tower.cooldown > 0) return;
const target = pickTarget(p, type.range);
if (!target) return;
tower.cooldown = type.fireRate;
shoot(type, p, target);
});
}
// #endregion
// #region shooting
// Classic targeting rule: of the enemies in range, the one furthest along,
// that is, the one closest to escaping.
function pickTarget(p, range) {
let best = null;
for (const e of get("enemy")) if (e.pos.dist(p) <= range && (!best || e.progress > best.progress)) best = e;
return best;
}
function shoot(type, from, target) {
const bullet = add([circle(type.splash ? 5 : 3), pos(from), anchor("center"), color(255, 240, 180)]);
bullet.onUpdate(() => {
if (!target.exists()) return destroy(bullet);
const dir = target.pos.sub(bullet.pos);
if (dir.len() < 8) {
destroy(bullet);
hit(type, target);
} else bullet.move(dir.unit().scale(340));
});
}
function hit(type, target) {
if (!type.splash) return hurt(target, type.damage);
add([circle(type.splash), pos(target.pos), anchor("center"), color(255, 180, 90), opacity(0.4), lifespan(0.25, { fade: 0.2 })]);
for (const e of get("enemy")) if (e.pos.dist(target.pos) <= type.splash) hurt(e, type.damage);
}
function hurt(e, damage) {
if (!e.exists()) return;
e.health -= damage;
if (e.health > 0) return;
destroy(e);
coins += e.bounty; // every kill pays for the next tower
updateHud();
}
// #endregion
// #region enemies
// Each enemy walks from corner to corner and accumulates `progress` (distance
// travelled). If it reaches the end, you lose a life.
function addEnemy(n) {
const maxHealth = 2 + n * 2;
const e = add([rect(18, 18), pos(ROUTE[0]), anchor("center"), color(200, 70, 90), "enemy",
{ health: maxHealth, maxHealth, speed: 42 + n * 5, leg: 1, progress: 0, bounty: 3 + n }]);
e.onUpdate(() => {
const step = e.speed * dt();
const dir = ROUTE[e.leg].sub(e.pos);
if (dir.len() <= step) e.pos = ROUTE[e.leg++].clone();
else e.pos = e.pos.add(dir.unit().scale(step));
e.progress += step;
if (e.leg < ROUTE.length) return;
destroy(e);
lives--;
updateHud();
shake(4);
if (lives <= 0) go("end", false, wave);
});
e.onDraw(() => drawRect({ pos: vec2(-10, -16), width: (20 * e.health) / e.maxHealth, height: 3, color: rgb(120, 230, 120) }));
}
// #endregion
// #region waves
// The difficulty curve: each wave brings more enemies, with more health,
// faster and closer together. Between waves there is a break to build.
function startWave() {
wave++;
toSpawn = 4 + wave * 2;
updateHud();
loop(Math.max(0.35, 1 - wave * 0.08), () => {
addEnemy(wave);
toSpawn--;
}, toSpawn);
}
onButtonPress("start", () => (breakTime = 0)); // call the next wave early
onUpdate(() => {
if (toSpawn > 0 || get("enemy").length > 0) return (notice.text = "");
if (wave === WAVES) return go("end", true, wave);
breakTime -= dt();
notice.text = t({ es: `Oleada ${wave + 1} en ${Math.ceil(breakTime)} s (espacio: ya)`, en: `Wave ${wave + 1} in ${Math.ceil(breakTime)} s (space: now)` });
if (breakTime <= 0) {
breakTime = 6;
startWave();
}
});
// #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: 540 }),
pos(center().add(0, 30)),
anchor("center"),
color(200, 210, 255),
]);
onButtonPress("start", next);
onClick(next);
}
scene("title", () =>
screen(
t({ es: "Defensa de torres", en: "Tower defense" }),
t({
es: "Elige una torre arriba y haz clic (o toca) en una casilla verde para construirla.\nQue ningún enemigo llegue al final del camino. Sobrevive 8 oleadas.\nHaz clic o pulsa espacio para empezar.",
en: "Pick a tower at the top and click (or tap) a green tile to build it.\nDon't let any enemy reach the end of the path. Survive 8 waves.\nClick or press space to start.",
}),
() => go("game"),
),
);
scene("end", (won, wave) =>
screen(
won ? t({ es: "¡Resististe!", en: "You held out!" }) : t({ es: "Cayó la defensa", en: "The defense fell" }),
t({ es: `Llegaste a la oleada ${wave}. Haz clic para repetir.`, en: `You reached wave ${wave}. Click to play again.` }),
() => go("game"),
),
);
go("title");
// #endregionRetos de diseño
- Equilibrar la curva de oleadas con la de ingresos, para que la defensa siempre vaya un poco justa pero nunca sea imposible.
- Diseñar torres con papeles distintos (barata y rápida, cara y en área, lenta pero que frena) para que no haya una sola opción dominante.
- Que el mapa ofrezca lugares buenos y malos para construir, porque la ubicación es la decisión central.
- Hacer legible lo que pasa (alcances, vida de los enemigos, por dónde vienen) aunque haya decenas de objetos en pantalla.
Historia
- 1990OrigenRampart
Space Invaders (1978) y Missile Command (1980) ya pedían defender un territorio, pero Rampart, que combina construir murallas, colocar cañones y resistir el ataque, se considera el prototipo. El nombre y la forma actual llegaron con los mapas hechos por aficionados para StarCraft y Warcraft III.
Tower defense (Wikipedia) - 1990Rampart
Rampart alterna fases de construir, armar y defender, y fija el esquema del género.
Rampart (Wikipedia) - 2000
Los mapas personalizados de StarCraft, como Turret Defense, y luego los de Warcraft III convierten la defensa de torres en un género propio hecho por aficionados.
Tower defense (Wikipedia) - 2007Desktop Tower Defense
Desktop Tower Defense quita el camino fijo, hace que las torres formen el laberinto y populariza el género en el navegador.
Desktop Tower Defense (Wikipedia) - 2009Plants vs. Zombies
Plants vs. Zombies cambia el camino por carriles en una rejilla y lleva el género a un público masivo.
Plants vs. Zombies (Wikipedia) - 2011Kingdom Rush
Kingdom Rush, del estudio uruguayo Ironhide, pule la fórmula de puntos de construcción fijos con un héroe controlable y supera los diecisiete millones de copias.
Tower defense (Wikipedia) - 2011
Sanctum mezcla el género con el disparo en primera persona y populariza el híbrido en el que el jugador también combate.
Tower defense (Wikipedia)
Juegos de referencia
1990
Rampart
Alternas fases cronometradas en las que rodeas castillos con murallas de piezas tipo Tetris, colocas cañones dentro y luego disparas a los barcos que atacan. Lo que no quede cercado se pierde. Se considera el prototipo de la defensa de torres.
2007
Desktop Tower Defense
Juego Flash sobre un escritorio dibujado en el que no hay camino fijo, porque las torres que colocas forman el laberinto que recorren los enemigos. Popularizó el género en el navegador.
2009
Plantas contra zombis(Plants vs. Zombies)
Siembras plantas en la rejilla de un jardín para frenar zombis que avanzan por carriles; el sol que recoges es la moneda. Llevó la defensa de torres a un público muy amplio.
2011
Kingdom Rush
Los enemigos siguen caminos fijos y tú construyes y mejoras cuatro tipos de torre (arqueros, magos, artillería y cuarteles) en puntos marcados, además de mover a un héroe. Lo hizo un estudio uruguayo y nació como juego gratuito de navegador.
2014
Elematter
Defensa de torres minimalista hecha para js13kGames 2014, en menos de 13 kB. Colocas torres de cuatro elementos (fuego, agua, tierra y aire) junto a un camino fijo y las mejoras. Su autor la publicó jugable y con código abierto.
Ejercicio
En la demo, cambia la fórmula de vida de los enemigos o el premio por baja y juega dos partidas; anota en qué oleada se rompe el equilibrio. Después añade una tercera torre que ralentice en lugar de dañar y comprueba si cambia dónde conviene construir.