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:

  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:

(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)
  • 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:

(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 #:tween and the tweens pipeline step on entities.
  • animation.org#:animations, #:anim-name, the animated mixin.
  • 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-render affect drawing.
  • audio.org — triggering sounds from entity update code.

Author: Gene Pasquet

Created: 2026-04-20 Mon 15:22

Validate