Plane

Vehicles · by Brixborg

A script flies it where the camera looks: W goes, look up to climb. Give it room to take off.

Use it in your games: in the studio, open View > Archive, go to the Vehicles shelf and pick "Plane".

The script

-- Flies the plane. Sit in the seat, then:
--   W          go: it flies the way the CAMERA is looking
--   S          slow down
--   the mouse  hold the right button and look where you want to go:
--              look up to climb, look down to dive, look round to turn
-- Hold W to roll down the runway. At TAKE_OFF speed the wings start to
-- carry it: look up and it climbs. Let go of W and it slows and sinks
-- gently back down: that is how to land.
--
-- It flies the way planes were scripted in 2013:
--   a BodyGyro       points the plane the way it should face
--   a BodyVelocity   moves it the way it is pointing
-- and this script decides, many times a second, what those should be.
--
-- Where the pilot is looking comes from the pilot's own computer: while
-- someone sits in the seat, the PilotView LocalScript (inside this script)
-- is put on their screen, and it sends their camera's direction here
-- through the Look RemoteEvent, many times a second. (In a game with many
-- players, the server has no camera of its own to look through.)
local plane = script.Parent
local body = plane.Fuselage
local seat = plane.DriverSeat
local point = body.BodyGyro
local push = body.BodyVelocity
local propeller = body:FindFirstChild("Motor")   -- the joint that spins the propeller

local TOP_SPEED = 90        -- studs per second at full power
local TAKE_OFF = 40         -- the wings carry the plane at this speed
local POWER_UP = 0.35       -- how fast W adds power (0.35 = full power in 3 seconds)
local POWER_DOWN = 0.5      -- how fast S takes it off
local COASTING = 0.12       -- how fast the power fades with neither key held
local QUICKNESS = 22        -- how fast it gains and loses speed, studs per second each second
local LEVEL_VIEW = 20       -- the camera usually looks DOWN on the plane by about this many
                            -- degrees. That counts as "straight ahead"; above it climbs
local STEEPEST = 45         -- the steepest it will climb or dive, in degrees
local BANK = 40             -- how far it leans over in a sharp turn, in degrees
local TURNING = 1.1         -- how fast it can turn, in radians per second
local SINK = 45             -- how fast it falls with no speed at all
local GENTLE_BELOW = 14     -- this close to the ground...
local GENTLE_SINK = 10      -- ...it sinks no faster than this
local GROUND_BELOW = 4      -- the ground is this far under the middle of the fuselage when it stands on its wheels (and a bit)

local weight = 0
for _, piece in ipairs(plane:GetChildren()) do
  if piece:IsA("BasePart") then weight = weight + piece:GetMass() end
end
local strong = weight * workspace.Gravity

local power = 0      -- 0 = engine off, 1 = flat out
local speed = 0
local look = body.CFrame.lookVector
local heading = math.atan2(-look.X, -look.Z)   -- which way it points, as an angle
local pitch, bank = 0, 0

-- The pilot's view, as their computer last sent it.
local pilotLook = nil
local lookEvent = Instance.new("RemoteEvent")
lookEvent.Name = "Look"
lookEvent.Parent = plane
local pilotView = script:WaitForChild("PilotView")
local pilot, viewScript = nil, nil
seat:GetPropertyChangedSignal("Occupant"):connect(function()
  if viewScript then viewScript:Destroy() viewScript = nil end
  pilot, pilotLook = nil, nil
  local humanoid = seat.Occupant
  local player = humanoid and game:GetService("Players"):GetPlayerFromCharacter(humanoid.Parent)
  if not player then return end
  pilot = player
  viewScript = pilotView:Clone()
  local link = Instance.new("ObjectValue")
  link.Name = "Plane"
  link.Value = plane
  link.Parent = viewScript
  viewScript.Disabled = false
  viewScript.Parent = player:FindFirstChild("PlayerGui")
end)
lookEvent.OnServerEvent:connect(function(player, view)
  if player == pilot and typeof(view) == "Vector3" and view.magnitude > 0 then pilotLook = view.unit end
end)

