The kernel is the small core every Phaser AE game runs on. It owns the frame loop and the services every game needs: the clock, the roster of objects, scenes, input, the loader and texture atlas, timers, the seeded random source, signals and hooks, standing commands, debug controls, inspection and a worker pool. It also defines the contracts that packs plug into: worlds, backends, render passes, simulation steps and the definitions registry. Everything else (drawing, physics, audio, UI, genres) is a pack, and the kernel never imports one.
This page is for reading or changing the kernel's source. To make a game, read the game's own docs/ folder: its
00-kernel.md is docs/kernel.md, the kernel as game code sees it.
The kernel is source-available under the PolyForm Shield License 1.0.0, with an additional permission to make and sell games: see LICENSE.md.
The rules
The kernel keeps seven rules, and its comments cite them as "Law 1" to "Law 7".
- Everything on stage is a roster object. The roster is the single ground truth of what exists; a thing not in it does not draw. A particle system or a tilemap is one roster object (an aggregate).
- No unattributed pixels. Game code cannot reach the canvas or the GPU. Packs draw, through the frame graph.
- All time flows from the clock. Simulation runs in fixed steps, in seconds. Nothing in a simulation reads
performance.now(),Date.now()orsetTimeout, so pause is one flag and slow motion is one number. - All chance flows from the rng. Every random number comes from the seeded
rng(or, without a game, acreateRng(seed)stream), so a seed and the same input replay the same game. On any machine: a build'sMathisdeterministicMath(math.ts), whose transcendental functions (sin,atan2,pow,hypot…) are fdlibm's algorithms in exactly specified arithmetic, where the engines' own differ in the last bit. - One conventions page. Units are set once, in
docs/reference/CONVENTIONS.md(every game'sdocs/01-conventions.md): world units, y down in 2D, y up and right-handed in 3D, radians, seconds, positions at the centre, CSS colour strings at the boundary. - Garbage fails loud. In dev mode (the default) a non-finite number, an unknown name or a write to a killed object throws; in production it is ignored.
- The kernel never imports a pack. Packs reach each other only through the composed game's keys, signals, hooks and the definitions registry.
Entry points
| import | for | holds |
|---|---|---|
@phaser-ae/kernel |
packs, the bakery | createGame, every public type, the 3D maths and drag conventions, colour helpers, tick sources, WebGL2 helpers |
@phaser-ae/kernel/verbs |
game code | move, moveTo, teleport, rotate, scale, tint, show, hide, kill, tag, untag, play |
@phaser-ae/kernel/internal |
world packs only | the raw roster tables and the shared 2D and 3D gathers |
Game code does not call createGame: a bake writes it into the game's engine.ts, composing the manifest's packs in
order, and game code imports the composed game from there. From the kernel itself, the bakery's game gate lets game
code import only the verbs, types and the documented maths helpers.
The modules
Each service is an interface in src/<name>.ts, which is the documented contract, and an implementation in
src/<name>.impl.ts. The implementations' own members are documented where they are not part of the interface.
| module | provides |
|---|---|
game.impl.ts |
createGame, the Game that owns every service, the frame loop, boot, stop and destroy, the host contract |
pack.ts |
Pack (what a pack is), Kernel (what its attach receives), GameBase (the kernel's half of game), GameOptions |
clock.ts |
the fixed-step clock, pause, time scale, hold and advance; the TickSource that fires frames |
roster.ts |
Handle, SpawnSpec, the roster's queries, blend modes and vertex deformers |
scene.ts |
the scene stack: go, push, pop, and suspension beneath a push |
scheduler.ts |
after and every: timers on the game clock, owned by a scene |
signals.ts |
step-batched signals, declared per topic |
hooks.ts |
the lifecycle hooks: boot, frame, assets, scene, input, app, debug |
frame.ts, order.ts |
the render pass graph, the simulation list, and the before/after ordering both use |
input.ts |
keys, pointers, gamepads, actions, gestures, the consumer layers and the scene-entry grace |
assets.ts |
the loader, asset packs (assets.json), the atlas, pictures in textures of their own, paint, pixels |
camera.ts |
the camera record a world transforms through and a rig drives |
screen.ts, safe-area.impl.ts |
the screen policy (fit), the surface backends draw to, safe-area insets |
render.ts |
the world/backend contract: instance tables, aggregates, deformers, drawers |
world2d.ts, world3d.ts |
the shared gathers that build instance tables, and the 2D and 3D camera transforms |
commands.ts |
standing commands a host wires once: pause, mute, fullscreen, screenshot, recording |
cheats.ts |
debug controls and their panel |
inspect.ts |
game.inspect(), game.advance() and what they report |
workers.ts |
the worker pool and its job protocol |
registry.impl.ts |
the definitions registry (Kernel.registry): who may replace a name, and sameDefinition, the equality that makes an identical definition a no-op (cheats use it too) |
rng.ts, color.ts, wrap.ts, types.ts |
the seeded rng (and createRng for code without a game), the one colour parser, toroidal wrapping, shared units |
history.ts |
createHistory: undo, redo, seek and replay for deterministic rules, with refusals, checkpoints and validated saves |
math.ts, math-shim.ts |
deterministicMath, a Math whose 22 implementation-approximated functions are ported from fdlibm and give the same bits on every engine; the module the bakery injects as every bundle's Math; installDeterministicMath() for a Node host |
maths3d.ts, controls3d.ts |
pure 3D maths (matrices, rays, quaternions, directions) and the three drag conventions |
curves.ts |
Catmull–Rom: a span in any dimension (uniform, centripetal or chordal), the scalar form for value curves, and an arc-length spline through points |
webgl.ts |
what the WebGL2 backend shares with packs, and the program compiler every pack uses |
verbs.ts |
the verbs |
One frame
Every tick runs Game.runFrame, the same on every backend:
- Finished worker jobs settle, in submission order.
- The clock turns the wall time since the last tick into a whole number of fixed steps (none while paused or held).
- Input is polled; the
worldlayer is suspended while the clock is paused.frame.startis emitted. - For each fixed step: input edges are consumed; signals emitted in the previous step are delivered; every object's
transform and the camera are copied to their previous values;
frame.simulateis emitted; the simulation steps run in their resolved order; each world'safterStepruns; animations advance; due timers fire; the step is counted; and the roster flush emits thespawnandkillsignals and thescene.spawnandscene.killhooks for what the step made and removed. - The camera is interpolated at the clock's alpha, and
frame.renderis emitted (game.framecallbacks run here). - Each world writes depth and builds its instance tables, already interpolated; the render passes run in their
resolved order;
frame.endis emitted.
Order among steps, passes and hook subscribers comes from each one's before/after names, never from kernel source.
A cycle throws the next time the order is resolved.
Hooks or signals. A hook is a call, made the moment something happens (scene.enter, app.resize). A signal is
a record: what one step emits, the next step reads as an array. Simulation talks in signals, so a step's outcome
never depends on who subscribed first; the lifecycle talks in hooks.
Ownership
Scenes own things. Every object, timer, game.step, game.frame and Kernel.own disposer belongs to the scene that
was current when it was made. When that scene exits, all of it goes: objects are killed, timers cancelled, steps
removed and disposers run newest first, and camera bounds go back to what they were. While a scene is suspended
beneath a push, its objects still draw but leave every query, and its timers, steps, frame callbacks and animations
hold. Anything made before the first scene lasts for the game.
A pack that changes state on request (a collision rule, gravity, ambient light) registers the undo with k.own, so
leaving a scene restores it without the game remembering to.
How a pack plugs in
A pack is a Pack object: an id, attach(k), and optionally requires, backends, topics, columns and
detach. createGame(...).use(pack) attaches packs in manifest order; each attach receives the Kernel and
returns the API added to game under its key. In attach a pack may:
- add fixed steps (
k.simulate.add) and render passes (k.frame.addPass), ordered by name; - subscribe to hooks (
k.hooks.on) and emit or read its declared signal topics; - declare per-object columns, read on a handle under the pack's key (
h.physics.vx), and handle its own spawn keys (k.onSpawnData); - register a world (
k.registerWorld) or, as a backend, draw tok.surface; - register definitions under a registry kind (
'deform','drawer','inspect'…) for the pack that reads them. Two packs registering one name throws at compose; after compose the last definition wins and an identical one changes nothing (the stored object stays), so a pack'sdefinecan run in a scene'senterevery time it does. A pack that caches work per definition keys the cache by the definition, not its name, so a replacement is picked up; - claim a standing command (
k.commands.claim), add debug controls (k.cheats.add), hold the ready moment while a loading screen is up (k.holdReady), and run jobs on worker threads (k.workers.run).
The page around the game
window.gameis the composed game fromcreateGameon, so a host page can callgame.commands.pause()or readgame.isReadywhile the game loads.window.PhaserGameReady(), if defined, is called once at the ready moment (game.ready), andwindow.PhaserGamePlaying()once when a scene namedplayfirst enters.- The backquote key opens the debug-controls panel;
?cheatsin the address adds a button for it on a phone. - The canvas goes in
GameOptions.mount, else the element with idgame, elsedocument.body, and is sized by the screen policy'sfit.
Headless and dev mode
With no document the kernel runs headless: no canvas in a page and no DOM listeners, a manual tick source where
there is no requestAnimationFrame, and every load resolving blank, so tests and replays run in Node exactly as the game would. A test harness can serve real files to
the loader through globalThis.__aeHeadlessFetch. GameOptions.dev (default true, standalone builds included) makes
misuse throw rather than pass silently.
