Adding spread to a raycasting gun

I’m pretty bad at scripting guns in general, so this is a challenge for me. I was wondering how I could add ‘spread’ to the bullet that is fired from the gun. Right now this is how I’m handling the bullet creation:

local function renderLocalBullet(dataArray)
	local startPos			= dataArray[1]
	local endPos			= dataArray[2]
	local brickColor		= dataArray[3]
	
	local newBullet			= Instance.new('Part')
	newBullet.BrickColor	= brickColor
	newBullet.Transparency	= 0.5
	newBullet.Reflectance	= 0
	newBullet.Material		= Enum.Material.Neon
	newBullet.Anchored		= true
	newBullet.Locked		= true
	newBullet.CanCollide	= false		
	
	newBullet.Size			= Vector3.new(.1, .1, (startPos - endPos).magnitude)
	newBullet.CFrame		= CFrame.new(startPos, endPos) * CFrame.new(0, 0, -newBullet.Size.z / 2)
	newBullet.Parent		= workspaceService
					
	debrisService:AddItem(newBullet, 0.05)
end

The dataArray is as follows:

local startPos	= localCharacter:WaitForChild('Head').CFrame.p
local mousePos	= localMouse.Hit.p
							
local hitPart, hitPosition	= workspaceService:FindPartOnRay(Ray.new(startPos, (mousePos - startPos).unit * 999), localCharacter)
		
renderLocalBullet({newTool.Barrel.Position, hitPosition, localPlayer.TeamColor})

Any help would be appreciated as I’ve been trying to do this for a while with no result, end goal is to create a variable called ‘bulletSpread’ that will be a multiplier of sorts of how much spread there is on the gun.

Thanks!

7 Likes

You can store the bullet spread as some parameters for a distribution. Then use those parameters to generate an angular spread from that distribution, via CFrame.Angles.

In the simplest case, the distribution is uniform, so that you can compute the direction vector with spread applied as CFrame.Angles(sy*r1,sx*r2,0)*mDirection with r1,r2 taking values in the interval (1,1) (you could generate them with the formula 2*math.random()-1).

If you want something somewhat more realistic, you can sample the random values from a normal distribution (i.e. bell curve) or an approximation to a normal distribution. In this case, the relevant parameter is the standard deviation of the normal distribution. In general, bullets will tend to hit closer to the center on average, while still being random.

2 Likes

I’m very new to Lua and I’m going to be completely honest I need a more simplified explanation, as I do not know how to do any of this, apologies.

My bad. Basically, you can use CFrames to rotate the path of the ray by an angle. For each bullet the angle is random, but for a simple raycasting gun the angle can be computed as (2*math.random()-1)*maxSpreadAngle.

If you were to have a horizontal and vertical angle for the spread, your bullets would be randomly distributed within a box shape and look something like this: https://www.mathworks.com/matlabcentral/answers/uploaded_files/23824/1.bmp

Alright I understand that now, however how would I modify my current CFrame for the bullet to use CFrame.Angles?

You take your bullet spread CFrame, and apply it to the ray’s direction vector: mDirection = cframe:VectorToWorldSpace(mDirection).

1 Like

Alternatively, if you wanted the bullet spread to be circular, you could generate the CFrame like so: cframe = CFrame.Angles(math.random()*maxSpreadAngle,0,2*math.pi*math.random()). This is equivalent to setting the direction of the spread to a random angle, then applying a random spread.

2 Likes

I know this is wrong but how would I correct this?

newBullet.Size= Vector3.new(.1, .1, (startPos - endPos).magnitude)
newBullet.CFrame= CFrame.new(startPos, endPos) * CFrame.new(0, 0, -newBullet.Size.z / 2)
newBullet.CFrame = CFrame.Angles(math.random()* 10, 0, 2 * math.pi * math.random())

I need to set the size of the bullet at the same time as applying the angles at which it is rotated, I’m not sure how

I would apply the bullet spread to endPos, i.e. endPos = startPos+cframe:VectorToWorldSpace(endPos-startPos). Then everything else is calculated automatically.

Also, it looks like you’re trying to apply a 10 degree spread, but CFrame.Angles accepts its arguments in radians, so you want the multiplier to be math.rad(10).

2 Likes

Alright, and by endPos do you mean the hitPosition (where the Ray hits the player at it’s end) in my case?

Nevermind I just realised endPos is my dataArray[2], which just turns out to be hitPosition.

1 Like

Well the spread certainly works! However as you can see there’s a small issue…

https://gyazo.com/38a5c2272036d35b6a664d3d178d6730

Here’s my code:

    local startPos	= localCharacter:WaitForChild('Head').CFrame.p
	local mousePos	= localMouse.Hit.p
						
	local hitPart, hitPosition	= workspaceService:FindPartOnRay(Ray.new(startPos, (mousePos - startPos).unit * 999), localCharacter)
	
	local maxSpreadAngle = math.rad(0.5)
	local cframe = CFrame.Angles(math.random()* maxSpreadAngle,0,2*math.pi*math.random())		
	hitPosition = startPos + cframe:VectorToWorldSpace(hitPosition - startPos)
	
	renderLocalBullet({newTool.Barrel.Position, hitPosition, localPlayer.TeamColor})	
	remoteEvent:FireServer(securityKey, 'RENDERBULLET', {newTool.Barrel.Position, hitPosition, localPlayer.TeamColor})

No matter what number I set maxSpreadAngle to, it’s the same.

1 Like

I think I screwed up, although I’m not sure if this is causing that bizarre behavior. Try computing cframe as CFrame.Angles(0,0,z)*CFrame.Angles(x,0,0).

Edit: disregard that. Let me think for a minute

Alright.

Okay, so first of all you should be applying the bullet spread before you do the raycasting, otherwise your bullet and your ray don’t match. In other words, you want to apply the spread to mousePos, then cast the ray.
Actually, since you have a bullet spread, the bullet no longer lands at mousePos, which means you have to extend the Ray instead of relying on it terminating at mousePos.

Secondly, I messed up by not converting to world coordinates. You should cast the ray in the direction CFrame.new(startPos,mousePos):ToWorldSpace(CFrame.Angles(0,0,Z)*CFrame.new(X,0,0)).lookVector. X and Z are the same as before in the definition of ‘cframe’.

Alright here’s how I solved the issue:

local startPos	= localCharacter:WaitForChild('Head').CFrame.p
local mousePos	= localMouse.Hit.p

local spreadInterval = (newTool.Barrel.Position - mousePos).magnitude
local Min, Max = -(gunConfig.bulletSpread/100) * spreadInterval, (gunConfig.bulletSpread/100) * spreadInterval
local finalBullet = Vector3.new((mousePos.X)+(math.random(Min, Max)), (mousePos.Y)+(math.random(Min, Max)), (mousePos.Z)+(math.random(Min, Max)))
					
local hitPart, hitPosition	= workspaceService:FindPartOnRay(Ray.new(startPos, (mousePos - startPos).unit * 999), localCharacter)

renderLocalBullet({newTool.Barrel.Position, finalBullet, localPlayer.TeamColor})	
remoteEvent:FireServer(securityKey, 'RENDERBULLET', {newTool.Barrel.Position, hitPosition, localPlayer.TeamColor})

Spread works now, I appreciate you helping for so long, so thank you.

5 Likes

thanks for putting the entire script, i’m was confused obs: i put the securitykey in my script too to avoid hacking

1 Like