Game Loop Architecture skill for Codex

Fixed ticks, smooth frames, no spiral of death. Codex separates simulation from rendering with a fixed-timestep accumulator, interpolates visuals between ticks, clamps long frames so hitches cannot snowball, and latches input so no press is lost or doubled. Game flow becomes a push-down state stack with queued transitions, and it proves frame-rate independence by running the same scenario at 30, 60 and 144 Hz.

  • Game Development
  • $game-loop-architecture
  • 182 lines
  • 2 files
  • ~1,950 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- game-loop-architecture

Installs to ~/.agents/skills/game-loop-architecture/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $game-loop-architecture, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Designs and fixes the core loop of a game - fixed-timestep simulation with an accumulator, render interpolation, spiral-of-death clamps, pause and time scale, input sampling per tick, finite state machines and pushdown scene stacks for game flow, and update ordering between systems. Use when movement speed depends on frame rate, physics explodes after a hitch, a custom engine or framework needs a main loop, game states (menu, play, pause, game over) are tangled in if-statements, or replays and netcode need deterministic ticks.

Use it when

  • Gameplay speed or physics changes with frame rate
  • Writing the main loop for a browser, Pygame, SDL or custom engine game
  • Game states and screens are tangled in flags and nested ifs
  • Preparing a simulation for replays or deterministic networking

Use something else when

How to check the result

The same scenario takes the same ticks and wall time at 30, 60 and 144 Hz, a forced 2-second stall recovers cleanly, and paused time advances no ticks.

The mistake it prevents

Using lerp(a, b, 0.1) per frame for smoothing, which silently makes camera and motion behave differently on every refresh rate.

~/.agents/skills/game-loop-architecture/SKILL.md

--- name: game-loop-architecture description: Designs and fixes the core loop of a game - fixed-timestep simulation with an accumulator, render interpolation, spiral-of-death clamps, pause and… license: MIT ---

Done well means: simulation advances in fixed ticks independent of display rate, rendering interpolates between the last two simulation states, a long hitch cannot snowball into a freeze, input is sampled into each tick exactly once, and game flow is an explicit state machine where every transition has one place it happens.

First, find out what owns the loop

EnvironmentLoop ownerYour job
UnityEngine: Update (variable) + FixedUpdate (fixed, 0.02 s default)Put simulation in fixed callbacks; interpolate visuals (Rigidbody interpolation)
GodotEngine: _process + _physics_process (60 Hz default)Same split; enable physics interpolation in project settings (2D in 4.3+, 3D in 4.4+)
UnrealEngine: variable Tick; physics substepping optionalUse DeltaSeconds; enable substepping for physics stability
BevyUpdate + FixedUpdate schedulesPut simulation in FixedUpdate; read Time<Fixed> overstep for interpolation
Browser (Canvas, Pixi, custom)You, via requestAnimationFrameWrite the accumulator loop below
Pygame, SDL, raylib, custom C++YouSame loop; add sleep or VSync to avoid spinning a core

Read AGENTS.md and the existing main loop before changing anything. In engines, do not write a second loop; fix where code runs.

The fixed-timestep loop

const TICK_HZ = 60;
const DT = 1 / TICK_HZ;           // seconds of simulation per tick
const MAX_FRAME = 0.25;           // clamp: never simulate more than 250 ms after a hitch

let prev = performance.now() / 1000;
let acc = 0;
let previous = cloneState(state);  // for interpolation

