Behavior
What a behavior module exports as default.
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
/**
* The usual pair, with its usual meaning: `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
| Member | Signature | Description |
|---|---|---|
confirmedOnly | readonly confirmedOnly?: boolean | 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. |
init | init?(ctx: Ctx<S, P>): void | Once after attach, and again after each hot reload (with state preserved). |
update | update?(ctx: Ctx<S, P>, dt: number): void | Every fixed tick; dt is 1/60 s. Not while the world is held (ctx.session.pause(), a pausesWorld screen). |
updateAll | updateAll?(ctx: Ctx<S, P>, dt: number, batch: Batch<S>): 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. |
pausedUpdate | pausedUpdate?(ctx: Ctx<S, P>, dt: number): 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. |
dispose | dispose?(ctx: Ctx<S, P>, reason?: "detach" | "reload"): 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. |
onCollision | onCollision?(ctx: Ctx<S, P>, collision: Collision): 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. |
onCollisionStay | onCollisionStay?(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. |
onTrigger | onTrigger?(ctx: Ctx<S, P>, trigger: TriggerEvent): void | |
onTriggerEnter | onTriggerEnter?(ctx: Ctx<S, P>, other: EntityHandle): void | The usual pair, with its usual meaning: 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. |
onTriggerExit | onTriggerExit?(ctx: Ctx<S, P>, other: EntityHandle): void | The other participant left: see {@link onTriggerEnter}. |
onEvent | onEvent?(ctx: Ctx<S, P>, event: GameEvent): void | Any 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