Log inGet started
World

Builtin Library

Branchmain
Branchesmainpublish
Commit3bb1da2647

README.md

VehicleController

Drives a car, truck, kart or buggy built from WheelColliders. Put it on the chassis, the dynamic rigid body the wheels sit under, and a driving intent (throttle, steer, brake, handbrake, boost) reaches the wheels. The intent comes from the source controlSource names:

  • input (the default): the player's controls, on keyboard, gamepad and touch alike, from @builtin::inputMaps.vehicle through Zin.controllers.vehicle.
  • script: the intent fields throttle, steer, brake, handbrake and boosting, which code sets: an AI driver, a replay, an autopilot, a test. setIntent sets them and switches the source (see "Driving it from code").

Its flight and its rollovers are physics alone: it flies ballistically off a ramp and stays on its roof after a rollover until the recover control or recover() rights it. Air control and self-righting are arcade assists a field turns on (see "Arcade assists").

It drives the wheels from the physics step (its fixedUpdate): each step it reads what the step left and writes the drive, the brakes and the steering for the next one. So the car answers its intent at the step rate whatever the frame rate, the same on every platform, and under Physics.advance from a held clock as in real time: an acceleration run or a braking distance is measured faster than real time with the intent set before the advance (or changed from a step hook).

ControlKeyboardGamepadTouch
throttleW / Sright / left triggerstick, vertical
steerD / Aleft stickstick, horizontal
brakeSpacesouthbutton
handbrakeCtrleastbutton
boostShiftnorthbutton
recoverRselectbutton
zoomscrollshoulderspinch

What each one does to the vehicle:

  • throttle drives the driven wheels (drive: all, front or rear): directly, the torque fading to nothing at topSpeed, or through an engine and a gearbox when engineCurve is set (see "An engine and a gearbox"). Pulled back while the vehicle rolls forward it brakes; at a stop it reverses, up to reverseTopSpeed. Pushed forward while it rolls backward it brakes too. Under the direct drive, a driven wheel that spins on its own (inertia above 0) is spun no faster than topSpeed at its tread (reverseTopSpeed in reverse, topSpeed times boost while boosting): the drive's rev limit, written to the wheel's maxDriveSpeed. So full throttle spins the wheels up on ice, in a burnout or in the air, and they settle at that tread speed.
  • steer turns the steered wheels, the pair nearest the chassis' forward (its local -Z), at up to maxSteerAngle at rest, narrowing to steerAtTopSpeed at topSpeed, turning at steerRate; with counterSteer set, they turn toward the way the vehicle travels as it slides (see "Holding a slide").
  • brake brakes the wheels, brakeBias of it on the front and the rest on the rear (see "Brakes"); handbrake holds the wheels handbrakeWheels names (the rear pair by default) with handbrakeTorque.
  • boost multiplies the drive torque, the top speed and the driven wheels' spin limit by boost.
  • recover (the control, or recover() from code) sets the chassis back on its wheels where it lay, recoverLift above it, facing the way it faced.
  • zoom changes how far the camera's chase controller trails, when that camera hears no zoom control of its own (a chase camera on its default @builtin::inputMaps.cameraRig map zooms itself).

The component writes motorTorque, maxDriveSpeed, brakeTorque and steerAngle on the wheels, each only when its value changes.

Wheelspin

On wheels with an inertia (see the WheelCollider README, "Spin") the drive spins a wheel faster than the road whenever it asks more than the tyre puts down. The spin is bounded by the controller's top speeds: a 600 kg car on wheels of inertia = 1.2 and radius 0.35, its rear pair driven with the default 1500 N·m from rest, spins its rear treads up to topSpeed (25 m/s) on asphalt and in the air and holds them there, and on forwardFriction = 0.15 still spins them past twice the road speed as it pulls away. The limit holds the drive: a gripping tyre turns its wheel at the speed the road passes under it, so a car rolling faster than topSpeed (downhill, or its rear end lifting over a crest) turns its wheels faster too. A lower topSpeed is a lower rev limit, and while boosting the limit is topSpeed times boost (a boost of 1 keeps it at topSpeed). tractionControl holds the drive short of spinning the wheels (see "Traction control"). Wheels with no inertia roll at the road speed, and their drive is held by their grip.

