Gun Shooting System with Divided Raycasting Doesn't Work

You can write your topic however you want, but you need to answer these questions:

  1. What do you want to achieve? Keep it simple and clear!
    I want to make a gun shooting system with raycasting and MeshPart bullets since I’ll be using raycasting to compensate the trouble we are having with Touched event and MeshPart bullets to make it look like a real bullet.

  2. What is the issue? Include screenshots / videos if possible!

Consecutive raycast doesn’t work as intended. The image is down below about what I want and what happens.

  1. What solutions have you tried so far? Did you look for solutions on the Developer Hub?
    I’ve tried multiple solutions but didn’t work and there was no answer in Hub.

Basically normal raycast works well but I wanted to add some realistic details and wanted to make bullet drop but raycast can’t be curved. So I tried to divide the raycast into little pieces and fire every raycast consecutively in every heartbeat so that I can change the direction and make it like a curve. Firstly, I tried to divide the raycast into little pieces before make it curvy but it didn’t work. Here is my code and my visualized goal (I used laser to test things out before using a bullet.):

local RunService = game:GetService("RunService")
local Character = nil 
local Muzzle = nil 
local MousePos = nil --basically the target
local previousPos = nil 
local shootingbol = false --prevent the heartbeat event to fire without firing gun
local curcamera = nil --the parent of viewmodel, it will be defined later
local origin = nil --raycast origin, will be defined later
local velocity = 20 --nevermind it

game.ReplicatedStorage.Events.Shoot.OnServerEvent:Connect(function(player, muzzle, mousePos, camera)
   if player.Character then
   	Character = player.Character:GetDescendants()
   	Muzzle = muzzle --the point of gun
   	MousePos = mousePos --the target
   	curcamera = camera --container where viewmodel is at
   	shootingbol = true -- lets the heartbeat fire
   end
end)

RunService.Heartbeat:Connect(function(deltaTime)
if shootingbol then
   	local origin = origin or Muzzle
   	local destination = MousePos
   	local direction = (destination - origin) * deltaTime

   	local midPoint = origin + direction/2
   	
   	local laser = game:GetService("ServerStorage").Laser:Clone()
   	
   	laser.CFrame = CFrame.new(midPoint, origin)
   	laser.Size = Vector3.new(.075, .075, direction.magnitude)
   	laser.Parent = workspace
   	table.insert(Character, laser) -- adding the laser directly to the raycastParams didn't work so that's what I do.
   	
   	local raycastParams = RaycastParams.new()
   	raycastParams.FilterType = Enum.RaycastFilterType.Exclude
   	raycastParams.FilterDescendantsInstances = {curcamera, Character}
   	raycastParams.IgnoreWater = true
   	local raycastResult = workspace:Raycast(origin, direction, raycastParams)

   	if raycastResult then
   		print("hit")
   		shootingbol = false
   	else
   		print("didnt hit")
   		origin = direction
   	end
   end
end)

robloxapp-20230711-1509063.wmv (3.3 MB)

The yellow laser moves only when I alt+tab and come back. I don’t know why.

2 Likes

You should look into fastcast as it offers bullet drop to projectiles and doesn’t use touched events to detect collision

1 Like

Bro I know it, my goal is not to use it. I want to use my own code.

You’ve got some fundamental issue to resolve before you can really experiment with this:

  1. You have a local variable ‘origin’ inside of the Heartbeat event handler function which shadows the upvalue ‘origin’ declared outside of the heartbeat function. The result is that the line where you set ‘origin = destination’ is updating the inner variable right before it goes out of scope. Name the variables differently so that you can actually access the outside copies in the way you expect. Currently, the inner origin variable is re-initialized to the value of the outer one on each Heartbeat, and the outer origin never changes. You’re making lots of laser parts, but they’re all stacked in the same place.

  2. You’re not using velocity. As coded, direction * deltaTime, is equivalently normalizing things so that the bullet will always reach the target in 1 second.

  3. You’re constructing a new instance of RaycastParams each Heartbeat. Not only is this a performance and memory problem, it’s why you couldn’t get your laser parts ignored; it’s because you start with a fresh copy of the filteredInstances every heartbeat and add just the most recent laser part. Set up your RaycastParams outside the Heartbeat handler so that you can build up the filteredInstances as you intend.

2 Likes

