Skip to content
GameGenresEspañol
Strategy

Tower defense

A strategy game in which you protect a point against waves of enemies advancing along a route; you barely control them or your defenses, and instead decide which towers to buy and where to put them on a limited budget.

Also known as: TD, tower defence

Defining mechanics

  • Enemies that follow a fixed path, or a maze the player forms with their own towers.
  • Towers that fire on their own at whatever enters their range; the player only chooses the type and the tile.
  • A closed economy in which every enemy defeated pays coins for the next tower or upgrade.
  • Waves that grow in number, health and speed, with breaks to build between them.
  • Lives lost whenever an enemy reaches the end of the route.

Gameplay loop

Look at the incoming wave, spend money on towers or upgrades, watch how the defense holds, collect the bounties and fix the weak spots before the next one.

Our demo

Controls. Pick the tower with the buttons at the top and click or tap a green tile to build it. Keys 1 and 2 also pick a tower, and space brings the wave forward.

Open full screen

A fixed path with several bends, two tower types and eight waves of ever more numerous, tougher and faster enemies. Just enough to feel the genre's two decisions, which are what to spend on and where to build.

Made with Kaplay, in about 198 lines of JavaScript.

How it's made

  1. Setup

    The game is created at a fixed size and the grid (tile size, rows and columns) and the match rules are defined, such as the number of waves, lives and starting money. Changing these constants is the quickest way to tune the difficulty.

    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. The path

    The route is a list of corners. From it the code works out the path tiles, where you cannot build, and the route in pixels that enemies follow. A long, winding path gives towers more time and creates spots that are better than others.

    // 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. Tower types

    Each tower is a data object with cost, range, fire rate, damage and area. The two complement each other: the archer is cheap and fast against one enemy, and the cannon is expensive and slow but hits a group. If one were better at everything, there would be no decision.

    // 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. Economy and scoreboard

    The top bar shows coins, lives and wave, and holds the buttons to pick a tower. Money is the resource that ties everything together, because killing enemies gives coins and coins buy towers that kill more enemies.

    // 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. Placing towers

    On a click, the mouse position is turned into a tile and the code checks that it is free, not on the path and affordable. The range is drawn under the cursor in green or red, so the player can see before spending whether that spot covers the path well.

    // 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. Aiming and shooting

    Each tower looks, among the enemies in its range, for the one that has advanced the most, because it is closest to escaping. The bullet chases its target; the cannon also damages everyone near the impact. Each kill adds its bounty to the coins.

    // 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. Enemies

    Enemies move from corner to corner and accumulate the distance travelled, which towers use to choose a target. Their health, speed and bounty depend on the wave number. If one reaches the end, a life is lost.

    // 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. The wave curve

    Each wave brings more enemies, with more health and closer together, and there are a few seconds to build between waves. This curve, compared with what the player earns from kills, is what decides whether the game feels fair, easy or impossible.

    // 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. Scenes

    The game has three scenes, title, game and end, and go() jumps between them. Restarting the game scene resets money, lives and the map to the start.

    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");

What we simplified

  • Towers cannot be upgraded or sold, and there is a single enemy type that gets stronger with each wave.
  • Towers, enemies and bullets are circles and squares, with no animations or sound.
  • A single map with a fixed path; there is no maze for the player to lengthen.

To modify it

  • Add a third tower that deals no damage but slows enemies for a few seconds, and think about where it is best placed.
  • Allow upgrading a tower by clicking on it (more damage or range in exchange for coins).
  • Create a fast enemy with little health and a slow one with a lot, and mix them in the waves.
  • Change the targeting rule to the enemy with most health or the closest one, and compare how many lives you lose.
Full code
// 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

Design challenges

  • Balancing the wave curve against the income curve, so the defense is always a little stretched but never hopeless.
  • Designing towers with distinct roles (cheap and fast, expensive and area damage, slow but slowing) so no single option dominates.
  • Making the map offer good and bad places to build, because placement is the core decision.
  • Keeping what happens readable (ranges, enemy health, where they come from) even with dozens of objects on screen.

History

  1. 1990
    OriginRampart

    Space Invaders (1978) and Missile Command (1980) already asked you to defend territory, but Rampart, which combines building walls, placing cannons and surviving the assault, is considered the prototype. The name and today's form came with fan-made maps for StarCraft and Warcraft III.

    Tower defense (Wikipedia)
  2. 1990
    Rampart

    Rampart alternates phases of building, arming and defending, and sets the genre's template.

    Rampart (Wikipedia)
  3. 2000

    Custom StarCraft maps such as Turret Defense, and later Warcraft III ones, turn tower defense into a genre of its own made by fans.

    Tower defense (Wikipedia)
  4. 2007
    Desktop Tower Defense

    Desktop Tower Defense removes the fixed path, lets towers form the maze and popularises the genre in the browser.

    Desktop Tower Defense (Wikipedia)
  5. 2009
    Plants vs. Zombies

    Plants vs. Zombies swaps the path for lanes on a grid and takes the genre to a mass audience.

    Plants vs. Zombies (Wikipedia)
  6. 2011
    Kingdom Rush

    Kingdom Rush, from Uruguayan studio Ironhide, polishes the fixed build-spot formula with a controllable hero and sells over seventeen million copies.

    Tower defense (Wikipedia)
  7. 2011

    Sanctum blends the genre with first-person shooting and popularises the hybrid in which the player also fights.

    Tower defense (Wikipedia)

Reference games

1990

Rampart

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

You alternate timed phases in which you surround castles with walls made of Tetris-like pieces, place cannons inside and then fire at attacking ships. Whatever is left unenclosed is lost. It is considered the prototype of tower defense.

2009

Plants vs. Zombies

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

You plant flora on a lawn grid to stop zombies advancing down lanes; the sun you collect is the currency. It brought tower defense to a very wide audience.

2011

Kingdom Rush

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

Enemies follow fixed roads while you build and upgrade four tower types (archers, mages, artillery and barracks) on marked spots, and move a hero around. It was made by a Uruguayan studio and began as a free browser game.

2014

Elematter

Jack Rugile. navegador

A minimalist tower defense made for js13kGames 2014 in under 13 kB. You place towers of four elements (fire, water, earth and air) beside a fixed path and upgrade them. Its author published it playable and open source.

Exercise

In the demo, change the enemies' health formula or the bounty per kill and play two rounds; note at which wave the balance breaks. Then add a third tower that slows instead of damaging and check whether it changes where it pays to build.

Sources