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.