A game is a changing state

A game remembers things: the position of a ball, whose turn it is, which cards are face up, or how long the player has survived. Together those values form the state. The screen is a picture of that state at a particular moment. Thinking in those terms helps separate the rules from the drawing.

Some games update only after an action. Three in a Row can wait until a square is selected. Others must update continuously: a falling asteroid keeps moving even if the player does nothing. Continuous games need a loop.

Input, update, draw

A typical frame has three responsibilities. Read the latest input, update positions and rules, then draw the resulting state. The browser offers requestAnimationFrame to schedule work before the next screen repaint. The callback receives a timestamp you can use to measure the time since the previous frame.

let previous;
function frame(now) {
  const seconds = previous === undefined
    ? 0 : Math.min((now - previous) / 1000, 0.04);
  previous = now;
  update(seconds);
  draw();
  requestAnimationFrame(frame);
}
requestAnimationFrame(frame);

The first frame uses zero elapsed time because there is no previous frame. The cap limits how far an object can jump after a delayed frame. A cap is a practical starter technique, although more demanding simulations benefit from a fixed-timestep accumulator.

Move by time, not by frames

If a ship moves four pixels every frame, it travels farther on a display that runs more frames per second. Expressing speed in pixels per second avoids that dependence. A ship moving at 240 pixels per second advances four pixels during a frame that lasts about one-sixtieth of a second.

ship.x += direction * speedPixelsPerSecond * seconds;
ship.x = Math.max(0, Math.min(boardWidth - ship.width, ship.x));

Clamping the position keeps the ship inside the board. This is separate from the input code, so it works whether the direction came from a keyboard, a touch control, or a mouse.

Use a grid when the rules want one

Snake is different from a freely moving ship. It advances exactly one cell at a time. The renderer can run every frame while a time accumulator decides when a movement step is due. Keep the simulation position in grid coordinates, then multiply by cell size when drawing. This avoids rounding errors and makes food placement straightforward.

Collision is a question about space

For rectangular objects, overlap occurs when neither object is completely to the left, right, above, or below the other. For Snake, collision can be as simple as asking whether two cells have the same coordinates. Choose the simplest representation that matches the game rather than introducing a physics library automatically.

A fast ball can pass through a thin brick between frames. You can reduce this risk with smaller update steps, sensible speed limits, or a swept collision test. After a bounce, move the ball out of the collision region so it does not reverse direction repeatedly while still inside the brick.

Make restart a complete reset

A new round should reset positions, score, timers, and the terminal state. Avoid creating another animation loop each time Start is selected. Use one loop and let a running flag decide whether the simulation updates. Cancel delayed callbacks when the round changes. In these templates, selecting Start while a round is running resets that round rather than stacking new loops.

Try this experiment

Open Orbit Dodge and change the obstacle speed. Then change how often obstacles appear. Those values affect the experience differently: speed changes reaction time, while spawn frequency changes how many decisions the player faces. Testing them separately is a useful first lesson in tuning difficulty.

Put it into practice: choose a free game template and try one small change.

Keep building