From 38eee24832fe6da4f135cae455881ab97953b23a Mon Sep 17 00:00:00 2001 From: Gene Pasquet Date: Sat, 18 Apr 2026 02:47:10 +0100 Subject: Refresh docs and re-indent --- docs/entities.org | 549 +++++++++++++++++++++++++++--------------------------- 1 file changed, 271 insertions(+), 278 deletions(-) (limited to 'docs/entities.org') 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. -- cgit v1.2.3