How can I stop the lag from this raycasting rain script?

Hey all, so I’ve been working on a rain system that uses raycasting, beams, and particles to create a realistic rain effect.
It seems to work pretty well, but after some time of running it, the system starts to cause lag. I have no idea why this is, but I’ve tried all sorts of methods to minimize the lag, such as changing from beams to parts, but nothing seemed to be working.

local RainRadius = 100
local MaxRainDistance = 800
local RainSpeed = 300
local Rate = 10
local CurrentMaxRainCount = 500 -- Stop the rain from creating more drops than it can destroy. (seems to cause lag once the rain has reached this number)

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

local Debris = game:GetService("Debris")
local RS = game:GetService("RunService")
local TweenService = game:GetService("TweenService")
local Terrain = workspace.Terrain

local PartTemplate = Instance.new("Part")
PartTemplate.Size = Vector3.new(0.2, 0.2, 0.2)
PartTemplate.Transparency = 1
PartTemplate.CanCollide = false
PartTemplate.Anchored = true
PartTemplate.Name = "RainSplashPart"

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

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 BlackListIndex = 0
local Blacklist = {}

local function SetupBlacklist(InstanceToSearch)
	for i, child in ipairs(InstanceToSearch:GetChildren()) do
		if child:IsA("BasePart") and (not child.CanCollide or child.Transparency == 1) then
			BlackListIndex = BlackListIndex + 1
			table.insert(Blacklist, BlackListIndex, child) -- Add the part to the blacklist
		else
			SetupBlacklist(child) -- Continue searching for baseparts
		end
	end
end