function frame(nowMs: number) {
  const now = nowMs / 1000;
  let frameTime = now - prev;
  prev = now;
  if (frameTime > MAX_FRAME) frameTime = MAX_FRAME;   // spiral-of-death guard
  acc += frameTime * timeScale;                        // timeScale 0 = paused, 0.5 = slow-mo

  while (acc >= DT) {
    const input = inputSampler.sample();               // one snapshot per tick
    copyState(previous, state);
    simulate(state, input, DT);                        // deterministic given state+input
    tickCount++;
    acc -= DT;
  }

  const alpha = acc / DT;                              // 0..1 between previous and current
  render(previous, state, alpha);
  requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
  • Simulation never reads wall-clock time; it only knows DT and tickCount.
  • render lerps positions (and slerps rotations) between previous and state by alpha. Without it, a 60 Hz sim on a 144 Hz display stutters.
  • Choose TICK_HZ by need: 30 for turn-like or slow games, 60 for action, 120+ only for physics that needs it. Higher tick rates cost CPU linearly.
  • Browsers throttle requestAnimationFrame in background tabs; the clamp keeps the game from trying to catch up minutes of time on return. Pause on visibilitychange instead.

Variable vs fixed: decision rules

CodeWhere
Movement, physics, AI decisions, cooldowns, anything affecting gameplay outcomeFixed tick
Camera smoothing, particles, UI tweens, animation blendingPer frame with real dt
Input pollingPer frame into a buffer; consumed per tick
Networking send/receiveFixed tick (send rate may be lower, e.g. every 2nd tick)

Input sampling without lost presses

A press and release can both happen between two ticks, or several ticks can run in one frame. Latch edges:

class InputSampler {
  private held = new Set<string>();
  private pressedSinceTick = new Set<string>();
  onKeyDown(k: string) { if (!this.held.has(k)) this.pressedSinceTick.add(k); this.held.add(k); }
  onKeyUp(k: string) { this.held.delete(k); }
  sample() {
    const snap = { held: new Set(this.held), pressed: new Set(this.pressedSinceTick) };
    this.pressedSinceTick.clear();          // consumed by exactly one tick
    return snap;
  }
}

Add a jump buffer (accept jump pressed up to ~6 ticks before landing) and coyote time (accept jump up to ~6 ticks after leaving a ledge) in simulation, counted in ticks.

Game flow as a state machine

Replace if (gameState === "playing" && !paused && !inCutscene) chains with explicit states:

interface GameState {
  enter(prev?: GameState): void;
  exit(): void;
  tick(input: InputSnapshot, dt: number): void;
  render(alpha: number): void;
  /** true = states below also render (pause menu over gameplay) */
  readonly transparent?: boolean;
}

class StateStack {
  private stack: GameState[] = [];
  push(s: GameState) { this.top()?.exit(); this.stack.push(s); s.enter(); }
  pop() { this.stack.pop()?.exit(); this.top()?.enter(); }
  replace(s: GameState) { this.stack.pop()?.exit(); this.stack.push(s); s.enter(); }
  top() { return this.stack.at(-1); }
  tick(i: InputSnapshot, dt: number) { this.top()?.tick(i, dt); }   // only the top simulates
  render(alpha: number) {
    let from = this.stack.length - 1;
    while (from > 0 && this.stack[from].transparent) from--;
    for (let i = from; i < this.stack.length; i++) this.stack[i].render(alpha);
  }
}
  • Menu -> replace(Play), Play -> push(Pause), Pause -> pop(), Play -> replace(GameOver).
  • Transitions requested during tick should be queued and applied after the tick, so a state never exits halfway through its own update.
  • For entity behaviour (enemy patrol/chase/attack) use the same pattern per entity, or a behaviour tree when states multiply (see game-ai).

Update order within a tick

Write the order down and make it explicit in code, not implied by registration order:

1. Apply input / AI intents
2. Movement and physics integration
3. Collision detection and response
4. Gameplay reactions (damage, pickups, triggers) from collision events
5. Spawn and despawn queued entities
6. Advance timers and cooldowns

Spawning or destroying during iteration is the classic bug: queue structural changes and apply them at step 5.

Gotchas

  • Multiplying by dt inside a fixed tick is still correct (and keeps tuning in per-second units); forgetting it in variable-rate code is the frame-rate-dependence bug.
  • Exponential smoothing with lerp(a, b, 0.1) per frame is frame-rate dependent. Use lerp(a, b, 1 - Math.exp(-rate * dt)).
  • setInterval loops drift and ignore VSync; use requestAnimationFrame in browsers.
  • Accumulating float time (time += dt) for hours loses precision; count integer ticks and derive time as tick * DT.
  • Pausing by skipping simulate but still advancing timers in render code desyncs cooldowns.
  • Determinism (replays, lockstep) also needs a seeded RNG owned by the simulation and no iteration over unordered hash maps; see multiplayer-netcode.

Verification

  • Run at 30, 60 and 144 Hz display rates (or throttle with an artificial frame delay): a character crossing the screen takes the same number of ticks and the same wall time.
  • Inject a 2-second stall (while (performance.now() < t + 2000) {} in a debug key): the game resumes without a burst of hundreds of ticks or physics explosions.
  • Record inputs for 60 seconds and replay them: final state hash matches if the game claims determinism.
  • Push Pause, pop it, and check no ticks ran while paused (tick counter unchanged).

Hand-off summary

**Loop:** 60 Hz fixed tick, 250 ms clamp, render interpolation (alpha) for transforms
**Input:** edge-latched sampler, jump buffer 6 ticks, coyote 6 ticks
**Flow:** StateStack with Menu, Play, Pause (transparent), GameOver; transitions queued
**Order:** intents -> integrate -> collide -> react -> spawn/despawn -> timers
**Checked:** 30/60/144 Hz parity, 2 s stall recovery, 60 s replay hash match

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "Game Loop Architecture"
  short_description: "Fixed ticks, smooth frames, no spiral of death"
  default_prompt: "$game-loop-architecture My character moves faster on my 144 Hz monitor, fix the game loop"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc
New

Introducing Codex Skills Pro

79 professional skills that take Codex through complete engagements, each with its own scripts, references and a report that verifies the result.

All-AccessOne-time payment, no subscription
$49
Get All-AccessOr browse individual skills from $2.99 →

Secure checkout by Creem