Saltar al contenido
GameGenresEnglish
Acción

Plataformas

Juego de acción en el que el reto principal es desplazar a un personaje por un espacio de plataformas a distintas alturas, sobre todo saltando, mientras se esquivan caídas y obstáculos.

También se llama: juego de plataformas, plataformero

Mecánicas que lo definen

  • Salto con gravedad, normalmente de altura variable según cuánto se mantiene el botón.
  • Movimiento horizontal con o sin inercia; la sensación de control es el corazón del género.
  • Peligros espaciales (vacíos, pinchos) y enemigos con patrones legibles.
  • Coleccionables que premian explorar rutas alternativas.

Bucle de juego

Ver el obstáculo, calcular el salto, ejecutarlo, avanzar o fallar y reintentar de inmediato.

Demo propia

Controles. Flechas o A/D para moverte, espacio, flecha arriba o W para saltar. En móvil, los botones en pantalla.

Abrir en pantalla completa

Un nivel corto con suelo, bloques, vacíos, monedas y enemigos que patrullan. Lo justo para sentir lo que define al género, que es saltar con precisión.

Hecha con Kaplay, en unas 149 líneas de JavaScript.

Cómo está hecha

  1. Configuración y controles

    Se crea el juego con un tamaño fijo y se definen botones virtuales (left, right, jump). El resto del código pregunta por esos botones, así el mismo juego funciona con teclado o con pantalla táctil. Las constantes de gravedad, velocidad y salto son los números que más cambian la sensación del juego.

    const k = kaplay({
      width: 640,
      height: 360,
      letterbox: true, pixelDensity,
      background: [30, 34, 72],
      // Virtual buttons: the game asks about "jump", not about a specific key.
      buttons: {
        left: { keyboard: ["left", "a"] },
        right: { keyboard: ["right", "d"] },
        jump: { keyboard: ["space", "up", "w"] },
      },
    });
    touchControls(k, [
      { button: "left", label: "◀" },
      { button: "right", label: "▶" },
      { button: "jump", label: "⤒", side: "right" },
    ]);
    
    const GRAVITY = 1600;
    const SPEED = 220;
    const JUMP_FORCE = 620;
    const COYOTE_TIME = 0.1; // seconds you can still jump after leaving a ledge
    const JUMP_BUFFER = 0.12; // seconds a jump pressed before landing is "remembered"
    const TILE = 32;
    setGravity(GRAVITY);
  2. El nivel como texto

    El nivel es una lista de cadenas; cada carácter se convierte en un objeto con addLevel. Diseñar niveles así es rápido y permite que cualquiera los modifique sin herramientas.

    // The level is a text drawing: each character is a kind of tile.
    // = ground · # block · o coin · e enemy · F flag
    const MAP = [
      "                                                   ",
      "                                                   ",
      "                                          o o      ",
      "                      o o                ###       ",
      "                     #####       o             F   ",
      "           o o                  ###       e    #   ",
      "          #####        e                 ####  #   ",
      "    o           ######====    ====   e   ====  #   ",
      "=========   ======        ====    ======   ========",
      "=========   ======        ====    ======   ========",
    ];
    
    function solidTile(c) {
      return [rect(TILE, TILE), color(c), area(), body({ isStatic: true }), "solid"];
    }
    
    function buildLevel() {
      return addLevel(MAP, {
        tileWidth: TILE,
        tileHeight: TILE,
        tiles: {
          "=": () => solidTile(rgb(84, 160, 92)),
          "#": () => solidTile(rgb(176, 120, 72)),
          o: () => [circle(8), color(255, 205, 60), pos(16, 16), area(), anchor("center"), "coin"],
          e: () => [rect(28, 24), color(220, 70, 60), pos(2, 8), area(), body(), { dir: -1 }, "enemy"],
          F: () => [rect(8, 64), color(240, 240, 255), pos(12, -32), area(), "flag"],
        },
      });
    }
  3. El jugador

    El jugador tiene area() para chocar y body() para que le afecte la gravedad. Moverse es aplicar una velocidad horizontal mientras se mantiene un botón.

    const player = add([rect(24, 32), color(120, 200, 255), pos(64, 160), area(), body(), anchor("bot"), "player"]);
    
    // Horizontal movement: constant speed while the button is held.
    onButtonDown("left", () => player.move(-SPEED, 0));
    onButtonDown("right", () => player.move(SPEED, 0));
  4. El salto

    Es la mecánica del género, y tres trucos la hacen sentir justa. El «tiempo de coyote» deja saltar una décima de segundo después de salir del borde. El búfer de salto hace lo contrario: si pulsas saltar un instante antes de tocar el suelo, el juego recuerda la orden y saltas justo al aterrizar, en vez de ignorarte. Y el salto variable corta la subida si sueltas el botón. Los dos primeros perdonan errores de unas centésimas que el jugador no siente como suyos.

    // Jumping is THE mechanic of the genre. Three details make it feel right:
    // 1) coyote time: you can still jump a moment after leaving a ledge;
    // 2) jump buffer: press just before landing and the jump happens on landing;
    // 3) variable height: release the button early for a shorter jump.
    let airTime = 0;
    let bufferedJump = 0; // time left on a jump that was pressed but not done yet
    function tryJump() {
      if (bufferedJump > 0 && airTime < COYOTE_TIME) {
        player.jump(JUMP_FORCE);
        airTime = 1;
        bufferedJump = 0;
        // If the button was already released (buffered jump), make it a short one.
        if (!isButtonDown("jump")) player.vel.y *= 0.45;
      }
    }
    player.onUpdate(() => {
      airTime = player.isGrounded() ? 0 : airTime + dt();
      bufferedJump = Math.max(0, bufferedJump - dt());
      tryJump();
    });
    onButtonPress("jump", () => {
      bufferedJump = JUMP_BUFFER;
      tryJump();
    });
    onButtonRelease("jump", () => {
      if (player.vel.y < 0) player.vel.y *= 0.45;
    });
  5. Enemigos que patrullan

    Cada enemigo avanza y gira cuando choca con un bloque o cuando no hay suelo delante. La colisión decide el resultado, porque pisarlo desde arriba lo elimina y tocarlo de lado te hace perder.

    // They patrol back and forth, turning at a wall or at the edge of a platform.
    for (const e of level.get("enemy")) {
      e.onUpdate(() => {
        e.move(e.dir * 60, 0);
        const ahead = e.pos.add(level.pos).add(e.dir > 0 ? 30 : -2, 30);
        const groundAhead = level.get("solid").some((s) => s.hasPoint(ahead));
        if (!groundAhead) e.dir *= -1;
      });
      e.onCollide("solid", (_, col) => {
        if (col.isLeft() || col.isRight()) e.dir *= -1;
      });
    }
    // Stomping an enemy from above destroys it; touching it from the side loses.
    player.onCollide("enemy", (e, col) => {
      if (col.isBottom()) {
        destroy(e);
        player.jump(JUMP_FORCE * 0.6);
      } else {
        go("end", false, coins, totalCoins);
      }
    });
  6. Monedas y meta

    Las monedas son el coleccionable que invita a tomar rutas más arriesgadas. El marcador muestra cuántas llevas del total del nivel, así sabes si te dejaste alguna. La bandera termina el nivel.

    player.onCollide("coin", (c) => {
      destroy(c);
      coins++;
      updateHud();
    });
    player.onCollide("flag", () => go("end", true, coins, totalCoins));
  7. La cámara

    La cámara sigue al jugador en horizontal sin salirse de los bordes del nivel. Caer por debajo de la pantalla cuenta como perder.

    // The camera follows the player horizontally; falling into a pit loses.
    const levelWidth = MAP[0].length * TILE;
    player.onUpdate(() => {
      const x = Math.min(Math.max(player.pos.x, width() / 2), levelWidth - width() / 2);
      camPos(x, height() / 2);
      if (player.pos.y > height() + 200) go("end", false, coins, totalCoins);
    });
  8. Escenas

    El juego tiene tres escenas (title, game y end) y go() salta de una a otra. Así se reinicia el nivel sin tener que limpiar nada a mano.

    function screen(title, subtitle, next) {
      add([text(title, { size: 34, font: FONT, align: "center" }), pos(center().sub(0, 30)), anchor("center")]);
      add([
        text(subtitle, { size: 16, font: FONT, align: "center", width: 520 }),
        pos(center().add(0, 30)),
        anchor("center"),
        color(200, 210, 255),
      ]);
      onButtonPress("jump", next);
      onClick(next);
    }
    
    scene("title", () =>
      screen(
        t({ es: "Plataformas", en: "Platformer" }),
        t({
          es: "Flechas para moverte, espacio para saltar. Llega a la bandera.\nPulsa espacio o toca para empezar.",
          en: "Arrows to move, space to jump. Reach the flag.\nPress space or tap to start.",
        }),
        () => go("game"),
      ),
    );
    
    scene("end", (won, coins, total) =>
      screen(
        won ? t({ es: "¡Llegaste!", en: "You made it!" }) : t({ es: "Inténtalo otra vez", en: "Try again" }),
        t({
          es: `Monedas: ${coins}/${total}. Pulsa espacio para repetir.`,
          en: `Coins: ${coins}/${total}. Press space to play again.`,
        }),
        () => go("game"),
      ),
    );
    
    go("title");

