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:
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?
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 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.
I mean thank you for doing the hard part for us . 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.