useGameLoop

Registers a per-frame callback with control over update stage, timestep mode, and integration with the global game phase. Built on R3F's useFrame with priority-based ordering, a max-delta cap, and optional fixed-timestep accumulator.

Import #

tsx
import { useGameLoop } from "@carverjs/core/hooks";

Signature #

ts
function useGameLoop(
  callback: (delta: number, elapsed: number) => void,
  options?: UseGameLoopOptions
): UseGameLoopReturn;

Parameters #

callback #

Called every frame (or every fixed tick) while the game phase is "playing".

ArgumentTypeDescription
deltanumberSeconds since last frame (or fixedDelta in fixed-timestep mode)
elapsednumberTotal seconds elapsed while in the "playing" phase

options (UseGameLoopOptions) #

OptionTypeDefaultDescription
stageGameLoopStage"update"Which update stage this callback runs in
fixedTimestepbooleanfalseUse fixed-timestep accumulator (only meaningful on "fixedUpdate" stage)
fixedDeltanumber1/60Seconds per fixed tick (~16.67ms)
maxDeltanumber0.1Maximum raw delta cap — prevents spiral-of-death after tab switches
enabledbooleantruePer-instance toggle, independent of global game phase

Return Value (UseGameLoopReturn) #

FieldTypeDescription
phaseGamePhaseCurrent game phase from the store
isPausedbooleantrue when phase is "paused" or "gameover"
elapsednumberTotal elapsed seconds since the game started playing

Update Stages #

Stages run in this order every frame:

StagePriorityIntended Use
earlyUpdate-10Input gathering, flag resets at frame start
fixedUpdate-20Deterministic physics and movement
update-30General game logic (default)
lateUpdate-40Camera follow, trailing effects, UI sync

After all stages, R3F auto-renders the scene at priority 0.

Game Phase #

The loop integrates with the global game store. Callbacks only fire when phase === "playing". Control the phase via useGameStore:

tsx
import { useGameStore } from "@carverjs/core/store";

// In a React component
const { pause, resume, reset, setPhase } = useGameStore();

// Or outside React (e.g., in a callback)
useGameStore.getState().pause();

The store initializes with phase: "loading". You must call setPhase("playing") to start the game loop.

Usage #

Basic per-frame update #

tsx
import { useRef } from "react";
import { useGameLoop } from "@carverjs/core/hooks";
import type { Mesh } from "three";

function RotatingBox() {
  const meshRef = useRef<Mesh>(null);

  useGameLoop((delta) => {
    if (meshRef.current) {
      meshRef.current.rotation.y += delta * Math.PI; // 180°/sec
    }
  });

  return <mesh ref={meshRef}><boxGeometry /><meshStandardMaterial /></mesh>;
}

Fixed timestep (deterministic physics) #

tsx
import { useRef } from "react";
import { useGameLoop } from "@carverjs/core/hooks";
import { Vector3 } from "three";

function FallingBody() {
  const velocity = useRef(new Vector3(0, 0, 0));
  const meshRef = useRef<Mesh>(null);

  useGameLoop(
    (fixedDelta) => {
      // Runs at exactly 60 Hz regardless of frame rate
      velocity.current.y += -9.8 * fixedDelta;
      if (meshRef.current) {
        meshRef.current.position.addScaledVector(velocity.current, fixedDelta);
      }
    },
    { stage: "fixedUpdate", fixedTimestep: true }
  );

  return <mesh ref={meshRef}><sphereGeometry /><meshStandardMaterial /></mesh>;
}

Pause and resume #

tsx
import { useEffect } from "react";
import { useGameStore } from "@carverjs/core/store";

function GameController() {
  const { pause, resume, phase } = useGameStore();

  useEffect(() => {
    const onKey = (e: KeyboardEvent) => {
      if (e.key === " ") {
        phase === "playing" ? pause() : resume();
      }
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [phase, pause, resume]);

  return null;
}

Staged updates (input → logic) #

tsx
// Read input in earlyUpdate
function InputReader() {
  const keys = useRef<Set<string>>(new Set());

  useEffect(() => {
    const down = (e: KeyboardEvent) => keys.current.add(e.key);
    const up = (e: KeyboardEvent) => keys.current.delete(e.key);
    window.addEventListener("keydown", down);
    window.addEventListener("keyup", up);
    return () => {
      window.removeEventListener("keydown", down);
      window.removeEventListener("keyup", up);
    };
  }, []);

  useGameLoop(() => {
    // Input is fresh for this frame
  }, { stage: "earlyUpdate" });

  return null;
}

// Consume input in update
function PlayerMovement() {
  useGameLoop((delta) => {
    // Input has already been gathered in earlyUpdate
  }, { stage: "update" });

  return null;
}

Conditional enable #

tsx
function BossAI({ active }: { active: boolean }) {
  useGameLoop(
    (delta) => {
      // Only runs when active is true
    },
    { enabled: active }
  );

  return null;
}

Type Definitions #

See Types for GameLoopStage, GamePhase, UseGameLoopOptions, UseGameLoopReturn, and GameLoopCallback.