Brakes

brakeTorque is each wheel's brake at full brake with brakeBias at 0.5, its default. brakeBias moves the brake between the axles: each front wheel brakes with brakeTorque times 2 x brakeBias, and each rear wheel with brakeTorque times 2 x (1 - brakeBias). So 0.5 brakes every wheel with brakeTorque on any vehicle, a trike or a six-wheeler as well, and on a four-wheeled car (or any with as many wheels at the front as at the rear) the front wheels take brakeBias of the whole brake: a bias of 0.65 puts 1.3 times brakeTorque on each front wheel and 0.7 times on each rear one. A road car's bias is 0.6 to 0.7: under a hard stop its weight moves onto the front tyres, and rear wheels braked as hard as the front ones lock first, after which the rear of the car lets go across the road and a brake into a corner spins it. A bias toward the front keeps the rear wheels turning, so the car stops straight and turns while it stops.

antiLock = true adds anti-lock brakes on wheels that spin on their own (an inertia above 0; a wheel without one rolls at the road speed and never locks). While braking above 1 m/s, each wheel is braked with the force its tyre pushes back with at 0.95 of the slip its grip peaks at, read each step from the grip curve its WheelCollider states (Physics.tyreCurve) at the load it carries, on the surface it touches and at the slip angle it runs at, so the wheel settles at that slip and turns as it brakes. A rear wheel takes that share of its tyre's peak in its slip along the road and its slip angle together, so the back of the car keeps the side grip its cornering needs; a front wheel brakes along the road whatever it corners with. The load is the one the tyre carries over the step to come: a falling load goes on falling by the share it fell by over the last step, and a rising one counts at what it was the step before, so the jolt of a bump or a landing does not brake the wheel into a lock. A wheel that slips past twice its peak all the same (its load fell away, it met a slippery patch) has its brake eased to half the torque its tyre pushed back with, and let back on by 2 times itself each second. A wheel off the ground, or touching it with no load on it, is left unbraked. Under an engine drive the clutch is out while the anti-lock brakes, so the engine neither brakes the driven wheels nor turns them. A tyre held short of its peak grips harder than a sliding one (slideGrip, see the WheelCollider README), so the car stops shorter and keeps steering: the 1420 kg rear-driven road car of the regression tests, on default tyres and braked from 25 m/s through its engine and gearbox, stops in about 36 m with no wheel locked above 2 m/s, and in about 40 m across three ridges 0.15 m high, against 31.9 m at its tyres' full grip on a flat floor; the 1200 kg car braked from 20 m/s stops with its wheels turning shorter than on an even brake that locks all four, and braked into a gentle corner with a bias of 0.65 it turns while it stops, where the locked car slides straight on.

Set up a road car with both: a bias toward the front and anti-lock. The anti-lock holds each rear wheel inside the grip its cornering leaves it, so a car braked into a corner keeps its back in line on any split; the bias divides the brake wherever the anti-lock leaves it as asked, on a wheel with no inertia and under a brake lighter than the tyres' grip.

