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
|
#+TITLE: Entities
Downstroke entities are the moving (and non-moving) things in your
scene: the player, enemies, coins, bullets, platforms, invisible
triggers. Internally each entity is an *alist* — a list of
=(keyword . value)= pairs — so an entity is just plain data that every
pipeline step reads, transforms, and returns a fresh copy of. There
are no classes, no inheritance, no hidden state. You build entities
either by hand as a plist and converting with =plist->alist=, or from a
*prefab* data file that composes named mixins with inline fields.
The module that owns this vocabulary is =downstroke-entity= (all the
=entity-*= procedures and the =define-pipeline= macro). The companion
module =downstroke-prefabs= loads prefab data files and instantiates
them. Most games will touch both.
* The minimum you need
The simplest entity is a plist of keyword keys converted to an alist.
From the getting-started demo (=demo/getting-started.scm=):
#+begin_src scheme
(import (only (list-utils alist) plist->alist)
downstroke-entity)
(define (make-player)
(plist->alist
(list #:type 'player
#:x 150 #:y 100
#:width 32 #:height 32
#:color '(100 160 255))))
#+end_src
Read a value with =entity-ref=, update it (functionally) with
=entity-set=:
#+begin_src scheme
(entity-ref player #:x) ; → 150
(entity-set player #:x 200) ; → new entity, player still has #:x 150
#+end_src
*Getting-started demo* — run with =bin/demo-getting-started=, source in
=demo/getting-started.scm=.
* Core concepts
** Entities are alists of CHICKEN keywords
An entity is an association list whose keys are CHICKEN keywords
(=#:type=, =#:x=, =#:vx=, etc.). For example, the platformer's player
after =plist->alist= looks like:
#+begin_src scheme
((#:type . player)
(#:x . 100) (#:y . 50)
(#:width . 16) (#:height . 16)
(#:vx . 0) (#:vy . 0)
(#:gravity? . #t) (#:on-ground? . #f)
(#:tile-id . 1) (#:tags . (player)))
#+end_src
The engine defines a handful of *shared keys* (=#:type=, =#:x=, =#:y=,
=#:width=, =#:height=, =#:vx=, =#:vy=, =#:tile-id=, =#:tags=, and a few
more per subsystem) that the built-in pipelines read and write. Your
game is free to add any other keys it needs — they're just data and
the engine ignores what it doesn't know.
There is also a minimal constructor for the positional fields, which
sets =#:type= to ='none=:
#+begin_src scheme
(make-entity x y w h)
;; → ((#:type . none) (#:x . x) (#:y . y) (#:width . w) (#:height . h))
#+end_src
In practice most entities are built via =plist->alist= (for ad-hoc
inline data) or via =instantiate-prefab= (for data-file-driven
composition).
Since an entity is a regular alist, you can inspect it the usual way
at the REPL: =(entity-ref e #:vx)=, =(assq #:tags e)=, =(length e)=.
** The entity API
All entity operations are *pure and immutable*: every call returns a
fresh alist; your input is never mutated. The full surface is small.
*** =entity-ref entity key [default]=
Looks up =key=. If absent, returns =default= (or calls =default= when
it's a procedure, so you can raise an error lazily):
#+begin_src scheme
(entity-ref player #:x) ; → 150
(entity-ref player #:missing #f) ; → #f
(entity-ref player #:x (lambda () (error "no x"))) ; default is a thunk
#+end_src
*** =entity-type entity=
Shorthand for =(entity-ref entity #:type #f)=.
*** =entity-set entity key val=
Returns a new entity with =key= bound to =val= (replacing any prior
binding). Guaranteed to leave at most one entry for that key:
#+begin_src scheme
(define moved (entity-set player #:x 200))
(entity-ref player #:x) ; → 150 (unchanged)
(entity-ref moved #:x) ; → 200
#+end_src
*** =entity-set-many entity pairs=
Applies a list of =(key . val)= pairs in order:
#+begin_src scheme
(entity-set-many player '((#:vx . 3) (#:facing . 1)))
#+end_src
Used internally by =instantiate-prefab= to layer all prefab fields onto
a fresh =make-entity= base.
*** =entity-update entity key proc [default]=
Shortcut for "read, transform, write":
#+begin_src scheme
(entity-update player #:x (lambda (x) (+ x 1))) ; → x incremented
(entity-update player #:score add1 0) ; with default 0
#+end_src
Because everything is immutable, update chains are usually written as
=let*= or =chain= (SRFI-197):
#+begin_src scheme
(let* ((p (entity-set player #:vx 3))
(p (entity-set p #:facing 1))
(p (animate-entity p anims)))
p)
#+end_src
** Prefabs and mixins
Hand-writing a long plist for every enemy gets old fast. The
=downstroke-prefabs= module loads a data file that declares reusable
*mixins* (named bundles of keys) and *prefabs* (named entities built
by combining mixins and inline overrides).
A prefab data file is a single sexp with =mixins=, =prefabs=, and an
optional =group-prefabs= section. Here is the animation demo's file
(=demo/assets/animation-prefabs.scm=):
#+begin_src scheme
((mixins)
(prefabs
(timed-frames animated #:type timed-frames #:anim-name walk
#:animations ((#:name walk #:frames ((28 10) (29 1000)))))
(std-frames animated #:type std-frames #:anim-name attack
#:animations ((#:name attack #:frames (28 29) #:duration 10)))))
#+end_src
Each prefab entry has the shape =(name mixin-name ... #:k v #:k v ...)=.
Before the first keyword, identifiers name *mixins* to pull in; from
the first keyword onward you write *inline fields*.
The engine ships a small mixin table via =(engine-mixins)=:
#+begin_src scheme
(engine-mixins)
;; → ((physics-body #:vx 0 #:vy 0 #:ay 0 #:gravity? #t #:solid? #t #:on-ground? #f)
;; (has-facing #:facing 1)
;; (animated #:anim-name idle #:anim-frame 0 #:anim-tick 0
;; #:tile-id 0 #:animations #t))
#+end_src
User-defined mixins go in the =(mixins ...)= section of the data file
and take precedence if they share a name with an engine mixin.
*** Merge semantics — inline wins
When a prefab is composed, the merge order is:
1. Inline fields on the prefab entry (highest priority).
2. Each mixin named in the entry, in the order written.
=alist-merge= is *earlier-wins*, so inline fields always override mixin
defaults. Using the entry:
#+begin_src scheme
(timed-frames animated #:type timed-frames #:anim-name walk ...)
#+end_src
=animated= contributes =#:anim-name idle= among other things, but the
inline =#:anim-name walk= wins.
Nested plist-valued keys (currently =#:animations= and =#:parts=) are
deep-converted to alists at load time, so =#:animations= ends up as a
list of alists like =((#:name walk #:frames (28 29) #:duration 10))=.
*** =load-prefabs= and =instantiate-prefab=
Load a prefab file once at =create:= time, then instantiate as many
entities as you need:
#+begin_src scheme
(import downstroke-prefabs)
(define registry
(load-prefabs "demo/assets/animation-prefabs.scm"
(engine-mixins) ; engine's built-in mixins
'())) ; no user hooks
(define e1 (instantiate-prefab registry 'std-frames 80 80 16 16))
(define e2 (instantiate-prefab registry 'timed-frames 220 60 16 16))
#+end_src
=instantiate-prefab= signature: =(registry type x y w h) → entity= (or
=#f= if the prefab isn't registered; also =#f= if =registry= itself is
=#f=). The =x y w h= arguments seed =make-entity= and are then
overwritten by any corresponding fields from the prefab.
If an entity carries an =#:on-instantiate= key — either a procedure or
a symbol naming a *user hook* passed into =load-prefabs= — the hook is
invoked on the fresh entity and its result replaces it. That's how
prefabs run per-type setup logic (e.g. computing sprite frames from
size) without the engine baking a policy in.
Group prefabs (=group-prefabs= section, instantiated via
=instantiate-group-prefab=) return a list =(origin member ...)= for
rigid assemblies like moving platforms; see the existing entity-groups
material in this file's future revisions and the sandbox demo
(=demo/assets/sandbox-groups.scm=) for a worked example.
** Skipping pipeline steps
Each frame the engine runs a sequence of per-entity *pipeline steps*
(acceleration, gravity, velocity-x, tile-collisions-x, velocity-y,
tile-collisions-y, on-solid, tweens, animation, entity-collisions,
…). An individual entity can opt out of any of these by listing the
step's symbol in its =#:skip-pipelines= key:
#+begin_src scheme
(entity-set player #:skip-pipelines '(gravity velocity-x))
;; → player now ignores gravity and horizontal motion integration
#+end_src
The predicate is =entity-skips-pipeline?=:
#+begin_src scheme
(entity-skips-pipeline? player 'gravity) ; → #t / #f
#+end_src
Every built-in step is defined with the =define-pipeline= macro
(=downstroke-entity=), which wraps the body in the skip check. The
macro has two shapes:
#+begin_src scheme
(define-pipeline (identifier name) (scene entity dt)
body)
(define-pipeline (identifier name) (scene entity dt)
guard: guard-expr
body)
#+end_src
- =identifier= is the procedure name (e.g. =apply-gravity=).
- =name= is the symbol users put into =#:skip-pipelines= (e.g. =gravity=).
- =guard-expr=, when given, must evaluate truthy for the body to run;
otherwise the entity is returned unchanged.
Example from =physics.scm=:
#+begin_src scheme
(define-pipeline (apply-gravity gravity) (scene entity dt)
guard: (entity-ref entity #:gravity? #f)
(entity-set entity #:vy (+ (entity-ref entity #:vy) *gravity*)))
#+end_src
Reading the shape: the procedure is =apply-gravity=; adding =gravity=
to =#:skip-pipelines= disables it on one entity; =guard:= means the
step is skipped entity-wide when =#:gravity?= is false.
For the full list of built-in step symbols, see
[[file:physics.org][physics.org]].
* Common patterns
** Build an ad-hoc entity inline with =plist->alist=
Good for one-offs, tiny demos, prototypes, and scripts where pulling
in a data file is overkill. The getting-started and scaling demos do
this exclusively:
#+begin_src scheme
(plist->alist
(list #:type 'box
#:x (/ +width+ 2) #:y (/ +height+ 2)
#:width +box-size+ #:height +box-size+
#:vx 0 #:vy 0
#:color '(255 200 0)))
#+end_src
*Scaling demo* — run with =bin/demo-scaling=, source in
=demo/scaling.scm=.
*Getting-started demo* — run with =bin/demo-getting-started=, source in
=demo/getting-started.scm=.
** Create a prefab file and instantiate from it
When several entities share fields, lift them into mixins and let
prefabs stamp them out:
#+begin_src scheme
;; assets/actors.scm
((mixins
(enemy-defaults #:solid? #t #:tags (enemy) #:hp 3))
(prefabs
(grunt physics-body has-facing enemy-defaults #:type grunt
#:tile-id 50 #:width 16 #:height 16)
(brute physics-body has-facing enemy-defaults #:type brute
#:tile-id 51 #:width 32 #:height 32 #:hp 8)))
#+end_src
#+begin_src scheme
(define reg (load-prefabs "assets/actors.scm" (engine-mixins) '()))
(define g (instantiate-prefab reg 'grunt 100 100 16 16))
(define b (instantiate-prefab reg 'brute 200 100 32 32))
#+end_src
=physics-body=, =has-facing=, =animated= are engine mixins — see
=(engine-mixins)= above. Inline fields (e.g. =#:hp 8= on =brute=)
override values from mixins.
*Animation demo* (prefab + load-prefabs) — run with =bin/demo-animation=,
source in =demo/animation.scm=.
*Platformer demo* (hand-built player via =plist->alist=) — run with
=bin/demo-platformer=, source in =demo/platformer.scm=.
** Add a user-defined mixin
User mixins live in the same data file's =(mixins ...)= section; if the
name collides with an engine mixin, the user version wins:
#+begin_src scheme
((mixins
;; Overrides the engine's physics-body: no gravity for this game.
(physics-body #:vx 0 #:vy 0 #:gravity? #f #:solid? #t)
;; A brand-new mixin:
(stompable #:stompable? #t #:stomp-hp 1))
(prefabs
(slime physics-body stompable #:type slime #:tile-id 70)))
#+end_src
No engine change is needed — mixin names are resolved at
=load-prefabs= time.
** Write your own pipeline step
When you have per-entity logic that should honor =#:skip-pipelines=,
reach for =define-pipeline= instead of writing a plain function. A
minimal example:
#+begin_src scheme
(import downstroke-entity)
;; A decay step. Users can skip it with #:skip-pipelines '(decay).
(define-pipeline (apply-decay decay) (scene entity dt)
guard: (entity-ref entity #:decays? #f)
(entity-update entity #:hp (lambda (hp) (max 0 (- hp 1))) 0))
#+end_src
Call it like any other step: =(apply-decay scene entity dt)=. Wiring
it into the frame is the engine's job; see [[file:physics.org][physics.org]] for how
built-in steps are composed and how to provide a custom
=engine-update= if you need a different order.
* See also
- [[file:guide.org][guide.org]] — getting started; the 20-line game that uses entities.
- [[file:physics.org][physics.org]] — full list of pipeline step symbols, =guard:= clauses,
and per-step behavior.
- [[file:tweens.org][tweens.org]] — using =#:tween= and the =tweens= pipeline step on
entities.
- [[file:animation.org][animation.org]] — =#:animations=, =#:anim-name=, the =animated= mixin.
- [[file:input.org][input.org]] — reading the input system to drive entity updates.
- [[file:scenes.org][scenes.org]] — scene-level queries (=scene-find-tagged=,
=scene-add-entity=, =update-scene=).
- [[file:rendering.org][rendering.org]] — how =#:tile-id=, =#:color=, =#:facing=, and
=#:skip-render= affect drawing.
- [[file:audio.org][audio.org]] — triggering sounds from entity update code.
|