Talos docs

Animator

Since 0.6Changed in 0.7

Present on ctx.animator when the entity has an animator component.

Plain text

Reached in a behaviour as ctx.animator.

Signature

export interface Animator {
  /**
   * Switch to a state defined in the component, over the blend of the transition that reaches
   * it. A name the animator does not have changes nothing and is said once as an
   * `animator-unknown` warning, counted on `GET /feel` as `animator_misses`. Setting the state
   * playing now does nothing (`play` restarts one).
   */
  set(state: string): void
  readonly state: string | null
  /**
   * Writes a blend-tree parameter — `ctx.animator.param("speed", 3.2)`.
   *
   * Every parameter a blend tree reads is declared in the component's `parameters`, so the
   * names that work are written down in the scene. A parameter the component does not
   * declare is refused: it throws an `Error` whose `name` is `AnimatorParamError` and whose
   * message opens with `animator-param-unknown`. A typo would otherwise leave the blend
   * frozen with nothing anywhere saying why.
   *
   * A parameter with a `source` is written by the engine every tick from the character
   * controller, so writing it here is overwritten on the next tick. Leave `source` off the
   * ones a behavior drives.
   *
   * A state carrying `rootMotion` must not read a parameter bound to `speed`, `strafeX` or
   * `strafeY`: root motion writes the velocity those report, so the tree would be reading
   * its own output and the character would never leave its first clip. Bind it to
   * `inputSpeed`, `inputStrafeX` or `inputStrafeY` — what the controller was asked for — or
   * write it here. The contract reports the combination as `animator-root-motion-source`.
   */
  param(name: string, value: number): void
  /**
   * How far root motion moved this entity, in world space and in metres.
   *
   * "On the last tick" inside `update`, where the animator has not run yet; this tick's
   * inside an `onEvent` handler, because animation events are delivered after the animator
   * pass that raised them. One tick apart, never wrong, only differently old.
   *
   * Zero unless the playing state sets `rootMotion: true`. A character with a `controller`
   * component has already been moved by it — the engine hands the delta to the controller,
   * which resolves it against the world — so reading it there is for inspection. An entity
   * with no controller has been moved by it too, on its transform. A behavior that wants to
   * own the movement takes this and applies it itself.
   */
  rootDelta(): Vec3
  /**
   * Whether an animation-event window called `name` is open: a window is an `events` entry
   * with `until`, open while its clip is the animator's event source and the playhead is in
   * `[at, until)`. The same state the `begin` and `end` events report, as of the last
   * animator pass — this tick's inside an `onEvent` handler, the previous tick's inside
   * `update`. `false` for a name no window has. Every animator layer is an event source of
   * its own, so this is true while the window is open on the base or on any layer; a layer's
   * events carry `layer` in their data.
   */
  window(name: string): boolean
  /**
   * A bone's world position and rotation as of the last animator pass (inside `update`, the
   * previous tick's), or `null` for a bone the rig does not have. A retargeted rig also
   * answers to the canonical humanoid's names.
   *
   * `pose` "final" (the default) is the pose drawn: after look-at, the pelvis drop and IK.
   * "animated" is the clips' and layers' pose before those constraints ran, which is what an
   * IK target must be measured from: a foot target taken from the final pose of the same foot
   * chases its own result.
   */
  bone(name: string, pose?: "final" | "animated"): BonePose | null
  /**
   * Points an IK chain's end bone at a world-space point, or releases it with `null`.
   *
   * The chain is declared in the component's `ik`, so the names that work are written down
   * in the scene; one it does not declare throws an `Error` whose `name` is
   * `AnimatorIkError`. A released chain returns to the pose the clips produced.
   *
   * The engine owns the solve and the game owns the target: where a foot should go is a game
   * question, because a stair tread and a puddle are the same collision geometry. Call it
   * every tick a foot is planted — the target is remembered, so a character standing still
   * costs one call and no bookkeeping.
   *
   * `weight` is the chain's blend, 0..1, applied whether or not the target moved. **Nothing
   * ramps it for you**: 0 to 1 in one tick snaps the foot, and the engine cannot tell a
   * footfall from a stance change, so the caller ramps it over a handful of ticks.
   *
   * The target may be an entity handle as well as a point, in which case its position is
   * used.
   */
  ik(chain: string, target: Vec3 | EntityHandle | null, weight?: number): void
  /**
   * Aims the look-at chain at a world-space point, or releases it with `null`.
   *
   * Throws `AnimatorLookAtError` when the component declares no `lookAt`. Releasing eases the
   * head back to whatever the clip is doing over the constraint's own `smoothing`, rather
   * than dropping the aim in one frame.
   */
  lookAt(target: Vec3 | EntityHandle | null): void
  /**
   * Plays a state, or a clip of the animator's asset that no state plays, **once** — and then
   * hands over by itself: to `opts.then`, or back to the state that was playing before.
   *
   * `ctx.animator.play("reload", { interrupt: ["die"] })` is a reload that nothing but death
   * cuts short; `ctx.animator.play("Wave")` is an emote straight from a clip name, with no
   * state authored for it; a door's `play("open", { then: "opened" })` swings once and rests.
   * A state already declared with `once` in the scene keeps its declared behaviour and the
   * options override it field by field.
   *
   * The hand-over starts `blendOut` seconds before the clip's last frame, on the tick the clip
   * gets there — deterministic, a function of the clip and the tick rate only. When it ends —
   * run out or cut short — the bus event `opts.event` (default `"animation-end"`) is raised
   * with {@link AnimationEndEvent} as its data. Calling `play` again with the one-shot that is
   * playing starts it over, which a `set` of the playing state never does.
   *
   * Returns `false`, changing nothing, when the one-shot playing now does not let this one cut
   * in (its `interrupt` list leaves it out). Throws an `Error` whose `name` is
   * `AnimatorPlayError` for a name that is neither a state nor a clip, a `then` that is not a
   * state (message opening `animator-play-unknown`), or a negative blend time
   * (`animator-play-invalid`).
   */
  play(name: string, opts?: AnimatorPlayOptions): boolean
  /**
   * Motion warping: from now until clip time `opts.by` (seconds into the clip as authored) the
   * playing state's ground root-motion travel is re-aimed and scaled so the character's root
   * arrives at `opts.to` (x and z) when the clip reaches `by`. The scale is clamped to
   * `[0, opts.maxScale]` (default 2.5); vertical and yaw root motion, and all travel after `by`,
   * are the clip's own, and the controller's collision still applies. A state change clears it,
   * and so does `warp(null)`.
   *
   * Returns `false`, changing nothing, when the playing state has no root motion to warp (no
   * `rootMotion`, a root that does not travel, a blend tree) or the clip is already past `by`.
   * Throws `TypeError` for a malformed `opts`.
   */
  warp(opts: AnimatorWarpOptions | null): boolean
  /**
   * Reads or sets the weight of an animator layer declared in the component's `layers`:
   * `layerWeight("upper")` is the weight it has, `layerWeight("upper", 0.5)` sets it (0..1,
   * clamped) and returns it. The layer's own fade in and out and a `weightParam` multiply on top.
   * Throws an `Error` whose `name` is `AnimatorLayerError` (message opening
   * `animator-layer-unknown`) for a layer the animator does not declare.
   */
  layerWeight(layer: string, weight?: number): number
  /**
   * Holds the model's morph channel `name` (one of its asset's `morphs`: a face shape such as
   * `jawOpen`, or any morph target) at `weight` over whatever the animator's clips and layers give
   * it, or releases it with `null`, after which it follows the animation again at once. Returns
   * the weight it holds (for a release, the one drawn last frame). 0 is the neutral shape and 1 the
   * full one. Throws an `Error` whose `name` is `AnimatorMorphError` (message opening
   * `animator-morph-unknown`, listing the model's channels) for a channel the model does not have,
   * or a weight that is not a number.
   */
  setMorph(name: string, weight: number | null): number
  /**
   * The weight the model's morph channel `name` was drawn at last frame. Throws
   * `AnimatorMorphError` as {@link setMorph} does.
   */
  morph(name: string): number
  /**
   * Releases an animator layer: what plays on it keeps playing while the layer fades out over its
   * `blendOut`, then the layer is empty. Throws `AnimatorLayerError` for an undeclared layer.
   */
  stopLayer(layer: string): void
  /**
   * The animator-wide playback multiplier: 1 (the default) plays every state at its own rate,
   * 2 doubles it, 0 freezes the pose where it is, a negative one plays backwards (a forward walk
   * clip as a back-step). It multiplies each state's `speed` and
   * `speedParam` from the scene, so it is the knob for one character's slow motion or a
   * hit-stop, not for pacing a state — declare `speed` on the state for that. Throws
   * `RangeError` when not finite. The effective rate, every multiplier in, is
   * `anim.speed` on `GET /entities`.
   */
  speed: number
}

