How to make cone-shaped bullet spread?

So I recently added bullet spread to my gun system and it works fine but I’m not really satisfied with it. This is how I add the bullet spread right now:

bullet.CFrame = gunModel.Gun.Muzzle.CFrame * CFrame.Angles(math.random(-5000, 5000)/100000, 5000, 5000)/100000, 0)

(bullet is moved with its lookvector)
This does, in fact, make the bullets shoot out in a spread, but when many bullets are shot you can notice that they spread in a square,


and I would really prefer if it was more of a cone-shape spread. The problem is I have no idea how I could do this. Is there any simple formula or something I could use with my system to achieve this?

1 Like

the easier solution would be to just have an incredibly large area, so you cant see so much bullets clustered together. Otherwise you could use this guy’s suggestion for a similar problem I encountered: Spawning parts in a non-rectanglular area (evenly) - #4 by nicemike40?

3 Likes

I think the most natural approach is to randomly generate two numbers.

  1. θ in the range [0 - MaxSpread]
  2. φ in the range [0 - 2π]

These will give a uniform spread over a circular region in the distance.

If the gun was pointing in the (1,0,0) direction then the final bullet direction would be (Cos(θ), Sin(θ)Cos(φ), Sin(θ)Sin(φ)).

If we do the same for the y and z directions we can form the necessary rotation matrix:

| Cos(θ)      , Sin(θ)Sin(φ), Sin(θ)Cos(φ) |
| Sin(θ)Cos(φ), Cos(θ)      , Sin(θ)Sin(φ) |
| Sin(θ)Sin(φ), Sin(θ)Cos(φ), Cos(θ)       |

Edit: last two columns swapped

Also this is wrong, see - How to make cone-shaped bullet spread? - #9 by McThor2

1 Like

To be more clear you can make a CFrame using these matrix components that can just replace your current random CFrame.

1 Like

Actually, I’d recommend this different post by me :slight_smile:

2 Likes

I’ve decided to use mike’s solution because it’s much easier, but I would also really like to understand this if it’s not too much trouble. How could I use this to get the random CFrame? (I’ve been sitting for a while trying to understand this but I am unfortunately braindead when it comes to math)

2 Likes

This is in the realm of trigonometry. A quick rundown is that a full circle is 360° which is also 2π radians. One radii is the radius of a circle laid down along the circumference. Since π is the ratio of the diameter of a circle to it’s circumference, and the radius is 1/2 the diameter, then it takes 2π radians to make a complete circle. In math, we use something called a unit circle. That’s a circle with a radius of 1. The trigonometric functions (sine, cosine, tangent) are defined as ratios of the lengths of various legs of a right triangle. The easy way to remember this is SOHCAHTOA. See the below image.

image

To actually calculate the trig values on a computer, you need to do it in radians. The actual math library uses what is known as a power series (from Calculus). All the transindental functions can be computed using power series (Taylor series and MacLauren series are closely related).

Putting it all together, we have the following image which is a right triangle superimposed onto the unit circle.

image

So what you end up having is a circle with a radius of r (spread angle) centered on the main axis of a bullet traveling through 3D space that is exactly one unit from point of origin. That sets the maximum spread angle. The coordinate field at the actual target is defined as the projection of that circle onto the target…and to do that requires Calculus level math because vectors are involved (dot product and cross product).

I’m going to leave it there because if you really want to know, there are sites like Kahn Academy that can help.

4 Likes

Sure, I had a feeling that I may have thrown a lot of maths out of nowhere.

More maths explanation here

The whole (Cos(θ), Sin(θ)Cos(φ), Sin(θ)Sin(φ)) thing comes from spherical polar co-ords. This image gives a rough overview of the geometry.
Spherical-coordinate-system

If you imagine the gun barel pointing along the z axis, then these angles give a conical deviation from the z axis. We can ignore the r variable as we are only interested in direction rather than distance (the direction of the bullet should be a unit vector so we have r = 1).

Code example here

This is the most direct translation into the code that I could think of. There is probably a way to do the same thing using CFrame.Angles though which may be more readable.

local MAX_SPREAD = math.rad(2) -- about 2 degrees of spread

