World
The World is the central orchestrator of the engine. It wires together the SceneManager, Renderer, Input, and Audio systems and drives the main connect → update → render loop.
Every project creates exactly one World. It is passed to every Actor at construction time and is available throughout the component tree via actor.world.
Creating a game instance
import {
SceneEngine,
ThreeRenderer,
World
} from "@jolly-pixel/engine";
const canvas = document.querySelector("canvas")!;
const sceneManager = new SceneManager();
const renderer = await ThreeRenderer.create(canvas, { sceneManager });
const game = new World(renderer, { sceneManager });The constructor accepts a Renderer and a WorldOptions object:
interface WorldOptions {
/** The scene that manages actors and components. */
sceneManager: SceneContract;
/** Input system for keyboard, mouse, gamepad, etc. @default auto-created from canvas */
input?: Input;
/** Global audio manager. @default new GlobalAudio() */
audio?: GlobalAudio;
/** Enable the exit mechanism on the input system. @default false */
enableOnExit?: boolean;
/** Abstraction over `window` (useful for testing). @default BrowserWindowAdapter */
windowAdapter?: WindowAdapter;
/** Abstraction over global references (useful for testing). @default BrowserGlobalsAdapter */
globalsAdapter?: GlobalsAdapter;
}Loading manager
Three.js assets (models, textures, audio) can share a single THREE.LoadingManager via the game instance:
const manager = new THREE.LoadingManager();
manager.onProgress = (_url, loaded, total) => {
console.log(`${loaded}/${total}`);
};
game.setLoadingManager(manager);The loading manager is available from anywhere as actor.world.loadingManager.
Connect and disconnect
connect() starts the game by wiring up input listeners, the window resize handler, and awakening the scene:
game.connect();Internally this:
- Connects the Input system.
- Registers the renderer's
resizecallback on the window adapter. - Calls
scene.awake(), which awakens all existing actors and emits the"awake"event.
disconnect() tears down the listeners:
game.disconnect();Dispose
dispose() stops the loop, disconnects, and releases the renderer's WebGL context:
game.dispose();Call it whenever a world is dropped, such as when closing a scene in an editor or swapping a canvas. Browsers cap the number of live WebGL contexts (~16 in Chrome), so a world that is garbage-collected without being disposed leaks one, and a long session eventually stops rendering altogether. The world must not be used after disposal.
Game loop
The caller owns a FrameScheduler from @jolly-pixel/loop and passes each FrameSchedule to the world. Use one scheduler per app. World reads no clock.
import { FrameScheduler } from "@jolly-pixel/loop";
const scheduler = new FrameScheduler({ fixedFps: 60, maxFps: 144 });
world.start();
function loop(now: number) {
const exited = world.tick(scheduler.advance(now));
if (exited) { /* stop loop */ }
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);@jolly-pixel/runtime provides a GameLoop that owns the frame source and scheduler. It calls tick() from the renderer's animation loop. Configure timing on the loop:
runtime.loop.scheduler.fixedFps = 60; // simulation rate
runtime.loop.scheduler.maxFps = 144; // render cap, independent of fixedFps
runtime.loop.timeScale = 0.5; // slow motion; 0 pauses the simulationtick(schedule)
schedule is the FrameSchedule produced for the current frame, and World uses it unchanged because it has no accumulator. Tests and editors can replay or construct any frame. Returns true when the input system asked to exit.
One frame, in order:
- Calls
sceneManager.beginFrame(), which snapshots the actor tree and starts pending components. The snapshot is reused by everyfixedUpdateandupdatecall in the frame. - Runs
schedule.stepsfixed steps, each preceded by an Input update. - Updates input once more if the frame ran no step at all.
- On a drawn frame, publishes transient input accumulated across the fixed samples, calls
sceneManager.update(deltaTime, alpha), thenrenderer.draw(). - Calls
endFrame().
fixedUpdate(deltaTime, stepIndex)
Runs deterministic logic at a fixed rate, 0 to maxStepsPerFrame times per frame, always with the same delta. stepIndex counts the steps within the current frame, from zero.
Input is sampled before each step, so a catch-up frame running three steps reports a press edge to the first step only, and a frame that runs no step does not diff the edge away before any step has seen it.
update(deltaTime, alpha)
Runs variable-rate logic once per drawn frame. alpha is how far the frame sits between the last fixed step and the next one, in [0, 1). Pass it to Interpolated from @jolly-pixel/loop to draw smoothly between steps.
Before this phase, World republishes mouse transitions and movement accumulated across every fixed-step input sample since the previous drawn frame. Variable-rate components therefore see each mouse edge once even when the current frame ran several fixed steps.
A frame suppressed by maxFps skips update and the draw, but still accumulates time and still runs its fixed steps.
endFrame(): boolean
Called once at the end of each animation frame:
- Calls
sceneManager.endFrame(), which destroys pending components and actors. - If the input system signals an exit, clears the renderer and returns
true. Otherwise returnsfalse.
render()
Delegates to renderer.draw(), which resizes if needed, clears the frame buffer, and renders the scene through all active cameras.
Accessing subsystems
Actors and components can access every subsystem through public World properties:
// From inside a Behavior
const { input, sceneManager, audio, renderer } = this.actor.world;
if (input.keyboard.isDown("Space")) {
audio.play("jump");
}See also
- SceneManager: actor tree, lifecycle, and destruction
- Renderer: rendering pipeline
- Input: input handling
- Actor: the engine's core entity