Casting a grid of rays

Trying to cast a grid of rays repeatedly for my acid rain

There’s not really any issue, I just want to find a performance friendly way.
I have not coded anything yet, because I’m trying to find the best way possible

You can try using

local MapSize = 512
local MapPositionX = 0
local MapPositionZ = 0
local AcidRainSpawnHeight = 100
local TimeBeforeNextDrop = 0.5

local PosX1 = MapPositionX + MapSize
local PosX2 = MapPositionX - MapSize

local PosZ1 = MapPositionZ + MapSize
local PosZ2 = MapPositionX - MapSize
while true do
local RainDrop = Instance.new("Part")
RainDrop.Parent = game.Workspace
RainDrop.Positon = Vector3.new(math.random(PosX2, PosX1), AcidRainSpawnHeight, math.random(PosZ2, PosZ1))
wait(TimeBeforeNextDrop)
end

It doesn’t use rays but you can use math.random() to randomise the position of the rain drops.

1 Like

Alright thanks, I’ll try this.

Is there any way other than using physical rain droplets?

You can try using Particle Emitters Particle Emitters
but you wont be able to track if it has been touched or not.

Isn’t there a way to use raycasting?

Im not sure as im not good at ray casts.

I don’t think there is a way to bulk raycast.
So you just have to shoot a bunch of individual raycasts.

What I’m getting from this question is how to decrease CPU usage from raycasts. Correct me if I’m wrong, but if this is what you’re asking this is my answer:

To solve this problem, it is helpful to simulate these grid raycasts in studio. I did just that, and I started with a basic grid raycast function:

--// written by bhristt (september 20, 2022)


local RunService = game:GetService("RunService")


local BOUNDS = {
	Vector2.new(50, 50),
	Vector2.new(-50, -50)
}

local ROWS = 100
local COLS = 100

local GROUND = 0
local HEIGHT = 50

local LINE_COLOR = Color3.fromRGB(156, 54, 89)
local LINE_THICKNESS = 0.1
local LINE_TRANSPARENCY = 0.15



local createLine: (Vector3, Vector3) -> BasePart, removeLines: () -> nil do
	
	local lineFolder = Instance.new("Folder", workspace)
	lineFolder.Name = "Lines"
	
	local lines: {BasePart} = {}
	
	function createLine(st: Vector3, en: Vector3): BasePart
		
		local displacement = en - st
		
		local line = Instance.new("Part", lineFolder)
		line.Anchored = true
		line.CanCollide = false
		line.CanTouch = false
		line.CanQuery = false
		line.Size = Vector3.new(LINE_THICKNESS, LINE_THICKNESS, displacement.Magnitude)
		line.CFrame = CFrame.lookAt((st + en) * 0.5, en)
		line.Color = LINE_COLOR
		line.Transparency = LINE_TRANSPARENCY
		
		table.insert(lines, line)
		
		return line
	end
	
	function removeLines()
		
		for i = 1, #lines do
			lines[i]:Remove()
		end
		lines = {}
	end
end


local function castRay(st: Vector3, en: Vector3): Vector3?
	
	local rayParams = RaycastParams.new()
	
	local raycast = workspace:Raycast(st, en - st,  rayParams)
	
	if raycast == nil then
		return nil
	end
	
	return raycast.Position
end


local function castRayGridNormal(): number
	
	local corner1 = BOUNDS[1]
	local corner2 = BOUNDS[2]

	local boundIntX = (corner2.X - corner1.X) / ROWS
	local boundIntZ = (corner2.Y - corner1.Y) / COLS

	local startTick = tick()

	removeLines()

	for i = 1, ROWS do
		for j = 1, COLS do

			local x = corner1.X + boundIntX * i
			local z = corner1.Y + boundIntZ * j

			local collisionPos = castRay(Vector3.new(x, GROUND + HEIGHT, z), Vector3.new(x, GROUND - 1, z))

			local line = createLine(Vector3.new(x, GROUND + HEIGHT, z), Vector3.new(x, collisionPos, z))
			line.Name = "I" .. tostring(i) .. "J" .. tostring(j)
		end
	end

	return tick() - startTick
end

local lastGridRaycastTime = 0
local lastJobCompleted = true
local function physicsStepped(runTime: number, dt: number)
	
	if not lastJobCompleted then
		return
	end
	lastJobCompleted = false
	
	local gridRaycastTime = castRayGridNormal()
	if gridRaycastTime > lastGridRaycastTime then
		lastGridRaycastTime = gridRaycastTime
		print("Job completed: " .. string.format("%.3f", gridRaycastTime) .. "s (slowest time)")
	else
		print("Job completed: " .. string.format("%.3f", gridRaycastTime) .. "s")
	end
	
	lastJobCompleted = true
