So I’ve had this code for a while now and keep forgetting to post it. From the amount of questions about it on the devforum, I think it will be helpful for some people. The code is a little bit messy and unintuitive but it gets the job done. It’s a little hacky and I couldn’t figure out how to make it nicer while still being efficient (if anyone can make it better, please do so and post it here).
This code was largely put together using projectile motion code from an old Roblox tool (slingshot I think) and the code for making the beam is from the tutorial by EgoMoose here.
Put the following code into a ModuleScript (Named “ProjectileLib” and Parented to ReplicatedStorage if you want the example below to work):
local ProjectileLib = {}
-- Returns Tuple: isInRange(boolean), lauchAngle(in Radians)
local function computeLaunchAngle(horizontalDistance, distanceY, intialSpeed, g) -- gravity must be (+)number
local distanceTimesG = g*horizontalDistance -- Micro-optimizations
local initialSpeedSquared = intialSpeed^2
local inRoot = initialSpeedSquared^2 - (g*((distanceTimesG*horizontalDistance)+(2*distanceY*initialSpeedSquared)))
if inRoot <= 0 then
return false, 0.25 * math.pi -- Returns false as it is out of range
-- Makes launch angle 45˚ for ranges past max Range
end
local root = math.sqrt(inRoot)
local inATan1 = (initialSpeedSquared - root) / distanceTimesG
local inATan2 = (initialSpeedSquared + root) / distanceTimesG
local answerAngle1 = math.atan(inATan1) -- When optimal launch angle is lofted
local answerAngle2 = math.atan(inATan2) -- When optimal launch angle is 'direct'
-- You might be able to change some things and force the launch angle to be lofted at times for certain circumstance
-- For example, shooting a basketball might require a more lofted trajectory, although EgoMoose's tutorial might be better for that
if answerAngle1 < answerAngle2 then -- I've honestly never seen it be answer2, I can't figure out the case when it would be. I might remove it at some point
return true, answerAngle1 -- Returns true as it is in range, same with below
else
return true, answerAngle2
end
end
function ProjectileLib.ComputeLaunchVelocity(distanceVector, initialSpeed, g, allowOutOfRange) -- gravity: (+)number
local horizontalDistanceVector = Vector3.new(distanceVector.X, 0, distanceVector.Z)
local horizontalDistance = horizontalDistanceVector.Magnitude
local isInRange, launchAngle = computeLaunchAngle(horizontalDistance, distanceVector.Y, initialSpeed, g)
if not isInRange and not allowOutOfRange then return end
local horizontaldirectionUnit = horizontalDistanceVector.Unit
local vy = math.sin(launchAngle)
local xz = math.cos(launchAngle)
local vx = horizontaldirectionUnit.X * xz
local vz = horizontaldirectionUnit.Z * xz
return Vector3.new(vx*initialSpeed, vy*initialSpeed, vz*initialSpeed)
end
function ProjectileLib.ComputeLaunchVelocityBeam(distanceVector, initialSpeed, g, allowOutOfRange) -- gravity: (+)number
local distanceY = distanceVector.Y
local horizontalDistanceVector = Vector3.new(distanceVector.X, 0, distanceVector.Z)
local horizontalDistance = horizontalDistanceVector.Magnitude
local isInRange, launchAngle = computeLaunchAngle(horizontalDistance, distanceY, initialSpeed, g)
if not isInRange and not allowOutOfRange then return end
local horizontaldirectionUnit = horizontalDistanceVector.Unit
local vy = math.sin(launchAngle)
local xz = math.cos(launchAngle)
local vx = horizontaldirectionUnit.X * xz
local vz = horizontaldirectionUnit.Z * xz
-- Just for beaming:
local v0sin = vy * initialSpeed
local horizontalRangeHalf = ((initialSpeed^2)/g * (math.sin(2*launchAngle)))/2
local flightTime
if horizontalRangeHalf <= horizontalDistance then
flightTime = ((v0sin+(math.sqrt(v0sin^2+(2*-g*((distanceY))))))/g)
else
flightTime = ((v0sin-(math.sqrt(v0sin^2+(2*-g*((distanceY))))))/g)
end
--
return Vector3.new(vx*initialSpeed, vy*initialSpeed, vz*initialSpeed), flightTime -- flightTime is used to beam
end
-- v0: initialVelocity(Vec3), x0: initialPosition(Vec3), t1: flightTime((+)number), g: gravity((+)number)
function ProjectileLib.BeamProjectile(v0, x0, t1, g)
local g = Vector3.new(0, -g, 0)
-- 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 cf0 = 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 cf1 = 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, cf0, cf1
end
return ProjectileLib
The “allowOutOfRange” and related variables are a bit hacky, but they’re good for example if a player is controlling some sort of artillery piece and the target is the player’s mouse –> if the mouse is past the MaxRange of the artillery piece, it will just beam and fire to the MaxRange and no further. Set “allowOutOfRange” true for that.
However, if your making an Archer NPC like the example below, we probably don’t want it to fire unless the target is in range, so we can set it false for that. If it is set false and the target is out of range, ComputeLaunchVelocity will just return nil and we can check for that.
Use Example:
local Debris = game:GetService("Debris")
local ProjectileLib = require(game:GetService("ReplicatedStorage").ProjectileLib)
local function weld2Parts(partA, partB) -- I think this is a very old way to weld
local weld = Instance.new("ManualWeld")
weld.Parent = partA
weld.C0 = partA.CFrame:inverse() * partB.CFrame
weld.Part0 = partA
weld.Part1 = partB
return weld
end
local GRAVITY = workspace.Gravity -- Mine is: 32.174 studs/sec^2 -- This is the actual value for gravity in ft/sec^2
-- You can change this in the "Workspace" object in Explorer
local cloneProjectilePart = workspace.ProjectilePart -- Have a part named this in workspace
cloneProjectilePart.Anchored = true
local beamHolderPart = workspace.BeamHolderPart -- Have a part named this in workspace, and inside of it
-- put 2 attachments and a beam that uses the 2 attachments.
-- You may want to increase the number of Segments of the beam
local Archer = {} -- Imagine this is an "Archer" Object
Archer.MaxRange = 200 -- This is the Archer's max horizontal range in studs, but only on flat land
-- If the Target is lower than the Archer, it will be able to shoot further than it's MaxRange
-- If the Target is higher than the Archer, its effective MaxRange will be smaller
Archer.ArrowSpeed = math.sqrt(Archer.MaxRange*GRAVITY)
Archer.Target = workspace.TargetPart -- Have a part named this in workspace
local function archerFireProjectile(ArcherObject)
local distanceVector = ArcherObject.Target.Position - beamHolderPart.Position
local initialVelocity, flightTime = ProjectileLib.ComputeLaunchVelocityBeam(distanceVector, ArcherObject.ArrowSpeed, GRAVITY, false)
if not initialVelocity then print("Target is out of range") return end -- Remove this altogether if allowOutOfRange is true
local curve0, curve1, cf0, cf1 = ProjectileLib.BeamProjectile(initialVelocity, beamHolderPart.Position, flightTime, GRAVITY)
beamHolderPart.Beam.CurveSize0 = curve0
beamHolderPart.Beam.CurveSize1 = curve1
beamHolderPart.Attach0.CFrame = beamHolderPart.Attach0.Parent.CFrame:Inverse() * cf0
beamHolderPart.Attach1.CFrame = beamHolderPart.Attach1.Parent.CFrame:Inverse() * cf1
-- Make Projectile and send it flying with initialVelocity:
local angle = math.atan2(-initialVelocity.X, -initialVelocity.Z) + math.pi*0.5
local projectilePart = cloneProjectilePart:Clone()
projectilePart.CFrame = CFrame.new(beamHolderPart.Position) * CFrame.Angles(0, angle - math.pi*0.5, 0)
projectilePart.CanCollide = false -- Make this true if you want the projectile to bounce off other objects, and remove the weld on touch below
projectilePart.Parent = workspace
projectilePart.AssemblyLinearVelocity = initialVelocity -- Give it velocity so it moves!
projectilePart.Anchored = false
--projectilePart:ApplyImpulse(initialVelocity) -- Apply Impulse is inaccurate, I'm not sure why
projectilePart.Touched:Connect(function(otherPartOrTerrain) -- Simple weld on hit for sake of example
if otherPartOrTerrain.Name ~= "ProjectilePart" and otherPartOrTerrain.Name ~= "BeamHolderPart" then
weld2Parts(projectilePart, otherPartOrTerrain)
end
end)
Debris:AddItem(projectilePart, 8) -- Destroy the projectile after 8 seconds
end
while wait(1) do
archerFireProjectile(Archer)
end
I’m still working on a way to get the projectile to look in the tangent of it’s parabolic arc (like an arrow would through flight), but I think it requires constantly updating a BodyGyro or some constraint as it flies since it’s angular rotation is non-linear. If anyone has any idea how to do that, let me know! Thanks.
Edit: Heres a quick, crude video showcase: