rootandClaude Fable 5.1 0a59513b74 Collider asset type, CollisionSystem and collision nodes
- Asset type 'collider' (box/circle) placed as child of a body object
- 'Add collider' shortcut (hierarchy context menu + inspector)
- CollisionSystem with enter/stay/exit pairs, sibling colliders excluded
- Nodes: On collision enter/stay/exit, Is touching
- Green outline in edit mode, hidden in play

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 15:25:04 +02:00
2026-09-07 10:07:06 +02:00
2026-09-07 10:07:06 +02:00

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, collider) 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 parent (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 parent is the owner the graph acts on.
Collider A shape (box or circle) for collision detection, placed as a child of the object it belongs to (its body). Drawn as a green outline in edit mode only.
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 variable read and write them.
  • Owner = parent: a placed behavior is usually parented to the object it controls (inspector → Parent, or drag it onto that object in the hierarchy; dashed line in the viewport). The Owner node returns it; Self returns the behavior itself; Parent walks up from any object.
  • Node set (src/core/graph/nodes.ts): events On start, On update, On key down/up/held, On timer, On collision enter/stay/exit · flow Branch, Sequence · data Self, Owner, Parent, Object by name, Get/Set variable, Get/Set property, Get position (local and world), literals, Add/Subtract/Multiply/Divide, Min/Max/Abs/Sqrt/Clamp, Length, Distance, Normalize, Axis input (four keys → a normalized direction, so diagonals are not faster), Compare, And/Or/Not, Key is down, Is touching · actions Move (dx, dy, scale, dt – connect an event's dt for per-second movement), Set position, Destroy, Log, Stop game. A node type is one registerNode() call; palette, ports, validation and inspector fields are generated from it. A port may declare a default for its inline value (e.g. Move.scale = 1), shown on the node and in the inspector.
  • Idiomatic movement (the demo's "Mover", five nodes): On update → Move(Owner, dx = Axis input.x, dy = Axis input.y, scale = Get variable speed, dt = On update.dt).
  • 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, Delete removes the selected node, wheel zooms, dragging the empty canvas pans. Everything is undoable.
  • Selection-bound: the Graph tab shows the behavior the current selection refers to (asset, placed object or node); with nothing or a non-behavior selected it is empty.
  • Live view: Play always switches to the Scene tab; switch back to Graph while playing and the exec edges and nodes that just ran light up – the same canvas draws editor and game, so this costs nothing extra.
  • Runtime: BehaviorSystem creates one GraphInterpreter per 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 (tree) order. It is edited in the inspector next to X/Y, undoable like any move, and applies in edit and play mode alike.

Hierarchy (parenting). Every scene object may have a parent in the same scene (SceneObject.parentId). Positions – including z – are local to the parent; the world position is the sum along the chain (src/core/model/hierarchy.ts). Children move with their parent and are removed with it (also when the parent's asset is deleted); undo brings the whole subtree back. Re-parenting keeps the world position by recomputing the local one, and cycles are rejected. In the Scene objects panel children are indented below their parent; drag a row onto another row to parent it, onto the empty space below the rows (or right-click → Unparent) to move it to the top level; the inspector offers the same as a Parent menu. In play mode entities carry the same parent link: moving a parent entity moves its subtree, Destroy removes it. Get position exposes both the local and the world coordinates.

Colliders and collisions

A collider is an asset (shape box/circle, width/height or radius) placed like any other object – typically as a child of the sprite it belongs to, so its position is the offset from that body and it moves with it. Right-click an object in Scene objects (or press + Add collider in its inspector) to get one with one click: a box collider sized like the sprite, parented at its centre. Colliders can be reused across objects, overridden per instance and stacked (hitbox + hurtbox on one sprite; colliders of the same body never collide with each other). They are editor symbols: green outlines in edit mode, invisible in play mode.

Play runs a CollisionSystem (src/runtime/systems/CollisionSystem.ts) before the behaviors: every frame it tests all collider pairs (world positions applied; touching edges do not count) and sorts them into enter, stay and exit. Detection only – nothing is pushed apart yet. A behavior receives On collision enter/stay/exit for the colliders of its owner (its parent; itself when top-level): the outputs are other (the other body), other collider and collider (the own one). Is touching(a, b) answers the same question on demand – an object stands for its collider children, a collider for itself. The geometry lives in src/core/properties/collider.ts so editor and runtime agree on it.

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 in data/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, CollisionSystem, 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

The toolbar's right edge shows the frame rate (frames counted over a 0.5 s window, updated twice a second) in edit and play mode alike; __gm.fps() exposes the same number to tests.

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, solid collisions (blocking movement, "move and slide"), spawning from graphs, multi-select, reordering the hierarchy, auth, editing while playing, touch-optimised layout, graph comments/groups.

S
Description
Browser game maker: Pixi v8 canvas editor with visual behavior graphs, PHP API
Readme
11 MiB
0 Stars 1 Watchers 0 Forks
Languages
TypeScript 80.5%
JavaScript 17.1%
PHP 2.3%