Skip to content

character-controller-3d.entity overview


Table of contents


utils

CharacterController3dEntity (class)

A capsule-bodied, physics-driven character entity: walk/run/crouch/jump gameplay logic that works identically on top of any physics backend implementing ICharacterController3dComponent (see that interface's doc for why - all of gravity/jump/speed integration happens here, not in the backend-specific component). Reusable for the player (see PlayerCharacterController, which adds keyboard/mouse input and a camera on top of this) or for an NPC driven by AI logic instead.

Gravity is integrated as a full 3D vector (gravityVector/_fallVelocity), not just its component along up: a tilted physicsWorld.gravity drags the character sideways while airborne, and standing on a surface steeper than maxSlopeClimbAngleRad (re-checked here every tick via isWalkableGround, regardless of what an adapter's own isGrounded reports) is treated as not stably grounded, so the character slides/falls down it under gravity instead of clinging to it - see updateMovement's doc for the exact resting-vs-falling rule. Getting blocked from above while ascending (e.g. jumping into a ceiling) is handled the same way as landing: updateMovement compares the actual post-move() displacement against the desired one along up and, if capped, immediately cancels _fallVelocity's upward component - otherwise it would keep decelerating on gravity's own time schedule regardless of the character's actual (blocked) position, making the character look glued to the ceiling for as long as an unobstructed jump's rise phase would have taken.

Horizontal movement is direct/instantaneous while resting on the ground (no momentum - snappy, input-follows-exactly control), but becomes velocity-based the instant the character leaves the ground: whatever horizontal ground speed was in effect at takeoff (walk or run) is captured into _airHorizontalVelocity and persists through the whole arc unless the input driver changes moveDirection/isRunning, in which case airControlFactor caps how fast the resulting steering can redirect it (see updateMovement's doc) - so a running jump travels exactly as far horizontally as the run speed implies, instead of the speed silently collapsing to a fraction of it the instant the character leaves the ground.

moveDirection is local-space (rotated by this.rotation internally): local +Y is "forward" at zero yaw, local +X is "right" at zero yaw, local Z is unused (always ignored - vertical motion is handled separately, see above) - the same right=X/forward=Y/up=Z axis paradigm RaycastVehicle3dEntity/GgCarEntity use (see e.g. AmmoRaycastVehicleComponent's setCoordinateSystem(0, 2, 1)), not the camera/FreeCameraController convention (local -Z forward, local Y up) - that convention matches a camera's rest orientation (forward down local -Z), whereas this entity's rest/identity orientation stands with its capsule's long axis along local Z (up), so this.rotation must only ever be a plain rotation around up (e.g. Qtrn.fromAngle(up, yaw)) for the capsule to stay upright - never a camera-style look-at basis change. A driver (e.g. PlayerCharacterController) must map input to moveDirection and compute this.rotation using this same convention (note Pnt3.toSpherical/fromSpherical's own theta is measured from +X, not +Y - converting a look-direction angle into this entity's yaw needs a -90° offset, see PlayerCharacterController.updateCamera's comment for the derivation).

Signature

export declare class CharacterController3dEntity<TypeDoc> {
  constructor(
    options: Partial<CharacterController3dEntityOptions> &
      Pick<CharacterController3dEntityOptions, 'radius' | 'centersDistance'>,
    object3D: TypeDoc['vTypeDoc']['displayObject'] | null,
    characterController: TypeDoc['pTypeDoc']['characterController']
  )
}

updateVisibility (method)

Signature

public updateVisibility(): void

onSpawned (method)

Signature

onSpawned(world: Gg3dWorld<TypeDoc>)

jump (method)

Triggers a jump (a takeoff velocity away from the ground, opposing gravity) only while stably grounded - the same isGrounded && isWalkableGround condition updateMovement uses to decide resting-vs-falling (see its doc), not just the adapter's raw isGrounded alone. Otherwise a character balanced on a too-steep surface (isGrounded === true but sliding, per isWalkableGround) could jump off it as if it were stable footing. A no-op mid-air, and a no-op while grounded on an unwalkably steep surface.

Signature

public jump(): void

updateMovement (method)

Signature

private updateMovement(deltaMs: number): void

tryStandUp (method)

Raycasts straight up from the current (crouched) capsule's top by the extra height standing would need. The ray starts a hair above that top point along up - the capsule's own outward surface normal at that exact point - so it originates just outside the character's own shape and can never register a self-hit, with no collision-group bookkeeping required.

Only ever called while isGrounded (see the updateMovement call site) - not merely as an optimization. A thin ray probe like this one needs some endpoint to start from a point known to be outside every other body, and the only such point this class can derive without a real shape-overlap query (which IPhysicsWorld3dComponent doesn't expose) is "just outside my own capsule" - which only actually holds when the capsule is at rest. While actively rising through a jump, the capsule's own top can end up, on some single tick, closer to a low ceiling than that same tiny margin - not yet blocked by move()'s own sweep (which stops it correctly the very next tick), but already close enough that this ray's start point lands inside the ceiling anyway, and a ray beginning inside a shape never registers an entry hit against it (the same false-negative failure mode as AmmoCharacterControllerComponent's embedded-landing-position pitfall - see gg-engine-physics-adapter-ammo - except this version needs no collision at all, just a close enough natural approach on a single tick, so it isn't fixed by keeping landing positions clean). Regression, found jumping while crouched under a ceiling too low to stand under: on the one tick the rising capsule's top passed within this margin of the ceiling but hadn't yet been blocked by it, the headroom check read "clear" and stood the character up, permanently (nothing re-checks once _isCrouching is already false), clipping the now-tall capsule into the ceiling for the rest of the jump. Swapping the ray's own two endpoints doesn't generally fix this either - a sufficiently thick ceiling can just as easily embed that end instead (confirmed empirically) - so the fix is to never run this check against a position that might still be mid-flight in the first place.

Signature

private tryStandUp(): void

recreateCapsule (method)

Swaps the underlying characterController component for a freshly-created one at a different centersDistance, keeping the character's feet planted in place. Used for crouch/stand transitions instead of resizing a component in place - see ICharacterController3dComponent's doc for why.

Signature

private recreateCapsule(newCentersDistance: number): void

tickOrder (property)

Signature

readonly tickOrder: number

options (property)

Signature

readonly options: Required<CharacterController3dEntityOptions>

moveDirection (property)

Local-space desired move direction (XY plane, "+Y forward / +X right", Z unused); set by an input driver - see this class's doc.

Signature

moveDirection: Readonly<MutablePoint3>

isRunning (property)

Whether to move at walkSpeed * runSpeedMultiplier. Ignored while isCrouching.

Signature

isRunning: boolean

object3D (property)

Signature

object3D: TypeDoc['vTypeDoc']['displayObject'] | null

characterController (property)

Signature

characterController: TypeDoc['pTypeDoc']['characterController']

CharacterController3dEntityOptions (type alias)

Options for a CharacterController3dEntity: the capsule shape/mover tuning from CharacterController3dOptions, plus the gameplay tuning (speed, jump, gravity) this entity owns itself so behavior stays identical across physics backends - see the class doc.

Signature

export type CharacterController3dEntityOptions = CharacterController3dOptions & {
  /** Walking speed, in m/s. Default 4. */
  walkSpeed: number
  /** Multiplier applied to `walkSpeed` while `isRunning`. Default 1.8. */
  runSpeedMultiplier: number
  /** Multiplier applied to `walkSpeed` while `isCrouching`. Default 0.5. */
  crouchSpeedMultiplier: number
  /** Capsule `centersDistance` used while `isCrouching`. Must be smaller than `centersDistance`. */
  crouchCentersDistance: number
  /**
   * Not consumed by this class - carried here purely so an input driver (e.g.
   * `PlayerCharacterController`) can read the crouch key behavior from the same options object
   * used to configure the character itself. `'hold'`: crouch while the key is held, stand up on
   * release (subject to the headroom check above). `'toggle'`: each press flips `isCrouching`.
   * Default `'hold'`.
   */
  crouchMode: 'hold' | 'toggle'
  /** Takeoff vertical speed applied by `jump()`, in m/s, launched along `up` opposing gravity. Default 5. */
  jumpSpeed: number
  /**
   * Downward acceleration integrated while airborne, in m/s², straight along `up` (no horizontal
   * component), **overriding** the world's own `physicsWorld.gravity` for this character. Leave
   * `undefined` (the default) to instead track `physicsWorld.gravity` live every tick, full vector
   * - direction, magnitude, *and* any horizontal component - exactly like it would affect a dynamic
   * rigid body (including live changes via the `gravity` dev-console command). A tilted/non-vertical
   * `physicsWorld.gravity` therefore drags this character sideways while airborne or sliding down a
   * too-steep surface, not just downward - see `CharacterController3dEntity`'s class doc. This
   * character's underlying `characterController` is always a kinematic mover unaffected by the
   * physics engine's own gravity integration (see `ICharacterController3dComponent`'s doc), which is
   * why this entity must read and apply gravity itself rather than relying on the backend to do it -
   * only set this to a number when a character deliberately needs a gravity scale different from the
   * rest of the world (e.g. floatier low-gravity player); a numeric override is always straight down
   * along `up`, with no horizontal drag.
   */
  gravity: number | undefined
  /**
   * How fast horizontal movement can be *steered* while airborne, as a fraction of the current
   * walk/run speed applied per second of acceleration (0..1) - **not** a flat multiplier on speed
   * itself. The horizontal velocity in effect at the moment of leaving the ground (walk or run) is
   * carried through the whole jump/fall arc unchanged as long as `moveDirection`/`isRunning` don't
   * change; this only caps how quickly that carried velocity can be redirected towards a *new*
   * desired direction/speed once airborne (see `updateMovement`'s doc). Default 0.3.
   */
  airControlFactor: number
}