Gun debounces not working

I’m trying to make a weapon system that mostly works on the client side. Right now, I’m hung up on making sure the player can’t just spam bullets. I’ve tried several different debounces to prevent this, but none of them seem to work. I understand that every time the mouse is clicked, it’s a separate thread running, but all my debounces operate outside the scope of those threads; they should be ensuring that it shouldn’t matter. I’ve got a “firing” boolean that checks whether the gun is firing or not and I’ve got a “lastShot” number value that works with tick() to see if a certain amount of time has passed; the latter messes with the frame rate on the client whenever the gun is fired.

Below are all the relevant functions:

-- creates a ray to pass off as a projectile direction
local function generateRay(tool, origin)
	local direction
	local mousePosition = UIS:GetMouseLocation()
	local mouseRay = workspace.Camera:ViewportPointToRay(mousePosition.X, mousePosition.Y)
	local raycastParams = RaycastParams.new()
	raycastParams.FilterType = Enum.RaycastFilterType.Exclude
	raycastParams.FilterDescendantsInstances = {tool.Parent}
	local raycastResult = workspace:Raycast(mouseRay.Origin, mouseRay.Direction * 1000, raycastParams)
	if raycastResult then
		direction = origin + (raycastResult.Position - origin)
	else
		direction = origin + mouseRay.Direction * 1000
	end
	return direction
end

-- calculates what comes out of each shot
function calculateShot(tool, direction)
	local fireTable = {}
	for i = 1, FIRE_SHOT do
		local randomX, randomY, randomZ = math.random(-FIRE_SPREAD, FIRE_SPREAD), math.random(-FIRE_SPREAD, FIRE_SPREAD), math.random(-FIRE_SPREAD, FIRE_SPREAD)
		table.insert(fireTable, CFrame.new(tool.Handle.Barrel.WorldPosition, direction) * CFrame.Angles(math.rad(randomX), math.rad(randomY), math.rad(randomZ)))
	end
	return fireTable
end

-- handles a press of the trigger
function triggerPress(tool)

	local direction = generateRay(tool, tool.Handle.Barrel.WorldPosition)
	for i = 1, FIRE_BURST do
		if reloading then break end
		local directions = calculateShot(tool, direction)
		for _, path in directions do
			fire(tool, path, FIRE_SPEED, FIRE_RANGE, players.LocalPlayer)
		end
		gunFired:FireServer(tool, directions, FIRE_SPEED, FIRE_RANGE)
		
		ammo -= 1
		mag -= 1
		--print(tool.Name, mag, ammo)
		if mag <= 0 or ammo <= 0 then return end

		task.wait((1 / FIRE_RATE * 2) / FIRE_BURST)
	end

end

-- creates a bullet from a given weapon on the client
function fire(gun, cFrame, bulletSpeed, bulletRange, player)
	local gunBullet = projectiles:FindFirstChild(gun.Name)
	if not gunBullet then return end
	local bullet = gunBullet:Clone()
	bullet.CFrame = cFrame
	bullet.Parent = workspace.Projectiles
	local speed = bulletSpeed
	local params = RaycastParams.new()
	local iterations = math.round(bulletRange / (bulletSpeed * 1/60))
	params.FilterDescendantsInstances = {gun.Parent}
	local bulletConnection
	bulletConnection = run.Heartbeat:Connect(function(deltaTime)
		local position, nextPosition = bullet.Position, bullet.CFrame.LookVector * deltaTime * speed
		local result = workspace:Raycast(position, nextPosition, params)
		if result then	
			if bullet:GetAttribute("Explosive") == true then
				dealDamage:FireServer(gun, result.Position, true)
				bulletConnection:Disconnect()
				bullet:Destroy()
			else
				bulletConnection:Disconnect()
				bullet:Destroy()
				local human = result.Instance.Parent:FindFirstChildWhichIsA("Humanoid")
				if human and human.Health > 0 and not players:GetPlayerFromCharacter(human.Parent) and player == players.LocalPlayer then
					dealDamage:FireServer(gun, result.Instance)
				end
			end
		else
			bullet.Position = position + nextPosition
		end

		iterations -= 1
		if iterations < 0 then
			bulletConnection:Disconnect()
			bullet:Destroy()
		end
	end)
