Processes & Tweens
YAGE’s process system (from @yagejs/core) gives you timers, tweens, and
sequenced workflows that run inside the game loop. Everything is tied to
entities, so processes are automatically cleaned up when an entity is destroyed.
ProcessComponent
Section titled “ProcessComponent”Add a ProcessComponent to any entity to gain access to entity-level timing:
import { ProcessComponent } from "@yagejs/core";
entity.add(new ProcessComponent());const pc = entity.get(ProcessComponent);A slot is a reusable, restartable process handle. It is ideal for cooldowns, timers, and anything that needs to be checked or restarted repeatedly.
const cooldown = pc.slot({ duration: 300 });
// In update():if (cooldown.completed) { cooldown.start(); // begin the cooldown fire();}restart() combines cancel + start in one call, which is useful when you want
to reset a timer that may still be running:
cooldown.restart();Slots can also run callbacks on completion:
const invincibility = pc.slot({ duration: 2000, onComplete: () => entity.get(HealthComponent).vulnerable = true,});One-Off Processes
Section titled “One-Off Processes”For fire-and-forget delays or single-use timers, use pc.run():
import { Process } from "@yagejs/core";
pc.run(Process.delay(500, () => { spawnExplosion(entity.transform.position); entity.destroy();}));Tweens
Section titled “Tweens”Tweens smoothly interpolate a value over time. They run as processes, so they respect pause state and entity lifetime.
import { Tween, easeOutQuad } from "@yagejs/core";
// Tween a single propertypc.run(Tween.to(sprite, "alpha", 0, 300, easeOutQuad));
// Tween a Vec2 (e.g., position)pc.run(Tween.vec2(entity.transform, "position", targetPos, 600, easeInOutQuad));
// Custom tween with a per-frame callbackpc.run( Tween.custom(500, easeOutQuad, (t) => { entity.transform.setScale(1 + t * 0.5, 1 + t * 0.5); }),);Built-In Easings
Section titled “Built-In Easings”| Function | Curve |
|---|---|
easeLinear | Constant speed |
easeInQuad | Accelerate |
easeOutQuad | Decelerate |
easeInOutQuad | Accelerate then decelerate |
easeOutBounce | Bounce at the end |
Import any easing from @yagejs/core.
Sequences
Section titled “Sequences”Chain multiple steps into a linear or branching workflow using the sequence builder:
import { Sequence } from "@yagejs/core";
pc.run( Sequence.create() .call(() => sprite.alpha = 1) .wait(200) .then(Tween.to(sprite, "alpha", 0, 400, easeOutQuad)) .call(() => entity.destroy()) .build(),);Parallel Steps
Section titled “Parallel Steps”Run multiple processes at the same time within a sequence:
Sequence.create() .parallel( Tween.to(sprite, "alpha", 0, 300, easeOutQuad), Tween.vec2(entity.transform, "position", offscreen, 300, easeInQuad), ) .call(() => entity.destroy()) .build();The sequence advances past a .parallel() step once all of its children
complete.
Looping
Section titled “Looping”// Loop foreverSequence.create() .then(Tween.to(sprite, "alpha", 0.5, 500, easeInOutQuad)) .then(Tween.to(sprite, "alpha", 1.0, 500, easeInOutQuad)) .loop() .build();
// Repeat a fixed number of timesSequence.create() .call(() => flash()) .wait(100) .repeat(5) .build();TimerEntity
Section titled “TimerEntity”For scene-level timing that doesn’t belong to any game object, use
TimerEntity. It comes with a ProcessComponent pre-attached so you don’t
need to create a custom entity:
import { TimerEntity } from "@yagejs/core";
const timer = scene.spawn(TimerEntity);const pc = timer.get(ProcessComponent);
pc.run(Process.delay(3000, () => { scene.switchTo(NextLevel);}));Cancelling by Tag
Section titled “Cancelling by Tag”Tag processes so you can cancel groups of them later:
pc.run(Tween.to(sprite, "alpha", 0, 300, easeOutQuad), { tag: "vfx" });pc.run(Tween.to(sprite, "scale", 2, 300, easeOutQuad), { tag: "vfx" });
// Cancel all processes tagged "vfx"pc.cancel("vfx");This is useful for cleaning up visual effects when an entity changes state (for example, cancelling hit-flash tweens when the entity dies).
Keyframe Animation
Section titled “Keyframe Animation”Tween is great for point-to-point animations — “fade from 1 to 0”, “slide
from A to B”. When you need multi-point animation — a bobbing motion that
goes up, back down, and past the start, or a four-frame easing curve that
isn’t expressible as a single easeInOut — reach for KeyframeAnimator.
KeyframeAnimator is a component that hosts multiple named keyframe tracks
and runs any number of them concurrently. Each track is a list of (time, value) control points, and the animator interpolates between them on every
frame, pushing the result to a setter function you supply.
import { KeyframeAnimator, ProcessComponent, Transform, easeInOutQuad,} from "@yagejs/core";
const entity = scene.spawn("lantern");entity.add(new Transform({ position: new Vec2(100, 200) }));entity.add(new ProcessComponent());
const anim = entity.add( new KeyframeAnimator({ bob: { keyframes: [ { time: 0, data: 0 }, { time: 500, data: 10 }, { time: 1000, data: 0 }, ], setter: (v) => (entity.get(Transform).y = 200 + (v as number)), loop: true, easing: easeInOutQuad, }, }),);
anim.play("bob");This lantern rises 10 pixels, falls back, and loops forever — something
neither a single Tween nor an easing function could express on its own.
KeyframeAnimator requires ProcessComponent on the same entity because it
runs as a process under the hood. Each keyframe’s time is in milliseconds
along the track.
Multiple Tracks
Section titled “Multiple Tracks”You can declare several named animations and play them independently:
const anim = entity.add( new KeyframeAnimator<"bob" | "pulse">({ bob: { keyframes: [ { time: 0, data: 0 }, { time: 500, data: 10 }, { time: 1000, data: 0 }, ], setter: (v) => (entity.get(Transform).y = baseY + (v as number)), loop: true, }, pulse: { keyframes: [ { time: 0, data: 1.0 }, { time: 400, data: 1.2 }, { time: 800, data: 1.0 }, ], setter: (v) => entity.get(Transform).setScale(v as number, v as number), loop: true, speed: 1.5, }, }),);
anim.play("bob");anim.play("pulse");// ... later:anim.stop("pulse");The generic parameter KeyframeAnimator<"bob" | "pulse"> gives you
autocomplete and compile-time checking on the track names — typos like
anim.play("bbo") become type errors.
Each KeyframeAnimationDef accepts the same options you’d expect:
| Option | Purpose |
|---|---|
keyframes | The array of { time, data, easing?, event? } control points |
setter | Function called every frame with the interpolated value |
loop | Restart at time 0 when the track finishes (default false) |
speed | Multiplier on track time (default 1) |
duration | Override the auto-computed track length |
easing | Default easing between keyframes (each keyframe can override) |
onEnter / onExit | Lifecycle callbacks when a track starts or stops |
You can also fire discrete events from within a track using the event
property on a keyframe — useful for syncing sound or VFX to animation beats.
Lower-Level Primitives
Section titled “Lower-Level Primitives”Under the hood, KeyframeAnimator is built on two primitives you can use
directly when you don’t need the named-track machinery:
import { createKeyframeTrack, interpolate } from "@yagejs/core";
// A one-off keyframe track as a Processpc.run( createKeyframeTrack({ keyframes: [ { time: 0, data: 0 }, { time: 600, data: 100 }, ], setter: (v) => (entity.get(Transform).x = v as number), }),);
// Raw interpolation for bespoke driver codeconst blended = interpolate(0, 100, 0.5, easeOutQuad); // ≈ 75Interpolatable — the type parameter for keyframe data and the
interpolate primitive — resolves to number | Vec2Like. Both are supported
out of the box; if you need to animate other types (colour, quaternion),
compose multiple number tracks or write a custom setter.