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 #
import { useGameLoop } from "@carverjs/core/hooks";Signature #
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".
| Argument | Type | Description |
|---|---|---|
delta | number | Seconds since last frame (or fixedDelta in fixed-timestep mode) |
elapsed | number | Total seconds elapsed while in the "playing" phase |
options (UseGameLoopOptions) #
| Option | Type | Default | Description |
|---|---|---|---|
stage | GameLoopStage | "update" | Which update stage this callback runs in |
fixedTimestep | boolean | false | Use fixed-timestep accumulator (only meaningful on "fixedUpdate" stage) |
fixedDelta | number | 1/60 | Seconds per fixed tick (~16.67ms) |
maxDelta | number | 0.1 | Maximum raw delta cap — prevents spiral-of-death after tab switches |
enabled | boolean | true | Per-instance toggle, independent of global game phase |
Return Value (UseGameLoopReturn) #
| Field | Type | Description |
|---|---|---|
phase | GamePhase | Current game phase from the store |
isPaused | boolean | true when phase is "paused" or "gameover" |
elapsed | number | Total elapsed seconds since the game started playing |
Update Stages #
Stages run in this order every frame:
| Stage | Priority | Intended Use |
|---|---|---|
earlyUpdate | -10 | Input gathering, flag resets at frame start |
fixedUpdate | -20 | Deterministic physics and movement |
update | -30 | General game logic (default) |
lateUpdate | -40 | Camera 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:
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 #
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) #
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 #
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) #
// 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 #
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.