Calculating the angle needed to hit a point on a parabolic trajectory

I’ve been trying to work this out and it’s been giving me such a headache.

Given an initial velocity of a projectile v and the gravity of the space g, I can work out the angle required to hit an arbitrary point at (x, y) from the origin using this formula:

image

Using Desmos to visualize it, everything seems normal:

The point is correctly hit by the curve. Note that I’m using d and H in place of x and y as Desmos reserves x and y (unless I’m incorrect and just don’t know how to use it)
However, implementing this in practice is where I can’t work this out.

The current code for this is:

local function Project3Dto2DVector(Vector: Vector3, Normal: Vector3)
	local Dot = Vector:Dot(Normal)
	return Vector3.new(Vector.X * Dot, Vector.Y * Dot, 0) * Vector.Magnitude
end

local function AngleBetweenVectors(VectorA: Vector3, VectorB: Vector3)
	return math.acos(VectorA.Unit:Dot(VectorB.Unit))
end

local function VectorBetweenPoints(PointA: Vector3, PointB: Vector3)
	return PointB - PointA
end

local function Aim(dt)
	local FlightTime = (Target.Position - Muzzle.WorldPosition).Magnitude / BulletSpeed
	local TargetPosition = Target.Position + (Target.AssemblyLinearVelocity * FlightTime * 1.2) -- 1.2 compensates for the servos lagging behind
	
	local YawProjectedVector = CFrame.lookAt(Base.Position, TargetPosition)
	local _, YawAngle, _ = YawProjectedVector:ToEulerAnglesYXZ()
	
	local PitchProjectedVector = Project3Dto2DVector(Base.CFrame:PointToObjectSpace(VectorBetweenPoints(Base.Position, TargetPosition)).Unit, Muzzle.WorldCFrame.RightVector)
	print(`Pitch projected vector: {PitchProjectedVector}`)
	print(`RightVector: {Muzzle.WorldCFrame.RightVector}`)
	print(`Dot product: {PitchProjectedVector:Dot(Muzzle.WorldCFrame.RightVector)}`) -- should always return close to 0
	local PitchAngle = math.atan((BulletSpeed ^ 2 + math.sqrt(math.pow(BulletSpeed, 4) - (workspace.Gravity * ((workspace.Gravity * TargetPosition.X ^ 2) + (2 * TargetPosition.Y * BulletSpeed ^ 2)))))
		/ workspace.Gravity * TargetPosition.X)
	print(`Pitch angle: {PitchAngle}`)
	
	PitchServo.TargetAngle = math.deg(PitchAngle)
	YawServo.TargetAngle = math.deg(YawAngle)
	
	script.Parent.Debug.Yaw.Text = `Yaw: {math.round(YawServo.TargetAngle)}`
	script.Parent.Debug.Pitch.Text = `Pitch: {math.round(PitchServo.TargetAngle)}`
end

I think it’s because I’m not projecting the vector properly, so if anyone can give some pointers on how to do it properly, that’d be great. I never did vectors in depth in high school, so this is all new to me.

Sorry, what happens when you run this? On the surface, it looks like the formula matches up (though it does look a bit messy; consider splitting parts of the formula into different sections, i.e. numerator / denominator, radicand, etc). Are the print statements what you expect? Do they line up with what Desmos says?
BTW, on a completely different note, what are you using this for?

Right now when I run this it causes it to just be completely upright.

You can see the target in the background, and yes it’s for a turret.

The formula is correct indeed, it’s just the rest of the vector math that transforms it to a 2D plane where it messes up I believe (as that formula is for a 2D space, I’m not sure how to expand it into a 3D space, so what I’m doing right now is projecting the 3D vector to a 2D plane and “solving” it like that, which in hindsight is not a great way to do it).

I believe your calculations for mapping 3D to 2D vectors is slightly wrong. To do this, I assume you already have a target Vector3. The Y value would be the difference between the Y value of the target and the Y Value of the origin. The X value would be the magnitude of the difference between the two vectors if their Y value was the same. I’m a little rusty on my vector math :sweat_smile: so I may not be the best source for this.
Then you can plug in these value as the respective X and Y values for your function.

P.S., perhaps consider using the acceleration property of FastCast or this tutorial: Modeling a projectile's motion

I mean thank you for doing the hard part for us :sweat_smile:. This function will calculate the angles required:

function CalculateAngle(TargetOffset, Gravity, Velocity, TurretPivotOffset)
	TurretPivotOffset = TurretPivotOffset or Vector2.zero
	local LateralOffset = Vector2.new(TargetOffset.X, TargetOffset.Z).Magnitude - TurretPivotOffset.X
	local Pitch = math.atan((Velocity^2 + math.sqrt(Velocity^4 - Gravity * (Gravity * LateralOffset^2 + 2 * (TargetOffset.Y - TurretPivotOffset.Y) * Velocity^2))) / (Gravity * LateralOffset))
	
	if Pitch ~= Pitch then return end -- The function sometimes returns NaN which means the target is out of reach. Value == Value checks if it is NaN
	
	local Yaw = math.atan(TargetOffset.X / TargetOffset.Z) + (TargetOffset.Z < 0 and math.pi or 0)
	return Pitch, Yaw
end

The pitch calculation function was from your Desmos notes. The yaw calculation function just looks straight at the target.

This is an example how to use it:

local Offset = TargetPosition - Base.Position

local Pitch, Yaw = CalculateAngle(Offset, workspace.Gravity, BulletSpeed)
if Pitch then
	YawServo.TargetAngle = math.deg(Yaw) - Turret.Base.Orientation.Y -- You maybe need +90 or -90.
	PitchServo.TargetAngle = math.deg(Pitch)
else
-- Target out of range
end

The CalculateAngle function also has a fourth argument called TurretPivotOffset which will compensate if the projectile will not be fired exactly at the Turret’s base. I can show a demonstration I made in ms paint


Because the projectile doesn’t spawn exactly at the Turret Base position, the calculations will be off. In this case, you can set the TurretPivotOffset to Vector2.new(0, 5). This also works in the lateral direction.

I suggest you spawn the projectile at the center of the Hinge’s Attachment point, instead of at the end of the barrel for better accuracy.

1 Like

Honestly, I wasn’t expecting an answer this in depth for this. Thanks!

As you can see it works almost perfectly. I’ll adjust it l;ater.

1 Like

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.