Import: import type { Animator } from "@argus/api"

Members

MemberSignatureDescription
setset(state: string): voidSwitch to a state defined in the component, over the blend of the transition that reaches it. A name the animator does not have changes nothing and is said once as an animator-unknown warning, counted on GET /feel as animator_misses. Setting the state playing now does nothing (play restarts one).
statereadonly state: string | null
paramparam(name: string, value: number): voidWrites a blend-tree parameter — ctx.animator.param("speed", 3.2). Every parameter a blend tree reads is declared in the component's parameters, so the names that work are written down in the scene. A parameter the component does not declare is refused: it throws an Error whose name is AnimatorParamError and whose message opens with animator-param-unknown. A typo would otherwise leave the blend frozen with nothing anywhere saying why. A parameter with a source is written by the engine every tick from the character controller, so writing it here is overwritten on the next tick. Leave source off the ones a behavior drives. A state carrying rootMotion must not read a parameter bound to speed, strafeX or strafeY: root motion writes the velocity those report, so the tree would be reading its own output and the character would never leave its first clip. Bind it to inputSpeed, inputStrafeX or inputStrafeY — what the controller was asked for — or write it here. The contract reports the combination as animator-root-motion-source.
rootDeltarootDelta(): Vec3How far root motion moved this entity, in world space and in metres. "On the last tick" inside update, where the animator has not run yet; this tick's inside an onEvent handler, because animation events are delivered after the animator pass that raised them. One tick apart, never wrong, only differently old. Zero unless the playing state sets rootMotion: true. A character with a controller component has already been moved by it — the engine hands the delta to the controller, which resolves it against the world — so reading it there is for inspection. An entity with no controller has been moved by it too, on its transform. A behavior that wants to own the movement takes this and applies it itself.
windowwindow(name: string): booleanWhether an animation-event window called name is open: a window is an events entry with until, open while its clip is the animator's event source and the playhead is in [at, until). The same state the begin and end events report, as of the last animator pass — this tick's inside an onEvent handler, the previous tick's inside update. false for a name no window has. Every animator layer is an event source of its own, so this is true while the window is open on the base or on any layer; a layer's events carry layer in their data.
bonebone(name: string, pose?: "final" | "animated"): BonePose | nullA bone's world position and rotation as of the last animator pass (inside update, the previous tick's), or null for a bone the rig does not have. A retargeted rig also answers to the canonical humanoid's names. pose "final" (the default) is the pose drawn: after look-at, the pelvis drop and IK. "animated" is the clips' and layers' pose before those constraints ran, which is what an IK target must be measured from: a foot target taken from the final pose of the same foot chases its own result.
ikik(chain: string, target: Vec3 | EntityHandle | null, weight?: number): voidPoints an IK chain's end bone at a world-space point, or releases it with null. The chain is declared in the component's ik, so the names that work are written down in the scene; one it does not declare throws an Error whose name is AnimatorIkError. A released chain returns to the pose the clips produced. The engine owns the solve and the game owns the target: where a foot should go is a game question, because a stair tread and a puddle are the same collision geometry. Call it every tick a foot is planted — the target is remembered, so a character standing still costs one call and no bookkeeping. weight is the chain's blend, 0..1, applied whether or not the target moved. Nothing ramps it for you: 0 to 1 in one tick snaps the foot, and the engine cannot tell a footfall from a stance change, so the caller ramps it over a handful of ticks. The target may be an entity handle as well as a point, in which case its position is used.
lookAtlookAt(target: Vec3 | EntityHandle | null): voidAims the look-at chain at a world-space point, or releases it with null. Throws AnimatorLookAtError when the component declares no lookAt. Releasing eases the head back to whatever the clip is doing over the constraint's own smoothing, rather than dropping the aim in one frame.
playplay(name: string, opts?: AnimatorPlayOptions): booleanPlays a state, or a clip of the animator's asset that no state plays, once — and then hands over by itself: to opts.then, or back to the state that was playing before. ctx.animator.play("reload", { interrupt: ["die"] }) is a reload that nothing but death cuts short; ctx.animator.play("Wave") is an emote straight from a clip name, with no state authored for it; a door's play("open", { then: "opened" }) swings once and rests. A state already declared with once in the scene keeps its declared behaviour and the options override it field by field. The hand-over starts blendOut seconds before the clip's last frame, on the tick the clip gets there — deterministic, a function of the clip and the tick rate only. When it ends — run out or cut short — the bus event opts.event (default "animation-end") is raised with {@link AnimationEndEvent} as its data. Calling play again with the one-shot that is playing starts it over, which a set of the playing state never does. Returns false, changing nothing, when the one-shot playing now does not let this one cut in (its interrupt list leaves it out). Throws an Error whose name is AnimatorPlayError for a name that is neither a state nor a clip, a then that is not a state (message opening animator-play-unknown), or a negative blend time (animator-play-invalid).
warpwarp(opts: AnimatorWarpOptions | null): booleanMotion warping: from now until clip time opts.by (seconds into the clip as authored) the playing state's ground root-motion travel is re-aimed and scaled so the character's root arrives at opts.to (x and z) when the clip reaches by. The scale is clamped to [0, opts.maxScale] (default 2.5); vertical and yaw root motion, and all travel after by, are the clip's own, and the controller's collision still applies. A state change clears it, and so does warp(null). Returns false, changing nothing, when the playing state has no root motion to warp (no rootMotion, a root that does not travel, a blend tree) or the clip is already past by. Throws TypeError for a malformed opts.
layerWeightlayerWeight(layer: string, weight?: number): numberReads or sets the weight of an animator layer declared in the component's layers: layerWeight("upper") is the weight it has, layerWeight("upper", 0.5) sets it (0..1, clamped) and returns it. The layer's own fade in and out and a weightParam multiply on top. Throws an Error whose name is AnimatorLayerError (message opening animator-layer-unknown) for a layer the animator does not declare.
setMorphsetMorph(name: string, weight: number | null): numberHolds the model's morph channel name (one of its asset's morphs: a face shape such as jawOpen, or any morph target) at weight over whatever the animator's clips and layers give it, or releases it with null, after which it follows the animation again at once. Returns the weight it holds (for a release, the one drawn last frame). 0 is the neutral shape and 1 the full one. Throws an Error whose name is AnimatorMorphError (message opening animator-morph-unknown, listing the model's channels) for a channel the model does not have, or a weight that is not a number.
morphmorph(name: string): numberThe weight the model's morph channel name was drawn at last frame. Throws AnimatorMorphError as {@link setMorph} does.
stopLayerstopLayer(layer: string): voidReleases an animator layer: what plays on it keeps playing while the layer fades out over its blendOut, then the layer is empty. Throws AnimatorLayerError for an undeclared layer.
speedspeed: numberThe animator-wide playback multiplier: 1 (the default) plays every state at its own rate, 2 doubles it, 0 freezes the pose where it is, a negative one plays backwards (a forward walk clip as a back-step). It multiplies each state's speed and speedParam from the scene, so it is the knob for one character's slow motion or a hit-stop, not for pacing a state — declare speed on the state for that. Throws RangeError when not finite. The effective rate, every multiplier in, is anim.speed on GET /entities.

Example

The tested fixture fixtures/app/agent.ts.

import type { Behavior } from "@argus/api"

// Sends a nav agent to `props.target` once, then reports arrival through ctx.state so the
// test can read it without polling the crowd directly.
export default {
  init(ctx) {
    ctx.state.arrived = false
    ctx.nav?.goTo(ctx.props.target as [number, number, number])
  },
  update(ctx) {
    if (ctx.nav?.arrived) ctx.state.arrived = true
  },
} satisfies Behavior