ThisCalculator

Everyday

Make Snake Move on a Grid Without Tying Speed to FPS

Updated · Standard formula, shown below

Make Snake Move on a Grid Without Tying Speed to FPS

Make Snake move at a fixed rate by tracking elapsed time and advancing the grid position only when a movement interval has passed. Keep drawing frames as often as the browser or display allows; the snake’s game speed then stays consistent even when refresh rates or frame rates differ.

Why frame-based movement changes speed

A display refresh rate describes how often a screen can show a new image. A game’s frame rate describes how often its update-and-draw loop runs. They are related, but neither is a reliable measure of how quickly a Snake game should advance.

If code moves the snake one cell every frame, a game running at 120 frames per second will move it about twice as often as one running at 60 frames per second. Performance dips can also make movement irregular. This is the common misconception to avoid: drawing more frequently should make animation look smoother, not make the game rules run faster.

Use elapsed time to schedule grid steps

Store the time accumulated since the last grid move. Each animation frame, add the elapsed time since the previous frame. When the accumulated time reaches the chosen movement interval, advance the snake by one cell and subtract that interval. Continue drawing independently.

For example, if the snake should move every 120 milliseconds, its target rate is about 8.33 cells per second. A display might draw at 60, 90, or 144 frames per second, but the grid step still occurs when enough time has elapsed. The interval is a game-design choice; a shorter interval means faster movement.

const stepMs = 120;
let accumulatedMs = 0;
let previousTime;

function frame(now) {
  if (previousTime === undefined) previousTime = now;
  const elapsed = now - previousTime;
  previousTime = now;

  accumulatedMs += elapsed;
  while (accumulatedMs >= stepMs) {
    updateSnakeOneCell();
    accumulatedMs -= stepMs;
  }

  drawGame();
  requestAnimationFrame(frame);
}

requestAnimationFrame(frame);

In a browser, the timestamp passed to the animation callback is measured in milliseconds. Using its difference gives elapsed time rather than assuming that each callback arrives at a particular rate. The while loop handles cases where more than one movement interval passes between callbacks.

Separate game updates from drawing

The snake’s grid position is discrete: it occupies a cell, then another cell. A frame can redraw the same cell many times before the next move. Organize the game around two responsibilities:

  • Update: advance the snake, check food and collisions, and change game state at the movement interval.
  • Draw: render the current state whenever a frame is available.

This separation also makes rendering effects easier. For instance, you can interpolate the snake’s visual position between two cells for smoother motion while keeping collision checks and food placement on whole grid cells. Interpolation changes appearance only; it should not allow the snake to occupy fractional cells in the game logic.

Choose a timing approach

For a simple Snake game, a timestamp-based accumulator is usually a good balance of simplicity and reliability. There are two common variants:

  • One-step threshold: If the elapsed time exceeds the interval, make one move and reset the timer. This is simple, but discarding extra elapsed time can make the game run slow after a delayed frame.
  • Accumulator with a loop: Add elapsed time, then perform as many fixed updates as fit. This preserves the intended simulation time after a brief delay.

For grid games, avoid an unbounded catch-up loop after a long pause. If a laptop sleeps or a background tab is suspended, a large elapsed interval could trigger many moves in one callback. A practical safeguard is to cap elapsed time added to the accumulator or limit catch-up steps, then discard excess backlog. This trades exact catch-up for a game that remains responsive.

Input, speed changes, and game state

Read direction input separately from movement timing. Store the requested direction, then apply it on a grid update. Many Snake implementations prevent an immediate reversal—for example, moving left and accepting an up turn is valid, while accepting right at that instant would send the snake directly back into itself. If several inputs arrive before a step, decide whether to keep only the latest valid turn or queue a limited number of turns.

To increase difficulty as the snake grows, change the movement interval deliberately, such as reducing it by a small amount after eating. Set a minimum interval so speed cannot become impractical. Apply speed changes at a clear point in the update, and reset or adjust the accumulator carefully; otherwise, leftover time from the old interval can cause an unexpectedly early move.

Pause and game-over states should stop movement updates. You can continue drawing a pause screen, but do not keep accumulating time while paused. On resume, reset the previous-frame timestamp or clear accumulated time so the game does not treat the pause as a massive delay.

Common timing problems

  • Using a fixed amount per frame: A line such as x += 1 in the drawing callback ties movement to callback frequency. Move by grid steps on a timer instead.
  • Mixing seconds and milliseconds: Animation timestamps and intervals in this pattern use milliseconds. A value of 0.12 is 0.12 milliseconds, not 120 milliseconds.
  • Dropping elapsed time unintentionally: Resetting the accumulator to zero after a delayed frame loses the remainder and can slow movement. Subtract the interval after each step instead.
  • Updating several times but drawing only once: This is normal catch-up behavior. The simulation catches up, then the next image displays its latest state.
  • Using timers as the rendering loop: General-purpose timers can run late or be throttled, particularly in background tabs. A rendering callback plus elapsed-time checks keeps drawing and game timing conceptually separate.

Timing does not guarantee identical gameplay under every condition: a suspended tab cannot display updates, and a long pause may be handled by the chosen catch-up policy. The goal is stable movement during normal play, not forcing the display to refresh at a particular rate.

Frequently Asked Questions

How do I make Snake move at the same speed on 60 Hz and 144 Hz screens?
Advance the snake according to elapsed time and a fixed movement interval, not once per rendered frame. The display can draw more or fewer images without changing the number of grid steps per second.

Should I use setInterval or requestAnimationFrame?
For a browser game that draws animation, requestAnimationFrame is a suitable rendering loop. Use its timestamps to measure elapsed time and schedule fixed game updates. A timer can also schedule logic, but it should not be assumed to fire with perfect precision.

Why does my snake speed up on a high-refresh-rate monitor?
Your update is likely running once per frame, or using a frame count as a clock. Replace that dependency with elapsed-time accumulation and move only when the configured interval has passed.

Why does the snake jump after I switch tabs?
Browsers may pause or throttle background-page callbacks. On return, the elapsed time can be large. Cap catch-up time, pause the game while hidden, or reset the timing baseline when play resumes.

How can I make the snake faster as the player scores?
Reduce the movement interval at defined score milestones, while enforcing a minimum interval. Keep the change in the game update logic rather than changing how often the screen is drawn.

Related

Method: the standard formula for this calculation, rounded as each result states. See how the calculators work or report an error.