Performant projectiles?

-- Script inside GetPlayerInput
local tool = script.Parent
local handle = tool:WaitForChild("Handle")
local firePoint = handle:WaitForChild("FirePoint")
local FireSound = handle:WaitForChild("Fire")
local sID = 8561647924
local mouseEvent = script.Parent:WaitForChild("MouseEvent")
local projectiles = game:GetService("ServerStorage"):WaitForChild("Projectiles")

--// Fire Rate Control
local fireRate = 0.1
local lastFireTime = {}

-- // Customizable Variables
local speed = 300
local damage = 15
local bulletMaterial = Enum.Material.Neon
local bulletSize = Vector3.new(0.2, 0.2, 0.3)
local bulletColor = Color3.fromRGB(255, 234, 0)
local bulletLife = 4

function fire()
	local cl = FireSound:Clone()
	cl.Parent = FireSound.Parent
	cl:Play() 
	game:GetService("Debris"):AddItem(cl, cl.TimeLength)
end

-- Function to create and fire a bullet
local function createBullet(player, mousePosition)
	-- Create the bullet
	local bullet = projectiles.BulletPistol:Clone()
	bullet.Name = "Bullet"
	bullet.Material = bulletMaterial
	bullet.Size = bulletSize
	bullet.Color = bulletColor -- Set to your desired color
	bullet.CanCollide = false
	bullet.Anchored = false
	bullet.CFrame = firePoint.WorldCFrame
	

	-- Parent the bullet to the Workspace
	bullet.Parent = game.Workspace
	print("Bullet created at position:", bullet.Position)
	-- Calculate direction and velocity
	local direction = (mousePosition - bullet.Position).Unit
	local velocity = direction * speed

	-- Set the bullet's velocity
	bullet.AssemblyLinearVelocity = velocity
	print("Bullet velocity set to:", bullet.AssemblyLinearVelocity)
	-- Add cleanup after some time (e.g., 5 seconds)
	game.Debris:AddItem(bullet, 3)

	bullet.Touched:Connect(function(otherPart)
		if otherPart.Parent ~= player.Character or otherPart.Name ~= "Handle" then
			if otherPart.Name == "Bullet" then return end
			local humanoid = otherPart.Parent:FindFirstChild("Humanoid")
			if humanoid and humanoid ~= player.Character:FindFirstChild("Humanoid") then
				humanoid:TakeDamage(damage)
				bullet:Destroy()
			end
		end
		if otherPart:IsA("BasePart") and not otherPart.Parent:FindFirstChild("Humanoid") then
			if otherPart.Name == "Bullet" then return end
			if otherPart:GetAttribute("IsWindow") then
				local hp = otherPart:GetAttribute("Health")
				hp -= damage
				otherPart:SetAttribute("Health", hp)
			else
				local snd = Instance.new("Sound")
				snd.Parent = bullet
				snd.Name = "ImpactPart"
				snd.RollOffMode = Enum.RollOffMode.Linear
				snd.RollOffMaxDistance = 150
				snd.RollOffMinDistance = 0
				snd.SoundId = "rbxassetid://4427232788"
				snd.Pitch = math.random(0.8, 1.4)
				snd:Play()
			end
		end
	end)
end

mouseEvent.OnServerEvent:Connect(function(player, mousePosition)
	local playerId = player.UserId

	if not lastFireTime[playerId] then
		lastFireTime[playerId] = 0
	end

	if tick() - lastFireTime[playerId] >= fireRate then
		fire()
		createBullet(player, mousePosition)
		lastFireTime[playerId] = tick()
	end
end)

I’m making guns for my game, but here’s the thing:
I decided to switch to cloning for testing purposes, but I there’s this tiny delay before the bullet moves, like a frame or so, is that normal? How do I prevent that?
Also, should I use bodymovers instead?

(Additionally: Feel free to optimize my script)

Hey, I just saw this, and though I don’t have the time to write an example I can explain probably the best way to do this.

Server

  • Use any info passed from the client like the speed and direction to calculate drop and other values.
  • Then, using these values, have the server move the CFrame of the Projectile or to update/create a method to move the projectile.
  • Then you could use a raycast or hitbox module (If you plan to do HitDetection on the server)

