Randomly generated angle heavily biased towards one plane/axis

In short, I’m trying to add a random spread to a shotgun I’m making for a top down shooter

My problem is that while the spread works mostly fine, it has a tendency to “lock”/have no spread along one axis when facing along that axis

I’ve tried a lot of googling and messing about with my math, as well as rewriting the code a bunch, but it never seems to work properly.

The one line of code that matters (spaced into multiple lines for easier reading cause my code sucks):

local beamCast = game.Workspace:Spherecast(beamOrigin.Position, beamRad, 
beamDir*Vector3.new(
math.rad((math.random()*2)*spreadH), -- X
math.rad((math.random()*2)*(spreadV*unlockAxis)), -- Y
math.rad((math.random()*2)*spreadH)), -- Z, this axis has a tendency to lock/snap - Setting this to 1 changes nothing
fireCastParams)

spreadH and spreadV are values related to the spread angles (ie would be set to 45 for a 45 degree spread in both left-right/up-down directions - totalling 90 degrees) - Don’t worry about the Y axis, I’m only looking for the X/Z axis fixes
If there was no spread in any direction (firing exactly where it was pointed) each axis would be set to 1

Some image examples of my issue:

As shown here, the spread has a tendency to “lock” along this axis (the Z axis), when vertically the spread works fine (the X axis)

image
It’s not a perfect lock and it’s only around the exact axis line that it really snaps tight, but I’m stuck on what could be going wrong here and need some help.

I’m not sure how well I can describe the issue with your code, but essentially this is doing nothing with angles, and it should generally be biased towards any nearby axis because it’s just vector multiplication with a few random positive numbers.

Since you want to modify this beamDir vector by some angles it’d be easiest to use CFrames:
CFrame.lookAt(Vector3.zero, beamDir)

Now to modify the angles you multiply by another CFrame and get the vector back:

(CFrame.lookAt(Vector3.zero, beamDir) * CFrame.Angles(
	math.rad((math.random()*2)*(spreadV*unlockAxis)),
	math.rad((math.random()*2)*spreadH),
	0
)).LookVector * beamDir.Magnitude -- LookVector is a unit vector so it gets multiplied here to match the same length

But this randomizing isn’t quite right. Since math.random()*2 only produces numbers between 0 and 2 it’s randomizing too far counterclockwise. You’ll need random numbers between -1 and 1:

(CFrame.lookAt(Vector3.zero, beamDir) * CFrame.Angles(
	math.rad((math.random()*2-1)*(spreadV*unlockAxis)),
	math.rad((math.random()*2-1)*spreadH),
	0
)).LookVector * beamDir.Magnitude
1 Like

Adjust logic or the variables here

local function RotateVector(beamDir, axis, angle)
    local cosAngle = math.cos(angle)
    local sinAngle = math.sin(angle)
    return beamDir * cosAngle + axis * (axis:Dot(beamDir) * (1 - cosAngle)) + beamDir:Cross(axis) * sinAngle
end

local spreadAngleRadians = math.rad(spreadH)
local randomAngle = math.random() * spreadAngleRadians - (spreadAngleRadians / 2)
local upVector = Vector3.new(0, 1, 0)
local rotationAxis = beamDir:Cross(upVector).Unit
local rotatedBeamDir = RotateVector(beamDir, rotationAxis, randomAngle)
local beamCast = game.Workspace:Spherecast(beamOrigin.Position, beamRad, rotatedBeamDir, fireCastParams)

Omg I completely forgot CFrame.lookAt() was a thing. I’ll have to give this a test as soon as I can!

As for math.random()*2 I made it be between 0 and 2 because it was multiplying beamDir by Vector3.new() - meaning if it was 1, it was centered/wouldn’t affect the direction (and in turn having it be less or more than one would make it shift left/right accordingly, with whole digits being equivalent to 90 degrees from my experience)