Improving the performance of many simultaneous animations

Over the past week, I have been trying to optimise a script for a game I am working on. The game is about getting to the other side of a busy highway by dodging vehicles. This script is responsible for generating the vehicles, animating the vehicles, and then disposing of the vehicles. However, the script isn’t great on performance, and it is imperative that it improves. This is a rough idea of how the script works. Note that the script is client sided:

  • Loops through each vehicle spawn and then creates a loop for each.
  • Continuously spawns a random vehicle after a random amount of time for each spawn. The vehicle is either retrieved from a recycled vehicle or cloned from storage.
  • Vehicle is added to a dictionary.
  • Function connected to a heartbeat event loops through the dictionary and moves each vehicle by the desired amount by setting the CFrame of the PrimaryPart (the other parts of the vehicles are unanchored and welded to the PrimaryPart).
  • When a vehicle reaches the end of the road it is put into a folder in ReplicatedStorage to be recycled.

Here’s the script:

local TS = game:GetService("TweenService")
local MS = game:GetService("MarketplaceService")

local Info = TweenInfo.new(25,Enum.EasingStyle.Linear,Enum.EasingDirection.Out,0,false,0)

local Easy = workspace.CarSpawns.Easy
local Cars = game.ReplicatedStorage.Vehicles.Normal:GetChildren()
local Trucks = game.ReplicatedStorage.Vehicles.Trucks:GetChildren()
local PoliceCars = {}
local Wheels = {}
local FastSpawn = true

local EasySpawnTime1 = 1000
local EasySpawnTime2 = 3000
local HardSpawnTime1 = 750
local HardSpawnTime2 = 1600

local EasySpeed1 = 120
local EasySpeed2 = 160
local HardSpeed1 = 200
local HardSpeed2 = 280

local EasyTruckSpawn = 3
local HardTruckSpawn = 8

local ChangeLaneTime = 5
local CanSwitch = true
local Tweens = {}
local ChangingLanes = 0
local MovingCars = {}
local Plr = game.Players.LocalPlayer
local Range = 800

local CarChances = {}
local PassCarChances = {}
local NormalColours = {
	Color3.fromRGB(255,255,255),
	Color3.fromRGB(21, 46, 92),
	Color3.fromRGB(0,0,0),
	Color3.fromRGB(186, 186, 186),
	Color3.fromRGB(117, 26, 18),
	Color3.fromRGB(229, 202, 122),
	Color3.fromRGB(80, 80, 80),
	Color3.fromRGB(22, 72, 33)
}

local SpecialColours = {
	["Bugatti Chiron '20"] = {
		{
			Secondary = Color3.fromRGB(49, 70, 192),
			Primary = Color3.fromRGB(23, 23, 23)
		},
	},
	["Cadillac V16 '30"] = {
		{
			Primary = Color3.fromRGB(0, 0, 0),
			Secondary = Color3.fromRGB(255, 255, 255)
		},
	},
	["Bugatti Veyron '06"] = {
		{
			Secondary = Color3.fromRGB(168, 0, 0),
			Primary = Color3.fromRGB(23, 23, 23)
		},
	},
	["Lamborghini Countach '90"] = {
		{
			Primary = Color3.fromRGB(70, 9, 107),
		},
	},
	["Volvo FH16 '90"] = {
		{
			Primary = Color3.fromRGB(190, 190, 190),
			Secondary = Color3.fromRGB(233, 233, 233),
		},
		{
			Primary = Color3.fromRGB(190, 190, 190),
			Secondary = Color3.fromRGB(101, 101, 101),
		},
		{
			Primary = Color3.fromRGB(190, 190, 190),
			Secondary = Color3.fromRGB(21, 46, 92),
		},
		{
			Primary = Color3.fromRGB(190, 190, 190),
			Secondary = Color3.fromRGB(33, 33, 33),
		},
	},
	["Dodge Charger '06"] = {
		{
			Primary = Color3.fromRGB(21, 46, 92),
		},
		{
			Primary = Color3.fromRGB(0,0,0),
		},
		{
			Primary = Color3.fromRGB(186, 186, 186),
		},
		{
			Primary = Color3.fromRGB(117, 26, 18),
		},
		{
			Primary = Color3.fromRGB(229, 202, 122),
		},
		{
			Primary = Color3.fromRGB(80, 80, 80),
		},
		{
			Primary = Color3.fromRGB(22, 72, 33),
		}
	},
	["Limousine"] = {}
}

