Talos docs

Behavior

Since 0.6Changed in 0.7Changed in next

What a behavior module exports as default.

Plain text

Every hook is optional. A throw disables this instance and emits script-error; the others keep running. One exception: a stale handle — a DespawnedEntityError, from a handle kept past its entity — thrown from update, pausedUpdate, onCollision, onTrigger, onEvent or a ctx.events.on listener ends that call only; the instance runs again next tick and the error is reported (at most once a second) with disabled: false. In init it disables like any throw. Each hook has a wall-clock budget (2 ms by default); ten consecutive overruns are reported as a script-error (phase budget, disabled: false, at most once every 600 ticks after the first) and the instance keeps running. A runaway loop is still stopped and disables the instance.

Signature

export interface Behavior<S = Record<string, any>, P = Record<string, any>> {
  /**
   * In a rollback session, run only on confirmed ticks: while the engine predicts ticks ahead of the
   * inputs it has, this module's behaviours run nothing (no `update`, no event hook, no `ctx.events`
   * listener; `init` and `dispose` still run). For what must happen once and never be taken back: a
   * score, a round ending, an AI director's decision, a purchase. It then acts a few ticks late (the
   * prediction depth, about 8 ticks at 60 Hz), on every machine at the same tick. Its `ctx.state` is
   * saved and restored with the rest. Default false: everything else runs on predicted ticks too,
   * which is what keeps movement and hits responsive. Without rollback it changes nothing.
   */
  readonly confirmedOnly?: boolean
  /** Once after attach, and again after each hot reload (with `state` preserved). */
  init?(ctx: Ctx<S, P>): void
  /** Every fixed tick; `dt` is 1/60 s. Not while the world is held (`ctx.session.pause()`, a `pausesWorld` screen). */
  update?(ctx: Ctx<S, P>, dt: number): void
  /**
   * Every fixed tick, once for the whole module, in place of each instance's `update` (which is then never
   * called): a system that steps every entity running this behaviour in one call, the cheap way to run
   * hundreds of them, above all under rollback, where a tick runs many times. `batch` lists the enabled
   * instances in attach order, and the call comes at the first one's place in that order. `ctx` is that
   * first instance's: reach the others through `batch.entities[k]` and `batch.states[k]` (each its own
   * `ctx.state`, the same object, so a write persists and rollback restores it). The budget, the hard cap
   * and the overrun count treat the call as the first instance's, and a throw or a stop from the guards
   * disables or suspends every instance in the batch. Not while the world is held.
   */
  updateAll?(ctx: Ctx<S, P>, dt: number, batch: Batch<S>): void
  /**
   * Every fixed tick while the world is held, in place of `update`: where a pause menu reads its
   * own key, since `update` is not running. `ctx.time.now` stands still; `dt` is still 1/60 s.
   */
  pausedUpdate?(ctx: Ctx<S, P>, dt: number): void
  /**
   * Before detach and before a hot reload swaps the module. `reason` says which: `"detach"` (the entity,
   * or the scene, is going) or `"reload"` (`init` follows, with `state` kept), so an effect that belongs
   * to the entity going away is not run by an edit.
   */
  dispose?(ctx: Ctx<S, P>, reason?: "detach" | "reload"): void
  /**
   * Once when this entity's body starts touching another: once per pair of
   * bodies, however many points the contact has, with the point that carried the most impulse. Not again
   * while they stay in touch; again after they have been apart for 3 ticks or more.
   */
  onCollision?(ctx: Ctx<S, P>, collision: Collision): void
  /**
   * Every tick this entity's body is touching another, the tick it started
   * included and after `onCollision`: once per pair per tick, with that tick's strongest point. For what
   * lasts as long as the contact does, such as a scrape along a wall. When it stops being called, the
   * contact has ended.
   */
  onCollisionStay?(ctx: Ctx<S, P>, collision: Collision): void
  onTrigger?(ctx: Ctx<S, P>, trigger: TriggerEvent): void
  /**
   * `other` is the other participant from this side — the volume
   * hears who entered, and the entrant hears the volume. (In {@link onTrigger}'s payload `other` is always
   * the entrant.) Each runs after `onTrigger` for the same contact.
   */
  onTriggerEnter?(ctx: Ctx<S, P>, other: EntityHandle): void
  /** The other participant left: see {@link onTriggerEnter}. */
  onTriggerExit?(ctx: Ctx<S, P>, other: EntityHandle): void
  /** Any `emit`, from any behavior or from the app. */
  onEvent?(ctx: Ctx<S, P>, event: GameEvent): void
}

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

