Saltar al contenido
GameGenresEnglish
Estrategia

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 completa

Un 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

  1. 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);
  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);
      }
    }
  3. 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] },
    ];
  4. 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();
  5. 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);
      });
    }
  6. 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();
    }
  7. 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) }));
    }
  8. 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();
      }
    });
  9. 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");
// #endregion

Retos 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

  1. 1990
    OrigenRampart

    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)
  2. 1990
    Rampart

    Rampart alterna fases de construir, armar y defender, y fija el esquema del género.

    Rampart (Wikipedia)
  3. 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)
  4. 2007
    Desktop 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)
  5. 2009
    Plants 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)
  6. 2011
    Kingdom 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)
  7. 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

Atari Games. arcade, NES, Super NES, Mega Drive

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.

2009

Plantas contra zombis(Plants vs. Zombies)

PopCap Games (George Fan). Windows, macOS, iOS, Android, Xbox 360, Nintendo DS

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

Ironhide Game Studio. navegador, iOS, Android, Windows, macOS, Nintendo Switch

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

Jack Rugile. navegador

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.

Fuentes