end
-- runs when the player pulls out a weapon or tool
character.ChildAdded:Connect(function(tool)
	if tool.ClassName ~= "Tool" then return end -- return if it's not a tool that's being added
	--zeroing out connections
	if equipConnection then equipConnection:Disconnect() equipConnection = nil end
	if unequipConnection then unequipConnection:Disconnect() unequipConnection = nil end
	-- grabbing the tool handle and attributes
	local handle = tool:WaitForChild("Handle")
	local automatic = tool:GetAttribute("Automatic") or nil
	-- function that runs when the tool is equipped
	equipConnection = tool.Equipped:Connect(function()
		--zeroing out connections
		if inputBegin or inputEnd or firing then
			inputBegin:Disconnect()
			inputEnd:Disconnect()
			inputBegin = nil
			inputEnd = nil
			firing = nil
		end
		FIRE_RATE, FIRE_SPEED, FIRE_RANGE, FIRE_SPREAD, FIRE_BURST, FIRE_SHOT, ammo, mag = queryInfo:InvokeServer(tool)
		-- function that runs when inputs begin
		inputBegin = UIS.InputBegan:Connect(function(input, GPE)
			if GPE then return end
			-- actions for if the mouse is pressed
			if input.UserInputType == Enum.UserInputType.MouseButton1 then
				-- runs for automatic guns
				if firing or reloading or ammo <= 0 or mag <= 0 then return end
				firing = true
				if automatic then
					while firing do
						if reloading or ammo <= 0 or mag <= 0 then break end
						if tick() - lastShot < 1 / FIRE_RATE then continue end
						print(tick() - lastShot)
						triggerPress(tool)
						lastShot = tick()
					end
				-- runs for semi-auto guns
				else
					triggerPress(tool)
					firing = false
				end
			end
			-- actions for if R is pressed
			if input.KeyCode == Enum.KeyCode.R then
				if not firing then reloading = true reload:FireServer(tool) end
			end
		end)
		-- function that runs when inputs end
		inputEnd = UIS.InputEnded:Connect(function(input, GPE)
			if GPE then return end
			-- stops firing loop for automatics
			if input.UserInputType == Enum.UserInputType.MouseButton1 then
				if firing then firing = false end
			end
		end)
	end)
	-- function that runs when the tool is unequipped
	unequipConnection = tool.Unequipped:Connect(function()
		if inputBegin then inputBegin:Disconnect() inputBegin = nil end
		if inputEnd then inputEnd:Disconnect() inputEnd = nil end
		firing = nil
		reloading = nil
		
		FIRE_RATE, FIRE_SPEED, FIRE_RANGE, FIRE_SPREAD, FIRE_BURST, FIRE_SHOT, ammo, mag = nil, nil, nil, nil, nil, nil, nil, nil
		cancelReload:FireServer(tool)
		lastShot = 0
	end)
end)

Please let me know if you need any more information.

2 Likes

I can’t see where you are setting FIRE_RATE.
Try putting

		print((1 / FIRE_RATE * 2) / FIRE_BURST)
		task.wait((1 / FIRE_RATE * 2) / FIRE_BURST)

to see what your debounce is being calculated at.

FIRE_RATE is initially set to nil, but when the tool is equipped, FIRE_RATE is set to the value of a NumberValue retrieved via a remote function. In my testing, it was set to a value of 5 (5 bullets per second → 0.2 seconds between each firing), and from what I can tell the spacing is applied correctly, right up until you start rapidly clicking, at which point things begin to break. This is what leads me to believe it’s an issue with the debounce itself, not the values I pass in.

(1 / FIRE_RATE * 2) / FIRE_BURST) is only a value that spaces each individual shot in an X-round burst gun. Added up over the whole for loop it should end up equaling 1 / FIRE_RATE.

The debounce doesn’t cover the input each time the MouseButton1 is clicked. The triggerPress function keeps getting called multiple times because each time you click the mouse it starts the whole inputBegin function in the bottom section, which calls the triggerPress function.

I think the debounce needs to be in the inputBegin function

I’m not sure what you mean. Both the debounces are already in the inputBegin function. I copied and pasted the tick() debounce into the semi-auto portion of the function to double-check and it still didn’t work. I should point out that doing that also made the frame rate problem worse.

