Modeling a projectile's motion

This guide was originally written for scriptinghelpers. The original can be found here.

As a somewhat active member of the Scripting Helpers discord one of the most common questions I see is how to have a projectile travel an arc. Most people want to know how to do this for things like basketballs or cannon balls and so forth. Since this is such a popular question I thought it would be worth writing a blog post on it and talking about a few other things we can extend from our findings.

Deriving the equation

Using basic knowledge of physics and simple calculus we can derive the basic equation of motion that a projectile will take.

We know that the derivative of position with respect to time is velocity and that the derivative of velocity with respect to time is acceleration. Thus, the reverse is also true, the integral of acceleration with respect to time is velocity and the integral of velocity with respect to time is position.

In our case we know that the only acceleration affecting our objects is gravity. Therefore, all we must do is integrate gravity with respect to time twice to give us an equation for position.

eq1

Due to the rules of indefinite integration we are left with an unknown constant, C, but using some intuition we can figure this out easily enough. We know that at t = 0 our velocity should be equal to our initial velocity which we will define as v0.

eq2

Next, we’ll integrate velocity with respect to time to find position.

eq3

Similar to before we’re left with some constant, C, and again we know that at t = 0 we should be at our initial position which we define as x0.

eq4

Awesome! We have a time-based equation for position!

Basketball example

One of the neat things we can do with this equation is find the initial velocity needed to reach a certain target given time. To figure this out we define our target as x1 and rearrange the position function from above for v0.

eq5

gif1

local t = 1;
local mouse = game.Players.LocalPlayer:GetMouse();
local hrp = game.Players.LocalPlayer.CharacterAdded:Wait():WaitForChild("HumanoidRootPart");
local bball = script.Parent:WaitForChild("Handle");

mouse.Button1Down:Connect(function()
	local g = Vector3.new(0, -game.Workspace.Gravity, 0);
	local x0 = hrp.CFrame * Vector3.new(0, 2, -2)
	
	-- calculate the v0 needed to reach mouse.Hit.p
	local v0 = (mouse.Hit.p - x0 - 0.5*g*t*t)/t;
	
	-- have the ball travel that path
	local c = bball:Clone();
	c.Velocity = v0;
	c.CFrame = CFrame.new(x0);
	c.CanCollide = true;
	c.Parent = game.Workspace;
end)

Alternatively we could do this directly with CFrames if we didn’t care as much about the collisions.

local t = 1;
local mouse = game.Players.LocalPlayer:GetMouse();
local hrp = game.Players.LocalPlayer.CharacterAdded:Wait():WaitForChild("HumanoidRootPart");
local bball = script.Parent:WaitForChild("Handle");
local rs = game:GetService("RunService").RenderStepped;

mouse.Button1Down:Connect(function()
	local g = Vector3.new(0, -game.Workspace.Gravity, 0);
	local x0 = hrp.CFrame * Vector3.new(0, 2, -2)
	
	-- calculate the v0 needed to reach mouse.Hit.p
	local v0 = (mouse.Hit.p - x0 - 0.5*g*t*t)/t;
	
	local c = bball:Clone();
	c.CanCollide = true;
	c.Anchored = true;
	c.Parent = game.Workspace;
	
	-- have the ball travel that path
	local nt = 0;
	while (nt < t*2) do
		c.CFrame = CFrame.new(0.5*g*nt*nt + v0*nt + x0);
		nt = nt + rs:Wait();
	end
end)

Jump power and height

For those of you who are unaware the property Humanoid.JumpPower can be thought of as the v0 used to model a jump. We can use this information with the position equation we derived to find the height of our jump or even the jump power we need to reach a certain height.

To find the jump height we need again to draw on some basic calculus intuition. We know that the derivative of a function allows us to calculate the slope at any given point. Looking at an example position function we can see that since it’s a parabola, its maximum height coincides with a slope of zero.

gif2

We already know the derivative of the position function is v(t) so if we set it equal to zero and solve for t then we know the time value when the position function has reached its maximum height.

eq6

We can take this time value and plug it into the position function to have a formula for jump height.

eq7

Noting that the initial height (position) is zero we’re left with the following formula to find jump height which we’ll define as h.

eq8

We can rearrange this formula to find v0 (jump power) given a non-negative height.

eq9

