Let's Build a Game Engine in HTML
Chapter 7
7 / 9

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.

gravity acceleration
jump impulse
ground friction
variable jump (optional)

How to do it

  1. 1

    Apply forces

    Each frame: vy += gravity * dt. Jump sets vy = -jumpForce when grounded and jump pressed.

  2. 2

    Friction only on ground

    When grounded and no horizontal input, multiply vx by friction. In air, use lighter air control.

  3. 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.

Type-along practice
Photo layout
Physics & Jumping
1 / 89 lines

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.

Prewritten engine scriptmistakes 0
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 };
15
16const canvas = document.getElementById('c');
17const ctx = canvas.getContext('2d');
18const W = canvas.width, H = canvas.height;
19const hud = document.getElementById('hud');
20
21const GRAVITY = 2000;
22const JUMP = 620;
23const MOVE = 230;
24const FRICTION = 0.18;
25const AIR = 0.55;
26
27const 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; });
35
36const 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];
45
46function 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}
53
54let 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;
60
61 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));
64
65 if (jumpPressed && player.grounded) {
66 player.vy = -JUMP;
67 player.grounded = false;
68 }
69 jumpPressed = false;
70
71 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);
77
78 if (player.y > H + 80) { player.x = 50; player.y = 80; player.vx = 0; player.vy = 0; }
79
80 ctx.fillStyle = '#0b1018'; ctx.fillRect(0,0,W,H);
81 // skyline
82 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);
84
85 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 }
92
93 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();
98
99 hud.textContent = 'Chapter 7 · g=' + GRAVITY + ' jump=' + JUMP + ' grounded=' + player.grounded;
100 requestAnimationFrame(loop);
101}
102requestAnimationFrame(loop);
103
104})();
Your lineline 1
(function () {

WASD/Arrows to run, Space or W/Up to jump. · Sliders update the play window after a short pause.

Chapter solution
Reference
Live-bound HTML
Play window
Live