local function Heartbeat(DT)
	for i, v in pairs(MovingCars) do
		if not i.Parent then
			MovingCars[i] = nil
			continue
		end
		local Negative = 1
		if MovingCars[i]["Lane"].Parent.Name == "Easy" then
			Negative = -1
			if MovingCars[i]["Value"] >= i:GetAttribute("Z") then
				MovingCars[i] = nil
				i.Parent = game.ReplicatedStorage.UsedVehicles
				continue
			end
		else
			if MovingCars[i]["Value"] <= i:GetAttribute("Z") then
				MovingCars[i] = nil
				i.Parent = game.ReplicatedStorage.UsedVehicles
				continue
			end
		end
		MovingCars[i]["Value"] -= (MovingCars[i]["Lane"]:GetAttribute("Speed")*DT) * Negative
		if Plr.Character and Plr.Character.PrimaryPart then
			local Magnitude = (Vector3.new(MovingCars[i]["X"],MovingCars[i]["Y"],MovingCars[i]["Value"])-Plr.Character.PrimaryPart.Position).Magnitude
			if Magnitude <= Range then
				i.Parent = workspace.Vehicles
				i.PrimaryPart.CFrame = CFrame.new(MovingCars[i]["PrimaryX"],MovingCars[i]["PrimaryY"],MovingCars[i]["Value"]) * CFrame.Angles(MovingCars[i]["XAngle"],MovingCars[i]["YAngle"],MovingCars[i]["XAngle"])
			else
				i.Parent = game.ReplicatedStorage.VehicleHolder
			end
		end
	end
end

