Help debugging Roblox's Endorsed Weapons Kit: Projectiles move at much greater speeds when fired at very steep angles

I was playing around with and modifying Roblox’s Endorsed Weapons Kit. By default you can only aim up and down 75 degrees. Changing the 75s on lines 130 and 131 in the ShoulderCamera module to 90s I made it so the player can look straight up and down. Firing a projectile at angles more than 75 or less than -75 degrees the projectile will speed up greatly which is undesirable for obvious reasons. To demonstrate, I turned the rocket speed down to 10 studs per second.

Place file where you can see the modules and try it out for yourself: Viewangle rocket speed demo.rbxl (109.3 KB)

There is a lot going on here and I’m not too great at math, but as I understand it, the BulletWeapon module simulates projectiles by drawing a complete path for each one and then at each tick it calculates the position on the parabola the projectile should be using the time since it was fired. It uses the Parabola:setDomain(x0, x1) function to determine where the tip and the tail of the projectile are. When the domain is too small, due to the projectile being at a steep angle, the calculation for the position gets messed up. I do not understand the math well enough yet to understand the cause of the bug.

There is a lot of code in that place file, and all of it is necessary to make the weapon system work without modification, but ctrl f for “parabola” in BulletWeapon and a look inside the parabola module is all the code involved with this bug.

I will continue to work on this but I feel a little stuck so I thought I would ask for a fresh perspective.

4 Likes

While I have no idea what actually causes this issue and don’t have an access to Studio, the easiest temporary fix is to just block the camera from reaching those angles. Nobody will be looking straight up or down anyways.

I don’t think this is entirely accurate, to be honest. Depending on the map and situations I sometimes need to shoot at something directly above or below me, an feel limited when this isn’t an option. It’s not generally needed, of course, but it’s a bit unfair to say that it never happens.


As for the actual bug, it seems to have to do with the parabola module. The path of the projectile is simulated as a purely parabolic path. That approach is sound - it’s just that the implementation is flawed.

I went ahead and tested the rocket launchers you’re using in Studio. Even at the default configurations, rockets fired horizontally travel noticeably slower than those fired upwards at the limit. I believe the issue occurs because the parabola simulation fires projectiles at a constant horizontal speed, and lets the Y position of the projectile follow suit. Obviously, if this is the case, the projectile will just zoom off into the distance if that curve is very steep. If you’re aiming straight up, the instantaneous velocity would be infinite, mathematically speaking.

We can actually see this in the code. The following function is only ever called with an argument of 1:

function Parabola:samplePoint(t)
	local a, b, c = self.a, self.b, self.c
	local x0, x1 = self.x0, self.x1
	local x = x0 + (t * (x1 - x0))
	local y = (a * x * x) + (b * x) + c
	return self.referenceFrame:pointToWorldSpace(Vector3.new(0, y, -x))
end

This function is used to change the domain every physics step:

function Parabola:setDomain(x0, x1)
	self.x0 = x0
	self.x1 = x1
end

From that we can already see that all points on the parabola are sampled from x1. Here’s where x1 comes from (from BulletWeapon):

local bulletSpeed = self:getConfigValue("BulletSpeed", 1000)

--...

local steppedCallback = function(dt)
	local now = tick()
	timeSinceStart = now - startTime

	local travelDist = bulletSpeed * dt -- distance projectile has travelled since last frame
	trailLength = trailLength or travelDist * trailLengthFactor

	-- Note: the next three variables are all in terms of distance from starting point (which should be tip of current weapon)
	local projBack = pTravelDistance - trailLength -- furthest back part of projectile (including the trail effect, so will be the start of the trail effect if any)
	local projFront = pTravelDistance -- most forward part of projectile
	local maxDist = hitInfo.maxDist or 0 -- before it collides, this is the max distance the projectile can travel. After it collides, this is the hit point

	-- This will make trailing beams render from tip of gun to wherever projectile is until projectile is destroyed
	if showEntireTrailUntilHit then
		projBack = 0
	end

	--...

	-- Update parabola domain
	parabola:setDomain(projBack, projFront)

	-- Continue updating pTravelDistance until projBack has reached maxDist (this helps with some visual effects)
	if projBack < maxDist then
		pTravelDistance = math.max(0, timeSinceStart * bulletSpeed)
	end

	--...
end

As you can see, x1 comes from projFront, which is calculated as math.max(0, timeSinceStart * bulletSpeed). The bulletSpeed variable comes directly from the configuration and isn’t adjusted to account for the angle the bullet is fired at.

Fortunately for us, this is really easy to fix. All we have to do is scale back the horizontal increment to make the projectile velocity accurate. We can do this by multiplying bulletSpeed by the horizontal length of the direction vector:

local horizontalLength = math.sqrt(dir.x^2 + dir.z^2) --dir is defined on line 295
pTravelDistance = math.max(0, timeSinceStart * bulletSpeed * horizontalLength)

And that simple change seems to fix the problem!

EDIT: I feel like I should mention that because this implementation is based on the horizontal distance travelled and not the amount of time elapsed, firing straight upwards or downwards at exactly 90° is going to lead to odd and unexpected behavior. Angles that are very slightly off of 90° should still work, though. Just make sure to catch that case somehow.

4 Likes

