Skip to content

GameLoop ​

Runs your callbacks on every frame: fixed simulation steps, then a render.

Use it when you want a ready-made loop. It owns a FrameSource and a FrameScheduler, and extends Emitter.

ts
import { GameLoop } from "@jolly-pixel/loop";

const loop = new GameLoop({
  fixedFps: 60,
  maxFps: 144
});

loop.on("panic", ({ droppedMs }) => {
  console.warn(`dropped ${droppedMs}ms`);
});

loop.start({
  fixedUpdate: (fixedDeltaMs) => {
    return world.step(fixedDeltaMs / 1000);
  },
  update: (renderDeltaMs, alpha) => {
    return renderer.draw(alpha);
  }
});

Options ​

ts
new GameLoop(options?: GameLoopOptions);

Takes every FrameScheduler option, plus:

OptionDefaultWhat it does
sourcenew RequestAnimationFrameSource()Where frames come from.
keepAlive() => trueReturn false to let the loop sleep. See Rendering on demand.
trailingRenders2Extra frames to render before sleeping. Integer >= 0, else RangeError.
idleDelay0Milliseconds to keep rendering after the last activity before sleeping. Finite and >= 0, else RangeError.

Callbacks ​

Passed to start(). All optional. Deltas are milliseconds.

CallbackWhen it runs
frame(schedule, now)Every frame, first. Gets the full FrameSchedule.
fixedUpdate(fixedDeltaMs, stepIndex)schedule.steps times. stepIndex restarts at 0 each frame.
update(renderDeltaMs, alpha)Last, only when schedule.render is true. Gets schedule.renderDelta: the time since the previous update, capped frames included.

Methods ​

start(callbacks?): this ​

Resets the scheduler and starts the source. Throws Error if already running.

Callbacks stick across stop()/start(). Omit them to reuse the last set.

stop(): this ​

Stops the source and clears paused. Does nothing when already stopped.

pause(): this / resume(): this ​

Freeze and unfreeze simulation time. Both are safe to call twice.

While paused, frames keep coming with frameDelta: 0 and no fixed steps, and update still runs. Paused time is not replayed on resume.

step(count = 1): this ​

Runs count extra fixed steps and one render on the next frame, then goes back to the current speed. Meant for frame-by-frame debugging while paused:

ts
loop.pause();
loop.step();

That frame's frameDelta grows by count * fixedDelta. Wakes a sleeping loop. Does nothing on a stopped loop. See FrameScheduler.queueSteps().

invalidate(): void ​

Asks for another rendered frame and wakes a sleeping loop. Does nothing on a stopped loop.

Properties ​

PropertyMeaning
schedulerThe FrameScheduler. Change scheduling options here.
sourceThe FrameSource.
runningstart() was called and stop() was not. Stays true while sleeping.
sleepingRunning, but the source is stopped until something calls invalidate().
pausedSimulation time is frozen.
timeScaleSimulation speed. Read and write.

IMPORTANT

Set the speed with loop.timeScale, not loop.scheduler.timeScale. Pausing writes 0 to the scheduler and resuming writes loop.timeScale back.

Events ​

EventPayloadEmitted when
startnonestart() runs, before the first frame.
stopnonestop() stops a running loop.
pause{ paused }pause() (true) or resume() (false) changes state.
clamp{ rawDelta, frameDelta }A frame delta was above maxFrameDelta.
panic{ droppedMs, steps }A frame hit the step budget and dropped time.
sleepnoneThe loop goes idle.
wakenoneinvalidate() wakes it.

clamp and panic fire before the frame callback.

Rendering on demand ​

Give the loop a keepAlive predicate and it stops its source while nothing needs a frame. Good for views that only change on input.

ts
import {
  AnimationLoopFrameSource,
  GameLoop
} from "@jolly-pixel/loop";

const loop = new GameLoop({
  source: new AnimationLoopFrameSource(renderer),
  keepAlive: () => camera.moving
});

canvas.addEventListener(
  "pointermove",
  () => loop.invalidate()
);
  • start(), invalidate(), and any frame where keepAlive() returns true queue 1 + trailingRenders rendered frames.
  • Once they are drawn and idleDelay milliseconds have passed since the last of those calls or frames, the loop sleeps. A delay keeps the loop from sleeping and waking between two inputs.
  • Frames skipped by maxFps don't count, so a cap never eats a requested frame.
  • On wake, the idle time is skipped: the first frame has a zero delta.

The loop stays running while asleep, so suspendWhenHidden still works.