Client

  • Tell the server (using a Remote/Bindable) direction, speed and any other values like drop so that the server can use the values to move the projectile, thus it’s movements will be automatically replicated for all clients.
  • Sometimes I’d actually have hit detection on the client (depending on the situation), and then fire to the server, to verify the hit. If you doing HitDetection on the Client. RaycastHitboxV4 is quite good, I’m also pretty sure it works on the server, though it may be less performant than if it is used on the client.

Performance review of your code

First off, I’d like to warn against using .Touched due to there being a lot more accurate and efficient alternatives, like RaycastHitboxV4.

For now that’s all the time I have, there are a few other areas where this could improve on, but I wish you luck!

1 Like

Ah, thank you!
So basically, just put all the values on the client and just transfer them to the server.

Along with some other stuff.

Pretty much, what I would do for the client would be:

  • Send the server the Speed, and Direction. (Use a RemoteFunction to call a Function that creates the projectile and returns the projectile)
  • Then when you have the Projectile Defined from that remote, use the HitBox module to detect a hit, and send it to the server
-- Variables
local tool = script.Parent
local player = game:GetService("Players").LocalPlayer or game:GetService("Players").PlayerAdded:Wait()
local char = player.Character or player.CharacterAdded:Wait()
local hum = char:WaitForChild("Humanoid")
local root = char:WaitForChild("HumanoidRootPart")
local mouse = player:GetMouse()
local mouseEvent = script.Parent:WaitForChild("MouseEvent")
local currentCamera = workspace.CurrentCamera
local automaticMode = true -- Toggle for automatic firing mode
local firing = false -- Debounce to prevent spamming
local active = false

-- // Customizable Variables
local speed = 100
local damage = 15
local bulletLife = 4
local bulletType = "BulletPistol"

-- Fire rate control (adjust as necessary)
local fireRate = 0.1
local lastFireTime = 0



-- ShiftLock Toggle
function toggleShiftLock(active) 
	if active then 
		---------------------------------------------------------
		game:GetService("RunService"):BindToRenderStep("ShiftLock", Enum.RenderPriority.Character.Value, function()
			game:GetService("UserInputService").MouseBehavior = Enum.MouseBehavior.LockCenter 
			
			local _, y = workspace.CurrentCamera.CFrame.Rotation:ToEulerAnglesYXZ() --Get the angles of the camera
			root.CFrame = CFrame.new(root.Position) * CFrame.Angles(0,y,0) 
			
			hum.AutoRotate = false
			hum.CameraOffset = Vector3.new(1.75,.5,0) 
			
			mouse.Icon = "rbxassetid://12829852445"
		end) 
		---------------------------------------------------------
	elseif not active then
		---------------------------------------------------------
		game:GetService("RunService"):UnbindFromRenderStep("ShiftLock")
		game:GetService("UserInputService").MouseBehavior = Enum.MouseBehavior.Default 
		
		hum.AutoRotate = true
		hum.CameraOffset = Vector3.new(0,0,0) 
		
		mouse.Icon = ""
	end
end


-- Function to fire bullets
local function fireBullet()
	if tick() - lastFireTime >= fireRate then
		mouseEvent:FireServer(mouse.Hit.Position, speed, damage, bulletLife, fireRate, bulletType)
		lastFireTime = tick()
	end
end

-- Function to handle mouse button input
local function onMouseButtonDown()
	if firing then return end
	if automaticMode then
		while mouseButton1Down do
			fireBullet()
			task.wait(fireRate) -- Ensuring bullets are fired at a controlled rate
		end
	else
		firing = true
		fireBullet()
		task.wait(fireRate)
		firing = false
	end
end

-- Detecting mouse button down event
mouse.Button1Down:Connect(function()
	if not active then return end
	mouseButton1Down = true
	onMouseButtonDown()
end)

-- Detecting mouse button up event (to stop automatic fire)
mouse.Button1Up:Connect(function()
	mouseButton1Down = false
end)

-- Detecting if tool equipped to change mouse/cam
tool.Equipped:Connect(function()
	active = true
	toggleShiftLock(active)
end)

-- Detecting if tool unequipped to change mouse/cam
tool.Unequipped:Connect(function()
	active = false
	toggleShiftLock(active)
end)

This is what my local script looks like, everything look good?

