Skip to content
GameGenresEspañol
Action

Platformer

An action game whose main challenge is moving a character through a space of platforms at different heights, mostly by jumping, while avoiding falls and obstacles.

Also known as: platform game, jump 'n' run

Defining mechanics

  • Jumping under gravity, usually with a height that depends on how long the button is held.
  • Horizontal movement with or without momentum; the feel of control is the heart of the genre.
  • Spatial hazards (pits, spikes) and enemies with readable patterns.
  • Collectibles that reward exploring alternative routes.

Gameplay loop

See the obstacle, judge the jump, perform it, then move on or fail and retry immediately.

Our demo

Controls. Arrows or A/D to move, space, up arrow or W to jump. On mobile, the on-screen buttons.

Open full screen

A short level with ground, blocks, pits, coins and patrolling enemies. Just enough to feel what defines the genre, which is jumping precisely.

Made with Kaplay, in about 149 lines of JavaScript.

How it's made

  1. Setup and controls

    The game is created at a fixed size and virtual buttons are defined (left, right, jump). The rest of the code asks about those buttons, so the same game works with a keyboard or a touch screen. The gravity, speed and jump constants are the numbers that most change how the game feels.

    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. The level as text

    The level is a list of strings; each character becomes an object through addLevel. Designing levels this way is fast and lets anyone change them without tools.

    // 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. The player

    The player has area() to collide and body() so gravity affects it. Moving means applying a horizontal speed while a button is held.

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

    This is the genre's mechanic, and three tricks make it feel fair. «Coyote time» lets you jump a tenth of a second after leaving a ledge. The jump buffer does the opposite: if you press jump just before touching the ground, the game remembers the input and you jump the moment you land, instead of being ignored. And the variable jump cuts the rise short if you release the button. The first two forgive mistakes of a few hundredths of a second that players do not feel are their own.

    // 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. Patrolling enemies

    Each enemy walks and turns around when it hits a block or when there is no ground ahead. The collision decides the outcome, because stomping from above destroys it and touching it from the side makes you lose.

    // 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. Coins and goal

    Coins are the collectible that invites riskier routes. The counter shows how many you have out of the level's total, so you know whether you missed any. The flag ends the level.

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

    The camera follows the player horizontally without leaving the level's edges. Falling below the screen counts as losing.

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

    The game has three scenes, title, game and end, and go() jumps between them. This restarts the level without cleaning anything up by hand.

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

What we simplified

  • The character and blocks are rectangles; there are no animations or sound.
  • A single level, with no lives or checkpoints.
  • Horizontal movement is at a constant speed, with no momentum; accelerating and slowing down gradually is left as an exercise.

To modify it

  • Add a second jump in mid-air (double jump) and think about how it changes the level design.
  • Make the player accelerate and slow down gradually instead of moving at a constant speed.
  • Add spikes (a new character in the map) that make you lose when touched.
Full code
// 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

Design challenges

  • Tuning control (acceleration, gravity, coyote time, variable jumps) so that failure feels like the player's fault, not the game's.
  • Designing levels that teach a new mechanic somewhere safe before demanding it somewhere dangerous.
  • A difficulty curve that rises without frustrating spikes, with well-placed checkpoints.
  • A camera that shows what is coming before the player has to react.

History

  1. 1980
    OriginSpace Panic

    Space Panic had ladders and platforms but no jumping; Donkey Kong (1981) introduced the jump and is the one that set the model.

    Platformer (Wikipedia)
  2. 1981
    Donkey Kong

    Donkey Kong makes jumping the central action and popularises the genre in arcades.

    Kent, 2001
  3. 1981
    Jump Bug

    Jump Bug is the first platformer with a scrolling screen, horizontally and, in one section, vertically.

    Jump Bug (Wikipedia)
  4. 1982
    Pitfall!

    Pitfall! expands the setting to a world of hundreds of screens on a home console.

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

    Super Mario Bros. combines side-scrolling, momentum and variable jumps, and becomes the genre's model.

    Donovan, 2010
  6. 1987
    Mega Man

    Mega Man lets you choose the order of its levels, and each defeated boss grants a new weapon.

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

    Prince of Persia uses rotoscoped animation for fluid, realistic movement and starts the line of cinematic platformers.

    Platformer (Wikipedia)
  8. 1991
    Sonic the Hedgehog

    Sonic the Hedgehog makes speed the game's identity.

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

    Super Mario 64 moves the genre into 3D worlds with a free camera.

    Super Mario 64 (Wikipedia)
  10. 2008
    Braid

    Braid, designed and self-funded by Jonathan Blow with no publisher, succeeds on Xbox Live Arcade with time-manipulation jumps and puzzles, and sparks wide interest in independent games.

    Braid (Wikipedia)
  11. 2010
    Super Meat Boy

    Super Meat Boy, made by a two-person team, demands fine control and split-second timing with unlimited attempts; its success spurred other hard indie games such as Cuphead.

    Super Meat Boy (Wikipedia)
  12. 2015
    Celeste Classic

    Maddy Thorson and Noel Berry release Celeste Classic, a hardcore platformer made in four days for the PICO-8 fantasy console, playable in the browser.

    Celeste (Lexaloffle BBS, PICO-8)

