Skip to main content

How to Make a Platformer Game

Copied to clipboard!
No boilerplate memorization required

How to Vibe Code a Crisp 2D Platformer Game

Stay in your creative flow. Direct AI models to tune tight jump physics, moving platforms, patrolling enemies, and collectible coinsโ€”and ship a multi-level 2D platformer in your browser in under 20 minutes.

Core Stack

HTML5 Canvas + JS

Physics Feel

Coyote Time + Stomp

AI Partner

Claude / Gemini

Target Time

15โ€“20 Mins

The Philosophy

The Vibe Coding Platformer Philosophy

A platformer lives or dies by how good it feels to jump. In traditional development, beginners spend days fighting sticky wall collisions and external tilemap importers before tuning their hero's jump arc. Vibe coding lets you operate as the Level & Movement Director: you ask for variable jump height, 6-frame coyote time, moving platforms, and Mario-style enemy head stomps while AI generates the physics solver and ASCII level loader in one file.

Old-School Platformer Stress

  • Getting stuck on the side edges of platforms because X and Y collisions weren't separated.
  • Stiff, robotic jump arcs with zero air control or ledge forgiveness.
  • Wrestling external level editors and broken JSON paths before building Level 1.
  • Player sliding off moving platforms instead of riding along with them.

The Vibe Coder Way

  • Design levels right in code using readable ASCII strings or clean platform arrays.
  • Prompt for pro game feel: "Add coyote time, jump buffering, and squash-and-stretch on landing."
  • Stomp enemies from above for a high bounce, collect all coins, and unlock the exit portal.
  • Instant playable HTML5 Canvas that you can upload to itch.io or GameJolt in 60 seconds.
Execution

The 4-Step Blueprint

From a single jumping hero to multi-stage levels with enemies, hazards, and exit goals.

Step 01

Nail the Jump & AABB Physics

Create a single index.html file. Start with a responsive hero character with horizontal acceleration, friction, and crisp gravity on an HTML5 Canvas.

๐Ÿ’ก Pro Vibe Tip: Ask the AI to resolve horizontal (X) collisions first and vertical (Y) collisions secondโ€”this completely eliminates snagging on platform corners.
Step 02

Design Your Levels & Moving Platforms

Lay out static ledges, horizontally/vertically moving platforms, spike hazards, bottomless pits, coin arcs, and a glowing level exit portal.

๐Ÿ”‘ Key Constraint: Tell the AI: "When the player stands on a moving platform, add the platform's deltaX to the player's X position each frame."
Step 03

Add Patrolling Enemies & Hero Upgrades

Populate ledges with patrolling creatures. Touching an enemy from the side deals damage/knockback, while landing on their head defeats them with a bounce and XP/score boost.

โœจ Iteration Secret: Include a 6-frame "Coyote Time" grace window so players can still jump for 100ms after stepping off a ledge.
Step 04

Playtest & Publish to itch.io

String 3 levels together culminating in a boss or trophy screen, test every jump gap, and upload your index.html directly to itch.io or GameJolt.

๐Ÿ† The Grand Payoff: A tight, joyful 2D side-scrolling platformer built in minutes through natural-language iteration.
Interactive Tool

Vibe Prompt Configurator

Choose your platformer world style, movement upgrades, and level mechanics. Copy the generated prompt into Claude, Cursor, or Gemini.

Special Feature Vibe
Optional custom details added straight into your master prompt.
Generated Master Prompt
Ready to paste into Claude, Gemini, or ChatGPT Single-file HTML5 Canvas ready
Platformer Spec

Essential Elements of a Platformer Game

Whether 2D side-scrolling or 3D, great platformer level design combines these core elements to challenge and delight players:

  • Static & Moving Platforms

    Combine solid stepping stones with horizontally and vertically moving platforms that test timing, momentum, and spatial awareness.

  • Obstacles, Spikes & Pits

    Place spike traps, lava hazards, and bottomless pits strategically to create exciting risk-reward platforming sequences.

  • Patrolling Enemies & Bosses

    Populate levels with creatures that patrol ledges, fire projectiles, or knock the player backโ€”culminating in memorable boss encounters.

  • Coins, XP & Hero Leveling

    Collecting coins and defeating enemies rewards XP and points that upgrade the hero's capabilities (like double-jump speed or extra hearts).

  • Clear Level Exit & Stage Goals

    Give every stage a rewarding finish lineโ€”such as unlocking a portal by collecting coins or reaching the top flagpole to advance to the next level.

  • Tight Jump Physics & Camera Follow

    Responsive air control, coyote time, and a smooth side-scrolling camera ensure the player always feels in complete command of every jump.

