game-loop

Fixed Timestep Game Loop

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "game-loop" with this command: npx skills add dadbodgeoff/drift/dadbodgeoff-drift-game-loop

Fixed Timestep Game Loop

Frame-rate independent game loop with physics interpolation and time manipulation.

When to Use This Skill

  • Building browser-based games or interactive simulations

  • Need consistent physics regardless of monitor refresh rate

  • Want smooth rendering with deterministic game logic

  • Implementing hitstop, slow-mo, or time manipulation effects

Core Concepts

The key insight is separating physics (fixed timestep) from rendering (variable). An accumulator tracks time debt, running physics at a consistent rate while interpolating between states for smooth visuals.

Frame → Accumulator += delta → While(accumulator >= fixedStep) { physics() } → Render(interpolation)

Implementation

TypeScript

interface GameLoopStats { fps: number; frameTime: number; physicsTime: number; renderTime: number; lagSpikes: number; interpolation: number; timeScale: number; isInHitstop: boolean; }

interface GameLoopCallbacks { onFixedUpdate: (fixedDelta: number, now: number) => void; onRenderUpdate: (delta: number, interpolation: number, now: number) => void; onLagSpike?: (missedFrames: number) => void; }

class GameLoop { private fixedTimestep: number; private readonly MAX_FRAME_TIME = 0.25;

private accumulator = 0; private lastTime = 0; private interpolation = 0;

private frameCount = 0; private fpsTimer = 0; private currentFps = 60; private lagSpikes = 0;

private running = false; private animationId: number | null = null; private callbacks: GameLoopCallbacks;

private hitstopTimer = 0; private hitstopIntensity = 0; private externalTimeScale = 1.0;

constructor(callbacks: GameLoopCallbacks, fixedTimestep = 1 / 60) { this.callbacks = callbacks; this.fixedTimestep = fixedTimestep; }

start(): void { if (this.running) return; this.running = true; this.lastTime = performance.now() / 1000; this.accumulator = 0; this.loop(); }

stop(): void { this.running = false; if (this.animationId !== null) { cancelAnimationFrame(this.animationId); this.animationId = null; } }

triggerHitstop(frames = 3, intensity = 0.1): void { this.hitstopTimer = frames * this.fixedTimestep; this.hitstopIntensity = intensity; }

setTimeScale(scale: number): void { this.externalTimeScale = Math.max(0, scale); }

getStats(): GameLoopStats { return { fps: this.currentFps, frameTime: 0, physicsTime: 0, renderTime: 0, lagSpikes: this.lagSpikes, interpolation: this.interpolation, timeScale: this.getEffectiveTimeScale(), isInHitstop: this.hitstopTimer > 0, }; }

private loop = (): void => { if (!this.running) return;

const now = performance.now() / 1000;
let frameTime = now - this.lastTime;
this.lastTime = now;

// Cap frame time to prevent spiral of death
if (frameTime > this.MAX_FRAME_TIME) {
  const missedFrames = Math.floor(frameTime / this.fixedTimestep);
  this.lagSpikes++;
  this.callbacks.onLagSpike?.(missedFrames);
  frameTime = this.MAX_FRAME_TIME;
}

frameTime *= this.getEffectiveTimeScale();

if (this.hitstopTimer > 0) {
  this.hitstopTimer -= frameTime / this.getEffectiveTimeScale();
}

this.accumulator += frameTime;

// Fixed timestep physics
while (this.accumulator >= this.fixedTimestep) {
  this.callbacks.onFixedUpdate(this.fixedTimestep, now);
  this.accumulator -= this.fixedTimestep;
}

// Interpolation for smooth rendering
this.interpolation = this.accumulator / this.fixedTimestep;
this.callbacks.onRenderUpdate(frameTime, this.interpolation, now);

// FPS calculation
this.frameCount++;
this.fpsTimer += frameTime / this.getEffectiveTimeScale();
if (this.fpsTimer >= 1.0) {
  this.currentFps = Math.round(this.frameCount / this.fpsTimer);
  this.frameCount = 0;
  this.fpsTimer = 0;
}

this.animationId = requestAnimationFrame(this.loop);

};

private getEffectiveTimeScale(): number { return this.hitstopTimer > 0 ? this.hitstopIntensity : this.externalTimeScale; } }

// Interpolation helpers function lerp(a: number, b: number, t: number): number { return a + (b - a) * t; }

function lerpAngle(a: number, b: number, t: number): number { let diff = b - a; while (diff > Math.PI) diff -= Math.PI * 2; while (diff < -Math.PI) diff += Math.PI * 2; return a + diff * t; }

Usage Examples

// Game state let playerX = 0, playerY = 0; let playerVelX = 0, playerVelY = 0; let prevPlayerX = 0, prevPlayerY = 0;

const gameLoop = new GameLoop({ onFixedUpdate: (fixedDelta) => { // Store previous for interpolation prevPlayerX = playerX; prevPlayerY = playerY;

// Deterministic physics
playerVelY += 980 * fixedDelta; // Gravity
playerX += playerVelX * fixedDelta;
playerY += playerVelY * fixedDelta;

// Collision
if (playerY > 500) {
  playerY = 500;
  playerVelY = 0;
}

},

onRenderUpdate: (delta, interpolation) => { // Smooth rendering between physics states const renderX = lerp(prevPlayerX, playerX, interpolation); const renderY = lerp(prevPlayerY, playerY, interpolation);

ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillRect(renderX - 10, renderY - 10, 20, 20);

},

onLagSpike: (missed) => console.warn(Lag: missed ${missed} frames), });

gameLoop.start();

// Hitstop on collision function onPlayerHit() { gameLoop.triggerHitstop(4, 0.05); // 4 frames at 5% speed }

// Slow-mo death function onPlayerDeath() { gameLoop.setTimeScale(0.3); setTimeout(() => gameLoop.setTimeScale(1.0), 2000); }

Best Practices

  • Always store previous state before physics update for interpolation

  • Cap frame time to prevent spiral of death (0.25s is reasonable)

  • Use fixed timestep for all game logic, variable only for rendering

  • Tune hitstop values for game feel (2-5 frames typical)

  • Consider 30Hz physics for mobile to save CPU

Common Mistakes

  • Running physics in render callback (frame-rate dependent)

  • Not interpolating positions (causes stuttering)

  • Forgetting to cap frame time (causes spiral of death on tab switch)

  • Using delta time for physics (non-deterministic)

Related Patterns

  • server-tick (server-side equivalent)

  • websocket-management (multiplayer sync)

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

oauth-social-login

No summary provided by upstream source.

Repository SourceNeeds Review
General

sse-streaming

No summary provided by upstream source.

Repository SourceNeeds Review
General

multi-tenancy

No summary provided by upstream source.

Repository SourceNeeds Review
General

deduplication

No summary provided by upstream source.

Repository SourceNeeds Review