Correct & Parallel-safe Piercing Raycast

Update:

I’ve made an improved and more informative post, including small improvements to this system;


This is an easy-to-use and efficient piercing/penetrating raycast solution for “advanced” people out there who want to make stuff like weapon frameworks.

An important key feature is the ability to specify a “penetration depth”, in studs.
This implementation properly handles cases like intersecting objects, where many others don’t. This is displayed in Demo Video #2 and can be tested within the demo file.

This is also fully parallel safe, which means it could be used for efficiently handling lots of bullets and whatever else you might want to make with it.


Demo video:

Demo video #2:

And here’s the demo file: (ReplicatedStorage > Actor > Script)
PiercingCast.rbxl (74.3 KB)


Raw code
--<em>Parallel safe</em>
--Returns inward results and outward results
--Expects <code>direction</code> to be normalized.
local function raycast_pierce(pos:Vector3, direction:Vector3, length:number, pierce:number):({RaycastResult}, {RaycastResult})
	--`ins` and `outs` represent the RaycastResults from the entrances and exits of the ray
	local ins, outs = {}, {}
	
	--Used to exclude hit parts from later raycasts
	local filter_params = RaycastParams.new()
	filter_params.RespectCanCollide = true
	
	while true do
		--The final enter & exit raycast results
		local in_result:RaycastResult
		local out_result:RaycastResult
		
		--The final enter & exit distances along the ray
		local in_dist:number
		local out_dist:number
		
		while true do
			local a_dir = direction * (out_dist or length)
			local a_result = workspace:Raycast(pos, a_dir, filter_params)
			
			--No hit; break loop
			if not a_result then
				break
			end
			
			--Add to filter
			filter_params:AddToFilter(a_result.Instance)
			
			--Store results if this entrance is closer than the previous entrance
			local a_dist = vector.magnitude(pos::any - a_result.Position::any)
			if not in_dist or a_dist < in_dist then
				in_result = a_result
				in_dist = a_dist
			end
			
			--Do a reverse cast to check the ray exit
			
			--Filter out all parts except the one we're currently checking
			local mask_params = RaycastParams.new()
			mask_params.FilterType = Enum.RaycastFilterType.Include
			mask_params:AddToFilter(a_result.Instance)
			
			local b_result = workspace:Raycast(a_result.Position + a_dir, -a_dir, mask_params)
			
			--No hit; break loop
			if not b_result then
				break
			end
			
			--Store results if this exit is farther than the previous exit
			local b_dist = vector.magnitude(pos::any - b_result.Position::any)
			if not out_dist or b_dist > out_dist then
				out_result = b_result
				out_dist = b_dist
			end
		end
		
		if in_result then
			--Store entrance result
			table.insert(ins, in_result)
		end
		
		--If nothing was pierced during this pass, return.
		if not out_result then
			break
		end
		
		--Return if the exit point is out-of-range
		if out_dist > length then
			break
		end
		
		--Update pierce & check if pierce has been depleted
		pierce -= out_dist - in_dist
		if pierce <= 0 then
			break
		end
		
		--Store exit result
		table.insert(outs, out_result)
	end
	
	return ins, outs
end

As a bonus, this little code snippet lets you tell if the ray ended early without modifying the original code:

local ins, outs = raycast_pierce(...)
local ended_early = #ins > #outs

Note: This implementation expects collisions to be convex, so rays will treat meshes with concave collisions as one solid object. This is generally a non-issue and the fix requires extra code complexity.

16 Likes

Update:

  • Reworked behavior to properly handle intersecting objects. This change also makes rays only ‘exit’ when they actually reach a gap in objects.
  • Slightly optimized code and improved some types.
1 Like

For anybody potentially wanting to use this to damage enemies, or do other similar things, I’ve made this example code to help you out, or point you in the right direction.

This is a slight modification of the original code:

local function raycast_pierce(pos:Vector3, direction:Vector3, length:number, pierce:number)
	--`ins` and `outs` represent the RaycastResults from the entrances and exits of the ray
	local ins, outs = {}, {}

	--Store hit humanoids to return
	local hit_humanoids:{[Humanoid]:true} = {}

	--Used to exclude hit parts from later raycasts
	local filter_params = RaycastParams.new()

	while true do
		--The final enter & exit raycast results
		local in_result:RaycastResult
		local out_result:RaycastResult

		--The final enter & exit distances along the ray
		local in_dist:number
		local out_dist:number

		while true do
			local a_dir = direction * (out_dist or length)
			local a_result = workspace:Raycast(pos, a_dir, filter_params)

			--No hit; break loop
			if not a_result then
				break
			end

			do--Humanoid hit check

				local model = a_result.Instance.Parent
				if model then
					local humanoid = model:FindFirstChildWhichIsA("Humanoid")

					if humanoid then
						hit_humanoids[humanoid] = true
					end
				end

			end

			--Add to filter
			filter_params:AddToFilter(a_result.Instance)

			--Store results if this entrance is closer than the previous entrance
			local a_dist = vector.magnitude(pos::any - a_result.Position::any)
			if not in_dist or a_dist < in_dist then
				in_result = a_result
				in_dist = a_dist
			end

			--Do a reverse cast to check the ray exit

			--Filter out all parts except the one we're currently checking
			local mask_params = RaycastParams.new()
			mask_params.FilterType = Enum.RaycastFilterType.Include
			mask_params:AddToFilter(a_result.Instance)

			local b_result = workspace:Raycast(a_result.Position + a_dir, -a_dir, mask_params)

			--No hit; break loop
			if not b_result then
				break
			end

			--Store results if this exit is farther than the previous exit
			local b_dist = vector.magnitude(pos::any - b_result.Position::any)
			if not out_dist or b_dist > out_dist then
				out_result = b_result
				out_dist = b_dist
			end
		end

		if in_result then
			--Store entrance result
			table.insert(ins, in_result)
		end

		--If nothing was pierced during this pass, return.
		if not out_result then
			break
		end

		--Return if the exit point is out-of-range
		if out_dist > length then
			break
		end

		--Update pierce & check if pierce has been depleted
		pierce -= out_dist - in_dist
		if pierce <= 0 then
			break
		end

		--Store exit result
		table.insert(outs, out_result)
	end

	return ins, outs, hit_humanoids
end


local ins, outs, hit_humanoids = raycast_pierce(...)
--Damage all hit humanoids
for humanoid in hit_humanoids do
	humanoid.Health -= 30
end

This code would benefit from using recursion instead of while do loop.