CameraController (client): - Scriptable chase cam only while in the DriveSeat; restores default on exit - Frame-rate independent damped follow with look-ahead aim point - Flat horizon (ignores car pitch/roll) so bumps don't shake the view - Speed-based FOV kick (quadratic ramp) for sense of pace - Cleans up on Died/despawn MapBuilder v2: - Highway extended 2400 -> 3200 studs with distinct feature zones - Gantry arches (seg 2), enclosed neon-lit tunnel (seg 3), jump ramp + landing pair on the right lane (seg 4, left lane stays flat) - Procedural neon skyline towers flanking both sides (deterministic seed, same city every server) - End kill-wall so you can't sail off the far edge
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
--!strict
|
||||
--[[
|
||||
CameraController.luau (CLIENT)
|
||||
Chase camera for driving. Replaces the default Roblox camera ONLY while the
|
||||
local player sits in a DriveSeat; hands the camera straight back when they
|
||||
get out.
|
||||
|
||||
Feel decisions (tunables at the top):
|
||||
- The camera chases a point behind/above the car with critically-damped
|
||||
smoothing (frame-rate independent lerp), so it lags into corners and
|
||||
catches up on straights — classic arcade racer feel.
|
||||
- It looks ahead of the car (not at it), which reads speed better.
|
||||
- FOV widens with speed for a cheap but very effective sense of pace.
|
||||
- Yaw follows the car's facing, but the position smoothing means drifts
|
||||
and slides read visually (car rotates inside the frame first).
|
||||
]]
|
||||
|
||||
local Players = game:GetService("Players")
|
||||
local RunService = game:GetService("RunService")
|
||||
local Workspace = game:GetService("Workspace")
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
local Shared = ReplicatedStorage.Shared
|
||||
local Chassis = require(Shared.Chassis)
|
||||
|
||||
local player = Players.LocalPlayer
|
||||
|
||||
local CameraController = {}
|
||||
|
||||
-- Tunables --------------------------------------------------------------------
|
||||
local CAM_DISTANCE = 24 -- studs behind the car
|
||||
local CAM_HEIGHT = 9 -- studs above the car
|
||||
local LOOK_AHEAD = 14 -- aim this far in front of the car
|
||||
local POSITION_RESPONSIVENESS = 6 -- higher = tighter follow (1/sec)
|
||||
local BASE_FOV = 70
|
||||
local MAX_FOV_KICK = 18 -- extra FOV at (chassis) top speed
|
||||
local FOV_RESPONSIVENESS = 3
|
||||
|
||||
-- State -------------------------------------------------------------------------
|
||||
local renderConn: RBXScriptConnection? = nil
|
||||
local seatedConn: RBXScriptConnection? = nil
|
||||
local savedCameraType: Enum.CameraType? = nil
|
||||
local savedFov: number? = nil
|
||||
|
||||
-- Frame-rate independent smoothing factor: fraction of remaining distance
|
||||
-- covered this frame. alpha = 1 - e^(-k*dt).
|
||||
local function smoothAlpha(k: number, dt: number): number
|
||||
return 1 - math.exp(-k * dt)
|
||||
end
|
||||
|
||||
local function startChase(seat: VehicleSeat)
|
||||
local model = seat:FindFirstAncestorOfClass("Model")
|
||||
local body = model and model.PrimaryPart
|
||||
if not body then
|
||||
return
|
||||
end
|
||||
local bodyPart = body :: BasePart
|
||||
|
||||
local camera = Workspace.CurrentCamera
|
||||
savedCameraType = camera.CameraType
|
||||
savedFov = camera.FieldOfView
|
||||
camera.CameraType = Enum.CameraType.Scriptable
|
||||
|
||||
-- Start from wherever the car is so there's no first-frame snap.
|
||||
local camPos = bodyPart.Position + bodyPart.CFrame.LookVector * -CAM_DISTANCE + Vector3.new(0, CAM_HEIGHT, 0)
|
||||
|
||||
renderConn = RunService.RenderStepped:Connect(function(dt)
|
||||
if not bodyPart.Parent then
|
||||
return -- car despawned; SeatPart signal will clean us up
|
||||
end
|
||||
|
||||
local look = bodyPart.CFrame.LookVector
|
||||
local flatLook = Vector3.new(look.X, 0, look.Z)
|
||||
if flatLook.Magnitude < 0.01 then
|
||||
flatLook = Vector3.new(0, 0, -1)
|
||||
else
|
||||
flatLook = flatLook.Unit
|
||||
end
|
||||
|
||||
-- Where the camera WANTS to be: behind and above, following flat yaw
|
||||
-- (ignoring car pitch/roll so bumps don't shake the horizon).
|
||||
local desired = bodyPart.Position - flatLook * CAM_DISTANCE + Vector3.new(0, CAM_HEIGHT, 0)
|
||||
camPos = camPos:Lerp(desired, smoothAlpha(POSITION_RESPONSIVENESS, dt))
|
||||
|
||||
local lookTarget = bodyPart.Position + flatLook * LOOK_AHEAD
|
||||
|
||||
camera.CFrame = CFrame.lookAt(camPos, lookTarget)
|
||||
|
||||
-- Speed-based FOV kick.
|
||||
local speed = bodyPart.AssemblyLinearVelocity.Magnitude
|
||||
-- Normalise against a nominal fast top speed; Chassis gives per-car
|
||||
-- values but a fixed reference keeps slow cars feeling slow.
|
||||
local speedFrac = math.clamp(speed / 190, 0, 1)
|
||||
local targetFov = BASE_FOV + MAX_FOV_KICK * speedFrac * speedFrac
|
||||
camera.FieldOfView = camera.FieldOfView + (targetFov - camera.FieldOfView) * smoothAlpha(FOV_RESPONSIVENESS, dt)
|
||||
end)
|
||||
end
|
||||
|
||||
local function stopChase()
|
||||
if renderConn then
|
||||
renderConn:Disconnect()
|
||||
renderConn = nil
|
||||
end
|
||||
local camera = Workspace.CurrentCamera
|
||||
if camera then
|
||||
camera.CameraType = savedCameraType or Enum.CameraType.Custom
|
||||
camera.FieldOfView = savedFov or BASE_FOV
|
||||
end
|
||||
savedCameraType = nil
|
||||
savedFov = nil
|
||||
end
|
||||
|
||||
function CameraController.init()
|
||||
local function onCharacter(character: Model)
|
||||
local humanoid = character:WaitForChild("Humanoid") :: Humanoid
|
||||
if seatedConn then
|
||||
seatedConn:Disconnect()
|
||||
end
|
||||
seatedConn = humanoid:GetPropertyChangedSignal("SeatPart"):Connect(function()
|
||||
stopChase()
|
||||
local seatPart = humanoid.SeatPart
|
||||
if seatPart and seatPart:IsA("VehicleSeat") and seatPart.Name == Chassis.SEAT_NAME then
|
||||
startChase(seatPart)
|
||||
end
|
||||
end)
|
||||
-- Character died/respawned while driving: make sure the camera is back.
|
||||
humanoid.Died:Connect(stopChase)
|
||||
end
|
||||
|
||||
if player.Character then
|
||||
onCharacter(player.Character)
|
||||
end
|
||||
player.CharacterAdded:Connect(onCharacter)
|
||||
end
|
||||
|
||||
return CameraController
|
||||
@@ -16,8 +16,10 @@ local Shared = ReplicatedStorage.Shared
|
||||
local Remotes = require(Shared.Remotes)
|
||||
local Cars = require(Shared.Cars)
|
||||
local ChassisController = require(script.ChassisController)
|
||||
local CameraController = require(script.CameraController)
|
||||
|
||||
ChassisController.init()
|
||||
CameraController.init()
|
||||
|
||||
local player = Players.LocalPlayer
|
||||
local playerGui = player:WaitForChild("PlayerGui")
|
||||
|
||||
Reference in New Issue
Block a user