Talos docs

Controller

Since 0.6

Present on ctx.controller when the entity has a character-controller component (kinematic capsule).

Plain text

Order within a tick. The controller steps once per tick, after physics and before behaviours, so everything a behaviour writes here — move, jump, setVelocity, addVelocity, gravityScale — takes effect on the controller's step at the top of the next tick, and everything it reads describes the step that just ran. At that step, in order: moving ground carries the character, an upward velocity write lifts it off the ground, move accelerates the horizontal velocity towards its target, a jump press sets the vertical speed, gravity is applied, and the capsule is swept. Velocity writes and move. The controller's velocity is one number that everything shares. setVelocity/addVelocity change it; move(dir, speed) then accelerates the horizontal part of it towards dir * speed at the ground acceleration (50 m/s²) or the air acceleration (12 m/s²). So a sideways shove on the ground is braked within a few ticks, and a launch's horizontal speed bleeds towards whatever the stick asks for while it is in the air. A game that wants a launch to keep its speed keeps asking move for that speed until it lands. Who else writes it. With the component's input: "auto" (the default) the ENGINE reads the device and drives this controller itself: at the top of every tick, before the controller steps and before any behaviour runs, it writes the player's move stick (keys, pad or a POST /input hold) as the movement, jumps on the jump button at the component's jump height, latches crouch and turns the heading with look. A move from here is written after that step, for the next one, and the next tick's stick overwrites it: while the stick is pushed, the stick wins, and the engine jumps whatever this behaviour does. inputBasis picks the stick's frame: "camera" (default: away from the drawn view, screen-right is right; world −Z with no camera), "world" (−Z) or "controller" (this heading). With input: "script" the engine writes nothing here; ctx.input still reads the device, this behaviour is the only writer, and ctx.camera.viewYaw gives the frame the engine would have used. See docs/API.md, "Who drives the controller".

Reached in a behaviour as ctx.controller.

Signature

export interface Controller {
  /** Request movement this tick in world direction `dir` at `speed` m/s; slopes, steps and collisions resolve in Rust. */
  move(dir: Vec3, speed: number): void
  /**
   * Jump on the next tick, if grounded and standing. `height` is metres above the take-off point, reached
   * under this character's own gravity (so `gravityScale` changes how long the jump takes, not how high it
   * goes); omitted, the component's `jump` height. A press in the air does nothing and is not saved for the
   * landing: a second jump in the air is the game's, built with {@link Controller.setVelocity}.
   * Holding the press (calling this every tick) jumps once. Throws `RangeError` for a negative or non-finite
   * height.
   */
  jump(height?: number): void
  crouch(on: boolean): void
  /**
   * Replace the velocity, m/s. Visible at once through {@link Controller.velocity}; integrated from at the
   * next step. A write whose `y` is above zero lifts a grounded character off the ground without a `jump`
   * event — a jump pad, a knockback, a vehicle's collision kick — which is the only way to launch a
   * character upward. Throws `RangeError` for a NaN or infinite component.
   */
  setVelocity(v: Vec3): void
  /**
   * Add to the velocity, m/s. A velocity change rather than a force: the character has no mass. Calls in
   * one tick add up. Same ground rule and errors as {@link Controller.setVelocity}.
   */
  addVelocity(v: Vec3): void
  readonly grounded: boolean
  readonly velocity: Vec3
  /** Normal of the surface under the feet, walkable or not (a slope too steep to stand on still reports); null in the air. */
  readonly groundNormal: Vec3 | null
  /** The entity stood on while grounded — a platform, a vehicle deck, a floor whose tag picks a footstep sound; null otherwise. */
  readonly groundEntity: EntityHandle | null
  /**
   * How fast moving ground carried the character on the last step, m/s; zero on still ground, in the air and
   * with `moveWithGround` off. A rider that jumps or walks off moving ground keeps this speed on its own —
   * the jump is relative to the platform — and {@link Controller.velocity} includes it while airborne.
   */
  readonly groundVelocity: Vec3
  /**
   * A moving body closing on the character on the last step — a door coming down on its head, a lift
   * carrying it into a ceiling, a kinematic wall sliding into it — or null. The character is never pushed
   * through the floor or a wall to make room: it stays where it is, can still walk out from under the
   * mover, and the engine raises `"character-squeezed"` ({@link BusCharacterSqueezed}) once when it starts.
   */
  readonly squeezedBy: EntityHandle | null
  /**
   * The last surface too steep to stand on that the character walked into on the last step, as a unit
   * normal pointing out of it; null when the move touched no wall. For a wall slide, a vault, a bump sound.
   */
  readonly wallNormal: Vec3 | null
  /** The last move ran into something it could neither climb nor slide past. */
  readonly blocked: boolean
  /**
   * Multiplier on this character's gravity — the component's `gravity`, else the length of the scene's
   * `meta.gravity`; 1 by default. 0.5 for a low-gravity zone or a glide, 2 for a
   * heavier fall, 0 to hang. Throws `RangeError` below 0 or when not finite.
   */
  gravityScale: number
  /**
   * Whether moving ground carries this character — a lift, a ferry, a turntable, a tipping seesaw — by its
   * movement and turn since the last tick, before the character's own move. On by default; off for a
   * character that should slide off a moving deck.
   */
  moveWithGround: boolean
}

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

