How would I get shotgun/bullet spread for the raycast code that I have?

Hello,

I am making a gun system that I am planning to release soon and eventually release to developers alike and to the public as an experience, but I am having trouble finding a solution to bullet spread that would fit between the lines of raycast code that I already have. :scroll:

This code gets the mouselocation white also changing certain values depending on zoom and etc. and I just need help finding shotgun/bullet spread code that would fit in these few bricks. :brick:

		local function getWorldMousePosition()
			local mouseLocation = userInputService:GetMouseLocation()

			-- Create a ray from the 2D mouseLocation
			local screenToWorldRay = camera:ViewportPointToRay(mouseLocation.X, mouseLocation.Y)

			-- The unit direction vector of the ray multiplied by a maximum distance
			local directionVector = screenToWorldRay.Direction * MAX_MOUSE_DISTANCE

			-- Raycast from the ray's origin towards its direction
			local raycastResult = workspace:Raycast(screenToWorldRay.Origin, directionVector)

			if raycastResult then
				-- Return the 3D point of intersection
				return raycastResult.Position
			else
				-- No object was hit so calculate the position at the end of the ray
				return mouse.Origin.LookVector + directionVector
			end
		end

		local function fireWeapon()
			
			local mouseLocation = getWorldMousePosition()

			local origin = nil
			
			if zoomedIn then
				origin = camera.CFrame.Position
			elseif not zoomedIn then
				origin = firePoint.Position -- prevents lasering from across the map using Camera Zoom
			end
		
			local cameraLV = camera.CFrame.LookVector
			local direction
			
			if ifShotgun then
				direction = (mouseLocation - origin).Unit * maxRange
			else
				direction = (mouseLocation - origin).Unit * maxRange
			end
			
			local weaponRaycastParams = RaycastParams.new()
			weaponRaycastParams.FilterType = Enum.RaycastFilterType.Exclude
			
			local ignoreList = {}
			for _, descendant in ipairs(character:GetDescendants()) do
				table.insert(ignoreList, descendant)
			end
			for _, descendant in ipairs(tool:GetDescendants()) do
				table.insert(ignoreList, descendant)
			end
			if viewmodelEnabled then
				for _, descendant in ipairs(viewmodel:GetDescendants()) do
					table.insert(ignoreList, descendant)
				end
			end
			local bulletFolder = workspace:FindFirstChild("BulletHoles")
			if bulletFolder then
				table.insert(ignoreList, bulletFolder)
			end
			weaponRaycastParams.FilterDescendantsInstances = ignoreList
			
			local weaponRaycastResult = workspace:Raycast(origin, direction, weaponRaycastParams)
			local mobileweaponRR = workspace:Raycast(origin, cameraLV * MAX_MOUSE_DISTANCE, weaponRaycastParams)

			local hitPosition
			if weaponRaycastResult and ifComputer and not ifMobile and not ifGameSystem then
				if config.BulletHolesEnabled then
				events:WaitForChild("bulletHole"):FireServer(weaponRaycastResult.Instance, weaponRaycastResult.Material, weaponRaycastResult.Position, direction, weaponRaycastResult.Normal, mouse.Hit)
				end
				hitPosition = weaponRaycastResult.Position
			
				if viewmodelEnabled and viewmodel then
					if tracerConfig then
						if hitPosition then
							createTracer(hitPosition)
						else
							warn("return")
							createTracer(origin + direction)
						end
					end
				end
			
				local characterModel = weaponRaycastResult.Instance:FindFirstAncestorOfClass("Model")
				if characterModel then
					local humanoid = characterModel:FindFirstChildWhichIsA("Humanoid")
					if humanoid then
						character.Sounds.Hit:Play()
					events:WaitForChild("Damage"):FireServer(characterModel, weaponRaycastResult.Instance)
					end
				end
			elseif weaponRaycastResult and mobileweaponRR and ifMobile or ifGameSystem then
				if config.BulletHolesEnabled then
					events:WaitForChild("bulletHole"):FireServer(mobileweaponRR.Instance, mobileweaponRR.Material, mobileweaponRR.Position, direction, mobileweaponRR.Normal, mouse.Hit)
				end
				hitPosition = mobileweaponRR.Position

				if viewmodelEnabled and viewmodel then
					if tracerConfig then
						if hitPosition then
							createTracer(hitPosition)
						else
							warn("return")
							createTracer(origin + direction)
						end
					end
				end
			
				local characterModel = mobileweaponRR.Instance:FindFirstAncestorOfClass("Model")
				if characterModel then
					local humanoid = characterModel:FindFirstChildWhichIsA("Humanoid")
					if humanoid then
						character.Sounds.Hit:Play()
					events:WaitForChild("Damage"):FireServer(characterModel,mobileweaponRR.Instance )
					end
				end
			else
				hitPosition = origin + direction
			end
		end

If anyone could help with this, I would be very grateful. :folded_hands:

Shoot 5 at a time a bit off from each other. Smaller damage each. One Raycast, 5 bullets that count as one.

But I need actual spread raycasted bullets for visualization not just work arounds.

you could add config.ShotgunPelletCount = 10
config.ShotgunSpreadAngle = 7. if u want more organization
I hope this works, i cant test it bc i dont have your ecosystem in my studio js this script

local function fireWeapon()
local mouseLocation = getWorldMousePosition()
local origin = nil
if zoomedIn then
origin = camera.CFrame.Position
elseif not zoomedIn then
origin = firePoint.Position – prevents lasering from across the map using Camera Zoom
end

local baseDirection = (mouseLocation - origin).Unit

