aboutsummaryrefslogtreecommitdiff
path: root/docs/entities.org
diff options
context:
space:
mode:
authorGene Pasquet <dev@etenil.net>2026-04-05 23:12:54 +0100
committerGene Pasquet <dev@etenil.net>2026-04-05 23:12:54 +0100
commitb99ada53b715def5492c7d04c0d327fa7048e5d3 (patch)
tree9e94dbc8ff863ef09ef18f4be31fb45e085572a4 /docs/entities.org
parent027053b11a3a5d861ed2fa2db245388bd95ac246 (diff)
Complete implementation
Diffstat (limited to 'docs/entities.org')
-rw-r--r--docs/entities.org375
1 files changed, 375 insertions, 0 deletions
diff --git a/docs/entities.org b/docs/entities.org
new file mode 100644
index 0000000..db5663d
--- /dev/null
+++ b/docs/entities.org
@@ -0,0 +1,375 @@
+#+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.
+
+* 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 touching a solid tile below (set by ~detect-ground~). Use this to gate jump input: only allow jumping if ~#:on-ground?~ is true. |
+| ~#:solid?~ | boolean | Whether this entity participates in entity-entity collision. If ~#t~, ~resolve-entity-collisions~ will check it against other solid entities. |
+| ~#:tile-id~ | integer | Sprite index in the tileset (1-indexed). Required for rendering with ~draw-sprite~. Updated automatically by animation (~animate-entity~). |
+| ~#: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, 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-ground 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.