#+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: input ↓ engine-update (default-engine-update unless overridden) ↓ update: (your game logic — set #:vx, #:vy, #:ay, ...) ↓ camera follow ↓ render 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) 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 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 (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 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. *** =define-pipeline= (how steps are declared) All per-entity pipeline steps are declared with =define-pipeline= from =downstroke-entity= (see =docs/entities.org=). The shape is: #+begin_src scheme (define-pipeline (procedure-name skip-symbol) (scene entity dt) guard: (some-expression-over entity) (body ...)) #+end_src 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. ** Overriding or disabling the pipeline A scene has an =engine-update= field. The game's frame loop dispatches on it every tick: #+begin_src scheme ;; 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 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 (aabb-overlap? x1 y1 w1 h1 x2 y2 w2 h2) ;; → #t or #f #+end_src 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. * Common patterns ** Moving platform (=vx= + =immovable?=) A platform that slides horizontally and carries the player: #+begin_src scheme (list #:type 'platform #:x 200 #:y 300 #:width 48 #:height 8 #:vx 1 #:vy 0 #:solid? #t #:immovable? #t #:tags '(platform)) #+end_src - =#: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. For a platform that should *not* move horizontally on its own (a purely static crate), leave =#:vx= / =#:vy= at 0. ** Disabling gravity on a bullet 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 (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 =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)=. Use =aabb-overlap?= in your =update:= hook to detect hits against enemies. ** Skipping just tile collisions on one entity Sometimes you have an entity (pickup, decorative particle, ghost) that should move via =#:vx= / =#:vy= but *not* snap against tiles: #+begin_src scheme (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 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. ** Replacing the pipeline with a custom =engine-update:= 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 (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 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. ** Turning the pipeline off entirely (='none=) 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 (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 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. ** Reading =#:on-ground?= to gate jumps The jump check in the *Platformer demo*: #+begin_src scheme (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 - =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)=.