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.