Artillery Aiming with Predicted Projectile Angle

Hey everyone!
Here’s a simple and realistic way to aim artillery/mortars in Roblox using physics-based projectile prediction.

If you treat 20 studs as 1 meter, Roblox’s gravity of 196.2 studs/s² perfectly matches real-world Earth gravity of 9.81 m/s². That means you can use real-world projectile speeds directly, without any scaling

Real gravity = 9.81 m/s²
Roblox gravity = 196.2 studs/s²
= 9.81 m/s² * 20 studs/m

Formula Used in Script

local underRoot = v^4 - g * (g * d² + 2 * h * v²)

θ = atan( (v² ± √(v⁴ − g(gd² + 2hv²))) / (g * d) )

  • θ = launch angle (in radians)
  • v = projectile speed
  • g = gravity
  • d = horizontal distance to target
  • h = vertical distance to target

the script for the High-Arc Prediction

local part = script.Parent
local target = workspace.Target
local projectileSpeed = 3000
local gravity = workspace.Gravity

function GetLaunchAngle(origin, targetPos, speed, gravity)
	local displacement = targetPos - origin
	local horizontal = Vector3.new(displacement.X, 0, displacement.Z).Magnitude
	local vertical = displacement.Y
	local speed2 = speed ^ 2

	local rootPart = speed2 ^ 2 - gravity * (gravity * horizontal ^ 2 + 2 * vertical * speed2)
	if rootPart < 0 then return nil end

	local root = math.sqrt(rootPart)
	local angle1 = math.atan((speed2 + root) / (gravity * horizontal))
	local angle2 = math.atan((speed2 - root) / (gravity * horizontal))

	return math.max(angle1, angle2) -- high arc
end

while true do
	task.wait(0.1)
	local angle = GetLaunchAngle(part.Position, target.Position, projectileSpeed, gravity)
	if angle then
		local flatDir = Vector3.new(target.Position.X, part.Position.Y, target.Position.Z) - part.Position
		local yaw = math.atan2(-flatDir.X, -flatDir.Z)
		part.CFrame = CFrame.new(part.Position) * CFrame.Angles(0, yaw, 0) * CFrame.Angles(angle, 0, 0)
	end
end

the script for the lower/flatter arc

Use math.min(...) instead of math.max(...) for a lower/flatter arc.

local part = script.Parent
local target = workspace.Target
local projectileSpeed = 150 -- adjust as needed
local gravity = workspace.Gravity

function GetLaunchAngle(origin, targetPos, speed, gravity)
	local displacement = targetPos - origin
	local horizontal = Vector3.new(displacement.X, 0, displacement.Z)
	local horizontalDistance = horizontal.Magnitude
	local verticalDistance = displacement.Y

	local speedSquared = speed ^ 2
	local g = gravity

	local underRoot = speedSquared ^ 2 - g * (g * horizontalDistance ^ 2 + 2 * verticalDistance * speedSquared)
	if underRoot < 0 then return nil end

	local root = math.sqrt(underRoot)
	local angle1 = math.atan((speedSquared + root) / (g * horizontalDistance))
	local angle2 = math.atan((speedSquared - root) / (g * horizontalDistance))

	return math.min(angle1, angle2) -- Use flatter arc
end

while true do
	task.wait(0.1)

	local origin = part.Position
	local targetPos = target.Position

	local angle = GetLaunchAngle(origin, targetPos, projectileSpeed, gravity)

	if angle then
		local flatDir = Vector3.new(targetPos.X, origin.Y, targetPos.Z) - origin
		local yaw = math.atan2(-flatDir.X, -flatDir.Z)

		local launchCFrame = CFrame.new(origin) * CFrame.Angles(0, yaw, 0) * CFrame.Angles(angle, 0, 0)
		part.CFrame = launchCFrame

		print("Pitch angle:", math.deg(angle))
	else
		warn("Target out of range")
	end
end

Mortar{ 150–300 m/s }

Howitzer (155mm){ ~800 m/s }

tank gun(M1 abrams){1500 m/s}

4 Likes