DEV Community

Devanshu Biswas
Devanshu Biswas

Posted on

Kaboom has no engine — it's a bomber, an array of bombs and one paddle, and gravity is two lines per frame

I keep expecting arcade games to need an engine, and they keep proving me wrong. Kaboom — the 1981 game where a mad bomber patrols the top of the sky lobbing bombs and you slide a stack of buckets under them to catch every one — turns out to be three plain objects drawn on a single canvas. No sprite sheet, no physics library, no scene graph. A bomber that is basically one x-coordinate, an array of falling bombs, one paddle you steer, and a requestAnimationFrame loop that advances all of it by real elapsed seconds. I built the whole thing — real gravity, wave speed-ramp, three lives — in about 130 lines of vanilla JavaScript. Here's how it actually fits together.

The whole game is three things on a canvas

There's nothing to simulate beyond a handful of variables. The bomber is { x, vx, dir } sliding across the top. Bombs are an array, each entry { x, y, vy, r }. The paddle is { x, hw } — a centre and a half-width. Add counters for score, wave and how many buckets you have left, and that is the game state. Everything on screen is redrawn from those numbers every frame with fillText for the emoji and a few trapezoids for the buckets. Because the state is this small, every rule below only ever touches a couple of numbers.

The loop moves everything by real time

A canvas game is a loop that repaints fast enough to look like motion. Each frame I measure dt — the real seconds since the last frame — advance everything by that amount, and redraw:

function loop(now){
  let dt = (now - last) / 1000;   // real seconds elapsed
  last = now;
  if (dt > 0.05) dt = 0.05;       // clamp a tab-switch / lag spike
  update(dt);
  draw();
  requestAnimationFrame(loop);
}
Enter fullscreen mode Exit fullscreen mode

Multiplying speeds by elapsed seconds is what makes it frame-rate independent — it runs the same on a 60fps laptop and a 144fps monitor. The dt clamp is the detail people miss: without it, a background tab hands you one enormous frame gap and a bomb teleports straight through the buckets.

Real gravity is two lines

This is the bit I refused to fake. A bomb doesn't fall at a constant speed — it accelerates, like a real falling object. Every frame gravity adds to its downward velocity, then that velocity moves it down:

b.vy += gravity * dt;   // velocity gains speed each frame
b.y  += b.vy   * dt;    // position moves by the NEW velocity
Enter fullscreen mode Exit fullscreen mode

That's Euler integration — the same two lines every physics engine uses. Because a bomb starts slow and speeds up, catching one late is far harder than catching it early, and that's the entire tension of the game. Bombs also spawn on a stopwatch, not per frame: I accumulate dt into a dropTimer and push a new bomb when it crosses dropInterval. A per-frame spawn would flood a fast screen and starve a slow one.

Catch is a rectangle-overlap test; a miss blows up the sky

Collision is easy because the buckets never move vertically — the rim is a fixed line, RIM_Y. A bomb is caught the instant its centre drops past that line while its x sits within the paddle's half-width; if it reaches the floor without lining up, it's a miss. I walk the bomb array backwards so splicing a caught or missed bomb mid-loop never disturbs the indices I haven't checked:

for (let i = bombs.length - 1; i >= 0; i--){
  const b = bombs[i];
  b.vy += gravity * dt; b.y += b.vy * dt;
  if (b.y >= RIM_Y && Math.abs(b.x - paddle.x) <= paddle.hw + b.r){
    bombs.splice(i, 1); onCatch(b);
  } else if (b.y - b.r > GROUND_Y){
    bombs.splice(i, 1); onMiss(); break;
  }
}
Enter fullscreen mode Exit fullscreen mode

A miss is the signature Kaboom blast: one dropped bomb detonates every bomb on screen, so onMiss empties the whole array, costs you a bucket, and sets a short negative dropTimer to give you a beat before bombs resume. Lose all three buckets and it's game over.

Difficulty is data, not scattered ifs

The arcade heartbeat is escalation, and I keep it in one place. Clear a wave and a single applyWave() re-reads the wave number and turns four knobs at once — the bomber flies faster, gravity pulls harder, dropInterval shrinks so bombs rain quicker, and points-per-catch climb (capped at eight). Every 1,000 points earns a bucket back, up to three, and the best score is saved to localStorage. Keeping all of it in one function means tuning the game is editing data, not chasing conditionals across the codebase. No engine required — the falling numbers are the game.

Play it (arrow keys, A/D, or just move the mouse; Space to start):
https://dev48v.infy.uk/game/day45-kaboom.html

Top comments (0)