aboutsummaryrefslogtreecommitdiff
path: root/docs/entities.org
diff options
context:
space:
mode:
Diffstat (limited to 'docs/entities.org')
-rw-r--r--docs/entities.org549
1 files changed, 271 insertions, 278 deletions
diff --git a/docs/entities.org b/docs/entities.org
index 4e451d3..94e572c 100644
--- a/docs/entities.org
+++ b/docs/entities.org
@@ -1,399 +1,392 @@
-#+TITLE: Entity Model
-#+AUTHOR: Downstroke
-#+DESCRIPTION: How to create, update, and manage entities in the Downstroke game engine
+#+TITLE: Entities
-* Overview
+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.
-Entities in Downstroke are plain Scheme **plists** (property lists) — alternating keyword/value pairs with no special structure or classes. An entity is just a list:
+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.
-#+begin_src scheme
-(list #:type 'player
- #:x 100 #:y 200
- #:width 16 #:height 16
- #:vx 0 #:vy 0
- #:gravity? #t
- #:on-ground? #f
- #:tile-id 1)
-#+end_src
-
-This minimal approach keeps the engine lean: your game defines whatever keys it needs. The shared keys listed below are *conventions* for physics, rendering, and animation — use them to integrate with the engine's built-in systems. Custom keys are always allowed.
-
-* Pipeline skips (~#:skip-pipelines~)
-
-The optional key ~#:skip-pipelines~ holds a list of **symbols** naming frame pipeline steps that should be skipped for that entity. The physics module defines the built-in step names (see ~docs/physics.org~). The predicate ~entity-skips-pipeline?~ and the syntax ~define-pipeline~ (with optional ~guard:~ clause per step) live in ~downstroke-entity~ so any subsystem (physics now; rendering or animation later if you extend the engine) can use the same mechanism without a separate “core pipeline” module.
-
-* Creating Entities
+* The minimum you need
-There is a basic ~make-entity~ constructor, which carries positional data (x y w h); its type is ='none=:
+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
- (define my-entity (make-entity 200 150 16 16))
+(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
-
-However creating an entity is as simple as a plain list:
+Read a value with =entity-ref=, update it (functionally) with
+=entity-set=:
#+begin_src scheme
-(define my-entity
- (list #:type 'enemy
- #:x 200 #:y 150
- #:width 16 #:height 16
- #:vx 0 #:vy 0
- #:gravity? #t
- #:tile-id 42))
+(entity-ref player #:x) ; → 150
+(entity-set player #:x 200) ; → new entity, player still has #:x 150
#+end_src
-Add whatever additional keys your game needs — tags, state, custom data, anything. Entities are pure data.
-
-* Accessing and Updating Entities
+*Getting-started demo* — run with =bin/demo-getting-started=, source in
+=demo/getting-started.scm=.
-Entities are **immutable**. Use these three functions to read and update them:
+* Core concepts
-** ~entity-ref entity key [default]~
+** Entities are alists of CHICKEN keywords
-Returns the value associated with ~key~ in the entity plist, or ~default~ if the key is absent.
+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
-(entity-ref player #:x 0) ; → 100
-(entity-ref player #:vx) ; → 0 (if #:vx is absent)
-(entity-ref player #:custom #f) ; → #f (no #:custom key)
+((#: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
-If ~default~ is a procedure, it is called to compute the default:
+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
-(entity-ref player #:x (lambda () (error "x is required")))
+(make-entity x y w h)
+;; → ((#:type . none) (#:x . x) (#:y . y) (#:width . w) (#:height . h))
#+end_src
-** ~entity-type entity~
+In practice most entities are built via =plist->alist= (for ad-hoc
+inline data) or via =instantiate-prefab= (for data-file-driven
+composition).
-Shorthand for ~(entity-ref entity #:type #f)~. Returns the entity's ~#:type~ or ~#f~.
+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)=.
-#+begin_src scheme
-(entity-type player) ; → 'player
-#+end_src
+** 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-set entity key val~
+*** =entity-ref entity key [default]=
-Returns a **new** plist with the key/value pair set (or replaced). The original entity is unchanged — this is functional, immutable update.
+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
-(define updated-player (entity-set player #:vx 5))
-;; original player is still unchanged:
-(entity-ref player #:vx) ; → 0
-(entity-ref updated-player #:vx) ; → 5
+(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-set-many entity pairs~
+*** =entity-type entity=
-Sets multiple attributes of an entity at once.
+Shorthand for =(entity-ref entity #:type #f)=.
-#+begin_src scheme
- (define updated-player (entity-set-many player '((#:vx 5) (#:vy 10))))
- ;; original player is still unchanged:
- (entity-ref player #:vx) ; → 0
- (entity-ref player #:vy) ; → 0
- (entity-ref updated-player #:vx) ; → 5
- (entity-ref updated-player #:vy) ; → 10
-#+end_src
-
-** ~entity-update entity key proc [default]~
+*** =entity-set entity key val=
-Returns a new entity with ~key~ set to ~(proc (entity-ref entity key default))~. Useful for incrementing or transforming a value:
+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
-(entity-update player #:x (lambda (x) (+ x 3))) ; move right 3 pixels
+(define moved (entity-set player #:x 200))
+(entity-ref player #:x) ; → 150 (unchanged)
+(entity-ref moved #:x) ; → 200
#+end_src
-** Chaining Updates: The let* Pattern
+*** =entity-set-many entity pairs=
-Since each update returns a new entity, chain updates with ~chain~ (srfi-197):
+Applies a list of =(key . val)= pairs in order:
#+begin_src scheme
- (chain player
- (entity-set _ #:vx 3)
- (apply-velocity-x _ scene dt)
- (resolve-tile-collisions-x _ scene dt))
+(entity-set-many player '((#:vx . 3) (#:facing . 1)))
#+end_src
-With the default =engine-update=, you normally set =#:vx= / =#:ay= in =update:= and do not chain physics steps yourself. This =let*= shape is for custom =engine-update= hooks or tests; per-entity steps take =(entity scene dt)=.
-
-* Plist Key Reference
-
-The engine recognizes these standard keys. Use them to integrate with the physics pipeline, rendering, and animation systems. Custom keys are always allowed.
-
-| Key | Type | Description |
-|--------------------------------------+----------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| ~#:type~ | symbol | Entity type, e.g., ~'player~, ~'enemy~, ~'coin~. No built-in enforcement; use for ~entity-type~ checks and scene queries. |
-| ~#:x~, ~#:y~ | number | World position in pixels (top-left corner of bounding box). Updated by ~apply-velocity-x~, ~apply-velocity-y~, and collision resolvers. |
-| ~#:width~, ~#:height~ | number | Bounding box size in pixels. Used for AABB tile collision checks and entity-entity collision. Required for physics. |
-| ~#:vx~, ~#:vy~ | number | Velocity in pixels per frame. ~#:vx~ is updated by ~apply-velocity-x~; ~#:vy~ is updated by ~apply-velocity-y~. Both consumed by collision resolvers. |
-| ~#:ay~ | number | Y acceleration (e.g., from jumping or knockback). Consumed by ~apply-acceleration~, which adds it to ~#:vy~. Optional; default is 0. |
-| ~#:gravity?~ | boolean | Whether gravity applies to this entity. Set to ~#t~ for platformers (gravity pulls down), ~#f~ for top-down or flying entities. Used by ~apply-gravity~. |
-| ~#:on-ground?~ | boolean | Whether the entity is supported from below (set by ~detect-on-solid~ in the default pipeline): solid tile under the feet and/or standing on another solid entity from ~(scene-entities scene)~. Use this in ~update:~ to gate jump input (~#:ay~). |
-| ~#:solid?~ | boolean | Whether this entity participates in entity-entity collision. If ~#t~, ~resolve-entity-collisions~ will check it against other solid entities. |
-| ~#:immovable?~ | boolean | If ~#t~ with ~#:solid? #t~, entity–entity resolution only moves the *other* entity (static platforms). Two overlapping immovable solids are not separated. |
-| ~#:skip-pipelines~ | list of symbols | Optional. Each symbol names a pipeline step to skip (e.g. ~gravity~, ~velocity-x~, ~tweens~). See ~docs/physics.org~ and ~docs/tweens.org~. |
-| ~#:tween~ | tween struct or ~#f~ | Optional. When present, ~step-tweens~ auto-advances the tween each frame. Removed automatically when the tween finishes. See ~docs/tweens.org~. |
-| ~#:tile-id~ | integer | Sprite index in the tileset (1-indexed). Used by ~render-scene!~ when the scene has a tileset texture and tile metadata (from the tilemap or ~scene-tileset~). Updated automatically by animation (~animate-entity~). |
-| ~#:color~ | list | Optional ~(r g b)~ or ~(r g b a)~ (0–255 each). When ~#:tile-id~ is not drawn as a sprite (missing ~#:tile-id~, or no tileset texture), ~render-scene!~ fills the entity rect with this color. |
-| ~#:facing~ | number | Horizontal flip direction: ~1~ = right (default), ~-1~ = left. Used by renderer to flip sprite horizontally. Update when changing direction. |
-| ~#:tags~ | list of symbols | List of tag symbols, e.g., ~'(player solid)~. Used by ~scene-find-tagged~ and ~scene-find-all-tagged~ for fast lookups. |
-| ~#:animations~ | alist | Animation definitions (see Animation section). Keys are animation names (symbols); values are animation specs. |
-| ~#:anim-name~ | symbol | Currently active animation name, e.g., ~'walk~, ~'jump~. Set with ~set-animation~; reset by ~animate-entity~. |
-| ~#:anim-frame~ | integer | Current frame index within the animation (0-indexed). Updated automatically by ~animate-entity~. |
-| ~#:anim-tick~ | integer | Tick counter for frame timing (0 to ~#:duration - 1~). Incremented by ~animate-entity~; resets when frame advances. |
-| ~#:group-id~ | symbol | Shared id for one rigid assembly (from ~instantiate-group-prefab~). All parts and the origin share the same symbol. |
-| ~#:group-origin?~ | boolean | When ~#t~, this entity is the assembly’s pose origin; world ~#:x~ / ~#:y~ drive the group. Members should not set this. |
-| ~#:group-local-x~, ~#:group-local-y~ | number | Offset from the origin’s top-left corner; members’ world position is origin + local (updated by ~sync-groups~ on the entity list, e.g. ~(scene-transform-entities scene sync-groups)~). |
-| ~#:skip-render~ | boolean | When ~#t~, ~render-scene!~ skips drawing this entity (used for invisible origins). |
+Used internally by =instantiate-prefab= to layer all prefab fields onto
+a fresh =make-entity= base.
-* Entity groups (prefab assemblies)
+*** =entity-update entity key proc [default]=
-A **group prefab** describes one *origin* entity plus several *parts* with local offsets. Data lives in the optional ~group-prefabs~ section of the prefab file (alongside ~mixins~ and ~prefabs~). Each group entry has the shape ~(name #:type-members SYMBOL #:parts (part ...) ...)~ with two optional flags:
+Shortcut for "read, transform, write":
-- ~#:pose-only-origin?~ — when ~#t~ (typical for tweened platforms), the origin is invisible, does not run physics pipelines, and is driven by tweens or scripts. When ~#f~ (default), the origin uses a small *physics-driving* profile (~#:gravity? #t~, no ~#:skip-pipelines~): integrate the origin like a mover, then ~(scene-transform-entities scene sync-groups)~ so parts stay glued as a rigid body. For that case, set ~#:origin-width~ and ~#:origin-height~ to the full assembly size (same box as the combined parts); otherwise the origin stays 0×0 and tile collision only sees a point at the reference corner, which can leave the raft overlapping solid floor tiles.
-- ~#:static-parts?~ — when ~#t~, each part gets static rigid-body defaults (no gravity on parts; pose comes from the origin). When ~#f~ (default), parts only have what you put in each part plist.
-
-Each ~part~ is a plist using ~#:local-x~ / ~#:local-y~ (or ~#:group-local-x~ / ~#:group-local-y~) and the usual ~#:width~, ~#:height~, ~#:tile-id~, physics keys, etc.
+#+begin_src scheme
+(entity-update player #:x (lambda (x) (+ x 1))) ; → x incremented
+(entity-update player #:score add1 0) ; with default 0
+#+end_src
-Use ~(instantiate-group-prefab registry 'name origin-x origin-y)~ from ~downstroke-prefabs~ to obtain ~(origin member ...)~. Append all of them to the scene. After moving origins (tweens and/or physics), ensure updated origins are in ~scene-entities~, then ~(scene-transform-entities scene sync-groups)~ so every part’s ~#:x~ / ~#:y~ matches the origin plus local offsets (see ~docs/api.org~ for ordering).
+Because everything is immutable, update chains are usually written as
+=let*= or =chain= (SRFI-197):
-* Entities in Scenes
+#+begin_src scheme
+(let* ((p (entity-set player #:vx 3))
+ (p (entity-set p #:facing 1))
+ (p (animate-entity p anims)))
+ p)
+#+end_src
-A **scene** is a level state: it holds a tilemap (optional), an optional standalone tileset for ~#:tile-id~ drawing without TMX, a camera, and a list of entities. These functions manipulate scene entities:
+** Prefabs and mixins
-** ~scene-entities scene~
+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).
-Returns the list of all entities in the scene.
+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
-(define all-entities (scene-entities scene))
+((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
-** ~update-scene~
+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*.
-Returns a new scene with the specified fields changed. Preferred over the mutating ~scene-entities-set!~.
+The engine ships a small mixin table via =(engine-mixins)=:
#+begin_src scheme
-(update-scene scene entities: (list updated-player updated-enemy))
+(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
-** ~scene-add-entity scene entity~
+User-defined mixins go in the =(mixins ...)= section of the data file
+and take precedence if they share a name with an engine mixin.
-Returns a new scene with the entity appended to the entity list.
+*** Merge semantics — inline wins
-#+begin_src scheme
-(scene-add-entity scene new-enemy)
-#+end_src
+When a prefab is composed, the merge order is:
-** ~scene-map-entities scene proc1 proc2 ...~
+1. Inline fields on the prefab entry (highest priority).
+2. Each mixin named in the entry, in the order written.
-Maps each procedure over the scene's entities, applying them in sequence. Each proc must be a function of one entity, returning a new entity.
+=alist-merge= is *earlier-wins*, so inline fields always override mixin
+defaults. Using the entry:
#+begin_src scheme
-;; Example: map per-entity physics steps (need scene + dt in scope):
-(scene-map-entities scene
- (lambda (e) (apply-gravity e scene dt))
- (lambda (e) (apply-velocity-x e scene dt))
- (lambda (e) (apply-velocity-y e scene dt)))
+(timed-frames animated #:type timed-frames #:anim-name walk ...)
#+end_src
-The result is equivalent to chaining three =scene-map-entities= passes, one per step.
+=animated= contributes =#:anim-name idle= among other things, but the
+inline =#:anim-name walk= wins.
-With default =engine-update=, the engine applies the full pipeline for you; use this pattern inside a custom =engine-update= or tools.
+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))=.
-** ~scene-filter-entities scene pred~
+*** =load-prefabs= and =instantiate-prefab=
-Keeps only entities satisfying the predicate; returns a new scene. Use to despawn dead enemies, collected items, etc.
+Load a prefab file once at =create:= time, then instantiate as many
+entities as you need:
#+begin_src scheme
-;; Remove all entities with #:health <= 0:
-(scene-filter-entities scene
- (lambda (e) (> (entity-ref e #:health 1) 0)))
+(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
-** ~scene-find-tagged scene tag~
+=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.
-Returns the first entity whose ~#:tags~ list contains ~tag~, or ~#f~ if none found.
+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.
-#+begin_src scheme
-(scene-find-tagged scene 'player) ; → player entity or #f
-#+end_src
+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.
-** ~scene-find-all-tagged scene tag~
+** Skipping pipeline steps
-Returns a list of all entities whose ~#:tags~ list contains ~tag~.
+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
-(scene-find-all-tagged scene 'enemy) ; → (enemy1 enemy2 enemy3) or ()
+(entity-set player #:skip-pipelines '(gravity velocity-x))
+;; → player now ignores gravity and horizontal motion integration
#+end_src
-* Animation
+The predicate is =entity-skips-pipeline?=:
-Entities with the ~#:animations~ key can cycle through sprite frames automatically. Animation is data-driven: define sprite sequences once, then switch between them in your update logic.
-
-** Animation Data Format
+#+begin_src scheme
+(entity-skips-pipeline? player 'gravity) ; → #t / #f
+#+end_src
-The ~#:animations~ key holds an **alist** (association list) of animation entries. Each entry is ~(name #:frames (frame-indices...) #:duration ticks-per-frame)~:
+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
-(list #:type 'player
- ...
- #:animations
- '((idle #:frames (28) #:duration 10)
- (walk #:frames (27 28) #:duration 6)
- (jump #:frames (29) #:duration 10)
- (fall #:frames (30) #:duration 10))
- #:anim-name 'idle
- #:anim-frame 0
- #:anim-tick 0)
-#+end_src
+(define-pipeline (identifier name) (scene entity dt)
+ body)
-- ~idle~, ~walk~, ~jump~, ~fall~ are animation **names** (symbols).
-- ~#:frames (28)~ means frame 0 of the animation displays sprite tile 28 (1-indexed in tileset).
-- ~#:frames (27 28)~ means frame 0 displays tile 27, frame 1 displays tile 28, then loops back to frame 0.
-- ~#:duration 6~ means each frame displays for 6 game ticks before advancing to the next frame.
+(define-pipeline (identifier name) (scene entity dt)
+ guard: guard-expr
+ body)
+#+end_src
-** Switching Animations
+- =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.
-Use ~set-animation~ to switch to a new animation, resetting frame and tick counters. If the animation is already active, it is a no-op (avoids restarting mid-loop):
+Example from =physics.scm=:
#+begin_src scheme
-(set-animation player 'walk) ; switch to walk animation
-;; → entity with #:anim-name 'walk, #:anim-frame 0, #:anim-tick 0
+(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
-** Advancing Animation
+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.
-Call ~animate-entity~ once per game frame to step the animation. Pass the entity and its animation table:
+For the full list of built-in step symbols, see
+[[file:physics.org][physics.org]].
-#+begin_src scheme
-(define animations
- '((idle #:frames (28) #:duration 10)
- (walk #:frames (27 28) #:duration 6)))
-
-(let* ((player (set-animation player 'walk))
- (player (animate-entity player animations)))
- player)
-#+end_src
+* Common patterns
-~animate-entity~ does four things:
-1. Increments ~#:anim-tick~ by 1.
-2. If ~#:anim-tick~ reaches ~#:duration~, advances ~#:anim-frame~ and resets tick to 0.
-3. Updates ~#:tile-id~ to the sprite ID for the current frame.
-4. Returns the updated entity (or unchanged entity if ~#:anim-name~ is not set).
+** Build an ad-hoc entity inline with =plist->alist=
-** Typical Animation Update Pattern
+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
-(define (update-player-animation player input)
- (let* ((anim (if (input-held? input 'left) 'walk 'idle))
- (player (set-animation player anim)))
- (animate-entity player player-animations)))
+(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
-* Tags
+*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=.
-The ~#:tags~ key is a list of symbols used to label and query entities. Tags are arbitrary — define whatever makes sense for your game.
+** Create a prefab file and instantiate from it
-** Creating Entities with Tags
+When several entities share fields, lift them into mixins and let
+prefabs stamp them out:
#+begin_src scheme
-(list #:type 'player
- #:tags '(player solid)
- ...)
-
-(list #:type 'enemy
- #:tags '(enemy dangerous)
- ...)
+;; 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
-(list #:type 'coin
- #:tags '(collectible)
- ...)
+#+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
-** Querying by Tag
+=physics-body=, =has-facing=, =animated= are engine mixins — see
+=(engine-mixins)= above. Inline fields (e.g. =#:hp 8= on =brute=)
+override values from mixins.
-Use the scene tag lookup functions:
+*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=.
-#+begin_src scheme
-;; Find the first entity tagged 'player:
-(define player (scene-find-tagged scene 'player))
+** Add a user-defined mixin
-;; Find all enemies:
-(define enemies (scene-find-all-tagged scene 'enemy))
+User mixins live in the same data file's =(mixins ...)= section; if the
+name collides with an engine mixin, the user version wins:
-;; Find all collectibles and remove them:
-(scene-filter-entities scene
- (lambda (e) (not (member 'collectible (entity-ref e #:tags '())))))
+#+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
-** Tag Conventions
-
-While tags are free-form, consider using these conventions in your game:
+No engine change is needed — mixin names are resolved at
+=load-prefabs= time.
-- ~player~: the player character
-- ~enemy~: hostile entities
-- ~solid~: entities that participate in collision
-- ~collectible~: items to pick up
-- ~projectile~: bullets, arrows, etc.
-- ~hazard~: spikes, lava, etc.
+** Write your own pipeline step
-* Example: Complete Entity Setup
-
-Here is a full example showing entity creation, initialization in the scene, and update logic (assumes =downstroke-physics= is imported for =*jump-force*=):
+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
-(define (create-player-entity)
- (list #:type 'player
- #:x 100 #:y 200
- #:width 16 #:height 16
- #:vx 0 #:vy 0
- #:ay 0
- #:gravity? #t
- #:on-ground? #f
- #:tile-id 1
- #:facing 1
- #:tags '(player solid)
- #:animations
- '((idle #:frames (1) #:duration 10)
- (walk #:frames (1 2) #:duration 6)
- (jump #:frames (3) #:duration 10))
- #:anim-name 'idle
- #:anim-frame 0
- #:anim-tick 0))
-
-(define (create-hook game)
- (let* ((scene (game-load-scene! game "assets/level.tmx"))
- (player (create-player-entity)))
- (scene-add-entity scene player)))
-
-(define (update-hook game dt)
- (let* ((input (game-input game))
- (scene (game-scene game))
- (player (scene-find-tagged scene 'player)))
- ;; Intent + presentation — default engine-update already ran physics this frame
- (let* ((player (entity-set player #:vx
- (cond
- ((input-held? input 'left) -3)
- ((input-held? input 'right) 3)
- (else 0))))
- (player (if (and (input-pressed? input 'a)
- (entity-ref player #:on-ground? #f))
- (entity-set player #:ay (- *jump-force*))
- player))
- (player (set-animation player
- (cond
- ((not (entity-ref player #:on-ground? #f)) 'jump)
- ((not (zero? (entity-ref player #:vx 0))) 'walk)
- (else 'idle))))
- (player (animate-entity player player-animations))
- (player (if (< (entity-ref player #:vx 0) 0)
- (entity-set player #:facing -1)
- (entity-set player #:facing 1))))
- (game-scene-set! game
- (update-scene scene entities: (list player))))))
+(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
-Import =*jump-force*= from =downstroke-physics= (or use a literal jump impulse for =#:ay=). The single ~game-scene-set!~ stores the scene after game logic; motion and =#:on-ground?= come from =default-engine-update=.
+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.