But each and every time MouseButton1 is pressed you have this function, which fires the triggerPress function. It doesn’t have a wait in it so you can just mash the mousebutton1 and it’ll fire again.
What happens if you put the triggerPressed function inside this function so that it has to finish before the

		inputBegin = UIS.InputBegan:Connect(function(input, GPE)
			if GPE then return end
			-- actions for if the mouse is pressed
			if input.UserInputType == Enum.UserInputType.MouseButton1 then
				-- runs for automatic guns
				if firing or reloading or ammo <= 0 or mag <= 0 then return end
				firing = true
				if automatic then
					while firing do
						if reloading or ammo <= 0 or mag <= 0 then break end
						if tick() - lastShot < 1 / FIRE_RATE then continue end
						print(tick() - lastShot)
						triggerPress(tool)
						lastShot = tick()
					end
				-- runs for semi-auto guns
				else
					--triggerPress(tool)  try putting the entire triggerPress function here, not separately
					firing = false
				end
			end
			-- actions for if R is pressed
			if input.KeyCode == Enum.KeyCode.R then
				if not firing then reloading = true reload:FireServer(tool) end
			end
		end)

Are you suggesting I copy the whole of the triggerPress() function and paste it where you commented out the code? I don’t understand how that changes anything. I just tried it and it may have made it even worse; almost crashed Studio.

I think what it should be doing it completing the function with the debounce wait in it before making the debounce value (firing) false, rather than firing the triggerPress function inside the else and immediately changing firing to false which would allow the original check for the input function to allow it to be called again.
I may be completely wrong here though.:
You could troubleshoot the MouseButton1 input yes by dioing this

                print(firing, "  ", reloading, "  ", ammo, "  ", mag)
				if firing or reloading or ammo <= 0 or mag <= 0 then return end

It would let you know what your values are each time you hit an input, and from there you may be able to troubleshoot why spamming the button will cause that section of code to run again.

This approach helped me fix an issue with the duration of the task.wait() calls (syntax error, was waiting for too long). I’m currently using a heartbeat loop to print all the variables you suggested in the same vein as what you suggested, will get back to you.

I just tested it and made serious changes to the debounce system as a result. It appears to work now; here’s the code:

-- creates a ray to pass off as a projectile direction
local function generateRay(tool, origin)
	local direction
	local mousePosition = UIS:GetMouseLocation()
	local mouseRay = workspace.Camera:ViewportPointToRay(mousePosition.X, mousePosition.Y)
	local raycastParams = RaycastParams.new()
	raycastParams.FilterType = Enum.RaycastFilterType.Exclude
	raycastParams.FilterDescendantsInstances = {tool.Parent}
	local raycastResult = workspace:Raycast(mouseRay.Origin, mouseRay.Direction * 1000, raycastParams)
	if raycastResult then
		direction = origin + (raycastResult.Position - origin)
	else
		direction = origin + mouseRay.Direction * 1000
	end
	return direction
end

-- calculates what comes out of each shot
function calculateShot(tool, direction)
	local fireTable = {}
	for i = 1, FIRE_SHOT do
		local randomX, randomY, randomZ = math.random(-FIRE_SPREAD, FIRE_SPREAD), math.random(-FIRE_SPREAD, FIRE_SPREAD), math.random(-FIRE_SPREAD, FIRE_SPREAD)
		table.insert(fireTable, CFrame.new(tool.Handle.Barrel.WorldPosition, direction) * CFrame.Angles(math.rad(randomX), math.rad(randomY), math.rad(randomZ)))
	end
	return fireTable
end

-- handles a press of the trigger
function triggerPress(tool, automatic)
	local direction = generateRay(tool, tool.Handle.Barrel.WorldPosition)
	for i = 1, FIRE_BURST do
		if reloading then break end
		local directions = calculateShot(tool, direction)
		for _, path in directions do
			fire(tool, path, FIRE_SPEED, FIRE_RANGE, players.LocalPlayer)
		end
		gunFired:FireServer(tool, directions, FIRE_SPEED, FIRE_RANGE)
		ammo -= 1
		mag -= 1
		ammoLabel.Text = mag .. " / " .. ammo
		if mag <= 0 or ammo <= 0 then reloading = true end
		task.wait((1 / (2 * FIRE_RATE)) / FIRE_BURST)
	end
	task.wait(1 / (2 * FIRE_RATE))
	if (trigger == true and not automatic) or trigger == false then firing = false end
end