I actually noticed this behavior in my custom kit that uses their parabola function because I needed to be able to shoot directly downward. I’m a huge fan of shooting rockets at my feet.

Thanks for figuring that out you are a huge help. As someone who seriously struggles to debug any mathy code, can I add on a question about your process to figuring this out? I figured out and made the connection that the issue had to do with the way the code considered the domain, but from there I was stumped on what to do with that knowledge.

So when you got to that point, did you see the next step just from your intuition and experience? Do you use another program to model the math to find the error? If you set breakpoints in the code what variables did you watch? Stuff like that, if that makes sense.

Sure! Finding stuff like this is usually pretty straight-forward once you know what to look for.

The most important thing when you’re debugging something is generally the ability to work backwards. You already know you’re getting the wrong result, so you start at where that result is shown and slowly try to see where that result comes from.

You essentially already did a decent chunk of the work for me by pointing out the problem likely lies within the Parabola:setDomain() method in your original post, so the first thing I did was find that module and read through it. With that philosophy of working backwards, I looked for the code responsible for actually calculating a point on the parabola, since that’s likely where I’ll start running into the problematic code. I found the Parabola:samplePoint() method, and I can see from the line y = (a * x * x) + (b * x) + c) (the general equation for a parabola is y = a * x^2 + b * x + c) that I’m on the right track.

I actually originally suspected that the problem was with the scaling of the t parameter, and that the domain would be set once and then never changed. But I couldn’t be sure of that, so I decided to check where Parabola:samplePoint() is actually called. In Studio, the fastest way to do that is usually to press Ctrl + Shift + F and search some text across all scripts. I searched for :samplePoint:

image

There’s a few results within the Parabola module, but those are all inside methods that seem to have dedicated purposes such as rendering a beam, finding what parts the parabola collides with, and so on, so I decided to check out the one result that was in the BulletWeapon module. That leads me to here:

And this immediately points out to me that I was wrong about my original assumptions: samplePoint() is only ever called with an argument of 1, and the domain slides along the parabola to determine where the projectile currently is along its path. Since samplePoint is only ever called with an argument of 1, that also means that the point is always sampled at parabola.x1 - we see that on this line: local x = x0 + (t * (x1 - x0)).

How we know this

That line calculates x as x0 plus the difference between x1 and x0. We can simplify that down:

x = x0 + (1 * (x1 - x0)) = x0 + x1 - x0

At which point the two terms of x0 cancel out and we’re left with just x1.

By extension, that means that the point is always sampled at projFront, so we work backwards to see where that value comes from. In this case, I’ve often found it helpful to highlight the variable’s name and just scroll through the code to see where else it comes up. By scrolling up, I found this:

And by repeating the same process, I found that pTravelDistance is set here:

I then do the same for bulletSpeed and find it’s set here:
image

Because it always comes from the same configuration value, I know that the value of pTravelDistance, and by extension, projFront always increase at the same rate regardless of the angle you fire at. This makes steeper trajectories increase the bullet velocity tremendously, because for the same distance travelled horizontally it covers a lot more distance vertically.

So, of course, now we know what the problem is and where the code that’s causing it is located. The next issue is solving it. Since that function clearly contains the bullet simulation code (or, well, in this case, rocket simulation code :wink: ), the direction the rocket in has to be passed in somewhere. And I didn’t have to look far:

In this case, I’m expecting fireInfo.dir to be a “unit vector”, i.e. a Vector3 that always has a magnitude of 1. Assuming that’s true, we can just get its horizontal length and multiply bulletSpeed by that value to scale it down when you’re firing at a steep angle.

In 3D space, the length of a vector is calculated as:

image

Because y will always be 0 (we’re only interested in X and Z), we can simplify that out. And that’s how we got the solution I suggested in my first post.

Hopefully this helps! :slight_smile:

2 Likes

Fantastic answer. Thank you so much. I wish I could mark two solutions.

1 Like

hey, this is unrelated, but does the weapons kit work with the folder for the system placed in replicatedstorage? you can check my post about it, I’m not sure why it’s not working and can’t find many other people who’ve used the kit. thanks.

1 Like

It does but you have to edit ServerWeaponsScript to initialize it properly and put it in server script service so it will run.

What edits have to be made to ServerWeaponsScript, apart from moving it to ServerScriptService?

I think I understand what happens in ServerWeaponsScript: apart from initializing the system itself, it checks if there’s a WeaponsSystem folder in ReplicatedStorage; if there’s not, it takes the parent folder of the script (curWeaponsSystem) clones it, and moves it into ReplicatedStorage. If there’s not a ServerWeaponsScript in ServerScriptService, it moves the running script into there. Lastly, it destroys the parent folder of the running script if it’s called WeaponsSystem, because the parent should be ServerScriptService? But if the script’s already in ServerScriptService and the folder with everything else is in ReplicatedStorage from the beginning, shouldn’t it run as planned? Why wouldn’t it, if it identifies weaponsSystemFolder as being in ReplicatedStorage and doesn’t need to clone it, identifies that the ServerWeaponsScript is already in ServerScriptService - why does it still not work? Thanks so much, by the way

1 Like

I made a post on your topic to keep things more organized. Here is a link if anyone else wants to take a look: Roblox's Official "Weapons Kit" breaks when placing weapons system into ReplicatedStorage - #2 by e100gamma