Log inGet started

animation

Updated October 3, 2026

Observe what the engine is animating: every body it is posing, the clips driving each one and where their playheads are, how many of a body's bones a clip's retarget reached, and — for a body that is not moving — the one reason why, from a closed set.

require("@builtin::modules.api.engine.animation") -- animation (also available as global 'animation')

The animation namespace: 29 functions.

globals/animation/animating

animation.animating() -> { AnimationBody }

The bodies the engine measured a changing pose on — what is animating right now.

Returns { AnimationBody } — An array of AnimationBody.

for _, b in animation.animating() do print(b.entity, b.clips[1] and b.clips[1].name) end

globals/animation/bodies

animation.bodies() -> { AnimationBody }

Every body the engine holds animation state for.

Returns { AnimationBody } — An array of AnimationBody.

for _, b in animation.bodies() do print(b.entity, b.matched .. "/" .. b.total) end

globals/animation/body

animation.body(entityId: string | EntityRef) -> AnimationBody?

The report for one body, or nil when the engine holds no animation state for it. Accepts the body itself or any ancestor of it, so a character root answers for the skinned body underneath it.

Parameters

  • entityId string | EntityRef — The entity's stable id, or an EntityRef.

Returns AnimationBody? — An AnimationBody, or nil.

local b = animation.body(hero.id); print(b and b.reason)

globals/animation/clips

animation.clips(entityId: string | EntityRef) -> { AnimationClip }

The clips contributing to a body's pose right now, with their playheads and their retarget coverage.

Parameters

  • entityId string | EntityRef — The entity's stable id, or an EntityRef.

Returns { AnimationClip } — An array of AnimationClip.

for _, c in animation.clips(hero.id) do print(c.name, c.time, c.matched) end

globals/animation/coverage

animation.coverage(entityId: string | EntityRef) -> (number, number)

How many of a body's bones the clips driving it actually reach. Returns (matched, total). A clip that retargets onto nothing reads (0, 50) while its playhead advances; a partial retarget reads its own count, so 3 of 50 is as visible as none.

Parameters

  • entityId string | EntityRef — The entity's stable id, or an EntityRef.

Returns (number, number) — (matched, total).

local m, t = animation.coverage(hero.id); print(m .. "/" .. t)

globals/animation/declare

animation.declare(entityId: string, facts: { [string]: any })

Publish what an animator is running on a body, so the observation names its clips, playheads and retarget coverage beside the pose the engine measures. The shipped animators declare through AnimGraph:publish; a custom animator calls this itself, once per frame it runs.

Parameters

  • entityId string — The body the animator drives.
  • facts { [string]: any } — { driver, bound, playing, outputKind, failure, clips }, where each clip is { name, nodeKind, time, duration, playing, finished, looping, weight, matched, total, unmatched }.
animation.declare(body.id, { driver = "MyAnimator", playing = true, clips = {} })

globals/animation/forget

animation.forget(entityId: string)

Drop the declaration and the pose evidence the engine holds for one body. An animator calls this when it releases a body, so the observation reports the body as undriven from the next frame.

Parameters

  • entityId string — The body to drop.
animation.forget(body.id)

globals/animation/observe

animation.observe() -> AnimationObservation

Report what the engine is posing right now and why a body is not moving. One read covering every body the engine holds animation state for, each with the pose evidence the engine measured on its armature beside the clips the animator driving it declared. Answers in edit mode as well as play mode.

Returns AnimationObservation — An AnimationObservation.

local a = animation.observe(); print(a.animatingCount, a.riggedBodyCount)
for _, b in animation.observe().bodies do print(b.entity, b.animating, b.reason) end

globals/animation/whyStill

animation.whyStill(entityId: string | EntityRef) -> (string?, string?)

Why the body on an entity is not animating. Returns nil when it IS animating, and otherwise one of deactivated, noRiggedSkeleton, noGraph, clipUnreadable, noOutputNode, retargetPending, retargetMatchedNoRoles, stopped, finished, paused, poseNotApplied, poseUnchanged — the nearest cause, so the answer names the thing to change. A second return carries the animator's own words when it could not build a graph.

An entity the engine holds no animation state for is answered from the entity itself, in the same order the engine resolves a body it does hold: one carrying no rigged Skeleton is noRiggedSkeleton, and a rigged one nothing drives is noGraph. An id no entity carries is neither — the reason is nil and the detail says so.

Parameters

  • entityId string | EntityRef — The entity's stable id, or an EntityRef.

Returns (string?, string?) — (reason, detail).

local why, detail = animation.whyStill(hero.id); if why then print(why, detail) end

modules/animation/animating

animating(): { AnimationBody }

The bodies the engine measured a changing pose on — what is animating right now.

for _, b in animation.animating() do print(b.entity, b.clips[1] and b.clips[1].name) end

