Skip to content

CharacterController

A capsule-based character controller (Jolt CharacterVirtual) for player and NPC movement. Created via Scene.createCharacterController. The controller handles ground detection, stair-stepping, slope limits, and state tracking automatically.

scene.createCharacterController(height, radius, x, y, z)

Section titled “scene.createCharacterController(height, radius, x, y, z)”

Returns: CharacterController — A new controller placed at the given position.

Parameters:

  • height (Num) — Capsule standing height in metres.
  • radius (Num) — Capsule radius in metres.
  • x, y, z (Num) — Initial world position.
_controller = _scene.createCharacterController(1.8, 0.3, 0, 2, 0)

Set the velocity the controller should try to reach this frame. Call every frame from update(dt). The physics system handles ground detection and slope response; do not set a Y velocity for jumps — use jump() instead.

var speed = 5
var vx = 0
var vz = 0
if (Input.key("a")) vx = -speed
if (Input.key("d")) vx = speed
if (Input.key("w")) vz = -speed
if (Input.key("s")) vz = speed
_controller.setDesiredVelocity(vx, 0, vz)

Apply an upward impulse. Only effective when the controller is grounded (isGrounded is true).

if (Input.keyJustPressed("space") && _controller.isGrounded) {
_controller.jump()
}

running=(value) / crouching=(value) / swimming=(value)

Section titled “running=(value) / crouching=(value) / swimming=(value)”

Set locomotion flags. These switch the controller into the corresponding state for speed selection and animation.

Get or set the yaw angle (radians) the controller is facing. Used for directional movement and animation.

Returns: Num — Current CharacterState constant.

Returns: Num — Ground contact state:

ValueMeaning
0On ground (flat/walkable)
1On steep ground (too steep to walk)
2Not supported (hanging, ledge)
3In air

Returns: Bool — true when the controller is on walkable ground (groundState == 0).

Returns: Bool — true when the controller has any ground contact (including steep slopes).

Returns: List — [nx, ny, nz] ground surface normal.

Returns: List — [vx, vy, vz] velocity of the surface the controller is standing on (for moving platforms).

Returns: List — [vx, vy, vz] current controller velocity.

if (_controller.state == CharacterState.JUMPING) {
// play jump animation
}
if (_controller.groundState == 1) {
// on steep slope — slide down
_controller.setDesiredVelocity(0, -5, 0)
}

Get or set the controller world position. Returns [x, y, z].

Get or set the rotation (Euler radians). Returns [rx, ry, rz].

Get or bind the controller to a Node. When bound, the node’s world position is updated each frame to follow the controller. Bind after creating the controller to drive a scene node.

_controllerNode = _scene.addNode("Player")
_controller.node = _controllerNode

Returns: Bool — true if the shape change succeeded (i.e. the new capsule fits in the current position without overlap).

Change the capsule dimensions at runtime (e.g. stand up from crouch). Returns false if the new shape would overlap existing geometry.

Read-only current capsule dimensions.

Characters carry an inner kinematic body (Jolt mInnerBodyShape, a capsule at 90 % of the outer dimensions), so they exist in the physics broadphase: Physics raycasts, shape sweeps and overlaps can hit them, and dynamic bodies and CCD collide with them instead of passing through. The inner body follows the character automatically (including setShape crouches) and never collides with its own character.

Characters register on their own gameplay collision layer (bit 1, value 2), so a query layer mask can include or exclude them deliberately. The default match-anything mask finds them; the built-in camera occlusion collider (CamRig.collider) masks them out so characters never yank the camera.

Returns: Num — the body id that Physics query hits carry when they strike this character, or null when the inner body is disabled.

A hit on a character has no scene node (hit.getNodeId() is 0) — attribute it by body id instead:

var hit = Physics.raycastMaxDist(scene, ox, oy, oz, dx, dy, dz, 100)
if (hit != null && hit.getBodyId() == _controller.bodyId) {
// the ray struck this character
}

Physics.overlapSphere resolves characters to their bound node (controller.node), so AI-perception style queries return characters like any other body-backed node.

Remove the character from the physics world: raycasts stop hitting it, dynamic bodies stop colliding with it, and this object is invalidated — every later call is a safe no-op. Idempotent. The bound node (your visuals) is untouched — detach it separately. Use this when despawning a character; without it, the character’s physics presence lives until the scene is destroyed.

_controller.destroy()
_controllerNode.detach()

To disable inner bodies game-wide (restoring the old query-invisible, non-solid characters), set character_inner_body = false in the game.toml [Physics] section — see Configuration.

PropertyDefaultDescription
walkSpeed / walkSpeed=(value)—Walk speed in m/s
runSpeed / runSpeed=(value)—Run speed in m/s
crouchSpeed / crouchSpeed=(value)—Crouch speed in m/s
swimSpeed / swimSpeed=(value)—Swim speed in m/s
jumpVelocity / jumpVelocity=(value)—Upward velocity applied on jump()
_controller.walkSpeed = 3
_controller.runSpeed = 7
_controller.jumpVelocity = 6

Constant class for state values returned by controller.state:

ConstantValueDescription
CharacterState.IDLE0Standing still
CharacterState.WALKING1Moving at walk speed
CharacterState.RUNNING2Moving at run speed
CharacterState.JUMPING3In a jump (rising)
CharacterState.FALLING4Falling (no ground contact)
CharacterState.SWIMMING5Swimming
CharacterState.CROUCHING6Crouching, stationary
CharacterState.CROUCH_WALKING7Moving while crouching
CharacterState.INTERACTING8Custom interaction state
CharacterState.CUSTOM_TOGGLE_ON9Custom toggle on
CharacterState.CUSTOM_TOGGLE_OFF10Custom toggle off
if (_controller.state == CharacterState.RUNNING) {
// Play run animation
}

See Physics for rigid body operations and contact events. See Scene for createCharacterController.