Qué simplificamos

  • El personaje y los bloques son rectángulos; no hay animaciones ni sonido.
  • Un solo nivel, sin vidas ni puntos de control.
  • El movimiento horizontal es a velocidad constante, sin inercia; acelerar y frenar poco a poco queda como ejercicio.

Para modificarla

  • Agrega un segundo salto en el aire (doble salto) y piensa cómo cambia el diseño del nivel.
  • Haz que el jugador acelere y frene poco a poco en lugar de moverse a velocidad constante.
  • Añade pinchos (un nuevo carácter en el mapa) que hagan perder al tocarlos.
Código completo
// Platformer 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: [30, 34, 72],
  // Virtual buttons: the game asks about "jump", not about a specific key.
  buttons: {
    left: { keyboard: ["left", "a"] },
    right: { keyboard: ["right", "d"] },
    jump: { keyboard: ["space", "up", "w"] },
  },
});
touchControls(k, [
  { button: "left", label: "◀" },
  { button: "right", label: "▶" },
  { button: "jump", label: "⤒", side: "right" },
]);

const GRAVITY = 1600;
const SPEED = 220;
const JUMP_FORCE = 620;
const COYOTE_TIME = 0.1; // seconds you can still jump after leaving a ledge
const JUMP_BUFFER = 0.12; // seconds a jump pressed before landing is "remembered"
const TILE = 32;
setGravity(GRAVITY);
// #endregion

