Moud

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 WheelConstraint in the model and steers the ones with steeringEnabled on. 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 a density under 1.
  • Plane adds wings, a tail and a fin to the primaryPart, sized from the plane's mass so it lifts off at half maxSpeed. 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 the primaryPart'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:

luau
-- 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
end

Raise 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:

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.steer is 1 for right, and a positive steerAngle turns 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 = 3 on 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:

luau
-- 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.3 the hull floats with about a third of it under water. The seat is welded on, so it counts too.
  • The VectorForce sits at the back of the hull, low down, and pushes along the hull's own -Z, forwards, because it is relativeTo = "attachment0".
  • The AeroForce is 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.