#+TITLE: Entity Model #+AUTHOR: Downstroke #+DESCRIPTION: How to create, update, and manage entities in the Downstroke game engine * Overview 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: #+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~ 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 There is no ~make-entity~ constructor. Create an entity as a plain list: #+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)) #+end_src Add whatever additional keys your game needs — tags, state, custom data, anything. Entities are pure data. * Accessing and Updating Entities Entities are **immutable**. Use these three functions to read and update them: ** ~entity-ref entity key [default]~ Returns the value associated with ~key~ in the entity plist, or ~default~ if the key is absent. #+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) #+end_src If ~default~ is a procedure, it is called to compute the default: #+begin_src scheme (entity-ref player #:x (lambda () (error "x is required"))) #+end_src ** ~entity-type entity~ Shorthand for ~(entity-ref entity #:type #f)~. Returns the entity's ~#:type~ or ~#f~. #+begin_src scheme (entity-type player) ; → 'player #+end_src ** ~entity-set entity key val~ Returns a **new** plist with the key/value pair set (or replaced). The original entity is unchanged — this is functional, immutable update. #+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 #+end_src ** ~entity-update entity key proc [default]~ Returns a new entity with ~key~ set to ~(proc (entity-ref entity key default))~. Useful for incrementing or transforming a value: #+begin_src scheme (entity-update player #:x (lambda (x) (+ x 3))) ; move right 3 pixels #+end_src ** Chaining Updates: The let* Pattern Since each update returns a new entity, chain updates with ~let*~: #+begin_src scheme (let* ((player (entity-set player #:vx 3)) (player (apply-velocity-x player)) (player (resolve-tile-collisions-x player tilemap))) ;; now use the updated player player) #+end_src This is how the platformer demo applies physics in order: each step reads the input, computes the next state, and passes it to the next step. * 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~): solid tile under the feet and/or standing on another solid entity when you pass the scene entity list. Use this to gate jump input. | | ~#: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 physics step to skip for this entity (e.g. ~gravity~, ~velocity-x~). See ~docs/physics.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. | * Entities in Scenes 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: ** ~scene-entities scene~ Returns the list of all entities in the scene. #+begin_src scheme (define all-entities (scene-entities scene)) #+end_src ** ~scene-entities-set! scene entities~ Mutates the scene to replace the entity list. Use after ~scene-update-entities~ or batch operations. #+begin_src scheme (scene-entities-set! scene (list updated-player updated-enemy)) #+end_src ** ~scene-add-entity scene entity~ Adds an entity to the scene and returns the scene. Appends to the entity list. #+begin_src scheme (scene-add-entity scene new-enemy) #+end_src ** ~scene-update-entities scene proc1 proc2 ...~ 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. #+begin_src scheme ;; Apply physics pipeline to all entities: (scene-update-entities scene apply-gravity apply-velocity-x apply-velocity-y) #+end_src The result is equivalent to: #+begin_src scheme (let* ((es (scene-entities scene)) (es (map apply-gravity es)) (es (map apply-velocity-x es)) (es (map apply-velocity-y es))) (scene-entities-set! scene es) scene) #+end_src ** ~scene-filter-entities scene pred~ Removes all entities that do not satisfy the predicate. Use to despawn dead enemies, collected items, etc. #+begin_src scheme ;; Remove all entities with #:health <= 0: (scene-filter-entities scene (lambda (e) (> (entity-ref e #:health 1) 0))) #+end_src ** ~scene-find-tagged scene tag~ Returns the first entity whose ~#:tags~ list contains ~tag~, or ~#f~ if none found. #+begin_src scheme (scene-find-tagged scene 'player) ; → player entity or #f #+end_src ** ~scene-find-all-tagged scene tag~ Returns a list of all entities whose ~#:tags~ list contains ~tag~. #+begin_src scheme (scene-find-all-tagged scene 'enemy) ; → (enemy1 enemy2 enemy3) or () #+end_src * Animation 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 The ~#:animations~ key holds an **alist** (association list) of animation entries. Each entry is ~(name #:frames (frame-indices...) #:duration ticks-per-frame)~: #+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 - ~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. ** Switching Animations 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): #+begin_src scheme (set-animation player 'walk) ; switch to walk animation ;; → entity with #:anim-name 'walk, #:anim-frame 0, #:anim-tick 0 #+end_src ** Advancing Animation Call ~animate-entity~ once per game frame to step the animation. Pass the entity and its animation table: #+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 ~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). ** Typical Animation Update Pattern #+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))) #+end_src * Tags The ~#:tags~ key is a list of symbols used to label and query entities. Tags are arbitrary — define whatever makes sense for your game. ** Creating Entities with Tags #+begin_src scheme (list #:type 'player #:tags '(player solid) ...) (list #:type 'enemy #:tags '(enemy dangerous) ...) (list #:type 'coin #:tags '(collectible) ...) #+end_src ** Querying by Tag Use the scene tag lookup functions: #+begin_src scheme ;; Find the first entity tagged 'player: (define player (scene-find-tagged scene 'player)) ;; Find all enemies: (define enemies (scene-find-all-tagged scene 'enemy)) ;; Find all collectibles and remove them: (scene-filter-entities scene (lambda (e) (not (member 'collectible (entity-ref e #:tags '()))))) #+end_src ** Tag Conventions While tags are free-form, consider using these conventions in your game: - ~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. * Example: Complete Entity Setup Here is a full example showing entity creation, initialization in the scene, and update logic: #+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)) (tm (scene-tilemap scene))) ;; Update input-driven velocity (let* ((player (entity-set player #:vx (cond ((input-held? input 'left) -3) ((input-held? input 'right) 3) (else 0)))) ;; Handle jump (player (if (and (input-pressed? input 'a) (entity-ref player #:on-ground? #f)) (entity-set player #:ay -5) player)) ;; Apply physics (player (apply-acceleration player)) (player (apply-gravity player)) (player (apply-velocity-x player)) (player (resolve-tile-collisions-x player tm)) (player (apply-velocity-y player)) (player (resolve-tile-collisions-y player tm)) (player (detect-on-solid player tm)) ;; Update animation (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))) ;; Update facing direction (let ((player (if (< (entity-ref player #:vx 0) 0) (entity-set player #:facing -1) (entity-set player #:facing 1)))) (scene-entities-set! scene (list player)))))) #+end_src Note the let*-chaining pattern: each update builds on the previous result, keeping the data flow clear and each step testable.