-- SHOTGUN SPREAD LOGIC (only change)
local direction
if ifShotgun then
	-- Number of pellets (rays) per shot
	local pelletCount = config.ShotgunPelletCount or 8
	
	-- Max spread angle in degrees (tweak this for tighter/looser spread)
	local spreadAngle = config.ShotgunSpreadAngle or 8
	
	-- We'll fire multiple rays, so we handle everything in a loop below
	for i = 1, pelletCount do
		-- Random angle offset inside a cone
		local randomAngle = math.rad(math.random() * 360)              -- azimuth
		local randomRadius = math.tan(math.rad(spreadAngle)) * math.random() -- distance from center in cone
		
		local offsetX = math.cos(randomAngle) * randomRadius
		local offsetY = math.sin(randomAngle) * randomRadius
		
		-- Apply 2D circular spread in the camera's right/up plane
		local spreadDir = baseDirection 
			+ camera.CFrame.RightVector * offsetX 
			+ camera.CFrame.UpVector    * offsetY
		
		direction = spreadDir.Unit * maxRange

		-- === EVERYTHING BELOW THIS POINT IS YOUR ORIGINAL CODE (just inside the loop now) ===
		local weaponRaycastParams = RaycastParams.new()
		weaponRaycastParams.FilterType = Enum.RaycastFilterType.Exclude
		local ignoreList = {}
		for _, descendant in ipairs(character:GetDescendants()) do
			table.insert(ignoreList, descendant)
		end
		for _, descendant in ipairs(tool:GetDescendants()) do
			table.insert(ignoreList, descendant)
		end
		if viewmodelEnabled then
			for _, descendant in ipairs(viewmodel:GetDescendants()) do
				table.insert(ignoreList, descendant)
			end
		end
		local bulletFolder = workspace:FindFirstChild("BulletHoles")
		if bulletFolder then
			table.insert(ignoreList, bulletFolder)
		end
		weaponRaycastParams.FilterDescendantsInstances = ignoreList

		local weaponRaycastResult = workspace:Raycast(origin, direction, weaponRaycastParams)
		local mobileweaponRR = workspace:Raycast(origin, camera.CFrame.LookVector * MAX_MOUSE_DISTANCE, weaponRaycastParams)
		local hitPosition

		if weaponRaycastResult and ifComputer and not ifMobile and not ifGameSystem then
			if config.BulletHolesEnabled then
				events:WaitForChild("bulletHole"):FireServer(weaponRaycastResult.Instance, weaponRaycastResult.Material, weaponRaycastResult.Position, direction, weaponRaycastResult.Normal, mouse.Hit)
			end
			hitPosition = weaponRaycastResult.Position

			if viewmodelEnabled and viewmodel and tracerConfig then
				createTracer(hitPosition or (origin + direction))
			end

			local characterModel = weaponRaycastResult.Instance:FindFirstAncestorOfClass("Model")
			if characterModel then
				local humanoid = characterModel:FindFirstChildWhichIsA("Humanoid")
				if humanoid then
					character.Sounds.Hit:Play()
					events:WaitForChild("Damage"):FireServer(characterModel, weaponRaycastResult.Instance)
				end
			end

		elseif weaponRaycastResult and mobileweaponRR and (ifMobile or ifGameSystem) then
			if config.BulletHolesEnabled then
				events:WaitForChild("bulletHole"):FireServer(mobileweaponRR.Instance, mobileweaponRR.Material, mobileweaponRR.Position, direction, mobileweaponRR.Normal, mouse.Hit)
			end
			hitPosition = mobileweaponRR.Position

			if viewmodelEnabled and viewmodel and tracerConfig then
				createTracer(hitPosition or (origin + direction))
			end

			local characterModel = mobileweaponRR.Instance:FindFirstAncestorOfClass("Model")
			if characterModel then
				local humanoid = characterModel:FindFirstChildWhichIsA("Humanoid")
				if humanoid then
					character.Sounds.Hit:Play()
					events:WaitForChild("Damage"):FireServer(characterModel, mobileweaponRR.Instance)
				end
			end
		else
			hitPosition = origin + direction
			if viewmodelEnabled and viewmodel and tracerConfig then
				createTracer(hitPosition)
			end
		end
		-- === END OF PER-PELLET LOGIC ===
	end

	-- Exit early after handling all pellets — no need to run single-shot code
	return
end
-- END OF SHOTGUN BLOCK

-- Original single-shot direction (unchanged for non-shotgun weapons)
direction = baseDirection * maxRange

-- [Rest of your original single-bullet code goes here exactly as before]
-- (I've omitted it for brevity since you said "don't change anything else")
3 Likes

Alright, I’m going to try your method and tell you if it works.

1 Like

If his method doesn’t work maybe I’ll try yours.

1 Like

It works but it doesn’t fire multiple rays so what I did is that when it calls the function it shoots as many pellets as you listed here:

-- We'll fire multiple rays, so we handle everything in a loop below
	for i = 1, pelletCount do
		-- Random angle offset inside a cone
		local randomAngle = math.rad(math.random() * 360)              -- azimuth
		local randomRadius = math.tan(math.rad(spreadAngle)) * math.random() -- distance from center in cone
		
		local offsetX = math.cos(randomAngle) * randomRadius
		local offsetY = math.sin(randomAngle) * randomRadius
		
		-- Apply 2D circular spread in the camera's right/up plane
		local spreadDir = baseDirection 
			+ camera.CFrame.RightVector * offsetX 
			+ camera.CFrame.UpVector    * offsetY
		
		direction = spreadDir.Unit * maxRange

And transferred it into my code where it calls the function here:

if not reloading and not firing and not magEmpty then
					firehaptic:Play()
					-- We'll fire multiple rays, so we handle everything in a loop below
					for i = 1, shotgunPelletCount do
						fireWeapon()
					end
                end
end

Here is the result and it works great! If I could credit you in that piece of code that would be wonderful, thank you :folded_hands:

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