vehicle-physics

SKILL.md

Roblox Vehicle Physics Implementation

When implementing vehicle physics, follow these Roblox-specific patterns and constraints.

Car Physics

Basic Setup

-- Use VehicleSeat for driver control
-- Connect wheels with HingeConstraint (ActuatorType = Motor)
-- Use SpringConstraint for suspension
-- CylindricalConstraint for steering column

local function setupWheel(chassis, wheelPart, isSteer, isDrive)
    local attachment0 = Instance.new("Attachment", chassis)
    local attachment1 = Instance.new("Attachment", wheelPart)

    -- Suspension spring
    local spring = Instance.new("SpringConstraint")
    spring.Attachment0 = attachment0
    spring.Attachment1 = attachment1
    spring.Stiffness = 5000  -- Adjust for vehicle weight
    spring.Damping = 500
    spring.FreeLength = 1
    spring.Parent = chassis

    -- Wheel rotation
    local hinge = Instance.new("HingeConstraint")
    hinge.ActuatorType = Enum.ActuatorType.Motor
    hinge.MotorMaxTorque = isDrive and 10000 or 0
    hinge.Parent = wheelPart
end

Critical Implementation Details

  1. Network Ownership: Always SetNetworkOwner(driver) when player enters vehicle
  2. Friction: Set wheelPart.CustomPhysicalProperties with specific friction values
  3. Anti-Roll: Use RodConstraint between left/right suspension attachments
  4. Steering Ackermann: Inner wheel turns sharper than outer: innerAngle = atan(L / (R - W/2))

Gear System

local gearRatios = {3.5, 2.5, 1.8, 1.4, 1.0, 0.8}  -- 6 gears
local finalDrive = 3.7
local maxRPM = 7000

local function calculateWheelTorque(throttle, currentGear, engineRPM)
    local torqueCurve = 1 - ((engineRPM - 4500) / maxRPM)^2  -- Peak at 4500 RPM
    return throttle * baseTorque * torqueCurve * gearRatios[currentGear] * finalDrive
end

Helicopter Physics

Lift and Control

-- Main rotor provides lift proportional to collective (throttle)
-- Cyclic control tilts the rotor disc for directional movement
-- Tail rotor counters main rotor torque

local function updateHelicopter(heli, collective, cyclicPitch, cyclicRoll, pedal)
    local rotorRPM = heli:GetAttribute("RotorRPM")
    local airDensity = 1.225  -- kg/m³ at sea level

    -- Lift = 0.5 * rho * A * Cl * V²
    local liftForce = collective * rotorRPM / 500 * heli.Mass * workspace.Gravity

    -- Apply forces
    local bodyForce = heli:FindFirstChild("LiftForce") or Instance.new("VectorForce", heli)
    bodyForce.Force = Vector3.new(
        cyclicRoll * liftForce * 0.3,
        liftForce,
        cyclicPitch * liftForce * 0.3
    )

    -- Anti-torque from tail rotor
    local tailTorque = pedal * rotorRPM * 10
    local bodyTorque = heli:FindFirstChild("YawTorque") or Instance.new("Torque", heli)
    bodyTorque.Torque = Vector3.new(0, tailTorque, 0)
end

Ground Effect

-- Increase lift when near ground (within 1 rotor diameter)
local function groundEffect(altitude, rotorDiameter)
    if altitude < rotorDiameter then
        return 1 + (1 - altitude/rotorDiameter) * 0.25  -- Up to 25% lift bonus
    end
    return 1
end

Boat Physics

Buoyancy Implementation

-- Sample multiple points below waterline
local buoyancyPoints = {
    Vector3.new(2, 0, 3),   -- Front-right
    Vector3.new(-2, 0, 3),  -- Front-left
    Vector3.new(2, 0, -3),  -- Back-right
    Vector3.new(-2, 0, -3), -- Back-left
    Vector3.new(0, 0, 0),   -- Center
}

