XrHandle
Since next
ctx.xr: the headset, or the desktop simulator (argus run --xr-sim), this tick.
active is false and everything else null when nothing runs XR (always on the web), so a game asks ctx.xr.active and runs one path for a headset and the simulator. Poses are in the world; the Local ones in the XR origin's frame (the scene's xrOrigin entity). docs/status/xr-sim.md.
Signature
export interface XrHandle {
/** XR runs: a headset, or the desktop simulator. */
readonly active: boolean
/** The desktop simulator rather than a headset. A game should not need to ask. */
readonly simulated: boolean
/** The headset's name, or null. */
readonly headset: string | null
/** The head in the world; null while it is not tracked. */
readonly head: XrPose | null
/** The head in the XR origin's frame. */
readonly headLocal: XrPose | null
/** The entity carrying the scene's `xrOrigin`, or null. */
readonly origin: EntityHandle | null
/** One hand this tick, or null when nothing runs XR. Throws `xr.hand-unknown` for another name. */
hand(name: "left" | "right"): XrHand | null
/** Put the head back on the XR origin. Applied after this tick. */
recenter(): void
/** A haptic pulse; the simulator logs it. Applied after this tick, never from a re-simulated one. */
vibrate(hand: "left" | "right", opts?: { seconds?: number; amplitude?: number; frequency?: number }): void
}
Import: import type { XrHandle } from "@argus/api"
Members
| Member | Signature | Description |
|---|---|---|
active | readonly active: boolean | XR runs: a headset, or the desktop simulator. |
simulated | readonly simulated: boolean | The desktop simulator rather than a headset. A game should not need to ask. |
headset | readonly headset: string | null | The headset's name, or null. |
head | readonly head: XrPose | null | The head in the world; null while it is not tracked. |
headLocal | readonly headLocal: XrPose | null | The head in the XR origin's frame. |
origin | readonly origin: EntityHandle | null | The entity carrying the scene's xrOrigin, or null. |
hand | hand(name: "left" | "right"): XrHand | null | One hand this tick, or null when nothing runs XR. Throws xr.hand-unknown for another name. |
recenter | recenter(): void | Put the head back on the XR origin. Applied after this tick. |
vibrate | vibrate(hand: "left" | "right", opts?: { seconds?: number; amplitude?: number; frequency?: number }): void | A haptic pulse; the simulator logs it. Applied after this tick, never from a re-simulated one. |
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>