Skip to content

Timers and tweens ​

Both are in CorePlugins, and both count fixed steps, so they are deterministic and behave the same headless and in the browser.

Timers ​

ts
commands.spawn(Timer({ duration: 2 }), IsBomb)                   // one-shot
commands.spawn(Timer({ duration: 0.5, repeat: true }), IsSpawner) // repeating
commands.spawn(Transform, Bullet, Lifetime({ seconds: 1.5 }))     // despawns itself

/** Bombs go off when their timer runs out. */
export const Explode = system([Timer, IsBomb], ([t], { commands, entity }) => {
  if (t.justFinished) commands.despawn(entity)
})

Timer fields:

FieldSet or read
durationsetSeconds
repeatsetRestart on completion
pausedsetHold the timer
ticks, elapsed, remainingreadProgress, in steps and seconds
finishedreadStays true once a one-shot completes
justFinishedreadTrue for exactly one fixed step per completion
countreadCompletions so far

justFinished is a Fixed-step edge. Update systems should read the TimerFinished event ({ entity, count }) instead, which every reader sees once.

To restart a timer, take Mut(Timer) and call restartTimer(row). Any system that takes Mut(Timer) must be ordered after the plugin's TickTimers:

ts
.add(Fixed, Rearm.after(TickTimers))

Lifetime({ seconds }) despawns the entity that many seconds after it spawned. Bullets, effects and popups are the usual users.

Delayed events ​

ts
res.mut(Delayed).emit(3, RoundStart, { round: 2 })   // emitted in Fixed, 3 s of steps from now
res.mut(Delayed).cancel(RoundStart)                  // drop pending ones (or all, with no argument)
res.get(Delayed).timeLeft(RoundStart)                // seconds until the next one, or null

Pending delayed events survive snapshots when the event is a named export.

Tweens ​

ts
const tw = res.mut(Tweens)
tw.to(entity, Transform, { y: 2, sx: 1.5 }, { duration: 0.4, ease: 'outBack' })
tw.to(pending, Transform, { sx: 1, sy: 1, sz: 1 }, { from: { sx: 0, sy: 0, sz: 0 }, delay: 0.1 })  // right after commands.spawn
tw.to(e, Transform, { y: 1 }, { duration: 0.3, yoyo: true, repeat: -1 })   // bob forever
tw.to(e, Transform, { sx: 1.3 }, { duration: 0.1 }).then(e, Transform, { sx: 0 }, { duration: 0.2, despawn: true })
tw.to(res.mut(Fade), { alpha: 1 })                                          // plain objects (resources) too

Any trait's number fields work, on entities and on PendingEntity values from commands.spawn. Tweens tick in Fixed; rendering interpolates Transform between steps, so they still look smooth.

OptionDefault
duration0.3Seconds per run
ease'outQuad'An ease name or a function of 0..1
delay0Seconds before it starts; start values are read when it starts
repeat0Extra runs; -1 forever
yoyofalseAlternate direction on each repeat
fromStart values, written at once (even during the delay)
tag''Carried by TweenDone so readers can tell tweens apart
despawnfalseDespawn the entity when the tween (and its .then chain) finishes

Eases: linear, and in, out or inOut with Quad Cubic Quart Sine Expo Back Elastic Bounce ('inOutSine', 'outBounce', ...). The functions are exported too: ease.outBack(t).

A newer tween on the same fields takes them over. tw.cancel(target, trait?) stops tweens and leaves values where they are; tw.isTweening(target, trait?) checks. TweenDone ({ id, tag, entity }) fires when a tween finishes all its repeats.