local function calculateBuoyancy(hull, waterHeight)
    local totalForce = Vector3.new(0, 0, 0)

    for _, localPos in ipairs(buoyancyPoints) do
        local worldPos = hull.CFrame:PointToWorldSpace(localPos)
        local depth = waterHeight - worldPos.Y

        if depth > 0 then
            -- Buoyancy force proportional to submerged depth
            local force = Vector3.new(0, depth * 1000 * hull.Mass / #buoyancyPoints, 0)
            totalForce = totalForce + force
        end
    end

    return totalForce
end

Wave Response

-- Tilt hull based on wave surface normal
local function getWaveHeight(x, z, time)
    local wave1 = math.sin(x * 0.1 + time) * 2
    local wave2 = math.sin(z * 0.15 + time * 1.3) * 1.5
    return wave1 + wave2
end

Airplane Physics

Lift and Drag

local wingArea = 20  -- m²
local dragCoefficient = 0.02
local liftCoefficient = 0.5

local function calculateAeroForces(velocity, angleOfAttack)
    local speed = velocity.Magnitude
    local dynamicPressure = 0.5 * 1.225 * speed^2

    -- Lift perpendicular to velocity
    local Cl = liftCoefficient * math.sin(2 * angleOfAttack)
    local lift = dynamicPressure * wingArea * Cl

    -- Stall: lift drops sharply past critical angle
    if math.abs(angleOfAttack) > math.rad(15) then
        lift = lift * (1 - (math.abs(angleOfAttack) - math.rad(15)) / math.rad(10))
    end

    -- Drag opposes velocity
    local Cd = dragCoefficient + Cl^2 / (math.pi * 8)  -- Induced drag
    local drag = dynamicPressure * wingArea * Cd

    return lift, drag
end

Motorcycle/Bike Balance

-- Self-stabilization using BodyGyro with low torque
local function setupBikeBalance(bike)
    local gyro = Instance.new("BodyGyro")
    gyro.MaxTorque = Vector3.new(5000, 0, 5000)  -- Only roll and pitch
    gyro.P = 1000
    gyro.D = 100
    gyro.Parent = bike.PrimaryPart

    -- Counter-steering: turn bars opposite to initiate lean
    local function handleSteering(turnInput, speed)
        if speed > 10 then
            -- At speed, steering input causes lean
            local targetLean = turnInput * math.min(speed / 50, 1) * math.rad(45)
            gyro.CFrame = CFrame.Angles(0, 0, targetLean)
        end
    end
end

Train/Rail Following

-- Constrain train to spline path
local function followRail(train, spline, distance)
    local position = spline:GetPositionAtDistance(distance)
    local tangent = spline:GetTangentAtDistance(distance)

    local alignPos = train:FindFirstChild("RailAlign") or Instance.new("AlignPosition", train.PrimaryPart)
    alignPos.Position = position
    alignPos.RigidityEnabled = true

    local alignOri = train:FindFirstChild("RailOrient") or Instance.new("AlignOrientation", train.PrimaryPart)
    alignOri.CFrame = CFrame.lookAt(position, position + tangent)
end

Common Patterns

Network Ownership Transfer

vehicleSeat:GetPropertyChangedSignal("Occupant"):Connect(function()
    local humanoid = vehicleSeat.Occupant
    if humanoid then
        local player = game.Players:GetPlayerFromCharacter(humanoid.Parent)
        if player then
            for _, part in ipairs(vehicle:GetDescendants()) do
                if part:IsA("BasePart") and not part.Anchored then
                    part:SetNetworkOwner(player)
                end
            end
        end
    else
        -- Return to server ownership
        for _, part in ipairs(vehicle:GetDescendants()) do
            if part:IsA("BasePart") then
                part:SetNetworkOwner(nil)
            end
        end
    end
end)

Speedometer/Tachometer

local function getVehicleSpeed(chassis)
    return chassis.AssemblyLinearVelocity.Magnitude * 3.6  -- Convert to km/h
end

local function getRPMFromWheel(hingeConstraint)
    -- Track angle change over time
    local currentAngle = hingeConstraint.CurrentAngle
    local deltaAngle = currentAngle - lastAngle
    local rpm = (deltaAngle / (2 * math.pi)) * 60 / deltaTime
    return math.abs(rpm)
end
Weekly Installs
2
First Seen
Jan 25, 2026
Installed on
windsurf1
opencode1
cursor1
codex1
claude-code1
antigravity1