Physics & Jumping
Gravity, jumps, and friction that feel intentional
Chapter goal
Layer gravity, jump impulse, coyote-free grounded checks, and ground friction on top of collision for a snappy platformer feel.
How to do it
- 1
Apply forces
Each frame: vy += gravity * dt. Jump sets vy = -jumpForce when grounded and jump pressed.
- 2
Friction only on ground
When grounded and no horizontal input, multiply vx by friction. In air, use lighter air control.
- 3
Tune with sliders
Feel is numbers. Gravity 1800–2400, jump 520–700, move 220 — then iterate. Your sliders make that instant.
Live lab
Solution code updates with sliders. Tinker is your sandbox. Play runs whichever document you launch.
Live settings
Every slider injects into the engine constants for this chapter.
The chapter engine script is prewritten below. Type it line by line — each line explains why it belongs there. Matching a line advances automatically; Enter re-checks if you are stuck.
1(function () {2 "use strict";3 // Single boot — if this document is re-parsed, abandon prior work.4 if (window.__engineBooted) return;5 window.__engineBooted = true;6 let alive = true;7 window.addEventListener("pagehide", function () { alive = false; }, { once: true });8 const raf = window.requestAnimationFrame.bind(window);9 window.requestAnimationFrame = function (cb) {10 return raf(function (ts) {11 if (!alive) return;12 try { cb(ts); } catch (err) { console.error(err); alive = false; }13 });14 };1516const canvas = document.getElementById('c');17const ctx = canvas.getContext('2d');18const W = canvas.width, H = canvas.height;19const hud = document.getElementById('hud');2021const GRAVITY = 2000;22const JUMP = 620;23const MOVE = 230;24const FRICTION = 0.18;25const AIR = 0.55;2627const keys = Object.create(null);28let jumpPressed = false;29addEventListener('keydown', e => {30 if (!keys[e.code] && (e.code === 'Space' || e.code === 'ArrowUp' || e.code === 'KeyW')) jumpPressed = true;31 keys[e.code] = true;32 if (e.code.startsWith('Arrow') || e.code === 'Space') e.preventDefault();33});34addEventListener('keyup', e => { keys[e.code] = false; });3536const player = { x: 50, y: 80, w: 26, h: 34, vx: 0, vy: 0, grounded: false };37const solids = [38 { x: 0, y: H-22, w: W, h: 22 },39 { x: 100, y: 280, w: 120, h: 14 },40 { x: 280, y: 230, w: 100, h: 14 },41 { x: 430, y: 180, w: 90, h: 14 },42 { x: 200, y: 150, w: 70, h: 14 },43 { x: 520, y: 260, w: 80, h: 14 },44];4546function aabb(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;}47function resolve(p,s){48 const ox=Math.min(p.x+p.w-s.x,s.x+s.w-p.x);49 const oy=Math.min(p.y+p.h-s.y,s.y+s.h-p.y);50 if(ox<oy){ if(p.x+p.w/2<s.x+s.w/2)p.x-=ox; else p.x+=ox; p.vx=0; }51 else { if(p.y+p.h/2<s.y+s.h/2){ p.y-=oy; p.vy=0; p.grounded=true; } else { p.y+=oy; p.vy=0; } }52}5354let last = performance.now();55function loop(now){56 const dt = Math.min((now-last)/1000, 0.05); last = now;57 let input = 0;58 if (keys['ArrowLeft']||keys['KeyA']) input -= 1;59 if (keys['ArrowRight']||keys['KeyD']) input += 1;6061 const control = player.grounded ? 1 : AIR;62 if (input !== 0) player.vx = input * MOVE * control + player.vx * (1 - control);63 else if (player.grounded) player.vx *= (1 - Math.min(1, FRICTION * 60 * dt));6465 if (jumpPressed && player.grounded) {66 player.vy = -JUMP;67 player.grounded = false;68 }69 jumpPressed = false;7071 player.vy += GRAVITY * dt;72 player.grounded = false;73 player.x += player.vx * dt;74 for (const s of solids) if (aabb(player,s)) resolve(player,s);75 player.y += player.vy * dt;76 for (const s of solids) if (aabb(player,s)) resolve(player,s);7778 if (player.y > H + 80) { player.x = 50; player.y = 80; player.vx = 0; player.vy = 0; }7980 ctx.fillStyle = '#0b1018'; ctx.fillRect(0,0,W,H);81 // skyline82 ctx.fillStyle = 'rgba(94,234,212,0.04)';83 for (let i=0;i<8;i++) ctx.fillRect(20+i*80, 40+(i%3)*30, 40, H);8485 ctx.fillStyle = '#1e2a3a';86 for (const s of solids) {87 ctx.fillRect(s.x,s.y,s.w,s.h);88 ctx.fillStyle = 'rgba(94,234,212,0.15)';89 ctx.fillRect(s.x,s.y,s.w,2);90 ctx.fillStyle = '#1e2a3a';91 }9293 ctx.fillStyle = '#5eead4';94 ctx.beginPath();95 const x=player.x,y=player.y,w=player.w,h=player.h,r=6;96 ctx.moveTo(x+r,y); ctx.arcTo(x+w,y,x+w,y+h,r); ctx.arcTo(x+w,y+h,x,y+h,r); ctx.arcTo(x,y+h,x,y,r); ctx.arcTo(x,y,x+w,y,r);97 ctx.fill();9899 hud.textContent = 'Chapter 7 · g=' + GRAVITY + ' jump=' + JUMP + ' grounded=' + player.grounded;100 requestAnimationFrame(loop);101}102requestAnimationFrame(loop);103104})();
WASD/Arrows to run, Space or W/Up to jump. · Sliders update the play window after a short pause.