UX Checklist Before Shipping: Separate X and Y axis collision resolution, carry the player smoothly on moving platforms, and add squash-and-stretch visual feedback on jumps and landings.
Starter Template

The "Golden" Single-File 2D Platformer Template

Save this as index.html and open it in any browser to play a multi-stage 2D Platformer with Moving Platforms, Enemy Stomps, Coins, Spikes, and Exit Portals.

index.html
HTML5 Canvas + Vanilla JS
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vibe Platformer: Sky Isles Quest</title>
<style>
  * { box-sizing: border-box; margin: 0; padding: 0; user-select: none; }
  body {
    background: #080d1a;
    color: #f8fafc;
    font-family: system-ui, -apple-system, sans-serif;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    min-height: 100vh;
    padding: 12px;
  }
  .hud {
    width: 880px;
    max-width: 100%;
    display: flex;
    justify-content: space-between;
    background: #131c31;
    border: 1px solid #283655;
    padding: 8px 16px;
    border-radius: 10px;
    margin-bottom: 8px;
    font-size: 14px;
    font-weight: 700;
  }
  canvas {
    border: 2px solid #38bdf8;
    border-radius: 14px;
    box-shadow: 0 20px 50px rgba(0,0,0,0.65);
    max-width: 100%;
    height: auto;
    background: linear-gradient(180deg, #0f172a 0%, #1e293b 100%);
  }
</style>
</head>
<body>
  <div class="hud">
    <span id="hud-left">๐Ÿ„ Stage 1/2  |  โค๏ธ Lives: 3  |  ๐Ÿช™ Coins: 0/5</span>
    <span>Move: <kbd>A</kbd>/<kbd>D</kbd> or <kbd>Arrows</kbd> โ€ข Jump / Double-Jump: <kbd>W</kbd>/<kbd>Space</kbd></span>
    <span id="hud-right">โญ XP Score: 0</span>
  </div>
  <canvas id="game" width="880" height="460"></canvas>

<script>
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const hudLeft = document.getElementById('hud-left');
const hudRight = document.getElementById('hud-right');
const keys = {};

const LEVELS = [
  {
    platforms: [
      { x: 0, y: 400, w: 320, h: 60 },
      { x: 370, y: 340, w: 140, h: 20, vx: 2, minX: 350, maxX: 520 },
      { x: 580, y: 280, w: 190, h: 22 },
      { x: 830, y: 360, w: 260, h: 100 },
      { x: 1150, y: 290, w: 150, h: 20, vx: 2.4, minX: 1120, maxX: 1320 },
      { x: 1400, y: 230, w: 280, h: 230 }
    ],
    spikes: [
      { x: 220, y: 384, w: 45, h: 16 },
      { x: 930, y: 344, w: 60, h: 16 }
    ],
    enemies: [
      { x: 610, y: 248, w: 30, h: 32, vx: 1.6, minX: 585, maxX: 735, alive: true },
      { x: 860, y: 328, w: 30, h: 32, vx: 2.0, minX: 840, maxX: 1050, alive: true }
    ],
    coins: [
      { x: 160, y: 350, taken: false },
      { x: 440, y: 290, taken: false },
      { x: 670, y: 235, taken: false },
      { x: 960, y: 300, taken: false },
      { x: 1240, y: 235, taken: false }
    ],
    goal: { x: 1580, y: 155, w: 44, h: 75 }
  },
  {
    platforms: [
      { x: 0, y: 400, w: 230, h: 60 },
      { x: 280, y: 330, w: 130, h: 20, vx: 2.6, minX: 260, maxX: 460 },
      { x: 540, y: 260, w: 130, h: 20, vx: -2.6, minX: 500, maxX: 690 },
      { x: 760, y: 350, w: 280, h: 110 },
      { x: 1100, y: 270, w: 160, h: 22 },
      { x: 1330, y: 200, w: 300, h: 260 }
    ],
    spikes: [
      { x: 850, y: 334, w: 60, h: 16 },
      { x: 1400, y: 184, w: 45, h: 16 }
    ],
    enemies: [
      { x: 780, y: 318, w: 30, h: 32, vx: 2.2, minX: 765, maxX: 1005, alive: true },
      { x: 1120, y: 238, w: 30, h: 32, vx: 1.8, minX: 1105, maxX: 1225, alive: true },
      { x: 1470, y: 168, w: 30, h: 32, vx: 2.4, minX: 1455, maxX: 1590, alive: true }
    ],
    coins: [
      { x: 350, y: 275, taken: false },
      { x: 600, y: 205, taken: false },
      { x: 880, y: 285, taken: false },
      { x: 1180, y: 220, taken: false },
      { x: 1510, y: 145, taken: false }
    ],
    goal: { x: 1560, y: 125, w: 44, h: 75 }
  }
];

let levelIdx = 0;
let lives = 3;
let score = 0;
let camX = 0;
let stage = null;
let wonGame = false;
let particles = [];

const player = {
  x: 60, y: 300, w: 30, h: 40,
  vx: 0, vy: 0,
  grounded: false,
  coyote: 0,
  jumps: 0,
  riding: null
};

function loadLevel(idx) {
  levelIdx = idx;
  stage = JSON.parse(JSON.stringify(LEVELS[idx]));
  player.x = 60;
  player.y = 310;
  player.vx = 0;
  player.vy = 0;
  camX = 0;
  updateHud();
}

function updateHud() {
  const collected = stage.coins.filter(c => c.taken).length;
  hudLeft.textContent = `๐Ÿ„ Stage ${levelIdx + 1}/${LEVELS.length}  |  โค๏ธ Lives: ${lives}  |  ๐Ÿช™ Coins: ${collected}/${stage.coins.length}`;
  hudRight.textContent = `โญ XP Score: ${score}`;
}

function burst(x, y, color) {
  for (let i = 0; i < 10; i++) {
    particles.push({
      x, y,
      vx: (Math.random() - 0.5) * 7,
      vy: (Math.random() - 0.7) * 7,
      color, life: 22
    });
  }
}

function loseLife() {
  lives--;
  burst(player.x, player.y, '#f43f5e');
  if (lives <= 0) {
    lives = 3;
    score = 0;
    loadLevel(0);
  } else {
    player.x = 60;
    player.y = 300;
    player.vx = 0;
    player.vy = 0;
    updateHud();
  }
}

window.addEventListener('keydown', e => {
  keys[e.code] = true;
  if (wonGame && e.code === 'Space') {
    wonGame = false;
    lives = 3;
    score = 0;
    loadLevel(0);
    return;
  }
  if (e.code === 'Space' || e.code === 'KeyW' || e.code === 'ArrowUp') {
    e.preventDefault();
    if (player.coyote > 0 || player.jumps < 2) {
      player.vy = -12.2;
      player.jumps++;
      player.coyote = 0;
      burst(player.x + player.w / 2, player.y + player.h, '#38bdf8');
    }
  }
});
window.addEventListener('keyup', e => { keys[e.code] = false; });

function overlap(a, b) {
  return a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y;
}

function update() {
  if (wonGame) return;

  // Move platforms
  stage.platforms.forEach(pl => {
    if (pl.vx) {
      pl.x += pl.vx;
      if (pl.x < pl.minX || pl.x + pl.w > pl.maxX) pl.vx *= -1;
    }
  });

  if (player.riding && player.riding.vx) {
    player.x += player.riding.vx;
  }

  // Horizontal movement
  if (keys['KeyA'] || keys['ArrowLeft']) player.vx = -4.6;
  else if (keys['KeyD'] || keys['ArrowRight']) player.vx = 4.6;
  else player.vx *= 0.78;

  player.x += player.vx;
  stage.platforms.forEach(pl => {
    if (overlap(player, pl)) {
      if (player.vx > 0) player.x = pl.x - player.w;
      else if (player.vx < 0) player.x = pl.x + pl.w;
    }
  });

  // Vertical movement & gravity
  const prevBottom = player.y + player.h;
  player.vy = Math.min(13, player.vy + 0.6);
  player.y += player.vy;
  player.grounded = false;
  player.riding = null;

  stage.platforms.forEach(pl => {
    if (overlap(player, pl)) {
      if (player.vy >= 0 && prevBottom <= pl.y + 14) {
        player.y = pl.y - player.h;
        player.vy = 0;
        player.grounded = true;
        player.coyote = 6;
        player.jumps = 1;
        player.riding = pl;
      } else if (player.vy < 0) {
        player.y = pl.y + pl.h;
        player.vy = 0;
      }
    }
  });

  if (!player.grounded && player.coyote > 0) player.coyote--;

  // Bottomless pit check
  if (player.y > canvas.height + 60) {
    loseLife();
    return;
  }

  // Spikes check
  for (const sp of stage.spikes) {
    if (overlap(player, sp)) {
      loseLife();
      return;
    }
  }

  // Enemies patrol & stomp vs hurt
  stage.enemies.forEach(en => {
    if (!en.alive) return;
    en.x += en.vx;
    if (en.x < en.minX || en.x + en.w > en.maxX) en.vx *= -1;

    if (overlap(player, en)) {
      if (player.vy > 0 && prevBottom <= en.y + 16) {
        en.alive = false;
        player.vy = -10.5;
        score += 150;
        burst(en.x + 15, en.y + 15, '#a855f7');
        updateHud();
      } else {
        loseLife();
      }
    }
  });

  // Coins
  stage.coins.forEach(c => {
    if (!c.taken && Math.hypot((player.x + 15) - c.x, (player.y + 20) - c.y) < 26) {
      c.taken = true;
      score += 100;
      burst(c.x, c.y, '#facc15');
      updateHud();
    }
  });

  // Exit Goal Portal
  if (overlap(player, stage.goal)) {
    score += 300;
    if (levelIdx + 1 < LEVELS.length) {
      loadLevel(levelIdx + 1);
    } else {
      wonGame = true;
    }
  }

  camX += ((player.x - 260) - camX) * 0.12;
  camX = Math.max(0, Math.min(820, camX));

  particles.forEach(p => {
    p.x += p.vx;
    p.y += p.vy;
    p.life--;
  });
  particles = particles.filter(p => p.life > 0);
}

function draw() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  ctx.save();
  ctx.translate(-camX, 0);

  // Platforms
  stage.platforms.forEach(pl => {
    ctx.fillStyle = pl.vx ? '#0284c7' : '#334155';
    ctx.beginPath();
    ctx.roundRect(pl.x, pl.y, pl.w, pl.h, 6);
    ctx.fill();
    ctx.fillStyle = pl.vx ? '#38bdf8' : '#10b981';
    ctx.fillRect(pl.x, pl.y, pl.w, 6);
  });

  // Spikes
  stage.spikes.forEach(sp => {
    ctx.fillStyle = '#f43f5e';
    for (let x = sp.x; x < sp.x + sp.w; x += 15) {
      ctx.beginPath();
      ctx.moveTo(x, sp.y + sp.h);
      ctx.lineTo(x + 7.5, sp.y);
      ctx.lineTo(x + 15, sp.y + sp.h);
      ctx.fill();
    }
  });

  // Coins
  stage.coins.forEach(c => {
    if (c.taken) return;
    ctx.fillStyle = '#facc15';
    ctx.beginPath();
    ctx.arc(c.x, c.y, 11, 0, Math.PI * 2);
    ctx.fill();
    ctx.strokeStyle = '#fef08a';
    ctx.lineWidth = 2;
    ctx.stroke();
  });

  // Exit Portal
  const g = stage.goal;
  ctx.fillStyle = '#a855f7';
  ctx.beginPath();
  ctx.roundRect(g.x, g.y, g.w, g.h, 20);
  ctx.fill();
  ctx.strokeStyle = '#f0abfc';
  ctx.lineWidth = 3;
  ctx.stroke();

  // Enemies
  stage.enemies.forEach(en => {
    if (!en.alive) return;
    ctx.fillStyle = '#fb7185';
    ctx.beginPath();
    ctx.roundRect(en.x, en.y, en.w, en.h, 8);
    ctx.fill();
    ctx.fillStyle = '#090d16';
    ctx.fillRect(en.x + 6, en.y + 8, 5, 6);
    ctx.fillRect(en.x + 19, en.y + 8, 5, 6);
  });

  // Hero
  ctx.fillStyle = '#38bdf8';
  ctx.beginPath();
  ctx.roundRect(player.x, player.y, player.w, player.h, 8);
  ctx.fill();
  ctx.fillStyle = '#f8fafc';
  ctx.fillRect(player.x + (player.vx >= 0 ? 16 : 6), player.y + 8, 8, 8);

  particles.forEach(p => {
    ctx.fillStyle = p.color;
    ctx.fillRect(p.x, p.y, 5, 5);
  });

  ctx.restore();

  if (wonGame) {
    ctx.fillStyle = 'rgba(8, 13, 26, 0.85)';
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    ctx.fillStyle = '#10b981';
    ctx.font = 'bold 36px sans-serif';
    ctx.textAlign = 'center';
    ctx.fillText('ALL LEVELS COMPLETED! ๐Ÿ†', canvas.width / 2, 210);
    ctx.fillStyle = '#facc15';
    ctx.font = '22px sans-serif';
    ctx.fillText(`Total XP Score: ${score}`, canvas.width / 2, 255);
    ctx.fillStyle = '#f8fafc';
    ctx.font = '18px sans-serif';
    ctx.fillText('Press SPACE to Play Again', canvas.width / 2, 300);
  }

  requestAnimationFrame(() => {
    update();
    draw();
  });
}

