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.
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.
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.
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")
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