Talos docs

Animator

Since 0.6

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; unknown states are a contract error. */
  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
  /**
   * 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
  /**
   * 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. 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` below 0 or 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; unknown states are a contract error.
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.
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).
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. 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 below 0 or 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