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

Velocity & Delta Time

Frame-rate independent motion

Chapter goal

Multiply movement by dt (seconds since last frame) so 200px/s feels the same at 30fps or 60fps.

delta time
velocity units
frame independence
max dt clamp

How to do it

  1. 1

    Measure dt

    const dt = (now - last) / 1000; last = now. dt is usually ~0.016 at 60fps.

  2. 2

    Integrate velocity

    position += velocity * dt. Express speed as pixels per second, not pixels per frame.

  3. 3

    Clamp huge dt

    If the tab was backgrounded, dt can be huge. Cap it (e.g. 0.05) so the player doesn't tunnel through walls.

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
Velocity & Delta Time
1 / 76 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 MAX_SPEED = 220;
22const ACCEL = 900;
23const DRAG = 3;
24const DT_CAP = 50 / 1000;
25
26const keys = Object.create(null);
27addEventListener('keydown', e => { keys[e.code]=true; if(e.code.startsWith('Arrow')||e.code==='Space') e.preventDefault(); });
28addEventListener('keyup', e => { keys[e.code]=false; });
29
30const player = { x: W/2, y: H/2, r: 14, vx: 0, vy: 0 };
31let last = performance.now();
32let fps = 60;
33
34function loop(now) {
35 let dt = (now - last) / 1000;
36 last = now;
37 if (dt > DT_CAP) dt = DT_CAP;
38 fps = fps * 0.9 + (1/Math.max(dt, 0.0001)) * 0.1;
39
40 let ax = 0, ay = 0;
41 if (keys['ArrowLeft']||keys['KeyA']) ax -= 1;
42 if (keys['ArrowRight']||keys['KeyD']) ax += 1;
43 if (keys['ArrowUp']||keys['KeyW']) ay -= 1;
44 if (keys['ArrowDown']||keys['KeyS']) ay += 1;
45 if (ax || ay) {
46 const len = Math.hypot(ax, ay) || 1;
47 ax /= len; ay /= len;
48 player.vx += ax * ACCEL * dt;
49 player.vy += ay * ACCEL * dt;
50 }
51
52 // exponential drag
53 const drag = Math.exp(-DRAG * dt);
54 player.vx *= drag;
55 player.vy *= drag;
56
57 const sp = Math.hypot(player.vx, player.vy);
58 if (sp > MAX_SPEED) {
59 player.vx = player.vx / sp * MAX_SPEED;
60 player.vy = player.vy / sp * MAX_SPEED;
61 }
62
63 player.x += player.vx * dt;
64 player.y += player.vy * dt;
65
66 if (player.x < player.r) { player.x = player.r; player.vx *= -0.4; }
67 if (player.x > W - player.r) { player.x = W - player.r; player.vx *= -0.4; }
68 if (player.y < player.r) { player.y = player.r; player.vy *= -0.4; }
69 if (player.y > H - player.r) { player.y = H - player.r; player.vy *= -0.4; }
70
71 ctx.fillStyle = '#0b1018';
72 ctx.fillRect(0, 0, W, H);
73
74 // velocity vector
75 ctx.strokeStyle = 'rgba(125,211,252,0.5)';
76 ctx.beginPath();
77 ctx.moveTo(player.x, player.y);
78 ctx.lineTo(player.x + player.vx * 0.12, player.y + player.vy * 0.12);
79 ctx.stroke();
80
81 ctx.fillStyle = '#5eead4';
82 ctx.beginPath();
83 ctx.arc(player.x, player.y, player.r, 0, Math.PI*2);
84 ctx.fill();
85
86 hud.textContent = 'Chapter 5 · ' + fps.toFixed(0) + ' fps · dt=' + (dt*1000).toFixed(1) + 'ms · speed=' + sp.toFixed(0) + 'px/s';
87 requestAnimationFrame(loop);
88}
89requestAnimationFrame(loop);
90
91})();
Your lineline 1
(function () {

WASD / Arrows — motion is px/second, not per-frame. · Sliders update the play window after a short pause.

Chapter solution
Reference
Live-bound HTML
Play window
Live