diff options
| author | Gene Pasquet <dev@etenil.net> | 2026-04-10 17:28:23 +0100 |
|---|---|---|
| committer | Gene Pasquet <dev@etenil.net> | 2026-04-10 17:28:23 +0100 |
| commit | 9ffd919e293324332acd87cd129c8d73ea27035a (patch) | |
| tree | be443b7ac6d4a2c33c823435e3f87d9269ff90b2 | |
| parent | 6734511622f6cc9c625bec6a2ee55413f0689946 (diff) | |
Rework animations and entities - somewhat
| -rw-r--r-- | animation.scm | 101 | ||||
| -rw-r--r-- | docs/entities.org | 83 | ||||
| -rw-r--r-- | entity.scm | 10 | ||||
| -rw-r--r-- | tests/animation-test.scm | 20 | ||||
| -rw-r--r-- | tests/entity-test.scm | 10 |
5 files changed, 145 insertions, 79 deletions
diff --git a/animation.scm b/animation.scm index 4caf1fe..468f7f0 100644 --- a/animation.scm +++ b/animation.scm @@ -2,54 +2,75 @@ (import scheme (chicken base) (chicken keyword) + (only srfi-1 filter) downstroke-entity downstroke-world) ;; ---- Animation data accessors ---- - (define (animation-frames anim) (get-keyword #:frames anim)) - (define (animation-duration anim) (get-keyword #:duration anim)) - - ;; ---- frame->tile-id ---- - ;; Given a frames list and frame index, return the tile ID (1-indexed). - - (define (frame->tile-id frames frame-idx) - (+ 1 (list-ref frames (modulo frame-idx (length frames))))) - - ;; ---- set-animation ---- - ;; Switch to a new animation, resetting frame and tick counters. - ;; No-op if the animation is already active (avoids restart mid-loop). - - (define (set-animation entity name) - (if (eq? (entity-ref entity #:anim-name #f) name) - entity - (entity-set (entity-set (entity-set entity #:anim-name name) - #:anim-frame 0) - #:anim-tick 0))) - - ;; ---- animate-entity ---- - ;; Advance the animation tick/frame counter for one game tick. - ;; Pass the animation table for this entity's type. - ;; Entities without #:anim-name are returned unchanged. - - (define (advance-animation entity anim) - (let* ((tick (+ 1 (entity-ref entity #:anim-tick 0))) - (duration (animation-duration anim)) - (frames (animation-frames anim)) - (frame (entity-ref entity #:anim-frame 0))) - (if (>= tick duration) - (let ((new-frame (modulo (+ frame 1) (length frames)))) - (entity-set (entity-set (entity-set entity - #:anim-tick 0) - #:anim-frame new-frame) - #:tile-id (frame->tile-id frames new-frame))) - (entity-set (entity-set entity #:anim-tick tick) - #:tile-id (frame->tile-id frames frame))))) + (define (animation-frames anim) + (get-keyword #:frames anim)) + (define (animation-duration anim) + (get-keyword #:duration anim)) + +(define (frame-by-idx frames frame-idx) + (list-ref frames (modulo frame-idx (length frames)))) + +;; The tile ID is 1-indexed. +(define (frame->tile-id frames frame-idx) + (let ((frame-def (frame-by-idx frames frame-idx))) + (+ 1 (if (list? frame-def) + (car frame-def) + frame-def)))) + +(define (frame->duration frames frame-idx) + (let ((frame-def (frame-by-idx frames frame-idx))) + (if (list? frame-def) + (cadr frame-def) + 10))) + +;; ---- set-animation ---- +;; Switch to a new animation, resetting frame and tick counters. +;; No-op if the animation is already active (avoids restart mid-loop). + +(define (set-animation entity name) + (if (eq? (entity-ref entity #:anim-name #f) name) + entity + (entity-set (entity-set (entity-set entity #:anim-name name) + #:anim-frame 0) + #:anim-tick 0))) + + +(define (animation-by-name animations name) + (let ((matching-anims (filter (lambda (anim) (eq? (get-keyword #:name anim) 'walk)) animations))) + (if matching-anims + (car matching-anims) + #f))) + +;; ---- animate-entity ---- +;; Advance the animation tick/frame counter for one game tick. +;; Pass the animation table for this entity's type. +;; Entities without #:anim-name are returned unchanged. + +(define (advance-animation entity anim) + (let* ((tick (+ 1 (entity-ref entity #:anim-tick 0))) + (duration (animation-duration anim)) + (frames (animation-frames anim)) + (frame (entity-ref entity #:anim-frame 0))) + (if (>= tick duration) + (let ((new-frame-id (modulo (+ frame 1) (length frames)))) + (entity-set-many entity + (list (cons #:anim-tick 0) + (cons #:anim-frame new-frame-id) + (cons #:tile-id (frame->tile-id frames new-frame-id)) + (cons #:duration (frame->duration frames new-frame-id))))) + (entity-set-many entity + (list (cons #:anim-tick tick) + (cons #:tile-id (frame->tile-id frames frame))))))) (define (animate-entity entity animations) (let* ((anim-name (entity-ref entity #:anim-name #f)) - (entry (and anim-name (assq anim-name animations))) - (anim (and entry (cdr entry)))) + (anim (and anim-name (animation-by-name animations anim-name)))) (if anim (advance-animation entity anim) entity))) diff --git a/docs/entities.org b/docs/entities.org index 06b1831..4e451d3 100644 --- a/docs/entities.org +++ b/docs/entities.org @@ -24,7 +24,14 @@ The optional key ~#:skip-pipelines~ holds a list of **symbols** naming frame pip * Creating Entities -There is no ~make-entity~ constructor. Create an entity as a plain list: +There is a basic ~make-entity~ constructor, which carries positional data (x y w h); its type is ='none=: + +#+begin_src scheme + (define my-entity (make-entity 200 150 16 16)) +#+end_src + + +However creating an entity is as simple as a plain list: #+begin_src scheme (define my-entity @@ -77,6 +84,19 @@ Returns a **new** plist with the key/value pair set (or replaced). The original (entity-ref updated-player #:vx) ; → 5 #+end_src +** ~entity-set-many entity pairs~ + +Sets multiple attributes of an entity at once. + +#+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]~ Returns a new entity with ~key~ set to ~(proc (entity-ref entity key default))~. Useful for incrementing or transforming a value: @@ -87,14 +107,13 @@ Returns a new entity with ~key~ set to ~(proc (entity-ref entity key default))~. ** Chaining Updates: The let* Pattern -Since each update returns a new entity, chain updates with ~let*~: +Since each update returns a new entity, chain updates with ~chain~ (srfi-197): #+begin_src scheme -(let* ((player (entity-set player #:vx 3)) - (player (apply-velocity-x player scene dt)) - (player (resolve-tile-collisions-x player scene dt))) - ;; now use the updated player - player) + (chain player + (entity-set _ #:vx 3) + (apply-velocity-x _ scene dt) + (resolve-tile-collisions-x _ scene dt)) #+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)=. @@ -103,31 +122,31 @@ With the default =engine-update=, you normally set =#:vx= / =#:ay= in =update:= 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). | +| 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). | * Entity groups (prefab assemblies) @@ -27,8 +27,14 @@ (loop (cddr lst) (cons v (cons k acc))))))))) (cons key (cons val cleaned)))) - (define (entity-update entity key proc #!optional default) - (entity-set entity key (proc (entity-ref entity key default)))) +(define (entity-set-many entity pairs) + (fold (lambda (pair working-ent) + (entity-set working-ent (car pair) (cdr pair))) + entity + pairs)) + +(define (entity-update entity key proc #!optional default) + (entity-set entity key (proc (entity-ref entity key default)))) ;; #:skip-pipelines — list of symbols naming frame pipeline steps to skip for this ;; entity. Physics documents the built-in step names (see docs/physics.org). Other diff --git a/tests/animation-test.scm b/tests/animation-test.scm index e12b35b..aaaba41 100644 --- a/tests/animation-test.scm +++ b/tests/animation-test.scm @@ -8,9 +8,19 @@ (test-begin "animation") (test-group "frame->tile-id" - (test-equal "first frame, frames (0)" 1 (frame->tile-id '(0) 0)) - (test-equal "wraps around" 1 (frame->tile-id '(0 1) 2)) - (test-equal "frame 1 of (27 28)" 29 (frame->tile-id '(27 28) 1))) + (test-group "tile IDs only" + (test-equal "first frame, frames (0)" 1 (frame->tile-id '(0) 0)) + (test-equal "wraps around" 1 (frame->tile-id '(0 1) 2)) + (test-equal "frame 1 of (27 28)" 29 (frame->tile-id '(27 28) 1))) + (test-group "tile IDs and durations" + (test-equal "first frame, frames (0)" 1 (frame->tile-id '((0 10)) 0)) + (test-equal "wraps around" 1 (frame->tile-id '((0 10) (1 10)) 2)) + (test-equal "frame 1 of (27 28)" 29 (frame->tile-id '((27 10) (28 10)) 1)))) + +(test-group "frame->duration" + (test-equal "first frame, frames (0)" 100 (frame->duration '((0 100)) 0)) + (test-equal "wraps around" 100 (frame->duration '((0 100) (1 200)) 2)) + (test-equal "frame 1 of (27 28)" 200 (frame->duration '((27 100) (28 200)) 1))) (test-group "set-animation" (let ((entity (list #:type 'player #:anim-name 'idle #:anim-frame 5 #:anim-tick 8))) @@ -21,12 +31,12 @@ (test-equal "resets tick" 0 (entity-ref switched #:anim-tick))))) (test-group "animate-entity" - (let* ((anims '((walk . (#:frames (0 1) #:duration 4)))) + (let* ((anims '((#:name walk #:frames (0 1) #:duration 4))) (entity (list #:type 'player #:anim-name 'walk #:anim-frame 0 #:anim-tick 0)) (stepped (animate-entity entity anims))) (test-equal "increments tick" 1 (entity-ref stepped #:anim-tick)) (test-equal "sets tile-id on first tick" 1 (entity-ref stepped #:tile-id))) - (let* ((anims '((walk . (#:frames (0 1) #:duration 2)))) + (let* ((anims '((#:name walk #:frames (0 1) #:duration 2))) (entity (list #:type 'player #:anim-name 'walk #:anim-frame 0 #:anim-tick 1)) (advanced (animate-entity entity anims))) (test-equal "advances frame when tick reaches duration" 1 (entity-ref advanced #:anim-frame)) diff --git a/tests/entity-test.scm b/tests/entity-test.scm index 270555c..9c7607c 100644 --- a/tests/entity-test.scm +++ b/tests/entity-test.scm @@ -79,6 +79,16 @@ (test-equal "existing key untouched" 10 (entity-ref e #:x)) (test-equal "list grows by one pair" 4 (length e))))) +(test-group "entity-set-many" + (test-group "Set multiple entities with lists" + (let ((e (entity-set-many '(#:x 10 #:y 20) '((#:x 15) (#:y 25))))) + (test-equal "value x updated" (entity-ref e #:x) 15) + (test-equal "value y updated" (entity-ref e #:y) 25))) + (test-group "Set multiple entities with cons" + (let ((e (entity-set-many '(#:x 10 #:y 20) (list (cons #:x 15) (cons #:y 25))))) + (test-equal "value x updated" (entity-ref e #:x) 15) + (test-equal "value y updated" (entity-ref e #:y) 25)))) + ;; Test: entity-update applies transformations (test-group "entity-update" (test-group "transform existing value" |