Reference games

1980

Space Panic

Universal. arcade

Ladder-and-platform arcade game where you dig holes to trap enemies. Often cited as the first platform game.

1981

Donkey Kong

Nintendo. arcade

Jumpman (later Mario) climbs girders and jumps over barrels to rescue Pauline. It made jumping the genre's central action.

1981

Jump Bug

Alpha Denshi (para Hoei). arcade

You drive a car that never stops bouncing through a city, mountains and a pyramid, shooting enemies and landing on money bags. It was the first platformer with horizontal scrolling.

1982

BurgerTime

Data East. arcade, Intellivision, Atari 2600, NES

You are a chef running across platforms and ladders; walking over each giant ingredient (bun, patty, tomato, lettuce, cheese) drops it one floor until the burgers are assembled, while you dodge the food items chasing you.

1982

Pitfall!

Activision (David Crane). Atari 2600

A jungle of 255 connected screens crossed by jumping pits, vines and crocodiles. It brought the genre home with a large world.

1985

Super Mario Bros.

Nintendo. NES

Continuous side-scrolling, running momentum, variable-height jumps and levels that teach their rules without text. It set the genre's grammar.

1987

Mega Man

Capcom. NES

You choose the order in which to play six run-and-jump shooting stages; beating each stage's boss earns you its weapon, which is another boss's weakness, so the order changes the difficulty.

1989

Prince of Persia

Jordan Mechner (Broderbund). Apple II

You have 60 minutes to escape the dungeons, avoiding spikes, pits and guillotines and fighting with a sword. Its rotoscoped animation makes every jump feel weighty and real, and forces you to judge your run-up carefully.

1991

Sonic the Hedgehog

Sonic Team (Sega). Mega Drive

A platformer built around speed and momentum, with loops and slopes that turn movement into spectacle.

1996

Super Mario 64

Nintendo. Nintendo 64

Took platforming into open 3D worlds with analogue control and a free camera, shifting the goal from reaching the end to exploring.

2008

Braid

Jonathan Blow (Number None). Xbox 360

A puzzle platformer in which you can rewind time without limit, even after dying, and each world changes the rules of time. Its success without a publisher sparked wide interest in independent games.

2010

Super Meat Boy

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

Over 300 short levels full of saw blades and crumbling blocks that demand fine control and split-second timing. You die a lot, but you respawn instantly, and at the end you watch all your attempts at once.

2012

Mark of the Ninja

Klei Entertainment. Xbox 360, Windows, macOS, Linux

Side-scrolling 2D stealth. It draws on screen what is usually invisible, such as how far the noise of your footsteps travels and which areas guards can see, so you plan with full information.

2015

Celeste Classic

Maddy Thorson y Noel Berry. PICO-8, navegador

Precision platformer made in four days for PICO-8, the origin of Celeste (2018). Its authors published it playable in the browser.

2018

ONOFF

Daniel Marino y Brad Dunbar. navegador

Twenty-five short levels in which you jump over spikes and pits and, with one button, switch between two layers of the level that appear and disappear. It was made for js13kGames 2018, where the whole game fits in 13 kB, and its code is open source.

2021

Celeste Classic 2

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

A free follow-up to Celeste Classic, made for PICO-8 on Celeste's third anniversary. Instead of the air dash you use a grappling hook that pulls you sideways and that you can chain with jumps. Its authors published it playable in the browser.

Exercise

In the demo, change gravity and jump force and note how the feel changes. Then design a section in the text map that teaches stomping enemies with no risk of falling into a pit.

Subgenres

  • Cinematic platformerYou guide a character with realistic, weighty movement who jumps like a real person and dies from a single hit or fall, so you advance screen by screen through trial and error until you work out how to get past each obstacle.
  • Endless runnerYour character runs forward on its own along a course that never ends; you only jump, dodge or switch lanes, and the goal is to get as far as possible before you crash.
  • Precision platformerA demanding platformer where every jump needs exact timing; you die often, but respawn instantly and learn each room until you beat it.

Sources