From e2d7283feb43312902e20616467682e52d3b63dd Mon Sep 17 00:00:00 2001 From: MelekTaus Date: Sat, 19 Sep 2026 11:36:12 +0200 Subject: [PATCH] One-way colliders and tiles, the Raycast node and collision stay with interval and contact point in Colliders, Physics, Behaviors, Tiles and Tilemaps, Architecture and Development updated, check total 1073 (#149, #153, #155) --- Architecture.md | 13 ++++++++----- Behaviors.md | 6 ++++-- Colliders.md | 49 +++++++++++++++++++++++++++++++++++++++++++++++-- Development.md | 2 +- Physics.md | 40 ++++++++++++++++++++++++++++++++++------ Tilemaps.md | 9 +++++++++ Tiles.md | 2 ++ 7 files changed, 105 insertions(+), 16 deletions(-) diff --git a/Architecture.md b/Architecture.md index cffab33..078e1fc 100644 --- a/Architecture.md +++ b/Architecture.md @@ -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, the slice grid, a frame's collision outlines), polygon (an outline checked, cut into convex pieces and clipped to a rectangle), 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, the outlined cells as polygons – an autotile's outlines cut into its quarters and pieced together per cell like its picture –, 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, convex (capsules and polygons: rounded convex pieces against each other), 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/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 (with the side a one-way collider blocks from), convex (capsules and polygons: rounded convex pieces against each other), ray (where a ray meets a shape, in double precision), 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), @@ -81,13 +81,16 @@ src/core/store/ serialize (untrusted JSON → Project), tilemapCells (the 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 (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, + solids (what `Move` collides with and how it slides — one-way solids let it through unless + it lands on them —, the first solid a ray meets, which body a collider belongs to, the + CollisionSource both collision systems give the behaviors, the contact of a staying pair), 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), 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; the push a - `Move` gives a dynamic body, and what the physics nodes reach through `NodeContext.physics`) + the fixed step, the poses handed back, the contact pairs sorted into events and their + normal, impulse and point; whether a dynamic body lands on or passes a one-way collider, + decided before each step for the contact hook; the push a `Move` gives a dynamic body, and + what the physics nodes reach through `NodeContext.physics`) 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; diff --git a/Behaviors.md b/Behaviors.md index 001b1c3..4eaef10 100644 --- a/Behaviors.md +++ b/Behaviors.md @@ -97,7 +97,7 @@ shown on the node and editable in the inspector. 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 enable`, `On update`, `On disable`, `On destroy` (see *Lifecycle*), `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`, `On state entered`, `On state exited` (one output per state, see *State machines*), `Start` · flow `Branch`, `Sequence`, `Repeat`, `Run behavior`, `End`, and the four that move or follow a state machine, `State branch`, `Set state`, `Set state by name`, `State transition` (see *State machines*) · data `Self`, `Owner`, `Parent`, `Object by name`, `Name of`, + `On timer`, `On collision enter/stay/exit` (stay every frame, or with an interval; while playing enter and stay tell normal, impulse and point of the contact), `On click` (the mouse pressed on the owner — anywhere when it has none), `On animation finished`, `On state entered`, `On state exited` (one output per state, see *State machines*), `Start` · flow `Branch`, `Sequence`, `Repeat`, `Run behavior`, `End`, and the four that move or follow a state machine, `State branch`, `Set state`, `Set state by name`, `State transition` (see *State machines*) · 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` (every property row of the target's type, the transform rows `scale`, `rotation`, `mirrorX`, `mirrorY` included — how a graph turns a sprite), `Get position` (local and world), `Is enabled`, literals `Number`/`Text`/`Boolean`/`Texture`/`State machine`/`Sound`/`Scene`, `Add/Subtract/Multiply/Divide`, `Min/Max/Abs/Sqrt/Clamp`, `Floor/Ceil/Round`, `Modulo`, `Random`, `Time`, `To text`, `To number`, `Join text`, @@ -115,7 +115,9 @@ shown on the node and editable in the inspector. direction; every play is a voice of its own, so two in a row overlap, and the voice ends with the behavior that started it unless `detached` says otherwise), `Load scene` (swaps the running scene once the frame is over — see [Scenes](Scenes)), `Log`, `Stop game`, `Restart game`, - `Apply force`, `Apply impulse`, `Set velocity`, `Set angular velocity`, `Set gravity` (see [Physics](Physics)). A node type is one file exporting a `NodeDef` + `Apply force`, `Apply impulse`, `Set velocity`, `Set angular velocity`, `Set gravity` (see [Physics](Physics)), + `Raycast` (what a ray meets first: object, collider, point, normal, distance — see [Colliders](Colliders#raycast)), + `Ignore one-way` (fall through one-way colliders and tiles for some seconds — see [Colliders](Colliders#one-way)). 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 (e.g. `Move.scale = 1`), shown on the node and in the inspector. diff --git a/Colliders.md b/Colliders.md index 6930446..3e0d90e 100644 --- a/Colliders.md +++ b/Colliders.md @@ -41,8 +41,11 @@ engine's work (`PhysicsSystem`, see [Physics](Physics)), which also simulates ri 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); 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 +and `collider` (the own one); while playing, `On collision enter` and `On collision stay` also +tell the direction, the strength and the point of the contact (see [Physics](Physics)). +`On collision stay` fires every frame by default; with an **Interval (s)** above 0 it fires once each +time that long has passed since the pair met, on a clock of its own per pair that starts over when +they meet again (#149). `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. @@ -73,3 +76,45 @@ always leave. A move longer than the thinnest collider involved is split into su 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. + +## One-way + +A collider with **One-way** on (#155) blocks only what comes from one side: **Blocks from** says +which — *Above* by default, or *Below*, *Left*, *Right* — in the collider's own axes, so it turns +and mirrors with its object. In the scene view that side is drawn thick. It is the platform a +figure jumps through from below and lands on from above: + +- It blocks a contact whose normal lies within **45 degrees** of that side; anything else passes - + from below, and from the side as well, as in Unity and Godot. +- What is already inside it passes on through for as long as it is inside, so a jump that did not + clear the top falls back out below. +- `Move` with *Collide with solids* reports `hit` only where it was stopped, and a fast fall is + still caught: the move is swept in steps no thicker than the platform. +- **Ignore one-way** (object, seconds, 0.25 by default) lets the object fall through all one-way + colliders and tiles for that long — drop down with the down key. When the time is up it still + passes what it is inside and lands on the next one it comes down on. +- A dynamic body lands, passes and drops the same way (see [Physics](Physics#one-way-colliders-in-the-engine)). +- A ray meets it only from the side it blocks from. + +Tiles can be one-way as well (see [Tilemaps](Tilemaps#one-way-tiles)). + +## Raycast + +The **Raycast** node (#153) casts a ray from `from x/y` along `dir x/y` (any length, it is made a +unit vector) for up to `length` world units, and says what it meets first: `hit`, the `object` +(the body, like `other` of a collision), its `collider` (or the tilemap), the `point x/y`, the +`normal x/y` of the face (pointing out of it) and the `distance`. For line of sight, a ground +check, a laser, a hitscan shot. + +- Met are solid colliders of every shape and the cells of a tilemap, outlined ones too. Triggers + only with **Hit triggers** on. A one-way collider or tile only from the side it blocks from. +- With **Ignore owner** on (the default) the behavior's owner and everything parented below it are + not met. And a shape the ray *starts inside* is never met — so a ray cast from the middle of a + figure finds what is around it; one starting on an outline and running in meets it at 0. +- Grazing a corner counts as meeting it. +- It is worked out in our own double-precision geometry (`core/properties/ray.ts`) over the same + shapes `Move` collides with, so the editor run and Play answer alike, and always about where + everything is right now — not about the last physics step. +- In the editor run every ray cast shows in the scene view for a quarter of a second: green to + where it met something, grey to its end when it met nothing. +- Only the first hit for now; a list of all hits and a filter by collision layer (#152) come later. diff --git a/Development.md b/Development.md index a4bb724..f04c0db 100644 --- a/Development.md +++ b/Development.md @@ -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 # 1068 editor checks, screenshots in /tmp/gm-e2e-*.png +node tools/seed-e2e.mjs && node tools/e2e.mjs # 1073 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 diff --git a/Physics.md b/Physics.md index 6949b5d..65d510f 100644 --- a/Physics.md +++ b/Physics.md @@ -67,10 +67,15 @@ 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. +`On collision enter` and `On collision stay` have five 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 — **impulse**, how hard they meet (kg · units per second), and **point x/y**, the middle +of the contact points in the world. Enter tells how the pair met, stay how it touches at the moment +it fires (#149) — which, with its **Interval (s)**, need not be every frame. The impulse is the +engine's own number: it grows with the weight, but a crate at rest reports about 1.25 times +mass · gravity · 1/60, not exactly that (measured). A trigger reports all of them as 0, and so does +the editor run, which simulates nothing. `Is touching` answers for the last step, not for a move +made in the same frame. ## Without a rigidbody @@ -92,6 +97,28 @@ weighs what its rigidbody's **Mass** says, or else what its colliders weigh by t dynamic mover what the engine says. The mover stays flush in front of the crate and never gets into it. +## One-way colliders in the engine + +A dynamic body lands on a one-way collider or tile (see [Colliders](Colliders#one-way)) and passes +it from below and from the side, like `Move` does. The engine is told pair by pair whether to push +the two apart (Rapier's `filterContactPair` hook). Its JavaScript version cannot touch the engine +from inside the hook — even reading a body's position there throws, the throw is swallowed and the +pair collides with nothing (found in the spike for #155) — so the answer for each dynamic body near a +one-way collider is worked out **before every step**: + +- It **lands** when it is on the blocking side or sunk in no deeper than 2 units (a landing at 60 + steps per second sinks in 1.9 before the engine pushes it out), the contact faces that side + within 45 degrees, and it is not moving away from it. +- Once landed it is **held** as long as the two overlap: a body falling 2000 units per second is + 13 units into an 8 units thick platform one step later, and is pushed back up instead of passing. +- Otherwise it passes, and so does a body told to by `Ignore one-way`. A resting body is woken for + that — a sleeping one would not fall by itself. + +Measured in the spike at 60 steps per second: a landing sinks in 1.9 units and rests 0.009 deep, a +body shot up from below comes out on top and lands, one sliding sideways goes through, and one +falling at 2000 units per second lands with and without CCD. Collision events come on passing +through as well: overlapping is colliding. + ## Moving bodies from a graph | Node | What it does | @@ -128,5 +155,6 @@ are placed, because what the editor run moves is saved. The nodes above do nothi it caught on the seams between boxes lying side by side (10 of 100 steps along a floor of 12 crates), kept a gap of up to 1.7 units and knows no friction; `Move` works out its contacts itself, in double precision, and slides over the same seams without catching (#150). -- The simulation in the editor run (#148), and `On collision stay` with contact data (#149). -- Collision layers, raycasts, joints, one-way platforms (#144). +- The simulation in the editor run (#148). +- Collision layers (#152) and joints (#154). Raycasts (#153) and one-way platforms (#155) are + there, see [Colliders](Colliders#raycast). diff --git a/Tilemaps.md b/Tilemaps.md index 14d5b62..7a3f7cf 100644 --- a/Tilemaps.md +++ b/Tilemaps.md @@ -201,6 +201,15 @@ In the running game the placed map is a **body of its own** in the collision sys There are no trigger tiles yet: a tile blocks or it does not. +### One-way tiles + +A solid tile with **One-way (from above)** on (#155) blocks only what comes down on it from above, +like a one-way collider blocking from *Above* (see [Colliders](Colliders#one-way)): a figure jumps +through from below and from the side, lands on top, and falls through with `Ignore one-way`. Above +means the map's top, turned with it. One-way cells are merged into rectangles the same way, but only +with each other — a platform next to a wall is two shapes, so the wall never blocks from below only. +In the scene view they are outlined thinner, their top drawn thick. + ## In the project file While you edit, a layer holds its cells as a map from `"x,y"` to a tile. The project file stores diff --git a/Tiles.md b/Tiles.md index 81057e4..b61def4 100644 --- a/Tiles.md +++ b/Tiles.md @@ -60,6 +60,8 @@ asks for; a picture that does not fit is kept and named, never silently dropped. Ping-pong turns on the last picture, so 0 1 2 1 rather than 0 1 2 2 1. - **Solid** — off by default. A solid tile blocks movement wherever it is painted, on every map and every layer (#132); see [Tilemaps](Tilemaps#collision). +- **One-way (from above)** — off by default. A solid tile with it on blocks only from above: a + platform one jumps through from below (#155); see [Tilemaps](Tilemaps#one-way-tiles). - **Collision scale** — 1 by default. How big a solid cell blocks against its square, drawn in or out only on the sides without a blocking neighbour (#140); see [Tilemaps](Tilemaps#collision-scale).