Help making projectile explosion with raycast

so, im making a zombie survival game and i decided to make my own rocket launcher tool for the game, you simply use it to fire a projectile that explodes on impact.
however roblox has a weird touch system so whenever the rocket colides, the explosion it makes is not on the exact position it touched, so i decided to use raycast to track the exact touch position, i have used raycast before but i have no idea on how to make a ray that goes to the rocket projectile’s facing surface direction. can anyone help me out?

1 Like

In this case, you will have to use either RightVector, LookVector, or UpVector. (Which you will use will require testing) The end point of the ray should be either on one of those vectors * a number (should be a low number), then just get the hit position from the ray

2 Likes

You can look into this. Making a combat game with ranged weapons? FastCast may be the module for you!

You would need to know to trajectory over time and raycast between time frames and move the projectile also.

If you don’t want to use FastCast I can write out the formula I personally use to simulate trajectories.

What you can do is make the server and client have a ray simulation trajectory, but the client will show a visual of the projectile to make it seem seamless.

Ray simulations can be done with the following:
StartPos | Gun muzzle or Player’s RootPart position
Direction | Camera Direction/LookVector
An end position is not needed here, we just need a direction and start position!

If you’re going to use a end position, then the direction should be: (endPos - startPos).Unit
Code:

-- Client | RunService.RenderStepped
-- Server | RunService.Heartbeat
local projectileSpeed = 3
local pos = rootPart.Position -- Start position
local direction = Camera.CFrame.LookVector.Unit * projectileSpeed-- Direction
--If you're going to use a end position, 
--then the direction should be: (endPos - startPos).Unit * projectileSpeed
local conn

conn = Services.RunService.RenderStepped:Connect(function(dt)
	local ray = workspace:Raycast(pos, direction)
	if ray then
		conn:Disconnect()
		conn = nil
	else
		pos += direction
        --visual projectile below
        visual.CFrame = CFrame.new(pos)
        visual.CFrame *= CFrame.Angles(0, math.rad(math.pi/2), 0)
	end
end)

Now that our trajectory & collision is done, we can work on our explosion!

We can use raycasts for the explosion, but how can we get the targets?
The solution to this question is workspace:GetPartBoundsInBox()!

With GetPartBoundsInBox, we can get the targets with the trajectory’s ray.
Once we get our targets, we check if they’re a Model and see if they contain a Humanoid & RootPart.
If successful, we raycast from the explosion to the target to confirm that they’re in cover or not.

Code:


-- Client | RunService.RenderStepped
-- Server | RunService.Heartbeat
local projectileSpeed = 3
local pos = rootPart.Position -- Start position
local direction = Camera.CFrame.LookVector * projectileSpeed-- Direction
--If you're going to use a end position, 
--then direction should be: (endPos - startPos).Unit * projectileSpeed
local conn

local function getInstance(parent, name)
	local _, returned = pcall(function()
		return parent[name]
	end)
	
	-- Sometimes if the protected call is unsuccessful, it will return an error string, so we turn it to nil.
	if typeof(returned) == "string" then returned = nil end
	
	return returned
end

local HitboxSize = 20
local baseDamage = 100
local baseForce = 30

-- This function is used to check if a target is under cover such as behind an obstacle to avoid getting damage from the explosion.
local function checkifTargetCover(pos, direction, ignore)
	-- Ignore should be a table
	local ignoreParams = RaycastParams.new()
	ignoreParams.FilterDescendantsInstances = {table.unpack(ignore)}
	ignoreParams.FilterType = Enum.RaycastFilterType.Blacklist
	
	local ray = workspace:Raycast(pos, direction, ignoreParams)
	if ray then
		return true
	end
	return false
end

