SoundController (client): - Looping engine sound on the car body: pitch 0.7->1.9 tracks speed (curved so low speed feels torquey), volume swells with throttle - Tyre squeal loop faded by lateral slip (velocity along body RightVector) and hard braking from speed; always playing at 0 volume to avoid pops - 3D sounds with rolloff so other players hear cars pass - Asset IDs from safe sources (Roblox Resources engine loop, ProSoundEffects squeal); swap in SOUNDS table after audition Ambience (server): - Atmosphere (purple-grey haze, glare) so neon reads in the air - BloomEffect for neon pop + subtle cool ColorCorrection - Slow ClockTime ping-pong 22:00->03:00 over 10 min: always night, but the sky breathes
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
--!strict
|
||||
--[[
|
||||
SoundController.luau (CLIENT)
|
||||
Engine + tyre audio while the local player drives. Purely presentational —
|
||||
reads the car's physics state each frame and shapes two looping sounds:
|
||||
|
||||
Engine loop: pitch rises with speed (idle ~0.7 -> redline ~1.9), volume
|
||||
swells slightly with throttle. Cheap single-loop "engine", reads
|
||||
surprisingly well with the pitch curve.
|
||||
Tyre squeal: fades in when the car is sliding — lateral velocity across
|
||||
the body's right axis beyond a threshold (drift/hard corner/handbrake
|
||||
moments), or heavy braking while still moving.
|
||||
|
||||
Sounds attach to the car BODY (3D, rolloff), so other players' cars are
|
||||
audible too when their sounds replicate — but this controller only shapes
|
||||
the LOCAL car's sounds (we own its physics).
|
||||
|
||||
Asset notes (both from safe sources):
|
||||
- Engine: "Car-Engine-Loop" by Roblox Resources (official account)
|
||||
- Squeal: "Vehicle Skids Long Heavy Tire Squeal" by ProSoundEffects
|
||||
(Roblox's licensed SFX library)
|
||||
Swap IDs freely in the SOUNDS table below if better loops are found.
|
||||
]]
|
||||
|
||||
local Players = game:GetService("Players")
|
||||
local RunService = game:GetService("RunService")
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
local Shared = ReplicatedStorage.Shared
|
||||
local Cars = require(Shared.Cars)
|
||||
local Chassis = require(Shared.Chassis)
|
||||
|
||||
local player = Players.LocalPlayer
|
||||
|
||||
local SoundController = {}
|
||||
|
||||
local SOUNDS = {
|
||||
Engine = "rbxassetid://80317022777602",
|
||||
Squeal = "rbxassetid://9120399077",
|
||||
}
|
||||
|
||||
-- Tunables --------------------------------------------------------------------
|
||||
local ENGINE_MIN_PITCH = 0.7
|
||||
local ENGINE_MAX_PITCH = 1.9
|
||||
local ENGINE_BASE_VOLUME = 0.35
|
||||
local ENGINE_THROTTLE_SWELL = 0.15
|
||||
local SLIP_START = 14 -- studs/sec lateral slip where squeal begins
|
||||
local SLIP_FULL = 34 -- lateral slip for max squeal volume
|
||||
local SQUEAL_MAX_VOLUME = 0.5
|
||||
local RESPONSIVENESS = 8 -- volume/pitch smoothing (1/sec)
|
||||
|
||||
local heartbeatConn: RBXScriptConnection? = nil
|
||||
local seatedConn: RBXScriptConnection? = nil
|
||||
local engineSound: Sound? = nil
|
||||
local squealSound: Sound? = nil
|
||||
|
||||
local function smoothAlpha(k: number, dt: number): number
|
||||
return 1 - math.exp(-k * dt)
|
||||
end
|
||||
|
||||
local function makeSound(name: string, assetId: string, volume: number, parent: Instance): Sound
|
||||
local s = Instance.new("Sound")
|
||||
s.Name = name
|
||||
s.SoundId = assetId
|
||||
s.Looped = true
|
||||
s.Volume = volume
|
||||
s.RollOffMode = Enum.RollOffMode.InverseTapered
|
||||
s.RollOffMinDistance = 15
|
||||
s.RollOffMaxDistance = 220
|
||||
s.Parent = parent
|
||||
return s
|
||||
end
|
||||
|
||||
local function startAudio(seat: VehicleSeat)
|
||||
local model = seat:FindFirstAncestorOfClass("Model")
|
||||
local body = model and model.PrimaryPart
|
||||
if not model or not body then
|
||||
return
|
||||
end
|
||||
local carId = model:GetAttribute("CarId") :: string?
|
||||
local car = carId and Cars.Get(carId) or nil
|
||||
if not car then
|
||||
return
|
||||
end
|
||||
local tuning = Chassis.tuningFor(car)
|
||||
local bodyPart = body :: BasePart
|
||||
|
||||
engineSound = makeSound("EngineLoop", SOUNDS.Engine, ENGINE_BASE_VOLUME, bodyPart)
|
||||
squealSound = makeSound("TyreSqueal", SOUNDS.Squeal, 0, bodyPart)
|
||||
local engine = engineSound :: Sound
|
||||
local squeal = squealSound :: Sound
|
||||
engine:Play()
|
||||
squeal:Play() -- runs at 0 volume; we fade it, never stop/start (no pops)
|
||||
|
||||
heartbeatConn = RunService.Heartbeat:Connect(function(dt)
|
||||
if not bodyPart.Parent then
|
||||
return
|
||||
end
|
||||
local velocity = bodyPart.AssemblyLinearVelocity
|
||||
local speed = velocity.Magnitude
|
||||
local speedFrac = math.clamp(speed / tuning.MaxSpeed, 0, 1)
|
||||
|
||||
-- Engine: pitch from speed (slight curve so low speeds feel torquey),
|
||||
-- volume swells with throttle input.
|
||||
local pitchTarget = ENGINE_MIN_PITCH + (ENGINE_MAX_PITCH - ENGINE_MIN_PITCH) * (speedFrac ^ 0.8)
|
||||
local volTarget = ENGINE_BASE_VOLUME + ENGINE_THROTTLE_SWELL * math.abs(seat.ThrottleFloat)
|
||||
local a = smoothAlpha(RESPONSIVENESS, dt)
|
||||
engine.PlaybackSpeed += (pitchTarget - engine.PlaybackSpeed) * a
|
||||
engine.Volume += (volTarget - engine.Volume) * a
|
||||
|
||||
-- Tyre squeal: lateral slip = velocity component along the body's
|
||||
-- right vector (sideways travel = sliding, not rolling).
|
||||
local lateral = math.abs(velocity:Dot(bodyPart.CFrame.RightVector))
|
||||
local slipFrac = math.clamp((lateral - SLIP_START) / (SLIP_FULL - SLIP_START), 0, 1)
|
||||
-- Also squeal under hard braking from speed (throttle released fast).
|
||||
local braking = (seat.ThrottleFloat == 0 and speed > tuning.MaxSpeed * 0.5) and 0.25 or 0
|
||||
local squealTarget = math.max(slipFrac, braking) * SQUEAL_MAX_VOLUME
|
||||
squeal.Volume += (squealTarget - squeal.Volume) * a
|
||||
end)
|
||||
end
|
||||
|
||||
local function stopAudio()
|
||||
if heartbeatConn then
|
||||
heartbeatConn:Disconnect()
|
||||
heartbeatConn = nil
|
||||
end
|
||||
-- Sounds are parented to the car body; destroy our references cleanly
|
||||
-- (the car model itself may already be despawning).
|
||||
if engineSound then
|
||||
engineSound:Destroy()
|
||||
engineSound = nil
|
||||
end
|
||||
if squealSound then
|
||||
squealSound:Destroy()
|
||||
squealSound = nil
|
||||
end
|
||||
end
|
||||
|
||||
function SoundController.init()
|
||||
local function onCharacter(character: Model)
|
||||
local humanoid = character:WaitForChild("Humanoid") :: Humanoid
|
||||
if seatedConn then
|
||||
seatedConn:Disconnect()
|
||||
end
|
||||
seatedConn = humanoid:GetPropertyChangedSignal("SeatPart"):Connect(function()
|
||||
stopAudio()
|
||||
local seatPart = humanoid.SeatPart
|
||||
if seatPart and seatPart:IsA("VehicleSeat") and seatPart.Name == Chassis.SEAT_NAME then
|
||||
startAudio(seatPart)
|
||||
end
|
||||
end)
|
||||
humanoid.Died:Connect(stopAudio)
|
||||
end
|
||||
|
||||
if player.Character then
|
||||
onCharacter(player.Character)
|
||||
end
|
||||
player.CharacterAdded:Connect(onCharacter)
|
||||
end
|
||||
|
||||
return SoundController
|
||||
@@ -17,9 +17,11 @@ local Remotes = require(Shared.Remotes)
|
||||
local Cars = require(Shared.Cars)
|
||||
local ChassisController = require(script.ChassisController)
|
||||
local CameraController = require(script.CameraController)
|
||||
local SoundController = require(script.SoundController)
|
||||
|
||||
ChassisController.init()
|
||||
CameraController.init()
|
||||
SoundController.init()
|
||||
|
||||
local player = Players.LocalPlayer
|
||||
local playerGui = player:WaitForChild("PlayerGui")
|
||||
|
||||
Reference in New Issue
Block a user