// #region level
// The level is a text drawing: each character is a kind of tile.
// = ground · # block · o coin · e enemy · F flag
const MAP = [
  "                                                   ",
  "                                                   ",
  "                                          o o      ",
  "                      o o                ###       ",
  "                     #####       o             F   ",
  "           o o                  ###       e    #   ",
  "          #####        e                 ####  #   ",
  "    o           ######====    ====   e   ====  #   ",
  "=========   ======        ====    ======   ========",
  "=========   ======        ====    ======   ========",
];

function solidTile(c) {
  return [rect(TILE, TILE), color(c), area(), body({ isStatic: true }), "solid"];
}

function buildLevel() {
  return addLevel(MAP, {
    tileWidth: TILE,
    tileHeight: TILE,
    tiles: {
      "=": () => solidTile(rgb(84, 160, 92)),
      "#": () => solidTile(rgb(176, 120, 72)),
      o: () => [circle(8), color(255, 205, 60), pos(16, 16), area(), anchor("center"), "coin"],
      e: () => [rect(28, 24), color(220, 70, 60), pos(2, 8), area(), body(), { dir: -1 }, "enemy"],
      F: () => [rect(8, 64), color(240, 240, 255), pos(12, -32), area(), "flag"],
    },
  });
}
// #endregion

scene("game", () => {
  const level = buildLevel();
  let coins = 0;
  const totalCoins = level.get("coin").length;
  const hud = add([text("", { size: 18, font: FONT }), pos(16, 12), fixed(), z(10)]);
  const updateHud = () => (hud.text = t({ es: "Monedas: ", en: "Coins: " }) + `${coins}/${totalCoins}`);
  updateHud();

  // #region player
  const player = add([rect(24, 32), color(120, 200, 255), pos(64, 160), area(), body(), anchor("bot"), "player"]);

  // Horizontal movement: constant speed while the button is held.
  onButtonDown("left", () => player.move(-SPEED, 0));
  onButtonDown("right", () => player.move(SPEED, 0));
  // #endregion

  // #region jump
  // Jumping is THE mechanic of the genre. Three details make it feel right:
  // 1) coyote time: you can still jump a moment after leaving a ledge;
  // 2) jump buffer: press just before landing and the jump happens on landing;
  // 3) variable height: release the button early for a shorter jump.
  let airTime = 0;
  let bufferedJump = 0; // time left on a jump that was pressed but not done yet
  function tryJump() {
    if (bufferedJump > 0 && airTime < COYOTE_TIME) {
      player.jump(JUMP_FORCE);
      airTime = 1;
      bufferedJump = 0;
      // If the button was already released (buffered jump), make it a short one.
      if (!isButtonDown("jump")) player.vel.y *= 0.45;
    }
  }
  player.onUpdate(() => {
    airTime = player.isGrounded() ? 0 : airTime + dt();
    bufferedJump = Math.max(0, bufferedJump - dt());
    tryJump();
  });
  onButtonPress("jump", () => {
    bufferedJump = JUMP_BUFFER;
    tryJump();
  });
  onButtonRelease("jump", () => {
    if (player.vel.y < 0) player.vel.y *= 0.45;
  });
  // #endregion

  // #region enemies
  // They patrol back and forth, turning at a wall or at the edge of a platform.
  for (const e of level.get("enemy")) {
    e.onUpdate(() => {
      e.move(e.dir * 60, 0);
      const ahead = e.pos.add(level.pos).add(e.dir > 0 ? 30 : -2, 30);
      const groundAhead = level.get("solid").some((s) => s.hasPoint(ahead));
      if (!groundAhead) e.dir *= -1;
    });
    e.onCollide("solid", (_, col) => {
      if (col.isLeft() || col.isRight()) e.dir *= -1;
    });
  }
  // Stomping an enemy from above destroys it; touching it from the side loses.
  player.onCollide("enemy", (e, col) => {
    if (col.isBottom()) {
      destroy(e);
      player.jump(JUMP_FORCE * 0.6);
    } else {
      go("end", false, coins, totalCoins);
    }
  });
  // #endregion

  // #region coins-goal
  player.onCollide("coin", (c) => {
    destroy(c);
    coins++;
    updateHud();
  });
  player.onCollide("flag", () => go("end", true, coins, totalCoins));
  // #endregion

  // #region camera
  // The camera follows the player horizontally; falling into a pit loses.
  const levelWidth = MAP[0].length * TILE;
  player.onUpdate(() => {
    const x = Math.min(Math.max(player.pos.x, width() / 2), levelWidth - width() / 2);
    camPos(x, height() / 2);
    if (player.pos.y > height() + 200) go("end", false, coins, totalCoins);
  });
  // #endregion
});

