Builtin Library
README.md
characterController
Physics-based character controller — capsule movement with
ground / wall / ceiling detection. A kinematic controller using
raycasts for collision; handles gravity, jumping, slope traversal,
step climbing, wall sliding, coyote time, and moving platforms. The
module owns a per-entity registry; attach once, call move / jump
each frame, then drive a per-frame update(dt). The
install / uninstall / preset surface wires up the standard set
of components (CharacterController + Locomotion + MovementState +
optional WASDInput and third-person camera child) in one call.
Require it to use the surface:
local CharacterController = require("@builtin::systems.characterController.characterController")
Exports
CC.attach(entityId: string, userConfig: Config?)— register an entity, merge user overrides overDefaults.standard.CC.detach(entityId: string)— drop the registry entry.CC.move(entityId: string, direction: Vec3)— set per-frame world-space movement input (yis ignored).CC.jump(entityId: string)— request a jump; honours coyote time and jump buffer.CC.getState(entityId: string) -> State— snapshot copy of grounded / falling / velocity / etc.CC.getVelocity(entityId: string) -> (number, number, number)— currentvx, vy, vz.CC.setGravity(entityId: string, gravity: number)— update gravity at runtime.CC.setMaxSlope(entityId: string, degrees: number)— update max walkable slope angle.CC.setConfig(entityId: string, key: string, value: any)— update a single config key.CC.getConfig(entityId: string) -> Config— config copy.CC.update(entityId: string, dt: number)— advance one frame (resolve movement, write position, tick state machine).dtis clamped to 0.1.CC.isAttached(entityId: string) -> boolean— whether the entity has a registration.CC.install(entityId: string, opts: InstallOpts?) -> InstallResult— high-level wire-up (components + optional camera / mesh / WASDInput).CC.uninstall(entityId: string) -> UninstallResult— remove everythinginstalladded.CC.preset(entityId: string, presetName: string) -> PresetResult— apply a preset to an already-installed character in place.
Types:
Vec3 = { x: number, y: number, z: number }Config = { height?, radius?, maxSlopeAngle?, stepHeight?, gravity?, jumpForce?, coyoteTime?, jumpBuffer?, maxJumps?, airControlFactor?, groundSnapDistance?, skinWidth?, moveSpeed? }State = { grounded, falling, sliding, wallContact, wallNormal, velocity, groundNormal, slopeAngle, jumpCount, timeSinceGrounded, timeSinceWallContact, onMovingPlatform, platformEntity? }BundleRef = { guid?, __ref?, identity?, path?, name? }InstallOpts = { preset?, camera?, input?, mesh?, overrides? }InstallResult = { entityId, cameraId?, meshHostId? }UninstallResult = { entityId, despawned }PresetResult = { entityId, preset, applied }
Usage
local CharacterController = require("@builtin::systems.characterController.characterController")
-- Low-level: manual attach + per-frame drive.
CharacterController.attach(entityId, { gravity = -15, jumpForce = 7 })
CharacterController.move(entityId, { x = 1, y = 0, z = 0 })
CharacterController.update(entityId, dt)
-- High-level: one-call wire-up.
CharacterController.install(entityId, {
preset = "humanoid_default",
camera = true,
overrides = { jumpForce = 8 },
})
Notes
attachmerges user config overDefaults.standard; passing unknown keys is allowed (they survive into the config table).updateclampsdtto 0.1s to prevent capsule tunnelling on lag spikes.getStateandgetConfigreturn shallow copies — mutating the result does not affect the controller. The returnedvelocity/wallNormal/groundNormalare also shallow copies.installis idempotent per component — re-running on an already wired entity adds nothing new.uninstallremoves the components and any third-person-camera child that targets this entity.presetmutates fields in place and never adds or removes components — callinstallfirst.