Log inGet started
World

Builtin Library

Branchmain
Branchesmainpublish
Commitdd348ac6c3

README.md

walls

Wall-detection and slide helpers for the character controller. Runs the horizontal pass of a character's step, sweeps the character's own capsule along the movement direction, puts a body that overlaps a wall back outside it, probes a single height with a horizontal ray, and slides a movement vector along a wall normal. No state.

Exports

  • M.move(x: number, y: number, z: number, dx: number, dz: number, height: number, radius: number, skinWidth: number, maxSlopeAngle: number, selfId: string) -> Step: the horizontal pass of one step. Puts a body that overlaps a wall back outside it (pushOut), then carries it along (dx, dz): the body stops skinWidth short of the first wall the step runs into, and the rest of the step slides along that wall and along the next one the slide meets. Returns { dx, dz, pushX, pushZ, blocked, hit }: where the body goes (push included), the push itself, whether the step met a wall and gained no ground towards where it asked to go, and the first wall it met.
  • M.pushOut(x: number, y: number, z: number, height: number, radius: number, skinWidth: number, maxSlopeAngle: number, selfId: string) -> (number, number): the horizontal push that puts a body overlapping a wall back outside it, skinWidth clear of the surface, along the overlap's shortest way out. 0, 0 for a body that overlaps no wall.
  • M.castBody(x: number, y: number, z: number, dirX: number, dirZ: number, height: number, radius: number, skinWidth: number, maxSlopeAngle: number, selfId: string, distance: number?) -> hit?: sweep the capsule spanning the character from the top of the ground band above (x, y, z) (its feet) up to y + height, reaching distance + skinWidth past its own surface (skinWidth alone when distance is omitted). Returns the wall standing across the path, or nil when the path is clear.
  • M.castDirection(x: number, y: number, z: number, dirX: number, dirZ: number, radius: number, skinWidth: number, selfId: string) -> hit? — single horizontal ray of length radius + skinWidth, from whichever sample height the caller passes. The ray passes through the colliders the body carries (selfId and every entity parented under it).
  • M.slideAlongWall(moveX: number, moveZ: number, normalX: number, normalZ: number) -> (number, number) — strip the component of move going into the wall (v - (v . n) * n, XZ only). Returns input unchanged when dot >= 0 or the XZ-normal is effectively zero.

Usage

local Walls = require("@builtin::systems.characterController.characterController.physics.walls")

local step = Walls.move(px, py, pz, vx * dt, vz * dt, 1.8, 0.3, 0.01, 45, selfId)
px, pz = px + step.dx, pz + step.dz

local hit = Walls.castBody(px, py, pz, dirX, dirZ, 1.8, 0.3, 0.01, 45, selfId)
if hit then
    local slideX, slideZ = Walls.slideAlongWall(dx, dz, hit.normal.x, hit.normal.z)
end

Notes

  • move sweeps the whole step, so however long a frame's step is, the body ends outside every wall it walks at, and the next step it asks for starts clear of them, away from a wall it rests against as much as along it.

  • A sweep begun inside a wall answers about the overlap in whichever direction it points. pushOut measures how deep the overlap goes by sweeping the same body back in from outside the wall, so a body placed inside a wall, or shoved into one, walks again on the next step.

  • A collider the body carries, on selfId or on an entity parented under it (a held prop, a trigger volume that travels with the character), is no wall: castBody sweeps through it and pushOut pushes nothing out of it, so a character carrying one stands where it is put and walks.

  • castBody sweeps the whole body, so an opening narrower than 2 * radius stops the character even when nothing stands on the centre line — two cabinets 6 cm apart are a wall, not a doorway.

  • A contact whose surface faces up or down — the ramps the character walks up, to maxSlopeAngle, and anything directly overhead — belongs to the ground and ceiling passes. castBody sweeps on past those, so a body on a ramp still reports the wall ahead of it.

  • castBody starts its sweep above the ground band: a cap of radius resting on ground of angle a reaches radius * (1 / cos(a) - 1) below the surface uphill of it, and a sweep begun inside a surface describes the shortest way out of that overlap instead of the surface standing across the path. The band is measured at maxSlopeAngle, the steepest ground the character walks, and the ground and step passes own everything under it.

  • slideAlongWall normalises the wall normal in XZ; callers don't need to pre-normalise. The Y component of the normal is ignored because slide-along is a horizontal operation.

  • The slide helper returns the input movement unchanged whenever the movement isn't pressing into the wall — there's no "stick to wall" behaviour, only "don't pass through".