// #region scenes
function screen(title, subtitle, next) {
  add([text(title, { size: 34, font: FONT, align: "center" }), pos(center().sub(0, 30)), anchor("center")]);
  add([
    text(subtitle, { size: 16, font: FONT, align: "center", width: 520 }),
    pos(center().add(0, 30)),
    anchor("center"),
    color(200, 210, 255),
  ]);
  onButtonPress("jump", next);
  onClick(next);
}

scene("title", () =>
  screen(
    t({ es: "Plataformas", en: "Platformer" }),
    t({
      es: "Flechas para moverte, espacio para saltar. Llega a la bandera.\nPulsa espacio o toca para empezar.",
      en: "Arrows to move, space to jump. Reach the flag.\nPress space or tap to start.",
    }),
    () => go("game"),
  ),
);

scene("end", (won, coins, total) =>
  screen(
    won ? t({ es: "¡Llegaste!", en: "You made it!" }) : t({ es: "Inténtalo otra vez", en: "Try again" }),
    t({
      es: `Monedas: ${coins}/${total}. Pulsa espacio para repetir.`,
      en: `Coins: ${coins}/${total}. Press space to play again.`,
    }),
    () => go("game"),
  ),
);

go("title");
// #endregion

Retos de diseño

  • Afinar el control (aceleración, gravedad, tiempo de coyote, salto variable) para que fallar se sienta culpa del jugador y no del juego.
  • Diseñar niveles que enseñen una mecánica nueva en un lugar seguro antes de exigirla en uno peligroso.
  • Una curva de dificultad que crece sin picos frustrantes, con puntos de control bien ubicados.
  • Que la cámara muestre lo que viene antes de que el jugador tenga que reaccionar.

