Log inGet started
World

Builtin Library

Branchmain
Branchesmainpublish
Commit3bb1da2647

README.md

controllers

Reusable controller factories on top of the Zin.actions handler, Zin.context and Zin.state surfaces. Each factory returns a stateful controller object with the :start / :stop / :update / :isActive lifecycle and a small set of intent readers. free, orbit and fps read keyboard and mouse, and their gamepadIndex opt is accepted and ignored. vehicle reads an input map, so every control answers to keyboard, gamepad and touch.

Exports

Factories (module-level):

  • M.free(opts: FreeOpts) -> FreeController — free-fly designer/debug camera (WASD plane + Q/E vertical + Shift sprint + mouse-look). Writes the entity transform directly.
  • M.orbit(opts: OrbitOpts) -> OrbitController — orbit camera around a target (drag-rotate, scroll-zoom, optional pan).
  • M.fps(opts: FpsOpts) -> FpsController — first-person walker. Produces motion intent for the host (getDesiredVelocity, consumeJumpRequest, isSprinting, isCrouching); convenience applyToTransform writes directly.
  • M.vehicle(opts: VehicleOpts?) -> VehicleController: driving intent for a ground vehicle, read from opts.inputMap (an inputMap ref or its path; @builtin::inputMaps.vehicle by default): getThrottle (-1..1, positive forward), getSteer (-1..1, positive right), getBrake (0..1), isHandbrakeHeld, isBoosting, consumeRecoverRequest (true once per press), consumeZoom (wheel notches since the last read). :start activates the map, which stands the walking scheme down for as long as it is live; :stop releases it. The VehicleController component drives a car's WheelColliders from this intent.

Controller-object methods (returned by the factories; called with :):

  • :start() — define actions, push context, optionally request pointer-lock.
  • :stop() — drop actions, pop context, release pointer-lock if :start took it.
  • :isActive() -> boolean
  • :update(dt: number) — per-frame; no-op when context doesn't match.
  • :actionNames() -> { string }
  • FreeController: :setMoveSpeed, :setLookSensitivity, :getCameraYaw, :getCameraPitch, :getVelocity.
  • OrbitController: :getDistance, :getYaw, :getPitch, :setDistance, :setTarget.
  • FpsController: :setMoveSpeed, :setLookSensitivity, :getCameraYaw, :getCameraPitch, :getDesiredVelocity, :isSprinting, :isCrouching, :consumeJumpRequest, :applyToTransform.

Types:

  • FreeOpts, OrbitOpts, FpsOpts — opts tables; entity is the only required field.
  • VehicleOpts: { inputMap?, label? }; every field is optional.
  • FreeController, OrbitController, FpsController — opaque (typed as any so the metatable plumbing doesn't fight the LSP).

Usage

local Zin = require("@builtin::modules.zinput")

local cam = Zin.controllers.free({ entity = cameraEntityId })
cam:start()

-- per frame:
Zin.tick(dt)
cam:update(dt)

Notes

  • Each controller's actions are namespaced under actionPrefix (default "free_fly.", "orbit.", "fps."). Two co-existing controllers with overlapping prefixes will trample each other — override the prefix when stacking.
  • The context push/pop pair makes Zin.context.push("ui") cleanly suppress every controller without :stop. The controller's :update early-outs when Zin.context.current() doesn't match.
  • Rotation convention: yaw=0, pitch=0 → (0, 0, -1) (-Z forward). Mouse Δx adds to yaw; mouse Δy subtracts from pitch (invert with invertY = true). Pitch is clamped to [-π/2 + ε, π/2 - ε].
  • FpsController produces intent — gravity, collision, and the jump impulse are the host's responsibility. The consumeJumpRequest() accessor returns true once per press, so the physics step can fire its impulse exactly once.
  • Free / FPS request pointer-lock at :start (opt out with requestPointerLock = false) and only release it on :stop when they were the ones who acquired it.