Builtin Library
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); convenienceapplyToTransformwrites directly.M.vehicle(opts: VehicleOpts?) -> VehicleController: driving intent for a ground vehicle, read fromopts.inputMap(an inputMap ref or its path;@builtin::inputMaps.vehicleby 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).:startactivates the map, which stands the walking scheme down for as long as it is live;:stopreleases it. TheVehicleControllercomponent drives a car'sWheelColliders 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:starttook 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;entityis the only required field.VehicleOpts:{ inputMap?, label? }; every field is optional.FreeController,OrbitController,FpsController— opaque (typed asanyso 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:updateearly-outs whenZin.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 withinvertY = true). Pitch is clamped to[-π/2 + ε, π/2 - ε]. FpsControllerproduces intent — gravity, collision, and the jump impulse are the host's responsibility. TheconsumeJumpRequest()accessor returnstrueonce per press, so the physics step can fire its impulse exactly once.- Free / FPS request pointer-lock at
:start(opt out withrequestPointerLock = false) and only release it on:stopwhen they were the ones who acquired it.