Historia

  1. 1980
    OrigenSpace Panic

    Space Panic tenía escaleras y plataformas pero no se podía saltar; Donkey Kong (1981) introdujo el salto y es el que fijó el modelo.

    Platformer (Wikipedia)
  2. 1981
    Donkey Kong

    Donkey Kong hace del salto la acción central y populariza el género en los arcades.

    Kent, 2001
  3. 1981
    Jump Bug

    Jump Bug es el primer plataformas con desplazamiento de pantalla, en horizontal y en un tramo también en vertical.

    Jump Bug (Wikipedia)
  4. 1982
    Pitfall!

    Pitfall! amplía el escenario a un mundo de cientos de pantallas en una consola doméstica.

    Pitfall! (Wikipedia)
  5. 1985
    Super Mario Bros.

    Super Mario Bros. combina desplazamiento lateral, inercia y salto variable, y se vuelve el modelo del género.

    Donovan, 2010
  6. 1987
    Mega Man

    Mega Man deja elegir en qué orden se juegan sus niveles, y cada jefe vencido da un arma nueva.

    Mega Man (1987) (Wikipedia)
  7. 1989
    Prince of Persia

    Prince of Persia usa animación rotoscopiada para un movimiento fluido y realista, y abre la línea de los plataformas cinematográficos.

    Platformer (Wikipedia)
  8. 1991
    Sonic the Hedgehog

    Sonic the Hedgehog hace de la velocidad la identidad del juego.

    Sonic the Hedgehog (1991) (Wikipedia)
  9. 1996
    Super Mario 64

    Super Mario 64 traslada el género a mundos 3D con cámara libre.

    Super Mario 64 (Wikipedia)
  10. 2008
    Braid

    Braid, diseñado y financiado por Jonathan Blow sin editorial, triunfa en Xbox Live Arcade con saltos y rompecabezas de manipulación del tiempo, y despierta el interés general por los juegos independientes.

    Braid (Wikipedia)
  11. 2010
    Super Meat Boy

    Super Meat Boy, hecho por un equipo de dos personas, exige control fino y reflejos de una fracción de segundo con intentos ilimitados; su éxito impulsó otros indies difíciles, como Cuphead.

    Super Meat Boy (Wikipedia)
  12. 2015
    Celeste Classic

    Maddy Thorson y Noel Berry publican Celeste Classic, un plataformas exigente hecho en cuatro días para la consola de fantasía PICO-8, que se juega en el navegador.

    Celeste (Lexaloffle BBS, PICO-8)

Juegos de referencia

1980

Space Panic

Universal. arcade

Arcade de escaleras y plataformas en el que se cavan agujeros para atrapar a los enemigos. Suele citarse como el primer juego de plataformas.

1981

Donkey Kong

Nintendo. arcade

Jumpman (luego Mario) sube andamios y salta barriles para rescatar a Pauline. Hizo del salto la acción central del género.

1981

Jump Bug

Alpha Denshi (para Hoei). arcade

Conduces un coche que no deja de rebotar por una ciudad, montañas y una pirámide, disparando a los enemigos y cayendo sobre bolsas de dinero. Fue el primer plataformas con desplazamiento horizontal de la pantalla.

1982

BurgerTime

Data East. arcade, Intellivision, Atari 2600, NES

Eres un cocinero que recorre plataformas y escaleras; al pasar sobre cada ingrediente gigante (pan, carne, tomate, lechuga, queso) lo haces caer un piso hasta que las hamburguesas quedan armadas, mientras esquivas a los alimentos que te persiguen.

