Advanced

Vectors and Physics

A direction with a size is one thing: a vector. Build a tiny Vec2 type, apply gravity as a vector operation, and re-create the game's jump-and-fall arc exactly.

What you will see

1500 1044

About 5 min of reading

Quick check

Before this new lesson, one small question about the lesson you just finished. Recall it, do not look it up.

Try It

The vectors lesson shows code turning into game mechanics. I run small snippets and watch the exact output, then copy the same pattern the finished game ships.

1

A vector is a quantity with both a size and a direction, here, (x, y) px/s. Gravity is a downward vector (0, 2400). Adding gravity changes only the y velocity, so I start at rest and apply gravity for 0.1 s to watch vy climb while vx stays put.

const vx = 0 const vy = 0 const gravity = 2400 const dt = 0.1 console.log(vx, vy + gravity * dt)

Before you run it, say out loud what it will print. Then open the answer.

What it prints
0 240
Gravity is a pure downward vector, so it adds to vy only. After 0.1 s the ball falls at 240 px/s while horizontal velocity stays 0. The (0, 2400) direction means all the action is vertical, that is the whole physics of a drop.
2

A jump is an upward vector. The game applies gravity for one frame, then the player rises; on the next frames gravity shrinks the rise until vy goes negative and the player falls. I run several frames from a -820 px/s jump to watch the arc: rise, slow, fall.

let y = 0, vy = -820 const gravity = 2400, dt = 1 / 60 for (let i = 0; i < 3; i++) { vy += gravity * dt; y += vy * dt; } console.log(Math.round(y * 100) / 100)

Before you run it, say out loud what it will print. Then open the answer.

What it prints
-37
Starting at -820 px/s and adding 40 px/s per frame, the ball rises, but after a few frames vy flips positive and y goes negative (falls). The arc is just gravity nixing the jump velocity every frame, no special 'peak' code needed.
3

Horizontal motion is a constant vector. The player auto-runs at RUN_SPEED = 340 px/s; multiplied by dt that is the per-frame distance. I print the run speed and one frame's displacement so constant-velocity movement is a one-liner.

const RUN_SPEED = 340 const dt = 1 / 60 console.log(RUN_SPEED) console.log(Math.round(RUN_SPEED * dt * 1000) / 1000)

Before you run it, say out loud what it will print. Then open the answer.

What it prints
340 5.667
A constant velocity vector means x += RUN_SPEED * dt each frame. At 60 fps that is ~5.667 px per frame; the same 340 px/s on 30 fps is ~11.33 px per frame. dt keeps the speed a rate, so the player runs identically on any machine.
4

Vectors add cleanly. A coin moving left at a vector and a player moving right combine as vector arithmetic on the shared x. The game's jump impulse and gravity are both vectors applied to the same velocity. I compute a jump frame: impulse up, gravity down, net result.

let vy = -820 const gravity = 2400 const dt = 1 / 60 vy = vy + gravity * dt console.log(vy) console.log(vy > 0 ? 'falling' : 'rising')

Before you run it, say out loud what it will print. Then open the answer.

What it prints
-780 falling
After one frame vy = -820 + 2400 * (1/60) = -780, still negative, so the ball is rising but 40 px/s slower. Only after enough frames does vy cross zero and become a fall. Every physics step is one vector addition.
5

The full vertical update is a two-line vector operation: add gravity times dt, then add velocity times dt. I trace one second of free fall from rest to show vy and y growing, then cap vy at MAX_FALL so a long drop never teleports.

let vy = 0, y = 0 const gravity = 2400, dt = 1 / 60, maxFall = 1500 for (let i = 0; i < 60; i++) { vy += gravity * dt; if (vy > maxFall) vy = maxFall; y += vy * dt; } console.log(Math.round(vy), Math.round(y))

Before you run it, say out loud what it will print. Then open the answer.

What it prints
1500 1044
After 60 frames (one second) vy reaches the MAX_FALL cap of 1500 and y is ~1044 px down. Capping vy at MAX_FALL (1500) stops a long tab-switch from delivering a huge velocity. Gravity is a vector; each frame it nudges the velocity vector, which nudges position.

Put it in order

Before you write anything, put these lines back in the right order. Click a line to move it up one step. This support step is easier than writing code from scratch, and it is here to make the last example stick.

Data Flow

A vector bundles position and velocity components; gravity integrates velocity each frame, then velocity integrates position, all scaled by dt.
1 Bundle x and y into a Vec2
↓
2 Add gravity * dt to the vertical component each frame
↓
3 Cap the vertical velocity at MAX_FALL
↓
4 Add velocity * dt to the position
↓
5 Detect ground contact and stop the fall
Why this works
Length and direction

A vector can also tell you how fast something moves regardless of direction, its length (a.k.a. magnitude). In 2D it's the Pythagorean theorem:

