diff options
Diffstat (limited to 'docs/physics.org')
| -rw-r--r-- | docs/physics.org | 1002 |
1 files changed, 582 insertions, 420 deletions
diff --git a/docs/physics.org b/docs/physics.org index 81c57b8..9cf9b96 100644 --- a/docs/physics.org +++ b/docs/physics.org @@ -1,493 +1,655 @@ -#+title: Physics System -#+author: Downstroke Contributors - -* Overview - -With the default =engine-update:= hook (=default-engine-update= from =downstroke-engine=), the physics pipeline runs **automatically** every frame *before* your =update:= hook. Input is processed first, then =engine-update= integrates motion and collisions, then =update:= runs so your game logic sees resolved positions and =#:on-ground?=. - -Set =engine-update: #f= on =make-game= to disable the built-in pass, or pass a custom =(lambda (game dt) ...)= to drive your own order — useful for shmups, menus, or experiments. - -All physics functions are **functional and immutable**: per-entity steps take an entity (plist) plus the current scene and frame =dt=, and return a NEW plist. Bulk steps take the entity list only. No in-place mutation of entities. - -Tile collision reads solid flags from the tilemap embedded in the scene (=scene-tilemap=). - -** Key Physics Functions - -- =step-tweens= — advance =#:tween= (see =docs/tweens.org=) -- =apply-acceleration= — consume one-shot #:ay into velocity -- =apply-gravity= — add gravity to falling entities -- =apply-velocity-x=, =apply-velocity-y= — move entity by velocity -- =resolve-tile-collisions-x=, =resolve-tile-collisions-y= — snap entity off tiles on collision -- =detect-on-solid= — set =#:on-ground?= from tiles below feet and/or solid entities in the scene list -- =resolve-entity-collisions= — all-pairs AABB push-apart for solid entities (=entities= only) -- =sync-groups= — group follow origins (=entities= only) -- =aabb-overlap?= — pure boolean collision test (for queries, not resolution) - -Jumping is **game logic**: set =#:ay= in =update:= (e.g. =(- *jump-force*)=) when the player jumps; there is no =apply-jump=. - -* Physics Pipeline - -The canonical order inside =default-engine-update= (and the recommended order when composing steps yourself) is: +#+TITLE: Physics +#+AUTHOR: Downstroke Contributors + +Downstroke ships with a built-in *physics pipeline* that runs every frame +before your =update:= hook. It advances tweens, integrates acceleration +and gravity into velocity, moves entities, resolves AABB collisions +against the tilemap and against each other, detects whether an entity is +standing on solid ground, and finally advances per-entity animation. The +pipeline is purely *functional*: every step takes an entity (alist) and +returns a new entity — no in-place mutation. You can opt specific +entities out of specific steps (=#:skip-pipelines=), replace the whole +pipeline with a custom procedure, or turn it off entirely +(=engine-update: 'none=) for shmups and menus that want full manual +control. Velocities are in *pixels per frame*, gravity is a constant +=+1= pixel/frame² added to =#:vy= every frame for entities with +=#:gravity? #t=, and tile collisions snap entities cleanly to tile edges +on both axes. The canonical reference example is the *Platformer demo* +— run with =bin/demo-platformer=, source in =demo/platformer.scm=. + +* The minimum you need + +To get physics behavior in your game, do three things: + +1. Give your player entity dimensions, velocity keys, and + =#:gravity? #t=: + + #+begin_src scheme + (list #:type 'player + #:x 100 #:y 50 + #:width 16 #:height 16 + #:vx 0 #:vy 0 + #:gravity? #t + #:on-ground? #f + #:tags '(player)) + #+end_src + +2. Let the default engine update run. When you call =make-game= + *without* overriding =engine-update:= on the scene, the built-in + pipeline runs automatically each frame. You do not need to import + or compose any physics procedure yourself. + +3. In your =update:= hook, set =#:vx= from input and set =#:ay= to + =(- *jump-force*)= on the frame the player jumps. The pipeline + integrates velocity, handles tile collisions, and refreshes + =#:on-ground?= *before* your next =update:= tick so you can gate + jumps on it: + + #+begin_src scheme + update: (lambda (game dt) + (let* ((input (game-input game)) + (scene (game-scene game)) + (player (car (scene-entities scene))) + (jump? (and (input-pressed? input 'a) + (entity-ref player #:on-ground? #f))) + (player (entity-set player #:vx + (cond ((input-held? input 'left) -3) + ((input-held? input 'right) 3) + (else 0))))) + (let ((player (if jump? + (entity-set player #:ay (- *jump-force*)) + player))) + (game-scene-set! game + (update-scene scene entities: (list player)))))) + #+end_src + +That is the entire contract: set intent (velocity / one-shot +acceleration) in =update:=, the pipeline resolves motion and collisions +before your next tick sees the entity again. The *Platformer demo* — +run with =bin/demo-platformer=, source in =demo/platformer.scm= — is +exactly this shape. + +Everything else in this document is either a refinement (skipping +specific steps, custom pipelines, entity–entity collisions) or a +diagnostic aid. + +* Core concepts + +** The engine pipeline, step by step + +=default-engine-update= (defined in =engine.scm=) is the procedure the +engine runs each frame when a scene's =engine-update= field is =#f= +(the default). It applies ten steps, in a fixed order, across the +scene's entity list. The first eight steps are *per-entity* (each +entity is processed independently via =scene-map-entities=). The last +two operate on the *whole entity list* (via +=scene-transform-entities=): #+begin_src -Frame loop (see =docs/api.org= =game-run!=): +Frame: input ↓ - engine-update (=default-engine-update= by default) + engine-update (default-engine-update unless overridden) ↓ - update (your game logic: set #:vx, #:vy, #:ay, …) + update: (your game logic — set #:vx, #:vy, #:ay, ...) ↓ - camera follow → render - -Inside =default-engine-update= (per entity, then bulk): -step-tweens (advance #:tween) - ↓ -apply-acceleration (consume #:ay into #:vy) - ↓ -apply-gravity (add gravity constant to #:vy) - ↓ -apply-velocity-x (add #:vx to #:x) - ↓ -resolve-tile-collisions-x (snap off horizontal tiles, zero #:vx) - ↓ -apply-velocity-y (add #:vy to #:y) - ↓ -resolve-tile-collisions-y (snap off vertical tiles, zero #:vy) - ↓ -detect-on-solid (tiles and/or scene entities underfoot → #:on-ground?) - ↓ -resolve-entity-collisions (whole entity list) - ↓ -sync-groups (whole entity list) -#+end_src - -Per-entity steps use signature =(entity scene dt)=. Bulk steps =(resolve-entity-collisions entities)= and =(sync-groups entities)= apply to the full list; use =scene-transform-entities=. - -**Not all steps apply to every entity** (=#:skip-pipelines=, guards like =#:gravity?=, missing tilemap). See the examples section for patterns: - -- **Platformer**: default =engine-update=; =update:= sets =#:vx= and =#:ay= for jumps -- **Top-down**: same auto pipeline; entities without =#:gravity?= skip gravity/acceleration/ground -- **Physics Sandbox**: default or custom =scene-map-entities= with the same step order -- **Custom / shmup**: =engine-update: #f= and move entities in =update:= - -* Skipping steps (~#:skip-pipelines~) - -An entity may include ~#:skip-pipelines=, a list of **symbols** naming steps to **omit** for that entity only. Absent or empty means no steps are skipped. - -| Symbol | Skipped call | -|--------+----------------| -| ~acceleration~ | ~apply-acceleration~ | -| ~gravity~ | ~apply-gravity~ | -| ~velocity-x~ | ~apply-velocity-x~ | -| ~velocity-y~ | ~apply-velocity-y~ | -| ~tile-collisions-x~ | ~resolve-tile-collisions-x~ | -| ~tile-collisions-y~ | ~resolve-tile-collisions-y~ | -| ~on-solid~ | ~detect-on-solid~ | -| ~entity-collisions~ | participation in ~resolve-entity-collisions~ / ~resolve-pair~ | -| ~tweens~ | ~step-tweens~ (see ~downstroke-tween~) | - -**Entity–entity collisions:** if *either* entity in a pair lists ~entity-collisions~ in ~#:skip-pipelines=, that pair is not resolved (no push-apart). Use this for “ghost” actors or scripted motion that should not participate in mutual solid resolution. - -**Legacy ~apply-velocity~:** skips each axis independently if ~velocity-x~ or ~velocity-y~ is listed. - -Helper: ~(entity-skips-pipeline? entity step-symbol)~ (from ~downstroke-entity~) returns ~#t~ if ~step-symbol~ is in the entity’s skip list. - -** ~define-pipeline~ (~downstroke-entity~) - -Physics steps are defined with ~(define-pipeline (procedure-name skip-symbol) (entity scene dt) body ...)~ from the entity module, optionally with ~guard: expr~ before ~body ...~: when the guard is false, the entity is returned unchanged before the body runs. The first formal must be the entity; ~scene~ and ~dt~ are standard for pipeline uniformity. The procedure name and skip symbol are separate (e.g. ~detect-on-solid~ vs ~on-solid~). ~apply-velocity~ is still written by hand because it consults ~velocity-x~ and ~velocity-y~ independently. - -The renderer and other subsystems do **not** use ~#:skip-pipelines~ today; they run after your ~update:~ hook. If you add render-phase or animation-phase skips later, reuse the same plist key and helpers from ~downstroke-entity~ and document the new symbols alongside physics. - -Use cases: - -- **Tweens / knockback:** skip ~acceleration~, ~gravity~, ~velocity-x~, ~velocity-y~ (and often ~tweens~ is *not* skipped so ~step-tweens~ still runs) while a tween drives ~#:x~ / ~#:y~, but keep tile resolution so the body does not rest inside walls. -- **Top-down:** entities without ~#:gravity?~ skip gravity, acceleration, and ground detection automatically; you usually do not need ~#:skip-pipelines= unless some entities differ from others. - -* Pipeline Steps - -** apply-acceleration - -#+begin_src scheme -(apply-acceleration entity scene dt) -#+end_src - -**Reads**: =#:ay= (default 0), =#:vy= (default 0), =#:gravity?= to decide whether to consume - -**Writes**: =#:vy= (adds #:ay to it), =#:ay= (reset to 0) - -**Description**: Consumes the one-shot acceleration into velocity. Used for jumps (set =#:ay= in =update:=) and other instant bursts. Only works if =#:gravity?= is true (gravity-enabled entities). =scene= and =dt= are unused but required for the pipeline signature. + camera follow + ↓ + render -** apply-gravity +Inside default-engine-update, per entity: + step-tweens (advance #:tween) + ↓ + apply-acceleration (consume #:ay into #:vy, clear #:ay) + ↓ + apply-gravity (add *gravity* to #:vy) + ↓ + apply-velocity-x (add #:vx to #:x) + ↓ + resolve-tile-collisions-x (snap off horizontal tiles, zero #:vx) + ↓ + apply-velocity-y (add #:vy to #:y) + ↓ + resolve-tile-collisions-y (snap off vertical tiles, zero #:vy) + ↓ + detect-on-solid (set #:on-ground? from tiles / solids) -#+begin_src scheme -(apply-gravity entity scene dt) +Then, across the whole entity list: + resolve-entity-collisions (AABB push-apart of solid entities) + ↓ + sync-groups (snap group members to their origin) + ↓ + apply-animation (advance #:anim-tick / #:anim-frame) #+end_src -**Reads**: =#:gravity?= (boolean), =#:vy= (default 0) - -**Writes**: =#:vy= (adds 1 pixel/frame²) - -**Description**: Applies a constant gravitational acceleration. The gravity constant is =*gravity* = 1= pixel per frame per frame. Only applies if =#:gravity?= is true. Safe to call every frame. - -** apply-velocity-x +Here is what each step does, with the exact keys it reads and writes. +All per-entity steps have the signature =(step scene entity dt)=. + +*** step-tweens + +- *Reads*: =#:tween= +- *Writes*: =#:tween= (advanced, or removed when finished), plus any + entity keys the tween is targeting (typically =#:x=, =#:y=, a color + component, etc. — see =docs/tweens.org=) +- *Guard*: does nothing if =#:tween= is =#f= +- Defined in =tween.scm=. + +*** apply-acceleration + +- *Reads*: =#:ay= (default 0), =#:vy= (default 0) +- *Writes*: =#:vy= (set to =(+ vy ay)=), =#:ay= (reset to 0) +- *Guard*: =(entity-ref entity #:gravity? #f)= — only runs on entities + with =#:gravity? #t= +- One-shot: =#:ay= is *consumed* every frame. This is the jump + mechanism — set =#:ay= to =(- *jump-force*)= on the frame you want + to jump and this step folds it into =#:vy= exactly once. + +*** apply-gravity + +- *Reads*: =#:gravity?=, =#:vy= (default 0) +- *Writes*: =#:vy= (set to =(+ vy *gravity*)=) +- *Guard*: only runs when =#:gravity? #t= +- =*gravity*= is exported from =downstroke-physics=; its value is + =1= pixel/frame². Gravity accumulates until =resolve-tile-collisions-y= + zeroes =#:vy= on contact with the floor. + +*** apply-velocity-x + +- *Reads*: =#:x= (default 0), =#:vx= (default 0) +- *Writes*: =#:x= (set to =(+ x vx)=) +- No guard. Every entity moves by =#:vx=, regardless of =#:gravity?=. + Top-down games therefore "just work" — set =#:vx= and =#:vy= from + input, the pipeline moves the entity and resolves tile collisions, + and the absence of =#:gravity?= makes gravity/acceleration/ground + detection no-op. + +*** resolve-tile-collisions-x + +- *Reads*: scene's tilemap via =scene-tilemap=, then =#:x=, =#:y=, + =#:width=, =#:height=, =#:vx= +- *Writes*: =#:x= (snapped), =#:vx= (set to 0 if a collision occurred) +- *Guard*: only runs when =(scene-tilemap scene)= is truthy. If the + scene has no tilemap this step is a no-op. +- Behavior: computes every tile cell overlapping the entity's AABB + (=entity-tile-cells=), walks them, and if any cell's tile id is + non-zero it snaps the entity to that tile's edge on the X axis. + Moving right (=#:vx > 0=) snaps the right edge to the *near* side of + the first solid tile found (shallowest penetration). Moving left + (=#:vx < 0=) snaps the left edge to the *far* side of the last solid + tile found. In both cases =#:vx= is zeroed. + +*** apply-velocity-y + +- *Reads*: =#:y= (default 0), =#:vy= (default 0) +- *Writes*: =#:y= (set to =(+ y vy)=) +- No guard. Same as =apply-velocity-x= on the Y axis. + +*** resolve-tile-collisions-y + +- *Reads*: scene's tilemap, then =#:x=, =#:y=, =#:width=, =#:height=, + =#:vy= +- *Writes*: =#:y= (snapped), =#:vy= (zeroed on collision) +- *Guard*: only runs when =(scene-tilemap scene)= is truthy. +- Behavior: same as the X-axis version, but on Y. Moving down + (=#:vy > 0=) snaps the feet to a floor tile's top; moving up + (=#:vy < 0=) snaps the head to a ceiling tile's bottom. X and Y are + resolved in *separate passes* (move X → resolve X → move Y → resolve + Y) to avoid corner-clipping. + +*** detect-on-solid + +- *Reads*: =#:gravity?=, =#:x=, =#:y=, =#:width=, =#:height=, =#:vy=; + and =(scene-entities scene)= for entity-supported ground +- *Writes*: =#:on-ground?= (=#t= or =#f=) +- *Guard*: only runs on gravity entities. The =?= in the name is + slightly misleading: the step returns an updated *entity* (with + =#:on-ground?= set), not a boolean. +- Two sources of "ground": + 1. *Tile ground.* Probes one pixel below the feet (=(+ y h 1)=) at + both lower corners; if either column's tile id at that row is + non-zero, the entity is grounded. If =scene-tilemap= is =#f= this + probe is skipped. + 2. *Entity ground.* Another entity in the scene with =#:solid? #t= + counts as ground if it horizontally overlaps the mover, its top + is within =*entity-ground-contact-tolerance*= (5 pixels) of the + mover's bottom, and the mover's =|#:vy|= is at most + =*entity-ground-vy-max*= (12) — so a body falling too fast is + *not* treated as supported mid-frame. + +*** resolve-entity-collisions + +This is a *bulk* step: it takes the entity list and returns a new +entity list. + +- *Reads*: for each entity, =#:solid?=, =#:immovable?=, =#:x=, =#:y=, + =#:width=, =#:height= +- *Writes*: for overlapping pairs, =#:x=, =#:y=, and whichever axis + velocity (=#:vx= or =#:vy=) the separation was applied on +- Behavior: all-pairs AABB overlap check (O(n²)). For each pair where + *both* entities have =#:solid? #t=: + - both =#:immovable? #t= → pair skipped + - one =#:immovable? #t= → the movable one is pushed out along the + shallow penetration axis, its velocity on that axis is zeroed. If + the mover's center is still *above* the immovable's center (a + landing contact), separation is *forced vertical* so the mover + doesn't get shoved sideways off the edge of a platform. + - neither immovable → both are pushed apart by half the overlap + along the smaller-penetration axis, and their velocities on that + axis are set to ±1 (to prevent sticking / drift back into each + other). + - If either entity lists =entity-collisions= in =#:skip-pipelines=, + the pair is skipped entirely. + +*** sync-groups + +- Bulk step. Takes the entity list and returns a new entity list. +- For each entity with =#:group-origin? #t= and a =#:group-id=, marks + it as the origin of its group (first-wins). +- For each entity with a =#:group-id= that is *not* the origin, snaps + its =#:x= / =#:y= to =(+ origin-x #:group-local-x)= / + =(+ origin-y #:group-local-y)=. +- Intended use: multi-part entities (a platform made of several tiles, + a boss with attached hitboxes) that should move as one body. Move + the origin only; sync-groups rigidly follows the members. + +*** apply-animation + +- *Reads*: =#:animations=, =#:anim-name=, =#:anim-frame=, =#:anim-tick= +- *Writes*: =#:anim-tick=, =#:anim-frame=, =#:tile-id=, =#:duration= +- *Guard*: only runs when =#:animations= is present. +- Advances the per-entity animation state machine and updates + =#:tile-id= so the renderer draws the correct sprite. Fully + documented in =docs/animation.org=. + +** Opting in/out of the pipeline + +Most per-entity steps have one or two ways to opt out. Use the one +that matches your intent: + +*** Per-step guard keys + +Some pipeline steps only run when a specific entity key is set. These +are *guards* declared with the =guard:= clause of =define-pipeline=: + +| Step | Guard | +|-----------------------------+-------------------------| +| =step-tweens= | =#:tween= | +| =apply-acceleration= | =#:gravity?= | +| =apply-gravity= | =#:gravity?= | +| =detect-on-solid= | =#:gravity?= | +| =apply-animation= | =#:animations= | +| =resolve-tile-collisions-x= | =(scene-tilemap scene)= | +| =resolve-tile-collisions-y= | =(scene-tilemap scene)= | + +A top-down entity with =#:gravity?= absent (or =#f=) therefore +automatically skips acceleration, gravity, and ground detection — the +rest of the pipeline (velocity, tile collisions, entity collisions, +animation) still runs. + +Entity–entity collisions have their own opt-*in* gate instead of a +guard: only entities with =#:solid? #t= participate in +=resolve-entity-collisions=. Non-solid entities are ignored by the +pair walk. + +*** =#:skip-pipelines= per-entity override + +Every per-entity step is also gated by =entity-skips-pipeline?= (from +=downstroke-entity=). If the entity's =#:skip-pipelines= list contains +the step's *symbol*, the step returns the entity unchanged. The +symbols match the second name in each =define-pipeline= form: + +| Skip symbol | Skips | +|---------------------+----------------------------------------------| +| =tweens= | =step-tweens= | +| =acceleration= | =apply-acceleration= | +| =gravity= | =apply-gravity= | +| =velocity-x= | =apply-velocity-x= | +| =velocity-y= | =apply-velocity-y= | +| =tile-collisions-x= | =resolve-tile-collisions-x= | +| =tile-collisions-y= | =resolve-tile-collisions-y= | +| =on-solid= | =detect-on-solid= | +| =entity-collisions= | participation in =resolve-entity-collisions= | +| =animation= | =apply-animation= | + +For =resolve-entity-collisions=, if *either* entity in a pair lists +=entity-collisions=, the whole pair is skipped. This is the clean way +to mark "ghosts" or script-driven actors that should not be pushed +apart from others. + +Example: an entity driven by a tween that shouldn't be integrated by +velocity or affected by gravity, but should still resolve against +walls and run animation: #+begin_src scheme -(apply-velocity-x entity scene dt) +(list #:type 'moving-platform + #:x 100 #:y 300 #:width 48 #:height 16 + #:vx 0 #:vy 0 + #:tween ... + #:skip-pipelines '(acceleration gravity velocity-x velocity-y)) #+end_src -**Reads**: =#:vx= (default 0), =#:x= (default 0) - -**Writes**: =#:x= (adds #:vx to it) +The pipeline will advance =#:tween= (which can overwrite =#:x= / =#:y= +directly), skip the four listed integrators, still resolve tile +collisions, and still detect ground under it. -**Description**: Moves the entity horizontally by its velocity. Call once per frame, before =resolve-tile-collisions-x=. +*** =define-pipeline= (how steps are declared) -** apply-velocity-y +All per-entity pipeline steps are declared with =define-pipeline= from +=downstroke-entity= (see =docs/entities.org=). The shape is: #+begin_src scheme -(apply-velocity-y entity scene dt) +(define-pipeline (procedure-name skip-symbol) (scene entity dt) + guard: (some-expression-over entity) + (body ...)) #+end_src -**Reads**: =#:vy= (default 0), =#:y= (default 0) - -**Writes**: =#:y= (adds #:vy to it) +The macro expands to a procedure =(procedure-name scene entity dt)= +that returns =entity= unchanged if either the guard is =#f= *or* the +entity's =#:skip-pipelines= list contains =skip-symbol=. Otherwise it +runs =body=. The =guard:= clause is optional; when absent, only +=#:skip-pipelines= is consulted. -**Description**: Moves the entity vertically by its velocity. Call once per frame, before =resolve-tile-collisions-y=. +** Overriding or disabling the pipeline -** resolve-tile-collisions-x +A scene has an =engine-update= field. The game's frame loop dispatches +on it every tick: #+begin_src scheme -(resolve-tile-collisions-x entity scene dt) +;; In game-run!, for the current scene: +(cond + ((eq? eu 'none)) ; do nothing + ((procedure? eu) (eu game dt)) ; run user procedure + ((not eu) (default-engine-update game dt))) ; run default pipeline #+end_src -**Reads**: =#:x=, =#:y=, =#:width=, =#:height=, =#:vx= - -**Writes**: =#:x= (snapped to tile edge), =#:vx= (zeroed if collision occurs) - -**Description**: Detects all solid tiles overlapping the entity's AABB and snaps the entity to the nearest tile edge on the X-axis. If a collision is found: - -- **Moving right** (vx > 0): entity's right edge is snapped to the left edge of the tile (position = tile-x - width) -- **Moving left** (vx < 0): entity's left edge is snapped to the right edge of the tile (position = tile-x + tile-width) - -Velocity is zeroed to stop the entity from sliding. Call this immediately after =apply-velocity-x=. - -** resolve-tile-collisions-y +This is three distinct modes: + +| =engine-update= | Behavior | +|-----------------+----------------------------------------------------------------------| +| =#f= (default) | Run =default-engine-update= — the full built-in pipeline. | +| A procedure | Call =(proc game dt)= each frame *instead of* the default. | +| ='none= | Run *no* engine update. Only your =update:= hook advances the scene. | + +=#f= is the normal case. =default-engine-update= is the recommended +starting point even for customized games — use =#:skip-pipelines= per +entity if you need selective opt-outs. Drop to a custom procedure when +you need a *different order* of steps (for instance, running +=detect-on-solid= *after* =resolve-entity-collisions= so that standing +on an entity that was pushed apart this frame is seen correctly). Drop +to ='none= when the pipeline has no value for your game at all — the +*shmup demo* is this case: bullets, enemies, and the player all move +in =update:= with per-type rules that don't map to gravity+tiles. + +See the *Common patterns* section below for worked examples of all +three modes. + +** Tile collisions & entity collisions + +Both resolution systems are *AABB-only*. Every entity and every tile +is treated as an axis-aligned rectangle; slopes, rotations, and +per-pixel collision are not supported. + +*** Tile collision algorithm + +Tiles come from a =downstroke-tilemap= parsed from a TMX file (Tiled +editor). The tilemap stores a grid of tile ids across one or more +layers; =tilemap-tile-at= returns =0= for an empty cell and a positive +id for a filled cell. Only *empty vs non-empty* is checked — there is +no per-tile "solid" flag in the core engine; any non-zero tile counts +as solid for collision purposes. + +For each axis: + +1. Compute the set of tile cells the entity's AABB overlaps + (=entity-tile-cells=), in tile coordinates. The AABB is computed + from =#:x=, =#:y=, =#:width=, =#:height=; the lower edges use + =(- (+ coord size) 1)= so an entity exactly flush with a tile edge + is *not* considered overlapping that tile. +2. For each overlapping cell, if its tile id is non-zero, snap the + entity to the tile edge on that axis. The snap function + (=tile-push-pos=) is: + + #+begin_src scheme + ;; Moving forward (v>0): snap leading edge to tile's near edge. + ;; Moving backward (v<0): snap trailing edge to tile's far edge. + (if (> v 0) + (- (* coord tile-size) entity-size) + (* (+ coord 1) tile-size)) + #+end_src + +3. Zero the axis velocity so the entity doesn't slide through. + +X and Y are resolved in *separate passes* (see the pipeline order +above). Resolving them together tends to produce corner-clip bugs +where a player moving diagonally gets stuck at the corner of a floor +and a wall; the two-pass approach avoids this entirely. + +*** Entity collision algorithm + +=resolve-entity-collisions= walks all unique pairs =(i, j)= with =i < +j= of the entity list and calls =resolve-pair=. For each pair: + +- Both must have =#:solid? #t= and neither may list =entity-collisions= + in =#:skip-pipelines=. +- AABB overlap test (=aabb-overlap?=): strictly *overlapping*; + edge-touching does *not* count. +- On overlap, =push-apart= picks the minimum-penetration axis (X vs Y, + whichever overlap is smaller) and pushes each entity half the + overlap away from the other, setting velocity on that axis to ±1. +- If exactly one entity has =#:immovable? #t= (static geometry like a + platform), only the *other* entity moves, by the full overlap, and + its velocity on the separation axis is zeroed. +- Landing-on-top preference: when a movable body is falling onto a + static one and the movable's center is still above the static's + center, separation is forced *vertical* regardless of which axis has + the smaller overlap. This is what makes moving platforms usable — a + narrow horizontal overlap at the edge of a platform doesn't shove + the player off sideways. + +*** =#:on-ground?= as a query + +=#:on-ground?= is the single key you'll read most often. It is a +*result* of the pipeline (written by =detect-on-solid=), not an input. +It is re-computed every frame *after* both tile-collision passes but +*before* entity collisions. If your game needs ground detection to +reflect entity push-apart (rare — it matters for very thin platforms), +install a custom =engine-update= that calls =detect-on-solid= after +=resolve-entity-collisions=. + +*** =aabb-overlap?= for manual queries + +If you want to detect overlap *without* resolving it (bullets hitting +enemies, damage zones, pickups), call =aabb-overlap?= directly: #+begin_src scheme -(resolve-tile-collisions-y entity scene dt) +(aabb-overlap? x1 y1 w1 h1 x2 y2 w2 h2) ;; → #t or #f #+end_src -**Reads**: =#:x=, =#:y=, =#:width=, =#:height=, =#:vy= - -**Writes**: =#:y= (snapped to tile edge), =#:vy= (zeroed if collision occurs) +This is pure — it does not touch either entity. The *shmup demo* +(=demo/shmup.scm=) uses exactly this pattern to find bullet/enemy +overlaps and remove them from the scene. -**Description**: Detects all solid tiles overlapping the entity's AABB and snaps the entity to the nearest tile edge on the Y-axis. If a collision is found: +* Common patterns -- **Moving down** (vy > 0): entity's bottom edge is snapped to the top edge of the tile (position = tile-y - height) -- **Moving up** (vy < 0): entity's top edge is snapped to the bottom edge of the tile (position = tile-y + tile-height) +** Moving platform (=vx= + =immovable?=) -Velocity is zeroed. Call this immediately after =apply-velocity-y=. - -** detect-on-solid +A platform that slides horizontally and carries the player: #+begin_src scheme -(detect-on-solid entity scene dt) +(list #:type 'platform + #:x 200 #:y 300 + #:width 48 #:height 8 + #:vx 1 #:vy 0 + #:solid? #t #:immovable? #t + #:tags '(platform)) #+end_src -**Reads**: =#:gravity?=, =#:x=, =#:y=, =#:width=, =#:height=, =#:vy=; other entities from =(scene-entities scene)= for solid support - -**Writes**: =#:on-ground?= (set to =#t= if supported by a solid tile probe and/or by another solid's top surface, else =#f=) +- =#:solid? #t= makes the platform participate in + =resolve-entity-collisions=. +- =#:immovable? #t= means only *other* entities are pushed; the + platform's =#:x= / =#:y= are never touched by entity collisions. +- =#:vx 1= makes =apply-velocity-x= move the platform 1 px/frame. +- The platform has no =#:gravity?= so it doesn't fall. +- When the player lands on it, =detect-on-solid= sees the solid entity + directly below (via =entity-solid-support-below?=) and sets the + player's =#:on-ground?= to =#t=. +- Turn the platform around at the ends by flipping =#:vx= in your + =update:= hook when =#:x= reaches a limit. -**Description**: Despite the =?=-suffix, this returns an **updated entity** (it sets =#:on-ground?=), not a boolean. Ground is **either** (1) a solid tile one pixel below the feet (probe at both lower corners of the AABB), **or** (2) resting on another solid entity's **top** from the scene list (horizontal overlap, feet within a few pixels of that entity's top, and vertical speed small enough that we treat the body as supported—so moving platforms and crates count). If =scene-tilemap= is =#f=, the tile probe is skipped; entity–entity support still applies when other solids exist in the list. Only runs when =#:gravity?= is true. +For a platform that should *not* move horizontally on its own (a +purely static crate), leave =#:vx= / =#:vy= at 0. -Use =#:on-ground?= in =update:= to gate jump input; in =default-engine-update= this step runs **after** tile collision and **before** =resolve-entity-collisions= (see engine order). If you need ground detection to account for entity push-apart first, use a custom =engine-update= and call =detect-on-solid= after =resolve-entity-collisions=. +** Disabling gravity on a bullet -** resolve-entity-collisions +A bullet wants straight-line motion, no gravity, no ground detection, +no entity push-apart (it destroys enemies on contact, it doesn't shove +them): #+begin_src scheme -(resolve-entity-collisions entities) +(list #:type 'bullet + #:x 100 #:y 200 + #:width 4 #:height 8 + #:vx 0 #:vy -6 + ;; Omit #:gravity?, so apply-acceleration, apply-gravity, + ;; and detect-on-solid are all no-ops for this entity. + ;; Omit #:solid?, so it doesn't enter resolve-entity-collisions. + #:tags '(bullet)) #+end_src -**Input**: A list of entity plists - -**Reads**: For each entity: =#:solid?=, =#:x=, =#:y=, =#:width=, =#:height=, =#:vx=, =#:vy= - -**Writes**: For colliding pairs: =#:x=, =#:y=, =#:vx=, =#:vy= (pushed apart, velocities set to ±1) - -**Description**: Performs all-pairs AABB overlap detection. For each pair of entities where BOTH have =#:solid? #t=, if they overlap: if one has =#:immovable? #t=, only the other entity is displaced (its velocity on that axis is zeroed). For movable vs immovable, separation prefers **vertical** resolution when the movable’s center is still **above** the immovable’s center (typical landing on a platform), so narrow horizontal overlap does not shove the mover sideways through the edge. Otherwise the shallow overlap axis is used. If both are immovable, the pair is skipped. If neither is immovable, the two bodies are pushed apart along the smaller overlap axis and their velocities on that axis are set to ±1. +=apply-velocity-x= and =apply-velocity-y= still move the bullet +(-6 px/frame upward). Tile collisions still run, which is usually what +you want — bullets can hit walls. For a bullet that passes through +walls, add =#:skip-pipelines '(tile-collisions-x tile-collisions-y)=. -Entities without =#:solid?= or with =#:solid? #f= are skipped. Returns a new entity list with collisions resolved. +Use =aabb-overlap?= in your =update:= hook to detect hits against +enemies. -This is relatively expensive: O(n²) for n entities. Use only when entity count is low (< 100) or for game objects where push-apart is desired. +** Skipping just tile collisions on one entity -** Using =resolve-entity-collisions= on a scene - -Apply the pure list function via =scene-transform-entities= (from =downstroke-world=): +Sometimes you have an entity (pickup, decorative particle, ghost) that +should move via =#:vx= / =#:vy= but *not* snap against tiles: #+begin_src scheme -(scene-transform-entities scene resolve-entity-collisions) +(list #:type 'ghost + #:x 100 #:y 100 #:width 16 #:height 16 + #:vx 2 #:vy 0 + #:skip-pipelines '(tile-collisions-x tile-collisions-y)) #+end_src -Returns a new scene with the updated entity list; the original scene is not modified. - -** aabb-overlap? - -#+begin_src scheme -(aabb-overlap? x1 y1 w1 h1 x2 y2 w2 h2) -#+end_src +Velocity integration still runs. Tile-collision snapping and velocity +zeroing are both bypassed for this entity only. The rest of the scene +collides with tiles as normal. -**Returns**: Boolean (#t if overlapping, #f if separated or touching) +** Replacing the pipeline with a custom =engine-update:= -**Description**: Pure collision test for two axis-aligned bounding boxes. Does not modify any state. Useful for manual collision queries (e.g., "did bullet hit enemy?") where you want to decide the response manually rather than using push-apart. - -#+begin_src scheme -;; Example: check if player overlaps enemy -(if (aabb-overlap? (entity-ref player #:x 0) (entity-ref player #:y 0) - (entity-ref player #:width 0) (entity-ref player #:height 0) - (entity-ref enemy #:x 0) (entity-ref enemy #:y 0) - (entity-ref enemy #:width 0) (entity-ref enemy #:height 0)) - (do-damage!)) -#+end_src - -* Tile Collision Model - -Tiles come from **TMX maps** loaded by the tilemap module. The tilemap parses the XML and builds a 2D tile grid. Each cell references a tile ID from the tileset. - -**Solid tiles** are identified by **tile metadata** in the TSX tileset (defined in Tiled editor). The physics module checks each tile to see if it has collision metadata. Tiles with no metadata are treated as non-solid background tiles. - -** How Collision Works - -1. Entity's AABB (x, y, width, height) is used to compute all overlapping tile cells -2. For each cell, the physics module queries the tilemap: "does this cell have a solid tile?" -3. If yes, the entity is snapped to the edge of that tile in the axis being resolved -4. Velocity is zeroed in that axis to prevent sliding - -** Why X and Y are Separate - -Tile collisions are resolved in two passes: - -#+begin_src -apply-velocity-x → resolve-tile-collisions-x → apply-velocity-y → resolve-tile-collisions-y -#+end_src - -This prevents a common platformer bug: if you moved and collided on both axes at once, you could get "corner clipped" (stuck in a diagonal corner). By separating X and Y, you guarantee the entity snaps cleanly. - -* Entity-Entity Collision - -Entity-entity collisions are for when two **game objects** overlap: a player and an enemy, two boxes stacked, etc. - -=resolve-entity-collisions= does all-pairs AABB checks. Any entity with =#:solid? #t= that overlaps another solid entity is pushed apart. - -The push-apart is along the **minimum penetration axis** (X or Y, whichever overlap is smaller). Entities are pushed in opposite directions by half the overlap distance, and their velocities are set to ±1 to prevent stacking. - -** When to Use - -- **Physics sandbox**: multiple boxes/balls that should bounce apart -- **Solid obstacles**: pushable crates that block the player -- **Knockback**: enemies that push back when hit - -** When NOT to Use - -- **Trigger zones**: enemies that should detect the player without colliding — use =aabb-overlap?= manually -- **Bullets**: should pass through enemies — don't mark bullet as solid, use =aabb-overlap?= to check hits -- **High entity count**: 100+ entities = O(n²) is too slow - -* Platformer Example - -A minimal platformer with a player, gravity, jumping, and tile collisions. Physics runs in =default-engine-update=; =update:= is input → intent only: - -#+begin_src scheme -;; Omit engine-update: or use default — physics runs automatically before this hook. -update: (lambda (game dt) - (let* ((input (game-input game)) - (scene (game-scene game)) - (player (car (scene-entities scene))) - (jump? (and (input-pressed? input 'a) - (entity-ref player #:on-ground? #f))) - (player (entity-set player #:vx - (cond - ((input-held? input 'left) -3) - ((input-held? input 'right) 3) - (else 0))))) - (when jump? (play-sound 'jump)) - (let ((player (if jump? - (entity-set player #:ay (- *jump-force*)) - player))) - (game-scene-set! game - (update-scene scene entities: (list player)))))) -#+end_src - -Import =*jump-force*= from =downstroke-physics= (or use a literal such as =-15= for =#:ay=). - -** Step-by-Step - -1. **Set #:vx** from input (left = -3, right = +3, idle = 0) -2. **Jump**: if A pressed and =#:on-ground?= (already updated for this frame — your =update:= runs after =engine-update=), play sound and set =#:ay= to =(- *jump-force*)= -3. **Next frame’s =engine-update=** runs =apply-acceleration= through =sync-groups= as documented above - -* Top-Down Example - -A top-down game with no gravity and free 4-way movement. Give the player =#:gravity? #f= so acceleration, gravity, and ground detection no-op inside =default-engine-update=: - -#+begin_src scheme -update: (lambda (game dt) - (let* ((input (game-input game)) - (scene (game-scene game)) - (player (car (scene-entities scene))) - (dx (+ (if (input-held? input 'left) -3 0) - (if (input-held? input 'right) 3 0))) - (dy (+ (if (input-held? input 'up) -3 0) - (if (input-held? input 'down) 3 0))) - (player (entity-set (entity-set player #:vx dx) #:vy dy))) - ;; Camera follow still runs after update: in game-run! - (camera-x-set! (scene-camera scene) (max 0 (- (entity-ref player #:x 0) 300))) - (game-scene-set! game - (update-scene scene entities: (list player))))) -#+end_src - -** Step-by-Step - -1. **Read input**: combine left/right into dx, up/down into dy -2. **Set velocity**: both #:vx and #:vy based on input; the next =engine-update= applies velocity and tile collision -3. **No gravity**: =#:gravity? #f= skips =apply-acceleration=, =apply-gravity=, =detect-on-solid= - -The built-in pipeline still runs; most steps no-op or pass through for non-gravity entities. - -* Physics Sandbox Example - -The real =demo/sandbox.scm= relies on =default-engine-update= for boxes, rafts, and tile collision; =update:= only runs per-entity AI (e.g. bots) via =scene-map-entities=. - -If you **replace** =engine-update= with your own procedure, a full multi-entity pass looks like this (same step order as =default-engine-update=, minus tweens/sync if you omit them): +If you need a different step order — the canonical reason is "check +=#:on-ground?= *after* entity collisions so an entity standing on a +just-pushed platform is seen as grounded in this tick" — you can pass +your own procedure on the scene: #+begin_src scheme (define (my-engine-update game dt) (let ((scene (game-scene game))) (when scene (game-scene-set! game - (scene-transform-entities - (scene-map-entities scene - (lambda (e) (apply-gravity e scene dt)) - (lambda (e) (apply-velocity-x e scene dt)) - (lambda (e) (resolve-tile-collisions-x e scene dt)) - (lambda (e) (apply-velocity-y e scene dt)) - (lambda (e) (resolve-tile-collisions-y e scene dt)) - (lambda (e) (detect-on-solid e scene dt))) - resolve-entity-collisions))))) -#+end_src - -Prefer reusing =default-engine-update= unless you need a different order (e.g. =detect-on-solid= after entity collisions). A complete custom hook should also run =step-tweens= first and =sync-groups= last, matching =engine.scm=. - -** step-by-step - -1. **scene-map-entities**: applies each step to all entities in order (snippet above starts at gravity for brevity) - - =apply-gravity= (all entities fall) - - =apply-velocity-x=, =resolve-tile-collisions-x= (move and collide on x-axis) - - =apply-velocity-y=, =resolve-tile-collisions-y= (move and collide on y-axis) - - =detect-on-solid= (set #:on-ground?) - -2. **scene-transform-entities** with **resolve-entity-collisions**: after all entities are moved and collided with tiles, resolve entity-entity overlaps (boxes pushing apart) - -This pattern matches how multiple movers interact in the sandbox demo, except the demo uses the built-in =default-engine-update= instead of a hand-rolled hook. - -** Capturing =scene= and =dt= - -=scene-map-entities= passes only each entity into the step. Per-entity physics steps take =(entity scene dt)=, so wrap them: - -#+begin_src scheme -(lambda (e) (resolve-tile-collisions-x e scene dt)) + (chain scene + (scene-map-entities _ (cut step-tweens <> <> dt)) + (scene-map-entities _ (cut apply-acceleration <> <> dt)) + (scene-map-entities _ (cut apply-gravity <> <> dt)) + (scene-map-entities _ (cut apply-velocity-x <> <> dt)) + (scene-map-entities _ (cut resolve-tile-collisions-x <> <> dt)) + (scene-map-entities _ (cut apply-velocity-y <> <> dt)) + (scene-map-entities _ (cut resolve-tile-collisions-y <> <> dt)) + (scene-transform-entities _ resolve-entity-collisions) + ;; Re-order: detect-on-solid AFTER entity collisions. + (scene-map-entities _ (cut detect-on-solid <> <> dt)) + (scene-transform-entities _ sync-groups) + (scene-map-entities _ (cut apply-animation <> <> dt))))))) + +;; Install it on the scene: +(make-scene + entities: ... + tilemap: tm + camera: (make-camera x: 0 y: 0) + tileset-texture: tex + engine-update: my-engine-update) #+end_src -Use the same =dt= (milliseconds) that =game-run!= passes to your hooks. +Prefer starting from =default-engine-update= and tweaking one thing, +rather than writing a pipeline from scratch. Forgetting =step-tweens=, +=sync-groups=, or =apply-animation= is the usual mistake — tweens stop +advancing, multi-part entities fall apart, animations freeze. -* Common Patterns +** Turning the pipeline off entirely (='none=) -** Jumping +For a shmup or any game where motion rules are entirely +per-entity-type, set =engine-update: 'none= on the scene. The +*Shmup demo* — run with =bin/demo-shmup=, source in =demo/shmup.scm= — +does exactly this: player moves via =apply-velocity-x= called +inline, bullets and enemies move via a bespoke =move-projectile= +helper, collisions are checked with =aabb-overlap?= and dead entities +are filtered out. No gravity, no ground detection, no pairwise +push-apart: #+begin_src scheme -;; In update: (after physics, #:on-ground? is current) -(let ((jump? (and (input-pressed? input 'a) - (entity-ref player #:on-ground? #f)))) - (if jump? - (entity-set player #:ay (- *jump-force*)) - player)) +(make-scene + entities: (list (make-player)) + tilemap: #f + camera: (make-camera x: 0 y: 0) + tileset-texture: #f + camera-target: #f + engine-update: 'none) #+end_src -=*jump-force*= (default 15) is exported from =downstroke-physics=. =apply-acceleration= and gravity run inside =default-engine-update= on the following frame. +With ='none=, nothing in =physics.scm= runs unless you call it +yourself. You still have access to every physics procedure as a +library: =apply-velocity-x=, =aabb-overlap?=, =resolve-tile-collisions-y=, +and so on are all exported from =downstroke-physics= and usable +individually. ='none= just disables the *automatic* orchestration. -** No Gravity +** Reading =#:on-ground?= to gate jumps -For top-down games, use =#:gravity? #f= on movers so acceleration, gravity, and ground detection no-op. For shmups or fully custom motion, use =engine-update: #f= and move entities in =update:=. - -** Knockback - -Set #:vx or #:vy directly to push an entity: +The jump check in the *Platformer demo*: #+begin_src scheme -(entity-set enemy #:vx -5) ;; Push left -#+end_src - -Gravity will resume on the next frame if =#:gravity?= is #t. - -** Trigger Zones - -For areas that don't collide but detect presence (spawn zones, damage zones): - -#+begin_src scheme -;; In update: -(if (aabb-overlap? (entity-ref trigger #:x 0) (entity-ref trigger #:y 0) - (entity-ref trigger #:width 0) (entity-ref trigger #:height 0) - (entity-ref player #:x 0) (entity-ref player #:y 0) - (entity-ref player #:width 0) (entity-ref player #:height 0)) - (on-trigger-enter! player)) +(define (update-player player input) + (let* ((jump? (and (input-pressed? input 'a) + (entity-ref player #:on-ground? #f))) + (player (entity-set player #:vx (player-vx input)))) + (when jump? (play-sound 'jump)) + (if jump? + (entity-set player #:ay (- *jump-force*)) + player))) #+end_src -Don't mark the trigger as solid; just use =aabb-overlap?= to query. - -** Slopes and Ramps - -The current tile collision system is AABB-based and does not support slopes. All tiles are axis-aligned rectangles. To simulate slopes, you can: - -1. Use small tiles to approximate a slope (many tiny tiles at angles) -2. Post-process the player's position after collision (rotate velocity vector) -3. Use a custom collision function instead of =resolve-tile-collisions-y= - -This is beyond the scope of the basic physics system. - -* Performance Notes - -- **Tile collision**: O(overlapping tiles) per entity. Usually 1–4 tiles per frame. -- **Entity collision**: O(n²) where n = entity count. Avoid for > 100 solid entities. -- **Ground detection**: O(2) tile lookups per entity (corners below feet). - -For large games, consider spatial partitioning (grid, quadtree) to cull entity pairs. The basic physics system is designed for small to medium games (< 100 entities). - -* Troubleshooting - -** Entity Sinks Into Floor - -- Make sure =#:height= is set correctly (not 0 or too large) -- Verify tileset metadata marks floor tiles as solid in Tiled editor -- Check that =resolve-tile-collisions-y= is called after =apply-velocity-y= - -** Double-Jump / Can't Jump - -- Ensure =#:on-ground?= is set: with =default-engine-update=, =detect-on-solid= runs each frame; for standing on other solids, note it runs *before* =resolve-entity-collisions= in the default order (see *detect-on-solid* above) -- Gate jump in =update:= with =#:on-ground?= and =input-pressed?= (not =input-held?=) -- Set =#:ay= only on the jump frame; =apply-acceleration= clears =#:ay= when it runs - -** Entity Slides Through Walls - -- Check that #:width and #:height are set correctly -- Verify tileset marks wall tiles as solid -- Ensure =resolve-tile-collisions-x= is called after =apply-velocity-x= - -** Entities Get Stuck Overlapping - -- Use =(scene-transform-entities scene resolve-entity-collisions)= after all physics steps -- Verify both entities have =#:solid? #t= -- Reduce =*gravity*= or max velocity if entities are moving too fast (can cause multi-frame overlap) - -** Performance Drops - -- Profile entity count (should be < 100 for full physics pipeline) -- Disable =resolve-entity-collisions= if not needed -- Use =aabb-overlap?= for queries instead of marking many entities solid +- =input-pressed?= (not =input-held?=) ensures one jump per keypress. +- =#:on-ground?= was updated by =detect-on-solid= in the pipeline + *this frame*, before your =update:= ran, so it reflects the current + position after tile / entity collisions. +- =#:ay= is cleared by =apply-acceleration= on the next frame, so the + impulse applies exactly once. + +* See also + +- [[file:guide.org][Getting started guide]] — overall game structure and the minimal + example that calls into the physics pipeline. +- [[file:entities.org][Entities]] — the keys the pipeline reads and writes + (=#:x=, =#:vy=, =#:gravity?=, =#:solid?=, =#:skip-pipelines=, etc.), + =entity-ref= / =entity-set=, and the =define-pipeline= macro. +- [[file:tweens.org][Tweens]] — =step-tweens= as the first step of the pipeline, and how + to combine tweens with =#:skip-pipelines= for knockback-style effects + that bypass velocity integration. +- [[file:animation.org][Animation]] — =apply-animation= as the last step of the pipeline; + =#:animations=, =#:anim-name=, =#:anim-frame=. +- [[file:input.org][Input]] — =input-held?= / =input-pressed?= for reading movement and + jump intent in =update:=. +- [[file:scenes.org][Scenes]] — the =engine-update= field of =make-scene=, group entities + (=#:group-id=, =#:group-origin?=) consumed by =sync-groups=, and + =scene-map-entities= / =scene-transform-entities= used throughout + the pipeline. +- [[file:rendering.org][Rendering]] — how =#:tile-id= (written by =apply-animation=) is drawn, + and where the camera transform is applied. +- [[file:../demo/platformer.scm][Platformer]] (=bin/demo-platformer=) — canonical gravity + jump + tile-collide example. +- [[file:../demo/shmup.scm][Shmup]] (=bin/demo-shmup=) — canonical =engine-update: 'none= example with manual collision checks via =aabb-overlap?=. +- [[file:../demo/sandbox.scm][Sandbox]] (=bin/demo-sandbox=) — multiple movers, entity–entity push-apart, the default pipeline in a busier scene. +- [[file:../demo/tweens.scm][Tweens]] (=bin/demo-tweens=) — =step-tweens= in action; useful when combined with =#:skip-pipelines '(velocity-x velocity-y)=. |
