aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--demo/platformer.scm35
-rw-r--r--demo/sandbox.scm46
-rw-r--r--demo/scaling.scm3
-rw-r--r--demo/shmup.scm5
-rw-r--r--demo/topdown.scm17
-rw-r--r--demo/tweens.scm8
-rw-r--r--docs/api.org109
-rw-r--r--docs/entities.org59
-rw-r--r--docs/guide.org50
-rw-r--r--docs/physics.org250
-rw-r--r--docs/tweens.org11
-rw-r--r--engine.scm28
-rw-r--r--physics.scm40
-rw-r--r--tests/engine-test.scm44
-rw-r--r--tests/physics-test.scm119
-rw-r--r--tests/tween-test.scm10
-rw-r--r--tween.scm2
-rw-r--r--world.scm3
18 files changed, 391 insertions, 448 deletions
diff --git a/demo/platformer.scm b/demo/platformer.scm
index 3bad9bd..1a24a8f 100644
--- a/demo/platformer.scm
+++ b/demo/platformer.scm
@@ -1,14 +1,11 @@
(import scheme
(chicken base)
(chicken process-context)
- (only srfi-197 chain)
(prefix sdl2 "sdl2:")
(prefix sdl2-ttf "ttf:")
(prefix sdl2-image "img:")
downstroke-engine
downstroke-world
- downstroke-tilemap
- downstroke-renderer
downstroke-input
downstroke-physics
downstroke-assets
@@ -31,19 +28,14 @@
((input-held? input 'right) 3)
(else 0)))
-(define (update-player player input tm)
- (let ((jump? (input-pressed? input 'a)))
- (when (and jump? (entity-ref player #:on-ground? #f))
- (play-sound 'jump))
- (chain (entity-set player #:vx (player-vx input))
- (apply-jump _ jump?)
- (apply-acceleration _)
- (apply-gravity _)
- (apply-velocity-x _)
- (resolve-tile-collisions-x _ tm)
- (apply-velocity-y _)
- (resolve-tile-collisions-y _ tm)
- (detect-on-solid _ tm))))
+(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)))
(define *game*
(make-game
@@ -55,16 +47,15 @@
(load-sounds! '((jump . "demo/assets/jump.wav"))))
create: (lambda (game)
- (game-scene-set! game
- (chain (game-load-scene! game "demo/assets/level-0.tmx")
- (scene-add-entity _ (make-player))
- (update-scene _ camera-target: 'player))))
+ (let* ((s0 (game-load-scene! game "demo/assets/level-0.tmx"))
+ (s1 (scene-add-entity s0 (make-player)))
+ (s2 (update-scene s1 camera-target: 'player)))
+ (game-scene-set! game s2)))
update: (lambda (game dt)
(let* ((input (game-input game))
(scene (game-scene game))
- (tm (scene-tilemap scene))
- (player (update-player (car (scene-entities scene)) input tm)))
+ (player (update-player (car (scene-entities scene)) input)))
(game-scene-set! game
(update-scene scene entities: (list player)))))))
diff --git a/demo/sandbox.scm b/demo/sandbox.scm
index a34ebd9..09c31fb 100644
--- a/demo/sandbox.scm
+++ b/demo/sandbox.scm
@@ -2,7 +2,6 @@
(chicken base)
(chicken random)
(only srfi-1 iota take)
- (only srfi-197 chain)
(prefix sdl2 "sdl2:")
(prefix sdl2-ttf "ttf:")
(prefix sdl2-image "img:")
@@ -73,39 +72,18 @@
#:tile-id 1
#:demo-id id #:demo-since-jump 0))
-;; ── Per-entity physics ──────────────────────────────────────────────────────
+;; ── Per-entity intent ───────────────────────────────────────────────────────
-(define (run-physics e tm)
- (chain e
- (apply-gravity _)
- (apply-velocity-x _)
- (resolve-tile-collisions-x _ tm)
- (apply-velocity-y _)
- (resolve-tile-collisions-y _ tm)))
-
-(define (update-demo-bot e dt tm)
+(define (update-demo-bot e dt)
(let* ((id (entity-ref e #:demo-id 0))
(phase (modulo (+ *demo-t* (* id 400.0)) +demo-bot-cycle-ms+))
(vx (if (< phase +demo-bot-half-cycle-ms+) 3.0 -3.0))
- (e (entity-set e #:vx vx))
(ground? (entity-ref e #:on-ground? #f))
(since (+ (entity-ref e #:demo-since-jump 0) dt))
(jump? (and ground? (>= since +demo-bot-jump-interval-ms+)))
- (since (if jump? 0 since)))
- (chain (entity-set e #:demo-since-jump since)
- (apply-jump _ jump?)
- (apply-acceleration _)
- (run-physics _ tm))))
-
-(define (integrate-entity e dt tm)
- (case (entity-type e)
- ((demo-bot) (update-demo-bot e dt tm))
- ((box) (run-physics e tm))
- (else
- (if (and (entity-ref e #:group-origin? #f)
- (entity-ref e #:gravity? #f))
- (run-physics e tm)
- e))))
+ (since (if jump? 0 since))
+ (ay (if jump? (- *jump-force*) 0)))
+ (entity-set (entity-set (entity-set e #:vx vx) #:demo-since-jump since) #:ay ay)))
;; ── Scene builder ───────────────────────────────────────────────────────────
@@ -159,14 +137,12 @@
update: (lambda (game dt)
(set! *demo-t* (+ *demo-t* dt))
- (let ((tm (scene-tilemap (game-scene game))))
+ (let ((scene (game-scene game)))
(game-scene-set! game
- (chain (game-scene game)
- (scene-map-entities _ (cut step-tweens <> dt))
- (scene-map-entities _ (cut integrate-entity <> dt tm))
- (scene-transform-entities _ sync-groups)
- (scene-transform-entities _ resolve-entity-collisions)
- (scene-map-entities _
- (lambda (e) (detect-on-solid e tm (scene-entities _))))))))))
+ (scene-map-entities scene
+ (lambda (e)
+ (if (eq? (entity-type e) 'demo-bot)
+ (update-demo-bot e dt)
+ e))))))))
(game-run! *game*)
diff --git a/demo/scaling.scm b/demo/scaling.scm
index f8bfdfb..982817a 100644
--- a/demo/scaling.scm
+++ b/demo/scaling.scm
@@ -36,7 +36,8 @@
camera: (make-camera x: 0 y: 0)
tileset-texture: #f
camera-target: #f
- background: '(30 30 50))))
+ background: '(30 30 50)
+ engine-update: 'none)))
update: (lambda (game dt)
(let* ((input (game-input game))
diff --git a/demo/shmup.scm b/demo/shmup.scm
index fdffd71..f4897ae 100644
--- a/demo/shmup.scm
+++ b/demo/shmup.scm
@@ -79,7 +79,7 @@
(define (update-player player input)
(let ((updated (chain player
(entity-set _ #:vx (player-vx input))
- (apply-velocity-x _)
+ (apply-velocity-x _ #f 0)
(clamp-player-x _))))
(when (input-pressed? input 'a)
(play-sound 'shoot))
@@ -133,7 +133,8 @@
tilemap: #f
camera: (make-camera x: 0 y: 0)
tileset-texture: #f
- camera-target: #f)))
+ camera-target: #f
+ engine-update: 'none)))
update: (lambda (game dt)
(set! *frame-count* (+ *frame-count* 1))
diff --git a/demo/topdown.scm b/demo/topdown.scm
index 1bf6536..7fa9b7e 100644
--- a/demo/topdown.scm
+++ b/demo/topdown.scm
@@ -1,15 +1,13 @@
(import scheme
(chicken base)
+ srfi-8
(only srfi-197 chain)
(prefix sdl2 "sdl2:")
(prefix sdl2-ttf "ttf:")
(prefix sdl2-image "img:")
downstroke-engine
downstroke-world
- downstroke-tilemap
- downstroke-renderer
downstroke-input
- downstroke-physics
downstroke-assets
downstroke-entity
downstroke-scene-loader)
@@ -28,15 +26,9 @@
(+ (if (input-held? input 'up) -3 0)
(if (input-held? input 'down) 3 0))))
-(define (update-player player input tm)
+(define (update-player player input)
(receive (dx dy) (input->velocity input)
- (chain player
- (entity-set _ #:vx dx)
- (entity-set _ #:vy dy)
- (apply-velocity-x _)
- (resolve-tile-collisions-x _ tm)
- (apply-velocity-y _)
- (resolve-tile-collisions-y _ tm))))
+ (entity-set (entity-set player #:vx dx) #:vy dy)))
(define *game*
(make-game
@@ -51,8 +43,7 @@
update: (lambda (game dt)
(let* ((input (game-input game))
(scene (game-scene game))
- (player (update-player (car (scene-entities scene))
- input (scene-tilemap scene))))
+ (player (update-player (car (scene-entities scene)) input)))
(game-scene-set! game
(update-scene scene entities: (list player)))))))
diff --git a/demo/tweens.scm b/demo/tweens.scm
index b2a22cc..609c541 100644
--- a/demo/tweens.scm
+++ b/demo/tweens.scm
@@ -6,8 +6,7 @@
downstroke-engine
downstroke-world
downstroke-renderer
- downstroke-entity
- downstroke-tween)
+ downstroke-entity)
;; ── Constants ────────────────────────────────────────────────────────────────
@@ -81,11 +80,6 @@
camera-target: #f
background: '(26 28 34))))
- update: (lambda (game dt)
- (game-scene-set! game
- (scene-map-entities (game-scene game)
- (cut step-tweens <> dt))))
-
render: (lambda (game)
(draw-ease-labels! (game-renderer game)
(scene-entities (game-scene game))))))
diff --git a/docs/api.org b/docs/api.org
index e60ef7d..673749f 100644
--- a/docs/api.org
+++ b/docs/api.org
@@ -23,6 +23,7 @@ The engine module provides the top-level game lifecycle and state management.
(preload #f)
(create #f)
(update #f)
+ (engine-update 'default)
(render #f)
(debug? #f))
#+end_src
@@ -39,7 +40,8 @@ Creates and initializes a game object. All parameters are optional keywords.
| ~input-config~ | input-config | *default-input-config* | Keyboard/controller mappings |
| ~preload~ | procedure/false | #f | Hook: ~(lambda (game) ...)~ called once before create |
| ~create~ | procedure/false | #f | Hook: ~(lambda (game) ...)~ called once at startup |
-| ~update~ | procedure/false | #f | Hook: ~(lambda (game dt) ...)~ called each frame |
+| ~engine-update~ | procedure/false / ~'default~ | ~'default~ | Built-in physics pipeline (~default-engine-update~), ~#f~ to disable, or ~(lambda (game dt) ...)~ to replace |
+| ~update~ | procedure/false | #f | Hook: ~(lambda (game dt) ...)~ called each frame *after* ~engine-update~ (see ~game-run!~) |
| ~render~ | procedure/false | #f | Hook: ~(lambda (game) ...)~ called after render-scene! |
| ~debug?~ | boolean | #f | Enable debug overlay drawing (collision boxes) |
@@ -61,6 +63,18 @@ This affects everything uniformly: tiles, sprites, text, colored rectangles, and
Only positive integers are accepted; fractional or zero values signal an error.
+*** ~engine-update:~ and ~default-engine-update~
+
+Omit ~engine-update:~ (or pass ~engine-update: 'default~) to use ~default-engine-update~: the standard per-frame physics pipeline on the current scene. Pass ~engine-update: #f~ if your game does not use the built-in pipeline (for example a shmup or menu that moves entities entirely in ~update:~). Pass a custom procedure ~(lambda (game dt) ...)~ to run your own integration step instead; it should read and write the scene via ~(game-scene game)~ and ~(game-scene-set! game scene)~ like ~default-engine-update~ does.
+
+#+begin_src scheme
+(default-engine-update game dt)
+#+end_src
+
+Runs the built-in pipeline once. Order: ~step-tweens~ → ~apply-acceleration~ → ~apply-gravity~ → ~apply-velocity-x~ → ~resolve-tile-collisions-x~ → ~apply-velocity-y~ → ~resolve-tile-collisions-y~ → ~detect-on-solid~ → ~resolve-entity-collisions~ (whole entity list) → ~sync-groups~ (whole entity list). Per-entity steps use signature ~(entity scene dt)~; bulk steps use ~(entities)~ with ~scene-transform-entities~.
+
+**Typical ~update:~ pattern:** treat ~update:~ as game logic only: read input, set *intent* on entities (~#:vx~, ~#:vy~, one-shot ~#:ay~ for jumps, flags), animation, and sound. Each frame, ~engine-update~ runs *before* ~update:~, so your hook sees positions and ~#:on-ground?~ after that frame’s integration and collisions. The ~#:vx~ / ~#:vy~ / ~#:ay~ you set in ~update:~ are applied starting on the following frame’s ~engine-update~.
+
*** Fullscreen
Downstroke does not provide a built-in ~fullscreen:~ keyword, but you can make any game fullscreen by setting the SDL2 window flag after the game starts. Use the ~preload:~ hook (the window exists by then):
@@ -90,15 +104,17 @@ Combine with ~scale:~ to get pixel-perfect fullscreen: set your logical resoluti
Starts the main event loop. Initializes SDL2, opens the window (at ~width×scale~ by ~height×scale~ pixels), sets the logical render size when ~scale~ > 1, and runs the frame loop indefinitely until the user quits or the ~quit~ action is pressed. Never returns.
-Lifecycle order within each frame:
+Lifecycle order within each frame (while not quitting):
1. Collect SDL2 events
2. Update input state
-3. Call ~update:~ hook (or active state's ~update~)
-4. Set the renderer clear color from the current scene's ~background:~ (see ~make-scene~), then clear the framebuffer (~#f~ or invalid value uses opaque black)
-5. Call ~render-scene!~ (if scene is set)
-6. Call ~render:~ hook (or active state's ~render~)
-7. Present renderer
-8. Apply frame delay
+3. Call ~engine-update-hook~ if set (~default-engine-update~ runs the physics pipeline: tweens, acceleration, gravity, velocity, collisions, ground detection, entity collisions, group sync)
+4. Call ~update:~ hook (or active state's ~update~) — runs *after* physics, so logic sees resolved positions and ~#:on-ground?~
+5. Apply camera follow (~update-scene~ with ~camera-target~, if set)
+6. Set the renderer clear color from the current scene's ~background:~ (see ~make-scene~), then clear the framebuffer (~#f~ or invalid value uses opaque black)
+7. Call ~render-scene!~ (if scene is set)
+8. Call ~render:~ hook (or active state's ~render~)
+9. Present renderer
+10. Apply frame delay
** ~game-camera~
@@ -269,11 +285,13 @@ Example:
(define (increment-x entity)
(entity-set entity #:x (+ 1 (entity-ref entity #:x 0))))
-(define (apply-gravity entity)
- (entity-set entity #:vy (+ 1 (entity-ref entity #:vy 0))))
+;; Per-entity physics steps need scene and dt (e.g. from your update: hook):
+(define dt 16) ; example: ms since last frame
-(scene-map-entities scene increment-x apply-gravity)
-; Each entity is passed through increment-x, then through apply-gravity
+(scene-map-entities scene
+ increment-x
+ (lambda (e) (apply-gravity e scene dt)))
+; Each entity is passed through increment-x, then apply-gravity with fixed scene/dt
#+end_src
** ~scene-transform-entities~
@@ -432,7 +450,7 @@ Returns true if ~step-symbol~ appears in the entity’s ~#:skip-pipelines~ list
The ~guard:~ clause is optional. When present, ~guard-expr~ is evaluated first; if it is false, the entity is returned unchanged and ~body ...~ does not run. When absent, the body applies to all entities (subject only to the skip-symbol check below).
-Syntax for authors of per-entity pipeline steps: expands to a ~define~ that returns the **first** formal (the entity) unchanged when ~skip-symbol~ is listed in ~#:skip-pipelines~; otherwise, if a guard is present and fails, returns the entity unchanged; otherwise runs ~body ...~ inside ~(let () ...)~. Used throughout ~downstroke-physics~; other modules can use it for consistent skip behavior. The procedure name and skip symbol differ when needed (e.g. ~detect-on-solid~ vs ~on-solid~).
+Syntax for authors of per-entity pipeline steps: expands to a ~define~ that returns the **first** formal (the entity) unchanged when ~skip-symbol~ is listed in ~#:skip-pipelines~; otherwise, if a guard is present and fails, returns the entity unchanged; otherwise runs ~body ...~ inside ~(let () ...)~. Used throughout ~downstroke-physics~ and ~step-tweens~ in ~downstroke-tween~; other modules can use it for consistent skip behavior. Extra formals after the entity are typically ~scene~ and ~dt~ so steps match ~(entity scene dt)~. The procedure name and skip symbol differ when needed (e.g. ~detect-on-solid~ vs ~on-solid~).
** Shared Entity Keys
@@ -465,21 +483,24 @@ All entities can have these keys. Not all are required:
(import downstroke-physics)
#+end_src
-The physics module provides functions for movement, collision detection, and ground sensing. Call them manually in your ~update:~ hook in the order that suits your game type (see ~docs/physics.org~ for examples).
+The physics module provides functions for movement, collision detection, and ground sensing. With the default ~engine-update:~ hook (~default-engine-update~), they run automatically each frame in a fixed order. You can still call them yourself when building a custom ~engine-update:~ or when experimenting in the REPL (see ~docs/physics.org~).
-** Physics Pipeline Order
+** Physics Pipeline Order (~default-engine-update~)
-The built-in physics functions are normally run in this order each frame (after reading input, before rendering):
+The built-in pipeline runs this order each frame (inside ~engine-update~, after input, before ~update:~):
-1. ~apply-jump~ — if jump pressed and on ground, set ~#:ay~
+1. ~step-tweens~ — advance ~#:tween~ (see ~downstroke-tween~)
2. ~apply-acceleration~ — consume ~#:ay~ into ~#:vy~
3. ~apply-gravity~ — add gravity to ~#:vy~
4. ~apply-velocity-x~ — move by ~#:vx~
-5. ~resolve-tile-collisions-x~ — snap against horizontal tile collisions
+5. ~resolve-tile-collisions-x~ — snap against horizontal tile collisions (uses ~scene-tilemap~)
6. ~apply-velocity-y~ — move by ~#:vy~
7. ~resolve-tile-collisions-y~ — snap against vertical tile collisions
-8. ~detect-on-solid~ — set ~#:on-ground?~ if standing on a tile and/or another solid entity (optional third argument)
-9. ~resolve-entity-collisions~ — push apart solid entities (whole list)
+8. ~detect-on-solid~ — set ~#:on-ground?~ from tiles below and/or other solids in ~scene-entities~
+9. ~resolve-entity-collisions~ — push apart solid entities (whole list; use ~scene-transform-entities~)
+10. ~sync-groups~ — align grouped entities to origins (whole list)
+
+Per-entity steps take ~(entity scene dt)~. Jumping is *not* a built-in step: set ~#:ay~ in your ~update:~ hook (e.g. ~(entity-set player #:ay (- *jump-force*))~) when the player jumps; ~apply-acceleration~ consumes it on the following ~engine-update~.
Entities may list ~#:skip-pipelines~ to omit specific steps; see ~entity-skips-pipeline?~ under ~downstroke-entity~ and ~docs/physics.org~.
@@ -488,15 +509,15 @@ Entities may list ~#:skip-pipelines~ to omit specific steps; see ~entity-skips-p
** ~apply-acceleration~
#+begin_src scheme
-(apply-acceleration entity)
+(apply-acceleration entity scene dt)
#+end_src
-Consumes ~#:ay~ (one-shot acceleration) into ~#:vy~ and clears ~#:ay~ to 0. Only applies if ~#:gravity?~ is true.
+Consumes ~#:ay~ (one-shot acceleration) into ~#:vy~ and clears ~#:ay~ to 0. Only applies if ~#:gravity?~ is true. ~scene~ and ~dt~ are accepted for pipeline uniformity; this step does not use them.
** ~apply-gravity~
#+begin_src scheme
-(apply-gravity entity)
+(apply-gravity entity scene dt)
#+end_src
Adds the gravity constant (1 pixel/frame²) to ~#:vy~. Only applies if ~#:gravity?~ is true.
@@ -504,7 +525,7 @@ Adds the gravity constant (1 pixel/frame²) to ~#:vy~. Only applies if ~#:gravit
** ~apply-velocity-x~
#+begin_src scheme
-(apply-velocity-x entity)
+(apply-velocity-x entity scene dt)
#+end_src
Updates ~#:x~ by adding ~#:vx~. Returns a new entity.
@@ -512,7 +533,7 @@ Updates ~#:x~ by adding ~#:vx~. Returns a new entity.
** ~apply-velocity-y~
#+begin_src scheme
-(apply-velocity-y entity)
+(apply-velocity-y entity scene dt)
#+end_src
Updates ~#:y~ by adding ~#:vy~. Returns a new entity.
@@ -528,15 +549,15 @@ Legacy function: updates both ~#:x~ and ~#:y~ by their respective velocities.
** ~resolve-tile-collisions-x~
#+begin_src scheme
-(resolve-tile-collisions-x entity tilemap)
+(resolve-tile-collisions-x entity scene dt)
#+end_src
-Detects and resolves collisions between the entity's bounding box and solid tiles along the X axis. Snaps the entity's ~#:x~ to the near/far tile edge and sets ~#:vx~ to 0. Returns a new entity.
+Detects and resolves collisions between the entity's bounding box and solid tiles along the X axis using ~(scene-tilemap scene)~. If there is no tilemap, the step is skipped. Snaps the entity's ~#:x~ to the near/far tile edge and sets ~#:vx~ to 0. Returns a new entity.
** ~resolve-tile-collisions-y~
#+begin_src scheme
-(resolve-tile-collisions-y entity tilemap)
+(resolve-tile-collisions-y entity scene dt)
#+end_src
Detects and resolves collisions between the entity's bounding box and solid tiles along the Y axis. Snaps the entity's ~#:y~ to the near/far tile edge and sets ~#:vy~ to 0. Returns a new entity.
@@ -544,18 +565,10 @@ Detects and resolves collisions between the entity's bounding box and solid tile
** ~detect-on-solid~
#+begin_src scheme
-(detect-on-solid entity tilemap #!optional other-entities)
+(detect-on-solid entity scene dt)
#+end_src
-Sets ~#:on-ground?~ to true if the entity is supported by a solid tile (probe below the feet) and/or, when ~other-entities~ is a list, by another solid's top surface (e.g. moving platforms). Omit the third argument for tile-only detection. Only applies if ~#:gravity?~ is true. Returns a new entity (the ~?~-suffix denotes call-site readability, not a boolean return value). Prefer calling after all collision resolution when using the entity list.
-
-** ~apply-jump~
-
-#+begin_src scheme
-(apply-jump entity jump-pressed?)
-#+end_src
-
-If the jump button is pressed and the entity is on ground, sets ~#:ay~ to ~(- #:jump-force)~ (default 15 pixels/frame). On the next frame, ~apply-acceleration~ consumes this into ~#:vy~. Returns a new entity.
+Sets ~#:on-ground?~ to true if the entity is supported by a solid tile (probe below the feet) and/or by another solid's top surface in ~(scene-entities scene)~ (e.g. moving platforms). Only applies if ~#:gravity?~ is true. Returns a new entity (the ~?~-suffix denotes call-site readability, not a boolean return value). Runs after tile and entity collision resolution in ~default-engine-update~.
** ~resolve-entity-collisions~
@@ -574,7 +587,7 @@ There is no scene-level wrapper; apply ~resolve-entity-collisions~ to the entity
** Physics Constants
- ~*gravity*~ = 1 (pixels per frame per frame)
-- ~*jump-force*~ = 15 (vertical acceleration on jump)
+- ~*jump-force*~ = 15 (conventional magnitude for a one-frame jump impulse; set ~#:ay~ to ~(- *jump-force*)~ in ~update:~ when jumping — there is no ~apply-jump~ helper)
* Input (~downstroke-input~)
@@ -681,13 +694,17 @@ Example:
#+begin_src scheme
(define (update-player game dt)
- (let ((input (game-input game))
- (player (car (scene-entities (game-scene game)))))
- (if (input-pressed? input 'a)
- (apply-jump player #t)
+ (let* ((input (game-input game))
+ (player (car (scene-entities (game-scene game))))
+ (jump? (and (input-pressed? input 'a)
+ (entity-ref player #:on-ground? #f))))
+ (if jump?
+ (entity-set player #:ay (- *jump-force*))
player)))
#+end_src
+~#:on-ground?~ is set by ~detect-on-solid~ during ~engine-update~, before ~update:~ runs. ~*jump-force*~ is exported from ~downstroke-physics~ (default 15).
+
* Renderer (~downstroke-renderer~)
#+begin_src scheme
@@ -1063,7 +1080,7 @@ Releases all audio resources. Call at shutdown or in a cleanup hook.
(import downstroke-tween)
#+end_src
-Time-based interpolation of numeric entity properties. Library-only — call from ~update:~; see ~docs/tweens.org~ for patterns with ~#:skip-pipelines~.
+Time-based interpolation of numeric entity properties. ~step-tweens~ runs inside ~default-engine-update~ before acceleration/gravity; you can still call ~tween-step~ from ~update:~ for manual tweening — see ~docs/tweens.org~ for patterns with ~#:skip-pipelines~.
** ~make-tween~
@@ -1095,10 +1112,10 @@ Returns two values: updated tween struct and updated entity. ~dt~ is elapsed mil
** ~step-tweens~
#+begin_src scheme
-(step-tweens entity dt)
+(step-tweens entity scene dt)
#+end_src
-Pipeline step: auto-advances ~#:tween~ on an entity. No-op if ~#:tween~ is absent. Removes ~#:tween~ when the tween finishes. Skipped when ~'tweens~ is in ~#:skip-pipelines~. See ~docs/tweens.org~ for patterns.
+Pipeline step: auto-advances ~#:tween~ on an entity. No-op if ~#:tween~ is absent. Removes ~#:tween~ when the tween finishes. Skipped when ~'tweens~ is in ~#:skip-pipelines~. ~scene~ is accepted for uniformity with other pipeline steps. See ~docs/tweens.org~ for patterns.
** Easing exports
diff --git a/docs/entities.org b/docs/entities.org
index 9f8e9ee..06b1831 100644
--- a/docs/entities.org
+++ b/docs/entities.org
@@ -91,13 +91,13 @@ Since each update returns a new entity, chain updates with ~let*~:
#+begin_src scheme
(let* ((player (entity-set player #:vx 3))
- (player (apply-velocity-x player))
- (player (resolve-tile-collisions-x player tilemap)))
+ (player (apply-velocity-x player scene dt))
+ (player (resolve-tile-collisions-x player scene dt)))
;; now use the updated player
player)
#+end_src
-This is how the platformer demo applies physics in order: each step reads the input, computes the next state, and passes it to the next step.
+With the default =engine-update=, you normally set =#:vx= / =#:ay= in =update:= and do not chain physics steps yourself. This =let*= shape is for custom =engine-update= hooks or tests; per-entity steps take =(entity scene dt)=.
* Plist Key Reference
@@ -111,7 +111,7 @@ The engine recognizes these standard keys. Use them to integrate with the physic
| ~#:vx~, ~#:vy~ | number | Velocity in pixels per frame. ~#:vx~ is updated by ~apply-velocity-x~; ~#:vy~ is updated by ~apply-velocity-y~. Both consumed by collision resolvers. |
| ~#:ay~ | number | Y acceleration (e.g., from jumping or knockback). Consumed by ~apply-acceleration~, which adds it to ~#:vy~. Optional; default is 0. |
| ~#:gravity?~ | boolean | Whether gravity applies to this entity. Set to ~#t~ for platformers (gravity pulls down), ~#f~ for top-down or flying entities. Used by ~apply-gravity~. |
-| ~#:on-ground?~ | boolean | Whether the entity is supported from below (set by ~detect-on-solid~): solid tile under the feet and/or standing on another solid entity when you pass the scene entity list. Use this to gate jump input. |
+| ~#:on-ground?~ | boolean | Whether the entity is supported from below (set by ~detect-on-solid~ in the default pipeline): solid tile under the feet and/or standing on another solid entity from ~(scene-entities scene)~. Use this in ~update:~ to gate jump input (~#:ay~). |
| ~#:solid?~ | boolean | Whether this entity participates in entity-entity collision. If ~#t~, ~resolve-entity-collisions~ will check it against other solid entities. |
| ~#:immovable?~ | boolean | If ~#t~ with ~#:solid? #t~, entity–entity resolution only moves the *other* entity (static platforms). Two overlapping immovable solids are not separated. |
| ~#:skip-pipelines~ | list of symbols | Optional. Each symbol names a pipeline step to skip (e.g. ~gravity~, ~velocity-x~, ~tweens~). See ~docs/physics.org~ and ~docs/tweens.org~. |
@@ -173,21 +173,16 @@ Returns a new scene with the entity appended to the entity list.
Maps each procedure over the scene's entities, applying them in sequence. Each proc must be a function of one entity, returning a new entity.
#+begin_src scheme
-;; Apply physics pipeline to all entities:
+;; Example: map per-entity physics steps (need scene + dt in scope):
(scene-map-entities scene
- apply-gravity
- apply-velocity-x
- apply-velocity-y)
+ (lambda (e) (apply-gravity e scene dt))
+ (lambda (e) (apply-velocity-x e scene dt))
+ (lambda (e) (apply-velocity-y e scene dt)))
#+end_src
-The result is equivalent to:
+The result is equivalent to chaining three =scene-map-entities= passes, one per step.
-#+begin_src scheme
-(chain scene
- (scene-map-entities _ apply-gravity)
- (scene-map-entities _ apply-velocity-x)
- (scene-map-entities _ apply-velocity-y))
-#+end_src
+With default =engine-update=, the engine applies the full pipeline for you; use this pattern inside a custom =engine-update= or tools.
** ~scene-filter-entities scene pred~
@@ -328,7 +323,7 @@ While tags are free-form, consider using these conventions in your game:
* Example: Complete Entity Setup
-Here is a full example showing entity creation, initialization in the scene, and update logic:
+Here is a full example showing entity creation, initialization in the scene, and update logic (assumes =downstroke-physics= is imported for =*jump-force*=):
#+begin_src scheme
(define (create-player-entity)
@@ -358,40 +353,28 @@ Here is a full example showing entity creation, initialization in the scene, and
(define (update-hook game dt)
(let* ((input (game-input game))
(scene (game-scene game))
- (player (scene-find-tagged scene 'player))
- (tm (scene-tilemap scene)))
- ;; Update input-driven velocity
+ (player (scene-find-tagged scene 'player)))
+ ;; Intent + presentation — default engine-update already ran physics this frame
(let* ((player (entity-set player #:vx
(cond
((input-held? input 'left) -3)
((input-held? input 'right) 3)
(else 0))))
- ;; Handle jump
(player (if (and (input-pressed? input 'a)
(entity-ref player #:on-ground? #f))
- (entity-set player #:ay -5)
+ (entity-set player #:ay (- *jump-force*))
player))
- ;; Apply physics
- (player (apply-acceleration player))
- (player (apply-gravity player))
- (player (apply-velocity-x player))
- (player (resolve-tile-collisions-x player tm))
- (player (apply-velocity-y player))
- (player (resolve-tile-collisions-y player tm))
- (player (detect-on-solid player tm))
- ;; Update animation
(player (set-animation player
(cond
((not (entity-ref player #:on-ground? #f)) 'jump)
((not (zero? (entity-ref player #:vx 0))) 'walk)
(else 'idle))))
- (player (animate-entity player player-animations)))
- ;; Update facing direction
- (let ((player (if (< (entity-ref player #:vx 0) 0)
- (entity-set player #:facing -1)
- (entity-set player #:facing 1))))
- (game-scene-set! game
- (update-scene scene entities: (list player)))))))
+ (player (animate-entity player player-animations))
+ (player (if (< (entity-ref player #:vx 0) 0)
+ (entity-set player #:facing -1)
+ (entity-set player #:facing 1))))
+ (game-scene-set! game
+ (update-scene scene entities: (list player))))))
#+end_src
-Note the let*-chaining pattern: each update builds on the previous result, keeping the data flow clear and each step testable. The single ~game-scene-set!~ at the boundary stores the final scene back on the game struct.
+Import =*jump-force*= from =downstroke-physics= (or use a literal jump impulse for =#:ay=). The single ~game-scene-set!~ stores the scene after game logic; motion and =#:on-ground?= come from =default-engine-update=.
diff --git a/docs/guide.org b/docs/guide.org
index 96663ea..d98b00e 100644
--- a/docs/guide.org
+++ b/docs/guide.org
@@ -5,7 +5,7 @@
Downstroke is a 2D tile-driven game engine for Chicken Scheme, built on SDL2. It is inspired by Phaser 2: a minimal game is about 20 lines of Scheme.
-The engine handles SDL2 initialization, the event loop, input, rendering, and physics. You provide lifecycle hooks to customize behavior. This guide walks you through building your first game with Downstroke.
+The engine handles SDL2 initialization, the event loop, input, rendering, and — by default — a full physics pipeline each frame (~default-engine-update~). You provide lifecycle hooks to customize behavior; ~update:~ is where you usually set movement *intent* (~#:vx~, ~#:vy~, ~#:ay~) and game rules. This guide walks you through building your first game with Downstroke.
* Installation
@@ -89,18 +89,13 @@ Now let's add an entity you can move with the keyboard. Create =square.scm=:
(let* ((input (game-input game))
(scene (game-scene game))
(box (car (scene-entities scene)))
- ;; Read input and update velocity
+ ;; Intent only: default engine-update integrates #:vx / #:vy each frame
(box (entity-set box #:vx (cond ((input-held? input 'left) -3)
((input-held? input 'right) 3)
(else 0))))
(box (entity-set box #:vy (cond ((input-held? input 'up) -3)
((input-held? input 'down) 3)
- (else 0))))
- ;; Apply velocity to position
- (box (entity-set box #:x (+ (entity-ref box #:x 0)
- (entity-ref box #:vx 0))))
- (box (entity-set box #:y (+ (entity-ref box #:y 0)
- (entity-ref box #:vy 0)))))
+ (else 0)))))
(game-scene-set! game
(update-scene scene entities: (list box)))))
@@ -129,7 +124,7 @@ Press arrow keys to move the yellow square around. Here are the key ideas:
- **Scenes**: =make-scene= creates a container for entities, tilemaps, and the camera. It holds the game state each frame. Optional =background:= ~(r g b)~ or ~(r g b a)~ sets the color used to clear the window each frame (default is black).
- **Entities**: Entities are plists (property lists). They have no class; they're pure data. Access properties with =entity-ref=, and update with =entity-set= (which returns a *new* plist — always bind the result).
- **Input**: =input-held?= returns =#t= if an action is currently pressed. Actions are symbols like ='left=, ='right=, ='up=, ='down= (from the default input config).
-- **Update & Render**: The =update:= hook runs first and updates entities. The =render:= hook runs after the default rendering pipeline and is used for custom drawing like this colored rectangle.
+- **Update & Render**: Each frame, after input, the built-in =engine-update= integrates =#:vx= / =#:vy= into position (gravity and tile steps no-op without =#:gravity?= or a tilemap). Your =update:= hook runs *after* that and sets velocities for the following frame. The =render:= hook runs after the default scene render and is used for custom drawing like this colored rectangle.
- **Rendering**: Since our tilemap is =#f=, the default renderer draws nothing; all drawing happens in the =render:= hook using SDL2 functions.
* Adding a Tilemap and Physics
@@ -171,32 +166,23 @@ For a real game, you probably want tilemaps, gravity, and collision detection. D
(scene-add-entity scene player)))
update: (lambda (game dt)
- ;; Typical pattern for platformer physics
+ ;; Game logic only — gravity, collisions, and #:on-ground? run in default-engine-update
(let* ((input (game-input game))
(scene (game-scene game))
- (tm (scene-tilemap scene))
(player (car (scene-entities scene)))
- ;; Set horizontal velocity from input
+ (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))))
- ;; Jump on button press if on ground
- (_ (when (and (input-pressed? input 'a)
- (entity-ref player #:on-ground? #f))
- (play-sound 'jump)))
- (player (apply-jump player (input-pressed? input 'a)))
- ;; Run physics pipeline
- (player (apply-acceleration player))
- (player (apply-gravity player))
- (player (apply-velocity-x player))
- (player (resolve-tile-collisions-x player tm))
- (player (apply-velocity-y player))
- (player (resolve-tile-collisions-y player tm))
- (player (detect-on-solid player tm)))
- (game-scene-set! game
- (update-scene scene entities: (list player)))))))
+ (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)))))))
(game-run! *game*)
#+end_src
@@ -206,9 +192,9 @@ Key points:
- **=game-load-scene!=** loads a TMX tilemap file (created with the Tiled editor), creates the tileset texture, and builds the scene. It returns the scene so you can add more entities.
- **=init-audio!=** and **=load-sounds!=** initialize the audio subsystem and load sound files. Call these in the =preload:= hook.
- **=play-sound=** plays a loaded sound effect.
-- **Physics pipeline**: Functions like =apply-gravity=, =apply-velocity-x=, =resolve-tile-collisions-x= form the physics pipeline. Apply them in order each frame to get correct platformer behavior.
-- **Tile collisions**: =resolve-tile-collisions-x= and =resolve-tile-collisions-y= snap entities to tile edges, preventing clipping.
-- **On-solid check**: =detect-on-solid= sets the =#:on-ground?= flag from tiles below the feet and, if you pass other scene entities, from standing on solids (moving platforms, crates). Call it after collisions; use the flag next frame to gate jumps. (Despite the =?=-suffix, it returns an updated entity, not a boolean.)
+- **Automatic physics**: By default, =make-game= uses =default-engine-update=, which runs the full pipeline (tweens, acceleration, gravity, velocity, tile and entity collisions, ground detection, group sync) *before* your =update:= hook. Your =update:= sets =#:vx= and a one-frame =#:ay= for jumps; =*jump-force*= (from =downstroke-physics=, default 15) is a conventional jump impulse.
+- **=engine-update: #f=** disables that pipeline — use this for games that move entities entirely in =update:= (see =demo/shmup.scm=, =demo/scaling.scm=) or supply your own =engine-update:= procedure to replace =default-engine-update=.
+- **=detect-on-solid=** (inside the default pipeline) sets =#:on-ground?= from tiles below the feet and from other solids in the scene entity list. When your =update:= runs, that flag already reflects the current frame.
See =demo/platformer.scm= in the engine source for a complete working example.
@@ -300,7 +286,7 @@ You now know how to:
- Create a game with =make-game= and =game-run!=
- Add entities to scenes
- Read input with =input-held?= and =input-pressed?=
-- Apply physics to entities
+- Rely on the default physics pipeline (or turn it off with =engine-update: #f=)
- Load tilemaps with =game-load-scene!=
- Play sounds with =load-sounds!= and =play-sound=
diff --git a/docs/physics.org b/docs/physics.org
index e5b3749..81c57b8 100644
--- a/docs/physics.org
+++ b/docs/physics.org
@@ -3,54 +3,72 @@
* Overview
-The downstroke physics system is **explicit**: YOU call each physics step in your =update:= hook. This design gives you full control over the game's behavior—skip gravity for top-down games, skip tile collision for shmups, or skip entity collision for single-player platformers.
+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?=.
-All physics functions are **functional and immutable**: they take an entity (plist) and return a NEW plist with updated values. No side effects. This makes it easy to reason about physics state and compose multiple steps together.
+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.
-The =tilemap= argument is required by collision functions, since collision data lives in the tilemap metadata (which tiles are solid, which are not).
+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
-- =apply-jump= — trigger jump acceleration if on ground
+- =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 underfoot
-- =resolve-entity-collisions= — all-pairs AABB push-apart for solid entities
+- =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 **per-entity** physics pipeline (what you typically call from =update:=) is:
+The canonical order inside =default-engine-update= (and the recommended order when composing steps yourself) is:
#+begin_src
-apply-jump (set #:ay if jump pressed and on-ground)
+Frame loop (see =docs/api.org= =game-run!=):
+ input
+ ↓
+ engine-update (=default-engine-update= by default)
+ ↓
+ 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)
+apply-velocity-x (add #:vx to #:x)
resolve-tile-collisions-x (snap off horizontal tiles, zero #:vx)
-apply-velocity-y (add #:vy to #:y)
+apply-velocity-y (add #:vy to #:y)
+ ↓
+resolve-tile-collisions-y (snap off vertical tiles, zero #:vy)
-resolve-tile-collisions-y (snap off vertical tiles, zero #:vy)
+detect-on-solid (tiles and/or scene entities underfoot → #:on-ground?)
-detect-on-solid (tiles and/or other solids underfoot, set #:on-ground?)
+resolve-entity-collisions (whole entity list)
-resolve-entity-collisions (push apart overlapping solid entities; whole list)
+sync-groups (whole entity list)
#+end_src
-Input and rendering live **outside** this list — you read input first, then run the steps you need, then render.
+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 are needed for all game types.** See the examples section for three different patterns:
+**Not all steps apply to every entity** (=#:skip-pipelines=, guards like =#:gravity?=, missing tilemap). See the examples section for patterns:
-- **Platformer**: uses all 9 steps
-- **Top-down**: skips gravity, acceleration, jump, ground detection
-- **Physics Sandbox**: uses all steps, applies them to multiple entities
+- **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~)
@@ -58,7 +76,6 @@ An entity may include ~#:skip-pipelines=, a list of **symbols** naming steps to
| Symbol | Skipped call |
|--------+----------------|
-| ~jump~ | ~apply-jump~ |
| ~acceleration~ | ~apply-acceleration~ |
| ~gravity~ | ~apply-gravity~ |
| ~velocity-x~ | ~apply-velocity-x~ |
@@ -67,6 +84,7 @@ An entity may include ~#:skip-pipelines=, a list of **symbols** naming steps to
| ~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.
@@ -76,57 +94,33 @@ Helper: ~(entity-skips-pipeline? entity step-symbol)~ (from ~downstroke-entity~)
** ~define-pipeline~ (~downstroke-entity~)
-Physics steps are defined with ~(define-pipeline (procedure-name skip-symbol) (formals ...) 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. 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.
+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 ~jump~, ~acceleration~, ~gravity~, ~velocity-x~, ~velocity-y~ while a tween drives ~#:x~ / ~#:y~, but keep tile resolution so the body does not rest inside walls.
-- **Top-down:** omit gravity, jump, acceleration, ground detection from your *call order*; you usually do not need ~#:skip-pipelines= unless some entities differ from others.
+- **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-jump
-
-#+begin_src scheme
-(apply-jump entity jump-pressed?)
-#+end_src
-
-**Signature**: Takes an entity plist and a boolean (jump button state).
-
-**Reads**: =#:on-ground?= (must be #t), =#:jump-force= (default: 15 pixels/frame²)
-
-**Writes**: =#:ay= (acceleration-y), set to =(- jump-force)= if jump pressed and on ground, else #:ay is left untouched
-
-**Description**: Sets up a one-shot vertical acceleration if the jump button was just pressed AND the entity is standing on ground. The acceleration is consumed on the next frame by =apply-acceleration=. Do not call this repeatedly in a loop; call it once per frame.
-
-#+begin_src scheme
-;; Example:
-(define entity (list #:type 'player #:on-ground? #t #:jump-force 15))
-(define jumped (apply-jump entity #t))
-;; jumped has #:ay -15
-
-(define jumped2 (apply-jump entity #f))
-;; jumped2 is unchanged (not on ground)
-#+end_src
-
** apply-acceleration
#+begin_src scheme
-(apply-acceleration entity)
+(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 and other instant bursts. Only works if =#:gravity?= is true (gravity-enabled entities). Call once per frame, after =apply-jump=.
+**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.
** apply-gravity
#+begin_src scheme
-(apply-gravity entity)
+(apply-gravity entity scene dt)
#+end_src
**Reads**: =#:gravity?= (boolean), =#:vy= (default 0)
@@ -138,7 +132,7 @@ Use cases:
** apply-velocity-x
#+begin_src scheme
-(apply-velocity-x entity)
+(apply-velocity-x entity scene dt)
#+end_src
**Reads**: =#:vx= (default 0), =#:x= (default 0)
@@ -150,7 +144,7 @@ Use cases:
** apply-velocity-y
#+begin_src scheme
-(apply-velocity-y entity)
+(apply-velocity-y entity scene dt)
#+end_src
**Reads**: =#:vy= (default 0), =#:y= (default 0)
@@ -162,7 +156,7 @@ Use cases:
** resolve-tile-collisions-x
#+begin_src scheme
-(resolve-tile-collisions-x entity tilemap)
+(resolve-tile-collisions-x entity scene dt)
#+end_src
**Reads**: =#:x=, =#:y=, =#:width=, =#:height=, =#:vx=
@@ -179,7 +173,7 @@ Velocity is zeroed to stop the entity from sliding. Call this immediately after
** resolve-tile-collisions-y
#+begin_src scheme
-(resolve-tile-collisions-y entity tilemap)
+(resolve-tile-collisions-y entity scene dt)
#+end_src
**Reads**: =#:x=, =#:y=, =#:width=, =#:height=, =#:vy=
@@ -196,16 +190,16 @@ Velocity is zeroed. Call this immediately after =apply-velocity-y=.
** detect-on-solid
#+begin_src scheme
-(detect-on-solid entity tilemap #!optional other-entities)
+(detect-on-solid entity scene dt)
#+end_src
-**Reads**: =#:gravity?=, =#:x=, =#:y=, =#:width=, =#:height=, =#:vy=; when =other-entities= is passed, each other entity's =#:solid?=, position, and size
+**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=)
-**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, same as before), **or** (2) when =other-entities= is a non-=#f= list, resting on another solid entity's **top** (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). Omit =other-entities= (or pass =#f=) to keep tile-only behavior. Only runs when =#:gravity?= is true. If =tilemap= is =#f=, the tile probe is skipped and only entity support applies when a list is provided.
+**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.
-Used by =apply-jump= (via =#:on-ground?= on the **next** frame). Call **after** tile collision and **after** =resolve-entity-collisions= when using entity support, so positions and velocities are settled.
+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=.
** resolve-entity-collisions
@@ -299,83 +293,52 @@ The push-apart is along the **minimum penetration axis** (X or Y, whichever over
* Platformer Example
-A minimal platformer with a player, gravity, jumping, and tile collisions:
+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))
- (tm (scene-tilemap scene))
(player (car (scene-entities scene)))
- ;; Set horizontal velocity based on input
+ (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))))
- ;; Play sound if jump pressed
- (_ (when (and (input-pressed? input 'a)
- (entity-ref player #:on-ground? #f))
- (play-sound 'jump)))
- ;; Apply jump acceleration if button pressed
- (player (apply-jump player (input-pressed? input 'a)))
- ;; Consume #:ay into #:vy
- (player (apply-acceleration player))
- ;; Apply gravity constant
- (player (apply-gravity player))
- ;; Move horizontally
- (player (apply-velocity-x player))
- ;; Resolve tile collisions on x-axis
- (player (resolve-tile-collisions-x player tm))
- ;; Move vertically
- (player (apply-velocity-y player))
- ;; Resolve tile collisions on y-axis
- (player (resolve-tile-collisions-y player tm))
- ;; Check if standing on ground
- (player (detect-on-solid player tm)))
- (game-scene-set! game
- (update-scene scene entities: (list player)))))
+ (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. **Check jump**: if A button pressed and on-ground, sound plays
-3. **apply-jump**: if A pressed and on-ground, set #:ay to -15 (jump force)
-4. **apply-acceleration**: consume #:ay into #:vy (so #:ay becomes 0)
-5. **apply-gravity**: add 1 to #:vy each frame (falling acceleration)
-6. **apply-velocity-x**: add #:vx to #:x (move left/right)
-7. **resolve-tile-collisions-x**: if hit a tile, snap #:x and zero #:vx
-8. **apply-velocity-y**: add #:vy to #:y (move up/down)
-9. **resolve-tile-collisions-y**: if hit a tile, snap #:y and zero #:vy
-10. **detect-on-solid**: set #:on-ground? from tiles and/or other solids (optional entity list), for next frame's jump check
+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, no jumping, and free 4-way movement:
+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))
- (tm (scene-tilemap scene))
(player (car (scene-entities scene)))
- ;; Combine directional input
(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)))
- ;; Set both velocities
- (player (entity-set (entity-set player #:vx dx) #:vy dy))
- ;; Move horizontally
- (player (apply-velocity-x player))
- ;; Resolve tile collisions on x-axis
- (player (resolve-tile-collisions-x player tm))
- ;; Move vertically
- (player (apply-velocity-y player))
- ;; Resolve tile collisions on y-axis
- (player (resolve-tile-collisions-y player tm)))
- ;; Update camera to follow player
+ (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)))))
@@ -384,72 +347,75 @@ update: (lambda (game dt)
** 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
-3. **apply-velocity-x** and **resolve-tile-collisions-x**: move and collide horizontally
-4. **apply-velocity-y** and **resolve-tile-collisions-y**: move and collide vertically
-5. **No gravity, no jumping, no ground detection**: top-down games don't need these
+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 pipeline is much simpler because there's no vertical acceleration to manage.
+The built-in pipeline still runs; most steps no-op or pass through for non-gravity entities.
* Physics Sandbox Example
-Multiple entities falling and colliding with each other:
+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):
#+begin_src scheme
-update: (lambda (game dt)
- (let* ((scene (game-scene game))
- (tm (scene-tilemap scene)))
- ;; Apply physics to all entities in one pass, then resolve entity-entity collisions
- (scene-transform-entities
- (scene-map-entities scene
- apply-gravity
- apply-velocity-x
- (lambda (e) (resolve-tile-collisions-x e tm))
- apply-velocity-y
- (lambda (e) (resolve-tile-collisions-y e tm))
- (lambda (e) (detect-on-solid e tm)))
- resolve-entity-collisions)))
+(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
+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? for next frame)
+ - =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 is efficient for sandbox simulations: apply the same pipeline to all entities, then resolve inter-entity collisions once.
+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.
-** Lambdas with Tilemap
+** Capturing =scene= and =dt=
-Notice that =resolve-tile-collisions-x= needs the tilemap argument, so it's wrapped in a lambda:
+=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 tm))
+(lambda (e) (resolve-tile-collisions-x e scene dt))
#+end_src
-Same for other functions that take tilemap. The =scene-map-entities= macro applies each function to all entities, so you wrap single-argument functions in a lambda to capture the tilemap.
+Use the same =dt= (milliseconds) that =game-run!= passes to your hooks.
* Common Patterns
** Jumping
#+begin_src scheme
-;; In update:
-(player (apply-jump player (input-pressed? input 'jump)))
-(player (apply-acceleration player))
-(player (apply-gravity player))
-;; ... rest of pipeline
+;; 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))
#+end_src
-You only need =apply-jump=, =apply-acceleration=, and =apply-gravity=. =detect-on-solid= is needed to prevent double-jumping.
+=*jump-force*= (default 15) is exported from =downstroke-physics=. =apply-acceleration= and gravity run inside =default-engine-update= on the following frame.
** No Gravity
-For top-down or shmup games, skip =apply-jump=, =apply-acceleration=, =apply-gravity=, and =detect-on-solid=. Just do velocity + tile collision.
+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
@@ -504,9 +470,9 @@ For large games, consider spatial partitioning (grid, quadtree) to cull entity p
** Double-Jump / Can't Jump
-- Ensure =detect-on-solid= is called after tile collision and, when using the optional entity list, after =resolve-entity-collisions=
-- Verify =#:on-ground?= is checked in =apply-jump= (not hardcoded)
-- Make sure you're checking =input-pressed?= (not =input-held?=) for 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
diff --git a/docs/tweens.org b/docs/tweens.org
index c278814..11bf510 100644
--- a/docs/tweens.org
+++ b/docs/tweens.org
@@ -63,17 +63,12 @@ Predicates on the tween struct.
** ~step-tweens~ (pipeline step)
#+begin_src scheme
-(step-tweens entity dt)
+(step-tweens entity scene dt)
#+end_src
-Auto-advances ~#:tween~ on an entity. If the entity has no ~#:tween~ key, returns the entity unchanged. When the tween finishes (no more repeats), ~#:tween~ is removed from the entity. Respects ~#:skip-pipelines~: skipped when ~'tweens~ is in the list.
+Auto-advances ~#:tween~ on an entity. If the entity has no ~#:tween~ key (guard fails), returns the entity unchanged. When the tween finishes (no more repeats), ~#:tween~ is removed from the entity. Respects ~#:skip-pipelines~: skipped when ~'tweens~ is in the list.
-This is the recommended way to run tweens in most games. Attach a tween to an entity and include ~step-tweens~ in your per-entity pipeline:
-
-#+begin_src scheme
-(scene-map-entities scene
- (lambda (e) (step-tweens e dt)))
-#+end_src
+~step-tweens~ is part of ~default-engine-update~ and runs automatically each frame. No manual wiring is needed — just attach a ~#:tween~ to an entity and the engine handles the rest.
** Entity key: ~#:tween~
diff --git a/engine.scm b/engine.scm
index e3a1fb0..95d33b9 100644
--- a/engine.scm
+++ b/engine.scm
@@ -10,6 +10,8 @@
defstruct
downstroke-world
downstroke-input
+ downstroke-physics
+ downstroke-tween
downstroke-assets
downstroke-renderer)
@@ -39,6 +41,26 @@
;; ── Public constructor wrapper ─────────────────────────────────────────────
;; Wraps the auto-generated make-game (renamed to make-game*) with default values
+;; ── Default engine update ────────────────────────────────────────────────
+;; Standard physics pipeline: tweens → acceleration → gravity → velocity →
+;; tile collisions → ground detection → entity collisions → group sync.
+;; Runs automatically each frame unless overridden or disabled.
+
+(define (default-engine-update game dt)
+ (let ((scene (game-scene game)))
+ (when scene
+ (let* ((scene (scene-map-entities scene (cut step-tweens <> scene dt)))
+ (scene (scene-map-entities scene (cut apply-acceleration <> scene dt)))
+ (scene (scene-map-entities scene (cut apply-gravity <> scene dt)))
+ (scene (scene-map-entities scene (cut apply-velocity-x <> scene dt)))
+ (scene (scene-map-entities scene (cut resolve-tile-collisions-x <> scene dt)))
+ (scene (scene-map-entities scene (cut apply-velocity-y <> scene dt)))
+ (scene (scene-map-entities scene (cut resolve-tile-collisions-y <> scene dt)))
+ (scene (scene-map-entities scene (cut detect-on-solid <> scene dt)))
+ (scene (scene-transform-entities scene resolve-entity-collisions))
+ (scene (scene-transform-entities scene sync-groups)))
+ (game-scene-set! game scene)))))
+
(define (make-game #!key
(title "Downstroke Game")
(width 640) (height 480)
@@ -192,6 +214,12 @@
(game-input-config game))))
(game-input-set! game input)
(unless (input-held? input 'quit)
+ (let ((scene (game-scene game)))
+ (when scene
+ (let ((eu (scene-engine-update scene)))
+ (cond
+ ((procedure? eu) (eu game dt))
+ ((not eu) (default-engine-update game dt))))))
(receive (update-fn render-fn) (resolve-hooks game)
(when update-fn (update-fn game dt))
(when (game-scene game)
diff --git a/physics.scm b/physics.scm
index 773d922..d418539 100644
--- a/physics.scm
+++ b/physics.scm
@@ -2,7 +2,7 @@
(resolve-entity-collisions resolve-pair
aabb-overlap? push-apart push-along-axis aabb-overlap-on-axis
entity-center-on-axis push-entity axis->velocity axis->dimension
- index-pairs list-set apply-jump detect-on-solid
+ index-pairs list-set detect-on-solid
resolve-tile-collisions-y resolve-tile-collisions-x resolve-tile-collisions-axis
tile-push-pos entity-tile-cells pixel->tile build-cell-list
apply-velocity apply-velocity-y apply-velocity-x apply-gravity apply-acceleration
@@ -32,25 +32,25 @@
;; for #:skip-pipelines symbol names).
;; Consume #:ay into #:vy and clear it (one-shot acceleration)
- (define-pipeline (apply-acceleration acceleration) (entity)
+ (define-pipeline (apply-acceleration acceleration) (entity scene dt)
guard: (entity-ref entity #:gravity? #f)
(let ((ay (entity-ref entity #:ay 0))
(vy (entity-ref entity #:vy 0)))
(entity-set (entity-set entity #:vy (+ vy ay)) #:ay 0)))
;; Apply gravity to an entity if it has gravity enabled
- (define-pipeline (apply-gravity gravity) (entity)
+ (define-pipeline (apply-gravity gravity) (entity scene dt)
guard: (entity-ref entity #:gravity? #f)
(entity-set entity #:vy (+ (entity-ref entity #:vy) *gravity*)))
;; Update entity's x by its vx velocity
- (define-pipeline (apply-velocity-x velocity-x) (entity)
+ (define-pipeline (apply-velocity-x velocity-x) (entity scene dt)
(let ((x (entity-ref entity #:x 0))
(vx (entity-ref entity #:vx 0)))
(entity-set entity #:x (+ x vx))))
;; Update entity's y by its vy velocity
- (define-pipeline (apply-velocity-y velocity-y) (entity)
+ (define-pipeline (apply-velocity-y velocity-y) (entity scene dt)
(let ((y (entity-ref entity #:y 0))
(vy (entity-ref entity #:vy 0)))
(entity-set entity #:y (+ y vy))))
@@ -128,16 +128,20 @@
(entity-tile-cells entity tilemap)))))
;; Resolve horizontal collisions with solid tiles
- (define-pipeline (resolve-tile-collisions-x tile-collisions-x) (entity tilemap)
- (let ((w (entity-ref entity #:width 0))
- (tw (tilemap-tilewidth tilemap)))
+ (define-pipeline (resolve-tile-collisions-x tile-collisions-x) (entity scene dt)
+ guard: (scene-tilemap scene)
+ (let* ((tilemap (scene-tilemap scene))
+ (w (entity-ref entity #:width 0))
+ (tw (tilemap-tilewidth tilemap)))
(resolve-tile-collisions-axis entity tilemap #:vx #:x
(lambda (v col row) (tile-push-pos v col tw w)))))
;; Resolve vertical collisions with solid tiles
- (define-pipeline (resolve-tile-collisions-y tile-collisions-y) (entity tilemap)
- (let ((h (entity-ref entity #:height 0))
- (th (tilemap-tileheight tilemap)))
+ (define-pipeline (resolve-tile-collisions-y tile-collisions-y) (entity scene dt)
+ guard: (scene-tilemap scene)
+ (let* ((tilemap (scene-tilemap scene))
+ (h (entity-ref entity #:height 0))
+ (th (tilemap-tileheight tilemap)))
(resolve-tile-collisions-axis entity tilemap #:vy #:y
(lambda (v col row) (tile-push-pos v row th h)))))
@@ -175,19 +179,13 @@
(or (not (zero? (tilemap-tile-at tilemap col-left row)))
(not (zero? (tilemap-tile-at tilemap col-right row))))))
- (define-pipeline (detect-on-solid on-solid)
- (entity tilemap #!optional (other-entities #f))
+ (define-pipeline (detect-on-solid on-solid) (entity scene dt)
guard: (entity-ref entity #:gravity? #f)
- (let* ((on-tile? (and tilemap (tile-ground-below? entity tilemap)))
- (on-entity? (and other-entities
- (entity-solid-support-below? entity other-entities))))
+ (let* ((tilemap (scene-tilemap scene))
+ (on-tile? (and tilemap (tile-ground-below? entity tilemap)))
+ (on-entity? (entity-solid-support-below? entity (scene-entities scene))))
(entity-set entity #:on-ground? (or on-tile? on-entity?))))
- ;; Set vertical acceleration for jump (consumed next frame by apply-acceleration)
- (define-pipeline (apply-jump jump) (entity jump-pressed?)
- guard: (and jump-pressed? (entity-ref entity #:on-ground? #f))
- (entity-set entity #:ay (- (entity-ref entity #:jump-force *jump-force*))))
-
;; Replace element at idx in lst with val
(define (list-set lst idx val)
(let loop ((lst lst) (i 0) (acc '()))
diff --git a/tests/engine-test.scm b/tests/engine-test.scm
index 2c9d6d5..aa44964 100644
--- a/tests/engine-test.scm
+++ b/tests/engine-test.scm
@@ -85,7 +85,7 @@
(import scheme (chicken base) defstruct)
(import downstroke-entity)
(defstruct camera x y)
- (defstruct scene entities tilemap tileset camera tileset-texture camera-target background)
+ (defstruct scene entities tilemap tileset camera tileset-texture camera-target background engine-update)
;; Mock camera-follow - returns a new camera
(define (camera-follow camera entity viewport-w viewport-h)
(update-camera camera
@@ -97,7 +97,15 @@
(cond
((null? entities) #f)
((member tag (entity-ref (car entities) #:tags '())) (car entities))
- (else (loop (cdr entities)))))))
+ (else (loop (cdr entities))))))
+ (define (scene-map-entities scene . procs)
+ (let loop ((ps procs) (es (scene-entities scene)))
+ (if (null? ps)
+ (update-scene scene entities: es)
+ (loop (cdr ps) (map (car ps) es)))))
+ (define (scene-transform-entities scene proc)
+ (update-scene scene entities: (proc (scene-entities scene))))
+ (define (sync-groups entities) entities))
(import downstroke-world)
;; --- Real deps ---
@@ -105,6 +113,25 @@
(include "assets.scm")
(import downstroke-assets)
+;; --- Physics module (mock) ---
+(module downstroke-physics *
+ (import scheme (chicken base))
+ (define (apply-acceleration e s d) e)
+ (define (apply-gravity e s d) e)
+ (define (apply-velocity-x e s d) e)
+ (define (apply-velocity-y e s d) e)
+ (define (resolve-tile-collisions-x e s d) e)
+ (define (resolve-tile-collisions-y e s d) e)
+ (define (detect-on-solid e s d) e)
+ (define (resolve-entity-collisions es) es))
+(import downstroke-physics)
+
+;; --- Tween module (mock) ---
+(module downstroke-tween *
+ (import scheme (chicken base))
+ (define (step-tweens e s d) e))
+(import downstroke-tween)
+
;; --- Renderer module (mock) ---
(module downstroke-renderer *
(import scheme (chicken base))
@@ -262,4 +289,17 @@
(test-assert "states is a hash-table" (hash-table? (game-states game)))
(test-equal "active-state defaults to #f" #f (game-active-state game))))
+(test-group "scene engine-update"
+ (test-equal "scene engine-update defaults to #f"
+ #f
+ (scene-engine-update (make-scene entities: '() tilemap: #f camera-target: #f)))
+ (let* ((my-eu (lambda (game dt) #t))
+ (s (make-scene entities: '() tilemap: #f camera-target: #f engine-update: my-eu)))
+ (test-assert "custom engine-update stored on scene"
+ (procedure? (scene-engine-update s))))
+ (let ((s (make-scene entities: '() tilemap: #f camera-target: #f engine-update: 'none)))
+ (test-equal "engine-update: 'none disables pipeline"
+ 'none
+ (scene-engine-update s))))
+
(test-end "engine")
diff --git a/tests/physics-test.scm b/tests/physics-test.scm
index a5b40e9..6d1da86 100644
--- a/tests/physics-test.scm
+++ b/tests/physics-test.scm
@@ -67,22 +67,26 @@
tileset-source: "" tileset: #f
layers: (list layer) objects: '())))
+;; Helper to wrap a tilemap (and optional entities) in a scene for pipeline functions
+(define (test-scene #!key (entities '()) (tilemap #f))
+ (make-scene entities: entities tilemap: tilemap camera-target: #f))
+
;; Integration helper: simulate one frame of physics
(define (tick e tm held?)
(let* ((e (apply-input-to-entity e held?))
- (e (apply-gravity e))
- (e (apply-velocity-x e))
- (e (resolve-tile-collisions-x e tm))
- (e (apply-velocity-y e))
- (e (resolve-tile-collisions-y e tm))
- (e (detect-on-solid e tm)))
+ (e (apply-gravity e #f 0))
+ (e (apply-velocity-x e #f 0))
+ (e (resolve-tile-collisions-x e (test-scene tilemap: tm) 0))
+ (e (apply-velocity-y e #f 0))
+ (e (resolve-tile-collisions-y e (test-scene tilemap: tm) 0))
+ (e (detect-on-solid e (test-scene tilemap: tm) 0)))
e))
;; Test: apply-gravity
(test-group "apply-gravity"
(test-group "gravity? true, vy starts at 0"
(let* ((e '(#:type rock #:x 0 #:y 0 #:vx 0 #:vy 0 #:gravity? #t))
- (result (apply-gravity e)))
+ (result (apply-gravity e #f 0)))
(test-equal "vy increased by gravity" *gravity* (entity-ref result #:vy))
(test-equal "x unchanged" 0 (entity-ref result #:x))
(test-equal "y unchanged" 0 (entity-ref result #:y))
@@ -90,44 +94,44 @@
(test-group "gravity? true, vy already has value"
(let* ((e '(#:type rock #:x 0 #:y 0 #:vx 0 #:vy 3 #:gravity? #t))
- (result (apply-gravity e)))
+ (result (apply-gravity e #f 0)))
(test-equal "vy increased by gravity" 4 (entity-ref result #:vy))))
(test-group "gravity? false"
(let* ((e '(#:type static #:x 0 #:y 0 #:vx 0 #:vy 0 #:gravity? #f))
- (result (apply-gravity e)))
+ (result (apply-gravity e #f 0)))
(test-equal "vy unchanged" 0 (entity-ref result #:vy))))
(test-group "no gravity? field at all"
(let* ((e '(#:type static #:x 5 #:y 5))
- (result (apply-gravity e)))
+ (result (apply-gravity e #f 0)))
(test-equal "entity unchanged" e result))))
(test-group "apply-velocity-x"
(test-group "basic horizontal movement"
(let* ((e '(#:type rock #:x 10 #:y 20 #:vx 5 #:vy -2))
- (result (apply-velocity-x e)))
+ (result (apply-velocity-x e #f 0)))
(test-equal "x moved by vx" 15 (entity-ref result #:x))
(test-equal "y unchanged" 20 (entity-ref result #:y))
(test-equal "vy unchanged" -2 (entity-ref result #:vy))))
(test-group "zero vx"
(let* ((e '(#:type rock #:x 10 #:y 20 #:vx 0 #:vy 3))
- (result (apply-velocity-x e)))
+ (result (apply-velocity-x e #f 0)))
(test-equal "x unchanged" 10 (entity-ref result #:x))
(test-equal "y unchanged" 20 (entity-ref result #:y)))))
(test-group "apply-velocity-y"
(test-group "basic vertical movement"
(let* ((e '(#:type rock #:x 10 #:y 20 #:vx 3 #:vy -5))
- (result (apply-velocity-y e)))
+ (result (apply-velocity-y e #f 0)))
(test-equal "x unchanged" 10 (entity-ref result #:x))
(test-equal "y moved by vy" 15 (entity-ref result #:y))
(test-equal "vx unchanged" 3 (entity-ref result #:vx))))
(test-group "zero vy"
(let* ((e '(#:type rock #:x 10 #:y 20 #:vx 3 #:vy 0))
- (result (apply-velocity-y e)))
+ (result (apply-velocity-y e #f 0)))
(test-equal "x unchanged" 10 (entity-ref result #:x))
(test-equal "y unchanged" 20 (entity-ref result #:y)))))
@@ -198,20 +202,20 @@
(test-group "no collision: entity unchanged"
(let* ((tm (make-test-tilemap '((0 0 0) (0 0 0) (0 0 0))))
(e '(#:type player #:x 0 #:y 0 #:width 16 #:height 16 #:vx 2 #:vy 0)))
- (let ((result (resolve-tile-collisions-x e tm)))
+ (let ((result (resolve-tile-collisions-x e (test-scene tilemap: tm) 0)))
(test-equal "x unchanged" 0 (entity-ref result #:x))
(test-equal "vx unchanged" 2 (entity-ref result #:vx)))))
(test-group "zero vx: skipped entirely"
(let* ((tm (make-test-tilemap '((0 1 0) (0 0 0) (0 0 0))))
(e '(#:type player #:x 0 #:y 0 #:width 16 #:height 16 #:vx 0 #:vy 0)))
- (test-equal "entity eq? when vx=0" e (resolve-tile-collisions-x e tm))))
+ (test-equal "entity eq? when vx=0" e (resolve-tile-collisions-x e (test-scene tilemap: tm) 0))))
(test-group "collision moving right: push left"
;; solid at col=1 (x=16..31); entity at x=20 overlaps it, vx>0
(let* ((tm (make-test-tilemap '((0 0 0) (0 1 0) (0 0 0))))
(e '(#:type player #:x 20 #:y 16 #:width 16 #:height 16 #:vx 5 #:vy 0)))
- (let ((result (resolve-tile-collisions-x e tm)))
+ (let ((result (resolve-tile-collisions-x e (test-scene tilemap: tm) 0)))
(test-equal "pushed left of solid tile" 0 (entity-ref result #:x))
(test-equal "vx zeroed" 0 (entity-ref result #:vx)))))
@@ -219,7 +223,7 @@
;; solid at col=1 (x=16..31); entity at x=16 overlaps it, vx<0
(let* ((tm (make-test-tilemap '((0 0 0) (0 1 0) (0 0 0))))
(e '(#:type player #:x 16 #:y 16 #:width 16 #:height 16 #:vx -5 #:vy 0)))
- (let ((result (resolve-tile-collisions-x e tm)))
+ (let ((result (resolve-tile-collisions-x e (test-scene tilemap: tm) 0)))
(test-equal "pushed right of solid tile" 32 (entity-ref result #:x))
(test-equal "vx zeroed" 0 (entity-ref result #:vx)))))
@@ -227,7 +231,7 @@
;; solid at col=1; entity at x=20.5 (float), vx>0
(let* ((tm (make-test-tilemap '((0 0 0) (0 1 0) (0 0 0))))
(e '(#:type player #:x 20.5 #:y 16 #:width 16 #:height 16 #:vx 2 #:vy 0)))
- (let ((result (resolve-tile-collisions-x e tm)))
+ (let ((result (resolve-tile-collisions-x e (test-scene tilemap: tm) 0)))
(test-equal "pushed left of solid tile" 0 (entity-ref result #:x))
(test-equal "vx zeroed" 0 (entity-ref result #:vx)))))
@@ -235,12 +239,12 @@
;; wall at col=3; 20px-wide entity at x=28 spans cols 1 and 2, no collision
(let* ((tm (make-test-tilemap '((0 0 0 1) (0 0 0 1) (0 0 0 1))))
(e '(#:type player #:x 28 #:y 0 #:width 20 #:height 16 #:vx 3 #:vy 0)))
- (let ((result (resolve-tile-collisions-x e tm)))
+ (let ((result (resolve-tile-collisions-x e (test-scene tilemap: tm) 0)))
(test-equal "no collision yet" 28 (entity-ref result #:x))))
;; entity moved to x=34 now spans cols 2 and 3 (solid), pushed left
(let* ((tm (make-test-tilemap '((0 0 0 1) (0 0 0 1) (0 0 0 1))))
(e '(#:type player #:x 34 #:y 0 #:width 20 #:height 16 #:vx 3 #:vy 0)))
- (let ((result (resolve-tile-collisions-x e tm)))
+ (let ((result (resolve-tile-collisions-x e (test-scene tilemap: tm) 0)))
(test-equal "pushed left of wall" 28 (entity-ref result #:x))
(test-equal "vx zeroed" 0 (entity-ref result #:vx))))))
@@ -248,20 +252,20 @@
(test-group "no collision: entity unchanged"
(let* ((tm (make-test-tilemap '((0 0 0) (0 0 0) (0 0 0))))
(e '(#:type player #:x 0 #:y 0 #:width 16 #:height 16 #:vx 0 #:vy 2)))
- (let ((result (resolve-tile-collisions-y e tm)))
+ (let ((result (resolve-tile-collisions-y e (test-scene tilemap: tm) 0)))
(test-equal "y unchanged" 0 (entity-ref result #:y))
(test-equal "vy unchanged" 2 (entity-ref result #:vy)))))
(test-group "zero vy: skipped entirely"
(let* ((tm (make-test-tilemap '((1 0 0) (0 0 0) (0 0 0))))
(e '(#:type player #:x 0 #:y 0 #:width 16 #:height 16 #:vx 0 #:vy 0)))
- (test-equal "entity eq? when vy=0" e (resolve-tile-collisions-y e tm))))
+ (test-equal "entity eq? when vy=0" e (resolve-tile-collisions-y e (test-scene tilemap: tm) 0))))
(test-group "collision moving down: push up"
;; solid at row=1 (y=16..31); entity at y=20 overlaps it, vy>0
(let* ((tm (make-test-tilemap '((0 0 0) (1 0 0) (0 0 0))))
(e '(#:type player #:x 0 #:y 20 #:width 16 #:height 16 #:vx 0 #:vy 5)))
- (let ((result (resolve-tile-collisions-y e tm)))
+ (let ((result (resolve-tile-collisions-y e (test-scene tilemap: tm) 0)))
(test-equal "pushed above solid tile" 0 (entity-ref result #:y))
(test-equal "vy zeroed" 0 (entity-ref result #:vy)))))
@@ -269,7 +273,7 @@
;; solid at row=1 (y=16..31); entity at y=16 overlaps it from below, vy<0
(let* ((tm (make-test-tilemap '((0 0 0) (0 1 0) (0 0 0))))
(e '(#:type player #:x 16 #:y 16 #:width 16 #:height 16 #:vx 0 #:vy -5)))
- (let ((result (resolve-tile-collisions-y e tm)))
+ (let ((result (resolve-tile-collisions-y e (test-scene tilemap: tm) 0)))
(test-equal "pushed below solid tile" 32 (entity-ref result #:y))
(test-equal "vy zeroed" 0 (entity-ref result #:vy)))))
@@ -277,7 +281,7 @@
;; solid at row=1; entity at y=20.5 (float), vy>0
(let* ((tm (make-test-tilemap '((0 0 0) (1 0 0) (0 0 0))))
(e '(#:type player #:x 0 #:y 20.5 #:width 16 #:height 16 #:vx 0 #:vy 3)))
- (let ((result (resolve-tile-collisions-y e tm)))
+ (let ((result (resolve-tile-collisions-y e (test-scene tilemap: tm) 0)))
(test-equal "pushed above solid tile" 0 (entity-ref result #:y))
(test-equal "vy zeroed" 0 (entity-ref result #:vy)))))
@@ -285,12 +289,12 @@
;; floor at row=3; 20px-tall entity at y=28 spans rows 1 and 2, no collision
(let* ((tm (make-test-tilemap '((0 0 0) (0 0 0) (0 0 0) (1 1 1))))
(e '(#:type player #:x 0 #:y 28 #:width 16 #:height 20 #:vx 0 #:vy 3)))
- (let ((result (resolve-tile-collisions-y e tm)))
+ (let ((result (resolve-tile-collisions-y e (test-scene tilemap: tm) 0)))
(test-equal "no collision yet" 28 (entity-ref result #:y))))
;; entity at y=34 now spans rows 2 and 3 (solid), pushed up
(let* ((tm (make-test-tilemap '((0 0 0) (0 0 0) (0 0 0) (1 1 1))))
(e '(#:type player #:x 0 #:y 34 #:width 16 #:height 20 #:vx 0 #:vy 3)))
- (let ((result (resolve-tile-collisions-y e tm)))
+ (let ((result (resolve-tile-collisions-y e (test-scene tilemap: tm) 0)))
(test-equal "pushed above floor" 28 (entity-ref result #:y))
(test-equal "vy zeroed" 0 (entity-ref result #:vy))))))
@@ -301,7 +305,7 @@
;; Correct: snap to top of row 2 → y=16. Bug was: fold overwrote row 2 snap with row 3 snap → y=32 (inside row 2).
(let* ((tm (make-test-tilemap '((0 0 0) (0 0 0) (1 0 0) (1 0 0) (0 0 0))))
(e '(#:type player #:x 0 #:y 34 #:width 16 #:height 16 #:vx 0 #:vy 20)))
- (let ((result (resolve-tile-collisions-y e tm)))
+ (let ((result (resolve-tile-collisions-y e (test-scene tilemap: tm) 0)))
(test-equal "snapped to first solid row" 16 (entity-ref result #:y))
(test-equal "vy zeroed" 0 (entity-ref result #:vy)))))
@@ -395,7 +399,7 @@
(test-equal "a x unchanged" 0 (entity-ref (list-ref result 0) #:x 0))
(test-equal "b x unchanged" 5 (entity-ref (list-ref result 1) #:x 0)))))
-;; New tests for detect-on-solid and apply-jump
+;; Tests for detect-on-solid
(test-group "detect-on-solid"
(test-group "entity standing on solid tile"
;; Tilemap: 3 rows, row 2 is solid (tile=1), rows 0-1 empty (tile=0)
@@ -404,7 +408,7 @@
(let* ((tm (make-test-tilemap '((0 0 0) (0 0 0) (1 1 1))))
(e (list #:type 'player #:x 0 #:y 16 #:width 16 #:height 16
#:vx 0 #:vy 0 #:gravity? #t #:on-ground? #f))
- (result (detect-on-solid e tm)))
+ (result (detect-on-solid e (test-scene tilemap: tm) 0)))
(test-assert "on-ground? is #t" (entity-ref result #:on-ground? #f))))
(test-group "entity in mid-air"
@@ -412,7 +416,7 @@
(let* ((tm (make-test-tilemap '((0 0 0) (0 0 0) (1 1 1))))
(e (list #:type 'player #:x 0 #:y 0 #:width 16 #:height 16
#:vx 0 #:vy 0 #:gravity? #t #:on-ground? #t))
- (result (detect-on-solid e tm)))
+ (result (detect-on-solid e (test-scene tilemap: tm) 0)))
(test-assert "on-ground? is #f" (not (entity-ref result #:on-ground? #f)))))
(test-group "entity probe spans two tiles, left is solid"
@@ -421,7 +425,7 @@
(let* ((tm (make-test-tilemap '((0 0 0) (0 0 0) (1 0 0))))
(e (list #:type 'player #:x 0 #:y 16 #:width 16 #:height 16
#:vx 0 #:vy 0 #:gravity? #t #:on-ground? #f))
- (result (detect-on-solid e tm)))
+ (result (detect-on-solid e (test-scene tilemap: tm) 0)))
(test-assert "on-ground? is #t (left foot on solid)" (entity-ref result #:on-ground? #f))))
(test-group "entity probe spans two tiles, right is solid"
@@ -430,7 +434,7 @@
(let* ((tm (make-test-tilemap '((0 0 0) (0 0 0) (0 1 0))))
(e (list #:type 'player #:x 8 #:y 16 #:width 16 #:height 16
#:vx 0 #:vy 0 #:gravity? #t #:on-ground? #f))
- (result (detect-on-solid e tm)))
+ (result (detect-on-solid e (test-scene tilemap: tm) 0)))
(test-assert "on-ground? is #t (right foot on solid)" (entity-ref result #:on-ground? #f))))
(test-group "standing on solid entity (no tile): moving platform / crate"
@@ -441,58 +445,33 @@
(player (list #:type 'player #:x 8 #:y 16 #:width 16 #:height 16
#:vx 0 #:vy 0 #:gravity? #t #:on-ground? #f))
(ents (list platform player))
- (result (detect-on-solid player tm ents)))
+ (result (detect-on-solid player (test-scene tilemap: tm entities: ents) 0)))
(test-assert "on-ground? from entity top" (entity-ref result #:on-ground? #f))))
- (test-group "two-arg detect-on-solid skips entity list (backward compatible)"
+ (test-group "scene with empty entity list: no entity below"
(let* ((tm (make-test-tilemap '((0 0 0) (0 0 0) (0 0 0))))
(platform (list #:type 'platform #:x 0 #:y 32 #:width 64 #:height 16 #:solid? #t))
(player (list #:type 'player #:x 8 #:y 16 #:width 16 #:height 16
#:gravity? #t #:on-ground? #f))
- (result (detect-on-solid player tm)))
- (test-assert "no third arg → not on ground" (not (entity-ref result #:on-ground? #f))))))
-
-(test-group "apply-jump"
- (test-group "on-ground and pressed → impulse applied"
- (let* ((e (list #:type 'player #:x 0 #:y 0 #:width 16 #:height 16
- #:vx 0 #:vy 0 #:on-ground? #t))
- (result (apply-jump e #t)))
- (test-equal "ay is -jump-force" (- *jump-force*) (entity-ref result #:ay 0))))
-
- (test-group "on-ground but not pressed → unchanged"
- (let* ((e (list #:type 'player #:x 0 #:y 0 #:width 16 #:height 16
- #:vx 0 #:vy 0 #:on-ground? #t))
- (result (apply-jump e #f)))
- (test-equal "vy unchanged" 0 (entity-ref result #:vy 0))))
-
- (test-group "in-air and pressed → no double jump"
- (let* ((e (list #:type 'player #:x 0 #:y 0 #:width 16 #:height 16
- #:vx 0 #:vy -5 #:on-ground? #f))
- (result (apply-jump e #t)))
- (test-equal "vy unchanged (no double jump)" -5 (entity-ref result #:vy 0))))
-
- (test-group "in-air and not pressed → unchanged"
- (let* ((e (list #:type 'player #:x 0 #:y 0 #:width 16 #:height 16
- #:vx 0 #:vy -5 #:on-ground? #f))
- (result (apply-jump e #f)))
- (test-equal "vy unchanged" -5 (entity-ref result #:vy 0)))))
+ (result (detect-on-solid player (test-scene tilemap: tm) 0)))
+ (test-assert "empty entity list → not on ground" (not (entity-ref result #:on-ground? #f))))))
(test-group "apply-acceleration"
(test-group "gravity? #t, ay set: consumed into vy and cleared"
(let* ((e '(#:type player #:x 0 #:y 0 #:vy 3 #:ay 5 #:gravity? #t))
- (result (apply-acceleration e)))
+ (result (apply-acceleration e #f 0)))
(test-equal "vy += ay" 8 (entity-ref result #:vy 0))
(test-equal "ay cleared" 0 (entity-ref result #:ay 0))))
(test-group "gravity? #t, ay is 0: vy unchanged"
(let* ((e '(#:type player #:x 0 #:y 0 #:vy 3 #:ay 0 #:gravity? #t))
- (result (apply-acceleration e)))
+ (result (apply-acceleration e #f 0)))
(test-equal "vy unchanged" 3 (entity-ref result #:vy 0))
(test-equal "ay still 0" 0 (entity-ref result #:ay 0))))
(test-group "gravity? #f: entity unchanged"
(let* ((e '(#:type player #:x 0 #:y 0 #:vy 3 #:ay 5 #:gravity? #f))
- (result (apply-acceleration e)))
+ (result (apply-acceleration e #f 0)))
(test-equal "entity unchanged" e result))))
(test-group "pixel->tile"
@@ -626,16 +605,12 @@
(test-group "skip-pipelines"
(test-group "apply-gravity"
(let* ((e '(#:type t #:vy 0 #:gravity? #t #:skip-pipelines (gravity)))
- (r (apply-gravity e)))
+ (r (apply-gravity e #f 0)))
(test-equal "skipped: vy unchanged" 0 (entity-ref r #:vy))))
(test-group "apply-velocity-x"
(let* ((e '(#:type t #:x 10 #:vx 5 #:skip-pipelines (velocity-x)))
- (r (apply-velocity-x e)))
+ (r (apply-velocity-x e #f 0)))
(test-equal "skipped: x unchanged" 10 (entity-ref r #:x))))
- (test-group "apply-jump"
- (let* ((e '(#:type t #:on-ground? #t #:skip-pipelines (jump)))
- (r (apply-jump e #t)))
- (test-assert "skipped: no ay" (not (memq #:ay r)))))
(test-group "resolve-pair with entity-collisions skip"
(define (make-solid x y) (list #:type 'block #:x x #:y y #:width 16 #:height 16 #:solid? #t))
(let* ((a (list #:type 'ghost #:x 0 #:y 0 #:width 16 #:height 16 #:solid? #t
diff --git a/tests/tween-test.scm b/tests/tween-test.scm
index 4420c94..f0622fb 100644
--- a/tests/tween-test.scm
+++ b/tests/tween-test.scm
@@ -181,7 +181,7 @@
(let* ((ent (list #:type 'a #:x 0
#:tween (make-tween (list #:x 0) props: '((#:x . 100))
duration: 100 ease: 'linear)))
- (e2 (step-tweens ent 50)))
+ (e2 (step-tweens ent #f 50)))
(test-equal "x moved to midpoint" 50.0 (entity-ref e2 #:x))
(test-assert "tween still attached" (entity-ref e2 #:tween #f))))
@@ -189,20 +189,20 @@
(let* ((ent (list #:type 'a #:x 0
#:tween (make-tween (list #:x 0) props: '((#:x . 100))
duration: 100 ease: 'linear)))
- (e2 (step-tweens ent 100)))
+ (e2 (step-tweens ent #f 100)))
(test-equal "x at target" 100.0 (entity-ref e2 #:x))
(test-equal "tween removed" #f (entity-ref e2 #:tween #f))))
(test-group "no-op without #:tween"
(let* ((ent (list #:type 'a #:x 42))
- (e2 (step-tweens ent 100)))
+ (e2 (step-tweens ent #f 100)))
(test-equal "x unchanged" 42 (entity-ref e2 #:x))))
(test-group "keeps repeating tween attached"
(let* ((ent (list #:type 'a #:x 0
#:tween (make-tween (list #:x 0) props: '((#:x . 100))
duration: 100 ease: 'linear repeat: -1 yoyo?: #t)))
- (e2 (step-tweens ent 100)))
+ (e2 (step-tweens ent #f 100)))
(test-equal "x at target" 100.0 (entity-ref e2 #:x))
(test-assert "tween still attached (repeating)" (entity-ref e2 #:tween #f))))
@@ -211,7 +211,7 @@
#:skip-pipelines '(tweens)
#:tween (make-tween (list #:x 0) props: '((#:x . 100))
duration: 100 ease: 'linear)))
- (e2 (step-tweens ent 100)))
+ (e2 (step-tweens ent #f 100)))
(test-equal "x unchanged (skipped)" 0 (entity-ref e2 #:x))
(test-assert "tween still there" (entity-ref e2 #:tween #f)))))
diff --git a/tween.scm b/tween.scm
index eb8fbd8..64ed05e 100644
--- a/tween.scm
+++ b/tween.scm
@@ -196,7 +196,7 @@
;; per-entity pipeline, e.g. (step-tweens entity dt). Removes #:tween
;; when the tween finishes.
- (define-pipeline (step-tweens tweens) (entity dt)
+ (define-pipeline (step-tweens tweens) (entity scene dt)
guard: (entity-ref entity #:tween #f)
(let ((tw (entity-ref entity #:tween)))
(receive (tw2 ent2) (tween-step tw entity dt)
diff --git a/world.scm b/world.scm
index de9027c..d09b9c9 100644
--- a/world.scm
+++ b/world.scm
@@ -39,7 +39,8 @@
camera
tileset-texture
camera-target ; symbol tag or #f
- background) ; #f or (r g b) / (r g b a) for framebuffer clear
+ background ; #f or (r g b) / (r g b a) for framebuffer clear
+ engine-update) ; #f = inherit from game, procedure = per-scene override
(define (scene-add-entity scene entity)
(update-scene scene