One sec lemme look over it rq.

Yeah everything looks good here, and on the server I assume you’re moving the projectile doing HitDetection there?

I’m cloning on the server, moving the projectile and using HitboxRaycast for HitDetection

Alright yea, sounds good, should be quite efficient, if you do notice any severe frame drop or lag, it would most likely come from either a memory leak with the hitboxes or memory leaks with the client, if it’s the client (LocalScript) that causes issues, I’d try and organize your loops into threads and then use a coroutine to schedule them, so they can run along side non cyclic code.

Hope this helps, God bless, and have a great day!

1 Like

Thank you! Have a nice day too, I’ll be sure to reach out should I have troubles.

No worries, if it work’s make sure to mark the topic as solved, so people know you don’t still need help with it!

I’ve been getting an error saying:
Players.RetroPect.Backpack.TestGun.MainControl:39: attempt to perform arithmetic (sub) on nil and Vector3

-- Variables
local tool = script.Parent
local player = game:GetService("Players").LocalPlayer or game:GetService("Players").PlayerAdded:Wait()
local char = player.Character or player.CharacterAdded:Wait()
local hum = char:WaitForChild("Humanoid")
local root = char:WaitForChild("HumanoidRootPart")
local mouse = player:GetMouse()
local mouseEvent = script.Parent:WaitForChild("MouseEvent")
local currentCamera = workspace.CurrentCamera
local automaticMode = true -- Toggle for automatic firing mode
local firing = false -- Debounce to prevent spamming
local active = false

-- // Customizable Variables
local speed = 100
local damage = 15
local bulletLife = 4

-- Fire rate control (adjust as necessary)
local fireRate = 0.1
local lastFireTime = 0



-- ShiftLock Toggle
function toggleShiftLock(active) 
	if active then 
		---------------------------------------------------------
		game:GetService("RunService"):BindToRenderStep("ShiftLock", Enum.RenderPriority.Character.Value, function()
			game:GetService("UserInputService").MouseBehavior = Enum.MouseBehavior.LockCenter 
			
			local _, y = workspace.CurrentCamera.CFrame.Rotation:ToEulerAnglesYXZ() --Get the angles of the camera
			root.CFrame = CFrame.new(root.Position) * CFrame.Angles(0,y,0) 
			
			hum.AutoRotate = false
			hum.CameraOffset = Vector3.new(1.75,.5,0) 
			
			mouse.Icon = "rbxassetid://12829852445"
		end) 
		---------------------------------------------------------
	elseif not active then
		---------------------------------------------------------
		game:GetService("RunService"):UnbindFromRenderStep("ShiftLock")
		game:GetService("UserInputService").MouseBehavior = Enum.MouseBehavior.Default 
		
		hum.AutoRotate = true
		hum.CameraOffset = Vector3.new(0,0,0) 
		
		mouse.Icon = ""
	end
end


-- Function to fire bullets
local function fireBullet()
	if tick() - lastFireTime >= fireRate then
		mouseEvent:FireServer(mouse.Hit.Position, speed, damage, bulletLife, fireRate, bulletType)
		lastFireTime = tick()
	end
end

-- Function to handle mouse button input
local function onMouseButtonDown()
	if firing then return end
	if automaticMode then
		while mouseButton1Down do
			fireBullet()
			task.wait(fireRate) -- Ensuring bullets are fired at a controlled rate
		end
	else
		firing = true
		fireBullet()
		task.wait(fireRate)
		firing = false
	end
end

-- Detecting mouse button down event
mouse.Button1Down:Connect(function()
	if not active then return end
	mouseButton1Down = true
	onMouseButtonDown()
end)

-- Detecting mouse button up event (to stop automatic fire)
mouse.Button1Up:Connect(function()
	mouseButton1Down = false
end)

-- Detecting if tool equipped to change mouse/cam
tool.Equipped:Connect(function()
	active = true
	toggleShiftLock(active)
end)

-- Detecting if tool unequipped to change mouse/cam
tool.Unequipped:Connect(function()
	active = false
	toggleShiftLock(active)
end)

Local Script ^

Server Script:

