Skip to content

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.

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,
});

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 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 property
pc.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 callback
pc.run(
Tween.custom(500, easeOutQuad, (t) => {
entity.transform.setScale(1 + t * 0.5, 1 + t * 0.5);
}),
);
FunctionCurve
easeLinearConstant speed
easeInQuadAccelerate
easeOutQuadDecelerate
easeInOutQuadAccelerate then decelerate
easeOutBounceBounce at the end

Import any easing from @yagejs/core.

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(),
);

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.

// Loop forever
Sequence.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 times
Sequence.create()
.call(() => flash())
.wait(100)
.repeat(5)
.build();

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);
}));

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).

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.

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:

OptionPurpose
keyframesThe array of { time, data, easing?, event? } control points
setterFunction called every frame with the interpolated value
loopRestart at time 0 when the track finishes (default false)
speedMultiplier on track time (default 1)
durationOverride the auto-computed track length
easingDefault easing between keyframes (each keyframe can override)
onEnter / onExitLifecycle 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.

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 Process
pc.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 code
const blended = interpolate(0, 100, 0.5, easeOutQuad); // ≈ 75

Interpolatable — 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.