Physics: Dealing with gravity offset issues in trajectory prediction

While Roblox’s physics engine allows us to simulate the movement of physical objects, we would sometimes like to be able to predict their trajectories beforehand. For one example, it could be used to show the trajectory of a projectile weapon before it is fired. There have been several threads on the matter, and if you’d like to know how to replicate this effect, I’d highly recommend to take a look at the following example, where a basketball tool displays a trail predicting the ball’s trajectory.

Looking at this thread, you’ll find an equation that allows you to calculate a projectile’s position at time t based on its initial position x0, initial velocity v0, and gravity g:

equation_parabola

This formula should perfectly define the projectile’s movement over time. However, when testing with this formula alongside an actual projectile in Roblox Studio, there is a slight downward offset in the real position compared to the predicted position. This offset accumulates the longer the projectile is in the air.

I eventually stumbled upon a discussion between developers @EgoMoose and @CloneTrooper1019 related to these trajectory inaccuracies, as well as a separate thread pointing out these issues when they could still be compared to those from the Legacy solver. Indeed, tracing the full trajectory of a flying projectile will show inaccuracies in its position after a short amount of time.

The recording below shows a projectile flying off at an arbitrary speed for exactly 5 seconds; the opaque ball is the recorded position of the projectile being moved by Roblox’s physics engine, while the transparent sphere is its predicted position using the equation above. The camera is constantly teleported to the latest sample to clearly show the offset increase over time.

Noting ΔP as the difference between the opaque and transparent positions after 5 seconds, we get:

ΔP = (0.027, -2.053, -0.003)
ΔP.Magnitude = 2.054

Click here to see the code that generates this test.
workspace.CurrentCamera.CameraType = Enum.CameraType.Custom

local Part = Instance.new("Part")
Part.Size = Vector3.new(2, 2, 2)
Part.Shape = Enum.PartType.Ball
Part.TopSurface = Enum.SurfaceType.Smooth
Part.BottomSurface = Enum.SurfaceType.Smooth

local Color = Color3.fromRGB(255, 0, 0)
local Connection

local function spawnDebugSphere(position, material)
	local Indicator = Part:Clone()
	Indicator.Color = Color
	Indicator.Anchored = true
	Indicator.CanCollide = false
	Indicator.Material = material
	Indicator.Position = position
	Indicator.Parent = workspace

	return Indicator
end

local h = 1 / 240 --Duration of a phyics step (1/240th of a second).
local g = Vector3.new(0, -workspace.Gravity, 0)
local p0, v0, t0

local function GetPosition(p0, v0, dt)
	return p0 + v0 * dt + 0.5 * g * (dt ^ 2)
end

local function GetVelocity(v0, dt)
	return v0 + g * dt
end

--This function computes the theoretical position and velocity (p1, v1)
--as well as the difference between theoretical and experimental (dp, dv).
local function Step()
	local t1 = time()
	local dt = t1 - t0

	local p1, v1 = GetPosition(p0, v0, dt), GetVelocity(v0, dt)
	local dp, dv = Part.Position - p1, Part.Velocity - v1

	local s1 = spawnDebugSphere(p1, Enum.Material.ForceField)
	local s2 = spawnDebugSphere(Part.Position, Enum.Material.SmoothPlastic)

	workspace.CurrentCamera.CameraSubject = s2

	if dt > 5 then
		Connection:Disconnect()
	end

end


wait(5)

--Initial parameters
p0 = Vector3.new(500, 10000, -700)
v0 = Vector3.new(-150, 65, 36.61).Unit * 1_000
t0 = time()
Part.Position = p0
Part.Velocity = v0
Part.Parent = workspace
workspace.CurrentCamera.CFrame = CFrame.new(p0 + Vector3.new(0, 100, 0))

--Connect the Step function
Connection = game:GetService("RunService").Heartbeat:Connect(Step)

In most scenarios, it’s not really useful to have the exact trajectory you expected for your projectiles. However, @GFink, @GloriedRage and I have been working for several months on a security system around projectile-based weapons. This system predicts a projectile’s trajectory with as much accuracy as possible so that the server may validate its movement and hit positions, preventing the use of homing projectile hacks or Hit-box expanders. For that reason, we would like to minimize the offset shown in the video above.

This error is caused by Roblox’s integration method, which is essentially the step in the physics process that will update a part’s position based on its velocity, and update its velocity based on its acceleration. There are many ways to do this, often with varying degrees of accuracy. It seems that when the PGS Solver update kicked into action in March 2019, it required the integration formula to be replaced with a slightly less accurate one called Implicit Euler (or Explicit Euler - if you know which method is used specifically, please let me know). This method causes an offset that essentially makes all objects and players fall slightly faster than they should.

This offset is known as the Local Truncation Error of the integration method, or LTE for short. Thankfully, this error can be accurately quantified:

LTE

Simplified to our context, we get this formula:

LTE simplified

This formula gives us the error caused at each physics step. Let’s explain all the factors:

  • LTE is, as we mentioned, the Local Truncation Error we’re trying to compute.
  • h is the time interval between each physics step. In Roblox’s case, physics is calculated at a fixed 240Hz, so we have h = 1/240.
  • y"(t0) is the current acceleration of our projectile. If you’re not applying any forces to that projectile, then the only acceleration is the Workspace.Gravity, noted g.

