Skip to content

Animation timeline

The Animation window (open it from the Windows menu) is a Unity-style keyframe timeline. Select an object, keyframe its transform over time, scrub to preview, loop-play, and save the result as a reusable Animation Clip asset that plays back in the editor, in standalone builds, and in multiplayer.

Each clip is a set of tracks — one per animated property of the object (or of a named child / bone):

PropertyWhat it animates
position / rotation / scalethe transform (rotation is authored in Euler degrees)
opacitythe material's transparency (fade in/out)
colorthe material's colour (pulse / flash)

Add a track with + Track ▾, then key the current value at the playhead with , double-click a lane to drop a key, or turn on ⏺ Record to auto-key every time you pose the object with the gizmo. Drag a keyframe to retime it (hold Alt for off-grid). Everything is undoable.

Interpolation

Each track has a default interpolation, chosen from the little select on its row:

  • Lin (linear) — constant-rate blend between keys.
  • Step — hold each key's value, then jump at the next key.
  • Smth (smooth) — eased blend.

Curve editor (per-key easing)

Switch the timeline to the Curves view (the Dope / Curves toggle) for a value-over-time graph with a per-key cubic-bezier easing editor — the same idea as a CSS cubic-bezier() timing function or an After-Effects speed graph.

  • Each channel of the selected track is drawn as a curve; keyframes are dots you can drag in 2D (left–right to retime, up–down to change the value).
  • Select a key to reveal the two bezier handles on its out-segment (the span from this key to the next). Drag a handle to shape how the value accelerates and settles. The horizontal reach is clamped to the segment; the vertical reach is free, so you can pull a handle past the endpoints for overshoot / anticipation.
  • The quick ease buttons in the footer (Lin / In / Out / In-Out) set common presets for the selected key. Lin clears the easing (back to the track's plain interpolation).

Easing is stored per key and governs the segment that starts at that key, so different segments of one track can ease differently. It applies to every channel of the key together (position X/Y/Z share one curve), and it works for rotation too — the easing reshapes the slerp between orientations.

Notes:

  • Easing has no effect on a Step track (step holds, there is nothing to shape) or on the last key of a track (it has no following segment).
  • A saved clip bakes each eased segment into dense samples at export, so eased motion plays back identically in every runtime — editor, standalone, and multiplayer. (A smooth-interpolation track that also carries easing is baked as linear between its non-eased keys.)

Animation events

Below the keyframe tracks is the ⚑ Events lane — Unity-style animation events that call a script method when the playhead reaches a given time. Use them for footsteps on a walk cycle, a hit frame on an attack, spawning a VFX partway through a clip, and so on.

  • Add — double-click the Events lane at a time, or press to drop one at the playhead.
  • Retime — drag a flag marker (hold Alt for off-grid).
  • Edit — click a marker to select it, then set its time, fn (the method to call), and an optional arg in the footer. The arg is parsed as a number when it looks numeric, otherwise kept as a string.

When the clip's playhead crosses an event, the playing entity's scripts receive it as onMessage(fn, arg):

ts
class Footsteps extends Behaviour {
  onMessage(fn: string, arg?: unknown) {
    if (fn === 'onFootstep') audio.play('step', { volume: arg === 'heavy' ? 1 : 0.6 })
  }
}

Events authored here live on the clip, so they fire for any entity that plays it. (An entity's animator component can also carry its own events for a one-off — those fire on top of the clip's.) Looped clips fire their events every loop.

Events fire in every runtime — the editor play-test, standalone builds, and multiplayer. In a published room the scripts run on the authoritative server, which fires the events itself so every player stays in sync. One current limit: events fire for auto-playing clips; a clip started on demand from a script (animation.play(...)) does not yet fire its events in multiplayer.