DEV Community

Devanshu Biswas
Devanshu Biswas

Posted on

How to build a 2D platformer: fixed-timestep loops and one-axis-at-a-time collision

A side-scroller looks like a mountain of game code, but strip it down and it is four small ideas stacked together: the world is a grid of characters, a fixed-timestep loop advances the physics in equal slices, the hero is a box that gravity pushes every tick, and — the part that actually makes it feel solid — collision is resolved one axis at a time. Get that last one right and everything else is a handful of lines.

The world is just text

There is no level editor and no data file. The entire level is an array of equal-length strings, one character per tile: # and = are solid, ^ a spike, o a coin, E an enemy, G the goal. To ask whether a pixel position is inside a wall, you divide by the tile size and read the character there.

const T = 32;                          // tile size in pixels
const SOLID = new Set(["#", "="]);
function tileAt(c, r){ return (LEVEL[r] && LEVEL[r][c]) || " "; }
function solid(c, r){
  if (c < 0) return true;              // invisible wall on the far left
  return SOLID.has(tileAt(c, r));      // everything off-map = empty
}
Enter fullscreen mode Exit fullscreen mode

Make the physics framerate-independent

If you advance the physics by however long the last frame took, the game runs at different speeds on a 60 Hz and a 144 Hz screen, and a single lag spike can shove the player clean through a wall. The fix is an accumulator: bank the real elapsed time, then spend it in fixed 1/120 s chunks so update always sees the same dt.

const STEP = 1 / 120;
let last = performance.now(), acc = 0;
function frame(now){
  let dt = (now - last) / 1000; last = now;
  if (dt > 0.25) dt = 0.25;            // clamp a tab-switch / lag spike
  acc += dt;
  while (acc >= STEP){ update(STEP); acc -= STEP; }  // deterministic
  render();
  requestAnimationFrame(frame);
}
Enter fullscreen mode Exit fullscreen mode

The one trick: resolve one axis at a time

This is the heart of the whole game. Never move both axes in a single step. First add horizontal velocity, find every solid tile the box now overlaps (cells(o) returns that tile range), and push the box back to that tile's edge, zeroing horizontal speed. Then, separately, add vertical velocity and do it again.

function moveX(o){
  o.x += o.vx * STEP;
  const b = cells(o);
  for (let r = b.r0; r <= b.r1; r++) for (let c = b.c0; c <= b.c1; c++)
    if (solid(c, r)){
      o.x = o.vx > 0 ? c * T - o.w : (c + 1) * T;   // unstick to the edge
      o.vx = 0; return;
    }
}
function moveY(o){
  o.y += o.vy * STEP;
  o.onGround = false;
  const b = cells(o);
  for (let r = b.r0; r <= b.r1; r++) for (let c = b.c0; c <= b.c1; c++)
    if (solid(c, r)){
      if (o.vy > 0){ o.y = r * T - o.h; o.onGround = true; }  // landed
      else          o.y = (r + 1) * T;                        // hit ceiling
      o.vy = 0; return;
    }
}
Enter fullscreen mode Exit fullscreen mode

Splitting the axes turns one nasty 2D corner case into two trivial 1D ones, and it makes tunnelling through a wall impossible no matter how fast you run. Landing snaps you to the tile top and flags you grounded; a rising hit stops the jump dead.

A jump that forgives you

A satisfying jump is three tiny rules, not one number:

  • Variable height — the jump sets a big upward velocity; release the key while still rising and you halve it, so a tap is a hop and a hold is a leap.
  • Coyote-time — for ~90 ms after walking off a ledge you can still jump, forgiving a late press.
  • Jump-buffer — a press made just before you land is remembered and fires the instant you touch down.

Together they close the gap between what the player meant and what they pressed.

On top of that skeleton — grid, fixed loop, box-with-gravity, split-axis collision — a following-and-clamped camera, box-vs-box coin grabs, a patrolling enemy with a stomp rule, spikes, pits and a goal flag are only a few lines apiece.

Play the finished engine and read all eight annotated build blocks live at https://dev48v.infy.uk/game/day56-platformer.html

Top comments (0)