end


RunService.Stepped:Connect(physicsStepped)

The castRayGridNormal() function attempts to ray cast an entire grid area. In theory, this function should cast multiple rays all in one for loop on each frame. However, we end up with this very laggy mess:

Even without the visual rays being created and destroyed, this function is still an incredibly heavy toll on the CPU.

To make this function less CPU intensive, we can add a wait interval after every ray cast.

local function castRayGridSpread(): number
	
	local corner1 = BOUNDS[1]
	local corner2 = BOUNDS[2]
	
	local boundIntX = (corner2.X - corner1.X) / ROWS
	local boundIntZ = (corner2.Y - corner1.Y) / COLS
	
	local startTick = tick()
	
	removeLines()
	
	for i = 1, ROWS do
		for j = 1, COLS do
			
			local x = corner1.X + boundIntX * i
			local z = corner1.Y + boundIntZ * j
			
			local collisionPos = castRay(Vector3.new(x, GROUND + HEIGHT, z), Vector3.new(x, GROUND - 1, z))
			
			local line = createLine(Vector3.new(x, GROUND + HEIGHT, z), Vector3.new(x, collisionPos, z))
			line.Name = "I" .. tostring(i) .. "J" .. tostring(j)
			
			task.wait()
		end
	end
	
	return tick() - startTick
end

Now, using castRayGridSpread() instead of castRayGridNormal(), we end up with a cleaner and less CPU intensive ray cast:

Though this method is much less CPU intensive, it is much slower at casting rays. We can improve this by making more ray casts before each wait interval. That way, it’s a combination of the first method and this second method to reduce CPU usage and increase speed:

local castRayGridOptimized: () -> number do
	
	local RAYCASTS_PER_WAIT_INTERVAL = 40 --// more raycasts means more cpu usage
	
	function castRayGridOptimized(): number
		
		local corner1 = BOUNDS[1]
		local corner2 = BOUNDS[2]

		local boundIntX = (corner2.X - corner1.X) / ROWS
		local boundIntZ = (corner2.Y - corner1.Y) / COLS

		local startTick = tick()
		local casts, completes = 0, 0
		
		removeLines()

		for i = 1, ROWS do
			for j = 1, COLS do
				
				casts += 1
				
				coroutine.resume(coroutine.create(function()
					
					local x = corner1.X + boundIntX * i
					local z = corner1.Y + boundIntZ * j

					local collisionPos = castRay(Vector3.new(x, GROUND + HEIGHT, z), Vector3.new(x, GROUND - 1, z))

					local line = createLine(Vector3.new(x, GROUND + HEIGHT, z), Vector3.new(x, collisionPos, z))
					line.Name = "I" .. tostring(i) .. "J" .. tostring(j)

					completes += 1
				end))
				
				if casts == RAYCASTS_PER_WAIT_INTERVAL then
					
					while completes < RAYCASTS_PER_WAIT_INTERVAL do
						task.wait()
					end
					casts = 0
					completes = 0
					task.wait()
				end
			end
		end
		
		return tick() - startTick
	end
end

Now, using castRayGridOptimized() instead of castRayGridSpread(), we get the following result:

As seen in the video, this function is much faster than its previous counterparts. It can be even faster by setting RAY_CASTS_PER_WAIT_INTERVAL to a higher number.

For example, setting RAY_CASTS_PER_WAIT_INTERVAL to 100 produces the following result:

Of course, RAY_CASTS_PER_WAIT_INTERVAL can be set to a higher value, but the higher the value the more intense the CPU usage will be. Besides, this is an exaggerated test that produces 10,000 raycasts as fast as possible. I’m sure most projects on roblox will not be needing 10,000 raycasts for anything, HAHA!

2 Likes

Hello, so for the last example is there a way to reuse the already generated raycasts to simulate “droplets” that damage players?

Edit: I obviously don’t need that many Lol

What I would do is use the data received from each ray cast to tell the raindrop where to stop. Since you are deciding what height to release the raindrops from, you may be able to just create a part, set it at the height and tween its position and size until it reaches the position the ray cast for that specific raindrop hit

The functions I provided aren’t meant to be used for this purpose, I just made them for proof of concept to answer your question.

Interesting, Thanks for your replies.

1 Like