Physics stage 1a: new page Physics, Rigidbody and collider material in Colliders, Concepts, Home, Sidebar and Architecture, check total 1052 (#146)

admin committed 2026-09-18 18:11:44 +02:00
1 parent f062b417a9
commit f6904445d8
7 files changed
+109 -10

No files matched your search

+7 -3
@@ -67,7 +67,7 @@
```
web/index.html Vite source page (dev) index.html built page (generated, served)
src/core/model/ Project (object holders), Asset + AssetTypes, SceneObject, Properties (value types), hierarchy, walk (generic parent/child walks), instance (container content resolved from its template), usages (where an asset is used), container (the cycle guard), placement (what may be dropped where), texture (frames and the slice grid), tile (the three kinds and their block shapes, reading a tile's pictures, the animation step), autotile (which quarter tile a neighbourhood asks for, the 47/16 tiles a block produces, 1 for a plain tile), tilemap (the cells of a layer, which neighbours a cell is joined to, the cells a stroke or a rectangle covers), tilemapCollision (the solid cells of a map, drawn in or out by their tile's collision scale and merged into rectangles, and where one sits in the world), animation (which frame at time t, a row's value there, what a row may aim at, the players on an object; KeyRef and its lookups) + animationPreview (the root at a frame, written into copies), graphLookup (what a graph sees of the project: behaviors to run, state machines to read), Scope (where a state machine's current state lives: per instance or one for the run — variables are shared by living on a Variables asset instead)
src/core/properties/ PropertySchema, kinds (one row per value kind), listItems (an item's kind, read off its value), resolve (the only override merge), deviation (what is gold), collider/camera/bounds geometry, aspect, enabled, editorRun, assetRef/textureRef (how a reference reads), declaredRows (the row a Property node declares in a shader or particle graph, and the params that node has in both)
src/core/properties/ PropertySchema, kinds (one row per value kind), listItems (an item's kind, read off its value), resolve (the only override merge), deviation (what is gold), collider/camera/bounds geometry, rigidbody (what the engine reads off a rigidbody, a collider's material and the scene), aspect, enabled, editorRun, assetRef/textureRef (how a reference reads), declaredRows (the row a Property node declares in a shader or particle graph, and the params that node has in both)
src/core/commands/ undoable commands, CommandStack, SetBagValueCommand base, RenameCommand base (undone by renaming back: variable, struct field, port), one import command shape for texture/font/sound, require.ts (validated lookups, and what an instance may not change), naming.ts
src/core/graph/ graph model, NodeRegistry/NodeContext/values, interpreter, dropTarget, nested (behaviors as nodes),
emitGraph (the walk both graph compilers share: each node once, in dependency order; cycles and property names reported),
@@ -80,9 +80,13 @@ src/core/store/ serialize (untrusted JSON → Project), tilemapCells (the
FileStore (what the server holds of one kind) + HttpFileStore (one generic client over the three
file collections) — TextureStore, FontStore and SoundStore are its type parameters, nothing else
src/core/time.ts the global clock: one scaled step a frame, shared by the world, the particles, the shaders and the animation preview
src/runtime/ World, Entity, System interface, Input tracker, CollisionSystem, AnimationSystem, BehaviorSystem,
src/runtime/ World, Entity, System interface, Input tracker, CollisionSystem (the editor run's detection),
solids (what `Move` collides with and how it slides, which body a collider belongs to, the
CollisionSource both collision systems give the behaviors), AnimationSystem, BehaviorSystem,
Audio (the AudioSink a run plays into — the counterpart of Input on the way out)
src/runtime/physics/ engine (loads Rapier once, reports its state, converts between pixels and metres)
src/runtime/physics/ engine (loads Rapier once, reports its state, converts between pixels and metres),
PhysicsSystem (Play's collisions: one engine body per rigidbody, free collider and tilemap,
the fixed step, the poses handed back, the contact pairs sorted into events)
src/ui/ widget toolkit (generic, knows nothing about assets)
src/viewport/ view (transforms), items (what to draw), symbols (how to draw it),
FileLibrary (load once, never retry a failure, tell the listeners) ← TextureLibrary, FontLibrary;
+9 -6
@@ -4,19 +4,22 @@ from that *body* and it moves with it. Right-click an object in **Scene objects*
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
and stacked (hitbox + hurtbox on one sprite; colliders of the same body never collide with each
other). They are editor symbols: green outlines in edit mode, invisible in play mode.
other). They are editor symbols: green outlines in edit mode, invisible in play mode. Three more rows –
friction, bounciness, density – are the material a rigidbody feels (see [Physics](Physics)).
A collider takes the transform of its body: a box turns and grows with the sprite it sits on, a
circle grows (see *Transform* in [Scene editing](Scene-editing)). A turned box is tested as the
box it is — separating axes, not its upright frame — so a wall turned by 45 degrees blocks along
its slant.
Play runs a `CollisionSystem` (`src/runtime/systems/CollisionSystem.ts`) before the behaviors:
every frame it tests all collider pairs (world transforms applied; touching edges do not count)
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
Before the behaviors, every frame sorts the colliders that overlap (world transforms applied;
touching edges do not count) into *enter*, *stay* and *exit*. While playing that is the physics
engine's work (`PhysicsSystem`, see [Physics](Physics)), which also simulates rigidbodies; the
editor run tests all pairs itself (`CollisionSystem`) and pushes nothing. Without a rigidbody
both come to the same answer. 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); while playing, `On collision enter` also tells the direction and
the strength of the contact (see [Physics](Physics)). `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.
+1
@@ -16,6 +16,7 @@
| **Variables asset** | Declarations and nothing else — no graph, no position, never placed. A `Variables` node naming it reads and writes **one value per key for the whole run**, however many graphs point at it. The values start at the declared ones on every Play and are never written back, so playing cannot change the project; while a run is on the asset's inspector shows them live and an edit there reaches the run at once. Two such assets may declare the same name without meeting. See [Behaviors](Behaviors). |
| **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. |
| **Rigidbody** | Placed as a **child** of an object, it hands that object to the physics engine while playing: it falls with the scene's gravity, bounces and rests on colliders, pushed by what it meets. At most one per object. See [Physics](Physics). |
| **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. |
+1 -1
@@ -12,7 +12,7 @@ npm run check # typecheck + lint + format:check + tests, the gate CI runs
# code is lost (tail's is reported), so a red run reads as green
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 # 1045 editor checks, screenshots in /tmp/gm-e2e-*.png
node tools/seed-e2e.mjs && node tools/e2e.mjs # 1052 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
./e2e.sh start [GM_STEPS] # the same run detached, see below
./e2e.sh status # live progress, or the last run's result
+1
@@ -14,6 +14,7 @@ Live: <https://nockal.com/game-maker/> · Widget playground: <https://nockal.com
| [Scene editing](Scene-editing) | Placing by drag, editor symbols, z-order, hierarchy and parenting, the editor offset |
| [Behaviors](Behaviors) | Node graphs: variables and properties, Variables assets, the node set, structs and lists, loops, behaviors as nodes, editing the graph, the live view |
| [Colliders](Colliders) | Collider assets, collision events, blocking movement |
| [Physics](Physics) | Rigidbodies, the collider material, gravity and units, the fixed step, the contact on `On collision enter` |
| [Textures](Textures) | Getting images in, where they go, tinting, the filter, frames, nine-slice, upload limits |
| [Fonts](Fonts) | Uploading a typeface, what a text wears, what the editor falls back to |
| [Sounds](Sounds) | Uploading a sound, what the asset says, `Play sound` and `Stop sound`, what a pause and a stop do to a voice |
+89
@@ -0,0 +1,89 @@
Play simulates **rigidbodies** on the physics engine [Rapier 2D](https://rapier.rs) (#146): they
fall, slide, bounce, tip over and come to rest. What has no rigidbody behaves exactly as it did
before the engine – see *Without a rigidbody* below.
## The rigidbody
A **rigidbody** is an asset placed as a child of the object it moves, like a collider. Right-click
an object in **Scene objects** → *Add rigidbody*; an object takes one at most, and a collider, a
behavior, a tilemap or another rigidbody takes none. It is drawn as a green ring with an arrow in
edit mode and not at all in the game. Its rows:
| Row | Default | What it does |
|---|---|---|
| Body type | Dynamic | **Dynamic** falls and is pushed. **Static** never moves on its own. **Kinematic** goes wherever the object is put (a graph, an animation) and pushes dynamic bodies out of its way. |
| Mass (0 = auto) | 0 | 0 lets the colliders' density and size decide; anything else is the mass itself, in kg. |
| Gravity scale | 1 | How strongly the scene's gravity pulls; 0 floats, −1 falls upwards. |
| Linear damping | 0 | Slows the movement down by itself, like air. |
| Angular damping | 0 | The same for turning. |
| Fixed rotation | off | On: the body never turns (a player). Off: a box that lands on an edge tips over. |
| Continuous (CCD) | off | For small, fast bodies that would otherwise pass through thin walls between two steps. |
The body carries every collider below its object – the object's own collider children and those of
objects parented further down, unless one of those has a rigidbody of its own. A rigidbody that is
switched off is as if it were not there. A body without any solid collider still falls, with its
mass or 1 kg.
A dynamic body is simulated in world units, also below a turned or scaled parent; what the engine
works out is handed back as the object's position and rotation, relative to its parent. An object
without rotation rows (a camera) never turns. Scaling an object while playing builds its shapes
again. Putting a dynamic object somewhere else from a graph moves the body there and keeps its speed.
## The material
Every collider has three more rows, read wherever a rigidbody takes part:
| Row | Default | |
|---|---|---|
| Friction | 0.5 | How much it grips. Two colliders meeting use the **average** of both. |
| Bounciness | 0 | 0 stays down, 1 bounces back as high as it fell. Two colliders meeting use the **larger** of both, so a ball bounces on plain ground. |
| Density | 1 | Mass per area, in kg per square metre. |
A trigger (`isTrigger`) weighs nothing and never pushes; it only reports.
## Gravity and units
The **scene** has **Gravity X** and **Gravity Y**, default (0, 980). Everything is in world units
(pixels); the engine works in metres and **100 units are one metre**, so 980 is the Earth's pull.
It is read every frame, so an animation over the scene can change it while playing.
The engine steps at a fixed **60 steps per second**, whatever the frames take; a frame that took
longer catches up with at most 8 steps and forgets the rest, so a hitch does not make the game run
fast for a while after. What the game view shows of a dynamic body lies between the last two steps,
so it moves evenly on any display – which also means it is drawn up to one step (1/60 s) behind the
engine.
## Collision events
`On collision enter/stay/exit` and `Is touching` come from the engine's contact pairs.
`On collision enter` has three more outputs: **normal x** and **normal y**, a unit vector pointing
from the other collider towards the own one – (0, −1) for something landing on the ground – and
**impulse**, how hard they met (kg · units per second). A trigger reports a normal of (0, 0) and no
impulse. `Is touching` answers for the last step, not for a move made in the same frame.
## Without a rigidbody
An object with colliders but no rigidbody is what every project was before: its colliders go
wherever the object is put, nothing pulls on them, and dynamic bodies bounce off them as off a wall.
Whether two of them touch is decided by the same overlap test as before – touching edges do not
count, only overlapping does – the engine only proposes which pairs to test. A placed tilemap is
static ground made of its merged solid cells (see [Tilemaps](Tilemaps)). `Move` with
*Collide with solids* works as described in [Colliders](Colliders); a dynamic body blocks it like a
wall.
## Play and the editor run
The editor loads the engine right after opening the project; Play waits for it if it is not there
yet (*Loading the physics engine…*), and does not start if it could not be loaded – the notice and
the console say why. There is no falling back to the old detection: a game would collide
differently.
A behavior with *Run in editor* sees collisions but no simulation: dynamic bodies stay where they
are placed, because what the editor run moves is saved.
## What is not there yet
- Forces, impulses and velocities from a graph (`Apply force`, `Apply impulse`, `Set velocity`, …)
and `Move` over the engine's character controller – slopes, steps, pushing boxes (#147).
- The simulation in the editor run (#148), and `On collision stay` with contact data (#149).
- Capsules and polygons, collision layers, raycasts, joints, one-way platforms (#144).
+1
@@ -5,6 +5,7 @@
- [Scene editing](Scene-editing)
- [Behaviors](Behaviors)
- [Colliders](Colliders)
- [Physics](Physics)
- [Textures](Textures)
- [Fonts](Fonts)
- [Sounds](Sounds)