-- creates a bullet from a given weapon on the client
function fire(gun, cFrame, bulletSpeed, bulletRange, player)
	local gunBullet = projectiles:FindFirstChild(gun.Name)
	if not gunBullet then return end
	local bullet = gunBullet:Clone()
	bullet.CFrame = cFrame
	bullet.Parent = workspace.Projectiles
	local speed = bulletSpeed
	local params = RaycastParams.new()
	local iterations = math.round(bulletRange / (bulletSpeed * 1/60))
	params.FilterDescendantsInstances = {gun.Parent}
	local bulletConnection
	bulletConnection = run.Heartbeat:Connect(function(deltaTime)
		local position, nextPosition = bullet.Position, bullet.CFrame.LookVector * deltaTime * speed
		local result = workspace:Raycast(position, nextPosition, params)
		if result then	
			if bullet:GetAttribute("Explosive") == true then
				dealDamage:FireServer(gun, result.Position, true)
				bulletConnection:Disconnect()
				bullet:Destroy()
			else
				bulletConnection:Disconnect()
				bullet:Destroy()
				local human = result.Instance.Parent:FindFirstChildWhichIsA("Humanoid")
				if human and human.Health > 0 and not players:GetPlayerFromCharacter(human.Parent) and player == players.LocalPlayer then
					dealDamage:FireServer(gun, result.Instance)
				end
			end
		else
			bullet.Position = position + nextPosition
		end

		iterations -= 1
		if iterations < 0 then
			bulletConnection:Disconnect()
			bullet:Destroy()
		end
	end)
end
-- runs when the player pulls out a weapon or tool
character.ChildAdded:Connect(function(tool)
	if tool.ClassName ~= "Tool" then return end -- return if it's not a tool that's being added
	--zeroing out connections
	if equipConnection then equipConnection:Disconnect() equipConnection = nil end
	if unequipConnection then unequipConnection:Disconnect() unequipConnection = nil end
	-- grabbing the tool handle and attributes
	local handle = tool:WaitForChild("Handle")
	local automatic = tool:GetAttribute("Automatic") or nil
	-- function that runs when the tool is equipped
	equipConnection = tool.Equipped:Connect(function()
		--zeroing out connections
		if inputBegin or inputEnd or firing then
			inputBegin:Disconnect()
			inputEnd:Disconnect()
			inputBegin = nil
			inputEnd = nil
			firing = nil
		end
		
		FIRE_RATE, FIRE_SPEED, FIRE_RANGE, FIRE_SPREAD, FIRE_BURST, FIRE_SHOT, ammo, mag = queryInfo:InvokeServer(tool)
		ammoLabel.Text = mag .. " / " .. ammo
		ammoLabel.Visible = true
		-- function that runs when inputs begin
		inputBegin = UIS.InputBegan:Connect(function(input, GPE)
			if GPE then return end
			-- actions for if the mouse is pressed
			if input.UserInputType == Enum.UserInputType.MouseButton1 then
				trigger = true
				-- runs for automatic guns
				if firing or reloading or ammo <= 0 or mag <= 0 then return end
				firing = true
				if automatic then
					while firing do
						if reloading or ammo <= 0 or mag <= 0 then firing = false break end
						triggerPress(tool, true)
					end
				-- runs for semi-auto guns
				else
					triggerPress(tool, false)
				end
			end
			-- actions for if R is pressed
			if input.KeyCode == Enum.KeyCode.R then
				if not firing then reloading = true reload:FireServer(tool) end
			end
		end)
		-- function that runs when inputs end
		inputEnd = UIS.InputEnded:Connect(function(input, GPE)
			if GPE then return end
			-- stops firing loop for automatics
			if input.UserInputType == Enum.UserInputType.MouseButton1 then
				trigger = false
			end
		end)
	end)
	-- function that runs when the tool is unequipped
	unequipConnection = tool.Unequipped:Connect(function()
		if inputBegin then inputBegin:Disconnect() inputBegin = nil end
		if inputEnd then inputEnd:Disconnect() inputEnd = nil end
		trigger = nil
		firing = nil
		reloading = nil
		
		FIRE_RATE, FIRE_SPEED, FIRE_RANGE, FIRE_SPREAD, FIRE_BURST, FIRE_SHOT, ammo, mag = nil, nil, nil, nil, nil, nil, nil, nil
		ammoLabel.Visible = false
		cancelReload:FireServer(tool)
	end)
end)
1 Like

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.