How do I improve my raycasting per step code?

I’m trying to improve the performance of my raycasting code. I cast a ray per render step and apparently it’s causing sight frame drop when this is being called frequently such as shooting a gun with really high fire rate or shotguns, which call this function around 5-10 times at once. This does not happen for weapons with slow fire rate or short range.

https://gyazo.com/d603bd9e5b0e416637c514a43bd5a62c

local stepped
coroutine.wrap(function()
	
	stepped = runservice.RenderStepped:Connect(function(deltaTime)
	
		BulletTrail.Enabled = true
		BulletPart.Transparency = 0
		
		local hit,pos,norm = RayCastModule.CastRay(RNG, MouseHitP, deltaTime, OldCF, temptoolStats, BulletPart, char)
		OldCF = CFrame.new(pos)		
		if Firstshot == true then
			Firstshot = false
			Remotes.SendBulletToClient:FireServer (BulletPart.CFrame, OldCF, BulletPart.Name, temptoolStats.MuzzleVel)
		end
		BulletPart.CFrame = CFrame.new(pos, pos + (MouseHitP -InitialCF.Position).unit + RNG )
		
				
		if hit ~= nil or (pos - InitialCF.Position).magnitude >= Range then
			stepped:Disconnect()
			BulletPart.Transparency = 1
			delay(5,function()
				debris:AddItem(BulletPart,0)
			end)
			Remotes.SendBulletToClientImpact:FireServer(BulletPart.Name, hit,pos,norm)
			if hit ~= nil then
				HandleHit(hit, pos, norm, (pos - InitialCF.Position).magnitude)
			end
			return
		elseif hit == nil then
			rayInitialCF = CFrame.new(pos, pos)
		end	
	end)
	
end)()

And here’s the raycasting module CastRay function:

function RayCastModule.CastRay(RNG, MouseHitP, deltaTime, OldCF, temptoolStats, BulletPart, char)
		
	cs:AddTag(BulletPart, "Ignore1")
	cs:AddTag(char,"Ignore1")
											
	local ignored = cs:GetTagged("Ignore1")
	local hit,pos,norm = workspace:FindPartOnRayWithIgnoreList(Ray.new(OldCF.Position, RNG * deltaTime * temptoolStats.MuzzleVel),ignored)
	return hit,pos,norm
end

Is there anyway to simplify/improve it? I know a lot of games use the similar approach but they have 0 performance problems.

3 Likes

Don’t use RenderStepped, use Stepped or Heartbeat. I’m pretty sure RenderStepped is only if you need to run code before the physics is simulated, meaning if you have a heavy load it will just cause more latency.

4 Likes

I tried Heartbeat and replaced DeltaTime with 0.01 since Stepped’s delta time and Heartbeat’s delta time are different from RenderStepped. However it ouptuts pretty much the same, it also slow down the bullet speed because they both runs slower than RenderStepped. I think something inside the function can be improved, maybe during the raycasting…
https://gyazo.com/f3e6b613d906bd1e0d546c03854e7261

1 Like

Hmmm… I’m not that familiar with raycasting, but I’m guessing that since it seems like the only “ignore” tags are for the bullets in question, you are raycasting to everything in the workspace. Do you really need to do that? I feel like there should be a more efficient method…

For starters, any objects that are behind you should be ignored. I’m pretty sure you can use a combination of the bullet velocity direction and a position vector relative to your player to determine this (dot product < 0 → ignore). This isn’t something I would include in RenderStepped - I think it would be better to have a separate thread that is actively creating an “ignore2” list, just by using the player’s current look vector; that would also mean quick turnaround shots might not be dealt with accurately. But those are two ways to go about that option.

The other way I was thinking is if you create a scope (not for the gun, I mean more of a cone of detection). I’m not sure how efficient this would be, given that you would need to constantly update this cone every frame. However I’m pretty sure that raycasts are far more expensive than distance checks.

Again, I’m not a raycast expert, it’s possible that raycast already has a distance constraint built-in. Perhaps someone with more experience can help with the details. Hopefully my ideas at least provide something meaningful :slight_smile:

1 Like

Oh, sorry for not pasting the entire part of the code, the Ignore1 tag includes some other parts too.

1 Like

Have a read here. It looks like there’s a lot going on deep down in the system. This should honestly be read by every developer looking to optimize their code with the use of RenderStepped. I haven’t gone through the whole thing but I do intend to read more. I’m just busy doing some things at the moment. You also might be interested in the topic that sparked this change. But below is the main, official announcement from 4 years ago with all the implications and debate.

After 2 hours of investigation: I found out some results and solved A BIT frame lag.

Somewhat rendering the bullet part cause a really minor lag, but that’s not the major problem.
Calling Workspace:FindPartOnRayWithIgnoreList is laggier than Workspace:FindPartOnRay
Now I clone a Bullet Part instead of creating a new one, such that I can add tag initially to thte pre-setted part. There’s non more AddTag inside CastRay, but it doesn’t really changes anything in a major way… :man_facepalming:
I’m wondering how does other games perform the same without any performance issues… :confused:

When I used to tinker with bullet physics, I found a few things to be very helpful:

  • Minimize the amount of work that has to be done per step
    What I mean by this is that you should move things like enabling the bullet trail, setting the bullets transparency, etc, outside of the function called every step.

  • Don’t call Vector3.new() or CFrame.new() or [Anything].new() inside of your function called every step
    These functions are quite expensive as you call them every step, especially if you have numerous bullets.

  • Minimize your ignorelist
    I’m not fully sure how big your ignorelist is, but it should include about 2 models (A main ignore model, which bullets/etc are all parented to; Your character).