-- Script inside GetPlayerInput
local tool = script.Parent
local handle = tool:WaitForChild("Handle")
local firePoint = handle:WaitForChild("FirePoint")
local FireSound = handle:WaitForChild("Fire")
local mouseEvent = script.Parent:WaitForChild("MouseEvent")
local projectiles = game:GetService("ServerStorage"):WaitForChild("Projectiles")
local hitbox = require(game:GetService("ReplicatedStorage").RaycastHitboxV4)
local bulletType = "BulletPistol"

local lastFireTime = {}

function fire()
	local cl = FireSound:Clone()
	cl.Parent = FireSound.Parent
	cl:Play() 
	game:GetService("Debris"):AddItem(cl, cl.TimeLength)
end

mouseEvent.OnServerEvent:Connect(function(player, mousePosition, speed, damage, bulletLife, fRate)
	local fireRate = fRate
	local function createBullet(player, mousePosition, speed, damage, bulletLife)
		local bullet = projectiles:FindFirstChild(bulletType):Clone()
		bullet.CFrame = firePoint.WorldCFrame

		local bulletHitbox = hitbox.new(bullet)
		local Params = RaycastParams.new()
		Params.FilterDescendantsInstances = {tool.Parent} --- remember to define our character!
		Params.FilterType = Enum.RaycastFilterType.Exclude
		local bodyForce = Instance.new("BodyForce")




		-- Parent the bullet to the Workspace
		bullet.Parent = game.Workspace

		--bullet.AssemblyLinearVelocity = velocity
		local forceDirection = (mousePosition - bullet.Position).Unit
		local forceStrength = speed
		bodyForce.Force = forceDirection * forceStrength
		print("BodyForce velocity = "..bodyForce.Force.." BodyForce dir = "..forceDirection)
		bodyForce.Parent = bullet
		print("Bullet velocity set to:", bullet.AssemblyLinearVelocity)
		-- Add cleanup after some time (e.g., 5 seconds)
		game.Debris:AddItem(bullet, bulletLife)




		bulletHitbox.OnHit:Connect(function(otherPart, humanoid)
			if otherPart.Parent ~= player.Character or otherPart.Name ~= "Handle" then
				if otherPart.Name == "Bullet" then return end
				local humanoid = otherPart.Parent:FindFirstChild("Humanoid")
				if humanoid and humanoid ~= player.Character:FindFirstChild("Humanoid") then
					humanoid:TakeDamage(damage)
					bullet:Destroy()
				end
			end
			if otherPart.Parent.ClassName == "Accessory" and otherPart.Name == "Handle" then
				if otherPart.Parent.Parent ~= player.Character then
					if otherPart.Parent.Parent.Name == "Bullet" then return end
					local humanoid = otherPart.Parent.Parent:FindFirstChild("Humanoid")
					if humanoid and humanoid ~= player.Character:FindFirstChild("Humanoid") then
						humanoid:TakeDamage(damage)
						bullet:Destroy()
					end
				end
			end
			if otherPart:IsA("BasePart") and not otherPart.Parent:FindFirstChild("Humanoid") then
				if otherPart.Name == "Bullet" then return end
				if otherPart:GetAttribute("IsWindow") then
					local hp = otherPart:GetAttribute("Health")
					hp -= damage
					otherPart:SetAttribute("Health", hp)
				else
					local snd = Instance.new("Sound")
					snd.Parent = bullet
					snd.Name = "ImpactPart"
					snd.RollOffMode = Enum.RollOffMode.Linear
					snd.RollOffMaxDistance = 150
					snd.RollOffMinDistance = 0
					snd.SoundId = "rbxassetid://4427232788"
					snd.Pitch = math.random(0.8, 1.4)
					snd:Play()
				end
			end
		end)
		bulletHitbox:HitStart()
	end
	
	local playerId = player.UserId

	if not lastFireTime[playerId] then
		lastFireTime[playerId] = 0
	end

	if tick() - lastFireTime[playerId] >= fireRate then
		fire()
		createBullet()
		lastFireTime[playerId] = tick()
	end
end)

I don’t get why it’s not firing the mouse position

Sorry for the late response, can I add you on discord, I can help you better from there.
My user: SwedishAeternum (SwedishAeternum#0332)

I sent you a dm on discord, I found out the reason behind your error, aka you forgot to pass the parameters through the createBullet() function on about ~ Line 100 (Server Script)

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