diff options
Diffstat (limited to 'docs/entities.org')
| -rw-r--r-- | docs/entities.org | 375 |
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. |
