#+TITLE: Animation Downstroke animates sprites by swapping an entity's =#:tile-id= on a timed cycle. You declare a table of named animations on the entity under =#:animations=, pick one by name with =#:anim-name=, and the engine's last pipeline stage — =apply-animation= — advances the frame counter every tick. There is no interpolation, no blending, no tween layer: each frame is a whole tile from the tileset, selected by index, held for a fixed number of ticks. The model is small enough to describe in one line of data and small enough to disable per-entity when you want to drive =#:tile-id= by hand. This file assumes you already know how entities are shaped (see [[file:entities.org][entities.org]]) and how the update pipeline runs (see [[file:physics.org][physics.org]]). * The minimum you need An animated entity needs three things: 1. A =#:tile-id= (so there is something to render before the first tick). 2. A non-empty =#:animations= list. 3. A =#:anim-name= that names one of those animations. The simplest hand-built entity looks like this: #+begin_src scheme (list (cons #:type 'player) (cons #:x 80) (cons #:y 80) (cons #:width 16) (cons #:height 16) (cons #:tile-id 28) (cons #:anim-name 'walk) (cons #:anim-frame 0) (cons #:anim-tick 0) (cons #:animations (list (list (cons #:name 'walk) (cons #:frames '(28 29)) (cons #:duration 10))))) #+end_src In practice you never write this by hand. You declare the animation in a prefab data file and let =load-prefabs= + =instantiate-prefab= build the alist for you: #+begin_src scheme ;; demo/assets/animation-prefabs.scm ((mixins) (prefabs (hero animated #:type hero #:anim-name walk #:animations ((#:name walk #:frames (28 29) #:duration 10))))) #+end_src Two things are happening here that are not obvious: - The =animated= mixin (from =engine-mixins=) supplies the =#:anim-frame 0=, =#:anim-tick 0=, and =#:tile-id 0= defaults so you don't have to. - The prefab loader deep-converts the plist-shaped =#:animations= value into a list of alists (see [[file:entities.org][entities.org]] for why prefab data is written as plists but entities are alists). =#:animations= is one of the keys in =+nested-plist-list-keys+= that gets this treatment. Once the prefab is in a scene, the engine does the rest — you don't need to call anything per frame just to make the animation play. * Core concepts ** Animation data shape An animation is an alist with these keys: | Key | Required | Meaning | |--------------+----------+---------------------------------------------------| | =#:name= | yes | Symbol used to select this animation. | | =#:frames= | yes | List of frames (see below). | | =#:duration= | no | Default /per-frame/ budget for *bare* frame | | | | entries (see [[*Duration resolution][Duration resolution]]). | ** =#:frames= layouts (prefab plist style) You can combine bare tile ids and per-frame pairs in the same =#:frames= list, but each element must be unambiguous: - *Bare ids* — every element is a tile id on its own (number or symbol, depending on your data). All such frames share one budget: the animation's =#:duration=, or the global default if the animation omits =#:duration=. #+begin_src scheme (#:name attack #:frames (1 2 3) #:duration 100) #+end_src - *Per-frame budgets* — a frame is a two-element /proper/ list ='(tile-id budget)=. That frame always uses =budget=; the animation's top-level =#:duration= does not apply to it. Lists with more than two elements are /not/ treated as timed frames (they fall through to the bare-id rules and will not behave as intended). #+begin_src scheme (#:name walk #:frames ((1 100) (2 300) (3 25))) #+end_src - *Bare ids, animation-wide default omitted* — same as the first case, but the budget for every bare frame is the global default =+default-anim-duration+= (100 in =animation.scm=, same units as engine =dt= — typically milliseconds). #+begin_src scheme (#:name idle #:frames (1 2 3)) #+end_src *Uniform example* (bare frames, shared =#:duration=): #+begin_src scheme (#:name attack #:frames (28 29) #:duration 10) #+end_src The =#:animations= entity key holds a *list* of these animation records, one per named animation: #+begin_src scheme #:animations ((#:name idle #:frames (10)) (#:name walk #:frames (28 29) #:duration 10) (#:name jump #:frames (30))) #+end_src For a real working example of timed vs uniform frames side by side, see the =timed-frames= and =std-frames= prefabs in =demo/assets/animation-prefabs.scm=. ** Duration resolution For the frame at index =i= in =#:frames=, =animation-frame-duration= resolves the tick budget in this order (and =advance-animation= always uses this — there is no separate “entity override” for the threshold): 1. *Per-frame* — if the entry is a two-element list ='(tile-id budget)=, use =budget=. 2. *Animation default* — otherwise use the animation alist's =#:duration= when present. 3. *Global default* — otherwise =+default-anim-duration+= (100 in =animation.scm=). The same resolution runs on every =apply-animation= tick: the accumulated =#:anim-tick= is compared to =(animation-frame-duration anim (entity-ref entity #:anim-frame))=, and =#:anim-duration= on the entity is set to that resolved value /every/ step so it always reflects the budget for the *current* frame index (whether the frame advanced this tick or not). ** The apply-animation pipeline step =apply-animation= is the *last* stage of =default-engine-update=, after physics, ground detection, entity collisions, and group sync. You can see the wiring in =engine.scm=: #+begin_src scheme (scene-map-entities _ (cut apply-animation <> <> dt)) #+end_src It is defined with =define-pipeline= under the pipeline name =animation=: #+begin_src scheme (define-pipeline (apply-animation animation) (scene entity dt) guard: (entity-ref entity #:animations #f) ...) #+end_src Two things fall out of this definition: 1. *The guard.* If an entity has no =#:animations= value, the pipeline returns the entity unchanged. You do not have to opt out of animation for non-animated entities — not declaring the key is the opt-out. 2. *Skip support.* Because the step is registered under the pipeline name =animation=, any entity with =#:skip-pipelines= containing =animation= is also returned unchanged. Same mechanism as the physics skips described in [[file:physics.org][physics.org]]. When the step does run, =animate-entity= looks up the current animation by =#:anim-name= in the entity's =#:animations= list, and =advance-animation= does the actual work: - Resolve the current frame's tick budget with =animation-frame-duration= (see [[*Duration resolution][Duration resolution]]); mirror that value onto =#:anim-duration= on the entity. - Accumulate =#:anim-tick= by the frame delta =dt= (same units as the budgets above). - If the tick reaches or exceeds that budget, advance =#:anim-frame= (modulo the number of frames), reset =#:anim-tick= to 0, update =#:anim-duration= to the /new/ frame's resolved budget, and write the new frame's =#:tile-id= onto the entity. - Otherwise, write the current frame's =#:tile-id=, the incremented tick, and keep =#:anim-duration= equal to the /current/ frame's resolved budget (unchanged value unless you changed =#:frames= or animation =#:duration= out of band). The renderer reads =#:tile-id= directly — so as long as the pipeline ran, what ends up on screen is always the current frame's tile. Running the =animated= pipeline after physics means your visual state reflects the entity's *post-physics* position and flags for the frame. If you want to swap animations based on state (e.g. walking vs jumping), your user =update:= hook — which runs after physics too — is the right place. ** Switching animations You change what an entity is playing by setting =#:anim-name=. The helper for this is =set-animation=: #+begin_src scheme (set-animation entity 'walk) #+end_src =set-animation= has one important subtlety: *if the requested name matches the entity's current =#:anim-name=, it is a no-op*. The entity is returned unchanged — tick and frame counters keep their values. This is what you want almost all the time: calling =(set-animation entity 'walk)= every frame while the player is walking should *not* restart the walk cycle on frame 0 each tick. If the name is different, =set-animation= resets both =#:anim-frame= and =#:anim-tick= to 0 so the new animation plays from its first frame. The typical usage pattern is in your =update:= hook, branching on input or physics state: #+begin_src scheme update: (lambda (game dt) (let* ((scene (game-scene game)) (input (game-input game)) (p0 (car (scene-entities scene))) (anim (cond ((not (entity-ref p0 #:on-ground? #f)) 'jump) ((input-held? input 'left) 'walk) ((input-held? input 'right) 'walk) (else 'idle))) (p1 (set-animation p0 anim))) (game-scene-set! game (update-scene scene entities: (list p1))))) #+end_src Because =set-animation= returns a fresh entity alist (it calls =entity-set= three times internally), the result must be written back into the scene. See [[file:entities.org][entities.org]] for the immutable-update convention. ** Interaction with the =animated= mixin =engine-mixins= in =prefabs.scm= includes a convenience mixin named =animated=: #+begin_src scheme (animated #:anim-name idle #:anim-frame 0 #:anim-tick 0 #:tile-id 0 #:animations #t) #+end_src Listing =animated= in a prefab gets you all the bookkeeping keys for free. You almost always want this — it saves repeating =#:anim-frame 0 #:anim-tick 0= on every animated prefab. Two of these defaults are booby-traps: 1. *=#:animations #t=* — a placeholder. The pipeline guard only checks that the value is truthy, so =#t= is enough to make the step run; but the =#t= itself is obviously not a valid animation list. You must override =#:animations= with a real list on any prefab that uses =animated=. If you forget, =animation-by-name= will fail (it calls =filter= on a non-list) the first time the step runs. 2. *=#:anim-name idle=* — also a default. The pipeline will then look up an animation named =idle= in your =#:animations= list. If you don't define one, =animation-by-name= returns =#f=, =animate-entity= returns the entity unchanged, and *=#:tile-id= is never written* by the pipeline. Your entity will render with whatever static =#:tile-id= it happens to have — which, since the mixin sets =#:tile-id 0=, is usually tile 0 (a blank or unexpected tile). The symptom is "my sprite is the wrong tile and never animates" with no error. The animation demo's prefabs show both ways to avoid this. Neither defines an =idle= animation; both override =#:anim-name= to match the animation they do define: #+begin_src scheme (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 The rule is simple: either define an =idle= animation, or override =#:anim-name= to a name you actually defined. * Common patterns ** Declaring walk/idle/jump in a prefab The usual player or enemy prefab declares all the animations it might switch between in one =#:animations= list: #+begin_src scheme (player animated physics-body #:type player #:width 16 #:height 16 #:tile-id 28 #:anim-name idle #:animations ((#:name idle #:frames (28)) (#:name walk #:frames (28 29) #:duration 8) (#:name jump #:frames (30)))) #+end_src Note that this prefab *does* define an =idle= animation, so the =animated= mixin's default =#:anim-name idle= works as-is — no override needed. If the player is standing still and =update:= never calls =set-animation=, the engine will happily play the single-frame =idle= cycle forever. A single-frame animation (like =jump= above) is the idiomatic way to hold a static pose: the cycle advances, the modulo wraps back to frame 0 every tick, and =#:tile-id= stays pinned to the one tile. ** Switching animation based on input Put the decision in your =update:= hook, after you've read input state but before you hand the entity back to the scene: #+begin_src scheme (define (choose-anim entity input) (cond ((not (entity-ref entity #:on-ground? #f)) 'jump) ((or (input-held? input 'left) (input-held? input 'right)) 'walk) (else 'idle))) update: (lambda (game dt) (let* ((scene (game-scene game)) (input (game-input game)) (p0 (car (scene-entities scene))) (p1 (set-animation p0 (choose-anim p0 input)))) (game-scene-set! game (update-scene scene entities: (list p1))))) #+end_src Two details worth noticing: - =set-animation= is safe to call every frame with the same name; it won't reset the cycle. - =apply-animation= runs as part of =default-engine-update=, which runs *before* your user =update:= hook. An =#:anim-name= change you make in =update:= takes effect on the next frame's =apply-animation= call, not this one — a one-frame latency that is imperceptible in practice. ** Per-frame durations for non-uniform timing Use the two-element ='(tile-id budget)= frame form when you want a cycle where one frame lingers and another flashes past. The canonical example (from =demo/assets/animation-prefabs.scm=) is a blink-heavy idle: #+begin_src scheme (#:name idle #:frames ((28 10) (29 1000))) #+end_src Tile 28 shows for 10 ticks (a quick flash), then tile 29 holds for 1000 ticks (a long eye-open pose), then the cycle repeats. Per-frame budgets are step /1/ in [[*Duration resolution][Duration resolution]]; they override the animation's top-level =#:duration=, which in turn overrides the global default for bare frames only. ** Disabling animation on an entity without touching =#:animations= If you want to freeze an animated entity on its current frame — for example, pausing an enemy during a cutscene — the cheapest way is to list =animation= in its =#:skip-pipelines=: #+begin_src scheme (entity-set entity #:skip-pipelines '(animation)) #+end_src This leaves =#:animations=, =#:anim-name=, =#:anim-frame=, =#:anim-tick=, and =#:anim-duration= untouched. When you remove =animation= from =#:skip-pipelines= later, the cycle resumes exactly where it left off. Contrast with =(entity-set entity #:animations #f)=, which strips the animation data entirely and would require you to rebuild it to resume. You can combine =animation= with any of the physics pipeline names in the same skip list — they all share one =#:skip-pipelines= key. See [[file:physics.org][physics.org]] for the full list of step names. * See also - [[file:guide.org][guide.org]] — the minimal-game walkthrough. - [[file:entities.org][entities.org]] — entity alists, the prefab system, and how =#:animations= is deep-converted from plist data. - [[file:physics.org][physics.org]] — the rest of the update pipeline and the =#:skip-pipelines= mechanism. - [[file:tweens.org][tweens.org]] — when you want interpolation rather than discrete frame-swapping. - [[file:../demo/animation.scm][Animation]] (=bin/demo-animation=) — prefab data in [[file:../demo/assets/animation-prefabs.scm][demo/assets/animation-prefabs.scm]].