function explosion(Raycast)
	local normal = Raycast.Normal
	local position = Raycast.Position
	local hitCF = CFrame.new(position, position+normal) -- This is very useful for bullet particles/sparks.
	
	local overlapParams = OverlapParams.new()
	overlapParams.FilterDescendantsInstances = {}
	overlapParams.FilterType = Enum.RaycastFilterType.Blacklist
	overlapParams.MaxParts = 5000
	-- OverlapParams is very similar to RaycastParams, but will not have ignoreWater and instead have a MaxParts setting.
	local hit = workspace:GetPartBoundsInBox(hitCF, Vector3.one*HitboxSize, overlapParams)
	
	if hit and #hit > 0 then
		for i, inst in ipairs(hit) do
			local rig = (inst.Parent ~= nil and inst.Parent:IsA("Model"))
			
			if rig then
				local Humanoid = getInstance(rig, "Humanoid")
				local RootPart = getInstance(rig, "HumanoidRootPart")
				-- I heard that FindFirstChild is 20% slower than WaitForChild, so I made my own function to get an object instead of using FindFirstChild
				-- You can replace getInstance with FindFirstChild if you want
				-- rig:FindFirstChild("Humanoid"), rig:FindFirstChild("HumanoidRootPart")
				
				if Humanoid and RootPart then
					local vector3 = (hitCF.Position - RootPart.Position)
					-- vector3 will be used for force, damage, and distance!
					local distance = vector3.Magnitude
					local direction = vector3.Unit 
					-- The reason why Unit is used in most direction vectors, is because it normalizes it, making it's magnitude 1.
					
					if not checkifTargetCover(RootPart.Position, direction * distance, {rig}) then
						local Damage = math.floor(baseDamage - distance)
						local Force = math.floor(baseForce - distance)/2
						-- math.floor will help round up our numbers

						Humanoid.Health -= Damage
						RootPart.AssemblyLinearVelocity += direction * Force
					end
				end
			end
		end
	end
end

conn = Services.RunService.RenderStepped:Connect(function(dt)
	local ray = workspace:Raycast(pos, direction)
	if ray then
        explosion(ray)
		conn:Disconnect()
		conn = nil
	else
		pos += direction
		--visual projectile below
		visual.CFrame = CFrame.new(pos)
		visual.CFrame *= CFrame.Angles(0, math.rad(math.pi/2), 0)
	end
end)

I hope this lazy guide is helpful, feel free to modify the code!

I have ran into this issue myself. The explosion would be centered on the middle of the projectile instead of the exact point of impact in which the difference can be significant. So I used your idea of raycasting to come up with a workable solution. However, I encountered a problem. The problem is that physics can make the projectile bounce, so the CFrame.LookAt vector becomes useless. The way that I solved this was in the weapon script on the server, add an attribute to the projectile part itself called “FlightDir”. That’s a Vector3 value that holds the original direction of the projectile’s flight path. Here’s some code:

local flightDir = projectile:GetAttribute("FlightDir")

-- Creates an explosion and removes the projectile. 
projectile.Touched:Connect(function(hit)
	-- Get the position as early as possible.	
	local position = projectile.Position
	
	-- Check to make sure that we should explode or
	-- fly through.
	if not weaponsMod.checkHitIntangible(hit) then
		-- Execute Effects
		if flySound ~= nil then
			flySound:Stop()
		end
		
		-- Raycast the impact so we know the exact point of impact.
		-- @JPO2006roblox came up with this idea.
		local result = game.Workspace:Raycast(position, flightDir * 20)
		if result ~= nil then
			position = result.Position
		end
		
		-- Generate Explosion		
		explosion.Position = position
		explosion.Parent = game.Workspace

		-- We reparent the script to the tool instance
		-- so it will continue to execute when the
		-- projectile is destroyed.
		debrisService:AddItem(script, 10)
		script.Parent = toolInstance
		projectile:Destroy()
	end
end)

Don’t worry about some of the module calls because those are specific to my weapons system. Also, I created the explosion beforehand to reduce processing inside the event handler. Feel free to use this.

1 Like