143 lines
8.8 KiB
Markdown
143 lines
8.8 KiB
Markdown
# 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 variable` read and write them.
|
||
- **Owner**: a placed behavior may point at another object of the scene (inspector → Owner,
|
||
dashed line in the viewport). The `Owner` node returns it; `Self` returns the behavior itself.
|
||
- **Node set** (`src/core/graph/nodes.ts`): events `On start`, `On update`, `On key down/up/held`,
|
||
`On timer` · flow `Branch`, `Sequence` · data `Self`, `Owner`, `Object by name`, `Get/Set
|
||
variable`, `Get/Set property`, `Get position`, 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` ·
|
||
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 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 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, 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`).
|
||
|
||
```bash
|
||
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.
|