While this is all cool and everything, I haven’t really explained anything with respect to code… But the general principle is that you want to make it run as fast as possible. Roblox runs on a single thread, and doesn’t actually support true multi-threading. This means that if something is taking too long, then your whole game will lag. Personally, my bullet calculations take anywhere from 0.00003 - 0.00005 seconds to run.

Here is the code for my bullet calculations:

runBind = Network:call("BindToRun", function(dt) -- Does the same thing as RunService.RenderStepped:Connect()
	local newPos = currentPos + velocity*dt;
	local posDiff = newPos - currentPos;
	local mag = posDiff.Magnitude + 0.025;
	local hit,pos,norm = workspace:FindPartOnRayWithIgnoreList(Ray.new(currentPos, posDiff.unit*mag), ignore)
	distance = distance + mag;
	
	if hit and pos and norm then
		life = 0;
		if hit.Anchored then
			Bullet.newHole(hit, pos, norm, true, true);
		end
	end
	
	newBullet.CFrame = angles+newPos;
	life = math.max(0, life-dt);
	currentPos = newPos;
	
	if life <= 0 or distance >= 1000 then
		remove();
	end
end)

If you have any questions, or want me to help optimize your code, let me know.

3 Likes

Sorry but what does Network refers to? Btw can you define some variables? I’m getting a bit confused.

I do lots of ray-casting in my projects.

I have a couple of suggestions for you, based on what has worked well for me.

First suggestion is that FindPartOnRayWithWhitelist might be your friend here. It seems to work really well when you only add entire folders/hierarchies to the whitelist table.

This means something like:

local rooms = {}
table.insert(rooms, workspace.MyStaticWorldGeometry)
local initialPart, initialEndPoint = workspace:FindPartOnRayWithWhitelist(finalRay, rooms)

The second thing I suggest, which helps even more, is to break your bullet collision detection up into two parts, a static check when the bullet is spawned, and then a per-frame dynamic check.

First step, when the bullet is fired, work out exactly what bit of static geometry its going to hit. This is too expensive to check every single frame, so just pre-calculate it! You know where your uninterrupted bullet will hit, so just set that as your bullets final “life”.

Second step, now that you know where your bullet will die if it doesn’t run into anything else, you can just check it against your non-static things like players or enemies every frame, which provided they’re in a folder, is pretty cheap to check.

Something like this for the initial check:

--bulletLife is a timer where the bullet "explodes" when it reaches 0
local part, pos, normal = worldState:BulletRay(Ray.new(org, direction * life * speed))

if (part ~= nil) then	
	--scale down life so it'll run out *exactly* when it would have hit a wall
	local dist = (pos - org).Magnitude
	local total = life * speed
	local frac = dist / total
	bulletLife = bulletLife * frac  		
   end
2 Likes

My Network:call("BindToRun", function(dt) Is similar to RunService.RenderStepped:Connect(), it just so happens that I use a main thread, so I had to create a module so I could call another one of my scripts to connect it to my main thread.

I see the problem with the other things though, as such I will supply more of my code as a sample.

function Bullet.newParticle(props)
	local origin = props.origin;
	local color = props.color or Color3.new(1,0.1,0.1);
	local transparency = props.transparency or 0.35;
	
	local position = origin.p;
	local velocity = props.velocity*2;
	local life = math.min(10, props.life or 2.5);
	local ignore = props.ignore;
	
	local currentPos = position;
	local angles = CFrame.new(Vector3.new(), velocity); angles = angles - angles.p;
	local distance = 0;
	
	local newBullet = bullet:Clone();
	newBullet.CFrame = origin;
	local bulletColor = ColorSequence.new(color, color);
	local bulletIn, bulletOut = search(newBullet, "In"), search(newBullet, "Out");
	local transIn, transOut = transparency < 1 and transparency - 0.15, transparency < 1 and transparency or 1;
	bulletIn.Transparency = NumberSequence.new(transIn, transIn);
	bulletOut.Transparency = NumberSequence.new(transOut, transOut);
	bulletOut.Color = bulletColor;
	newBullet.Parent = ignoreModel;
	
	local runBind;
	
	local function remove()
		if runBind then
			runBind();
			runBind = nil;
				delay(0.5, function()
					if newBullet then
						newBullet:Destroy();
					end
				end)
		end
	end
	
	runBind = Network:call("BindToRun", function(dt)
		local newPos = currentPos + velocity*dt;
		local posDiff = newPos - currentPos;
		local mag = posDiff.Magnitude + 0.025;
		local hit,pos,norm = workspace:FindPartOnRayWithIgnoreList(Ray.new(currentPos, posDiff.unit*mag), ignore)
		distance = distance + mag;
		
		if hit and pos and norm then
			life = 0;
			if hit.Anchored then
				Bullet.newHole(hit, pos, norm, true, true);
			end
		end
		
		newBullet.CFrame = angles+newPos;
		life = math.max(0, life-dt);
		currentPos = newPos;
		
		if life <= 0 or distance >= 1000 then
			remove();
		end
	end)
end

Some variable information:

props: table containing all of the properties I want to edit.

props.origin → CFrame value with the origin position as one of its components
props.velocity → Vector3 value, can be defined with CFrame.new(origin.p, target).lookVector*speed
for velocity: target is a vector3 and where you want your bullet to end up going, speed is the studs/s of the bullet
props.ignore → ignore list (table) with everything you want to ignore

If you want me to define more variables let me know.

2 Likes