1982

Pitfall!

Activision (David Crane). Atari 2600

Una selva de 255 pantallas conectadas que se recorre saltando fosos, lianas y cocodrilos. Llevó el género a la consola doméstica con un mundo amplio.

1985

Super Mario Bros.

Nintendo. NES

Desplazamiento lateral continuo, inercia al correr, salto de altura variable y niveles que enseñan sus reglas sin texto. Fijó la gramática del género.

1987

Mega Man

Capcom. NES

Eliges en qué orden recorres seis niveles de plataformas y disparos; al vencer al jefe de cada uno ganas su arma, que es la debilidad de otro jefe, así que el orden cambia la dificultad.

1989

Prince of Persia

Jordan Mechner (Broderbund). Apple II

Tienes 60 minutos para escapar de las mazmorras esquivando pinchos, fosos y guillotinas y luchando a espada. Sus animaciones rotoscopiadas hacen que cada salto se sienta pesado y real, y obligan a calcular bien la carrera.

1991

Sonic the Hedgehog

Sonic Team (Sega). Mega Drive

Plataformas centrado en la velocidad y el impulso, con rizos y pendientes que convierten el movimiento en espectáculo.

1996

Super Mario 64

Nintendo. Nintendo 64

Llevó el plataformas a mundos 3D abiertos con control analógico y cámara libre, y lo cambió de objetivo, de llegar al final a explorar.

2008

Braid

Jonathan Blow (Number None). Xbox 360

Plataformas de rompecabezas en el que puedes rebobinar el tiempo sin límite, incluso después de morir, y cada mundo cambia las reglas del tiempo. Su éxito sin editorial abrió el interés por los juegos independientes.

2010

Super Meat Boy

Team Meat (Edmund McMillen y Tommy Refenes). Xbox 360

Más de 300 niveles cortos llenos de sierras y bloques que se desmoronan, que exigen control fino y reflejos de una fracción de segundo. Mueres mucho, pero reapareces al instante, y al final ves todos tus intentos a la vez.

2012

Mark of the Ninja

Klei Entertainment. Xbox 360, Windows, macOS, Linux

Sigilo en 2D de vista lateral. Dibuja en pantalla lo que normalmente es invisible, como hasta dónde llega el ruido de tus pasos y qué zonas ven los guardias, para que planifiques con información completa.

2015

Celeste Classic

Maddy Thorson y Noel Berry. PICO-8, navegador

Plataformas de precisión hecho en cuatro días para PICO-8, origen de Celeste (2018). Sus autores lo publicaron jugable en el navegador.

2018

ONOFF

Daniel Marino y Brad Dunbar. navegador

Veinticinco niveles cortos en los que saltas pinchos y vacíos y, con un botón, alternas entre dos capas del escenario que aparecen y desaparecen. Se hizo para js13kGames 2018, donde todo el juego cabe en 13 kB, y su código es libre.

2021

Celeste Classic 2

Maddy Thorson, Noel Berry y Lena Raine. PICO-8, navegador

Secuela gratuita de Celeste Classic, hecha para PICO-8 en el tercer aniversario de Celeste. En vez del impulso en el aire usas un gancho que te lanza en horizontal y que puedes encadenar con los saltos. Sus autores lo publicaron jugable en el navegador.

Ejercicio

Cambia en la demo la gravedad y la fuerza de salto, y anota cómo cambia la sensación. Luego diseña en el mapa de texto una sección que enseñe a pisar enemigos sin riesgo de caer al vacío.

Subgéneros

  • Corredor sin finTu personaje corre solo y sin parar por un recorrido que nunca termina; tú solo saltas, esquivas o cambias de carril, y la meta es llegar lo más lejos posible antes de chocar.
  • Plataformas cinematográficoGuías a un personaje de movimientos realistas y pesados, que salta como una persona y muere de un golpe o una caída, así que avanzas pantalla a pantalla probando y equivocándote hasta dar con la forma de superar cada obstáculo.
  • Plataformas de precisiónUn plataformas muy exigente en el que cada salto pide una sincronización exacta; mueres a menudo, pero reapareces al instante y aprendes cada sala hasta superarla.

Fuentes