Firstly, thanks for pointing the issues out, I’ve fixed them and it works pretty well except some new issues.
Here is my new code :

RunService.Heartbeat:Connect(function(deltaTime)
	if shootingbol then
		origin = origin or Muzzle
		local destination = MousePos
		local direction = (destination - origin).Unit * deltaTime * velocity --velocity is 450

		local midPoint = origin + direction/2
		
		local laser = game:GetService("ServerStorage").Laser:Clone()
		
		laser.CFrame = CFrame.new(midPoint, origin)
		laser.Size = Vector3.new(.075, .075, direction.magnitude)
		laser.Parent = workspace
		--table.insert(Character, laser)
		
		
		raycastParams.FilterType = Enum.RaycastFilterType.Exclude
		raycastParams.FilterDescendantsInstances = {curcamera, Character, laser}
		raycastParams.IgnoreWater = true
		local raycastResult = workspace:Raycast(origin, direction, raycastParams)

		if raycastResult then
			print(raycastResult.Instance)
			origin = nil
			shootingbol = false
		else
			print("bullet is going")
			origin = origin + direction
		end
	end
end)
				
game.Players.PlayerAdded:Connect(OnPlayerAdded)

Whenever I shoot once, it works pretty well but when I wanted to shoot consecutively, since

 if raycastResult then
			print(raycastResult.Instance)
			origin = nil
			shootingbol = false
		else
			print("bullet is going")
			origin = origin + direction
		end

If the previous raycast doesn’t hit somewhere, the next shoot starts from where the previous shoot is and goes for the next target. You can think of it as if it is changing rotation whenever I shoot again while it hasn’t hit somewhere yet. I can’t create multiple raycast with this code. To make it easier to visualize I can record a video of it if you want.

I’ll think of solutions but I’d like to hear others’ too.

Well yeah, you should be doing the origin = nil at the same place you’re setting shootingbol = true, so that it gets re-initialized to Muzzle with each new shot.

1 Like

But if I do so, the previous shot will be stopped.

Well, the way you set up this script, it is only going to support one shot at a time, for all players. Not even one shot per player, because all of those variables you have at the top of the script, as well as the single function connected to heartbeat, are shared by all players.

Assuming you want all players to be able to shoot, and for players to take a shot while their previous shot is still traveling, then you need to take this test code and package it up in a way that has a copy of these relevant variables for each shot fired. There are lots of ways to do this, both OOP and non-OOP approaches. But this is a much larger problem than just getting the raycasts broken up into segments, you need to design and code a whole multiplayer-capable projectile management system.

1 Like

I see, I will be struggling with this for a while. :sweat_smile:

Okay, so I tried something in module script and it works for multiple shots but I still have few questions and I’m not sure if it works optimized. Can you take a look, please ?

---This script is in client script. I define the positions for each player here.
local mousePos = nil
local muzzlePos = nil 
local direction = nil

...

if input.UserInputType == Enum.UserInputType.MouseButton1 then
mousePos = mouse.hit.p
muzzlePos = framework.muzzle.Position
direction = (mousePos - muzzlePos).Unit
					
local newTable = {muzzlePos, mousePos, direction, camera:GetDescendants(), character:GetChildren()}
game.ReplicatedStorage.Events.Shoot:FireServer(newTable)
local origin = nil
local direction = nil
local destination = nil
local rayTable = {} --This is where I put all of the shot information that is taken from local script. It contains tables.
local filterTable = {} --This is temporary filter table where I put exclude objects for raycast.
local module = require(game.ServerScriptService.ModuleScript) -- the module script(see below for full code)
local laser = game:GetService("ServerStorage"):WaitForChild("Laser")
local raycastParams = RaycastParams.new()	

game.ReplicatedStorage.Events.Shoot.OnServerEvent:Connect(function(player, newTable)
table.insert(rayTable, newTable) -- It puts the shot information in form of table to the rayTable so that the objects won't mix up.
end)

