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
:
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 @Maximum_ADHD 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:
Simplified to our context, we get this formula:
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 haveh = 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, notedg
.
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:
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.