local function GetColours(Car)
	if SpecialColours[Car.Name] then
		if not SpecialColours[Car.Name][1] then
			return
		end
		local Primary = SpecialColours[Car.Name][math.random(1,#SpecialColours[Car.Name])]["Primary"]
		local Secondary = SpecialColours[Car.Name][math.random(1,#SpecialColours[Car.Name])]["Secondary"]
		for i, v in pairs(Car:GetDescendants()) do
			if v.Name == "Hull" or v.Name == "Primary" then
				v.Color = Primary
			end
			if v.Name == "Secondary" then
				v.Color = Secondary
			end
		end
	else
		local Colour = NormalColours[math.random(1,#NormalColours)]
		for i, v in pairs(Car:GetDescendants()) do
			if v.Name == "Hull" or v.Name == "Secondary" then
				v.Color = Colour
			end
		end
	end
end

local function SirenTransparency(Car,Transparency)
	if not Car.Body:FindFirstChild("SirenBlue") then return end
	Car.Body.SirenBlue.Transparency = Transparency
	Car.Body.SirenRed.Transparency = Transparency
	Car.Body.SirenHolder.Transparency = Transparency
	Car.Body.SirenMiddle.Transparency = Transparency
end

local function New(Type,Name,Value,Parent)
	local New = Instance.new(Type)
	New.Name = Name
	New.Value = Value
	New.Parent = Parent
	return New
end

local function GetCar(CarName,Fol)
	local Recycled = false
	local Car
	if game.ReplicatedStorage.UsedVehicles:FindFirstChild(CarName) then
		Recycled = true
		Car = game.ReplicatedStorage.UsedVehicles[CarName]
	else
		Car = Fol[CarName]:Clone()
	end
	return Car,Recycled
end

local function SpawnCar(CarSpawn,Speed,AdjacentLanes)
	local PoliceSpawn = 50
	if CarSpawn.Parent.Name == "Hard" then
		PoliceSpawn = 40
	end
	local Police = math.random(1,PoliceSpawn)
	local TruckSpawn
	if CarSpawn.Parent.Name == "Easy" then
		TruckSpawn = EasyTruckSpawn
	else
		TruckSpawn = HardTruckSpawn
	end
	local Type = math.random(1,TruckSpawn)
	local Truck
	if CarSpawn.Parent.Name == "Easy" then
		if Type == 1 then
			Truck = true
		end
	elseif Type == 1 or Type == 2 or Type == 3 then
		Truck = true
	end
	local Car
	local Chances = CarChances
	if MS:UserOwnsGamePassAsync(game.Players.LocalPlayer.UserId,22882286) or game.Players.LocalPlayer:WaitForChild("Data"):WaitForChild("PassesTrying"):FindFirstChild("Lucky Cars") then
		Chances = PassCarChances
	end
	local Recycled = false
	if Police == 1 then
		local CarName = PoliceCars[math.random(1,#PoliceCars)].Name
		Car,Recycled = GetCar(CarName,game.ReplicatedStorage.Vehicles.Normal)
		SirenTransparency(Car,0)
		for i, v in pairs(Car.Body:GetChildren()) do
			if v.Name == "Hull" and not v:GetAttribute("Police") then
				v.Color = Color3.fromRGB(0,0,0)
			end
		end
	elseif Truck then
		local CarName = Trucks[math.random(1,#Trucks)].Name
		Car,Recycled = GetCar(CarName,game.ReplicatedStorage.Vehicles.Trucks)
	else
		local CarName = Chances[math.random(1,#Chances)].Name
		Car,Recycled = GetCar(CarName,game.ReplicatedStorage.Vehicles.Normal)
		SirenTransparency(Car,1)
	end
	if Police ~= 1 then
		Car:SetAttribute("Police",false)
		GetColours(Car)
	end
	if Truck then
		Type = "Truck"
	else
		Type = "Car"
	end
	local Val
	if Recycled then
		Val = Car.Lane
		Val.Value = CarSpawn
		Car.AdjacentLanes:Destroy()
	else
		local Cframe,Size = Car:GetBoundingBox()
		local Cframe2 = Cframe
		local Size2 = Size + Vector3.new(0,10,0)
		local Cframe3 = Cframe
		local Size3 = Size
		if Type ~= 1 then
			Cframe *= CFrame.new(0,-Size.Y/2,7)
			Cframe2 *= CFrame.new(0,Size.Y+5,0)
		end
		Size = Vector3.new(CarSpawn.Size.X,Size.Y,Size.Z-7)
		local Hitbox = Instance.new("Part")
		Hitbox.CanCollide = false
		Hitbox.Size = Size
		Hitbox.Transparency = 1
		Hitbox.Name = "HitBox"
		Hitbox.CFrame = Cframe
		local Weld = Instance.new("WeldConstraint")
		Weld.Part0 = Hitbox
		Weld.Part1 = Car.PrimaryPart
		Weld.Parent = Car.PrimaryPart
		Hitbox.Parent = Car.Body
		if Type ~= "Truck" or Police == 1 then
			local Jumped = Instance.new("Part")
			Jumped.CanCollide = false
			Jumped.Size = Size2
			Jumped.Transparency = 1
			Jumped.Name = "Jumped"
			Jumped.CFrame = Cframe2
			local Weld = Instance.new("WeldConstraint")
			Weld.Part0 = Jumped
			Weld.Part1 = Car.PrimaryPart
			Weld.Parent = Car.PrimaryPart
			Jumped.Parent = Car.Body
		end
		Val = New("ObjectValue","Lane",CarSpawn,Car)
	end
	AdjacentLanes:Clone().Parent = Car
	Car.Lanes.Name = "AdjacentLanes"
	for i, v in pairs(Car.Wheels:GetChildren()) do
		local Clone
		if Recycled then
			Clone = v.Water
		else
			Clone = game.ReplicatedStorage.Particles.Water:Clone()		
			Clone.Parent = v
		end
		if game.ReplicatedStorage.Values.Weather.Value == "Flood" then
			Clone.Enabled = true
		else
			Clone.Enabled = false
		end
	end
	local Y
	if CarSpawn.Parent.Name == "Easy" then
		Y = -180
	else
		Car:SetAttribute("Hard",true)
		Y = 0
	end
	Car:SetPrimaryPartCFrame(CFrame.new(CarSpawn.Position.X,Car.PrimaryPart.Position.Y,CarSpawn.Position.Z)*CFrame.Angles(0,math.rad(Y),0))
	local Z
	if CarSpawn.Parent.Name == "Easy" then
		Z = 4245.46
	else
		Z = -2986.449
	end
	local Distance = (CarSpawn.Position-Vector3.new(CarSpawn.Position.X,CarSpawn.Position.Y,Z)).Magnitude
	local Time = Distance/Speed
	for i, v in pairs(Car.Body:GetChildren()) do
		if v:IsA("BasePart") then
			if not Recycled then
				if v.Material == Enum.Material.Metal then
					v.Material = Enum.Material.SmoothPlastic
					v.Reflectance = .05
				end
				if v == Car.PrimaryPart then
					v.Anchored = true
				else
					v.Anchored = false
				end
				v.CanCollide = false
			end
			if v.Name == "Lights" and (game.Lighting.ClockTime > 17.5 or game.Lighting.ClockTime < 6.3 or game.ReplicatedStorage.Values.Lights.Value) then
				v.Material = Enum.Material.Neon
				if v:FindFirstChild("Light") then
					v.Light.Enabled = true
				end
			end
		end
	end
	if game.Lighting.ClockTime > 17.5 or game.Lighting.ClockTime < 6.3 or game.ReplicatedStorage.Values.Lights.Value then
		if Car:GetAttribute("Police") then
			local New = Instance.new("ObjectValue")
			New.Value = Car
			New.Parent = game.ReplicatedStorage.Sirens
			game:GetService("Debris"):AddItem(New,Time)
			Car.Body.SirenHolder.Siren:Play()
		end
	end
	if not Recycled then
		local Clone = game.ReplicatedStorage.Sounds[Type]:Clone()
		Clone.PlaybackSpeed = math.random((Clone.PlaybackSpeed - .2)*1000,(Clone.PlaybackSpeed + .2)*1000)/1000
		Clone:Play()
		Clone.Parent = Car.Body.HitBox
		local Clone = game.ReplicatedStorage.Sounds.Water:Clone()
		Clone.PlaybackSpeed = math.random((Clone.PlaybackSpeed - .355)*1000,(Clone.PlaybackSpeed + .35)*1000)/1000
		Clone.Parent = Car.Body.HitBox
	end
	if game.ReplicatedStorage.Values.Weather.Value == "Flood" then
		Car.Body.HitBox.Water:Play()
	end
	MovingCars[Car] = {
		Value = CarSpawn.Position.Z,
		Lane = CarSpawn,
		X = Car.Body.HitBox.Position.X,
		Y = Car.Body.HitBox.Position.Y,
		Z = Car.Body.HitBox.Position.Z,
		PrimaryX = Car.PrimaryPart.Position.X,
		PrimaryY = Car.PrimaryPart.Position.Y,
		PrimaryZ = Car.PrimaryPart.Position.Z,
		XAngle = math.rad(Car.PrimaryPart.Orientation.X),
		YAngle = math.rad(Car.PrimaryPart.Orientation.Y),
		ZAngle = math.rad(Car.PrimaryPart.Orientation.Z),
	}
	Car:SetAttribute("Z",Z)
	Car.Parent = workspace.Vehicles
end

for i, v in pairs(game.ReplicatedStorage.Vehicles.Normal:GetChildren()) do
	for x = 1,v:GetAttribute("Chance"),1 do
		table.insert(CarChances,v)
	end
end

for i, v in pairs(game.ReplicatedStorage.Vehicles.Normal:GetChildren()) do
	local Num = v:GetAttribute("Chance")
	if Num ~= 10000 then
		Num *= 2
	end
	for x = 1,Num,1 do
		table.insert(PassCarChances,v.Name)
	end
end

for i, v in pairs(Cars) do
	if v:GetAttribute("Police") then
		table.insert(PoliceCars,v)
	end
end

repeat wait(.1) until #Easy:GetChildren() == 5

local Tweens = {}

for i, v in pairs(workspace.CarSpawns:GetChildren()) do
	for x, y in pairs(v:GetChildren()) do
		local Z
		if v.Name == "Easy" then
			Z = -2986.449
		else
			Z = 4245.46
		end
		local Tween = TS:Create(y,Info,{Position = Vector3.new(y.Position.X,y.Position.Y,Z)})
		Tween:Play()
		table.insert(Tweens,Tween)
		local Speed1
		local Speed2
		if v.Name == "Easy" then
			Speed1 = EasySpeed1
			Speed2 = EasySpeed2
		else
			Speed1 = HardSpeed1
			Speed2 = HardSpeed2
		end
		local Speed = math.random(Speed1,Speed2)
		y:SetAttribute("Speed",Speed)
		spawn(function()
			while true do
				local SpawnTime1
				local SpawnTime2
				if v.Name == "Easy" then
					SpawnTime1 = EasySpawnTime1
					SpawnTime2 = EasySpawnTime2
				else
					SpawnTime1 = HardSpawnTime1
					SpawnTime2 = HardSpawnTime2
				end
				if FastSpawn then
					SpawnTime1 /= 2
					SpawnTime2 /= 2
				end
				wait(math.random(SpawnTime1,SpawnTime2)/1000)
				SpawnCar(y,y:GetAttribute("Speed"),y.Lanes)
			end
		end)
	end
end

game:GetService("RunService").Heartbeat:Connect(Heartbeat)

wait(25)
for i, v in pairs(Tweens) do
	v:Destroy()
	v = nil
end
Tweens = nil
FastSpawn = false