Members

MemberSignatureDescription
movemove(dir: Vec3, speed: number): voidRequest movement this tick in world direction dir at speed m/s; slopes, steps and collisions resolve in Rust.
jumpjump(height?: number): voidJump on the next tick, if grounded and standing. height is metres above the take-off point, reached under this character's own gravity (so gravityScale changes how long the jump takes, not how high it goes); omitted, the component's jump height. A press in the air does nothing and is not saved for the landing: a second jump in the air is the game's, built with {@link Controller.setVelocity}. Holding the press (calling this every tick) jumps once. Throws RangeError for a negative or non-finite height.
crouchcrouch(on: boolean): void
setVelocitysetVelocity(v: Vec3): voidReplace the velocity, m/s. Visible at once through {@link Controller.velocity}; integrated from at the next step. A write whose y is above zero lifts a grounded character off the ground without a jump event — a jump pad, a knockback, a vehicle's collision kick — which is the only way to launch a character upward. Throws RangeError for a NaN or infinite component.
addVelocityaddVelocity(v: Vec3): voidAdd to the velocity, m/s. A velocity change rather than a force: the character has no mass. Calls in one tick add up. Same ground rule and errors as {@link Controller.setVelocity}.
groundedreadonly grounded: boolean
velocityreadonly velocity: Vec3
groundNormalreadonly groundNormal: Vec3 | nullNormal of the surface under the feet, walkable or not (a slope too steep to stand on still reports); null in the air.
groundEntityreadonly groundEntity: EntityHandle | nullThe entity stood on while grounded — a platform, a vehicle deck, a floor whose tag picks a footstep sound; null otherwise.
groundVelocityreadonly groundVelocity: Vec3How fast moving ground carried the character on the last step, m/s; zero on still ground, in the air and with moveWithGround off. A rider that jumps or walks off moving ground keeps this speed on its own — the jump is relative to the platform — and {@link Controller.velocity} includes it while airborne.
squeezedByreadonly squeezedBy: EntityHandle | nullA moving body closing on the character on the last step — a door coming down on its head, a lift carrying it into a ceiling, a kinematic wall sliding into it — or null. The character is never pushed through the floor or a wall to make room: it stays where it is, can still walk out from under the mover, and the engine raises "character-squeezed" ({@link BusCharacterSqueezed}) once when it starts.
wallNormalreadonly wallNormal: Vec3 | nullThe last surface too steep to stand on that the character walked into on the last step, as a unit normal pointing out of it; null when the move touched no wall. For a wall slide, a vault, a bump sound.
blockedreadonly blocked: booleanThe last move ran into something it could neither climb nor slide past.
gravityScalegravityScale: numberMultiplier on this character's gravity — the component's gravity, else the length of the scene's meta.gravity; 1 by default. 0.5 for a low-gravity zone or a glide, 2 for a heavier fall, 0 to hang. Throws RangeError below 0 or when not finite.
moveWithGroundmoveWithGround: booleanWhether moving ground carries this character — a lift, a ferry, a turntable, a tipping seesaw — by its movement and turn since the last tick, before the character's own move. On by default; off for a character that should slide off a moving deck.

Example

The tested fixture fixtures/recipes/drone-search/scenes/behaviors/carrier.ts.

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

/** Moves a kinematic body round `props.path` (world [x, z] points) at `props.speed` m/s, facing where it goes: the
 * stand-in for a boat or a truck the drone launches from, and for the contact it looks for. */
interface Props {
  path: [number, number][]
  speed: number
}

interface State {
  next: number
}

export default {
  init(ctx) {
    ctx.state.next ??= 0
  },
  update(ctx, dt) {
    const path = ctx.props.path
    const goal = path[ctx.state.next % path.length]
    if (goal === undefined) return
    const p = ctx.entity.position
    const to: [number, number, number] = [goal[0], p[1], goal[1]]
    ctx.entity.lookAt(to)
    if (ctx.entity.moveToward(to, ctx.props.speed * dt)) ctx.state.next = (ctx.state.next + 1) % path.length
  },
} satisfies Behavior<State, Props>