RunService.Heartbeat:Connect(function(deltaTime)
if rayTable ~= {} then --check if someone has shot
	for index, value in ipairs(rayTable) do -- it iterates for shot informations 
		for i,v in ipairs(value) do -- gets the table full of shot information and put its values into origin, destination, direction or filterTable
			if i == 1 then
				origin = v --the first parameter taken from local script is origin so...
			elseif i == 2 then
				destination = v --same but it's second
			elseif i == 3 then
				direction = v --same but it's third
			else
				table.insert(filterTable, v) --else is just exclude objects
			end
		end
                 --now we pass these to module script
		local shootingBol = true
		module.shoot(origin, destination, direction, deltaTime, velocity, raycastParams, filterTable, laser, shootingBol)
		origin = nil --cleans it if there is another shot at the same time.
		destination = nil --cleans
		direction = nil -- cleans
		filterTable = {} --also cleans the filterTable

		end
	end		
	rayTable = {} --After each shot is given to module script, we clear it too.
end)
local moduleShoot = {} --it's almost the same script but in module script.

local RunService = game:GetService("RunService")

function moduleShoot.shoot(origin, destination, direction, deltaTime, velocity, raycastParams, filterTable, laser, shootingBol)
	RunService.Heartbeat:Connect(function()
	if shootingBol then
		local multipliedDirection = direction * deltaTime * velocity

		local midPoint = origin + multipliedDirection/2
		local laserC = laser:Clone()

		laserC.CFrame = CFrame.new(midPoint, origin)
		laserC.Size = Vector3.new(.075, .075, multipliedDirection.magnitude)
		laserC.Parent = workspace
		table.insert(filterTable, laserC)
		raycastParams.FilterDescendantsInstances = {unpack(filterTable)}
		raycastParams.FilterType = Enum.RaycastFilterType.Exclude

		raycastParams.IgnoreWater = true
		local raycastResult = workspace:Raycast(origin, multipliedDirection, raycastParams)

		if raycastResult then
				print(raycastResult.Instance)
				shootingBol = false
		else
			print("bullet is going")
			origin = origin + multipliedDirection
			end
		end
	end)
		
end
return moduleShoot

Overall looks like a good start. A few notes though:

This is just for debug visualization, right? You can avoid the whole filtered instances thing just by making your laser parts CanCollide = false and setting the raycastParams to ignore non-collidable parts.

This line is making a new anonymous function every time a shot is fired, and while the function doesn’t really do anything after shootingBol becomes false, you’re basically leaking memory and performance here by never cleaning up these functions. You should store a reference to the connection in a variable so you can call Disconnect() on it. In fact, if you declare the reference on a line before you make the connection, you can use it as an upvalue inside the function and the function can disconnect itself.

But even better than this would be to just have on function in your server script that’s updating all live shots on Hearbeat, which would include removing ones that have completed from the list of live shots.

1 Like

Yeah, it is for debugging but I didn’t know that we could just do CanCollide = false lol. Though I prefer not to use it since every time a character spawns, its parts are also non-collidables, which leads me to make them collide for every part with pairs etc. So it also puts some weight on performance, right ?

Could you explain it more, I didn’t get it. Am I not already removing/updating the completed ones, actually the “sent to modules” ones, via this ?

rayTable = {} 

So, does it work as you said ?

local func = nil
function moduleShoot.shoot(origin, destination, direction, deltaTime, velocity, raycastParams, filterTable, bullet, shootingBol)
	func = RunService.Heartbeat:Connect(function()
...
	if raycastResult then
				print(raycastResult.Instance)
				shootingBol = false
				func:Disconnect()
		else

Here is the final(?) product of it. What do you think ?

1 Like

My raycast doesn’t work
Here’s my script
local rayOrigin = script.Parent.Position
local rayDirection = workspace.Reciever.Position

local raycastParams = RaycastParams.new()
raycastParams.FilterDescendantsInstances = {script.Parent}
raycastParams.FilterType = Enum.RaycastFilterType.Exclude
raycastParams.IgnoreWater = true

while true do
raycastResult = workspace:Raycast(rayOrigin, rayDirection)
if raycastResult then
print(“Instance:”, raycastResult.Instance)
print(“Position:”, raycastResult.Position)
print(“Distance:”, raycastResult.Distance)
print(“Material:”, raycastResult.Material)
print(“Normal:”, raycastResult.Normal)
else
warn(“No raycast result!”)
wait(0.01)
end
end

and this is what i’m recieving in the output
09:40:30.044 No raycast result! (x2) - Server - Script:18
09:40:34.236 No raycast result! (x128) - Server - Script:18
09:40:44.370 No raycast result! (x118) - Server - Script:18
this still keeps going on although i placed the part directly in the ray