Optimizing raycast ignore lists

Hello,

I implemented guns using workspace:FindPartOnRayWithIgnoreList() because I want the gun beam to pass through parts without CanCollide. Currently, every time I raycast, I create a table and iterate through each descendant of the workspace and append it to the previously mentioned table if it matches the desired criteria. However, this causes lag if there’s a lot of descendants in the workspace (around 10 000) and the gun has a high fire rate.

Possible solutions:
Of course, I could cache the ignore list and update it every at an interval but for things such as turrets, there might be a bit of lag between a part removing CanCollide and the turret beam updating and displaying how it should interact with the environment.

Another solution could be to recreate the ignore list when a descendant’s CanCollide is changed. The problem is that parts change their CanCollide fairly often which could make the code run at a higher frequency than if I just updated it at an interval making it possibly even laggier than the first solution.

My final idea was to listen to all the CanCollide property change events of the BasePart descendants of the workspace and delete or append the BasePart to the ignore list table. The problem is that the only way I know of deleting an object from a table is with table.remove(index) which requires knowledge of the entry’s index which will change as other entries get removed.

Thanks,
Riley

1 Like

No need to add everything possible to the ignore list. Just keep raycasting and adding parts you hit to the ignore list until the next raycast either hits nothing or a collidable part.

10 Likes

An example of Avigant’s solution:

local function rayFilter(hit)
    if hit then
        return hit.CanCollide
    else
        return true
    end
end

local maxHit = 5 --limit before you don't check parts anymore, so the game doesn't slow down too much

local ray = -- ray here
local IgnoreTable = {} -- obviously non-collidable parts will probs be in here

local hit
for i = 1, maxHit do
    hit = workspace:FindPartOnRayWithIgnoreList(ray,IgnoreTable)
    IgnoreTable[#IgnoreTable + 1] = hit
    if rayFilter(hit) then
        break
    end
end
-- do something with hit/ other variables

if you don’t want to restrict yourself to a limited number of hits, even though this is not recommended since it could potentially slow down the client, this would replace the for loop:

local hit
repeat
    hit = workspace:FindPartOnRayWithIgnoreList(ray,IgnoreTable)
    IgnoreTable[#IgnoreTable + 1] = hit
until rayFilter(hit)
3 Likes

This is usually the best solution. Managing a potentially enormous ignore list is probably not what you want; it could mean all raycasts have the overhead of a huge ignorelist of things that mostly won’t be hit. It would only be a good option if the whole place has just a handful of non-collidable parts. To build this list you’d initially iterate through all workspace descendants, but you’d never do that again, you’d instead maintain the list by add/remove. The list does not need to be an array, it can be a dictionary keyed by part instance, so you can remove by ignoreList[part] = nil. Even still, you’d want to handle workspace.DescendantAdded and Removing, and probably also make a utility function to wrap getting and setting of CanCollide, so you don’t have a property change listener for every part in game!

Keep in mind too, that Character limb parts are normally CanCollide=false, so you don’t want to ignore these unless you only want head and trunk hits to be counted.

4 Likes

What do you mean by “a utility function to wrap getting and setting of CanCollide?” I didn’t know there was an approach different from property change listeners.

What @EmilyBendsSpace means is that you create a custom function for getting and setting properties instead of doing it all raw.

So instead of something like:

part.CanCollide = true

You use a function that does it for you, plus some other stuff:

local function setCollideWrapper(part, bool)
    part.CanCollide = bool
    IgnoreTable[part] = not bool or nil
    -- change the part's transparency or whatever else
end

This is so you never forget to mend these statements together and you can use this additional logic in your raycasting function.

2 Likes

I think you need to keep the ignoreList all in the array part of a table for it to work, and the array can’t be sparse, so you either have to rebuild the array every time (lazy O(N) way), or you need to so something like this to retain the O(1) benefit of the dictionary:

IgnoreList = {}
IgnoreDict = {}
local function setCollideWrapper(part,bool)
	part.CanCollide = bool

	if bool then
		local index = IgnoreDict[part]
		if index then
			IgnoreList[index] = IgnoreList[#IgnoreList]
			IgnoreList[#IgnoreList] = nil
			IgnoreDict[part] = nil
		end
	else
		local index = #IgnoreList + 1
		IgnoreList[index] = part
		IgnoreDict[part] = index
	end
end
1 Like