Members

MemberSignatureDescription
confirmedOnlyreadonly confirmedOnly?: booleanIn a rollback session, run only on confirmed ticks: while the engine predicts ticks ahead of the inputs it has, this module's behaviours run nothing (no update, no event hook, no ctx.events listener; init and dispose still run). For what must happen once and never be taken back: a score, a round ending, an AI director's decision, a purchase. It then acts a few ticks late (the prediction depth, about 8 ticks at 60 Hz), on every machine at the same tick. Its ctx.state is saved and restored with the rest. Default false: everything else runs on predicted ticks too, which is what keeps movement and hits responsive. Without rollback it changes nothing.
initinit?(ctx: Ctx<S, P>): voidOnce after attach, and again after each hot reload (with state preserved).
updateupdate?(ctx: Ctx<S, P>, dt: number): voidEvery fixed tick; dt is 1/60 s. Not while the world is held (ctx.session.pause(), a pausesWorld screen).
updateAllupdateAll?(ctx: Ctx<S, P>, dt: number, batch: Batch<S>): voidEvery fixed tick, once for the whole module, in place of each instance's update (which is then never called): a system that steps every entity running this behaviour in one call, the cheap way to run hundreds of them, above all under rollback, where a tick runs many times. batch lists the enabled instances in attach order, and the call comes at the first one's place in that order. ctx is that first instance's: reach the others through batch.entities[k] and batch.states[k] (each its own ctx.state, the same object, so a write persists and rollback restores it). The budget, the hard cap and the overrun count treat the call as the first instance's, and a throw or a stop from the guards disables or suspends every instance in the batch. Not while the world is held.
pausedUpdatepausedUpdate?(ctx: Ctx<S, P>, dt: number): voidEvery fixed tick while the world is held, in place of update: where a pause menu reads its own key, since update is not running. ctx.time.now stands still; dt is still 1/60 s.
disposedispose?(ctx: Ctx<S, P>, reason?: "detach" | "reload"): voidBefore detach and before a hot reload swaps the module. reason says which: "detach" (the entity, or the scene, is going) or "reload" (init follows, with state kept), so an effect that belongs to the entity going away is not run by an edit.
onCollisiononCollision?(ctx: Ctx<S, P>, collision: Collision): voidOnce when this entity's body starts touching another: once per pair of bodies, however many points the contact has, with the point that carried the most impulse. Not again while they stay in touch; again after they have been apart for 3 ticks or more.
onCollisionStayonCollisionStay?(ctx: Ctx<S, P>, collision: Collision): voidEvery tick this entity's body is touching another, the tick it started included and after onCollision: once per pair per tick, with that tick's strongest point. For what lasts as long as the contact does, such as a scrape along a wall. When it stops being called, the contact has ended.
onTriggeronTrigger?(ctx: Ctx<S, P>, trigger: TriggerEvent): void
onTriggerEnteronTriggerEnter?(ctx: Ctx<S, P>, other: EntityHandle): voidother is the other participant from this side — the volume hears who entered, and the entrant hears the volume. (In {@link onTrigger}'s payload other is always the entrant.) Each runs after onTrigger for the same contact.
onTriggerExitonTriggerExit?(ctx: Ctx<S, P>, other: EntityHandle): voidThe other participant left: see {@link onTriggerEnter}.
onEventonEvent?(ctx: Ctx<S, P>, event: GameEvent): voidAny emit, from any behavior or from the app.

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