Follow-up to #43: animation ergonomics — named clips, Anim.play, frame events, fluent Tween chains #48

Closed
opened 2026-08-31 14:43:00 +02:00 by orkun · 1 comment
Owner

Follow-up to #43, which shipped the engine-owned-system hook and the two components it drives (b014333): a system the engine inserts into the frame loop over a component a game merely declares and carries — SpriteAnim { ticks, fps, frames, mode, frame } (auto-advanced spritesheet frame, loop/once/pingpong) and Motion { ticks, dur, from, to, ease, value, done } (auto-advanced value tween). That was the dependency #43 named as blocking ("Ludic has no auto-injected systems over user components yet"); it now exists, is deterministic, and is spliced only when a game declares the component. See runtime/native/systems.ludic, examples/library/anim_ecs.ludic, and the Engine-owned systems section of LANGUAGE.md.

This tracks the ergonomic layer on top, deliberately separate because it needs a clip/asset registry and codegen rather than the pure per-frame tick that shipped:

  • Named spritesheet clips: Sprite.sheet(...) / Sprite.clip(name, frames, fps, loop) and Anim.play(entity, "run") resolving a name to the SpriteAnim fields — needs a compile-time clip registry keyed by name.
  • Anim.play(entity, fps, frames, mode) / Motion.to(entity, from, to, dur, ease) namespace sugar over the component writes (today a game sets the fields directly in spawn).
  • Frame events / callbacks: fire an event on a given frame (footstep, hitbox-active) — needs a system to emit an event mid-tick.
  • Fluent Tween handles: Tween.chain / Tween.parallel / delay, a disposable handle the engine advances beyond a single Motion.

Related: #43 (the shipped hook + components), #47 (the other consumer of the same hook).

Follow-up to #43, which shipped the **engine-owned-system hook** and the two components it drives (b014333): a system the engine inserts into the frame loop over a component a game merely declares and carries — `SpriteAnim { ticks, fps, frames, mode, frame }` (auto-advanced spritesheet frame, loop/once/pingpong) and `Motion { ticks, dur, from, to, ease, value, done }` (auto-advanced value tween). That was the dependency #43 named as blocking ("Ludic has no auto-injected systems over user components yet"); it now exists, is deterministic, and is spliced only when a game declares the component. See `runtime/native/systems.ludic`, `examples/library/anim_ecs.ludic`, and the *Engine-owned systems* section of LANGUAGE.md. This tracks the **ergonomic layer** on top, deliberately separate because it needs a clip/asset registry and codegen rather than the pure per-frame tick that shipped: - [ ] **Named spritesheet clips**: `Sprite.sheet(...)` / `Sprite.clip(name, frames, fps, loop)` and `Anim.play(entity, "run")` resolving a name to the SpriteAnim fields — needs a compile-time clip registry keyed by name. - [ ] **`Anim.play(entity, fps, frames, mode)` / `Motion.to(entity, from, to, dur, ease)`** namespace sugar over the component writes (today a game sets the fields directly in `spawn`). - [ ] **Frame events / callbacks**: fire an `event` on a given frame (footstep, hitbox-active) — needs a system to emit an event mid-tick. - [ ] **Fluent Tween handles**: `Tween.chain` / `Tween.parallel` / `delay`, a disposable handle the engine advances beyond a single `Motion`. Related: #43 (the shipped hook + components), #47 (the other consumer of the same hook).
Author
Owner

Shipped in 1f5e3c1 (feat(anim): animation ergonomics, #48). The ergonomic layer over the engine-owned SpriteAnim/Motion systems (#43), all integer + deterministic so animation and motion reproduce exactly under replay/lockstep.

  • Named spritesheet clips: Anim.clip("run", frames, fps, mode) registers a clip by name (a name-keyed registry in systems.ludic, up to 32 clips), and Anim.play(entity, "run") resolves the name to the SpriteAnim fields and plays it.
  • Anim.play / Motion.to namespace sugar: Anim.play(entity, fps, frames, mode) sets the clip directly and rewinds it; Motion.to(entity, from, to, dur, ease) starts a value tween over the Motion component — both one call over the reflection-ABI writes, instead of setting the fields in spawn by hand.
  • Frame events / callbacks: Anim.on_frame(entity, frame) arms optional SpriteAnim event_frame/event_fired fields; the engine flags event_fired on the tick the clip first lands on that frame, and Anim.fired(entity) reads it. The engine detects the boundary and gameplay owns the reaction (emit its own event, spawn a hitbox, footstep) — which keeps it inside Ludic's no-runtime-dispatch event model (emit desugars to direct calls, so a generic runtime system can't fire a game event itself).
  • Fluent Tween handles: a new runtime/native/tween.ludic — Tween.to starts a disposable handle, Tween.chain / Tween.delay append segments for a sequence, and Tween.value / Tween.done / Tween.parallel / Tween.stop read and control it. Handles are advanced by a new engine-owned system esys_tween, inserted into the Update phase (gated on g_uses_tween_rt rather than a component). The 1-arg Tween.done(handle) is disambiguated from the existing 2-arg pure Tween.done(timer, dur).

Worked example + regression: examples/library/anim_sugar.ludic (4 8 2 1 0 100 100 0 0 1 20 20 30 0 1). Twelve new docs pages (Anim, a new Motion namespace, the Tween handles). Full suite 77 passed, self-host C-free fixpoint intact, no golden drift.

Shipped in 1f5e3c1 (feat(anim): animation ergonomics, #48). The ergonomic layer over the engine-owned SpriteAnim/Motion systems (#43), all integer + deterministic so animation and motion reproduce exactly under replay/lockstep. - [x] **Named spritesheet clips**: `Anim.clip("run", frames, fps, mode)` registers a clip by name (a name-keyed registry in `systems.ludic`, up to 32 clips), and `Anim.play(entity, "run")` resolves the name to the SpriteAnim fields and plays it. - [x] **`Anim.play` / `Motion.to` namespace sugar**: `Anim.play(entity, fps, frames, mode)` sets the clip directly and rewinds it; `Motion.to(entity, from, to, dur, ease)` starts a value tween over the Motion component — both one call over the reflection-ABI writes, instead of setting the fields in `spawn` by hand. - [x] **Frame events / callbacks**: `Anim.on_frame(entity, frame)` arms optional `SpriteAnim` `event_frame`/`event_fired` fields; the engine flags `event_fired` on the tick the clip first lands on that frame, and `Anim.fired(entity)` reads it. The engine detects the boundary and gameplay owns the reaction (emit its own event, spawn a hitbox, footstep) — which keeps it inside Ludic's no-runtime-dispatch event model (`emit` desugars to direct calls, so a generic runtime system can't fire a game event itself). - [x] **Fluent Tween handles**: a new `runtime/native/tween.ludic` — `Tween.to` starts a disposable handle, `Tween.chain` / `Tween.delay` append segments for a sequence, and `Tween.value` / `Tween.done` / `Tween.parallel` / `Tween.stop` read and control it. Handles are advanced by a new engine-owned system `esys_tween`, inserted into the Update phase (gated on `g_uses_tween_rt` rather than a component). The 1-arg `Tween.done(handle)` is disambiguated from the existing 2-arg pure `Tween.done(timer, dur)`. Worked example + regression: `examples/library/anim_sugar.ludic` (`4 8 2 1 0 100 100 0 0 1 20 20 30 0 1`). Twelve new docs pages (Anim, a new Motion namespace, the Tween handles). Full suite 77 passed, self-host C-free fixpoint intact, no golden drift.
orkun closed this issue 2026-08-31 15:53:34 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: workshopsoft/ludic#48
No description provided.