Packages
Optional, composable pieces you can add to a game. They are subpaths of the quantum-forge-engine package, so using one is an import rather than an install.
Two of them need an optional peer dependency first:
npm install pixi.js # PixiRenderer
npm install howler # AudioManagerAvailable packages
| Package | Import | Description |
|---|---|---|
| Input | quantum-forge-engine/input | Keyboard, mouse, gamepad, touch, gestures, local multiplayer |
| Collision | quantum-forge-engine/collision | Rect, circle, and point tests plus a SpatialGrid |
| Audio | quantum-forge-engine/audio | Howler.js wrapper for sounds and music |
| Particles | quantum-forge-engine/particles | Burst, trail, and explosion effects |
| Animation | quantum-forge-engine/animation | Tweening with easing functions |
| Entities | quantum-forge-engine/entities | Entity registry with spatial queries; disposes quantum handles on removal |
| State machine | quantum-forge-engine/state-machine | Config-driven finite state machine |
| Timer | quantum-forge-engine/timer | Game-aware timers, pause- and scale-aware |
| Save | quantum-forge-engine/save | Slot-based saves with versioning and auto-save |
| Scenes | quantum-forge-engine/scenes | Stack-based scene lifecycle |
Collision
Collision is a set of pure functions plus a grid for narrowing candidates.
import { SpatialGrid, circleOverlaps } from "quantum-forge-engine/collision";
const grid = new SpatialGrid<Enemy>(64);
for (const enemy of state.enemies) grid.add(enemy);
for (const other of grid.getNearby(player.x, player.y)) {
if (circleOverlaps(player, other)) {
// handle collision
}
}getNearby(x, y) takes coordinates and returns everything in that cell and the eight around it. Confirm real overlaps with rectOverlaps, circleOverlaps, rectCircleOverlaps, or one of the point tests.
Audio
import { AudioManager } from "quantum-forge-engine/audio";
const audio = new AudioManager({ soundVolume: 0.8, musicVolume: 0.4, logger });
await audio.loadSound("hit", "/sounds/hit.mp3");
audio.play("hit");
audio.setMasterVolume(0.8);
audio.mute(); // no argument
audio.unmute();Browsers block audio until the player interacts with the page. isUnlocked() reports whether that has happened, which is what a "tap to enable sound" prompt keys off.
Particles
import { ParticleSystem } from "quantum-forge-engine/particles";
const particles = new ParticleSystem({ gravity: 200 });
particles.emit({ x, y, count: 20, color: "#a855f7", speed: 100, life: 0.5 });
particles.burst(x, y, Math.PI / 2, 20, "#a855f7"); // positional arguments
// In game loop
particles.update(dt);
particles.render(ctx); // Canvas 2D
particles.renderToGraphics(this.graphics); // PixiJSburst, trail, and explode take positional arguments. emit(config) is the options-object entry point, and its lifetime field is life, in seconds.
Animation
import { Tween, MultiTween, Easing } from "quantum-forge-engine/animation";
const fade = new Tween({
from: 0,
to: 1,
duration: 500, // milliseconds
easing: Easing.easeOutCubic,
onUpdate: (value) => { sprite.alpha = value; },
});
fade.start();
function frame(now: number) {
fade.update(now); // pass a timestamp, not a delta
if (fade.isActive()) requestAnimationFrame(frame);
}Tween animates one number. For object-shaped animations use MultiTween with a values map:
const move = new MultiTween({
values: { x: { from: 0, to: 100 }, y: { from: 0, to: 200 } },
duration: 500,
easing: Easing.easeOutCubic,
onUpdate: (values) => { sprite.x = values.x; sprite.y = values.y; },
});Easings live on the Easing object. AnimationManager starts animations for you and updates them all from one call.
Entities
EntityManager keeps entities by id and answers spatial queries. From engine 2.0 it also ends the life of the quantum handles an entity carries.
import { ensureLoaded, quantum, type Quantum } from "quantum-forge/quantum";
import { EntityManager } from "quantum-forge-engine/entities";
await ensureLoaded();
interface Ghost {
id: string;
x: number;
y: number;
here: Quantum<boolean>;
}
const ghosts = new EntityManager<Ghost>();
ghosts.add({ id: "g1", x: 40, y: 60, here: quantum([false, true]).superpose() });
ghosts.get("g1")?.here.probability(true); // 0.5, drawn as alpha
ghosts.remove("g1"); // disposes g1.hereremove() and clear() dispose the handles stored directly on an entity, under a string or symbol key, and the handles inside an array stored there. Nothing deeper is walked. add() with an id that is already present replaces the entity and disposes the old entity's handles that the new one no longer carries.
Disposing measures, so removing an entity collapses whatever its handles were entangled with. A handle belongs to one entity: if two entities share it, removing either one disposes it for both. Pass { disposeQuantum: false } to remove(), clear() or add() to keep the handles, or to the constructor to change the default. See Lifecycle.
Save
import { SaveManager } from "quantum-forge-engine/save";
const saves = new SaveManager<GameState>({ prefix: "my-game", version: 1 });
saves.save("slot-1", engine.getState());
const data = saves.load("slot-1");
saves.enableAutoSave("autosave", () => engine.getState(), 30); // seconds
saves.update(dt); // in the game loop, drives auto-saveEvery method takes a named slot. Auto-save is turned on with enableAutoSave() rather than a constructor flag, and it advances only when you call update(dt).
Saves are JSON. A quantum handle is a live reference into the WASM simulator, not data, so keep handles out of the state you save. To save quantum state, record it with QuantumRecorder and store the serialized log next to the game state.
Scenes
import { SceneManager, fadeTransition } from "quantum-forge-engine/scenes";
const scenes = new SceneManager<GameContext>({ logger });
await scenes.push("menu", menuScene, context);
await scenes.push("game", gameScene, context, fadeTransition(0.3, drawFade));
await scenes.pop(); // back to menu, which gets onResume()There is no register() step: you pass the scene object to push() or replace(). A scene implements update(dt) and render(), and may add onEnter, onExit, onPause, and onResume. Drive the stack by calling scenes.update(dt) and scenes.render() each frame.