We can also look at the original position equation and solve for x(t) = 0 with the quadratic equation to find the time it takes for the jump land.

eq10

Unsurprisingly, this gives us the max height time value doubled since it takes the same amount of time to go up as it does down.

local humanoid = game.Players.LocalPlayer.CharacterAdded:Wait():WaitForChild("Humanoid");

-- note that typically gravity should be a negative value, but in this case I leave it positive hence the lack of -2 throughout the code
-- set the jump height to 10
humanoid.JumpPower = math.sqrt(2*game.Workspace.Gravity*10);

humanoid.StateChanged:Connect(function(old, new)
	if (new == Enum.HumanoidStateType.Jumping) then
		local h = humanoid.JumpPower^2/(2*game.Workspace.Gravity);
		local t = 2*humanoid.JumpPower/game.Workspace.Gravity;
		wait(t);
		print(string.format("Jump took %s seconds and reached a max height of %s studs", t, h));
	end
end)

Mapping the projectile’s path with a beam

One of the common things people like to do with this equation is use it to help players aim. The simplest way to do this would be to draw parts between intervals on the curve. However, another way to do this is to use beams!

Beams are represented by cubic Bezier curves which is great because it means they can perfectly match any polynomial that is at most of third degree (meaning cubic). Our function is second degree (quadratic) so we’re all set.

Our first step is going to be to head over to the Wikipedia page on Bezier curves. Luckily for us they already have the cubic Bezier curve equation and the first derivative which we’ll also be needing.

eq11

A few things we should take note of here if we’re trying to find a Bezier curve equivalent to our projectile motion function. We know that P0 = x0 and that P3 = x1, thus we have two unknowns we want to solve for, P1 and P2. We can also recognize that the Bezier curve function is normalized such that s = [0, 1]. As such we’ll need to similarly normalize our position and velocity functions as we’ll be using them soon.

We’ll define t1 as the time value that can be used to find x1, that is x(t1) = x1.

eq12

We will now set these equal to the two Bezier functions and solve for the unknowns. We’ll make sure to pick s values that make this process as easy as possible.

eq13

eq14

Great, we now have all four points that make up the equivalent Bezier curve. Our next step is to look at the beam wiki page and see how these points can be used to find the correct values and properties we need to adjust to find our beam object.

Looking at the page it seems that P1 and P2 are defined by the following:

P1 = attachment0.WorldCFrame.rightVector * beam.CurveSize0
P2 = -attachment1.WorldCFrame.rightVector * beam.CurveSize1

As such the last step is going to be to calculate these world CFrame values such that the right vectors are parallel with P1 – P0 and P3 – P2. This is easy enough with knowledge of the cross product and how the CFrame rotation matrix is formed.

As such our final code is:

gif3

local attach0 = Instance.new("Attachment", game.Workspace.Terrain);
local attach1 = Instance.new("Attachment", game.Workspace.Terrain);

local beam = Instance.new("Beam", game.Workspace.Terrain);
beam.Attachment0 = attach0;
beam.Attachment1 = attach1;

local function beamProjectile(g, v0, x0, t1)
	-- calculate the bezier points
	local c = 0.5*0.5*0.5;
	local p3 = 0.5*g*t1*t1 + v0*t1 + x0;
	local p2 = p3 - (g*t1*t1 + v0*t1)/3;
	local p1 = (c*g*t1*t1 + 0.5*v0*t1 + x0 - c*(x0+p3))/(3*c) - p2;
	
	-- the curve sizes
	local curve0 = (p1 - x0).magnitude;
	local curve1 = (p2 - p3).magnitude;
	
	-- build the world CFrames for the attachments
	local b = (x0 - p3).unit;
	local r1 = (p1 - x0).unit;
	local u1 = r1:Cross(b).unit;
	local r2 = (p2 - p3).unit;
	local u2 = r2:Cross(b).unit;
	b = u1:Cross(r1).unit;
	
	local cf1 = CFrame.new(
		x0.x, x0.y, x0.z,
		r1.x, u1.x, b.x,
		r1.y, u1.y, b.y,
		r1.z, u1.z, b.z
	)
	
	local cf2 = CFrame.new(
		p3.x, p3.y, p3.z,
		r2.x, u2.x, b.x,
		r2.y, u2.y, b.y,
		r2.z, u2.z, b.z
	)
	
	return curve0, -curve1, cf1, cf2;
