WorldQuery
Since 0.6Changed in 0.7
Read access to every entity plus the physics and navigation queries.
Reached in a behaviour as ctx.world.
Signature
export interface WorldQuery {
/**
* Entities matching the filter. No filter returns everything. Order: slot order unless `sort` says
* otherwise (see {@link QueryFilter.sort}); it is deterministic but not id order. `near` and `radius`
* are inclusive (see {@link QueryFilter.near}).
*/
query(filter?: QueryFilter): EntityHandle[]
/** Look an entity up by scene id. */
find(id: string): EntityHandle | null
/**
* The centre line of the road entity `id` (an entity with a `road` component), or null when `id`
* is not a road. See {@link RoadLine}.
*/
road(id: string): RoadLine | null
/** The instances of an entity with an `instancedMesh` component (by id or handle), or null when it has none. See {@link Instances}. */
instances(entity: string | EntityHandle): Instances | null
/** Number of live entities. */
readonly count: number
/**
* First physics hit along a ray, or null: a miss is `null`, never `undefined`. A hit is
* `{ entity, point, normal, distance, instance? }` (see {@link Hit}).
*
* - `from`: the ray's start, world space. `dir`: its direction; it need not be normalised (it is
* normalised before the cast, so `distance` and `max` are in metres whatever its length). A zero or
* non-finite `dir` hits nothing and answers `null`.
* - `max`: the longest the ray reaches, metres. Default (absent, `null` or `undefined`): no limit.
* - `opts`: {@link CastOptions}: `triggers`, `exclude`, `excludeTags`, `layers`.
*
* Trigger volumes count as hits unless `opts.triggers` is false — pass that for a camera boom, a line
* of sight or an aim ray, which should see through a volume the player walks through. An exact tie
* between two hits goes to the entity whose id comes first (docs/API.md, "Projectiles in a game that
* must agree across machines").
*/
raycast(from: Vec3, dir: Vec3, max?: number, opts?: CastOptions): Hit | null
/**
* One `raycast` per ray, for a system-style `updateAll`: `from` and `dir` hold 3 numbers per ray (a
* `Float32Array` such as {@link positions} answers, or an array of numbers). One entry per ray: the
* entity the ray hit first, or `null` for a miss (never `undefined`); use `raycast` where the point,
* normal or distance matter. `opts` apply to every ray, and `opts.excludeEach` holds one entity (or
* `null`) per ray that only that ray passes through, such as each shooter's own capsule.
*/
raycastMany(from: Float32Array | number[], dir: Float32Array | number[], max?: number, opts?: CastOptions & { excludeEach?: (string | EntityHandle | null)[] }): (EntityHandle | null)[]
/**
* The positions of `entities` (handles or ids), 3 numbers each, `[x0, y0, z0, x1, …]`, in the order
* given. An entity that is gone, or an unknown id, reads as three NaN rather than throwing.
*/
positions(entities: (string | EntityHandle)[]): Float32Array
/**
* For each point in `from`, the index in `to` of the nearest point within `radius`, or -1; both hold
* 3 numbers per point. The distance, the inclusive radius and the tie rule are `query`'s: f32
* distance rounded after the square root, and equal distances to the lower index (`query`'s stable
* sort keeps entity order). So with `to` in `query`'s order, the pick is what
* `query({ near, sort: "distance" })` puts first, near-ties included, on every machine. A negative
* radius finds nothing. A NaN point is never nearest, and a `from` point does not skip itself when
* `to` holds it too.
*/
nearestMany(from: Float32Array | number[], to: Float32Array | number[], radius: number): Int32Array
/** Like `raycast` but sweeping a shape. */
shapeCast(shape: CastShape, from: Vec3, dir: Vec3, max?: number, opts?: CastOptions): Hit | null
/**
* Every entity whose collider `shape` overlaps at `position`, turned by `rotation` (a quaternion
* `[x, y, z, w]`, default none), sorted by entity: the same order on every run. A static question:
* nothing is swept and nothing moves, so it answers about the colliders as the last physics step
* left them. Touching counts. `opts` are a cast's: `triggers` (default true here, as for a cast),
* `exclude`, `excludeTags`, `layers`. Throws a `TypeError` for a shape that is none of the three.
*/
overlap(shape: CastShape, position: Vec3, rotation?: Quat, opts?: CastOptions): EntityHandle[]
/**
* Who `entity`'s sensor (an `attach`ed entity with a trigger collider) touches as of this tick's
* query, sorted by entity; null when it has no (resolved) sensor. A sensor queries its collider
* every tick at the bone's pose and delivers `overlap` events: `name` "overlap", `data`
* `{ sensor, other, phase: "begin" | "end" }` (entity ids), by sensor; within a sensor its ends,
* then its begins, each by other.
*/
overlaps(entity: EntityHandle | string): EntityHandle[] | null
/**
* Switches `entity`'s sensor on or off (it starts on). Off, it queries nothing and ends every pair
* it held, with `end` events on this tick's pass; with `activeWhile` it also needs that animation
* window open. Returns false when the entity has no sensor.
*/
setSensor(entity: EntityHandle | string, on: boolean): boolean
/**
* The dynamic body a pick ray lands on first, or null — what "the thing I am pointing at" means for a
* grab. The first thing the ray meets decides: a wall in front of a crate hides it, and a static or
* kinematic body, one over `maxMass` or one without `tag` answers null rather than letting the ray
* carry on. Exclude the picker.
*
* ```ts
* const eye = ctx.entity.worldPosition; eye[1] += 1.6
* const hit = ctx.world.pick(eye, aim, 2.5, { exclude: ctx.entity, tag: "grabbable" })
* ```
*/
pick(from: Vec3, dir: Vec3, max?: number, opts?: PickOptions): Hit | null
/**
* Start carrying `target`'s dynamic body at `holder` (any entity: a character, a hand, a crane hook).
* True when it is now held. False, having changed nothing, when the target is gone, has no body, is not
* dynamic, is the holder, or is over `maxMass`. A NaN, infinite or non-positive option throws a `RangeError`.
*
* The body stays a physics body: each tick it is given the velocity that brings it to its hold point,
* so it still collides with everything except its holder, stops against walls (it never passes through
* one), and pushes other bodies aside. When it falls more than `breakDistance` behind and keeps falling
* behind — the holder walked away from a crate wedged in a doorway — the hold breaks and the body drops;
* `heldBy` reads null from that tick. However a hold ends — a break, a `release`, a despawn — the engine
* raises a `"hold-broken"` bus event ({@link BusHoldBroken}) saying which and why. A character's
* controller passes through what it is holding.
*
* Holding something already held replaces that hold: a hand-over, or a new `offset`, is one more call.
* Held state is part of the physics checkpoint, so `GET /snapshot` → `POST /snapshot` restores a carry.
*/
hold(holder: EntityHandle | string, target: EntityHandle | string, opts?: HoldOptions): boolean
/** Let `target` go; `opts.velocity` throws it. False if it was not held. */
release(target: EntityHandle | string, opts?: ReleaseOptions): boolean
/** Who is holding `target`, or null. */
heldBy(target: EntityHandle | string): EntityHandle | null
/** What `holder` is holding, in entity order; empty when nothing. */
holding(holder: EntityHandle | string): EntityHandle[]
/**
* Navmesh path as a polyline, or null when unreachable. Throws `nav-undeclared` in a scene that
* declares no navigation (no `meta.nav`, no `navagent`): navigation is opt-in, and such a scene
* has no navmesh. Add `"nav": {}` to the scene's meta.
*/
path(from: Vec3, to: Vec3): Vec3[] | null
/**
* Put another copy of an entity the scene already has, or of a prefab, into the world, and get a
* handle to it.
*
* `like` is the scene id of a live entity, and the new entity is a full copy of it: its name,
* its tags, its parent, its scale and every one of its components, wired the way the scene's
* own entities are — a collider becomes a physics body the solver knows, a mesh becomes
* something the renderer draws, a script gets its own behavior instance with its own `state`,
* all within the tick that asked for it.
*
* `prefab` instead of `like` spawns a copy of a project prefab file, which need not be placed in
* any scene: its root and every entity under it, the root's id as below and each other entity's
* `"<root id>/<prefab entity id>"`, parented as the prefab has them, with `overrides` applied as a
* scene instance's are. The handle is the root's; {@link EntityHandle.despawn} on it removes the
* whole copy. The project's prefabs are read and checked once, at load, so a spawn parses nothing.
*
* Either way, what is spawned is a shape the contract already validated: an entity of the scene,
* or a prefab file.
*
* **Ids.** Give one and it must be free, or this throws. Leave it out and the engine derives
* `"<like>#1"`, `"<like>#2"`, … from a counter kept per source id. Both are deterministic: two
* runs of the same scene making the same calls in the same order get the same ids and the same
* entity slots, which is what the lockstep critic checks.
*
* Throws rather than returning null, so a spawn that did not happen cannot be mistaken for one
* that did. The error's `name` is `SpawnError` and its message starts with the code:
* `spawn-no-source`, `spawn-two-sources`, `spawn-unknown-source`, `spawn-unknown-prefab`,
* `spawn-prefab-cycle`, `spawn-bad-override`, `spawn-duplicate-id`.
*
* The copy's behaviour runs `init` on this tick, after every `update` has returned, and is then
* handed each event emitted this tick after this call (so an `emit` straight after the spawn
* reaches its `onEvent` and the listeners its `init` subscribed); its first `update` is next tick.
*
* A spawned entity is runtime state, not a scene change. The scene file is untouched,
* `GET /snapshot` carries the entity and `POST /snapshot` puts it back — and
* {@link SessionHandle.restart} rebuilds from the scene as loaded, so a restart removes every
* spawned entity and brings back every authored one.
*
* ```ts
* const cone = ctx.world.spawn({ like: "cone-prototype", position: [4, 0, -12] })
* cone.set("material", { baseColor: "#ff6600" })
*
* const imp = ctx.world.spawn({ prefab: "prefabs/imp.prefab.json", position: [2, 0, 5],
* overrides: { "root/script:chase/props/speed": 5 } }) // imp#1, and imp#1/<child> for each child
* ```
*/
spawn(options: SpawnOptions): EntityHandle
}
Import: import type { WorldQuery } from "@argus/api"
Members
| Member | Signature | Description |
|---|---|---|
query | query(filter?: QueryFilter): EntityHandle[] | Entities matching the filter. No filter returns everything. Order: slot order unless sort says otherwise (see {@link QueryFilter.sort}); it is deterministic but not id order. near and radius are inclusive (see {@link QueryFilter.near}). |
find | find(id: string): EntityHandle | null | Look an entity up by scene id. |
road | road(id: string): RoadLine | null | The centre line of the road entity id (an entity with a road component), or null when id is not a road. See {@link RoadLine}. |
instances | instances(entity: string | EntityHandle): Instances | null | The instances of an entity with an instancedMesh component (by id or handle), or null when it has none. See {@link Instances}. |
count | readonly count: number | Number of live entities. |
raycast | raycast(from: Vec3, dir: Vec3, max?: number, opts?: CastOptions): Hit | null | First physics hit along a ray, or null: a miss is null, never undefined. A hit is { entity, point, normal, distance, instance? } (see {@link Hit}). - from: the ray's start, world space. dir: its direction; it need not be normalised (it is normalised before the cast, so distance and max are in metres whatever its length). A zero or non-finite dir hits nothing and answers null. - max: the longest the ray reaches, metres. Default (absent, null or undefined): no limit. - opts: {@link CastOptions}: triggers, exclude, excludeTags, layers. Trigger volumes count as hits unless opts.triggers is false — pass that for a camera boom, a line of sight or an aim ray, which should see through a volume the player walks through. An exact tie between two hits goes to the entity whose id comes first (docs/API.md, "Projectiles in a game that must agree across machines"). |
raycastMany | raycastMany(from: Float32Array | number[], dir: Float32Array | number[], max?: number, opts?: CastOptions & { excludeEach?: (string | EntityHandle | null)[] }): (EntityHandle | null)[] | One raycast per ray, for a system-style updateAll: from and dir hold 3 numbers per ray (a Float32Array such as {@link positions} answers, or an array of numbers). One entry per ray: the entity the ray hit first, or null for a miss (never undefined); use raycast where the point, normal or distance matter. opts apply to every ray, and opts.excludeEach holds one entity (or null) per ray that only that ray passes through, such as each shooter's own capsule. |
positions | positions(entities: (string | EntityHandle)[]): Float32Array | The positions of entities (handles or ids), 3 numbers each, [x0, y0, z0, x1, …], in the order given. An entity that is gone, or an unknown id, reads as three NaN rather than throwing. |
nearestMany | nearestMany(from: Float32Array | number[], to: Float32Array | number[], radius: number): Int32Array | For each point in from, the index in to of the nearest point within radius, or -1; both hold 3 numbers per point. The distance, the inclusive radius and the tie rule are query's: f32 distance rounded after the square root, and equal distances to the lower index (query's stable sort keeps entity order). So with to in query's order, the pick is what query({ near, sort: "distance" }) puts first, near-ties included, on every machine. A negative radius finds nothing. A NaN point is never nearest, and a from point does not skip itself when to holds it too. |
shapeCast | shapeCast(shape: CastShape, from: Vec3, dir: Vec3, max?: number, opts?: CastOptions): Hit | null | Like raycast but sweeping a shape. |
overlap | overlap(shape: CastShape, position: Vec3, rotation?: Quat, opts?: CastOptions): EntityHandle[] | Every entity whose collider shape overlaps at position, turned by rotation (a quaternion [x, y, z, w], default none), sorted by entity: the same order on every run. A static question: nothing is swept and nothing moves, so it answers about the colliders as the last physics step left them. Touching counts. opts are a cast's: triggers (default true here, as for a cast), exclude, excludeTags, layers. Throws a TypeError for a shape that is none of the three. |
overlaps | overlaps(entity: EntityHandle | string): EntityHandle[] | null | Who entity's sensor (an attached entity with a trigger collider) touches as of this tick's query, sorted by entity; null when it has no (resolved) sensor. A sensor queries its collider every tick at the bone's pose and delivers overlap events: name "overlap", data { sensor, other, phase: "begin" | "end" } (entity ids), by sensor; within a sensor its ends, then its begins, each by other. |
setSensor | setSensor(entity: EntityHandle | string, on: boolean): boolean | Switches entity's sensor on or off (it starts on). Off, it queries nothing and ends every pair it held, with end events on this tick's pass; with activeWhile it also needs that animation window open. Returns false when the entity has no sensor. |
pick | pick(from: Vec3, dir: Vec3, max?: number, opts?: PickOptions): Hit | null | The dynamic body a pick ray lands on first, or null — what "the thing I am pointing at" means for a grab. The first thing the ray meets decides: a wall in front of a crate hides it, and a static or kinematic body, one over maxMass or one without tag answers null rather than letting the ray carry on. Exclude the picker. ts const eye = ctx.entity.worldPosition; eye[1] += 1.6 const hit = ctx.world.pick(eye, aim, 2.5, { exclude: ctx.entity, tag: "grabbable" }) |
hold | hold(holder: EntityHandle | string, target: EntityHandle | string, opts?: HoldOptions): boolean | Start carrying target's dynamic body at holder (any entity: a character, a hand, a crane hook). True when it is now held. False, having changed nothing, when the target is gone, has no body, is not dynamic, is the holder, or is over maxMass. A NaN, infinite or non-positive option throws a RangeError. The body stays a physics body: each tick it is given the velocity that brings it to its hold point, so it still collides with everything except its holder, stops against walls (it never passes through one), and pushes other bodies aside. When it falls more than breakDistance behind and keeps falling behind — the holder walked away from a crate wedged in a doorway — the hold breaks and the body drops; heldBy reads null from that tick. However a hold ends — a break, a release, a despawn — the engine raises a "hold-broken" bus event ({@link BusHoldBroken}) saying which and why. A character's controller passes through what it is holding. Holding something already held replaces that hold: a hand-over, or a new offset, is one more call. Held state is part of the physics checkpoint, so GET /snapshot → POST /snapshot restores a carry. |
release | release(target: EntityHandle | string, opts?: ReleaseOptions): boolean | Let target go; opts.velocity throws it. False if it was not held. |
heldBy | heldBy(target: EntityHandle | string): EntityHandle | null | Who is holding target, or null. |
holding | holding(holder: EntityHandle | string): EntityHandle[] | What holder is holding, in entity order; empty when nothing. |
path | path(from: Vec3, to: Vec3): Vec3[] | null | Navmesh path as a polyline, or null when unreachable. Throws nav-undeclared in a scene that declares no navigation (no meta.nav, no navagent): navigation is opt-in, and such a scene has no navmesh. Add "nav": {} to the scene's meta. |
spawn | spawn(options: SpawnOptions): EntityHandle | Put another copy of an entity the scene already has, or of a prefab, into the world, and get a handle to it. like is the scene id of a live entity, and the new entity is a full copy of it: its name, its tags, its parent, its scale and every one of its components, wired the way the scene's own entities are — a collider becomes a physics body the solver knows, a mesh becomes something the renderer draws, a script gets its own behavior instance with its own state, all within the tick that asked for it. prefab instead of like spawns a copy of a project prefab file, which need not be placed in any scene: its root and every entity under it, the root's id as below and each other entity's "<root id>/<prefab entity id>", parented as the prefab has them, with overrides applied as a scene instance's are. The handle is the root's; {@link EntityHandle.despawn} on it removes the whole copy. The project's prefabs are read and checked once, at load, so a spawn parses nothing. Either way, what is spawned is a shape the contract already validated: an entity of the scene, or a prefab file. Ids. Give one and it must be free, or this throws. Leave it out and the engine derives "<like>#1", "<like>#2", … from a counter kept per source id. Both are deterministic: two runs of the same scene making the same calls in the same order get the same ids and the same entity slots, which is what the lockstep critic checks. Throws rather than returning null, so a spawn that did not happen cannot be mistaken for one that did. The error's name is SpawnError and its message starts with the code: spawn-no-source, spawn-two-sources, spawn-unknown-source, spawn-unknown-prefab, spawn-prefab-cycle, spawn-bad-override, spawn-duplicate-id. The copy's behaviour runs init on this tick, after every update has returned, and is then handed each event emitted this tick after this call (so an emit straight after the spawn reaches its onEvent and the listeners its init subscribed); its first update is next tick. A spawned entity is runtime state, not a scene change. The scene file is untouched, GET /snapshot carries the entity and POST /snapshot puts it back — and {@link SessionHandle.restart} rebuilds from the scene as loaded, so a restart removes every spawned entity and brings back every authored one. ts const cone = ctx.world.spawn({ like: "cone-prototype", position: [4, 0, -12] }) cone.set("material", { baseColor: "#ff6600" }) const imp = ctx.world.spawn({ prefab: "prefabs/imp.prefab.json", position: [2, 0, 5], overrides: { "root/script:chase/props/speed": 5 } }) // imp#1, and imp#1/<child> for each child |
Example
The tested fixture fixtures/recipes/collectible-and-goal/scenes/behaviors/collectible.ts.
import type { Behavior } from "@argus/api"
/**
* A coin the player collects by walking into it.
*
* It says so once, as `collected`, and leaves the world. Counting is somebody else's job —
* `goal-exit.ts` — so a coin never needs to know how many there are.
*
* The contact is `onTrigger`, which needs three things in the scene: a collider with
* `isTrigger: true` (the volume), a `trigger` component (what fires), and a body on the player —
* the character controller's capsule is one.
*/
interface State {
collected: boolean
}
interface Props {
/** Tag the toucher must carry. Default `player`. */
playerTag?: string
}
export default {
init(ctx) {
if (typeof ctx.state.collected !== "boolean") ctx.state.collected = false
},
onTrigger(ctx, trigger) {
if (ctx.state.collected || !trigger.entering) return
if (!trigger.other.hasTag(ctx.props.playerTag ?? "player")) return
ctx.state.collected = true
// The `goal` component on this coin has the same id, so the playtester and the HUD agree.
ctx.events.emit("collected", { goal: ctx.entity.id })
// Last: after `despawn` every member of this entity's handles throws.
ctx.entity.despawn()
},
} satisfies Behavior<State, Props>