- TextField: new onSubmit callback (Enter only, independent of value change) - NodePalette: use onSubmit instead of onChange, add per-open picked guard - e2e: new check 13a (type + click row -> exactly one node), wait for Chromium exit in finally with SIGKILL escalation after 5s Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Game Maker
A browser-based tool "one step before a game engine": create assets (scenes, cameras, sprites), place them into scenes, tweak their properties in an inspector and press Play – all in one program, all drawn inside a single WebGL canvas.
Live: https://nockal.com/game-maker/ · Widget playground: https://nockal.com/game-maker/?playground
Concepts
| Term | Meaning |
|---|---|
| Asset | Named thing with a type (scene, camera, sprite, behavior) and typed properties. Assets have no position. |
| Scene object | An asset placed into a scene: a link to the asset + a position (x, y, z) + sparse overrides of the asset's properties + an optional owner (another object of the scene). Scenes cannot be placed. |
| Behavior | Logic as an asset: a node graph plus the variables it declares. Placed like anything else (a diamond symbol in the scene); its owner is the object the graph acts on. |
| Override | A per-object value that replaces the asset's value for one property. Only changed keys are stored; the inspector marks them bold and offers a reset (↺). |
| Default | Every property has a schema default. Asset values that differ from it are marked bold in the inspector and can be reset (↺) the same way an override can. |
| Edit mode | The viewport draws the project model; panels, grid, gizmos and drag-to-move are active. |
| Play mode | A World is instantiated from the active scene (one entity per scene object, resolved properties as mutable state). Systems tick it; nothing writes back. Stop discards the world. |
The camera defines what is rendered. A camera asset has a view size (width × height,
default 640×360) and a zoom; the frame it covers in world units is size / zoom. In edit
mode that frame is drawn as an outline. In play mode only that frame is rendered – fitted into
the viewport with black letterboxing – and objects outside it are culled. A scene without a
camera renders nothing in play mode. Play has no built-in behaviour (no input, no movement):
everything that happens is defined by behavior graphs.
Behaviors (node graphs)
A behavior asset holds a Blueprint-style graph edited in the Graph tab of the centre
area (the Scene tab shows the viewport). Ports come in two kinds: exec (white
arrows, control flow) and data (coloured circles; number, boolean, string,
object, any). Event nodes start a pulse; the interpreter follows exec edges and pulls
data inputs lazily from connected outputs. Unconnected data inputs use the inline value
shown on the node and editable in the inspector.
- Variables are the behavior's own property schema (
BehaviorAsset.variables): they appear in the inspector, can be overridden per placed instance, and each running instance gets its own mutable copy.Get variable/Set variableread and write them. - Owner: a placed behavior may point at another object of the scene (inspector → Owner,
dashed line in the viewport). The
Ownernode returns it;Selfreturns the behavior itself. - Node set (
src/core/graph/nodes.ts): eventsOn start,On update,On key down/up/held,On timer· flowBranch,Sequence· dataSelf,Owner,Object by name,Get/Set variable,Get/Set property,Get position, literals,Add/Subtract/Multiply/Divide,Compare,And/Or/Not,Key is down· actionsMove,Set position,Destroy,Log,Stop game. A node type is oneregisterNode()call; palette, ports, validation and inspector fields are generated from it. - Editing: right-click the canvas for the searchable palette, drag port → port to connect
(types are checked; a data input holds one link and is replaced), drag nodes, right-click a
node or port to delete/disconnect,
Deleteremoves the selected node, wheel zooms, dragging the empty canvas pans. Everything is undoable. - Live view: while playing with the Graph tab open, the exec edges and nodes that just ran light up – the same canvas draws editor and game, so this costs nothing extra.
- Runtime:
BehaviorSystemcreates oneGraphInterpreterper behavior entity; graphs only touch world entities. A runaway loop throws after 1000 steps and stops play with a notice.
Editor symbols are always on top. The viewport draws scene content (sprites) in an
objects layer and editor symbols (camera icon, labels) in an overlay layer above it, so a
sprite can never hide a symbol regardless of z or hierarchy order.
Z-order is part of the position. position.z is the draw order of a scene object: higher
z is drawn on top; objects with equal z keep their hierarchy order. It is edited in the
inspector next to X/Y, undoable like any move, and applies in edit and play mode alike.
Stack
- PixiJS v8 – rendering and pointer events. No game engine; runtime systems live in
src/runtime/. - Own canvas widget toolkit (
src/ui/) – panels, buttons, lists, text/number/color fields, popups. The only DOM element is a hidden<input>used as a keyboard bridge (IME, mobile keyboard, clipboard) while a canvas text field is focused. - Vite + TypeScript, Vitest for the pure core.
- PHP 8 JSON API (
api/) storing one JSON file per project indata/projects/, with atomic writes and an integer revision that rejects stale saves (HTTP 409).
Layout
web/index.html Vite source page (dev) index.html built page (generated, served)
src/core/ model, property schema, resolve, commands, store – no Pixi imports
src/core/graph/ graph model, node registry + built-in nodes, interpreter (headless, unit-tested)
src/runtime/ World, Entity, System interface, Input tracker, BehaviorSystem
src/ui/ widget toolkit (generic, knows nothing about assets)
src/viewport/ scene renderer: model in edit mode, world in play mode
src/editor/ App composition, state, panels, inspector field factory
src/editor/graph/ GraphEditor (canvas), NodeView, NodePalette
api/ PHP backend data/projects/ persisted projects
tests/ Vitest unit tests tools/ e2e.mjs (CDP smoke test), seed-demo.mjs
Rules that keep it organised: core/ imports nothing from other layers; ui/ never sees
assets; position exists only on scene objects; overrides are merged in exactly one place
(core/properties/resolve.ts); the inspector is generated from PropertySchema – a new
property is one schema line.
Development
Node 22 is required (on this machine: PATH=/opt/node22/bin:$PATH).
npm install
npm run dev # http://localhost:5175/game-maker/dist/ – API calls are proxied to production
npm test # unit tests (core)
npm run typecheck
npm run build # writes dist/ and regenerates index.html
node tools/seed-demo.mjs <projectId> # reset the demo project to the state the e2e run expects
node tools/e2e.mjs <projectId> # headless end-to-end run with screenshots in /tmp/gm-e2e-*.png
The workspace folder itself is what nginx serves, so npm run build is the deployment.
API
All routes go through api/index.php?r=… (the site's nginx does not pass PATH_INFO).
| Method | Route | Body → Result |
|---|---|---|
| GET | projects |
[{id,name,revision,updatedAt}] |
| POST | projects |
{name} → project document (201) |
| GET | projects/{id} |
{id,name,revision,updatedAt,project} |
| PUT | projects/{id} |
{revision, project} → {revision,updatedAt} or 409 {revision} when stale |
| DELETE | projects/{id} |
204 |
There is no authentication yet; data/ is world-readable. Both are phase-2 topics.
Keyboard
Ctrl+Z / Ctrl+Shift+Z / Ctrl+Y undo/redo · Delete remove selection · Ctrl+Enter play ·
Esc stop · Tab cycles fields · number fields: drag to scrub (Shift ×10, Alt ×0.1),
↑/↓ step · viewport: wheel zooms, middle-drag or Shift+drag pans.
Not yet
Textures on sprites, physics/collisions, spawning from graphs, multi-select, auth, editing while playing, touch-optimised layout, graph comments/groups.