local function getSpreadCframe(maxSpread)
    local theta = math.random() * maxSpread -- random value in range [0, MaxSpread]
    local phi = math.random() * 2*math.pi -- random value in [0, 2π]
    local cTheta = math.cos(theta)
    local sTheta = math.sin(theta)
    local cPhi = math.cos(phi)
    local sPhi = math.sin(phi)
    -- Construct cframe from components R_{ij}
    return CFrame.new(0,0,0,
        cTheta       ,  sTheta * sPhi,  sTheta * cPhi,
        sTheta * cPhi,  cTheta       ,  sTheta * sPhi, 
        sTheta * sPhi,  sTheta * cPhi,  cTheta
    )
end

bullet.CFrame = gunModel.Gun.Muzzle.CFrame * getSpreadCframe(MAX_SPREAD)
2 Likes

Turns out the maths was off for this. I went back and sorted out the details for it. I’ve made a place with a fixed version of the maths and I’ll explain the fixed way of doing it below.

Bullet Spread CFrame.rbxl (51.1 KB)

My approach uses the fact that for a matrix R_ij:

    | R_11, R_12, R_13 |
R = | R_21, R_22, R_23 |
    | R_31, R_32, R_33 |

we know that:

R * (1,0,0) == (R_11, R_21, R_31)
R * (0,1,0) == (R_12, R_22, R_32)
R * (0,0,1) == (R_13, R_23, R_33)

so we can build an appropriate CFrame by thinking about how we want the three axis to change after being transformed by the cframe. The fact that we just want to rotate the vector means we actually only need to think about two axis as the third can be determined by the cross product of the other two (a rotation matix is orthogonal).

We know that (0, 0, 1) goes to (Sin(θ)Cos(φ), Sin(θ)Sin(φ), Cos(θ)) so we just need to think about one other axis.

It is useful to think of the overall transformation as two rotations. The first around the z axis by the angle φ. The second around the new y axis by angle θ. The interesting thing about these transformations is that the second one won’t alter the new y axis, so we only need to know how the first transformation affects the original y axis.

2D Rotation

This means that (0, 1, 0)(-Sin(φ), Cos(φ), 0). We then know two columns of the matrix:

    | R_11, -Sin(φ),  Sin(θ)Cos(φ) |
R = | R_21,  Cos(φ),  Sin(θ)Sin(φ) |
    | R_31,    0   ,    Cos(θ)     |

If you work out the cross product you can fill out the final column which happens to be:

    | Cos(θ)Cos(φ), -Sin(φ),  Sin(θ)Cos(φ) |
R = | Cos(θ)Sin(φ),  Cos(φ),  Sin(θ)Sin(φ) |
    |   -Sin(θ)   ,    0   ,    Cos(θ)     |

The code then looks something like this:

local function getSpreadCframe(theta, phi)
	local cTheta = math.cos(theta)
	local sTheta = math.sin(theta)
	local cPhi = math.cos(phi)
	local sPhi = math.sin(phi)
	
	local vY = Vector3.new(-sPhi, cPhi, 0)
	local vZ = Vector3.new(sTheta * cPhi, sTheta * sPhi, cTheta)
	local vX = vY:Cross(vZ)
	return CFrame.fromMatrix(
		Vector3.new(),
		vX, vY, vZ
	)
end
4 Likes

Ooh this absolutely amazing, thank you! I’m really bad at trig and vector math and this helps so much.

So I pasted the code and I realized it could use horizontal spread and vertical spread! And I added another angle to work with! Hope this helps anyone trying to create a modular spread system where vertical and horizontal is different. :smile:

local function RandomCone(axis: Vector3, angleX: number, angleY: number)
	local cosAngleX = math.cos(angleX)
	local cosAngleY = math.cos(angleY)
	local zX = 1 - math.random()*(1 - cosAngleX)
	local zY = 1 - math.random()*(1 - cosAngleY)
	local phi = math.random()*math.pi*2
	local rX = math.sqrt(1 - zX*zX)
	local rY = math.sqrt(1 - zY*zY)
	local x = rX * math.cos(phi)
	local y = rY * math.sin(phi)
	local vec = Vector3.new(x, y, (zX + zY)/2)
	if axis.Z > 0.9999 then
		return vec
	elseif axis.Z < -0.9999 then
		return -vec
	end
	local orth = Vector3.zAxis:Cross(axis)
	local rot = math.acos(axis:Dot(Vector3.zAxis))
	return CFrame.fromAxisAngle(orth, rot) * vec
end