handbrakeWheels names the wheels the handbrake holds: rear (the default, a road car's: locking the rear wheels steps the back end out into a slide), front or all. While it is held under an engine drive the clutch is out, so the locked wheels do not drag the engine down.

-- a road car that stops straight, and a drift car whose handbrake swings the back out
car.component.add("VehicleController", { brakeBias = 0.65, antiLock = true })
drifter.component.add("VehicleController", { drive = "rear", brakeBias = 0.6, handbrakeTorque = 4000 })

Traction control

tractionControl = true holds each driven wheel near the slip its tyre grips best at under the throttle, the way antiLock holds a braked one, on wheels that spin on their own (an inertia above 0). Each step, each driven wheel on the ground takes no more drive than its tyre pushes back with at 0.95 of the slip its grip peaks at, read from the grip curve its WheelCollider states (Physics.tyreCurve) at the load it carries, on the surface it touches and at the slip angle it runs at, so it holds from a standstill up, on tarmac, dirt, grass or ice, and on ground that changes under the wheel. A rear wheel takes that share of its tyre's peak in its slip along the road and its slip angle together, so a car driven out of a corner keeps the side grip its back needs. A wheel that slips past twice its peak all the same (a bump unloads it, it lands spinning) has its drive eased to half the torque its tyre pushed back with, and let back on by 8 times itself each second. It reads the player's controls (keyboard, gamepad and touch alike) and setIntent, and acts whatever the throttle asks; a wheel in the air is left to the drive's rev limit.

  • The direct drive: each driven wheel's motorTorque is held to that torque.
  • Through an engine (engineCurve, or a ShaftNetwork of your own with a part named engine): each driven wheel's share of the engine's torque is measured from the network each step (the torque its links put on the wheel over the torque the engine puts into the drivetrain), so it holds through any gearing, clutch and differentials. The engine's throttle is eased to the torque that drives the wheel with the least room with what its tyre holds, plus the engine's own drag, trimmed toward the slip the wheels run at while it is what holds the drive back (neither the throttle asked for nor the clutch taking up the drive holds it to less). A wheel's share follows each shift of the gearbox until it is measured in the new gear, and the trim starts over off the throttle or on the brake, so every pull-away from a stop holds the drive the way the first one did, whatever gear and speed the car drove at before it. While it eases the throttle the link named clutch carries no more than that torque, so neither the clutch taking up the drive from a standstill nor the engine speed carried into a shift turns the wheels past their grip; a clutch holding an engine that turns faster than the gear it is in after a shift closes the throttle until the engine has come down to it.

driveState().tractionLimited reads whether it held the drive back this tick, what a dashboard's traction light reads. The 1420 kg rear-driven road car of the regression tests (an engine peaking at 452 N·m, six gears, a limited-slip differential), launched at full throttle from rest, reaches 25 m/s on tarmac (friction 1) in 4.7 s with no driven wheel past a slip ratio of 0.35 after its first second, against 5.8 s with its rear wheels spinning to a slip ratio of 8 without it; on dirt (friction 0.65) in 8.2 s against 10.3 s, and on grass (friction 0.45) in 13.5 s against 18.5 s; and it pulls away straight, where without it the wheelspin turns it 19 degrees on dirt and spins it round 118 degrees on grass. The same car on the direct drive at 2500 N·m reaches 25 m/s in 4.9 s against 5.8 s, and driven through an engine network of the author's, in 4.7 s against 5.8 s.

It is off by default, so the throttle drives the wheels as asked: set it false for a burnout, a power slide, a drift car steered on the throttle or a monster truck's wheelspin.

-- a road car that pulls away and stops at the edge of its tyres' grip
car.component.add("VehicleController", {
    drive = "rear", brakeBias = 0.65, antiLock = true, tractionControl = true,
    engineCurve = { {1000, 180}, {4500, 260}, {6500, 220}, {7000, 0} },
})

An engine and a gearbox

With engineCurve set, the throttle drives the wheels through an engine rather than with motorTorque: the controller puts a ShaftNetwork on the chassis (see its README) holding an engine with that torque curve, a clutch, a gearbox and differentials down to the driven wheels, solved with the tyres inside the physics step. The car accelerates with the torque the curve gives at the engine's speed, multiplied by the gear and the final drive, and the gearbox shifts by itself.

car.component.add("VehicleController", {
    drive = "rear",
    engineCurve = { {1000, 180}, {4500, 260}, {6500, 220}, {7000, 0} },  -- rpm, N·m
    gears = { 3.6, 2.2, 1.5, 1.1, 0.9 }, reverseGear = 3.4, finalDrive = 3.9,
    differential = "limited",
})
  • The engine turns with engineInertia (kg·m²) and the torque of engineCurve times the throttle (and boost while boosting). The curve's last point is its rev limit: a curve whose last point still gives torque is cut off 5% past it. engineBraking is the engine's own friction (N·m), always on, so off the throttle in gear the car slows through its wheels.
  • The clutch takes up the drive as the engine revs past the curve's first point, and is in by the time the engine (or the gearbox side of it) turns 30% of the way from there to the rev limit: the car pulls away from a stop with the engine revving, and the engine idles down with the clutch out once the car stops. It is out while the handbrake is held, and carries no more than tractionControl asks the engine for while it eases the throttle.
  • The gearbox starts in first and picks its gear by the road speed: it shifts up once the engine speed the road speed calls for in the gear (the driven wheels rolling at the road speed under them, through the gear and the final drive) passes shiftUpRpm on the throttle, and down once it falls below shiftDownRpm (0, their default, is 90% and 45% of the curve's last rpm), holding each gear 0.4 s. So driven wheels spinning on a full-throttle launch keep it in first, and wheels locked under the brake keep it in the gear the car's speed calls for. It shifts only to a gear the engine would turn in between the two. The throttle pulled back at a stop selects reverse (reverseGear), and forward throttle at a stop first gear again.
  • The differentials split the gearbox's output, through finalDrive, equally between the driven wheels: the front and rear halves first, then each axle's left and right. differential makes them open (the wheel with less grip spins), limited (a torque bias ratio of 3) or locked (the wheels turn together).
  • A driven wheel that states no inertia spins under the engine as a 20 kg wheel of its radius does (10 times its radius squared).

topSpeed still sets the speed the steering lock narrows to steerAtTopSpeed at; motorTorque, reverseTorque and reverseTopSpeed are the direct drive's alone. Changing an engine field, drive or boost builds the drivetrain again; gears, reverseGear and the shift points apply from the next physics step, in the gear the car is in. driveState() reads drivetrain (engine), gear (1 for first, -1 for reverse) and rpm, and the network itself reads through Physics.shaftNetworkState(car.id): the engine part is engine, the links clutch and gearbox.

A ShaftNetwork the author put on the chassis is driven in its place: a network with a part named engine takes the throttle on that part, the gear on its link named gearbox (shifting through gears and reverseGear, with the rev limit and the clutch's take-up read from the engine part's curve) and the clutch on its link named clutch, where it has them. So a drivetrain of your own (a transfer case, a locked rear and an open front differential, a tank's two tracks) keeps the controller's input, intent and shifting.

Holding a slide

The A and D keys steer full lock or none, so a driver on a keyboard cannot give a slide the small, changing opposite lock a stick gives it, and the wheels spring back to straight the moment a key is let go. counterSteer (0..1) turns the steered wheels toward the way the vehicle travels by that share of its slide angle, the angle between its heading and its path over the ground, and adds the driver's steer to it, within maxSteerAngle at any speed (the driver's own lock narrows with speed). At 1, with the keys let go the front wheels point along the path, as a car's front wheels trail it with the wheel let go, and the driver holds the slide by working the keys against it as it comes: W and D while it is narrower than wanted, A while it is wider. On the ground the slide angle is driveState().slideAngle (degrees, positive with the path to the right of the nose; 0 in the air, below 2 m/s and backing up), what a drift score or a tyre sound reads.

-- a drift car driven from the keyboard
car.component.add("VehicleController", { drive = "rear", motorTorque = 2000, counterSteer = 1 })

The 1100 kg rear-driven car of the regression tests (wheels of radius 0.32, inertia 1), with stability off, driven from the keys at 17 m/s into a handbrake slide (D and Ctrl for 0.6 s) and then held with W, D and A decided every 0.1 s from the slide a quarter of a second ahead (its angle plus a quarter of a second of its rate), holds a slide of 14 to 38 degrees for 5 s, gaining speed, its yaw rate within 40 degrees per second from half a second after the handbrake is let go, with counterSteer = 1; with it at 0 the same keys spin the car through 180 degrees at 100 degrees per second. With counterSteer = 1 the same car still spins when its driver decides from the slide as it stands, or a tenth of a second ahead, and when W, or W and D, are held throughout; lifting W as the slide grows, with no steering key, catches the slide at 46 degrees and the car straightens. counterSteer applies to whatever the intent comes from: the keys, a stick, or code. It is 0 by default, which steers only where the driver steers.

An ordinary corner holds a few degrees between heading and path as well, and counterSteer acts on those too. On the same car at 20 m/s a steer of 0.3 turns it at 11 degrees per second with counterSteer = 1 and at 25 with it at 0 (its front wheels at 1.7 degrees against 3.8), while full lock turns it about as fast either way; at 6 m/s the same steer turns it a little faster with it (24 degrees per second against 20). A share below 1 catches a slide less and leaves an ordinary corner's steering closer to the driver's.

Stability

A car whose front and rear tyres grip alike, steered past what they grip with, turns faster than its path can follow; the rear tyres slide past their peak and lose grip while the front ones still push, and the car spins. stability (on by default) watches for that: once the rear tyres' slip, along the road and across it together, is past the slip they grip best at, and while the car turns toward the side the rear slides from faster than its path does (the slide grows), the steered wheels turn no further into that turn than lets the front tyres push with the share of their grip the rear tyres still grip with, times the share of the car's turning its path keeps up with, and never past maxSteerAngle. Read from the grip curves the wheels' WheelColliders state (Physics.tyreCurve) at the slip each runs at, its brake and drive counted, so it holds on any tyre and surface, braking, coasting or on the throttle. Each tyre is read on the surface under it at the step the assist acts, so a wheel crossing onto other ground (a painted terrain's layers included), a surface redefined and a tyre retuned count from that step. The steering is the driver's again once the slide holds steady or closes, so a slide the driver makes with the throttle, or with a pull of the handbrake, and holds with the steer stays theirs; what the assist takes away is the spin. driveState().stabilizing reads whether it held the steering back this tick.

A rear wheel the handbrake holds grips with next to nothing across the road, so while the handbrake is held at speed the assist lets the front turn the car little further than its path: the road car below, held on the handbrake from 25 m/s for 4 s with the wheel at full lock and counterSteer at 0, turns 5 degrees, where with stability off it spins.

The 1420 kg rear-driven road car of the regression tests (an engine and six gears, brakeBias = 0.66, antiLock, counterSteer = 0.6, its full lock 32 degrees at rest narrowing to 5 at 32 m/s) steered at 0.3 to 1 from 25 m/s keeps its slide under 17 degrees at full brake and under 13 coasting (with counterSteer at 0 or 1 as well) and keeps turning; with stability off the same car slides to 93 degrees at full brake and steer 0.8, and to 68 coasting at steer 0.6. The 1100 kg keyboard car of the regression tests, with every default, still steps out into a handbrake slide and holds it at 15 to 19 degrees for 4.8 s on the keys, whether its driver decides from the slide a quarter of a second ahead or from the slide as it stands; with counterSteer = 1 as well it holds it at 15 to 17 degrees for 4 s.

Set it false for a car whose spins or handbrake turns are the game, or to leave the steering wholly to the driver.

Arcade assists

Two assists turn on with a field, for a stunt game, a kart racer or a monster truck show:

  • Air control (airControl above 0): with no wheel on the ground the chassis turns toward level, its nose raised to airPitch by throttle and lowered by reverse or brake, spinning at airSpin where it is steered; airControl is how fast it gets there. 4 is a stunt vehicle's.
  • Self-righting (recoverAfter above 0): lying on its side or roof slower than 1 m/s for recoverAfter seconds sets it back on its wheels, the way recover does. 3 is a stunt vehicle's.
car.component.add("VehicleController", { airControl = 4, recoverAfter = 3 })

Both are 0 by default, so a vehicle added with no fields set is a simulation vehicle. A scene that relied on a vehicle steering in the air or righting itself states airControl = 4 and recoverAfter = 3, the values these fields held before they defaulted to 0.

Driving it from code

setIntent drives the vehicle the way the player's controls do: the drive split, the top speed, the steering lock that narrows with speed and the assists all apply. It takes the keys driveState reports (throttle -1..1 positive forward, steer -1..1 positive right, brake 0..1, handbrake and boost booleans) and switches controlSource to script. The table is the whole intent: a key left out is neutral (0, or false), and the numbers are clamped to their ranges. An unknown key, or a value of the wrong type (handbrake = 1), is refused with an error naming it, and the intent stays as it was. Writing the fields (throttle, steer, brake, handbrake, boosting, controlSource) directly changes one at a time.

-- an AI car that steers toward a target each tick
local car = entity.find("aiCar")
local target = entity.find("waypoint")
local vc = car.component.get("VehicleController")
task.loop(function()
    if not car:isLocal() then return end
    local p, w = car.position, target.position
    local dx, dz = w.x - p.x, w.z - p.z
    local f, r = car.transform.forward, car.transform.right
    local ahead = dx * f.x + dz * f.z   -- metres ahead of the car
    local side = dx * r.x + dz * r.z    -- metres to its right
    vc:setIntent({ throttle = 0.6, steer = math.clamp(2 * side / math.max(math.abs(ahead), 1), -1, 1) })
end)

Set controlSource = "input" to hand a vehicle that holds the controls back to the player; a vehicle that never took them answers them once takeControl() is called. A vehicle added with controlSource = "script" does not take the controls as it wakes, so a player car and a code-driven car drive side by side. A player car switched to script (an autopilot) keeps the controls it holds, so the walking controls stay stood down until releaseControl.

Only the peer that owns the chassis drives it. The intent fields are synced: code sets them on the owning peer (car:isLocal()), the way every synced field is written, every peer reads the same intent, and a peer that takes the vehicle over drives on with it.

Fields

FieldDefaultUnitMeaning
inputMap@builtin::inputMaps.vehicleThe controls it reads.
takeControlOnAwaketrueTake the controls as it wakes; false waits for takeControl().
driveallWhich wheels the throttle drives.
motorTorque1500N·mDrive torque per driven wheel at full throttle.
reverseTorque800N·mDrive torque per driven wheel at full reverse.
brakeTorque3000N·mBrake torque per wheel at full brake, with brakeBias at 0.5.
brakeBias0.50..1Front against rear: each front wheel brakes with 2 x it, each rear one with 2 x (1 - it), times brakeTorque.
antiLockfalseAnti-lock brakes on wheels with an inertia: each braked wheel held just short of its tyre's peak slip, a rear wheel within the grip its cornering leaves it, the clutch out.
tractionControlfalseTraction control on wheels with an inertia: each driven wheel's drive held to what its tyre grips with just short of its peak slip, a rear wheel within the grip its cornering leaves it; an engine's throttle (and its clutch) eased to it.
handbrakeTorque6000N·mBrake torque per wheel the handbrake holds.
handbrakeWheelsrearWhich wheels the handbrake holds: rear, front or all.
topSpeed25m/sForward speed where the direct drive has faded to nothing, the fastest tread speed it spins a driven wheel with an inertia to, and the speed the steering lock narrows to steerAtTopSpeed at.
reverseTopSpeed8m/sReverse speed where the direct drive has faded to nothing, and the fastest tread speed the reverse drive spins a driven wheel with an inertia to.
engineCurve{}rpm, N·mThe engine's torque curve; set, the throttle drives through an engine and a gearbox.
gears{ 3.6, 2.2, 1.5, 1.1, 0.9 }×Forward gear ratios, first gear first.
reverseGear3.4×Reverse gear ratio.
finalDrive3.9×Final drive ratio.
shiftUpRpm0rpmShift up once the engine speed the road speed calls for passes it; 0 is 90% of the curve's last rpm.
shiftDownRpm0rpmShift down once the engine speed the road speed calls for falls below it; 0 is 45% of the curve's last rpm.
engineInertia0.2kg·m²The engine's moment of inertia.
engineBraking20N·mThe engine's friction, always on: its braking off the throttle.
differentialopenopen, limited or locked.
boost1.6×Drive and top speed multiplier while boosting.
maxSteerAngle30degreesFull lock at rest.
steerAtTopSpeed8degreesFull lock at topSpeed.
steerRate150degrees/sHow fast the wheels turn toward the lock asked for.
counterSteer00..1Share of the slide angle the steered wheels turn toward the path by, added to the driver's steer within maxSteerAngle; 1 trails them along the path with the steer let go.
stabilitytrueStability assist: while the rear tyres slide past their grip and the slide grows, the steered wheels turn no further into it than the rear tyres' grip answers; false leaves the steering to the driver and counterSteer.
airControl0rad/s²Air control assist: how fast the chassis turns in the air toward the attitude asked for; 0 leaves its flight to physics.
airPitch20degreesNose angle held in the air at full throttle or full reverse, under air control.
airSpin90degrees/sSpin about the up axis at full steer in the air, under air control.
recoverAfter0sSelf-righting assist: time overturned before it rights itself; 0 waits for the recover control or recover().
recoverLift1mHeight above where it lay that a righted vehicle is set down.
cameraA camera with @builtin::controller.chase that zoom moves, while that camera hears no zoom of its own.
controlSourceinputWhere the intent comes from: input (the player's controls) or script (the intent fields).
throttle0-1..1Intent under script: positive forward, negative reverse.
steer0-1..1Intent under script: positive right.
brake00..1Intent under script: share of brakeTorque.
handbrakefalseIntent under script: the handbrake holds the wheels handbrakeWheels names.
boostingfalseIntent under script: boost held.

Methods

  • takeControl(): the controls go live; the walking controls stand down.
  • releaseControl(): the controls go back; the wheels coast straight.
  • hasControl() -> boolean
  • setIntent({ throttle?, steer?, brake?, handbrake?, boost? }): drive it from code; switches controlSource to script.
  • recover(): set it back on its wheels now.
  • driveState(): { source, throttle, steer, brake, handbrake, boost, speed, groundedWheels, wheels, steerAngle, slideAngle, stabilizing, tractionLimited, overturned, recoveries, lastRecover, drivetrain, gear, rpm }: the intent from the current source; slideAngle is the slide in degrees (see "Holding a slide"); stabilizing and tractionLimited whether stability held the steering back and tractionControl the drive this tick; lastRecover is control, call or overturned; drivetrain is direct or engine, with the gear (1 first, -1 reverse) and the engine's rpm under an engine.

Example

A monster truck: a heavy chassis on four big wheels with long travel, the suspension force raised to carry the landings, with the arcade assists on.

local truck = entity.spawn("truck")
truck.position = { 0, 2.2, 0 }
truck.component.add("Physics", { kind = "dynamic", mass = 2000 })
truck.component.add("BoxCollider", { half = { 1.3, 0.4, 2.2 } })
for _, at in { { -1.5, -1.7 }, { 1.5, -1.7 }, { -1.5, 1.7 }, { 1.5, 1.7 } } do
    local wheel = entity.spawn("wheel", { parent = truck.id })
    wheel.localPosition = { at[1], -0.2, at[2] }
    local tyre = entity.spawn("tyre", { parent = wheel.id })
    tyre.component.add("Model", { model = "cylinder" })
    tyre.localRotation = { 0, 0, 0.7071068, 0.7071068 }
    tyre.localScale = { 1.6, 0.6, 1.6 }
    wheel.component.add("WheelCollider", {
        radius = 0.8, width = 0.6,
        suspensionDistance = 0.8, springRate = 25000, damperRate = 5000,
        maxSuspensionForce = 40000,
        visual = tyre,
    })
end
truck.component.add("VehicleController", {
    motorTorque = 4000, brakeTorque = 8000, topSpeed = 25,
    airControl = 4, recoverAfter = 3,
})

A car the player gets into takes the controls when the driver enters:

car.component.add("VehicleController", { takeControlOnAwake = false })
car.component.get("VehicleController"):takeControl()   -- driver in
car.component.get("VehicleController"):releaseControl() -- driver out