class Vec2 {
  length() {
    return Math.hypot(this.x, this.y);   // sqrt(x*x + y*y)
  }
}

const vel = new Vec2(300, 400);
console.log(vel.length());   // 500

Math.hypot(a, b) computes √(a² + b²) directly. The game uses length() to decide whether the player is "moving fast enough right now to skip this animation," or to measure how far an obstacle is.

You can also normalize a vector (make it length 1) to get a pure direction. That's common for spawning effects in a chosen direction. But for movement, the raw x, y components are all you need.

Terminal velocity and frame independence

Two rules keep the physics believable and fair:

  1. Cap the fall speed (MAX_FALL). Without it, a long frame lets gravity accumulate too much and the player bursts through the ground.
  2. Everything scales with dt. position += velocity * dt means the motion is measured in px/s, so the jump looks identical at 30 or 60 fps.

this.vy += CONFIG.GRAVITY * dt;   // integrate velocity
this.vy = Math.min(this.vy, CONFIG.MAX_FALL);  // cap
this.y += this.vy * dt;            // integrate position

Drop the dt and the game speeds up on fast machines. Drop the MAX_FALL cap and a single slow frame can teleport the player through the floor. Both are the same idea we met in clamp and the game loop: bounds keep a game fair.

Examples

Why bundle numbers into a vector

The player has x, y and vx, vy. Right now those are just separate numbers. But "moving 340 px right and 200 px down" is naturally **one thing*: a vector.

A vector is two (or more) numbers bundled together that describe a magnitude and direction. In a 2D game that's (x, y). Positions, velocities, accelerations (gravity) are all vectors. Bundling them makes physics code short, correct, and reusable.

class Vec2 {
  constructor(x = 0, y = 0) {
    this.x = x;
    this.y = y;
  }

  add(v) {           // add both components
    this.x += v.x;
    this.y += v.y;
    return this;
  }

  scale(s) {         // stretch both components by the same factor
    this.x *= s;
    this.y *= s;
    return this;
  }
}

Velocity becomes a first-class thing: const velocity = new Vec2(340, 0);. add/scale are methods from the Classes lesson, so they operate on this: the specific vector you called them on.

Gravity as a vector operation

Gravity is just a constant downward vector added to velocity every frame. That's the entire physics model:

velocity.y += GRAVITY * dt   (every frame, until it hits the ground)
position.y += velocity.y * dt (every frame)

The game uses GRAVITY = 2400 px/s² and a MAX_FALL cap:

const GRAVITY = 2400;
const MAX_FALL = 1500;
const dt = 1 / 60;

const vy = new Vec2(0, 0);
for (let t = 0; t < 2 * 60; t++) {          // 2 seconds of falling
  vy.y += GRAVITY * dt;                     // gravity pulls down
  if (vy.y > MAX_FALL) vy.y = MAX_FALL;     // but never exceed terminal velocity
}
console.log('final vy =', vy.y.toFixed(0), '(capped at MAX_FALL = 1500)');

vy.y climbs 2400 × 1/60 = 40 each frame until it hits the MAX_FALL ceiling, then stays there. That ceiling keeps the player from falling through the floor on a slow frame.

This cap is a fairness rule: without it, a slow frame lets gravity add too much and the player teleports below the ground.

Jumping: reverse the vector, then let gravity win

A jump is simple: set the vertical vector pointing up (negative, because up is smaller y), then let gravity drain it away:

const JUMP_IMPULSE = -820;   // launch upward
let vy = new Vec2(0, JUMP_IMPULSE);

for (let frame = 0; frame < 60 * 3; frame++) {
  vy.y += GRAVITY * dt;              // gravity pulls back down
  if (vy.y > MAX_FALL) vy.y = MAX_FALL;
  y += vy.y * dt;                    // apply velocity to position
  if (y >= 0) { y = 0; break; }      // landed (ground at y=0)
}

The arc is: vy starts very negative (fast up), gravity shrinks it toward zero at the top, then it grows positive (falling down). The object rises, slows, pauses at the peak, then accelerates back down, exactly the feel the real game uses.

This is exactly what Player.update(dt) does:

update(dt, speed) {
  this.vx = speed;                       // horizontal auto-run
  this.vy += CONFIG.GRAVITY * dt;        // gravity adds to vertical velocity
  if (this.vy > CONFIG.MAX_FALL) this.vy = CONFIG.MAX_FALL;
  this.x += this.vx * dt;
  this.y += this.vy * dt;
}

Watch out: multiply by dt everywhere. Without it the jump depends on frame rate.

Words this lesson uses

Each one is defined here on this site. Open the entry for the full definition and the other words it points at.

← Classes and OOP
Previous Lesson
Classes and OOP
Next Lesson
Collision Detection
Collision Detection →