Entities
Table of Contents
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.
1. 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):
(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))))
Read a value with entity-ref, update it (functionally) with
entity-set:
(entity-ref player #:x) ; → 150 (entity-set player #:x 200) ; → new entity, player still has #:x 150
Getting-started demo — run with bin/demo-getting-started, source in
demo/getting-started.scm.
2. Core concepts
2.1. 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:
((#:type . player) (#:x . 100) (#:y . 50) (#:width . 16) (#:height . 16) (#:vx . 0) (#:vy . 0) (#:gravity? . #t) (#:on-ground? . #f) (#:tile-id . 1) (#:tags . (player)))
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:
(make-entity x y w h) ;; → ((#:type . none) (#:x . x) (#:y . y) (#:width . w) (#:height . h))
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).
2.2. 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.
2.2.1. 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):
(entity-ref player #:x) ; → 150 (entity-ref player #:missing #f) ; → #f (entity-ref player #:x (lambda () (error "no x"))) ; default is a thunk
2.2.2. entity-type entity
Shorthand for (entity-ref entity #:type #f).
2.2.3. 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:
(define moved (entity-set player #:x 200)) (entity-ref player #:x) ; → 150 (unchanged) (entity-ref moved #:x) ; → 200
2.2.4. entity-set-many entity pairs
Applies a list of (key . val) pairs in order:
(entity-set-many player '((#:vx . 3) (#:facing . 1)))
Used internally by instantiate-prefab to layer all prefab fields onto
a fresh make-entity base.
2.2.5. entity-update entity key proc [default]
Shortcut for "read, transform, write":
(entity-update player #:x (lambda (x) (+ x 1))) ; → x incremented (entity-update player #:score add1 0) ; with default 0
Because everything is immutable, update chains are usually written as
let* or chain (SRFI-197):
(let* ((p (entity-set player #:vx 3))
(p (entity-set p #:facing 1))
(p (animate-entity p anims)))
p)
2.3. 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):
((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)))))
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):
(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))
User-defined mixins go in the (mixins ...) section of the data file
and take precedence if they share a name with an engine mixin.
2.3.1. Merge semantics — inline wins
When a prefab is composed, the merge order is:
- Inline fields on the prefab entry (highest priority).
- 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:
(timed-frames animated #:type timed-frames #:anim-name walk ...)
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)).
2.3.2. load-prefabs and instantiate-prefab
Load a prefab file once at create: time, then instantiate as many
entities as you need:
(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))
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.
2.4. 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:
(entity-set player #:skip-pipelines '(gravity velocity-x)) ;; → player now ignores gravity and horizontal motion integration
The predicate is entity-skips-pipeline?:
(entity-skips-pipeline? player 'gravity) ; → #t / #f
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:
(define-pipeline (identifier name) (scene entity dt)
body)
(define-pipeline (identifier name) (scene entity dt)
guard: guard-expr
body)
identifieris the procedure name (e.g.apply-gravity).nameis 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:
(define-pipeline (apply-gravity gravity) (scene entity dt)
guard: (entity-ref entity #:gravity? #f)
(entity-set entity #:vy (+ (entity-ref entity #:vy) *gravity*)))
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 physics.org.
3. Common patterns
3.1. 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:
(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)))
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.
3.2. Create a prefab file and instantiate from it
When several entities share fields, lift them into mixins and let prefabs stamp them out:
;; 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)))
(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))
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.
3.3. 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:
((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)))
No engine change is needed — mixin names are resolved at
load-prefabs time.
3.4. 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:
(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))
Call it like any other step: (apply-decay scene entity dt). Wiring
it into the frame is the engine's job; see physics.org for how
built-in steps are composed and how to provide a custom
engine-update if you need a different order.
4. See also
- guide.org — getting started; the 20-line game that uses entities.
- physics.org — full list of pipeline step symbols,
guard:clauses, and per-step behavior. - tweens.org — using
#:tweenand thetweenspipeline step on entities. - animation.org —
#:animations,#:anim-name, theanimatedmixin. - input.org — reading the input system to drive entity updates.
- scenes.org — scene-level queries (
scene-find-tagged,scene-add-entity,update-scene). - rendering.org — how
#:tile-id,#:color,#:facing, and#:skip-renderaffect drawing. - audio.org — triggering sounds from entity update code.