How can I improve the performance of this raycast rain script?

Hey. So I have been trying to make a realistic rain system (which collisions and splash particles). Currently, the framerate drops a bit when using this system (compared to just using particles as rain)

How can improve this?

local RainRadiusX = 80 -- Left, Right
local RainRadiusZ = 70 -- Forward, Back
local MaxRainDistance = 800 -- How far the drops can travel
local RainSpeed = 300
local Rate = 15
local CurrentMaxRainCount = 400 -- Stop the rain from creating more drops than it can destroy.
local RainOffsetZ = 30

local Params = RaycastParams.new()
Params.FilterType = Enum.RaycastFilterType.Whitelist

local Gamesettings = UserSettings().GameSettings
local RS = game:GetService("RunService")
local TweenService = game:GetService("TweenService")
local Terrain = workspace.Terrain


local RainPartsFolder = Instance.new("Folder")
RainPartsFolder.Name = "RainContent"
RainPartsFolder.Parent = workspace

local RainPartTemplate = Instance.new("Part")
RainPartTemplate.Material = Enum.Material.SmoothPlastic
RainPartTemplate.Color = Color3.fromRGB(255, 255, 255)
RainPartTemplate.Size = Vector3.new(0.1, 5, 0.1)
RainPartTemplate.Transparency = 0.9
RainPartTemplate.CanCollide = false
RainPartTemplate.CanTouch = false
RainPartTemplate.CastShadow = false
RainPartTemplate.Anchored = true
RainPartTemplate.Name = "RainPart"

local SplashPart = Instance.new("Part")
SplashPart.Size = Vector3.new(0.1, 0.1, 0.1)
SplashPart.Transparency = 1
SplashPart.CanCollide = false
RainPartTemplate.CastShadow = false
SplashPart.Anchored = true
SplashPart.CanTouch = false
SplashPart.Name = "RainSplashPart"
SplashPart.Parent = RainPartsFolder

local SplashParticle = script.SplashEffect:Clone()
SplashParticle.Parent = SplashPart

local AbsorbantMaterials = { -- Materials that don't create the rain splash effect
	Enum.Material.Grass,
	Enum.Material.Fabric,
	Enum.Material.Sand,
}

local DeltaTime = 0
local RainDeltaIncrement = RainSpeed

local WhiteListIndex = 0
local Whitelist = {}

local function SetupWhitelist(InstanceToSearch)
	for i, child in ipairs(InstanceToSearch:GetChildren()) do
		if child:IsA("BasePart") and child.CanCollide and child.Transparency < 1 then
			WhiteListIndex = WhiteListIndex + 1
			table.insert(Whitelist, WhiteListIndex, child) -- Add the part to the whitelist
		else
			SetupWhitelist(child) -- Continue searching for baseparts
		end
	end
end

function CastRain()
	Params.FilterDescendantsInstances = Whitelist
	if #RainPartsFolder:GetChildren() <= CurrentMaxRainCount then
		local RainOrigin = (game.Workspace.CurrentCamera.CFrame * CFrame.new(0, 0, -RainOffsetZ)).Position + Vector3.new(0, math.random(100, 120), 0)
		local RandomPosition = RainOrigin + Vector3.new(math.random(-RainRadiusX/2, RainRadiusX/2), 0, math.random(-RainRadiusZ/2, RainRadiusZ/2))
		local Direction = Vector3.new(0, -MaxRainDistance, 0)
		local RainRay = workspace:Raycast(RandomPosition, Direction, Params)
		
		if RainRay then
			local HitPos = RainRay.Position
			
			local Dist = (RainOrigin - HitPos).Magnitude
			
			-- Create the rain drop
			local RainPart = RainPartTemplate:Clone()
			RainPart.Position = RandomPosition
			RainPart.Parent = RainPartsFolder
			
			-- Move the rain drop until it hits the surface
			repeat
				RS.RenderStepped:Wait()
				RainPart.Position = RainPart.Position - Vector3.new(0, RainDeltaIncrement, 0)
				
			until RainPart.Position.Y <= HitPos.Y
			
			-- Create the splash effect
			if not table.find(AbsorbantMaterials, RainRay.Instance.Material) and RainRay.Instance.CanCollide then
				SplashPart.Position = HitPos
				SplashParticle:Emit(1)
			end
			
			RainPart:Destroy()
		end
	end
end

local function CheckQuality()
	local Quality = Gamesettings.SavedQualityLevel.Value
	if Quality == 10 then
		Rate = 15
	elseif Quality >= 8 then
		Rate = 10
	elseif Quality >= 5 then
		Rate = 6
	else
		Rate = 3
	end
end

Gamesettings.Changed:Connect(CheckQuality)

RS.RenderStepped:Connect(function(step)
	for i = 1, Rate do
		CastRain()
		RainDeltaIncrement = RainSpeed * step
		DeltaTime = step
	end
end)

SetupWhitelist(workspace)
CheckQuality()

You can start by not using RenderStepped to perform rather expensive code. The number one advice I find myself giving developers working with RunService is to never use RenderStepped unless something you need to do should happen before frames render. Rain simulation is not one such case. Use Heartbeat instead, or Stepped if you’re relying on this system finishing before physics simulation.

Read: RenderStepped

RenderStepped does not run in parallel to Roblox’s rendering tasks and code connected to RenderStepped must be executed prior to the frame being rendered. This can lead to significant performance issues if RenderStepped is used inappropriately. To avoid this, only use RenderStepped for code that works with the camera or character . Otherwise, RunService.Heartbeat should be used.

1 Like

It should not be necessary to use the RenderStepped event to only detect whether a rain drop should be spawned or not. Instead, you should keep track of your rain drops, so that you do not have to check if a new one should be spawned on RenderStepped.

Raycasting is quite expensive in terms of performance. I think the rain would be more performant if you use client-sided Parts as rain drops. This way, they do not lag since they run on the client and you can detect if it splashed on the ground by using the Touched event.

Furthermore, you can improve the performance of the rain drop Parts by disabling CastShadow, CanTouch and CanCollide if possible.

1 Like

You could also try to just create rainparts once and then use the same parts over and over again instead of destroying them and then creating them again. It can help with performance

2 Likes