end

game:GetService("RunService").RenderStepped:Connect(function(dt)
	local g = Vector3.new(0, -game.Workspace.Gravity, 0);
	local x0 = hrp.CFrame * Vector3.new(0, 2, -2)
	local v0 = (mouse.Hit.p - x0 - 0.5*g*t*t)/t;
	
	local curve0, curve1, cf1, cf2 = beamProjectile(g, v0, x0, t);
	beam.CurveSize0 = curve0;
	beam.CurveSize1 = curve1;
	-- convert world space CFrames to be relative to the attachment parent
	attach0.CFrame = attach0.Parent.CFrame:inverse() * cf1;
	attach1.CFrame = attach1.Parent.CFrame:inverse() * cf2;
end)
559 Likes
Best Methods To Handle Arrow Projectiles
How to model projectile motion?
Locally-Replicated Projectile Hit Detection
How to count ballistic curve?
Moving and Rotating with CFrame without sliding
What is a better way to script throwable objects?
How do I angle a projectile to hit a player
Curving Projectile Motion
Easiest way to Bezier a projectile? (Using BodyMovers)
Making a combat game with ranged weapons? FastCast may be the module for you!
How would I simulate throwing a ball?
Any ideas on how to script a cannon that moves a player to a specific direction?
Physics: Dealing with gravity offset issues in trajectory prediction
How to go about a basketball game?
How can I add a projection to this projectile ball's path before it is thrown?
Angled projectile
Destroying part on client freezes parts' physics on server
Help with scripting a rocket
Projectile Motion
How can I script a golf ball projectile?
Bezier Curve not firing smoothly
Untitled Game Credits
How Does FastCast Do Bullet Drops?
How to make a grenade system?(help)
How to throw an object to the mouseposition?
What to use as alternative to Part.Velocity?
Help with client-side collision accuracy
Locally-Replicated Projectile Hit Detection
Velocity to Studs?
A little math problem about circular position changing
How do I move an object along a trajectory with a VectorForce?
I'm having a hard time with coordinates in general, including angles
How do I understand CFrame better?
How should I go about making a throwable object depending on the players mouse position?
Best Method of Bullet Drop?
Help with making bow
Raycasting in the correct direction
Need help figuring out the exact position a projectile will land at
How can I add mouse aiming to my physics-based projectile?
How do I simulate a basketball being shot with physics?
Old Images Being Erased: Needs Prioritization System
How to make a throwable object with curved indicator
Whenever there is memory leakage, velocity goes off track [SOLVED]
How should I go about making a throwable object depending on the players mouse position?
Projectile Aim-Assist
Basketball Velocity Freezing
How do I make a visual aiming system?
How to make ball reach position using physics
How to find trajectory given height, speed, and distance
Fastcast hitting more times than it should
How to create accurate Bullet drop?
Cowbow lasso does not have enough gravity
How to calculate the landing position of a ball
How to determine how fast a player is falling and apply to bodyVelocity?
How to calculate the necessary velocity of the projectile
Projectile system with Prediction
Help with ragdoll cannon
Need better and accurate projectile throwing
How would I make an automatic cannon using projectiles?
How do I make a ball curve?
Making a combat game with ranged weapons? FastCast may be the module for you!
How to make part follow a Trajectory?
Calculating the angle needed to hit a point on a parabolic trajectory
Projectile path for throwing objects?
How to check where the Cannon ball will land before shooting?
Find launch angle given height, velocity, and vertex
How to make a beam from several points in space
How can i make a "Missile trajectory"
Ball is Curving incorrectly
Model projectile trajectory
How do I make a bullet "bend" like in Big Paintball or Phantom Forces?
How to make the projectory of roblox bedwars bow
How would I make bullets that are affected by gravity?
What would be a good way of making a cannon shell fire and drop off at certain time
Artillery shell trajectory
Using a Beam to Model Projectile Motion
Ball Bouncing System
Help with deflecting knockback
How to slow down a projectile's arc of motion?
Physics formula calculation inconsistency / not being accurate
Calculate projectile curve between start and end point without beside curve
Determine projectile impact position
Need help with bezier cuve
How do I show path of projectile?
Why is this Mortar not working as expected?
Throwing coin and detection system (hitman)
Angleing a part to shoot projectile at player
Help making a system that predict where a projectile is going to land
Cannonball doesn't launch from an explosion
How do I create a bow and arrow? I already got the builds
How would I make something like this?
Need help with body movers
How to Throw a Part
How do I make a projectile arc towards player?
Projectile Curves System
CFrame Help (Rainbow Effect)
Practical systems and solutions (scripting)
How do I make the curve less tall / how do I make the arc smaller?
Projectile physics
How to Convert Raycast Function? How To Compare Two Rays?
What are the pre requisites of math do I need to understand this
Predicting Rolling Physics of a Ball
Trying to make parabola between points
Calculating a ball's trajectory
Throwing Or arching object
Launching a part to an exact Vector3 value
Making a projectile with BodyVelocity

