Vehicles#
A vehicle is an assembly with a VehicleSeat in it: loose
parts joined by welds and joints, and a seat that reads the keys of whoever sits on it. The player in
the seat owns the whole assembly, so their own client simulates it and it answers their keys at once.
The driver is held on the seat in the same frame the car is drawn, camera included, so they never
slide about on it, and the keys reach the seat before scripts step on the server.
See Seats for the seat itself.
Each kind moves by a different engine piece:
| Kind | What moves it | Engine piece |
|---|---|---|
| Car | wheels on springs, driven and steered | WheelConstraint |
| Boat | a hull lighter than water, a push and a keel | floating, VectorForce, AeroForce |
| Plane | wings, a tail and a fin, and a push | AeroForce, VectorForce |
Controller scripts#
The editor has a controller script for each kind. Build the vehicle as a Model with a
VehicleSeat in it, right click the model in the Explorer, and pick Add script > Car, Boat
or Plane. Press Play, walk into the seat and drive. Jump to get out.
Each one is an ordinary Script with its handling numbers at the top. Open it to tune it, or to see
how it works and change it. The rest of the handling comes from the seat, so the same script drives a
go-kart and a truck:
| Seat property | Car | Boat | Plane |
|---|---|---|---|
maxSpeed |
top speed | top speed | top speed; it lifts off at half of it |
torque |
how hard each wheel pushes | how hard the engine pushes | how hard the engine pushes |
turnSpeed |
how fast it turns |
What each one expects:
- Car drives every
WheelConstraintin the model and steers the ones withsteeringEnabledon. It works out each wheel's size and which way round it has to spin, so wheels made on either side with the constraint tool all drive forwards. With nobody in the seat it holds the brakes. - Boat pushes the model's
primaryPart, or the seat if there is none. It adds a push at the back of the hull, a turn, and a keel that stops it sliding sideways. The hull has to float on its own, so give it adensityunder 1. - Plane adds wings, a tail and a fin to the
primaryPart, sized from the plane's mass so it lifts off at halfmaxSpeed. The engine runs while someone is in the seat. Forward pitches the nose down, back pulls it up, and left and right bank and turn. Give it wheels or skids to roll on, and point theprimaryPart's front, its -Z face, the way it flies.
Tuning the car#
The numbers at the top of the Car script set how it handles. Speed and grip stay on the seat.
| In the Car script | Default | |
|---|---|---|
MAX_STEER |
30 | how far the front wheels turn at a standstill, in degrees |
STEER_AT_SPEED |
0.4 | the share of MAX_STEER left at maxSpeed. Lower is steadier at speed, 1 turns as sharply at any speed |
BRAKE |
3 | the brakes' torque, times the seat's torque. Also what holds a parked car |
COAST |
0.05 | how much the wheels drag with the keys let go, times the seat's torque. 0 rolls on for ever |
The script reads the seat every tick, so a server script can change a car while someone drives it. An engine upgrade bought in a garage:
-- server
local ENGINES = {
{ maxSpeed = 16, torque = 40 },
{ maxSpeed = 22, torque = 60 },
{ maxSpeed = 30, torque = 90 },
}
local function fitEngine(car: Model, level: number)
local seat = car:descendants("VehicleSeat")[1] :: VehicleSeat
seat.maxSpeed = ENGINES[level].maxSpeed
seat.torque = ENGINES[level].torque
endRaise torque with maxSpeed. A faster top speed on the same torque only takes longer to reach.
A car in a script#
A chassis, a seat and four wheels, two of them steering, all from server/main.luau:
-- server/main.luau
local world = game.world
world:add("SpawnLocation", { name = "Start", cframe = cframe(0, 64, 10) })
world:add("Part", { name = "Floor", size = vec3(200, 1, 200), cframe = cframe(0, 63.5, 0) })
local car = world:add("Model", { name = "Car" }) :: Model
local chassis = car:add("Part", {
name = "Chassis", anchored = false, size = vec3(3, 0.6, 5.6), density = 3,
cframe = cframe(0, 65.4, 0), color = color(0.85, 0.2, 0.2),
}) :: Part
car.primaryPart = chassis
local seat = car:add("VehicleSeat", {
name = "Seat", anchored = false, size = vec3(1.4, 0.4, 1.4), cframe = cframe(0, 65.9, 0.6),
maxSpeed = 20, torque = 60,
}) :: VehicleSeat
car:add("WeldConstraint", { part0 = chassis, part1 = seat })
local RADIUS = 0.7
local wheels: { WheelConstraint } = {}
for _, spot in { vec3(-1.8, -0.5, -1.9), vec3(1.8, -0.5, -1.9), vec3(-1.8, -0.5, 1.9), vec3(1.8, -0.5, 1.9) } do
local at = cframe(spot.x, spot.y, spot.z)
local wheel = car:add("Part", {
name = "Wheel", anchored = false, shape = "cylinder", size = vec3(0.5, RADIUS * 2, RADIUS * 2),
cframe = chassis.worldCframe * at, density = 2, friction = 1.2, color = color(0.1, 0.1, 0.1),
}) :: Part
table.insert(wheels, car:add("WheelConstraint", {
attachment0 = chassis:add("Attachment", { cframe = at }),
attachment1 = wheel:add("Attachment"),
steeringEnabled = spot.z < 0,
suspensionFrequency = 5, lowerLimit = -0.3, upperLimit = 0.2,
}) :: WheelConstraint)
end
game.stepped:connect(function()
local driven = seat.occupant ~= nil
for _, axle in wheels do
axle.steerAngle = -seat.steer * 30
axle.angularVelocity = -seat.throttle * seat.maxSpeed / RADIUS
axle.motorMaxTorque = if not driven then seat.torque * 3
elseif seat.throttle == 0 then 2
else seat.torque
end
end)- Everything in the car is loose and joined to the chassis, so it is one assembly and the driver owns all of it.
- Each wheel's attachment0 sits in the chassis at the middle of the wheel and keeps the chassis's rotation: X along the axle, Y up. The wheel's own attachment is at its middle, so the wheels start at rest on their springs.
- The front of a part is -Z, so the two wheels at negative Z steer.
seat.steeris 1 for right, and a positivesteerAngleturns left, so it is negated. - A wheel spinning a positive angle about +X rolls towards +Z, so the speed is negated to drive
forwards. Speed over radius is the spin that rolls the car at
maxSpeed. - With nobody in the seat the motors hold the wheels still at three times the torque, which is the parking brake. With the keys let go they hold them only lightly, so the car rolls on.
density = 3on the chassis and 2 on the wheels gives the car some weight to sit on its springs. A car much lighter than its wheels bounces about.
The steering here turns as far at speed as standing still. The Car controller script turns less the faster the car goes, and brakes when you press against the way the car is rolling.
A boat#
A hull with a density under 1 floats on water. What makes it a boat is a push and a keel:
-- server: a hull and a seat, then the push and the keel
local boat = world:add("Model", { name = "Boat" }) :: Model
local hull = boat:add("Part", { name = "Hull", anchored = false, size = vec3(2.4, 0.8, 5), density = 0.3,
cframe = cframe(0, 63.6, 40) }) :: Part
boat.primaryPart = hull
local seat = boat:add("VehicleSeat", { name = "Seat", anchored = false, size = vec3(1.2, 0.3, 1.2),
cframe = cframe(0, 64.15, 41), maxSpeed = 14, torque = 6 }) :: VehicleSeat
boat:add("WeldConstraint", { part0 = hull, part1 = seat })
local thrust = hull:add("VectorForce", {
attachment0 = hull:add("Attachment", { cframe = cframe(0, -0.4, 2.5) }),
relativeTo = "attachment0",
}) :: VectorForce
hull:add("AeroForce", {
attachment0 = hull:add("Attachment", { cframe = cframe.angles(0, 0, math.pi / 2) }),
area = 4, liftCoefficient = 0, dragCoefficient = 0, faceDrag = 1, airDensity = 1000,
})
game.stepped:connect(function()
thrust.force = vec3(0, 0, -seat.throttle * seat.torque * 2)
end)- At
density = 0.3the hull floats with about a third of it under water. The seat is welded on, so it counts too. - The
VectorForcesits at the back of the hull, low down, and pushes along the hull's own -Z, forwards, because it isrelativeTo = "attachment0". - The
AeroForceis turned so its surface faces sideways and given water's density. Water hitting its face, from the side, is pushed against hard; water running along it, from the front, barely at all. That is a keel: the boat goes where it points instead of drifting sideways. - The water itself slows the hull down and stops it spinning, so the boat coasts to a stop when you let go.
Turning is the one thing left out here. The Boat controller script adds a Torque that chases a turn
rate from seat.steer, turning faster the faster the boat goes and the other way round in reverse.
A plane#
A plane is a body with lift on it. Each surface is an AeroForce on an attachment in the body, and
the controls are those attachments turned a few degrees:
| Surface | Where | Faces | Control |
|---|---|---|---|
| wings | either side of the middle | up, a few degrees nose up | tilted against each other to roll |
| tail | at the back | up | tilted to pitch |
| fin | at the back, on top | sideways | turned to yaw |
The wings sit nose up so they lift in level flight. The tail sits behind the centre of mass and lifts nothing in level flight, but as soon as the nose rises the tail meets the air at an angle and pushes the back up again, which is what keeps a plane flying straight. The fin does the same sideways.
The Plane controller script builds all of that on the primaryPart, works out the wing area from
the plane's mass and maxSpeed, and turns the surfaces from the seat's keys every tick. A
VectorForce at the nose pulls it forward while someone is in the seat. Read it to see the numbers,
or start from it for a plane of your own.
When something goes wrong#
- A vehicle that answers every key a round trip late has an anchored part in it. An anchored part keeps the whole assembly on the server. The controller scripts also write on the server, so their changes reach the driver one round trip after the key.
- A driver who moves off without the car is sitting on a seat that is not welded or jointed to the rest. The seat is its own assembly, and the driver owns only the seat.
- A car that bounces about has wheels much heavier or much lighter than the chassis. Keep the chassis a few times heavier than all the wheels together.
