aboutsummaryrefslogtreecommitdiff
path: root/docs/api.org
diff options
context:
space:
mode:
authorGene Pasquet <dev@etenil.net>2026-04-08 07:08:54 +0100
committerGene Pasquet <dev@etenil.net>2026-04-08 07:08:54 +0100
commitafc30a12e25215ff5e9226c3b4f8fd127d9a4d68 (patch)
treef736393fb8ebfd8982a4b79310a08c57ee430ff0 /docs/api.org
parent9e8b75f9949259ef01942cd3717b79b044efddf7 (diff)
Move the engine-update to the scene
Diffstat (limited to 'docs/api.org')
-rw-r--r--docs/api.org109
1 files changed, 63 insertions, 46 deletions
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