modules/animation/bodies

bodies(): { AnimationBody }

Every body the engine holds animation state for.

for _, b in animation.bodies() do print(b.entity, b.matched .. "/" .. b.total) end

modules/animation/body

body(entityId: string | EntityRef): AnimationBody?

The report for one body, or nil when the engine holds no animation state for it. Accepts the body itself or any ancestor of it, so a character root answers for the skinned body underneath it.

Parameters

  • entityId string | EntityRef — The entity's stable id, or an EntityRef.
local b = animation.body(hero.id); print(b and b.reason)

modules/animation/clips

clips(entityId: string | EntityRef): { AnimationClip }

The clips contributing to a body's pose right now, with their playheads and their retarget coverage.

Parameters

  • entityId string | EntityRef — The entity's stable id, or an EntityRef.
for _, c in animation.clips(hero.id) do print(c.name, c.time, c.matched) end

modules/animation/coverage

coverage(entityId: string | EntityRef): (number, number)

How many of a body's bones the clips driving it actually reach. Returns (matched, total). A clip that retargets onto nothing reads (0, 50) while its playhead advances; a partial retarget reads its own count, so 3 of 50 is as visible as none.

Parameters

  • entityId string | EntityRef — The entity's stable id, or an EntityRef.
local m, t = animation.coverage(hero.id); print(m .. "/" .. t)

modules/animation/declare

declare(entityId: string, facts: { [string]: any })

Publish what an animator is running on a body, so the observation names its clips, playheads and retarget coverage beside the pose the engine measures. The shipped animators declare through AnimGraph:publish; a custom animator calls this itself, once per frame it runs.

Parameters

  • entityId string — The body the animator drives.
  • facts { [string]: any } — { driver, bound, playing, outputKind, failure, clips }, where each clip is { name, nodeKind, time, duration, playing, finished, looping, weight, matched, total, unmatched }.
animation.declare(body.id, { driver = "MyAnimator", playing = true, clips = {} })

modules/animation/forget

forget(entityId: string)

Drop the declaration and the pose evidence the engine holds for one body. An animator calls this when it releases a body, so the observation reports the body as undriven from the next frame.

Parameters

  • entityId string — The body to drop.
animation.forget(body.id)

modules/animation/observe

observe(): AnimationObservation

Report what the engine is posing right now and why a body is not moving. One read covering every body the engine holds animation state for, each with the pose evidence the engine measured on its armature beside the clips the animator driving it declared. Answers in edit mode as well as play mode.

local a = animation.observe(); print(a.animatingCount, a.riggedBodyCount)
for _, b in animation.observe().bodies do print(b.entity, b.animating, b.reason) end

modules/animation/whyStill

whyStill(entityId: string | EntityRef): (string?, string?)

Why the body on an entity is not animating. Returns nil when it IS animating, and otherwise one of deactivated, noRiggedSkeleton, noGraph, clipUnreadable, noOutputNode, retargetPending, retargetMatchedNoRoles, stopped, finished, paused, poseNotApplied, poseUnchanged — the nearest cause, so the answer names the thing to change. A second return carries the animator's own words when it could not build a graph.

An entity the engine holds no animation state for is answered from the entity itself, in the same order the engine resolves a body it does hold: one carrying no rigged Skeleton is noRiggedSkeleton, and a rigged one nothing drives is noGraph. An id no entity carries is neither — the reason is nil and the detail says so.

Parameters

  • entityId string | EntityRef — The entity's stable id, or an EntityRef.
local why, detail = animation.whyStill(hero.id); if why then print(why, detail) end

tools/animation/observe

animation.observe(target?: (string | EntityRef)) -> AnimObserveResult

What the engine is posing right now, one line per body: whether its pose is actually changing, the clip driving it with its playhead, how many of its bones that clip reaches, and — when it is not moving — the one reason why. Call it with no arguments for every body in the scene, or pass an entity to narrow it to that one (a character root resolves down to the skinned body under it).

Parameters

  • target (string | EntityRef) (optional)

Returns AnimObserveResult

"hero"

tools/animation/play

animation.play(animation: string | AssetRef<animation>, entityArg: string | EntityRef, opts?: AnimPlayOpts) -> AnimPlayResult

Play an animation clip on an entity's skinned body for a test window, then clean up. Resolves animation by identity or name and entity by id or name, finds the SkinnedModel to drive, and plays it through the canonical AnimGraph — the same path ClipPlayer uses. Returns { ok = false, error } when the clip, the entity, or a SkinnedModel under it cannot be found. When the body already runs an animation the clip overrides it for the window and the original resumes untouched on cleanup.

Parameters

  • animation string | AssetRef<animation>
  • entityArg string | EntityRef
  • opts AnimPlayOpts (optional)

