game-loop

Server-side game loop implementation with fixed timestep, physics simulation, and tick rate optimization

game
loop

SKILL.md

GitHub

Server Game Loop

Implement deterministic game loops with fixed timestep for consistent gameplay.

Fixed Timestep Loop

class GameLoop {
  constructor(tickRate = 60) {
    this.tickRate = tickRate;
    this.tickMs = 1000 / tickRate;
    this.tick = 0;
    this.running = false;
  }

  start() {
    this.running = true;
    this.lastTime = process.hrtime.bigint();
    this.accumulator = 0n;
    this.loop();
  }

  loop() {
    if (!this.running) return;

    const now = process.hrtime.bigint();
    const deltaNs = now - this.lastTime;
    this.lastTime = now;
    this.accumulator += deltaNs;

    const tickNs = BigInt(Math.round(this.tickMs * 1_000_000));
    let ticksProcessed = 0;

    while (this.accumulator >= tickNs && ticksProcessed < 5) {
      this.update(this.tickMs);
      this.tick++;
      this.accumulator -= tickNs;
      ticksProcessed++;
    }

    // Prevent spiral of death
    if (ticksProcessed === 5) {
      this.accumulator = 0n;
      console.warn('Tick catchup limit reached');
    }

    const sleepMs = Math.max(1, this.tickMs - Number(this.accumulator) / 1_000_000);
    setTimeout(() => this.loop(), sleepMs);
  }

  update(dt) {
    this.processInputs();
    this.updatePhysics(dt);
    this.updateEntities(dt);
    this.checkGameState();
    this.broadcastState();
  }
}

Tick Rate Guide

Game Type Rate Budget Rationale
FPS 60-128 Hz 8-16ms Hit precision
MOBA 30-60 Hz 16-33ms Balance
BR 20-30 Hz 33-50ms Scale
MMO 10-20 Hz 50-100ms Massive

Performance Monitoring

class TickMetrics {
  constructor(size = 100) {
    this.samples = [];
    this.size = size;
  }

  record(ms) {
    this.samples.push(ms);
    if (this.samples.length > this.size) this.samples.shift();
  }

  stats() {
    const sorted = [...this.samples].sort((a, b) => a - b);
    return {
      avg: this.samples.reduce((a, b) => a + b, 0) / this.samples.length,
      p50: sorted[Math.floor(sorted.length * 0.5)],
      p99: sorted[Math.floor(sorted.length * 0.99)]
    };
  }
}

Troubleshooting

Common Failure Modes

Error Root Cause Solution
Tick spikes GC pauses Object pooling
Drift Float precision Fixed-point math
Explosion Large dt Cap dt value
Stuttering Variable tick Fixed timestep

Debug Checklist

const stats = metrics.stats();
console.log(`Avg: ${stats.avg.toFixed(2)}ms`);
console.log(`P99: ${stats.p99.toFixed(2)}ms`);
console.log(`Budget: ${(stats.avg / tickBudget * 100).toFixed(1)}%`);

Unit Test Template

describe('GameLoop', () => {
  test('maintains tick rate', async () => {
    const loop = new GameLoop(60);
    const ticks = [];

    loop.update = () => ticks.push(Date.now());
    loop.start();

    await new Promise(r => setTimeout(r, 100));
    loop.running = false;

    expect(ticks.length).toBeGreaterThan(4);
  });
});

Resources

  • assets/ - Loop templates
  • references/ - Optimization guides