Hey, amazing tutorial!

Although, I have one small nitpick with it;

When viewed with the Dark Theme, the equations are somewhat unreadable. Maybe add some white stroke to it?

36 Likes

Thanks for letting me know. I never use dark theme on the dev forum so I would have never known. Should be fixed now via block quotes.

9 Likes

Unfortunately not.

9 Likes

Wonderful tutorial! This will finally allow me to make a few throwing systems I’ve been thinking of.

4 Likes

That’s about the best I can do unfortunately :frowning_face:

17 Likes

This is amazing! I’m so excited to finally learn how to do this. Thanks!!

4 Likes

I uploaded the Tool to Roblox Library and a .rbmx file here for ease of accessibility.

Projectile Script.rbxm (4.4 KB)



@EgoMoose Quick question what is t and nt?

Those Variables aren’t self explanatory, they don’t make sense.

11 Likes

t is time it takes to reach the target. nt is the elapsed time.

7 Likes

So recently @Maximum_ADHD posted this on twitter and through a series of events I was asked if I would post a tutorial showing how to do it. I’m not sure if this is exactly how Clone did it, but this is how I approached it.

So to start off let’s cover the process that doing something like this would entail.

  1. Start with a position and velocity which we can use the above equations to find the projectile’s path.
  2. Find where this path intersects with in game geometry.
  3. Reflect the current velocity against the geometry’s surface.
  4. Use the intersection position and reflected velocity to repeat from step 1 as many times as desired.

We already know how to do steps 1 and 4 from the above post so all we need to cover are steps 2 and 3.

To find where our projectile intersects with a surface we’ll do two things.

We’ll first split our projectile’s path into multiple rays which can then be used to find an intersection with in game geometry.

local g = Vector3.new(0, game.Workspace.Gravity, 0)

local function x(t, v0, x0)
	return 0.5*g*t*t + v0*t + x0
end

local function intersection(x0, v0)
	local t = 0
	local hit, pos, normal
	
	repeat
		local p0 = x(t, v0, x0)
		local p1 = x(t + 0.1, v0, x0)
		t = t + 0.1
		
		local ray = Ray.new(p0, p1 - p0)
		hit, pos, normal = game.Workspace:FindPartOnRay(ray)
	until (hit or t > 5)
	
	-- so our plane is defined by the pos and normal variables
end

This will provide two key pieces of information; an estimated intersection position and a surface normal. We’ll then use those pieces of information to do a plane-quadratic intersection which will give us an exact intersection value.

Using a general form of the quadratic formula:

eq1

We know the quadratic will intersect with a plane defined by P and N when:

eq2

This gives us two situations where we solve for t differently.

The first being when a . N ≠ 0 in which case we use the quadratic formula.

eq3

The second being when a . N = 0 in which case we simple solve the linear equation for t:

eq4

In code this translates to:

local function planeQuadraticIntersection(v0, x0, p, n)
	local a = (0.5*g):Dot(n)
	local b = v0:Dot(n)
	local c = (x0 - p):Dot(n)
	
	if (a ~= 0) then
		local d = math.sqrt(b*b - 4*a*c)
		return (-b - d)/(2*a)
	else
		return -c / b
	end
end

All that’s left is to solve step 3 which is to reflect our velocity vector against the surface normal. This is simple enough and is currently explained on the dot product devhub wiki page. The only thing you might do here is slightly damp the velocity such that your reflection loses some energy as it bounces.

When you put all that together you get:

