1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
|
#+TITLE: Physics
#+AUTHOR: Downstroke Contributors
Downstroke ships with a built-in *physics pipeline* that runs every frame
before your =update:= hook. It advances tweens, integrates acceleration
and gravity into velocity, moves entities, resolves AABB collisions
against the tilemap and against each other, detects whether an entity is
standing on solid ground, and finally advances per-entity animation. The
pipeline is purely *functional*: every step takes an entity (alist) and
returns a new entity — no in-place mutation. You can opt specific
entities out of specific steps (=#:skip-pipelines=), replace the whole
pipeline with a custom procedure, or turn it off entirely
(=engine-update: 'none=) for shmups and menus that want full manual
control. Velocities are in *pixels per frame*, gravity is a constant
=+1= pixel/frame² added to =#:vy= every frame for entities with
=#:gravity? #t=, and tile collisions snap entities cleanly to tile edges
on both axes. The canonical reference example is the *Platformer demo*
— run with =bin/demo-platformer=, source in =demo/platformer.scm=.
* The minimum you need
To get physics behavior in your game, do three things:
1. Give your player entity dimensions, velocity keys, and
=#:gravity? #t=:
#+begin_src scheme
(list #:type 'player
#:x 100 #:y 50
#:width 16 #:height 16
#:vx 0 #:vy 0
#:gravity? #t
#:on-ground? #f
#:tags '(player))
#+end_src
2. Let the default engine update run. When you call =make-game=
*without* overriding =engine-update:= on the scene, the built-in
pipeline runs automatically each frame. You do not need to import
or compose any physics procedure yourself.
3. In your =update:= hook, set =#:vx= from input and set =#:ay= to
=(- *jump-force*)= on the frame the player jumps. The pipeline
integrates velocity, handles tile collisions, and refreshes
=#:on-ground?= *before* your next =update:= tick so you can gate
jumps on it:
#+begin_src scheme
update: (lambda (game dt)
(let* ((input (game-input game))
(scene (game-scene game))
(player (car (scene-entities scene)))
(jump? (and (input-pressed? input 'a)
(entity-ref player #:on-ground? #f)))
(player (entity-set player #:vx
(cond ((input-held? input 'left) -3)
((input-held? input 'right) 3)
(else 0)))))
(let ((player (if jump?
(entity-set player #:ay (- *jump-force*))
player)))
(game-scene-set! game
(update-scene scene entities: (list player))))))
#+end_src
That is the entire contract: set intent (velocity / one-shot
acceleration) in =update:=, the pipeline resolves motion and collisions
before your next tick sees the entity again. The *Platformer demo* —
run with =bin/demo-platformer=, source in =demo/platformer.scm= — is
exactly this shape.
Everything else in this document is either a refinement (skipping
specific steps, custom pipelines, entity–entity collisions) or a
diagnostic aid.
* Core concepts
** The engine pipeline, step by step
=default-engine-update= (defined in =engine.scm=) is the procedure the
engine runs each frame when a scene's =engine-update= field is =#f=
(the default). It applies ten steps, in a fixed order, across the
scene's entity list. The first eight steps are *per-entity* (each
entity is processed independently via =scene-map-entities=). The last
two operate on the *whole entity list* (via
=scene-transform-entities=):
#+begin_src
Frame:
input
↓
engine-update (default-engine-update unless overridden)
↓
update: (your game logic — set #:vx, #:vy, #:ay, ...)
↓
camera follow
↓
render
Inside default-engine-update, per entity:
step-tweens (advance #:tween)
↓
apply-acceleration (consume #:ay into #:vy, clear #:ay)
↓
apply-gravity (add *gravity* to #:vy)
↓
apply-velocity-x (add #:vx to #:x)
↓
resolve-tile-collisions-x (snap off horizontal tiles, zero #:vx)
↓
apply-velocity-y (add #:vy to #:y)
↓
resolve-tile-collisions-y (snap off vertical tiles, zero #:vy)
↓
detect-on-solid (set #:on-ground? from tiles / solids)
Then, across the whole entity list:
resolve-entity-collisions (AABB push-apart of solid entities)
↓
sync-groups (snap group members to their origin)
↓
apply-animation (advance #:anim-tick / #:anim-frame)
#+end_src
Here is what each step does, with the exact keys it reads and writes.
All per-entity steps have the signature =(step scene entity dt)=.
*** step-tweens
- *Reads*: =#:tween=
- *Writes*: =#:tween= (advanced, or removed when finished), plus any
entity keys the tween is targeting (typically =#:x=, =#:y=, a color
component, etc. — see =docs/tweens.org=)
- *Guard*: does nothing if =#:tween= is =#f=
- Defined in =tween.scm=.
*** apply-acceleration
- *Reads*: =#:ay= (default 0), =#:vy= (default 0)
- *Writes*: =#:vy= (set to =(+ vy ay)=), =#:ay= (reset to 0)
- *Guard*: =(entity-ref entity #:gravity? #f)= — only runs on entities
with =#:gravity? #t=
- One-shot: =#:ay= is *consumed* every frame. This is the jump
mechanism — set =#:ay= to =(- *jump-force*)= on the frame you want
to jump and this step folds it into =#:vy= exactly once.
*** apply-gravity
- *Reads*: =#:gravity?=, =#:vy= (default 0)
- *Writes*: =#:vy= (set to =(+ vy *gravity*)=)
- *Guard*: only runs when =#:gravity? #t=
- =*gravity*= is exported from =(downstroke physics)=; its value is
=1= pixel/frame². Gravity accumulates until =resolve-tile-collisions-y=
zeroes =#:vy= on contact with the floor.
*** apply-velocity-x
- *Reads*: =#:x= (default 0), =#:vx= (default 0)
- *Writes*: =#:x= (set to =(+ x vx)=)
- No guard. Every entity moves by =#:vx=, regardless of =#:gravity?=.
Top-down games therefore "just work" — set =#:vx= and =#:vy= from
input, the pipeline moves the entity and resolves tile collisions,
and the absence of =#:gravity?= makes gravity/acceleration/ground
detection no-op.
*** resolve-tile-collisions-x
- *Reads*: scene's tilemap via =scene-tilemap=, then =#:x=, =#:y=,
=#:width=, =#:height=, =#:vx=
- *Writes*: =#:x= (snapped), =#:vx= (set to 0 if a collision occurred)
- *Guard*: only runs when =(scene-tilemap scene)= is truthy. If the
scene has no tilemap this step is a no-op.
- Behavior: computes every tile cell overlapping the entity's AABB
(=entity-tile-cells=), walks them, and if any cell's tile id is
non-zero it snaps the entity to that tile's edge on the X axis.
Moving right (=#:vx > 0=) snaps the right edge to the *near* side of
the first solid tile found (shallowest penetration). Moving left
(=#:vx < 0=) snaps the left edge to the *far* side of the last solid
tile found. In both cases =#:vx= is zeroed.
*** apply-velocity-y
- *Reads*: =#:y= (default 0), =#:vy= (default 0)
- *Writes*: =#:y= (set to =(+ y vy)=)
- No guard. Same as =apply-velocity-x= on the Y axis.
*** resolve-tile-collisions-y
- *Reads*: scene's tilemap, then =#:x=, =#:y=, =#:width=, =#:height=,
=#:vy=
- *Writes*: =#:y= (snapped), =#:vy= (zeroed on collision)
- *Guard*: only runs when =(scene-tilemap scene)= is truthy.
- Behavior: same as the X-axis version, but on Y. Moving down
(=#:vy > 0=) snaps the feet to a floor tile's top; moving up
(=#:vy < 0=) snaps the head to a ceiling tile's bottom. X and Y are
resolved in *separate passes* (move X → resolve X → move Y → resolve
Y) to avoid corner-clipping.
*** detect-on-solid
- *Reads*: =#:gravity?=, =#:x=, =#:y=, =#:width=, =#:height=, =#:vy=;
and =(scene-entities scene)= for entity-supported ground
- *Writes*: =#:on-ground?= (=#t= or =#f=)
- *Guard*: only runs on gravity entities. The =?= in the name is
slightly misleading: the step returns an updated *entity* (with
=#:on-ground?= set), not a boolean.
- Two sources of "ground":
1. *Tile ground.* Probes one pixel below the feet (=(+ y h 1)=) at
both lower corners; if either column's tile id at that row is
non-zero, the entity is grounded. If =scene-tilemap= is =#f= this
probe is skipped.
2. *Entity ground.* Another entity in the scene with =#:solid? #t=
counts as ground if it horizontally overlaps the mover, its top
is within =*entity-ground-contact-tolerance*= (5 pixels) of the
mover's bottom, and the mover's =|#:vy|= is at most
=*entity-ground-vy-max*= (12) — so a body falling too fast is
*not* treated as supported mid-frame.
*** resolve-entity-collisions
This is a *bulk* step: it takes the entity list and returns a new
entity list.
- *Reads*: for each entity, =#:solid?=, =#:immovable?=, =#:x=, =#:y=,
=#:width=, =#:height=
- *Writes*: for overlapping pairs, =#:x=, =#:y=, and whichever axis
velocity (=#:vx= or =#:vy=) the separation was applied on
- Behavior: all-pairs AABB overlap check (O(n²)). For each pair where
*both* entities have =#:solid? #t=:
- both =#:immovable? #t= → pair skipped
- one =#:immovable? #t= → the movable one is pushed out along the
shallow penetration axis, its velocity on that axis is zeroed. If
the mover's center is still *above* the immovable's center (a
landing contact), separation is *forced vertical* so the mover
doesn't get shoved sideways off the edge of a platform.
- neither immovable → both are pushed apart by half the overlap
along the smaller-penetration axis, and their velocities on that
axis are set to ±1 (to prevent sticking / drift back into each
other).
- If either entity lists =entity-collisions= in =#:skip-pipelines=,
the pair is skipped entirely.
*** sync-groups
- Bulk step. Takes the entity list and returns a new entity list.
- For each entity with =#:group-origin? #t= and a =#:group-id=, marks
it as the origin of its group (first-wins).
- For each entity with a =#:group-id= that is *not* the origin, snaps
its =#:x= / =#:y= to =(+ origin-x #:group-local-x)= /
=(+ origin-y #:group-local-y)=.
- Intended use: multi-part entities (a platform made of several tiles,
a boss with attached hitboxes) that should move as one body. Move
the origin only; sync-groups rigidly follows the members.
*** apply-animation
- *Reads*: =#:animations=, =#:anim-name=, =#:anim-frame=, =#:anim-tick=
- *Writes*: =#:anim-tick=, =#:anim-frame=, =#:tile-id=, =#:duration=
- *Guard*: only runs when =#:animations= is present.
- Advances the per-entity animation state machine and updates
=#:tile-id= so the renderer draws the correct sprite. Fully
documented in =docs/animation.org=.
** Opting in/out of the pipeline
Most per-entity steps have one or two ways to opt out. Use the one
that matches your intent:
*** Per-step guard keys
Some pipeline steps only run when a specific entity key is set. These
are *guards* declared with the =guard:= clause of =define-pipeline=:
| Step | Guard |
|-----------------------------+-------------------------|
| =step-tweens= | =#:tween= |
| =apply-acceleration= | =#:gravity?= |
| =apply-gravity= | =#:gravity?= |
| =detect-on-solid= | =#:gravity?= |
| =apply-animation= | =#:animations= |
| =resolve-tile-collisions-x= | =(scene-tilemap scene)= |
| =resolve-tile-collisions-y= | =(scene-tilemap scene)= |
A top-down entity with =#:gravity?= absent (or =#f=) therefore
automatically skips acceleration, gravity, and ground detection — the
rest of the pipeline (velocity, tile collisions, entity collisions,
animation) still runs.
Entity–entity collisions have their own opt-*in* gate instead of a
guard: only entities with =#:solid? #t= participate in
=resolve-entity-collisions=. Non-solid entities are ignored by the
pair walk.
*** =#:skip-pipelines= per-entity override
Every per-entity step is also gated by =entity-skips-pipeline?= (from
=(downstroke entity)=). If the entity's =#:skip-pipelines= list contains
the step's *symbol*, the step returns the entity unchanged. The
symbols match the second name in each =define-pipeline= form:
| Skip symbol | Skips |
|---------------------+----------------------------------------------|
| =tweens= | =step-tweens= |
| =acceleration= | =apply-acceleration= |
| =gravity= | =apply-gravity= |
| =velocity-x= | =apply-velocity-x= |
| =velocity-y= | =apply-velocity-y= |
| =tile-collisions-x= | =resolve-tile-collisions-x= |
| =tile-collisions-y= | =resolve-tile-collisions-y= |
| =on-solid= | =detect-on-solid= |
| =entity-collisions= | participation in =resolve-entity-collisions= |
| =animation= | =apply-animation= |
For =resolve-entity-collisions=, if *either* entity in a pair lists
=entity-collisions=, the whole pair is skipped. This is the clean way
to mark "ghosts" or script-driven actors that should not be pushed
apart from others.
Example: an entity driven by a tween that shouldn't be integrated by
velocity or affected by gravity, but should still resolve against
walls and run animation:
#+begin_src scheme
(list #:type 'moving-platform
#:x 100 #:y 300 #:width 48 #:height 16
#:vx 0 #:vy 0
#:tween ...
#:skip-pipelines '(acceleration gravity velocity-x velocity-y))
#+end_src
The pipeline will advance =#:tween= (which can overwrite =#:x= / =#:y=
directly), skip the four listed integrators, still resolve tile
collisions, and still detect ground under it.
*** =define-pipeline= (how steps are declared)
All per-entity pipeline steps are declared with =define-pipeline= from
=(downstroke entity)= (see =docs/entities.org=). The shape is:
#+begin_src scheme
(define-pipeline (procedure-name skip-symbol) (scene entity dt)
guard: (some-expression-over entity)
(body ...))
#+end_src
The macro expands to a procedure =(procedure-name scene entity dt)=
that returns =entity= unchanged if either the guard is =#f= *or* the
entity's =#:skip-pipelines= list contains =skip-symbol=. Otherwise it
runs =body=. The =guard:= clause is optional; when absent, only
=#:skip-pipelines= is consulted.
** Overriding or disabling the pipeline
A scene has an =engine-update= field. The game's frame loop dispatches
on it every tick:
#+begin_src scheme
;; In game-run!, for the current scene:
(cond
((eq? eu 'none)) ; do nothing
((procedure? eu) (eu game dt)) ; run user procedure
((not eu) (default-engine-update game dt))) ; run default pipeline
#+end_src
This is three distinct modes:
| =engine-update= | Behavior |
|-----------------+----------------------------------------------------------------------|
| =#f= (default) | Run =default-engine-update= — the full built-in pipeline. |
| A procedure | Call =(proc game dt)= each frame *instead of* the default. |
| ='none= | Run *no* engine update. Only your =update:= hook advances the scene. |
=#f= is the normal case. =default-engine-update= is the recommended
starting point even for customized games — use =#:skip-pipelines= per
entity if you need selective opt-outs. Drop to a custom procedure when
you need a *different order* of steps (for instance, running
=detect-on-solid= *after* =resolve-entity-collisions= so that standing
on an entity that was pushed apart this frame is seen correctly). Drop
to ='none= when the pipeline has no value for your game at all — the
*shmup demo* is this case: bullets, enemies, and the player all move
in =update:= with per-type rules that don't map to gravity+tiles.
See the *Common patterns* section below for worked examples of all
three modes.
** Tile collisions & entity collisions
Both resolution systems are *AABB-only*. Every entity and every tile
is treated as an axis-aligned rectangle; slopes, rotations, and
per-pixel collision are not supported.
*** Tile collision algorithm
Tiles come from a =(downstroke tilemap)= parsed from a TMX file (Tiled
editor). The tilemap stores a grid of tile ids across one or more
layers; =tilemap-tile-at= returns =0= for an empty cell and a positive
id for a filled cell. Only *empty vs non-empty* is checked — there is
no per-tile "solid" flag in the core engine; any non-zero tile counts
as solid for collision purposes.
For each axis:
1. Compute the set of tile cells the entity's AABB overlaps
(=entity-tile-cells=), in tile coordinates. The AABB is computed
from =#:x=, =#:y=, =#:width=, =#:height=; the lower edges use
=(- (+ coord size) 1)= so an entity exactly flush with a tile edge
is *not* considered overlapping that tile.
2. For each overlapping cell, if its tile id is non-zero, snap the
entity to the tile edge on that axis. The snap function
(=tile-push-pos=) is:
#+begin_src scheme
;; Moving forward (v>0): snap leading edge to tile's near edge.
;; Moving backward (v<0): snap trailing edge to tile's far edge.
(if (> v 0)
(- (* coord tile-size) entity-size)
(* (+ coord 1) tile-size))
#+end_src
3. Zero the axis velocity so the entity doesn't slide through.
X and Y are resolved in *separate passes* (see the pipeline order
above). Resolving them together tends to produce corner-clip bugs
where a player moving diagonally gets stuck at the corner of a floor
and a wall; the two-pass approach avoids this entirely.
*** Entity collision algorithm
=resolve-entity-collisions= walks all unique pairs =(i, j)= with =i <
j= of the entity list and calls =resolve-pair=. For each pair:
- Both must have =#:solid? #t= and neither may list =entity-collisions=
in =#:skip-pipelines=.
- AABB overlap test (=aabb-overlap?=): strictly *overlapping*;
edge-touching does *not* count.
- On overlap, =push-apart= picks the minimum-penetration axis (X vs Y,
whichever overlap is smaller) and pushes each entity half the
overlap away from the other, setting velocity on that axis to ±1.
- If exactly one entity has =#:immovable? #t= (static geometry like a
platform), only the *other* entity moves, by the full overlap, and
its velocity on the separation axis is zeroed.
- Landing-on-top preference: when a movable body is falling onto a
static one and the movable's center is still above the static's
center, separation is forced *vertical* regardless of which axis has
the smaller overlap. This is what makes moving platforms usable — a
narrow horizontal overlap at the edge of a platform doesn't shove
the player off sideways.
*** =#:on-ground?= as a query
=#:on-ground?= is the single key you'll read most often. It is a
*result* of the pipeline (written by =detect-on-solid=), not an input.
It is re-computed every frame *after* both tile-collision passes but
*before* entity collisions. If your game needs ground detection to
reflect entity push-apart (rare — it matters for very thin platforms),
install a custom =engine-update= that calls =detect-on-solid= after
=resolve-entity-collisions=.
*** =aabb-overlap?= for manual queries
If you want to detect overlap *without* resolving it (bullets hitting
enemies, damage zones, pickups), call =aabb-overlap?= directly:
#+begin_src scheme
(aabb-overlap? x1 y1 w1 h1 x2 y2 w2 h2) ;; → #t or #f
#+end_src
This is pure — it does not touch either entity. The *shmup demo*
(=demo/shmup.scm=) uses exactly this pattern to find bullet/enemy
overlaps and remove them from the scene.
* Common patterns
** Moving platform (=vx= + =immovable?=)
A platform that slides horizontally and carries the player:
#+begin_src scheme
(list #:type 'platform
#:x 200 #:y 300
#:width 48 #:height 8
#:vx 1 #:vy 0
#:solid? #t #:immovable? #t
#:tags '(platform))
#+end_src
- =#:solid? #t= makes the platform participate in
=resolve-entity-collisions=.
- =#:immovable? #t= means only *other* entities are pushed; the
platform's =#:x= / =#:y= are never touched by entity collisions.
- =#:vx 1= makes =apply-velocity-x= move the platform 1 px/frame.
- The platform has no =#:gravity?= so it doesn't fall.
- When the player lands on it, =detect-on-solid= sees the solid entity
directly below (via =entity-solid-support-below?=) and sets the
player's =#:on-ground?= to =#t=.
- Turn the platform around at the ends by flipping =#:vx= in your
=update:= hook when =#:x= reaches a limit.
For a platform that should *not* move horizontally on its own (a
purely static crate), leave =#:vx= / =#:vy= at 0.
** Disabling gravity on a bullet
A bullet wants straight-line motion, no gravity, no ground detection,
no entity push-apart (it destroys enemies on contact, it doesn't shove
them):
#+begin_src scheme
(list #:type 'bullet
#:x 100 #:y 200
#:width 4 #:height 8
#:vx 0 #:vy -6
;; Omit #:gravity?, so apply-acceleration, apply-gravity,
;; and detect-on-solid are all no-ops for this entity.
;; Omit #:solid?, so it doesn't enter resolve-entity-collisions.
#:tags '(bullet))
#+end_src
=apply-velocity-x= and =apply-velocity-y= still move the bullet
(-6 px/frame upward). Tile collisions still run, which is usually what
you want — bullets can hit walls. For a bullet that passes through
walls, add =#:skip-pipelines '(tile-collisions-x tile-collisions-y)=.
Use =aabb-overlap?= in your =update:= hook to detect hits against
enemies.
** Skipping just tile collisions on one entity
Sometimes you have an entity (pickup, decorative particle, ghost) that
should move via =#:vx= / =#:vy= but *not* snap against tiles:
#+begin_src scheme
(list #:type 'ghost
#:x 100 #:y 100 #:width 16 #:height 16
#:vx 2 #:vy 0
#:skip-pipelines '(tile-collisions-x tile-collisions-y))
#+end_src
Velocity integration still runs. Tile-collision snapping and velocity
zeroing are both bypassed for this entity only. The rest of the scene
collides with tiles as normal.
** Replacing the pipeline with a custom =engine-update:=
If you need a different step order — the canonical reason is "check
=#:on-ground?= *after* entity collisions so an entity standing on a
just-pushed platform is seen as grounded in this tick" — you can pass
your own procedure on the scene:
#+begin_src scheme
(define (my-engine-update game dt)
(let ((scene (game-scene game)))
(when scene
(game-scene-set! game
(chain scene
(scene-map-entities _ (cut step-tweens <> <> dt))
(scene-map-entities _ (cut apply-acceleration <> <> dt))
(scene-map-entities _ (cut apply-gravity <> <> dt))
(scene-map-entities _ (cut apply-velocity-x <> <> dt))
(scene-map-entities _ (cut resolve-tile-collisions-x <> <> dt))
(scene-map-entities _ (cut apply-velocity-y <> <> dt))
(scene-map-entities _ (cut resolve-tile-collisions-y <> <> dt))
(scene-transform-entities _ resolve-entity-collisions)
;; Re-order: detect-on-solid AFTER entity collisions.
(scene-map-entities _ (cut detect-on-solid <> <> dt))
(scene-transform-entities _ sync-groups)
(scene-map-entities _ (cut apply-animation <> <> dt)))))))
;; Install it on the scene:
(make-scene
entities: ...
tilemap: tm
camera: (make-camera x: 0 y: 0)
tileset-texture: tex
engine-update: my-engine-update)
#+end_src
Prefer starting from =default-engine-update= and tweaking one thing,
rather than writing a pipeline from scratch. Forgetting =step-tweens=,
=sync-groups=, or =apply-animation= is the usual mistake — tweens stop
advancing, multi-part entities fall apart, animations freeze.
** Turning the pipeline off entirely (='none=)
For a shmup or any game where motion rules are entirely
per-entity-type, set =engine-update: 'none= on the scene. The
*Shmup demo* — run with =bin/demo-shmup=, source in =demo/shmup.scm= —
does exactly this: player moves via =apply-velocity-x= called
inline, bullets and enemies move via a bespoke =move-projectile=
helper, collisions are checked with =aabb-overlap?= and dead entities
are filtered out. No gravity, no ground detection, no pairwise
push-apart:
#+begin_src scheme
(make-scene
entities: (list (make-player))
tilemap: #f
camera: (make-camera x: 0 y: 0)
tileset-texture: #f
camera-target: #f
engine-update: 'none)
#+end_src
With ='none=, nothing in =physics.scm= runs unless you call it
yourself. You still have access to every physics procedure as a
library: =apply-velocity-x=, =aabb-overlap?=, =resolve-tile-collisions-y=,
and so on are all exported from =(downstroke physics)= and usable
individually. ='none= just disables the *automatic* orchestration.
** Reading =#:on-ground?= to gate jumps
The jump check in the *Platformer demo*:
#+begin_src scheme
(define (update-player player input)
(let* ((jump? (and (input-pressed? input 'a)
(entity-ref player #:on-ground? #f)))
(player (entity-set player #:vx (player-vx input))))
(when jump? (play-sound 'jump))
(if jump?
(entity-set player #:ay (- *jump-force*))
player)))
#+end_src
- =input-pressed?= (not =input-held?=) ensures one jump per keypress.
- =#:on-ground?= was updated by =detect-on-solid= in the pipeline
*this frame*, before your =update:= ran, so it reflects the current
position after tile / entity collisions.
- =#:ay= is cleared by =apply-acceleration= on the next frame, so the
impulse applies exactly once.
* See also
- [[file:guide.org][Getting started guide]] — overall game structure and the minimal
example that calls into the physics pipeline.
- [[file:entities.org][Entities]] — the keys the pipeline reads and writes
(=#:x=, =#:vy=, =#:gravity?=, =#:solid?=, =#:skip-pipelines=, etc.),
=entity-ref= / =entity-set=, and the =define-pipeline= macro.
- [[file:tweens.org][Tweens]] — =step-tweens= as the first step of the pipeline, and how
to combine tweens with =#:skip-pipelines= for knockback-style effects
that bypass velocity integration.
- [[file:animation.org][Animation]] — =apply-animation= as the last step of the pipeline;
=#:animations=, =#:anim-name=, =#:anim-frame=.
- [[file:input.org][Input]] — =input-held?= / =input-pressed?= for reading movement and
jump intent in =update:=.
- [[file:scenes.org][Scenes]] — the =engine-update= field of =make-scene=, group entities
(=#:group-id=, =#:group-origin?=) consumed by =sync-groups=, and
=scene-map-entities= / =scene-transform-entities= used throughout
the pipeline.
- [[file:rendering.org][Rendering]] — how =#:tile-id= (written by =apply-animation=) is drawn,
and where the camera transform is applied.
- [[file:../demo/platformer.scm][Platformer]] (=bin/demo-platformer=) — canonical gravity + jump + tile-collide example.
- [[file:../demo/shmup.scm][Shmup]] (=bin/demo-shmup=) — canonical =engine-update: 'none= example with manual collision checks via =aabb-overlap?=.
- [[file:../demo/sandbox.scm][Sandbox]] (=bin/demo-sandbox=) — multiple movers, entity–entity push-apart, the default pipeline in a busier scene.
- [[file:../demo/tweens.scm][Tweens]] (=bin/demo-tweens=) — =step-tweens= in action; useful when combined with =#:skip-pipelines '(velocity-x velocity-y)=.
|