Replace characters Gitea flags as ambiguous (en dash, ×, ›, −)
1 parent
9be12944fc
commit
293cc2b4ca
13 files changed
+158
-158
No files matched your search
+1
-1
@@ -7,7 +7,7 @@ All routes go through `api/index.php?r=…` (the site's nginx does not pass `PAT
|
||||
| GET | `projects/{id}` | `{id,name,revision,updatedAt,project}` |
|
||||
| PUT | `projects/{id}` | `{revision, project}` → `{revision,updatedAt}` or **409** `{error,revision}` when stale |
|
||||
| DELETE | `projects/{id}` | 204 |
|
||||
| GET | `textures` | `[{file,width,height,bytes,updatedAt}]` – what the image picker offers |
|
||||
| GET | `textures` | `[{file,width,height,bytes,updatedAt}]` — what the image picker offers |
|
||||
| POST | `textures` | **raw image bytes** → `{file,width,height,bytes}` (201) |
|
||||
| DELETE | `textures/{file}` | 204 |
|
||||
|
||||
|
||||
+5
-5
@@ -1,14 +1,14 @@
|
||||
An **animation asset** is a list of pictures played one after another. Each entry is what a
|
||||
sprite's `texture` can hold – a whole texture or one of its frames – so an animation may mix
|
||||
sprite's `texture` can hold — a whole texture or one of its frames — so an animation may mix
|
||||
frames of several sheets, and a picture listed twice is held twice as long; there is no
|
||||
per-frame duration beyond that. Its two properties are **Frames per second** (default 10) and
|
||||
**Playback**: *Loop*, *Once* (stops on the last frame) or *Ping-pong* (back and forth, without
|
||||
showing the turning frames twice).
|
||||
|
||||
**A sprite wears it through `texture`** – the same property, the same picker (animations are
|
||||
**A sprite wears it through `texture`** — the same property, the same picker (animations are
|
||||
listed first, marked "(animation)") and the same instance overrides as a texture or a frame.
|
||||
The sprite stays what is drawn, sized, tinted and collided with; the animation is only a picture
|
||||
that changes over time. *Set to W × H px* takes the size of the first frame. The fastest way in
|
||||
that changes over time. *Set to W x H px* takes the size of the first frame. The fastest way in
|
||||
is right-clicking a texture → **Create animation from texture**, which plays its frames in
|
||||
order (the whole picture when it has none), named after the texture, in one undo step.
|
||||
|
||||
@@ -27,11 +27,11 @@ strip the wheel scrolls the tiles instead.
|
||||
no clock and redraws on changes only. The `AnimationSystem` gives every sprite its own clock, kept on the entity and
|
||||
not in `props` (the world writes changed props back as overrides). The one rule that makes it
|
||||
usable from a graph: **a different animation starts from its first frame, the same one set again
|
||||
runs on** – so `On update → Properties (Sprite, texture on Set)` may choose Run or Idle every
|
||||
runs on** — so `On update → Properties (Sprite, texture on Set)` may choose Run or Idle every
|
||||
frame without freezing the animation on frame one. The system ticks before the behaviors, so
|
||||
**`On animation finished`** fires in the frame a *Once* animation reaches its end, exactly once,
|
||||
with the sprite as `object`. Like collisions it is heard by the behaviors of the sprite's owner
|
||||
subtree – the sprite itself, or a sprite placed next to the behavior below the same object – and
|
||||
subtree — the sprite itself, or a sprite placed next to the behavior below the same object — and
|
||||
by a behavior without an owner for every sprite; its *Animation* setting narrows it to one.
|
||||
There is no node to restart an animation that is already running; that is left for when a game
|
||||
needs it.
|
||||
|
||||
+13
-13
@@ -1,11 +1,11 @@
|
||||
## Stack
|
||||
|
||||
- **PixiJS v8** – rendering and pointer events. No game engine; runtime systems live in `src/runtime/`.
|
||||
- **PixiJS v8** — rendering and pointer events. No game engine; runtime systems live in `src/runtime/`.
|
||||
`main.ts` creates the WebGL context itself with `desynchronized`, which Pixi has no option for:
|
||||
frames skip the compositor queue, so the UI keeps up with the pointer (click to picture measured
|
||||
72–77 ms before, 16–18 ms after). The price is possible tearing while things move fast;
|
||||
72-77 ms before, 16-18 ms after). The price is possible tearing while things move fast;
|
||||
`?vsync` falls back to Pixi's own context. A browser without the mode simply ignores it.
|
||||
- **Own canvas widget toolkit** (`src/ui/`) – panels, buttons, lists, text/number/color fields,
|
||||
- **Own canvas widget toolkit** (`src/ui/`) — panels, buttons, lists, text/number/color fields,
|
||||
popups. The only lasting DOM element is a hidden `<input>` used as a keyboard bridge (IME,
|
||||
mobile keyboard, clipboard) while a canvas text field is focused; *Upload…* adds a short-lived
|
||||
file input.
|
||||
@@ -14,23 +14,23 @@
|
||||
that trusted `e.detail` never saw a second click. Presses are counted per key rather than per
|
||||
widget, because a list rebuilds its rows when the first click changes the selection.
|
||||
Menus read as lists: entries are left-aligned and scroll once they pass 320 px instead of
|
||||
running off the screen. A menu that grows with the project – the parent picker lists every
|
||||
object of the scene – is opened with `{ searchable: true }`, which puts a filter box above
|
||||
running off the screen. A menu that grows with the project — the parent picker lists every
|
||||
object of the scene — is opened with `{ searchable: true }`, which puts a filter box above
|
||||
the entries (Enter takes the first match). A field that opens a menu is a `Dropdown`: value
|
||||
flush left, chevron drawn flush right rather than written into the label.
|
||||
Every label that can be wider than its control is cut with an ellipsis (`truncate` in
|
||||
`Label.ts`, shared by `Label`, `ListRow` and `Button`). A `Button` keeps the full text and
|
||||
measures from it – only what is drawn is shortened – and a subclass that puts something else
|
||||
measures from it — only what is drawn is shortened — and a subclass that puts something else
|
||||
inside the control takes its room off `labelSlot()`, so a long value is cut rather than run
|
||||
underneath the chevron. Given a `width`, a button is exactly that wide and truncates instead:
|
||||
the Behavior, Texture, Animation and Container pickers keep their size whatever they show; the tab
|
||||
bar uses 90 px as a floor. A button that can draw its label bold – on an accent fill: primary,
|
||||
danger, a toggled tab – is measured bold, or a dialog's `Delete` comes out as `Del…`.
|
||||
bar uses 90 px as a floor. A button that can draw its label bold — on an accent fill: primary,
|
||||
danger, a toggled tab — is measured bold, or a dialog's `Delete` comes out as `Del…`.
|
||||
The padding is an inset rather than a claim, so a 24 px icon button spends it on the glyph
|
||||
instead of truncating it away; a button that switches between label forms (`↺` and `↺ ▾`) asks
|
||||
for `minWidth` and grows. `__gm.textOverflows()` guards the whole class of bug: it reports any
|
||||
visible text drawn wider than the widget owning it, `__gm.labelsDropped()` guards the other
|
||||
direction – a button too narrow to draw any of its label – and the `pickerWidth()` of
|
||||
direction — a button too narrow to draw any of its label — and the `pickerWidth()` of
|
||||
`__gm.graph` / `__gm.texture` / `__gm.container` catch a picker that resizes itself around its
|
||||
content (e2e step 14).
|
||||
- **Vite + TypeScript**, **Vitest** for core, toolkit and editor logic.
|
||||
@@ -51,13 +51,13 @@ src/core/store/ serialize (untrusted JSON → Project), http (one API cal
|
||||
src/runtime/ World, Entity, System interface, Input tracker, CollisionSystem, AnimationSystem, BehaviorSystem
|
||||
src/ui/ widget toolkit (generic, knows nothing about assets)
|
||||
src/viewport/ view (transforms), items (what to draw), symbols (how to draw it),
|
||||
textureFill, TextureLibrary, activeCamera – no editor concepts, so tests reach it freely
|
||||
textureFill, TextureLibrary, activeCamera — no editor concepts, so tests reach it freely
|
||||
src/editor/ App → Editor (layout + ticker), PlaySession (the run), ProjectSession (load/create),
|
||||
EditorRun (run in editor), state, actions, shortcuts, debugHook, autosave, textureImport,
|
||||
framing (fit/marquee/snap maths), AssetTabEditor (the shared shell of the asset tabs)
|
||||
src/editor/viewport/ WorldViewport base + SceneViewport/GameViewport – editing tools, hence not in src/viewport/
|
||||
src/editor/viewport/ WorldViewport base + SceneViewport/GameViewport — editing tools, hence not in src/viewport/
|
||||
src/editor/inspector/ Inspector panel, host (what a section builds into), field factory, previewHandlers (scrub → preview → command), valueSpecs
|
||||
(where a value lives – shared with the List tab),
|
||||
(where a value lives — shared with the List tab),
|
||||
one section per selection kind (asset/object/node/frame), variableRows (one row shape for
|
||||
every variable), portSection, aspectAction, objectQueries/vectorRows/parentRow
|
||||
src/editor/panels/ AssetPanel, HierarchyPanel, Toolbar
|
||||
@@ -79,7 +79,7 @@ twice a second) in edit and play mode alike; `__gm.fps()` exposes the same numbe
|
||||
|
||||
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
|
||||
(`core/properties/resolve.ts`); the inspector is generated from `PropertySchema` — a new
|
||||
property is one schema line; a new node is one file **plus a row in the table in
|
||||
`tests/core/node-catalog.test.ts`**, which fails if a node has no case. Comments state contracts
|
||||
and reasons, not what the code does: a `/** */` on every export whose name does not say it all,
|
||||
|
||||
+47
-47
@@ -8,7 +8,7 @@ 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. One node reads and writes them, `Variables`: its settings are a *list*
|
||||
of variables, and each one is an output that can be read at any time – no flow has to reach it –
|
||||
of variables, and each one is an output that can be read at any time — no flow has to reach it —
|
||||
and is read at the moment it is asked, so a read after a write sees the new value. A variable
|
||||
switched to **Set** in the inspector is an input too, written when the flow arrives; the node has
|
||||
its flow pins only while at least one is. Unconnected and with nothing typed, it writes the zero
|
||||
@@ -21,59 +21,59 @@ shown on the node and editable in the inspector.
|
||||
`+ Property` offers: *Sprite*, *Text*, *Collider*, *Camera* and *Container* offer their fields,
|
||||
*Behavior* the variables of the behavior picked below it (renaming such a variable carries the
|
||||
port along, in any graph), and *Any* offers the kinds (Number, Text, …) and leaves the name to be
|
||||
typed. A picked property keeps its field and default – an unconnected `color` is a colour field
|
||||
typed. A picked property keeps its field and default — an unconnected `color` is a colour field
|
||||
starting white, shown on the node as `#ffffff`, and a camera's `width` is its whole-pixel view
|
||||
width. Switching the type keeps the ports; one the new type lacks is marked. The node shows its
|
||||
type as a setting row (*Type: Sprite*, plus *Behavior: Game* under that type). Textures and
|
||||
scenes are no type here: they are not objects in a running game. A variable is a
|
||||
**number, a text, a yes/no, a colour, a texture, a state machine, a list or a struct**; a list declares **nothing** about its items –
|
||||
**number, a text, a yes/no, a colour, a texture, a state machine, a list or a struct**; a list declares **nothing** about its items —
|
||||
each one is a number, a text, a yes/no, a colour, a texture, a state machine or a struct and says so itself, so one list may
|
||||
hold several kinds at once. A list is
|
||||
compared by its items, never by identity – otherwise every reload would look like an override.
|
||||
compared by its items, never by identity — otherwise every reload would look like an override.
|
||||
There is no list *of objects*: a list is stored in the project file and an object is a live
|
||||
handle that no reload survives. A set of objects is a list of their names, walked with `Repeat`
|
||||
and `Object by name`. A list property is shown as a button saying how many items it holds;
|
||||
clicking it opens the **List** tab, which gives each item a row with the same widget the
|
||||
inspector would use for that kind – a struct item is its fields as rows under each other. The inspector stays visible beside the tab, so what the list
|
||||
inspector would use for that kind — a struct item is its fields as rows under each other. The inspector stays visible beside the tab, so what the list
|
||||
belongs to is still on screen. Which list the tab shows is a `ListSlot` (asset property, object
|
||||
override or node setting, plus the `field` of a struct that holds one) on the editor state,
|
||||
because no tab may reach into the `Editor` to switch itself on. An **unconnected `list` input**
|
||||
of a node is the same button and the same tab: its inline value lives in the very same params
|
||||
bag a setting does, and the slot stops resolving the moment an edge feeds the port. What an item holds
|
||||
is **its own** and is chosen there: the **kind mark** in front of a row (`#`, `Aa`, `✓`, `◧`, `▦`, `⊞`)
|
||||
is the picker – a click opens the menu, with no chevron, since the mark has to stay as narrow as
|
||||
its column – and `+ Item ▾` asks for the kind of the one it appends. Because the kind lives in the
|
||||
is the picker — a click opens the menu, with no chevron, since the mark has to stay as narrow as
|
||||
its column — and `+ Item ▾` asks for the kind of the one it appends. Because the kind lives in the
|
||||
value, every such change is an ordinary **value edit**: one command, one undo step, and no
|
||||
neighbouring item touched. A **struct item** carries its own shape the same way: its fields are
|
||||
renamed, retyped, added (`+ Field ▾`) and removed on its own rows, which grows that one item and
|
||||
not the list. Three kinds need saying, because JSON cannot tell them from the value beside them: a
|
||||
**colour** is a number, and a **texture** or a **state machine** is an id, which is a text. Such a *tagged* kind wears its
|
||||
name as the one key of a small object – `{"$color": 16711680}`, `{"$texture": "walk-up"}` (the
|
||||
picker a sprite's `texture` property has – a whole picture, one frame of a sheet, or an animation),
|
||||
name as the one key of a small object — `{"$color": 16711680}`, `{"$texture": "walk-up"}` (the
|
||||
picker a sprite's `texture` property has — a whole picture, one frame of a sheet, or an animation),
|
||||
`{"$statemachine": "doors"}`.
|
||||
A field name cannot start with `$` (`isValidFieldName`), so a tag can never collide with a struct
|
||||
item of one field. Either reads as what it wraps on a data
|
||||
edge (`Item at`), the graph having neither colours nor pictures of its own, and `Set item` leaves
|
||||
the slot the kind it was. A **texture is a kind wherever a kind is picked** – a variable, a struct
|
||||
field, a list item, an Any port of `Properties`, a field of `Pack` – and the `Texture` node hands
|
||||
the slot the kind it was. A **texture is a kind wherever a kind is picked** — a variable, a struct
|
||||
field, a list item, an Any port of `Properties`, a field of `Pack` — and the `Texture` node hands
|
||||
one into the graph, as `Number` and `Text` do a number and a text; a **state machine** is the same
|
||||
kind of kind, with the `State machine` node. **Every kind is one row of one
|
||||
table** (`core/properties/kinds.ts`): its word and mark, what it travels as on an edge, its empty
|
||||
value, how a value is read as it, whether it is tagged and where it may be picked. The kind lists,
|
||||
the `+ …` menus, the tags, the port fields and the literal nodes all follow from the row, so a new
|
||||
kind is that row plus, if it names something in the project, its picker. Declared (a variable, a struct
|
||||
field of one, a port) it needs no tag: the declaration says what it is, and the value is the id –
|
||||
field of one, a port) it needs no tag: the declaration says what it is, and the value is the id —
|
||||
the way a colour field is a number. A declared port of kind colour or texture carries the field
|
||||
that edits it and travels as a number or a text, since the graph has neither; a port declared as
|
||||
the number a colour travels as still names the property (`isPortOf`). A **struct** variable is the other way round: few fields, so they fit under
|
||||
each other right in the inspector. Its head row renames it and adds fields (`+ Field`), each
|
||||
field row below carries its own name, its value and a remove – the port editor `Pack` already
|
||||
has, one level down and with a value column – and a field may itself be a **list**, which is
|
||||
field row below carries its own name, its value and a remove — the port editor `Pack` already
|
||||
has, one level down and with a value column — and a field may itself be a **list**, which is
|
||||
the one count button that opens the tab one level deeper. Adding, renaming or removing a field reaches the
|
||||
declaration, the behavior's own value **and every instance override** in one command, so no
|
||||
stored value keeps a shape its declaration no longer promises. A placed instance draws every
|
||||
variable, whatever its kind, with the very same rows (`buildVariable`), minus the renaming, the
|
||||
kind chooser and the `+ Field` / `×` buttons: it may override the values, not declare them. A
|
||||
kind chooser and the `+ Field` / `x` buttons: it may override the values, not declare them. A
|
||||
struct's ↺ / ↑ sit on its head row, because an override covers the whole bundle rather than a
|
||||
single field. All of it sits on **one column grid** with the rest of the inspector: mark (28 px),
|
||||
gap and name (90 px) are exactly as wide as a plain row's label (124 px), a nested field's name gives up what
|
||||
@@ -83,24 +83,24 @@ shown on the node and editable in the inspector.
|
||||
viewport). The `Owner` node returns it; `Self` returns the behavior itself; `Parent` walks up
|
||||
from any object.
|
||||
- **Auto-repeat on `On key down`**: two settings on the node, *Repeat after (ms)* and *Repeat
|
||||
interval (ms)* – milliseconds, so both stay whole numbers. A delay of 0 leaves the node as it was – one fire per press; with a delay set, a
|
||||
interval (ms)* — milliseconds, so both stay whole numbers. A delay of 0 leaves the node as it was — one fire per press; with a delay set, a
|
||||
key that stays held fires again after it and then every interval (0 = every frame). That is the
|
||||
sideways slide of a Tetris piece, or a held arrow walking a menu. The browser's own key repeat is
|
||||
deliberately not used: its delay and rate are an operating-system setting, not a game's. Both
|
||||
rows stay off the node body while the delay is 0, so a plain key node keeps its single line.
|
||||
- **Node set** (`src/core/graph/nodes/`, one file per node): events `On start`, `On update`, `On key down/up/held`,
|
||||
`On timer`, `On collision enter/stay/exit`, `On click` (the mouse pressed on the owner – anywhere when it has none), `On animation finished`, `Start` · flow `Branch`, `Sequence`, `Repeat`, `Run behavior`, `End` · data `Self`, `Owner`, `Parent`, `Object by name`, `Name of`,
|
||||
`Is asset` (is this object an instance of asset X – how you tell what you collided with),
|
||||
`On timer`, `On collision enter/stay/exit`, `On click` (the mouse pressed on the owner — anywhere when it has none), `On animation finished`, `Start` · flow `Branch`, `Sequence`, `Repeat`, `Run behavior`, `End` · data `Self`, `Owner`, `Parent`, `Object by name`, `Name of`,
|
||||
`Is asset` (is this object an instance of asset X — how you tell what you collided with),
|
||||
`Variables`, `Properties`, `Get position` (local and world), `Is enabled`, literals `Number`/`Text`/`Boolean`/`Texture`, `Add/Subtract/Multiply/Divide`,
|
||||
`Min/Max/Abs/Sqrt/Clamp`, `Floor/Ceil/Round`, `Modulo`, `Random`, `Time`, `To text`, `To number`, `Join text`,
|
||||
`Substring`, `Find text`, `Equals`,
|
||||
lists `List` (a constant), `Fill` (one value repeated – how an empty board is made), `Count`,
|
||||
lists `List` (a constant), `Fill` (one value repeated — how an empty board is made), `Count`,
|
||||
`Item at`, `Set item` (a *new* list, the one that came in is left alone), `Sub list`,
|
||||
`Join lists`, `Index of`, structs `Pack`/`Expand`,
|
||||
`Length`, `Distance`, `Normalize`, `Axis input` (four keys → a
|
||||
normalized direction, so diagonals are not faster), `Compare`, `And/Or/Not`, `Key is down`,
|
||||
`Mouse position`, `Mouse button`, `Is hovered`, `Is touching` ·
|
||||
actions `Move` (dx, dy, scale, dt – connect an event's dt for per-second movement), `Set position`, `Spawn`
|
||||
actions `Move` (dx, dy, scale, dt — connect an event's dt for per-second movement), `Set position`, `Spawn`
|
||||
(asset setting + x/y/z, optional parent → outputs the new object), `Destroy`, `Log`, `Stop game`, `Restart game`. A node type is one file exporting a `NodeDef`
|
||||
(ports, settings, `run`), listed in `nodes/index.ts`; palette, ports, validation and
|
||||
inspector fields are generated from it. A port may declare a `default` for its inline value
|
||||
@@ -110,7 +110,7 @@ shown on the node and editable in the inspector.
|
||||
settings and from the graph around it). `resolveDef(node, graph, lookup)` is what applies both,
|
||||
so the canvas and the inspector see a definition that fits *this* node; a node whose ports are
|
||||
derived reads them back off that definition, which is why `run` is handed it. `Sequence` keeps
|
||||
one free output below the last connected one – connecting the spare grows it, unplugging the
|
||||
one free output below the last connected one — connecting the spare grows it, unplugging the
|
||||
bottom edge shrinks it again (keys `a`, `b`, `c`, … up to 26). `Run behavior` takes its ports
|
||||
from the behavior it names, and a behavior's `Start`/`End` node declares them. Pointing a call
|
||||
at a different behavior **takes the edges of the ports that go away** and gives them back on
|
||||
@@ -118,71 +118,71 @@ shown on the node and editable in the inspector.
|
||||
would come back to life once the port returns.
|
||||
- **Structs**: several named values travelling as one, so a bundle needs one edge instead of one
|
||||
per part. `Pack` declares its fields as inputs, `Expand` reads them back out as outputs, a
|
||||
behavior port may be declared `struct` – which is how a bundle crosses a call – and a
|
||||
behavior port may be declared `struct` — which is how a bundle crosses a call — and a
|
||||
**variable** may be one, which is how a bundle is stored. A `Pack`/`Expand` can take its fields
|
||||
from a variable that already has the shape (**From variable**: a struct variable, or the first
|
||||
struct item a list holds – a list declares nothing, so its value is the only source) instead of
|
||||
struct item a list holds — a list declares nothing, so its value is the only source) instead of
|
||||
having them typed again; that adds the fields it does not
|
||||
have yet as one undo step and leaves the ones it has alone. Writing a struct variable keeps the
|
||||
fields it declares: what arrives fills the ones it names, the rest stay put (`mergeStruct`), so
|
||||
one node can set part of a bundle without erasing it – "the same piece, one row lower" is a
|
||||
one node can set part of a bundle without erasing it — "the same piece, one row lower" is a
|
||||
`Pack` of that one field. Depth is bounded by the *value*, not by convention: a list item is a primitive, a
|
||||
tagged colour, texture or state machine, or a struct of flat ones (`listItems.ts`), a struct field says what it holds
|
||||
(`StructFieldKind`, primitives plus `list`), and the list a *field* holds takes flat items only.
|
||||
Forbidding just that one combination is what keeps the shapes finite:
|
||||
`list → struct → primitive` and `struct → list → primitive` are the two deepest a stored value
|
||||
goes – exactly what `asStoredValue` lets through an untyped slot – so the deep compare and the
|
||||
goes — exactly what `asStoredValue` lets through an untyped slot — so the deep compare and the
|
||||
editor stay finite. The List tab **offers only the kinds its slot can hold** (`ListAccess.kinds`):
|
||||
on a list of its own all six, on a list a struct field holds the five flat ones. Offering the
|
||||
struct there would add an item the next write drops – which reads as the menu entry doing nothing. A live entity is
|
||||
struct there would add an item the next write drops — which reads as the menu entry doing nothing. A live entity is
|
||||
an object on a data edge too, so it carries a `Symbol` brand (`ENTITY_HANDLE`) that no struct
|
||||
field name can impersonate; nothing may hand a property value to `String()` (`textOf` instead),
|
||||
or a struct would read as `[object Object]`. A struct port has no inline value (its parts come
|
||||
from a `Pack`), and compatibility is per *kind*, not per shape – any struct fits any struct port.
|
||||
from a `Pack`), and compatibility is per *kind*, not per shape — any struct fits any struct port.
|
||||
- **Loops**: `Repeat` runs its `body` output a number of times, counting `index` up from 0, and
|
||||
continues at `done`. Data nodes are cached for the length of one event pulse, but a loop round
|
||||
drops that cache, so an expression built on `index` really changes with it. That is what makes
|
||||
a graph able to work through a list: the Tetris example draws its 200 board cells that way,
|
||||
`Object by name` taking the name the round put together (`"c" + index`). Two guards, not one:
|
||||
a pulse may take 100 000 steps (a loop legitimately runs long), but exec edges may only nest
|
||||
1000 deep – that is what catches a flow feeding back into itself before the call stack does.
|
||||
1000 deep — that is what catches a flow feeding back into itself before the call stack does.
|
||||
- **A behavior as a node** (nesting): a behavior that has a **Start** and an **End** node can be
|
||||
run as a step of another behavior's graph. Every input declared on the start node becomes an
|
||||
input port of the calling node, every output declared on the end node one of its outputs; the
|
||||
node is drawn under the behavior's own name, and the palette lists it there too, so a call is
|
||||
one pick. Declaring a port keeps both sides in step – renaming carries the typed-in value along,
|
||||
one pick. Declaring a port keeps both sides in step — renaming carries the typed-in value along,
|
||||
removing takes the edges hanging on it in *every* graph that calls the behavior. A call is a
|
||||
**call, not an inlining**: the nested graph runs in its own pulse cache with a fresh copy of its
|
||||
variable defaults, so values travel in and out through the ports and nothing else. That makes a
|
||||
call repeatable and keeps a nested behavior free of hidden state – for state, use a behavior
|
||||
call repeatable and keeps a nested behavior free of hidden state — for state, use a behavior
|
||||
object in the scene (see *Cross-object state* below). A behavior that would run itself raises an
|
||||
error instead of looping; there is one start and one end node per behavior.
|
||||
Three ways in and out of a call: **drag** a behavior out of the asset panel onto the graph and it
|
||||
lands as a call where you let go; **double-click** a call node to open the graph it runs. The
|
||||
way down stays above the graph as a **breadcrumb** (`Game › Move › Other`): click a crumb to go
|
||||
way down stays above the graph as a **breadcrumb** (`Game > Move > Other`): click a crumb to go
|
||||
back there; the first one returns to where you started and the bar goes away. Opening a behavior
|
||||
any other way – the asset panel, the scene, the picker – ends the trail. And a call that cannot
|
||||
do anything **says so on the node** – no behavior picked, the behavior deleted,
|
||||
any other way — the asset panel, the scene, the picker — ends the trail. And a call that cannot
|
||||
do anything **says so on the node** — no behavior picked, the behavior deleted,
|
||||
or no start node in it. The last one is the one worth naming: without a start node there is no
|
||||
way in, so the call is skipped and the flow simply carries on at its `exec` output. A missing
|
||||
*end* node is not a defect – a call is always synchronous, "done" is the nested flow running out,
|
||||
*end* node is not a defect — a call is always synchronous, "done" is the nested flow running out,
|
||||
and the end node only decides what comes back.
|
||||
- **Cross-object state**: a behavior's variables *are* the properties of its object, so another
|
||||
graph reads and writes them with `Object by name` → `Properties` of type *Behavior*. That is
|
||||
how the example game keeps one coin counter for the whole run: picking a coin up is one node,
|
||||
`coins` fed through `Add` back into itself.
|
||||
- **Mouse**: the Game tab feeds the pointer to the world in world units. `On click` fires for
|
||||
the behavior whose owner was hit – colliders decide when there are any, otherwise the sprites
|
||||
the behavior whose owner was hit — colliders decide when there are any, otherwise the sprites
|
||||
and texts of the owner and everything below it, so a button made of a box plus a label works
|
||||
as one. A switched-off screen cannot swallow clicks. A behavior **without an owner** has no
|
||||
shape that could be hit, so it takes every click inside the camera frame instead (the
|
||||
`world x`/`world y` outputs say where) – that is the "click anywhere" listener.
|
||||
`world x`/`world y` outputs say where) — that is the "click anywhere" listener.
|
||||
- **Idiomatic movement** (the demo's "Mover", five nodes): `On update → Move(Owner, dx = Axis
|
||||
input.x, dy = Axis input.y, scale = speed, dt = On update.dt)`, `speed` being a `Variables` node.
|
||||
- **Idiomatic platformer** (the example game's "Player"): `On update` adds `gravity · dt` to a
|
||||
`vy` variable and moves the owner by `(axis.x · speed, vy)` with *Collide with solids* on;
|
||||
`blocked y` while falling means standing, so `vy` goes back to 0 and `onGround` becomes true;
|
||||
`On key down (Space)` sets `vy = −jumpSpeed` when `onGround`.
|
||||
`On key down (Space)` sets `vy = -jumpSpeed` when `onGround`.
|
||||
- **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 selection, wheel zooms. Everything is
|
||||
@@ -190,12 +190,12 @@ shown on the node and editable in the inspector.
|
||||
- **Mouse and selection work as in the scene view.** The **middle button pans** from anywhere,
|
||||
a node under the pointer included (shift-drag on empty canvas does too); the left button never
|
||||
pans. Dragging on empty canvas pulls a **rubber band** instead, which selects every node it
|
||||
touches – nodes are large rectangles, so touching counts, unlike the scene view's test of an
|
||||
touches — nodes are large rectangles, so touching counts, unlike the scene view's test of an
|
||||
object's position. Ctrl/Shift-click adds or removes single nodes, dragging any node of a
|
||||
selection moves all of them, and `Delete` removes all of them – one undo step either way.
|
||||
selection moves all of them, and `Delete` removes all of them — one undo step either way.
|
||||
- **Dragging an edge tells you where it would land** before you let go: the port it would connect
|
||||
to wears a white ring and the line snaps to it. Dropping on a node's *body* rather than a port
|
||||
takes that node's first free compatible port – or, when none is free, the last compatible one,
|
||||
takes that node's first free compatible port — or, when none is free, the last compatible one,
|
||||
whose link is then replaced. A drop into empty space opens the palette filtered to the node
|
||||
types this edge could reach at all, and the picked node arrives already wired; node and link go
|
||||
in as **one** undo step (`core/graph/dropTarget.ts` decides all of this, the canvas only draws
|
||||
@@ -204,12 +204,12 @@ shown on the node and editable in the inspector.
|
||||
- **A link can be hidden**, so a connection across the whole graph does not have to be drawn as a
|
||||
line across the whole graph. Right-click a port → *Hide link*; the curve is replaced by a short
|
||||
stub at each end, ending in a dot captioned with the node at the *other* end. The connection is
|
||||
untouched – hiding is drawing only, and `folded` on the edge is the entire change. The curve
|
||||
untouched — hiding is drawing only, and `folded` on the edge is the entire change. The curve
|
||||
comes back, dimmed, **while either of its two nodes is selected**, and always while a pulse is
|
||||
travelling it, so the live trace never loses its thread. Clicking a dot selects the node at the
|
||||
far end, which both answers "where does this go?" and lends the curve back;
|
||||
right-clicking it offers *Show link* and *Disconnect* (`editor/graph/folding.ts` holds the
|
||||
visibility rule, the canvas only draws it). **A port's hidden links share one mark** – drawn per
|
||||
visibility rule, the canvas only draws it). **A port's hidden links share one mark** — drawn per
|
||||
link, several of them would stack stub, dot and caption on the very same pixels. The mark then
|
||||
grows a little, is captioned `Add +2`, and one click selects *every* far end at once, which is
|
||||
also what lends all of its curves back; the menu counts what it acts on (*Show 3 links*), and
|
||||
@@ -219,7 +219,7 @@ shown on the node and editable in the inspector.
|
||||
acts on the whole selection as one undo step. Every link of one side then meets at a single
|
||||
point on the title bar, and its hidden links share one mark per side. It narrows to its title
|
||||
(never wider than an open node, where a long title is cut). A collapsed node has no
|
||||
ports to grab, so a link dropped on it goes nowhere – expand it to wire it. Like hiding a link,
|
||||
ports to grab, so a link dropped on it goes nowhere — expand it to wire it. Like hiding a link,
|
||||
this is drawing only: `collapsed` on the node is saved with the project. A collapsed
|
||||
`Variables` or `Properties` node, titled just that while open, is titled by what it does, since
|
||||
its ports are out of sight:
|
||||
@@ -227,9 +227,9 @@ shown on the node and editable in the inspector.
|
||||
only read, else *Set speed* / *Get speed*, and *No variables* when it has none.
|
||||
- **Header bar** (the same one the Texture and Container tabs have): the behavior picker,
|
||||
**Fit** (which zooms out far enough to hold every node, measuring each by its own drawn
|
||||
height – `editor/framing.ts`), and the size of the graph as "N nodes · M links".
|
||||
height — `editor/framing.ts`), and the size of the graph as "N nodes · M links".
|
||||
- **Snap** (header checkbox, off by default): a drag drops each node it carries on the nearest
|
||||
24 px grid line – the same grid the background draws. It takes effect on the next drag only, so
|
||||
24 px grid line — the same grid the background draws. It takes effect on the next drag only, so
|
||||
switching it on never shuffles a graph that is already laid out, and a node nobody drags never
|
||||
moves. Every node of a selection snaps **for itself**, so dragging a group is how you line one
|
||||
up; the offset is a decision about rectangles and lives with the others in `editor/framing.ts`.
|
||||
@@ -237,9 +237,9 @@ shown on the node and editable in the inspector.
|
||||
or one of its nodes shows that graph; a selection about anything else (a sprite, nothing at
|
||||
all) leaves the tab on the behavior you were editing, because checking a sprite's position
|
||||
must not empty your work. Only a behavior that no longer exists is dropped, and then the tab
|
||||
falls back to another one – the picker is how you switch on purpose.
|
||||
falls back to another one — the picker is how you switch on purpose.
|
||||
- **Live view**: Play always switches to the Game tab; switch to Behavior while playing and
|
||||
the exec edges and nodes that just ran light up – the same canvas draws editor and game, so
|
||||
the exec edges and nodes that just ran light up — the same canvas draws editor and game, so
|
||||
this costs nothing extra. Each fire of an exec edge also sends a bead along it (0.3 s, at most
|
||||
one per 0.15 s and edge): a key press is a single bead, `On update` an even chain. Beads run
|
||||
on game time, so a paused game holds them where they are. Every data output shows, in the same colour, the value it last
|
||||
|
||||
+4
-4
@@ -1,5 +1,5 @@
|
||||
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
|
||||
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** → *Add
|
||||
collider* 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
|
||||
@@ -11,19 +11,19 @@ every frame it tests all collider pairs (world positions applied; touching edges
|
||||
and sorts them into *enter*, *stay* and *exit*. Detection never pushes anything apart. 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
|
||||
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.
|
||||
|
||||
**Blocking** is opt-in per movement: the `Move` node has a **Collide with solids** setting
|
||||
(default off). When enabled it moves an object one axis at a time and, after each axis, pushes
|
||||
its *solid* colliders back to just touch any solid collider they ran into – so the other axis
|
||||
its *solid* colliders back to just touch any solid collider they ran into — so the other axis
|
||||
still moves and the object slides along walls. Its `blocked x` / `blocked y` outputs tell which
|
||||
axes hit (always `false` with the setting off). A collider with
|
||||
**Trigger (no blocking)** enabled (`isTrigger`, drawn amber instead of green) still produces
|
||||
collision events but never blocks. An overlap that already exists is never resolved (no jolt
|
||||
when objects start stacked or inside a wall) but also never deepened, so such an object can
|
||||
always leave. A move longer than the thinnest collider involved is split into sub-steps – a
|
||||
always leave. A move longer than the thinnest collider involved is split into sub-steps — a
|
||||
fast object cannot jump across a thin wall and end up on the other side. Colliders of the
|
||||
moving object and of anything parented under it travel with it and cannot
|
||||
block it. `Move` without the setting and `Set position` ignore colliders entirely.
|
||||
+20
-20
@@ -3,25 +3,25 @@
|
||||
| **Asset** | Named thing with a type (`scene`, `camera`, `sprite`, `text`, `behavior`, `collider`, `container`, `texture`, `animation`, `statemachine`) and typed properties. Assets have **no position**. |
|
||||
| **Texture** | An uploaded image. Scenes, textures, animations and state machines are the four types that cannot be placed: a texture has no position, it is *worn* by a sprite through the ordinary property `texture`. See [Textures](Textures). |
|
||||
| **State machine** | A named set of **states**, edited as nodes in its own tab. A behavior puts it into a state with `Set state` and branches on it with `State branch`; each behavior instance keeps its own current state. See [State machines](State-machines). |
|
||||
| **Text** | An asset that draws a line of text (`text`, `fontSize`, `color`, `align`) – titles, HUD readouts, button labels. Its content is an ordinary property, so a `Properties` node of type Text with `text` switched to Set changes it while playing. |
|
||||
| **Enabled** | Every placeable asset has the switch `enabled` (default on). A disabled object is not drawn, does not collide and its behavior does not run – **and neither does anything parented below it**. Switching one instance off is a normal override, which makes a container a *screen*: switch it on to show it, off to hide it. From a graph, `Properties` with `enabled` switched to Set does it, and `Is enabled` asks (including the parents). |
|
||||
| **Text** | An asset that draws a line of text (`text`, `fontSize`, `color`, `align`) — titles, HUD readouts, button labels. Its content is an ordinary property, so a `Properties` node of type Text with `text` switched to Set changes it while playing. |
|
||||
| **Enabled** | Every placeable asset has the switch `enabled` (default on). A disabled object is not drawn, does not collide and its behavior does not run — **and neither does anything parented below it**. Switching one instance off is a normal override, which makes a container a *screen*: switch it on to show it, off to hide it. From a graph, `Properties` with `enabled` switched to Set does it, and `Is enabled` asks (including the parents). |
|
||||
| **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. |
|
||||
| **Container** | A reusable group ("prefab"): the asset holds a **template** of objects (asset link, local position, overrides, parent links) centred on its origin. Placing a container stores the container object **and nothing else** – its content is resolved from the template on every read, so a template edit reaches every instance at once. **Create container** turns the current selection into such an asset: the selected objects move into the template and the scene keeps one placed container at their centre (world positions unchanged). The template is edited in the **Container** tab (see [Containers](Containers)) or from an instance through **Apply to container**. |
|
||||
| **Container** | A reusable group ("prefab"): the asset holds a **template** of objects (asset link, local position, overrides, parent links) centred on its origin. Placing a container stores the container object **and nothing else** — its content is resolved from the template on every read, so a template edit reaches every instance at once. **Create container** turns the current selection into such an asset: the selected objects move into the template and the scene keeps one placed container at their centre (world positions unchanged). The template is edited in the **Container** tab (see [Containers](Containers)) or from an instance through **Apply to container**. |
|
||||
| **Selection** | One or more scene objects. Click selects, `Ctrl`/`Shift`-click adds/removes, dragging on empty scene space draws a rubber band (objects whose position lies inside are selected). Dragging a selected object moves the whole selection; `Delete` removes it; the inspector offers **Create container** and **Remove** for several objects. |
|
||||
| **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. |
|
||||
| **Run in editor** | A switch only behaviors have (default off). With it on the behavior keeps running while the project is **not** playing – and unlike Play it then edits the *saved* model: what it moves or writes is a real change, autosaved and not on the undo stack. See *Running without Play*. |
|
||||
| **Run in editor** | A switch only behaviors have (default off). With it on the behavior keeps running while the project is **not** playing — and unlike Play it then edits the *saved* model: what it moves or writes is a real change, autosaved and not on the undo stack. See *Running without Play*. |
|
||||
| **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 paints their label gold and offers a reset (↺) and an **apply** (↑) that writes the value into the asset instead. Both appear only where the value really differs from its base – an override carrying the asset's own value is none. |
|
||||
| **Default** | Every property has a schema default. Asset values that differ from it are marked gold in the inspector and can be reset (↺) the same way an override can. The asset's name in the **Assets** panel carries the same gold while any of them does (`assetDeviates`, `src/core/properties/deviation.ts` – the inspector marks the rows of that same schema, so the list cannot claim a deviation the inspector then fails to show). A behavior's variables are not in it: their default is declared on the variable itself, so the asset value cannot differ from it. |
|
||||
| **Instance** | An object resolved from a placed container's template. Its **values** can be edited – position, name, property overrides – and only what actually deviates is stored, as a sparse `instanceEdits` entry on the container object keyed by the template path. Everything else keeps following the template. Its **structure** cannot: it can not be removed, re-parented or given a collider, and nothing of the scene can be parented into it. ↺ ▾ resets to the nearest base, which inside a container is the **template as the containers in between leave it**; ↑ ▾ offers **Apply to container** (write the value into the template, where every other instance still at the old value picks it up) and/or **Apply to asset** (which drops the template's override as well). With one base left the menu collapses to a plain ↺ / ↑, with none the buttons disappear. Gold marks the same thing the buttons offer. |
|
||||
| **Override** | A per-object value that replaces the asset's value for one property. Only changed keys are stored; the inspector paints their label gold and offers a reset (↺) and an **apply** (↑) that writes the value into the asset instead. Both appear only where the value really differs from its base — an override carrying the asset's own value is none. |
|
||||
| **Default** | Every property has a schema default. Asset values that differ from it are marked gold in the inspector and can be reset (↺) the same way an override can. The asset's name in the **Assets** panel carries the same gold while any of them does (`assetDeviates`, `src/core/properties/deviation.ts` — the inspector marks the rows of that same schema, so the list cannot claim a deviation the inspector then fails to show). A behavior's variables are not in it: their default is declared on the variable itself, so the asset value cannot differ from it. |
|
||||
| **Instance** | An object resolved from a placed container's template. Its **values** can be edited — position, name, property overrides — and only what actually deviates is stored, as a sparse `instanceEdits` entry on the container object keyed by the template path. Everything else keeps following the template. Its **structure** cannot: it can not be removed, re-parented or given a collider, and nothing of the scene can be parented into it. ↺ ▾ resets to the nearest base, which inside a container is the **template as the containers in between leave it**; ↑ ▾ offers **Apply to container** (write the value into the template, where every other instance still at the old value picks it up) and/or **Apply to asset** (which drops the template's override as well). With one base left the menu collapses to a plain ↺ / ↑, with none the buttons disappear. Gold marks the same thing the buttons offer. |
|
||||
| **Unpack** | Writes a placed container's content into the scene as ordinary objects (hierarchy context menu, or the inspector's *Content* section). The ids survive it, so the selection does; from then on the structure is editable and the objects no longer follow the template. |
|
||||
| **Edit mode** | The Scene tab draws the project model; panels, grid, gizmos and drag-to-move are active. |
|
||||
| **Play mode** | The editor switches to a **runtime copy** of the project (same ids). A `World` is instantiated from its active scene (one entity per scene object, resolved properties as mutable state) and shown in the **Game** tab. Systems tick it and mirror their changes into the copy; every panel stays open and edits the copy, which the world picks up immediately. **Stop discards world and copy** – the saved model is never touched. |
|
||||
| **Play mode** | The editor switches to a **runtime copy** of the project (same ids). A `World` is instantiated from its active scene (one entity per scene object, resolved properties as mutable state) and shown in the **Game** tab. Systems tick it and mirror their changes into the copy; every panel stays open and edits the copy, which the world picks up immediately. **Stop discards world and copy** — the saved model is never touched. |
|
||||
|
||||
**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`. The Scene
|
||||
tab draws that frame as an outline. The Game tab renders only that frame – fitted into the
|
||||
view with black letterboxing – and objects outside it are culled. Of several cameras the first
|
||||
**The camera defines what is rendered.** A camera asset has a view size (`width` x `height`,
|
||||
default 640x360) and a `zoom`; the frame it covers in world units is `size / zoom`. The Scene
|
||||
tab draws that frame as an outline. The Game tab renders only that frame — fitted into the
|
||||
view with black letterboxing — and objects outside it are culled. Of several cameras the first
|
||||
one in hierarchy order that is **switched on** (`enabled`, its parents included) is the active
|
||||
one, so switching it off hands the view to the next; with none on the tab stays empty and says
|
||||
*No active camera*. Without playing the tab previews the scene through that same camera, so it
|
||||
@@ -38,7 +38,7 @@ from texture* and *Delete…*. In
|
||||
container*, *Unparent*, *Expand all* / *Collapse all* and *Remove*.
|
||||
|
||||
**The console.** Under whichever centre tab is showing sits the **Console** panel, always
|
||||
visible and resized by the divider above it – dragged all the way shut it keeps its own header
|
||||
visible and resized by the divider above it — dragged all the way shut it keeps its own header
|
||||
to be dragged open by. It mirrors everything that reaches the browser console: a `Log` node's
|
||||
message, Pixi's warnings, an uncaught error, and the editor's own notices (a failure in red, a
|
||||
hint in plain text; the *Playing · …* status line is a mode rather than an event and stays out).
|
||||
@@ -51,13 +51,13 @@ Scene tab shows the live positions (symbols, gizmos and dragging included), the
|
||||
inspector show and edit the running objects, the graph can be changed, undo/redo work on the
|
||||
play session's own history. All of it happens in the runtime copy: values a behavior changes
|
||||
appear as overrides (gold, ↺ resets to the asset value), objects a graph destroys vanish from
|
||||
the lists, objects a graph spawns (`Spawn` node) appear in them – selectable, editable, still
|
||||
only in the runtime copy –, and Esc/Stop brings back exactly what was saved. Autosave pauses
|
||||
the lists, objects a graph spawns (`Spawn` node) appear in them — selectable, editable, still
|
||||
only in the runtime copy —, and Esc/Stop brings back exactly what was saved. Autosave pauses
|
||||
while playing.
|
||||
|
||||
**Pausing.** The toolbar's *Pause* button (or `Ctrl+Enter`) holds the running world: it is no
|
||||
longer ticked, so behaviors, timers and collisions stand still and the world clock stops with
|
||||
them. It is a sub-state of play mode, not a third mode – the runtime copy, the panels and the
|
||||
them. It is a sub-state of play mode, not a third mode — the runtime copy, the panels and the
|
||||
keyboard behave exactly as while playing, and everything the editor can do during play it can
|
||||
do during a hold. Keys and clicks that arrive while it is held are **dropped** rather than
|
||||
queued, so resuming does not fire a frame's worth of input at once. Stop clears the hold, so a
|
||||
@@ -67,24 +67,24 @@ paused game can never outlive its run.
|
||||
project is not playing (`src/editor/EditorRun.ts`, ticked by the Editor next to the play session).
|
||||
The switch is an ordinary property, so it can be set on the behavior asset and overridden per
|
||||
object. What it runs is a `World` over the **saved** model, and that is the whole difference to
|
||||
Play: a position or variable it changes is a real edit – it is autosaved, the panels show it, and
|
||||
Play: a position or variable it changes is a real edit — it is autosaved, the panels show it, and
|
||||
**undo does not take it back**. Which is why the switch is off by default and why it belongs on
|
||||
behaviors that arrange or build content rather than on ones that simulate.
|
||||
|
||||
**Input goes to the tab you are looking at.** While the **Scene** tab is on screen the keyboard and
|
||||
the mouse are the editor's – typing a name or walking the hierarchy with the arrow keys must not
|
||||
the mouse are the editor's — typing a name or walking the hierarchy with the arrow keys must not
|
||||
steer a game at the same time. In the **Game** tab, where no key means anything to the editor, they
|
||||
reach the run instead: `On key …`, `Key is down` and `On click` work there, and the tab says so in
|
||||
its top-left corner. Leaving the tab (or focusing a field in it) hands the keyboard back and lets
|
||||
every held key go, so nothing keeps walking with nobody pressing anything. What the run does *not*
|
||||
get:
|
||||
|
||||
- **structural changes to the content of a placed container** – such content is resolved from the
|
||||
- **structural changes to the content of a placed container** — such content is resolved from the
|
||||
template, not stored (see [Containers](Containers)), and the editor run must not bake it out, since that is
|
||||
itself an edit. Its behaviors do run, and what they move or set is kept as an `instanceEdits`
|
||||
entry on the instance; but a `Destroy` of resolved content is dropped from the running world
|
||||
without being saved, and comes back the next time the world is rebuilt.
|
||||
- **a second life** – only the behaviors of the **loaded** scene run, and only in edit mode; Play
|
||||
- **a second life** — only the behaviors of the **loaded** scene run, and only in edit mode; Play
|
||||
stops the editor run, since that world runs every behavior anyway. An exception in a graph, or a
|
||||
`Stop game` node, ends the run and puts the reason in the notice line instead of repeating it
|
||||
every frame.
|
||||
+10
-10
@@ -1,8 +1,8 @@
|
||||
The **Container tab** shows one container's template with the *same* viewport the Scene tab uses –
|
||||
The **Container tab** shows one container's template with the *same* viewport the Scene tab uses —
|
||||
`ContainerCanvas` subclasses `SceneViewport` and swaps nothing but which asset holds the objects,
|
||||
so picking, rubber band, dragging, gizmos, labels and parent lines are not written twice. It adds a
|
||||
cross at the **origin**, because that is the point the container is held by when placed: where the
|
||||
content sits relative to it is the thing being authored. A template has no background of its own –
|
||||
content sits relative to it is the thing being authored. A template has no background of its own —
|
||||
painting one would invent the scene it is not in.
|
||||
|
||||
The **Scene objects** panel lists whatever the visible tab edits, so in the Container tab it lists
|
||||
@@ -15,7 +15,7 @@ a scene asset and a container asset are both `ObjectHolder`s, and every object c
|
||||
included) and the size of the template as "N objects". Placing needs no button of its own: an
|
||||
asset dragged from the panel onto this view goes into the template, and a container dragged onto
|
||||
its own template is refused while it hovers rather than at the drop.
|
||||
Like the Behavior tab it follows the selection without being bound to it – the container asset, one of
|
||||
Like the Behavior tab it follows the selection without being bound to it — the container asset, one of
|
||||
its placed copies or one of its template objects shows it, anything else leaves the tab on what you
|
||||
were editing, and only a deleted container makes it fall back to another one. Selecting a template
|
||||
*object* also switches to the tab, the rule a graph node and a texture frame already follow: it
|
||||
@@ -23,10 +23,10 @@ exists in no other view. Selecting the asset itself does not.
|
||||
|
||||
Placing a container anywhere creates **one** object. Its content is never copied: `resolveObjects`
|
||||
(`src/core/model/instance.ts`) weaves the template in whenever the holder is read, one level per
|
||||
nesting step, and the resolved objects get derived ids – `<instance id>~<template id>`, joined by
|
||||
nesting step, and the resolved objects get derived ids — `<instance id>~<template id>`, joined by
|
||||
`/` for a container inside a container. That is what the hierarchy lists, what the viewport draws
|
||||
and what a command addresses. A container that reaches this one, directly or through another, is
|
||||
still refused – that cycle would resolve forever.
|
||||
still refused — that cycle would resolve forever.
|
||||
|
||||
So **editing a template changes every instance**, structure included: add, delete or re-parent a
|
||||
template object and every placed container shows it immediately. What an instance may deviate in
|
||||
@@ -35,13 +35,13 @@ is values, and only per slot: an override key, one position axis, the name. Thos
|
||||
base, which is what keeps "untouched follows, touched keeps" exact without any bookkeeping. That
|
||||
base is the template **with the edits of every container in between woven in** (`InstanceTarget.base`):
|
||||
inside nested containers the innermost template is not what a value falls back to, so comparing
|
||||
against it would drop an edit that does deviate – and the value would jump to the value of the
|
||||
against it would drop an edit that does deviate — and the value would jump to the value of the
|
||||
container in between.
|
||||
|
||||
The other direction is therefore just as small: **Apply to container** writes the value into the
|
||||
template and drops the edit, **Reset to container** drops the edit alone.
|
||||
|
||||
Two places do spell the content out. **Unpack** does it on purpose – the objects become ordinary
|
||||
Two places do spell the content out. **Unpack** does it on purpose — the objects become ordinary
|
||||
ones and stop following. And **play mode bakes** every placed container into stored objects, in
|
||||
the throwaway copy of the model, because a running behavior may destroy one of them and a resolved
|
||||
object has nowhere to remember that. Both keep the derived ids, so the selection and a check script
|
||||
@@ -54,12 +54,12 @@ has nowhere to go there.
|
||||
A **screen** (title, win, lose, a pause overlay) is just a container whose `enabled` is
|
||||
switched on and off. It keeps working while it is off in one important way: `Object by name`
|
||||
still finds it, so a graph can switch it back on. A behavior inside a disabled screen has no
|
||||
interpreter yet – it gets one, and its `On start`, the moment the screen is switched on, which
|
||||
interpreter yet — it gets one, and its `On start`, the moment the screen is switched on, which
|
||||
makes "the screen initialises itself when shown" the default. Its variables survive being
|
||||
switched off and on again. A switched-off object is hidden in the Scene tab as well, so the
|
||||
editor shows what the game shows – selected, it keeps a faint outline, so where it sits is still
|
||||
editor shows what the game shows — selected, it keeps a faint outline, so where it sits is still
|
||||
visible, but it cannot be dragged. It stays listed in the hierarchy panel, which is where it is
|
||||
selected and switched back on.
|
||||
|
||||
`Restart game` throws the run away and starts the scene again from the saved model – what the
|
||||
`Restart game` throws the run away and starts the scene again from the saved model — what the
|
||||
"Play again" buttons of the example game do.
|
||||
+28
-28
@@ -2,7 +2,7 @@ Node 22 is required (`/usr/bin/node`, in the default PATH).
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run dev # http://localhost:5175/game-maker/dist/ – API calls are proxied to production
|
||||
npm run dev # http://localhost:5175/game-maker/dist/ — API calls are proxied to production
|
||||
npm test # unit tests (core, editor, ui, viewport)
|
||||
npm run typecheck
|
||||
npm run lint # ESLint (type-aware for src and tests); lint:fix for the fixable half
|
||||
@@ -13,7 +13,7 @@ npm run check # typecheck + lint + format:check + tests, the gate CI runs
|
||||
npm run build # writes dist/ and regenerates index.html
|
||||
./build.sh # the same under `set -o pipefail`, for callers that pipe the output
|
||||
node tools/seed-e2e.mjs && node tools/e2e.mjs # 671 editor checks, screenshots in /tmp/gm-e2e-*.png
|
||||
GM_STEPS=20 node tools/e2e.mjs # or 1-12, or 6,20 – a subset while working on one step
|
||||
GM_STEPS=20 node tools/e2e.mjs # or 1-12, or 6,20 — a subset while working on one step
|
||||
./e2e.sh start [GM_STEPS] # the same run detached, see below
|
||||
./e2e.sh status # live progress, or the last run's result
|
||||
node tools/seed-jumprun.mjs && node tools/smoke-jumprun.mjs # 24 checks: the example game, played end to end
|
||||
@@ -25,19 +25,19 @@ node tools/seed-actor.mjs && node tools/smoke-actor.mjs # 20 checks: the w
|
||||
in software that is ruinous: measured here on an idle page over 20 s, software WebGL costs **373% of
|
||||
a core at 8 fps**, the same page on the Intel HD 630 via ANGLE/Vulkan costs **69% at 56 fps**. The
|
||||
full suite takes about four minutes that way. `tools/lib/browser.mjs` therefore always asks for the GPU and has
|
||||
**no software fallback** – on a host without one, chromium should fail loudly rather than melt the
|
||||
**no software fallback** — on a host without one, chromium should fail loudly rather than melt the
|
||||
machine down. (`--use-angle=gl` and `--use-gl=egl` measure cheaper still, but get no WebGL context at
|
||||
all, so the app never renders.)
|
||||
|
||||
**Leave the full run to CI.** Every push runs the whole proof on the runner (see *Continuous
|
||||
integration*), so locally take only what the change needs: the gate, plus the step that covers it –
|
||||
integration*), so locally take only what the change needs: the gate, plus the step that covers it —
|
||||
`GM_STEPS` keeps that to seconds. The whole suite works here too and costs about four minutes and
|
||||
roughly 0.8 of the four cores of this box (60% renderer, 18% GPU process, load ~1.0), but four
|
||||
minutes of waiting buys nothing the push does not give.
|
||||
|
||||
**`./e2e.sh start` runs the suite detached**, in a transient `game-maker-e2e.service`, and returns at
|
||||
once; output goes to `logs/e2e-<date>_<time>.log` (gitignored) and ends in `=== E2E EXIT n ===`.
|
||||
Backgrounding it from a shell instead is not enough – four runs in a row died mid-suite with no
|
||||
Backgrounding it from a shell instead is not enough — four runs in a row died mid-suite with no
|
||||
closing line, because the whole process group is killed when the calling shell exits (`nohup` only
|
||||
covers SIGHUP, and `/proc/vmstat` confirmed `oom_kill 0`, so it was never memory). The unit outlives
|
||||
its caller. It takes the same `GM_STEPS` argument, refuses to start a second run over a live one, and
|
||||
@@ -50,24 +50,24 @@ reported as `ABORTED`, not as its PASS count: a run that was cut short is not a
|
||||
**Prettier owns the formatting** (110 columns, single quotes, `.prettierrc.json`); Markdown is
|
||||
excluded because it pads table cells to equal width. ESLint therefore only ever means "this is a
|
||||
mistake", never "this looks different": `eslint-config-prettier` switches every stylistic rule
|
||||
off, and what is left is type-aware – unnecessary conditions, floating promises, duplicate
|
||||
off, and what is left is type-aware — unnecessary conditions, floating promises, duplicate
|
||||
imports, base-to-string. Where a rule fires on something deliberate, the disable comment states
|
||||
why (the `Log` node's `console.log`, a guard against untyped test hooks).
|
||||
|
||||
Each script writes to its own project (`tools/lib/projects.mjs`): the **fixture** project is
|
||||
rewritten by every e2e run, the **jump-run** and **tetris** projects by their seeders. The
|
||||
**playground** project is the author's own and is maintained in the editor – every tool refuses
|
||||
**playground** project is the author's own and is maintained in the editor — every tool refuses
|
||||
to touch it.
|
||||
|
||||
The e2e run is a list of steps in `tools/e2e/` (hierarchy, inspector, play, assets, drawing,
|
||||
graph, parenting, colliders, containers, text/enabled, menus, textures, frames, overflow, aspect,
|
||||
container tab, run-in-editor, pause, nested behaviors, key repeat, list tab, struct variable,
|
||||
variable nodes, property nodes, animations, state machines, console) that build on each other's
|
||||
scene state and share the CDP driver in `tools/lib/harness.mjs` – `press`, `undo`, `play`,
|
||||
scene state and share the CDP driver in `tools/lib/harness.mjs` — `press`, `undo`, `play`,
|
||||
`object(id)`, `world()`, `mutate(js)` and friends, so a check reads like the interaction it
|
||||
exercises.
|
||||
|
||||
`GM_STEPS=20`, `GM_STEPS=1-12` or `GM_STEPS=6,20` runs a subset – seconds instead of minutes
|
||||
`GM_STEPS=20`, `GM_STEPS=1-12` or `GM_STEPS=6,20` runs a subset — seconds instead of minutes
|
||||
while one step is being written. Because a step lives off what the earlier ones left behind, a
|
||||
subset that skips a prerequisite fails on purpose; take `1-N` when in doubt.
|
||||
|
||||
@@ -75,22 +75,22 @@ subset that skips a prerequisite fails on purpose; take `1-N` when in doubt.
|
||||
platformer built only from assets, containers and graphs: a title screen with a clickable Start
|
||||
button, a level of boxes with ten coins, gravity and jumping, a 30-second clock in the HUD, and
|
||||
a win / lose screen with "Play again". A coin is a container of its own (sprite plus trigger
|
||||
collider) placed ten times inside the level container – nested containers resolve through both levels,
|
||||
collider) placed ten times inside the level container — nested containers resolve through both levels,
|
||||
and picking one up destroys the container, not just the sprite the collider reported. `tools/smoke-jumprun.mjs` plays it in a headless browser
|
||||
(start, jump, collect, win, restart, lose) – it is the regression test for everything screens,
|
||||
(start, jump, collect, win, restart, lose) — it is the regression test for everything screens,
|
||||
text, mouse and platformer physics need.
|
||||
|
||||
**The second example** (`tools/seed-tetris.mjs`, project *Tetris*) is the same idea with data
|
||||
instead of physics. The board is a **list of 200 numbers**, one per cell – `-1` for empty,
|
||||
instead of physics. The board is a **list of 200 numbers**, one per cell — `-1` for empty,
|
||||
otherwise the shape that came to rest there. A cell is `Item at(grid, row · 10 + column)`, a
|
||||
landing is `Set item`, and clearing a full line is an empty row joined in front of everything
|
||||
above and below it. What the player sees is a pool of objects the graphs address by name –
|
||||
`c0…c199` for the well, `p0…p3` for the piece that is falling – so nothing is spawned or
|
||||
above and below it. What the player sees is a pool of objects the graphs address by name —
|
||||
`c0…c199` for the well, `p0…p3` for the piece that is falling — so nothing is spawned or
|
||||
destroyed while playing. Every frame tries exactly **one** move (turning beats moving beats
|
||||
falling), which is why the game needs only a single "does it fit?" test instead of one per key.
|
||||
|
||||
What is *not* loose numbers is the piece. `piece` and the candidate `next` are **structs** of
|
||||
shape/rot/x/y, and the shape table is a **list of `{x, y}` cells** – so `Next move` hands back a
|
||||
shape/rot/x/y, and the shape table is a **list of `{x, y}` cells** — so `Next move` hands back a
|
||||
whole candidate for `Fits` to judge and the master to accept in one write, rather than three
|
||||
parallel variables copied back and forth, and each strand packs only the field it changes
|
||||
(`mergeStruct` keeps the rest). Reading a cell is `((shape · 4 + rot) · 4 + corner)` into that
|
||||
@@ -99,38 +99,38 @@ list instead of the same index doubled and read twice.
|
||||
Where a step needs several variables at once, one `Variables` node stands for the
|
||||
lot: the call to `Next move` has **one node at each side** of it rather than five, and the master
|
||||
graph is **44 nodes / 64 edges** where the same game in single nodes was 69 / 76. Inside `Next
|
||||
move` each strand writes its whole answer – the candidate, what kind of move it was and the wish
|
||||
it used up – in one node rather than three to five, which is **30 nodes** against 40. A writing
|
||||
move` each strand writes its whole answer — the candidate, what kind of move it was and the wish
|
||||
it used up — in one node rather than three to five, which is **30 nodes** against 40. A writing
|
||||
node reads all its inputs before it writes any, so an input fed from a variable the same node
|
||||
writes sees the value from before.
|
||||
|
||||
It is **ten behaviors**, not one graph. `Tetris` owns the state and does none of the work: each
|
||||
step of a frame is a behavior of its own, called as a node – `Spawn piece`, `Next move`, `Fits`,
|
||||
step of a frame is a behavior of its own, called as a node — `Spawn piece`, `Next move`, `Fits`,
|
||||
`Draw piece`, `Lock piece`, `Clear lines`, `Show lines`, `Draw board`, with `Piece cell` (where
|
||||
one of the four cells of a piece sits) used by two of them. None of them holds state: what they
|
||||
need arrives through their inputs and every answer leaves through their outputs, which is what
|
||||
lets `Fits` answer for turning, moving and falling alike, and `Piece cell` serve the fit test
|
||||
and the drawing instead of being the same twenty nodes twice. What is left in
|
||||
the master graph is the order the steps run in and which variable each answer lands in – it holds
|
||||
the master graph is the order the steps run in and which variable each answer lands in — it holds
|
||||
no loop and no list access of its own, which `tools/smoke-tetris.mjs` checks. That script plays
|
||||
the game in a headless browser (fall, move, turn, drop, land, clear a line, fill the well) – it
|
||||
the game in a headless browser (fall, move, turn, drop, land, clear a line, fill the well) — it
|
||||
is the regression test for loops, lists and behaviors used as nodes.
|
||||
|
||||
**The walk demo** (`tools/seed-actor.mjs`, project *Actor demo*) is the smallest of the three and the
|
||||
only one that ships its own picture: it uploads `tools/assets/Actor1.png` and slices the 576×384
|
||||
sheet into the 96 cells of a 12×8 grid. That is the usual RPG-Maker layout – eight characters, each
|
||||
three columns (left foot / standing / right foot) by four rows (down, left, right, up) – and the demo
|
||||
only one that ships its own picture: it uploads `tools/assets/Actor1.png` and slices the 576x384
|
||||
sheet into the 96 cells of a 12x8 grid. That is the usual RPG-Maker layout — eight characters, each
|
||||
three columns (left foot / standing / right foot) by four rows (down, left, right, up) — and the demo
|
||||
wears the one in the top left corner. Walking is four **ping-pong** animations of three frames, which
|
||||
is exactly the original walk cycle; standing is no animation at all but the middle frame of the row.
|
||||
The graph picks what the sprite wears the way the editor does: two **list variables** hold the four
|
||||
walk animations and the four standing frames, every item chosen with the texture picker, and
|
||||
`facing` is the row of the sheet, which indexes both (`Item at`). No id is spelled anywhere in it,
|
||||
so the whole graph can be rebuilt by hand. Which way it looks is the key pressed last, corrected whenever only one axis is held – otherwise letting go of one key of a
|
||||
so the whole graph can be rebuilt by hand. Which way it looks is the key pressed last, corrected whenever only one axis is held — otherwise letting go of one key of a
|
||||
diagonal would leave it walking backwards. It is the worked example for animations, for a seed that
|
||||
brings its own texture, and for `Axis input`, whose vector is already normalized so a diagonal is not
|
||||
faster than a straight line. `tools/smoke-actor.mjs` walks it in a headless browser; that the
|
||||
animation really advances it reads off the **drawn pixels**, standing at the edge of the camera where
|
||||
he walks on the spot – a clock that ticks without the picture following would pass otherwise.
|
||||
he walks on the spot — a clock that ticks without the picture following would pass otherwise.
|
||||
|
||||
The workspace folder itself is what nginx serves, so `npm run build` is the deployment.
|
||||
|
||||
@@ -139,14 +139,14 @@ The workspace folder itself is what nginx serves, so `npm run build` is the depl
|
||||
Every push runs the whole proof on the Gitea runner: `.gitea/workflows/ci.yml` installs, runs
|
||||
`./check.sh`, builds, then `tools/ci/run-e2e.sh` (the editor suite) and `tools/ci/run-smokes.sh`
|
||||
(the three games). Both scripts source `tools/ci/serve.sh`, which puts the checkout behind
|
||||
`http://127.0.0.1:8080/game-maker/` with `php -S` – the build wires its assets absolutely under
|
||||
`/game-maker/dist/`, so the path segment is not optional – and writes the empty project files the
|
||||
`http://127.0.0.1:8080/game-maker/` with `php -S` — the build wires its assets absolutely under
|
||||
`/game-maker/dist/`, so the path segment is not optional — and writes the empty project files the
|
||||
seeders need (`PUT` does not create a project, `POST` hands out a random id). Every tool takes its
|
||||
server from `GAME_MAKER_BASE_URL`.
|
||||
|
||||
The job runs in the image from `tools/ci/Dockerfile` under the label `game-maker-ci`: chromium
|
||||
with the Vulkan drivers, `php-cli`, and the host's font packages, because the checks measure text
|
||||
widths to find click points. The **render device is not in this repo** – `/dev/dri/renderD128`
|
||||
widths to find click points. The **render device is not in this repo** — `/dev/dri/renderD128`
|
||||
and a 1 GB `/dev/shm` come from `container.options` in the runner's own `config.yaml`, which
|
||||
applies to every job on that runner. Without them chromium falls back to llvmpipe, which gets
|
||||
WebGL 1.0 only. Measured in the job container: `ANGLE (Intel, Vulkan 1.3.230, HD Graphics 630)`,
|
||||
|
||||
+5
-5
@@ -1,5 +1,5 @@
|
||||
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** –
|
||||
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>
|
||||
@@ -31,13 +31,13 @@ Live: <https://nockal.com/game-maker/> · Widget playground: <https://nockal.com
|
||||
|
||||
They live in the issue tracker of the Gitea repo: <https://git.nockal.com/meleki/game-maker/issues>.
|
||||
The folder `todo-notes/` that held them as files until 2026-09-12 is gone; all 50 notes were moved
|
||||
into issues #1–#50, closed ones included, each carrying the commits it was filed and finished with.
|
||||
into issues #1-#50, closed ones included, each carrying the commits it was filed and finished with.
|
||||
|
||||
## Not yet
|
||||
|
||||
Several scenes in one run (a `Load scene` node – screens are containers
|
||||
Several scenes in one run (a `Load scene` node — screens are containers
|
||||
today), sound, auth, touch-optimised layout, graph comments/groups. A frame is always stretched to
|
||||
the sprite's `width` × `height`: no nine-slice. Animations have no state machine, no restart node
|
||||
the sprite's `width` x `height`: no nine-slice. Animations have no state machine, no restart node
|
||||
and no preview in the Scene tab. Slicing cuts a regular grid only — no detection of where the sprites actually
|
||||
sit on the sheet. Unreferenced image files are never collected – deliberately, since the image
|
||||
sit on the sheet. Unreferenced image files are never collected — deliberately, since the image
|
||||
picker offers them again; nothing deletes a stored image except `DELETE ?r=textures/{file}`.
|
||||
+1
-1
@@ -4,7 +4,7 @@ container from the selection · `Ctrl+Enter` play, and pause/resume while playin
|
||||
`Ctrl+V` import an image from
|
||||
the clipboard as a texture (ignored while a text field has focus) · `Tab` / `Shift+Tab` cycle fields · lists: `↑`/`↓` move,
|
||||
`←`/`→` fold, `Enter` activates · number
|
||||
fields: drag to scrub (`Shift` ×10, `Alt` ×0.1, snapping to the grid of that increment, so a
|
||||
fields: drag to scrub (`Shift` x10, `Alt` x0.1, snapping to the grid of that increment, so a
|
||||
step-1 field lands on whole numbers), `↑`/`↓` step, shown with three decimals (`ui/math.ts`,
|
||||
`NUMBER_DECIMALS`) so a fractional value never reads as a whole number — except where the
|
||||
value is integral by nature (frame rectangles, the slice dialog, a camera's view size, font
|
||||
|
||||
+12
-12
@@ -1,9 +1,9 @@
|
||||
**Placing an asset: drag it in.** Drag a row out of the **Assets** panel and drop it on the
|
||||
view – the Scene tab places into the active scene, the Container tab into the open template, so
|
||||
view — the Scene tab places into the active scene, the Container tab into the open template, so
|
||||
what you see is what you fill. While the pointer is over a view, an outline shows the footprint
|
||||
the object will have and a crosshair the origin it snaps to; it turns **red** when the drop would
|
||||
be refused, which today means a container that would end up inside itself (`placementRejection`,
|
||||
`src/core/model/placement.ts` – the same function the command runs, so the preview cannot promise
|
||||
`src/core/model/placement.ts` — the same function the command runs, so the preview cannot promise
|
||||
a placement the drop then denies). Picking a container up selects it, but the Container tab does
|
||||
*not* follow that while a drag is in flight: the target must not swap out from under the pointer.
|
||||
Dragging is the only way to place: a double-click on a scene makes it the active one, on anything
|
||||
@@ -12,8 +12,8 @@ else it does nothing.
|
||||
**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. A viewport label keeps a
|
||||
constant size on screen however far you zoom, and both kinds of them – the object names of the
|
||||
Scene tab and the frame names of the Texture tab – read that size from one token
|
||||
constant size on screen however far you zoom, and both kinds of them — the object names of the
|
||||
Scene tab and the frame names of the Texture tab — read that size from one token
|
||||
(`Theme.viewportLabelSize`); the two `viewportLabel` styles differ in colour only.
|
||||
|
||||
**Z-order is part of the position.** `position.z` is the draw order of a scene object: higher
|
||||
@@ -21,32 +21,32 @@ Scene tab and the frame names of the Texture tab – read that size from one tok
|
||||
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
|
||||
(`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.
|
||||
Dragging into the **gap** above or below a row – its outer quarter, marked with a line instead of
|
||||
an outline – orders the object among *that* row's siblings rather than putting it inside, and
|
||||
Dragging into the **gap** above or below a row — its outer quarter, marked with a line instead of
|
||||
an outline — orders the object among *that* row's siblings rather than putting it inside, and
|
||||
re-parents it on the way when the gap belongs to another parent. Since equal `z` falls back to
|
||||
this order, that is also how two objects at the same depth are put in front of each other. A drag
|
||||
that lands where it started changes nothing and costs no undo step.
|
||||
A row's **name turns gold** when the object deviates from what it follows – its container
|
||||
template, or its asset – and also when anything below it does, so a folded subtree still shows
|
||||
A row's **name turns gold** when the object deviates from what it follows — its container
|
||||
template, or its asset — and also when anything below it does, so a folded subtree still shows
|
||||
that something inside was changed. It is the same gold the inspector paints an overridden
|
||||
label in.
|
||||
Rows with children **fold**: click the chevron (or press ←/→, or double-click the row) to open
|
||||
and close a subtree, right-click → **Expand all** / **Collapse all** for the whole scene. A
|
||||
scene opens fully collapsed – which rows are open is view state of the panel, never stored in
|
||||
the project – and selecting an object anywhere (viewport, `Add collider`, `Create container`)
|
||||
scene opens fully collapsed — which rows are open is view state of the panel, never stored in
|
||||
the project — and selecting an object anywhere (viewport, `Add collider`, `Create container`)
|
||||
unfolds the way down to it and scrolls it into view, so a selection is never hidden. 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.
|
||||
|
||||
**Editor offset (scene view only).** Every scene object additionally carries an
|
||||
`editorOffset` – an x/y shift of where it is *drawn, selected and dragged* in the Scene tab,
|
||||
`editorOffset` — an x/y shift of where it is *drawn, selected and dragged* in the Scene tab,
|
||||
and nothing else. The game, collisions and the graph never see it; it is there to pull apart
|
||||
objects that sit on top of each other, such as the stacked title/win/lose screens of a game.
|
||||
It cascades like a position (offsetting a container offsets its content) and is edited in the
|
||||
|
||||
+7
-7
@@ -2,9 +2,9 @@ A **state machine asset** is a named set of states and nothing more, yet: its gr
|
||||
`State` node per state (a title and a *Name* setting, no ports) and no edges. Transitions,
|
||||
events like "on state entered" and a highlight of the current state while playing are later
|
||||
steps. The states are edited in the **State machine** tab, which is the graph canvas the Behavior
|
||||
tab uses – so placing, dragging, the header's **Snap** checkbox, renaming in the inspector,
|
||||
tab uses — so placing, dragging, the header's **Snap** checkbox, renaming in the inspector,
|
||||
`Delete` and undo are the graph's.
|
||||
Selecting the asset opens the tab (a behavior, texture or container asset does not switch tabs –
|
||||
Selecting the asset opens the tab (a behavior, texture or container asset does not switch tabs —
|
||||
a machine has nothing to show anywhere else). Right-click on its canvas offers a `State` and
|
||||
nothing else, named by its number (`State 3` for the third); no behavior palette offers one.
|
||||
|
||||
@@ -13,16 +13,16 @@ unconnected (or takes a `State machine` node, the literal every kind has):
|
||||
|
||||
- **`State branch`** grows **one exec output per state**, named after the state, and continues
|
||||
along the one the machine is in. Before `Set state` has run the machine is in **no state**, and
|
||||
the flow ends there – there is no start state. A state the machine no longer has (deleted, or of
|
||||
the flow ends there — there is no start state. A state the machine no longer has (deleted, or of
|
||||
another machine) ends it too.
|
||||
- **`Set state`** is the mirror image: **one exec input per state**, one exec output. The input the
|
||||
flow *arrives through* names the new state – so `OnStart → Set state (Open)` and `On key down →
|
||||
flow *arrives through* names the new state — so `OnStart → Set state (Open)` and `On key down →
|
||||
Set state (Closed)` are two edges into one node. The interpreter hands every node the exec input
|
||||
a pulse entered through (`run(…, entry)`); this is the first node to read it.
|
||||
|
||||
The ports are keyed by the **state's node id** and labelled by its name: renaming a state keeps
|
||||
every edge, deleting one leaves its edges dangling like any port that is gone. Which machine the
|
||||
ports belong to is read off the graph, not off a running value – the inline pick, or a
|
||||
ports belong to is read off the graph, not off a running value — the inline pick, or a
|
||||
`State machine` literal wired in; a variable or an `Item at` feeding the input yields no state
|
||||
ports, exactly as `Run behavior` knows its behavior only as a setting. Picking another machine
|
||||
orphans the edges of the old states, the way any setting that decides a node's ports does.
|
||||
@@ -30,5 +30,5 @@ orphans the edges of the old states, the way any setting that decides a node's p
|
||||
**The current state is the behavior instance's own**, kept beside its variables
|
||||
(`NodeContext.states`, machine id → state id): two crates running the same behavior are in
|
||||
states of their own, and a behavior run through `Run behavior` starts in no state, as it starts
|
||||
with fresh variables. A state machine is a **kind** like a texture – a variable, a struct field,
|
||||
a list item (tagged `{"$statemachine": id}`), a port – and a `statemachine` asset is never placed.
|
||||
with fresh variables. A state machine is a **kind** like a texture — a variable, a struct field,
|
||||
a list item (tagged `{"$statemachine": id}`), a port — and a `statemachine` asset is never placed.
|
||||
+5
-5
@@ -15,7 +15,7 @@ never collected (below), so a picture one project dropped is still there for the
|
||||
up, and uploading the same bytes twice buys nothing. Picking one and pressing **Use image**
|
||||
makes a texture asset on that file; **Upload…** is the way in for a picture the server does not
|
||||
have yet, and lands in the same import as a drop. A tile captioned with an asset name is one
|
||||
this project already wears – picking it selects that asset rather than making a second one.
|
||||
this project already wears — picking it selects that asset rather than making a second one.
|
||||
|
||||
**Where it goes.** The image is uploaded as raw bytes to `POST ?r=textures` and stored as
|
||||
`data/textures/{sha256}.{ext}` — the file is named after its own content. That name buys four
|
||||
@@ -61,22 +61,22 @@ plain colour and comes back by itself on undo. Deleting the whole texture counts
|
||||
wearing *any* of its frames, not just the ones naming the texture directly.
|
||||
|
||||
**Fitting the sprite to the picture.** The *Width* and *Height* rows of a sprite carry a ↔ / ↕
|
||||
button offering two things: **Fit to 64 × 48** sets that one side from the other and the picture's
|
||||
button offering two things: **Fit to 64 x 48** sets that one side from the other and the picture's
|
||||
proportions — the frame's rectangle when it wears a frame, the whole image otherwise — and **Set
|
||||
to 64 × 48 px** puts both sides at the picture's own pixel size. Like ↺ and ↑ the button exists
|
||||
to 64 x 48 px** puts both sides at the picture's own pixel size. Like ↺ and ↑ the button exists
|
||||
only when it would do something, and follows the same rule for how many: no offer means no button,
|
||||
one is a direct click, both make it the menu form ↔ ▾. So a sprite without a picture, or one
|
||||
already in proportion *and* at pixel size, carries nothing. Either action is an ordinary property
|
||||
write, so on an object it becomes an override, and setting both sides is still one undo step. The
|
||||
proportions are compared with a relative tolerance rather than exactly — of the 40 000 picture
|
||||
sizes up to 200 × 200, 3135 do not survive the round trip through a division, and an exact test
|
||||
sizes up to 200 x 200, 3135 do not survive the round trip through a division, and an exact test
|
||||
would leave the button standing after its own click. The pixel size is compared exactly, because
|
||||
those two numbers are written as they are.
|
||||
|
||||
**A sprite straight from a texture.** Right-click a texture in the asset list → *Create sprite
|
||||
from texture* makes a sprite asset that wears the whole picture at its pixel size, named after the
|
||||
texture (numbered like any name clash, so `Coin` gives `Coin 2`) and selected. It is the same as creating a sprite, picking the texture and clicking *Set to
|
||||
W × H px*, but in one undo step.
|
||||
W x H px*, but in one undo step.
|
||||
|
||||
Rendering a frame is a texture matrix on the fill, not a Pixi sub-texture — a Graphics fill
|
||||
ignores a `Texture`'s `frame` (measured, see `viewport/textureFill.ts`). One consequence is
|
||||
|
||||
Reference in new issue
Block a user