loadLevel(0);
draw();
</script>
</body>
</html>
Troubleshooting

How to Debug Without Losing Your Vibe

Platformer physics can get tripped up by corner collisions and moving ledges. Use these three vibe fixes:

Separate X & Y Collision Passes

To stop your hero from getting stuck on the side of a floor tile, prompt the AI: "Move player.x and resolve horizontal platform collisions first; then apply gravity to player.y and resolve vertical collisions."

Stick to Moving Platforms

So the hero rides moving platforms instead of having them slide out from underneath, tell the AI: "Store a reference to the platform the player landed on and add platform.vx to player.x every frame."

Accurate Enemy Stomp Detection

Ensure head-stomps never accidentally count as player damage by prompting: "Count an enemy overlap as a Stomp whenever player.vy > 0 and the player's previous frame bottom was above enemy.y + 16."

Extensions

The Vibe Prompt Vault

Copy-paste prompts to add a Mario Maker Level Editor, Wall-Jumps & Air-Dashes, or a Final Boss Fight.

๐Ÿ› ๏ธ Prompt: Add a "Mario Maker" Live Level Editor Mode
Prompt Template

"Extend my HTML5 Canvas platformer in index.html with an 'Edit / Play' toggle button at the top. In Edit Mode, let me click on the canvas to place or erase Platforms, Moving Ledges, Spikes, Patrolling Enemies, Coins, and the Exit Portal, and save custom levels to localStorage."

๐Ÿ’จ Prompt: Add Celeste-Style Wall-Slide, Wall-Jump & 8-Way Air Dash
Prompt Template

"Upgrade my platformer hero's movement kit with two advanced mechanics: 1) Holding toward a vertical wall while airborne slows fall speed into a Wall-Slide and pressing Jump kicks off into a diagonal Wall-Jump. 2) Pressing Shift triggers an 8-directional Air Dash with afterimage ghosts that refreshes upon touching the ground."

๐Ÿ‘‘ Prompt: Add a Stage 3 Boss Battle with Health Bar & Attack Phases
Prompt Template

"Add a final Boss Arena stage to my 2D platformer featuring a giant King Slime boss with a top screen health bar (5 HP). Have the boss leap across the arena and fire radial shockwaves on landing, becoming vulnerable to head-stomps for 2 seconds after every 3rd jump."

We use cookies for ads and analytics.