Controller
Present on ctx.controller when the entity has a character-controller component (kinematic capsule).
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
| Member | Signature | Description |
|---|---|---|
move | move(dir: Vec3, speed: number): void | Request movement this tick in world direction dir at speed m/s; slopes, steps and collisions resolve in Rust. |
jump | jump(height?: 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. |
crouch | crouch(on: boolean): void | |
setVelocity | setVelocity(v: Vec3): 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. |
addVelocity | addVelocity(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}. |
grounded | readonly grounded: boolean | |
velocity | readonly velocity: Vec3 | |
groundNormal | readonly groundNormal: Vec3 | null | Normal of the surface under the feet, walkable or not (a slope too steep to stand on still reports); null in the air. |
groundEntity | readonly groundEntity: EntityHandle | null | The entity stood on while grounded — a platform, a vehicle deck, a floor whose tag picks a footstep sound; null otherwise. |
groundVelocity | readonly groundVelocity: Vec3 | 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. |
squeezedBy | readonly squeezedBy: EntityHandle | null | 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. |
wallNormal | readonly wallNormal: Vec3 | 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. |
blocked | readonly blocked: boolean | The last move ran into something it could neither climb nor slide past. |
gravityScale | gravityScale: number | 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. |
moveWithGround | moveWithGround: boolean | 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. |
Example
The tested fixture fixtures/recipes/jump-pad-and-elevator/scenes/behaviors/elevator.ts.
import type { Behavior } from "@argus/api"
/**
* A lift: a kinematic slab that rides between two heights and waits at each end.
*
* It only moves itself, by `position`. Whoever stands on it is carried by the engine — the character
* controller follows moving ground on its own — so there is no rider list and no parenting here.
*/
interface State {
/** +1 going up, -1 going down, 0 waiting. */
heading: number
/** Seconds left to wait at the current end. */
wait: number
/** Where the next leg goes once the wait is over. */
next: number
}
interface Props {
/** Height of the slab's origin at the bottom and the top, metres. */
bottom?: number
top?: number
/** Metres per second. Default 1.5. */
speed?: number
/** Seconds to wait at each end. Default 1. */
pause?: number
}
export default {
init(ctx) {
if (typeof ctx.state.heading !== "number") ctx.state = { heading: 0, wait: ctx.props.pause ?? 1, next: 1 }
},
update(ctx, dt) {
const bottom = ctx.props.bottom ?? 0
const top = ctx.props.top ?? 4
const p = ctx.entity.position
if (ctx.state.heading === 0) {
ctx.state.wait -= dt
if (ctx.state.wait <= 0) ctx.state.heading = ctx.state.next
return
}
let y = p[1] + ctx.state.heading * (ctx.props.speed ?? 1.5) * dt
const end = ctx.state.heading > 0 ? top : bottom
if ((ctx.state.heading > 0 && y >= end) || (ctx.state.heading < 0 && y <= end)) {
y = end
ctx.state.next = -ctx.state.heading
ctx.state.heading = 0
ctx.state.wait = ctx.props.pause ?? 1
}
// `position`, not `teleport`: this is a move, so it is smoothed between ticks and the rider is
// carried by exactly the distance the slab travelled.
ctx.entity.position = [p[0], y, p[2]]
},
} satisfies Behavior<State, Props>