Returns AnimPlayResult

"A_Walk_F_Masc", "hero"
"Idle", "hero", { offset = "5s", loop = true, duration = 8 }

typed/builtin//modules/api/engine/animation/animation/animating

animation.animating() -> { AnimationBody }

The bodies the engine measured a changing pose on — what is animating right now.

Returns { AnimationBody } — An array of AnimationBody.

for _, b in animation.animating() do print(b.entity, b.clips[1] and b.clips[1].name) end

typed/builtin//modules/api/engine/animation/animation/bodies

animation.bodies() -> { AnimationBody }

Every body the engine holds animation state for.

Returns { AnimationBody } — An array of AnimationBody.

for _, b in animation.bodies() do print(b.entity, b.matched .. "/" .. b.total) end

typed/builtin//modules/api/engine/animation/animation/body

animation.body(entityId: string | EntityRef) -> AnimationBody?

The report for one body, or nil when the engine holds no animation state for it. Accepts the body itself or any ancestor of it, so a character root answers for the skinned body underneath it.

Parameters

  • entityId string | EntityRef — The entity's stable id, or an EntityRef.

Returns AnimationBody? — An AnimationBody, or nil.

local b = animation.body(hero.id); print(b and b.reason)

typed/builtin//modules/api/engine/animation/animation/clips

animation.clips(entityId: string | EntityRef) -> { AnimationClip }

The clips contributing to a body's pose right now, with their playheads and their retarget coverage.

Parameters

  • entityId string | EntityRef — The entity's stable id, or an EntityRef.

Returns { AnimationClip } — An array of AnimationClip.

for _, c in animation.clips(hero.id) do print(c.name, c.time, c.matched) end

typed/builtin//modules/api/engine/animation/animation/coverage

animation.coverage(entityId: string | EntityRef) -> (number, number)

How many of a body's bones the clips driving it actually reach. Returns (matched, total). A clip that retargets onto nothing reads (0, 50) while its playhead advances; a partial retarget reads its own count, so 3 of 50 is as visible as none.

Parameters

  • entityId string | EntityRef — The entity's stable id, or an EntityRef.

Returns (number, number) — (matched, total).

local m, t = animation.coverage(hero.id); print(m .. "/" .. t)

typed/builtin//modules/api/engine/animation/animation/declare

animation.declare(entityId: string, facts: { [string]: any })

Publish what an animator is running on a body, so the observation names its clips, playheads and retarget coverage beside the pose the engine measures. The shipped animators declare through AnimGraph:publish; a custom animator calls this itself, once per frame it runs.

Parameters

  • entityId string — The body the animator drives.
  • facts { [string]: any } — { driver, bound, playing, outputKind, failure, clips }, where each clip is { name, nodeKind, time, duration, playing, finished, looping, weight, matched, total, unmatched }.
animation.declare(body.id, { driver = "MyAnimator", playing = true, clips = {} })

typed/builtin//modules/api/engine/animation/animation/forget

animation.forget(entityId: string)

Drop the declaration and the pose evidence the engine holds for one body. An animator calls this when it releases a body, so the observation reports the body as undriven from the next frame.

Parameters

  • entityId string — The body to drop.
animation.forget(body.id)

typed/builtin//modules/api/engine/animation/animation/observe

animation.observe() -> AnimationObservation

Report what the engine is posing right now and why a body is not moving. One read covering every body the engine holds animation state for, each with the pose evidence the engine measured on its armature beside the clips the animator driving it declared. Answers in edit mode as well as play mode.

Returns AnimationObservation — An AnimationObservation.

local a = animation.observe(); print(a.animatingCount, a.riggedBodyCount)
for _, b in animation.observe().bodies do print(b.entity, b.animating, b.reason) end

typed/builtin//modules/api/engine/animation/animation/whyStill

animation.whyStill(entityId: string | EntityRef) -> (string?, string?)

Why the body on an entity is not animating. Returns nil when it IS animating, and otherwise one of deactivated, noRiggedSkeleton, noGraph, clipUnreadable, noOutputNode, retargetPending, retargetMatchedNoRoles, stopped, finished, paused, poseNotApplied, poseUnchanged — the nearest cause, so the answer names the thing to change. A second return carries the animator's own words when it could not build a graph.

An entity the engine holds no animation state for is answered from the entity itself, in the same order the engine resolves a body it does hold: one carrying no rigged Skeleton is noRiggedSkeleton, and a rigged one nothing drives is noGraph. An id no entity carries is neither — the reason is nil and the detail says so.

Parameters

  • entityId string | EntityRef — The entity's stable id, or an EntityRef.

Returns (string?, string?) — (reason, detail).

local why, detail = animation.whyStill(hero.id); if why then print(why, detail) end
  • api
  • reference