
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 screenA 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
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);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"], }, }); }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));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; });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); } });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));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); });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");
// #endregionDesign 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
- 1980OriginSpace 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) - 1981Donkey Kong
Donkey Kong makes jumping the central action and popularises the genre in arcades.
Kent, 2001 - 1981Jump Bug
Jump Bug is the first platformer with a scrolling screen, horizontally and, in one section, vertically.
Jump Bug (Wikipedia) - 1982Pitfall!
Pitfall! expands the setting to a world of hundreds of screens on a home console.
Pitfall! (Wikipedia) - 1985Super Mario Bros.
Super Mario Bros. combines side-scrolling, momentum and variable jumps, and becomes the genre's model.
Donovan, 2010 - 1987Mega Man
Mega Man lets you choose the order of its levels, and each defeated boss grants a new weapon.
Mega Man (1987) (Wikipedia) - 1989Prince of Persia
Prince of Persia uses rotoscoped animation for fluid, realistic movement and starts the line of cinematic platformers.
Platformer (Wikipedia) - 1991Sonic the Hedgehog
Sonic the Hedgehog makes speed the game's identity.
Sonic the Hedgehog (1991) (Wikipedia) - 1996Super Mario 64
Super Mario 64 moves the genre into 3D worlds with a free camera.
Super Mario 64 (Wikipedia) - 2008Braid
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) - 2010Super 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) - 2015Celeste 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
Ladder-and-platform arcade game where you dig holes to trap enemies. Often cited as the first platform game.
1981
Donkey Kong
Jumpman (later Mario) climbs girders and jumps over barrels to rescue Pauline. It made jumping the genre's central action.
1981
Jump Bug
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
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!
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.
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
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
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
A platformer built around speed and momentum, with loops and slopes that turn movement into spectacle.
1996
Super Mario 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
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.
2008
Spelunky
A platformer where every cave is randomly generated and dying sends you back to the start with no saves. It was one of the first to carry roguelike ideas into real-time action, and the original version is free.
2010
Super Meat Boy
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
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
Precision platformer made in four days for PICO-8, the origin of Celeste (2018). Its authors published it playable in the browser.
2018
ONOFF
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
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.