Skip to content

i-character-controller-2d.component overview


Table of contents


utils

ICharacterController2dComponent (interface)

A capsule-shaped kinematic character controller: a thin, physics-engine-specific "move and collide" primitive - the 2D counterpart of ICharacterController3dComponent (see that interface's doc for the full rationale, repeated here only where the 2D case differs). Unlike a plain rigid body it does not own any gameplay logic (gravity, jumping, walk/run speed) - that lives once, backend-agnostically, in CharacterController2dEntity, which is the class apps/other core code should normally use instead of this interface directly.

Implementations must make move() fully synchronous: by the time it returns, position, isGrounded and groundNormal must already reflect the result of that call, with no dependency on a subsequent IPhysicsWorld2dComponent.simulate() call. This is what lets CharacterController2dEntity integrate gravity/jumping itself and get identical behavior regardless of which physics backend is plugged in, instead of relying on (and diverging on) each native engine's own character-controller/kinematic-body model - relevant here specifically because packages/matter's kinematic_pos/kinematic_vel BodyTypes have no native matter-js equivalent at all (they fall back to a plain static body - see Body2DOptions.kinematic_pos's doc), so this interface's own move() is the only way a matter-js-backed character can be driven at all; there is no shortcut through an ordinary kinematic rigid body for that backend.

removeFromWorld(world, dispose) (inherited from IBodyComponent/IWorldComponent - see their doc for the general contract) must free this component's own native shape/body/collider handles when dispose is true, exactly as ICharacterController3dComponent's doc describes (repeated there in more detail) - this is load-bearing for CharacterController2dEntity's own crouch/stand transition too, which swaps this component wholesale for a freshly-created one at a different centersDistance rather than resizing one in place, and relies on dispose: true to free the outgoing capsule's native handles.

Signature

export interface ICharacterController2dComponent<PTypeDoc extends PhysicsTypeDocRepo2D = PhysicsTypeDocRepo2D>
  extends IBodyComponent<Point2, number, PTypeDoc> {
  /** Capsule radius, as given at creation time. */
  readonly radius: number
  /** Distance between the two capsule hemisphere centers, as given at creation time. */
  readonly centersDistance: number
  /** World "up" direction used to tell the floor from walls/ceilings. */
  up: Point2
  /** Whether the capsule is currently resting on the ground (as of the last `move()` call). */
  readonly isGrounded: boolean
  /** The ground's surface normal, if `isGrounded`; `null` otherwise. */
  readonly groundNormal: Point2 | null

  /**
   * Rigid bodies this character's own collision queries (the sweeps/overlap-recovery behind
   * `move()`) must skip entirely - not just "don't collide", genuinely invisible to this character's
   * queries, as if temporarily removed from the world. See
   * `ICharacterController3dComponent.ignoredBodies`'s doc for the full argument for why this exists
   * (`ownCollisionGroups`/`interactWithCollisionGroups` can't express a single-pair exclusion).
   *
   * A plain mutable `Set`, not a getter/setter pair. Implementations must consult this set fresh on
   * every `move()` call - membership can change between ticks. Empty by default (no exclusions).
   */
  readonly ignoredBodies: Set<PTypeDoc['rigidBody']>

  /**
   * Attempts to move the character by exactly this desired displacement, sliding along
   * obstacles, automatically stepping over ledges up to `maxStepHeight`, and snapping to the
   * ground per `snapToGroundDistance` - see `CharacterController2dOptions`. Fully resolves
   * `position`/`isGrounded`/`groundNormal` before returning (see interface doc).
   *
   * `dt`, when given, is the real time (seconds) `desiredTranslation` was computed to cover -
   * `CharacterController2dEntity` always passes it (its own tick delta). It exists purely so an
   * implementation that pushes dynamic bodies (see `CharacterController2dOptions.pushMass`) can
   * recover the character's actual speed (`desiredTranslation` magnitude / `dt`) rather than
   * working from a per-tick distance alone; a mover that doesn't implement pushing is free to
   * ignore it entirely. See `ICharacterController3dComponent.move()`'s doc for why a mover that
   * *does* push must skip the push (rather than misusing the raw per-tick distance as a speed)
   * whenever `dt` isn't available.
   *
   * Calling this before the component has been added to a world (see `addToWorld`) must be a
   * silent no-op rather than throwing, so backend-agnostic caller code behaves identically
   * regardless of which adapter is plugged in.
   */
  move(desiredTranslation: Point2, dt?: number): void

  clone(): ICharacterController2dComponent<PTypeDoc>
}