Skip to content
Back to skills

Realtime Game Loop

ASecurity

Runs real-time play: a fixed-step UI-thread loop, typed-array sims, (tick, command) replays, replayable physics. Use when adding live motion, bouncing balls, a physics shot or a replay test. Not for turn animations (skia-canvas-rendering).

  • 3 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 18, 2026
developmentrustgoreactnodetestingapiperformance

Works with

  • cli
  • api

Security analysis

A100/100

Pro scans all 19 files and shows the line behind each finding

Scanned October 18, 2026

npx -y skills add zakariaf/ReactNative-Skills --skill realtime-game-loop --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Realtime Game Loop?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Realtime Game Loop
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/zakariaf-realtime-game-loop/badge)](https://www.skillsdirectory.com/skills/zakariaf-realtime-game-loop)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: realtime-game-loop
description: "Runs real-time play: a fixed-step UI-thread loop, typed-array sims, (tick, command) replays, replayable physics. Use when adding live motion, bouncing balls, a physics shot or a replay test. Not for turn animations (skia-canvas-rendering)."
---

# Real-time game loop

Real-time play advances in fixed ticks of 1/120 s, never in frame time. A real-time game runs a
typed-array sim on the UI thread inside a Reanimated frame callback; a turn-based game with a continuous
phase (a ricochet shot, a cascade) runs that phase inside its pure reducer and replays it as a timeline.
Either way a recorded run replays bit-exactly in Jest, on iOS and on Android. This skill ships the
loop kit (`src/core/realtime/`, `src/core/geom/`), the real-time host (`src/canvas/realtime/`), a
template game (a bouncing-orbs arena) and a checker that proves them.

## Rules that must hold

1. **Simulate in fixed steps of `STEP_MS = 1000 / 120` with a 250 ms catch-up clamp; the first frame
   after (re)activation simulates nothing.** Why: results depend on ticks, never on 60, 90 or 120 Hz or
   frame timing, and a resumed sim never jumps.
2. **Gameplay counts ticks, never milliseconds:** speeds are per tick, timers are tick counts, the sim
   never sees a timestamp. Why: tick counts are exact integers on every device.
3. **Sim state is one plain object of typed arrays in ONE shared value, mutated in place on the UI
   thread, then `sim.modify()`.** Why: no per-frame copies or allocations, and in-place mutation does not
   notify the picture by itself.
4. **A tick allocates nothing:** buffers (entities, events, RNG words, spatial-hash scratch) are created
   once; a full event queue drops edge and hit events but keeps reserved slots so input, save-point and
   run-end events never drop; `step…` and every helper it reaches do scalar maths on the typed arrays
   (`vec2`/`sweep` return new objects and belong to the JS thread). One documented exception: an
   occasional sfc32 draw through a `nextU32` wrapper; per-entity, per-tick randomness uses `hashU32`.
   Why: the loop may run 30 ticks in one frame on the UI thread, garbage collection there is a visible
   hitch, and a lost input breaks the replay.
5. **Sims, the UI-thread kit and real-time draw files are `'worklet'` modules that import values only
   from other `'worklet'` modules.** Why: a plain function called on the UI thread throws at runtime,
   not at build time.
6. **Sims, the kit and simulate-then-replay rules obey the determinism policy:** only `+ - * / %`,
   bitwise, `Math.sqrt/imul/floor/round/abs/min/max/PI`; no `**`, other `Math.*`, `Math.random`, `Date`,
   `performance.now`, `Intl`; randomness from the sim's own sfc32 words. Why: Hermes computes
   transcendental functions with each platform's libm and Jest uses V8, so replays would diverge
   between an iPhone, an Android phone and CI.
7. **Input is an integer command recorded as `(tick, command)`,** from the committed 16-direction table
   (1 right, 5 down, 9 left, 13 up; 0 idle), identical to the stick gesture's table. Why: only integer
   logs replay bit-exactly.
8. **The sim never talks to JS or the app:** it appends `[kind, value, tick]` events; `runLoopFrame`
   drains them once per frame and sends one batch with `scheduleOnRN`; a sim imports nothing from React,
   React Native, Expo, Skia or the app's layers. Why: one crossing per frame, and the sim stays
   testable in plain Jest and reusable by headless bots.
9. **The frame-callback body is one call to `runLoopFrame`, which holds the `try/catch`;** errors stop
   the loop and pause the game. Why: an uncaught UI-thread exception kills the app, and React Compiler
   bails out of hooks with value blocks inside `try/catch`.
10. **The loop runs only while the surface may run** (app active, screen focused, not covered, not
    paused, not ended) through `useCanvasLifecycle`; stopping takes a save point and shows the pause UI;
    real-time games never auto-resume, and resume resets the accumulator. Why: no simulation in the
    background or under an interstitial, and a killed app loses at most the current wave.
11. **Every sim has a replay test and a frame-grouping test.** Why: they are the proof of determinism;
    saves, bots and bug reports all depend on it.
12. **A real-time game exposes a `RealtimeSpec` (createSim, step, drainEvents, snapshot, savePoints,
    isSavePoint) with a tested snapshot round trip, and is saved only at save points.** Why: the host and
    the save path need one contract, a save must restore exactly, and a frame is too short for a write.
13. **Simulate-then-replay games run the continuous phase inside the pure reducer with the same fixed
    step, capped by `maxTicks`, and return timed events (`atMs`).** Why: the game stays turn-based for
    saves, undo, bots and goldens; only the picture moves in real time, and a volley always ends.
14. **A real-time surface is proven like every canvas:** a draw-call budget test with every entity slot
    in play (≤ 1,000), bodies inside the canvas at 3 sizes, and pixel goldens through `paintSimPng` at
    3 sizes × moments 0, 0.5, 1 of a scripted run (9 baselines). Why: the loop's tests prove the numbers,
    not the picture.
15. **No physics engine:** swept circles, `reflect` and the spatial hash; `planck` on the JS thread is the
    documented fallback only for stacking, joints or friction. Why: engines bring non-deterministic
    floats and a second runtime.
16. **Every import of the kit, host, sims and draw files resolves; prerequisites come from their owner
    skills.** Why: a half-copied kit fails at runtime on one screen; the checker names who creates the
    missing file.

## Workflow

1. **Classify the game.** Real-time (an arena chase): read [references/fixed-step-loop.md](references/fixed-step-loop.md)
   and [references/sim-design.md](references/sim-design.md). A continuous phase inside a turn (a shot, a
   drop, a cascade): read [references/simulate-then-replay.md](references/simulate-then-replay.md).
   Geometry: [references/geom-kit.md](references/geom-kit.md). Turn-based only: this skill does not apply
   (game-rules-engine, skia-canvas-rendering).
2. **Check prerequisites.** The templates import `@/core/rng/sfc32` (seeded-determinism-and-golden-vectors;
   a synced copy ships in `templates/src/core/rng/`), the canvas kit's `draw-kit`, `describe-error`,
   `use-canvas-lifecycle` and `test/goldens/skia-golden.ts` (skia-canvas-rendering, golden-and-snapshot-tests;
   canonical `canvas-types`, `canvas-layout` and `timeline/` copies ship in `templates/src/canvas/`),
   `@/hooks/use-is-app-active` (react-components-and-hooks), and fast-check pinned exactly.
3. **Copy the kit.** Run `node ${CLAUDE_SKILL_DIR}/scripts/check-realtime-loop.mjs .` from the repo root.
   For every `loop-file-missing` copy the file verbatim from `templates/` to the path its first line names
   (`templates/src/core/**`, `templates/src/canvas/realtime/**`, tests included). Fix every
   `loop-import-unresolved` with the skill it names.
4. **Real-time game, test-first:** copy `templates/src/games/__GAME_ID__/` to `src/games/<game-id>/`,
   replacing `__GAME_ID__` (kebab), `__GAME_PASCAL__` and `__GAME_CONST__` (UPPER_SNAKE) in names and
   contents. Run the replay, frame-grouping and round-trip tests, then replace the gameplay slot
   (`moveEntity`) with the game's rules, keeping every test green after each change. Add event kinds as
   exported integer constants. Imitate [examples/arena-chase/](examples/arena-chase/).
5. **Its surface and proof:** adapt `canvas/` (palettes with `canvas-contrast.json`, `draw-arena.ts`, the
   budget test; skia-canvas-rendering's palette and contrast rules apply) and copy
   `templates/test/goldens/canvas/__GAME_ID__-arena.golden.test.ts`. Create the baselines deliberately:
   `npx jest test/goldens/canvas/<game-id>-arena.golden.test.ts --selectProjects golden -u`, open all 9
   PNGs (bodies inside the canvas, nothing clipped at `small`), then `CI=1 npx jest --ci --selectProjects golden`;
   commit the baselines with a `Gate-Change:` trailer (golden-and-snapshot-tests).
6. **Wire the board into the host** (game-session-host owns the Game screen, the run store and the
   writes): the game's `Surface` receives `GameSurfaceProps` and renders `RealtimeCanvasHost` with
   `initialSim={realtime.createSim(props.state)}`, `game={realtime}`, `draw`, `colors` and `kit`
   (makeCanvasColors, makeDrawKit), `makeGesture={makeStickGesture}`, `onEvents` (sounds, HUD,
   statistics, input log), `isSavePoint={realtime.isSavePoint}`, `onSavePoint={(copy) =>
   props.onSavePoint(realtime.snapshot(copy), inputLog)}` (every save point and one final one at the
   run's end), `isEnded={props.isEnded}`, `isPaused={!props.isInputEnabled && !props.isEnded}`,
   `onAutoPause` (a no-op inside a hosted Surface: the Game screen already pauses on background, blur
   and full-screen ads), `isFocused` (`useIsFocused()` from expo-router; React Navigation 7:
   `@react-navigation/native`), `isInterrupted`, `onFailure`, a translated label, and in measurement
   builds `onFrameTime` (performance-budgets' `sampleFrame` worklet). Keep `game` a module
   constant; the host keeps the JS callbacks behind stable trampolines. Join the spec to
   the module: `GameModule<…> & { realtime }` with `savePolicy: { kind: 'save-points', points }`.
7. **Simulate-then-replay game:** imitate [examples/ricochet-volley/](examples/ricochet-volley/): a pure
   `simulate…` function in `src/games/<game-id>/rules/` called by `applyMove`, events with
   `atMs: tickToMs(tick)`, a `maxTicks` cap, a tracks builder in the surface folder, and the property tests.
8. **Lint, format, type-check and test:** `npx eslint --fix src/games/<game-id> src/core/realtime src/core/geom src/canvas/realtime`,
   `npx prettier --write` on them, `npm run typecheck`, then
   `npx jest src/games/<game-id> src/core/realtime src/core/geom src/canvas/realtime --ci --selectProjects unit`
   (paths first: `--selectProjects` takes several values and would swallow them).
9. **Run the check** `node ${CLAUDE_SKILL_DIR}/scripts/check-realtime-loop.mjs .`; fix every `FAIL` line
   (file, rule, fix) and rerun until `RESULT: PASS`. Options, config keys and every rule:
   [references/checker.md](references/checker.md).
10. **Play it for real.** The host is unit-tested with Skia inert and the loop mocked; it has not run on
    an Android device yet. On the first real-time game, run a Release build on a named iOS simulator
    (ios-simulator-build) and a named Android emulator (android-emulator-build): the picture updates
    every frame, pause, background and Android back stop the loop, resume starts clean. Then a human
    plays at least 60 s on a 120 Hz phone of each platform with the frame-time recorder passed as the
    host's `onFrameTime` (hitch rate ≤ 10 ms/s, performance-budgets) and judges the feel.

## Definition of done

- [ ] The kit exists with its tests: `src/core/realtime/{fixed-step,replay-commands,realtime-spec}.ts`,
      `src/core/geom/{vec2,sweep,spatial-hash}.ts`; for a real-time game also `src/canvas/realtime/`
      (`run-loop-frame`, `use-fixed-step-loop`, `record-sim`, `paint-sim-png`, `realtime-canvas-host`).
- [ ] Each sim is a `'worklet'` module of typed arrays that allocates nothing per tick, counts ticks,
      records `(tick, command)`, drains `[kind, value, tick]` events, and its replay and frame-grouping
      tests pass; its `RealtimeSpec` round-trip test passes.
- [ ] Each real-time surface has light and dark palettes with `canvas-contrast.json`, a `'worklet'`
      draw, a draw-call budget test (≤ 1,000) and 9 reviewed golden PNGs that pass under
      `CI=1 npx jest --ci --selectProjects golden`.
- [ ] Each simulate-then-replay phase is pure, capped by `maxTicks`, emits `atMs` events, and its
      properties (inside the arena, never faster, deterministic, tracks replay the path) pass.
- [ ] The host stops the loop, saves and shows the pause UI on pause, background, blur and
      interruptions, and never auto-resumes.
- [ ] `npm run typecheck`, `npx eslint --max-warnings 0` and `npx prettier --check` pass on the new
      files; no `__GAME_…__` placeholder is left.
- [ ] `node ${CLAUDE_SKILL_DIR}/scripts/check-realtime-loop.mjs .` prints `RESULT: PASS`

The checker prints no SKIP lines. It prints `NOT APPLICABLE: no real-time code: …` (a pass) in an app
whose games are all turn-based, and stops with exit 2 in a folder without `package.json`.

## Anti-patterns

- **Moving entities by `dt` in milliseconds** (`x += speed * dt`). Frame timing leaks into gameplay and
  replays diverge; move per tick inside `step`.
- **Keeping the world in React state or copying it to JS every frame.** Keep it in the shared value; JS
  gets batched events and a copy only at save points.
- **Angles and `Math.atan2`/`Math.cos` in the sim.** Use committed unit-vector tables, dot products and
  `reflect`.
- **Calling `scheduleOnRN` from inside the sim, or once per event.** Queue events in the sim; the runner
  sends one batch per frame.
- **Growing arrays per tick** (`events.push` on a JS array, `new Float32Array`, `.map`). Pre-allocate
  and write in place.
- **A module-level `let` or array in a sim file** (a counter, a hit log). The UI thread works on its own
  captured copy, and saves and replays never see it; keep every changing value in the typed arrays.
- **Passing a stride-4 `body` array to the spatial hash** (it expects stride 2). Wrong cells, no error.
- **Auto-resuming a real-time game** when the app returns to the foreground or an interstitial closes.
  The player expects the pause UI; resume only on their tap.
- **Running a volley live in the frame loop instead of inside `applyMove`.** It breaks saves, undo, bots
  and goldens; simulate first, then replay the events.
- **A second frame callback for measurements.** The frame-time recorder goes in through the host's
  `onFrameTime` and is sampled as the first statement of `runLoopFrame`'s `try`; an extra callback keeps
  idle screens rendering.
- **Saving every frame or every tick.** Save at wave ends, pause, background and blur only.
- **Trusting the simulator or emulator for speed.** The iOS simulator caps at 60 fps and emulators
  usually render at 60 Hz; judge feel and hitch rate on real 120 Hz phones.

## Files in this skill

| File | What it is | Read/run when |
|---|---|---|
| [references/fixed-step-loop.md](references/fixed-step-loop.md) | The accumulator, runner and hook, the real-time host, lifecycle and save points on iOS and Android, sprites, refresh rates, what is verified, multi-package | Workflow step 1 for real-time games, and before touching the loop |
| [references/sim-design.md](references/sim-design.md) | Typed-array state, one tick, events, sfc32 words, the determinism policy, integer commands, replay tests, save points, the RealtimeSpec | Steps 1 and 4 |
| [references/simulate-then-replay.md](references/simulate-then-replay.md) | Continuous phases inside `applyMove`: fixed step, timed events, tracks, the collision recipe, discrete runs, tests | Steps 1 and 7 |
| [references/geom-kit.md](references/geom-kit.md) | `vec2`, `sweep`, `spatial-hash` APIs and rules for new geometry | Whenever a game needs vectors or collisions |
| [references/checker.md](references/checker.md) | The checker's options, config keys, discovery, every rule with its fix, NOT APPLICABLE, troubleshooting | Step 9, when a line is unclear or folders differ |
| `templates/src/core/realtime/` | `fixed-step.ts`, `replay-commands.ts` (each + test), `realtime-spec.ts` | Step 3 (copy verbatim) |
| `templates/src/core/geom/` | `vec2.ts`, `sweep.ts`, `spatial-hash.ts`, each with its test | Step 3 (copy verbatim) |
| `templates/src/canvas/realtime/` | `run-loop-frame.ts` (+ test), `use-fixed-step-loop.ts`, `record-sim.ts` (+ test), `paint-sim-png.ts`, `realtime-canvas-host.tsx` (+ lifecycle test) | Step 3 (copy verbatim) for a real-time game |
| `templates/src/canvas/` (other files) | `canvas-layout.ts` (+ test), `canvas-types.ts`, `timeline/track.ts`, `timeline/sample.ts` (+ test): synced canonical copies (skia-canvas-rendering owns them); keep the app's copy | Step 2, only when the app lacks them |
| `templates/src/core/rng/` | `sfc32.ts` and its golden test: synced canonical copy (seeded-determinism-and-golden-vectors owns it) | Step 2, only when the app lacks it |
| `templates/src/games/__GAME_ID__/` | The template game: `sim/` (sim + replay tests), `<id>-realtime.ts` (RealtimeSpec + round-trip test), `canvas/` (palettes, contrast pairs, draw, budget test) | Steps 4 and 5 |
| `templates/test/goldens/canvas/` | `__GAME_ID__-arena.golden.test.ts` and its nine reviewed baselines in `__image_snapshots__/` | Step 5 |
| [examples/arena-chase/](examples/arena-chase/) | A chase sim (player, chasers, hits) with its replay tests: the shape of real gameplay in a sim | Step 4 |
| [examples/ricochet-volley/](examples/ricochet-volley/) | Simulate-then-replay volley with sweep and reflect, its property tests, and the tracks builder with its test (one piece of the game's surface; skia-canvas-rendering builds the rest) | Step 7 |
| `scripts/check-realtime-loop.mjs` | The checker (21 rules; references/checker.md) | Steps 3 and 9 and at the end |
| `scripts/selftest.mjs` | Proves the checker passes the templates and examples and catches each planted bug | After changing the checker, a template or an example |
| `scripts/lib/source-scan.mjs` | Synced source helpers (worklet directive, imports, resolution); do not edit here | When changing the checker |
| `scripts/check-lib.mjs` | Shared script library, synced (do not edit here) | Never by hand |
| `assets/shared.json` | Declares the synced files | When adding a shared file |
| `assets/template-placeholders.json` | The values the self-test and template verification fill in (`bounce-arena`) | When adding a placeholder |
| `tests/bases/app/` | The fixture base under the templates: `package.json`, `tsconfig.json` and stand-ins for two canvas kit files | When the fixture base changes |
| `tests/fixtures/` | `check-realtime-loop/`: `good/` (templates + ricochet example), one `bad-*` per planted bug, `pass-*` (turn-based app, config folder, installed skills, an RNG draw in the tick, a destructured frame callback with a renamed pause flag) and `error-*` | When adding a rule to the checker |

## Related skills

- `skia-canvas-rendering` - drawing rules, palettes, the canvas kit and lifecycle, turn timelines that replay simulate-then-replay events.
- `canvas-gestures-and-hit-testing` - the stick gesture that writes integer commands, and aim intents.
- `game-rules-engine` - `applyMove`, events, the game module and `SavePoint`.
- `seeded-determinism-and-golden-vectors` - the determinism policy and the sfc32 PRNG.
- `game-session-host` - the game screen: pause UI, save points, HUD from events, results.
- `game-balance-and-bots` - thousands of seeded headless runs of the same `step`.
- `golden-and-snapshot-tests` - the golden policy and accepting baselines.
- `audio-and-haptics-feedback` - drained events become sounds and haptics on JS.
- `performance-budgets` - the frame-time recorder and the on-device hitch-rate budget.
- `ios-simulator-build` and `android-emulator-build` - the Release builds of step 10.

Files in this skill

  • SKILL.md19 KB
  • assets/shared.json1 KB
  • assets/template-placeholders.json291 B
  • examples/arena-chase/arena-chase-sim.test.ts2.1 KB
  • examples/arena-chase/arena-chase-sim.ts4.5 KB
  • examples/ricochet-volley/simulate-volley.test.ts2.3 KB
  • examples/ricochet-volley/simulate-volley.ts3.8 KB
  • examples/ricochet-volley/volley-timeline.test.ts1.1 KB
  • examples/ricochet-volley/volley-timeline.ts1.1 KB
  • references/checker.md10.5 KB
  • references/fixed-step-loop.md16.2 KB
  • references/geom-kit.md4.3 KB
  • references/sim-design.md12.1 KB
  • references/simulate-then-replay.md4.7 KB
  • scripts/check-realtime-loop.mjs40.6 KB
  • scripts/lib/source-scan.mjs35.1 KB
  • scripts/selftest.mjs506 B
  • templates/src/canvas/canvas-layout.test.ts4.1 KB
  • templates/src/canvas/canvas-layout.ts6.1 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.