Improving Raycast Projectiles for Long Range Weapons

Hi,

So right now I’m trying to improve my projectile system to be accurate using raycast’s, however it still has its issues, where when firing at a specific area, and the object obscures the path, it will go through the object, and go to the destination when it was fired from.

Visual Example from ms paint

ex
Issue neing Ray continues as normal, and Projectile will pass through it

While this wouldn’t happen under most circumstances when using weapons like Pistols, Swords, or Shotguns, it becomes a big issue when using long range weapons, such as: Snipers, and Rifles, and I want to figure out a way to fix it for said weapons.

This is my code used for this system

(Its a big mess of OOP and not OOP, which I hope to improve later on)

self.FireEvent = Fire.OnServerEvent:Connect(function(Player, Position: Vector3)
	self.RaycastParams.FilterDescendantsInstances = {Player.Character}; -- Filter out Player

	local Root = Player.Character.PrimaryPart; -- Ray Origin
	local Distance = Player:DistanceFromCharacter(Position); -- Distance from point fired at
	local TimeDelay = Distance/self.ProjectileSpeed; -- Time it will take to get there

	if self.RequiresAmmo then -- If the weapon uses ammo
		self.Config:SetAttribute("Ammo",math.clamp(self.Config:GetAttribute("Ammo") - 1, 0, self.Config:GetAttribute("MaxAmmo")));
	end
		
	local DelayedPositions = {}; -- gets positions and directions for raycasting
	for i = 1, self.ProjectileCount do -- gets total for all projectiles
		local x = self.Random:NextNumber(-self.ProjectileSpread, self.ProjectileSpread) -- random angle if added
		local y = self.Random:NextNumber(-self.ProjectileSpread, self.ProjectileSpread)
		local Direction = CFrame.new(Root.Position, Position) * CFrame.Angles(math.rad(x), math.rad(y), 0)  -- direction
			
		local ExtDirection = Direction.LookVector * (self.Range - utl.BulletOffset) --Direction to fire from
        -- BulletOffset is so the weapon can properly hit any targets in the area
		local Result = workspace:Raycast(Root.Position, ExtDirection, Params); -- Raycast

		DelayedPositions[i] = {Root.Position, ExtDirection, Direction.LookVector, Result and Result.Position+Result.Normal or Root.Position+ExtDirection}; -- Add Data to table
	end
		
	utl.GlobalEffectEvent:FireAllClients(self.Tool, DelayedPositions, self.Projectile, TimeDelay, self.Sounds)
	print(`Ping...`);

	task.delay(TimeDelay, function() -- Bullet Delay
		warn(`PONG! ({TimeDelay})`);

		for i = 1, #DelayedPositions do -- get data for raycasts
			local Items = DelayedPositions[i];
				
			if (Items[1] - Items[4]).Magnitude > self.Range then continue; end -- if the data exceeds the range, cotinue
				
			local Result = workspace:Raycast(Items[4],  Items[3] * utl.BulletOffset, Params) -- Cast ray with new info

			if Result then -- Player checking stuff.
				local Humanoid = self:VerifyTarget(Result.Instance)
				if Humanoid then
					local Damage = self.Damage.X * (self:IsHeadshot(Result.Instance) and self.Damage.Y or 1)
					self:DealDamage(Humanoid, Damage);
				end
			end
		end
	end)
end)

How can I improve this code to work with weapons like Snipers, and Rifles?


Edit: I’m still looking to get this fixed as I’m not really sure where I should start wit this system.

Instead of using a raycast to determine the hit part, use the raycast to determine the expected end postion. Then simulate the projectile, moving it towards the expected end position - you can now check for collisions with objects that unexpectedly get in the way.

To be honest though, you can just drop the raycast system in general if you’re not doing hitscan weapons. You have the simulate the projectiles anyway, I don’t see the point.

True, however collisions will be very inconsistent at very high speeds, as the bullets are very fast (typically 1600 - 3000 studs per second, though its intended to simulate their irl speeds), so raycasting seems to be a more efficient solution for that purpose.

Why dont you just use a module where all of these variables can be easily adjusted and collisions are accounted for

Because I want to learn how to make my own things, and learn new things for future use. I dont really like using other peoples stuff.

Plus, I have tried looking at other examples, but I could not find what I was looking for, so I’m trying to post here.

1 Like

You can cross reference their code; regardless— if ur not already visualizing the rays, thatd be helpful in debugging. moreover, if you want greater accuracy, maybe perform several raycasts along the bullet’s path for each frame

And just to clarify; ur rays are not detecting intersecting objects because theyre moving at high speeds, right ?

1 Like

I spent a few hours creating a function to check for collisions along its path, and this is what I made.
Probably not the most efficient solution, but it appears to work for what I need it to.

code:

function PerformBulletRaycasting(Data)
	local Distance = (Data.Origin - Data.Position).Magnitude; -- Distance
	if Distance >= Data.Range then return end -- Stop if Range Exceeds Maximum
	
	local Sections = math.ceil(Data.Range/utl.SectionLength) - 1; -- Sections the rays will be split up in to detect collision;
	local TimeBetween = (Distance/Data.Speed)/Sections; -- Time for each ray to fire
	print(`{Sections} Sections will be fired each for {TimeBetween} seconds.`);
	
	local self = Data.Source
	
	local CurrentPos = Data.Origin; -- check for current position of "projectile"
	local CurrentObject; -- if an object is found
	
	for i = 1,Sections do -- iterate for how many sections there are
		if (Data.Origin - CurrentPos).Magnitude >= Data.Range then break end
		local NewDirection = Data.Direction * utl.SectionLength; -- direction
		local Result = workspace:Raycast(CurrentPos, NewDirection, self.RaycastParams); -- raycasting
		
		if Result then -- if there is a result
			CurrentPos = Result.Position; -- new Position
			CurrentObject = Result.Instance; -- new Object
			break; -- exit loop
		else
			CurrentPos = CurrentPos + NewDirection; -- Skip to next Position / Sections
		end
		print("E") -- end
		task.wait(TimeBetween); -- wait
	end
	
	if CurrentObject then -- if an object was found
		print("Hit an Object!")
		local Humanoid = self:VerifyTarget(CurrentObject)
		if Humanoid then
			local Damage = self.Damage.X * (self:IsHeadshot(CurrentObject) and self.Damage.Y or 1)
			self:DealDamage(Humanoid, Damage);
		end
	end
end