-- The difference between two angles, the short way round.
local function angleBetween(from, to)
  return (to - from + math.pi) % (2 * math.pi) - math.pi
end

while true do
  local passed = wait()
  local flying = seat.Occupant ~= nil

  -- W adds power, S takes it off, and with neither it slowly fades.
  -- (With nobody flying it, the engine winds down.)
  local throttle = flying and seat.Throttle or -1
  if throttle > 0 then
    power = power + POWER_UP * passed
  elseif throttle < 0 then
    power = power - POWER_DOWN * passed
  else
    power = power - COASTING * passed
  end
  power = math.max(0, math.min(1, power))

  -- Speed creeps towards what this much power gives.
  local wanted = power * TOP_SPEED
  if speed < wanted then
    speed = math.min(wanted, speed + QUICKNESS * passed)
  else
    speed = math.max(wanted, speed - QUICKNESS * passed)
  end

  -- How much of the plane's weight are the wings carrying? (0 to 1)
  local lift = math.min(1, (speed / TAKE_OFF) ^ 2)

  -- Where is the pilot looking?
  local wantedHeading, wantedPitch = heading, 0
  if flying and pilotLook then
    local view = pilotLook
    wantedHeading = math.atan2(-view.X, -view.Z)
    -- Up and down only count once the wings are carrying it.
    if lift >= 1 then
      wantedPitch = math.deg(math.asin(math.max(-1, math.min(1, view.Y)))) + LEVEL_VIEW
      wantedPitch = math.max(-STEEPEST, math.min(STEEPEST, wantedPitch))
    end
  end

  -- Turn towards it, no faster than TURNING (and more slowly when slow),
  -- leaning into the turn.
  local most = TURNING * math.min(1, speed / TAKE_OFF + 0.2) * passed
  local turn = math.max(-most, math.min(most, angleBetween(heading, wantedHeading)))
  heading = heading + turn
  local wantedBank = 0
  if lift >= 1 and passed > 0 then wantedBank = (turn / passed) / TURNING * BANK end
  -- (Lean and tip gradually, not all at once.)
  bank = bank + (wantedBank - bank) * math.min(1, passed * 3)
  pitch = pitch + (wantedPitch - pitch) * math.min(1, passed * 2)

  local facing = CFrame.Angles(0, heading, 0) * CFrame.Angles(math.rad(pitch), 0, math.rad(bank))

  -- Are the wheels on the ground? (Is there anything just underneath?)
  local below = workspace:FindPartOnRay(Ray.new(body.Position, Vector3.new(0, -GROUND_BELOW, 0)), plane)
  local onGround = below ~= nil

  if onGround and power == 0 and speed == 0 then
    -- Parked: let go, and leave it standing on its wheels, with just
    -- enough of a brake to stop it rolling away.
    point.maxTorque = Vector3.new()
    push.velocity = Vector3.new()
    push.maxForce = Vector3.new(strong, 0, strong) * 0.2
    look = body.CFrame.lookVector
    heading = math.atan2(-look.X, -look.Z)
  else
    point.maxTorque = Vector3.new(strong, strong, strong) * 40
    point.cframe = facing
    -- Go the way the nose points, sinking if the wings aren't carrying
    -- everything.
    local sink = (1 - lift) * SINK
    -- Close to the ground it eases off, to touch down gently.
    if workspace:FindPartOnRay(Ray.new(body.Position, Vector3.new(0, -GENTLE_BELOW, 0)), plane) then
      sink = math.min(sink, GENTLE_SINK)
    end
    push.velocity = facing.lookVector * speed - Vector3.new(0, sink, 0)
    if onGround and pitch <= 1 then
      -- Rolling along the ground: push forwards only, and let the wheels
      -- carry the weight.
      push.maxForce = Vector3.new(strong, 0, strong) * 2
    else
      push.maxForce = Vector3.new(strong, strong, strong) * 2
    end
  end

  -- Spin the propeller. (A Motor turns towards DesiredAngle; one that is
  -- very far away never gets there, so it spins for ever.)
  if propeller then
    propeller.MaxVelocity = power * 0.6
    propeller.DesiredAngle = 1e9
  end
end

More vehicles