function CastRain()
	Params.FilterDescendantsInstances = Blacklist
	if #RainPartsFolder:GetChildren() <= CurrentMaxRainCount then -- Stop the rain from creating too many instances (seems to create lag when this statement becomes false)
		local RainOrigin = game.Workspace.CurrentCamera.CFrame.Position + Vector3.new(0, math.random(100, 120), 0)
		local RandomPosition = RainOrigin + Vector3.new(math.random(-RainRadius/2, RainRadius/2), 0, math.random(-RainRadius/2, RainRadius/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 RainAttachment1 = Instance.new("Attachment")
			RainAttachment1.WorldPosition = RandomPosition + Vector3.new(0, 3, 0)
			local RainAttachment2 = Instance.new("Attachment")
			RainAttachment2.WorldPosition = RandomPosition - Vector3.new(0, 3, 0)
			local RainBeam = script.RainBeam:Clone()
			RainBeam.Parent = RainPartsFolder
			RainBeam.Attachment0 = RainAttachment1
			RainBeam.Attachment1 = RainAttachment2
			RainAttachment1.Parent = Terrain
			RainAttachment2.Parent = Terrain
			
			-- Move the rain drop until it hits the surface
			repeat
				RS.RenderStepped:Wait()
				RainAttachment1.WorldPosition = RainAttachment1.WorldPosition - Vector3.new(0, RainDeltaIncrement, 0)
				RainAttachment2.WorldPosition = RainAttachment1.WorldPosition - Vector3.new(0, RainDeltaIncrement, 0)
			until RainAttachment2.WorldPosition.Y <= HitPos.Y
			
			-- Create the splash effect
			if not table.find(AbsorbantMaterials, RainRay.Instance.Material) then
				local SplashPart = PartTemplate:Clone()
				SplashPart.Parent = RainPartsFolder
				SplashPart.Position = HitPos
				local SplashParticle = script.SplashEffect:Clone()
				SplashParticle.Parent = SplashPart
				SplashPart.Material = RainRay.Instance.Material
				SplashParticle:Emit(1)
				Debris:AddItem(SplashPart, SplashParticle.Lifetime.Max)
			end
			
			RainBeam:Destroy()
			RainAttachment1:Destroy()
			RainAttachment2:Destroy()
		end
	end
end

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

SetupBlacklist(workspace)

while true do -- Loop every 2 seconds to check and add new parts that get added to blacklist.
	wait(2)
	SetupBlacklist(workspace)
end

Some help will be greatly appreciated as always.

1 Like

raycast rain is bad as it produces hell lag especially on low end devices i would reccomend trying a different method

I’ve seen other peoples rain system that uses raycasting and they seem to work flawlessly. Even on mobile it ran 60 fps. Also let me point out that raycasting in roblox is a lot less expensive than you think

I have heard Debris has issues because it internally uses wait(). So try using FastDebris.

Also it’s unadvisable to search through workspace every two seconds. You are basically calling for :GetDescendants() every two seconds.

I advise using workspace Instance | Roblox Creator Documentation instead as it only runs once for each new instance (immediately better) and avoids checking twice and using collision groups to manage the raycast params instead of a table (if possible set the ignored parts to one that doesn’t get hit in a new collision group). Also I believe the current method has a memory leak, instances added to table but not getting removed.

For further optimization.

You should only make it rain in front where the camera is looking. This is what windshake has done to make it extremely performative using octrees, I believe you can do this by detecting the rain particles in front of the camera in the same method then parenting them to workspace though I’m not sure.

1 Like

Wow, this is very good to know. I never thought the debris service would use the wait() function internally.

Yeah, I instantly knew that this would also be a problem. Especially for larger games.

I am very suprised that I haven’t thought of this my self. Not only can I have more of rain be shown on screen, but I can basically get rid of half of the rain that isn’t even being rendered on-screen!


Wow, just by applying these three simple things, this rain system runs greatly now. Thank you for your help, kind sir!

Some optimization tips that I use for my game:

  • I only send 1 raycast for every raindrop, rather than one raycast for every time a raindrop moves.

  • I haven’t actually benchmarked this to determine whether this is actually true or not, but my raindrops fully consist out of just parts, with CanCollide and CastShadow toggled off. Since parts are dirt cheap due to them being baked into the engine, this makes them very efficient. For extra performance I set their Transparency to 0 (most performant option), and material to SmoothPlastic (i.e no textures).

  • When moving raindrops, I use BulkMoveTo rather than moving each raindrop separately.

  • Use a part cache to re-use the same raindrops, rather than creating new raindrops every time. A naive implementation would be to just teleport all raindrops you are not using to somewhere extremely far away (such as 9e9), so that Roblox automatically stops rendering them.

  • Frustum culling. Don’t bother updating parts that the player cannot see. A simple way to determine whether a part is in-front of the screen would be to just use the dot product between the camera and the droplet.

  • When creating a splash effect, I found it most efficient to only have one splash part, and then every time a droplet hits the ground you do:

SplashPart.CFrame = RaindropCFrame
SplashPart.SplashX:Emit(1)

With all this, I can spawn in 10 new raindrops a frame, and then update every single raindrop currently alive (usually about 100) in 1ms.

Hope this helped!

7 Likes

Wow. This has really helped a lot!

wow, really would’ve thought rendering beams with attachments instead of parts would run better. But boy was I wrong! Replacing this with parts with no shadows brought up the performance by heaps!

This is one of those simple, but very efficient solutions that I’ve somehow never thought of.


With all of these optimizations in place, this thing will run so well that I can have over 200 rain drops on screen while running at max framerate!

1 Like

Hey I was wondering, how do you cast only 1 raycast instead of every time a drop moves? I’ve been making a raining script with parts and raycasts by using a raycast that follows each drops. By 1 raycast, you mean a single ray origin that doesn’t move but follows the raindrop until it hits something?

image
Basically, you start a raycast from where the raindrop is about to spawn from, and then raycast all the way down to the ground - in the direction of where the raindrop will fall. Then, you just move the raindrop along the raycast, and since you already know when & where it will hit the ground, you don’t have to raycast more than once for the whole raindrop.

1 Like

Oh I see! That’s interesting (thank you for the visual). I’m wondering though, is it possible to detect whether the droplet collides with something in it’s trajectory if there’s only 1 raycast? For example a player. I was using multiple raycasts as a way to detect any hits in it’s way

You’re right, being unable to detect collisions of characters, or anything that moves, wouldn’t work due to this optimization. It was fine in my usecase, though that’s up to you to decide the cost-benefits of.

Actually, I might be able to figure this one out even with a single raycast, just gotta do some tests. Thank you so much for your replies, much appreciated!! :smiley:

1 Like