Again, this formula gives us the error that happens during one physics step. This final formula will give us the error after t seconds have elapsed:

LTE final2

Finally, we can obtain a corrected parabola based on the initial position and velocity, taking this acceleration-induced offset into account:

Click here to see the code that generates this test.
workspace.CurrentCamera.CameraType = Enum.CameraType.Custom

local Part = Instance.new("Part")
Part.Size = Vector3.new(2, 2, 2)
Part.Shape = Enum.PartType.Ball
Part.TopSurface = Enum.SurfaceType.Smooth
Part.BottomSurface = Enum.SurfaceType.Smooth

local Color = Color3.fromRGB(255, 0, 0)
local Connection

local function spawnDebugSphere(position, material)
	local Indicator = Part:Clone()
	Indicator.Color = Color
	Indicator.Anchored = true
	Indicator.CanCollide = false
	Indicator.Material = material
	Indicator.Position = position
	Indicator.Parent = workspace

	return Indicator
end

local h = 1 / 240 --Duration of a phyics step (1/240th of a second).
local g = Vector3.new(0, -workspace.Gravity, 0)
local p0, v0, t0

local function GetPosition(p0, v0, dt)
	return p0 + v0 * dt + 0.5 * g * (dt ^ 2)
end

local function GetVelocity(v0, dt)
	return v0 + g * dt
end

local function GetCorrection(dt)
	return 0.5 * h * g * dt
end

--This function computes the theoretical position and velocity (p1, v1)
--as well as the difference between theoretical and experimental (dp, dv).
local function Step()
	local t1 = time()
	local dt = t1 - t0

	local p1, v1 = GetPosition(p0, v0, dt), GetVelocity(v0, dt)
	p1 += GetCorrection(dt)
	local dp, dv = Part.Position - p1, Part.Velocity - v1

	local s1 = spawnDebugSphere(p1, Enum.Material.ForceField)
	local s2 = spawnDebugSphere(Part.Position, Enum.Material.SmoothPlastic)

	workspace.CurrentCamera.CameraSubject = s2

	if dt > 5 then
		Connection:Disconnect()
	end

end


wait(5)

--Initial parameters
p0 = Vector3.new(500, 10000, -700)
v0 = Vector3.new(-150, 65, 36.61).Unit * 1_000
t0 = time()
Part.Position = p0
Part.Velocity = v0
Part.Parent = workspace
workspace.CurrentCamera.CFrame = CFrame.new(p0 + Vector3.new(0, 100, 0))

--Connect the Step function
Connection = game:GetService("RunService").Heartbeat:Connect(Step)

We obtain the following result (again, the projectile flies for 5 seconds):

ΔP = 0.027, -0.007, -0.003
ΔP Magnitude = 0.028

There’s still a tiny inaccuracy here, though I have to admit I don’t know what’s causing this one. However, our parabola trajectory is now far more accurate, and can now be reliably used to predict a projectile’s trajectory with a margin of error of less than 0.006 studs per second.

This post does not explicitly detail the calculations behind it, nor is it really the aim here; the objective is mainly to show that simulated physics have some inaccuracy to them, and offer a way to cancel this inaccuracy for specific applications. However, feel free to ask if you have any questions about the calculations themselves, and I’ll do my best to answer them.

Thanks again to GFink and GloriedRage for helping me tackle this issue, along with many others we encountered in the security system we’ve been working on for the better part of the season.


Additional Notes

It just so happens that a blog post recently appeared, describing new approaches to numerical integration that would improve the engine’s robustness when handling the movement of spinning bodies. If this were to be included in a future update, the error calculated above would either disappear entirely, or be replaced by a new, more negligible one, in which case it would need to be recalculated from the new numerical integration formulas.

As of the time of posting, the method mentioned in this thread is still a valid way to compensate for the offset caused by gravity and other forces. That being said, if anyone reading this already knows how to compute the LTE for the future integration method, I would love to know about it.

39 Likes

Figured I would add that when the PGS physics solver was released, player jump height actually decreased. Here is an exchange I had a while back with staff who were reaching out to developers for feedback on the new physics solver and how it affected their games:


If you’re thinking “no way that’s right, LTE could not have caused that much of a difference,” I would agree that it seems like a drastic change for it to have gone largely unnoticed and that the local truncation error would have to be far larger than has been recorded in this thread for the jump height to change by a factor of 10%.

However at the time I made that post about the jump height difference, we were still able to toggle the PGS physics solver on and off at will, so it was very easy to test with and come to that conclusion. It seems possible to me that the new jump height could have been impacted by something other than the topic of the thread. Either way, the change made this jump at Crossroads suddenly much more difficult:

to the point that I had to lower it in my game so you could always make the jump.

Considering this, however:

I wonder if jump height will naturally increase should they implement the new integration method. If that’s the case, I’m curious to see if some of those difficulty chart obbies will become easier considering their creators have built their games for current physics conditions.

2 Likes

Thanks for this great explanation of the LTE!

2 Likes