It’s not perfect mainly because how velocity is damped, but it’s a pretty good projection of the path traveled. Enjoy!

Trajectory.rbxl (25.2 KB)

138 Likes

PLS, can you put the formulas to block quote, bec this is unreadeable, at topic, it isnt best, but it is much better.

4 Likes

A very well done tutorial with a great explanation with some beautiful representations. However It is harder for younger developers like me to understand all this hard math that contains integrals and derives, I tried learning and studying them on my own, I also had been discussing with teachers about how I could learn them. But the problem still remains. I would highly appreciate if you could do another post where you explain everything with some simple math without any hard calculus. I really regret that I am not able to learn and understand everything you mentioned. Amazing tutorial and keep up the great work.

9 Likes

Is it possible to do this with a bodyvelocity?

5 Likes

Heya, sorry to necro-bump this but i was wondering how i could check when the :Travel() method is finished or atleast approximate how long it might take.

Sorry again for bumping

2 Likes

Does the part’s mass matter in this equation, Also I’m trying to do this with a body force in the part that counteracts gravity not sure how to incorporate that with the gravity calculation.

3 Likes

How would we go about keeping a consistant height on the trajectory? When you point the mouse far the launch looks a bit crazy fir most cases like throwing a basketball or launching out of a cannon.

4 Likes

Mass should not matter. Acceleration due to gravity remains the same for all objects.

8 Likes

Never knew this was possible! I used to simulate it in a for loop then just put bricks at its positions

2 Likes

This topic can use integral calc, but it mostly regards with physics and projectile motion.
A short 13 minute video to help you understand a bit about projectile motion is rather simple in the way the video explains it. I’m obviously not going to type a whole page about projectile motion when there’s a video that does it all.
https://youtu.be/M8xCj2VPHas
This is for projectile motion, and you’ll immediately notice it’s very similar to the tutorial. In the equations, they just use different variables and the terms are in different order, but I’m sure you can easily understand the equations.

9 Likes

Love the tutorial, especially showing the importance of math to doing this stuff!

One question though, if I don’t have a sphere following the path but a rod, what would be the best way to rotate it so that it is always pointing in the direction of the arc? At first I was thinking about doing a BodyAngularVelocity from 0 to pi, but that won’t work in a local script. Would I have to use CFrames then?

EDIT: Ok, here’s what I’ve got so far:
Right before the while (nt < t*2) loop, I put this in to set the starting angle of the projectile and point it at the target.

local angle = math.pi / 4
p.CFrame = CFrame.new(p.Position, hrp.Position)

Then, inside of the while loop I have:

p.CFrame = p.CFrame * CFrame.Angles(0, angle, 0)

And after the RenderStepped update:

angle = (math.pi / 4) - ((nt / (t * 2)) * (math.pi / 2))

What this should do is start the projectile pointing at the target, then up at a 45 degree angle. Then, on each render step, it subtracts from that angle a proportion of the full rotation of the projectile. The end result is moving from a +45 angle to a -45 angle by the end of the animation.

However, when I run it, I don’t see the projectile actually rotating. I printed out all my angles as it calculates them, and they look correct, running from +.76 to -.80.

Can anyone see what I’m doing wrong?

EDIT 2: I figured out some of the mistakes I made in that code. Turns out that I can’t do the CFrame.Angles with the argument of the angle I want it pointed to, but I have to use an argument for how much I want it rotated by.

So instead of calculating the necessary angle for each step, I just have to calculate the change in angle between each frame:

local angleDelta = ((math.pi / 4) / t) / 60

Because I couldn’t see a good way to calculate the number of times the animation loop will run, I took the amount of the rotation (45 degrees) divided by the time to take that rotation (t) divided by the number of frames per second (I assumed 60 because that is the cap and I don’t know a good way to get the actual value).

Then, in the animation loop, I have:

projectile.CFrame = projectile.CFrame * CFrame.Angles(-(angleDelta), 0, 0)

Now when I have a static projectile that is not moving, it rotates the correct amount in the correct duration. But when I try to combine it with the CFrame change from projectile.CFrame = CFrame.new((.5 * g * nt * nt) + (v0 * nt) + source.Position), it doesn’t rotate. I also tried combining the two together with a multiplication, but that doesn’t work either.

Any ideas why I can’t get the two working together?

6 Likes