I want one of my projectiles to follow an arching path before hitting the target. No matter how much I think about it, I just can't wrap my head around it. Any help would be grateful
1 Answers
Bezier Curves are a great way to make interesting and smooth arcs and paths.
Bezier curve calculations are essentially just fancy weighted average functions : when you pass in 0 you get your starting point, and when you pass in 1 you get your end point. All the values in between are the path your projectile will take.
So knowing the projectile's velocity, we can measure how far it has traveled over a span of time. And if we compare that distance to the length of the Bezier curve, we can come up with a ratio between 0 and 1. With that ratio, we can plug it into our Bezier curve calculation and get the position of the projectile.
But a cubic Bezier curve requires extra information. You need the start and end points, and you also need to choose two additional control points to define the curve. The start and end points are easy, those can be the player's position and wherever they are aiming. And using CFrame math, you can set up control points at right angles relative to the start and end points.
As a small working example, I made a simple tool that would spawn a projectile whenever you clicked.
In the Tool's LocalScript :
script.Parent.Activated:Connect(function()
-- choose a spot in front of the player
local player = game.Players.LocalPlayer
local playerCFrame = player.Character:GetPrimaryPartCFrame()
playerCFrame += playerCFrame.LookVector * 2.0
-- choose the target based on where they clicked
local target = player:GetMouse().Hit
-- choose how much to rotate the control points around the player and target.
-- set this to zero to always fire to the right
local rotation = math.random(0, 180)
-- tell the server to spawn a projectile
game.ReplicatedStorage.RemoteEvent:FireServer(playerCFrame.Position, target.Position, rotation)
end)
Then, in a Script in ServerScriptService, listen for the firings of the RemoteEvent :
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local BULLET_VELOCITY = 50.0 -- studs / second
local BULLET_MAX_LIFETIME = 60.0 -- seconds
local BZ_NUM_SAMPLE_POINTS = 100
local BZ_ARC_RADIUS = 10
-- define some helper functions...
local function spawnBullet(startingPos)
local bullet = Instance.new("Part")
bullet.Position = startingPos
bullet.Shape = Enum.PartType.Ball
bullet.Size = Vector3.new(0.5, 0.5, 0.5)
bullet.BrickColor = BrickColor.Blue()
bullet.CanCollide = true
bullet.Anchored = true
bullet.Parent = game.Workspace
return bullet
end
-- Bezier Curve functions adapted from :
-- https://developer.roblox.com/en-us/articles/Bezier-curves
local function lerp(a, b, c)
return a + (b - a) * c
end
function cubicBezier(t, p0, p1, p2, p3)
-- t = % complete between [0, 1]
-- p0 = starting point
-- p1 = first control point
-- p2 = second control point
-- p3 = ending point
local l1 = lerp(p0, p1, t)
local l2 = lerp(p1, p2, t)
local l3 = lerp(p2, p3, t)
local a = lerp(l1, l2, t)
local b = lerp(l2, l3, t)
local cubic = lerp(a, b, t)
return cubic
end
function cubicBzLength(p0, p1, p2, p3)
local calcLength = function(n, func, ...)
local sum, ranges, sums = 0, {}, {}
for i = 0, n-1 do
-- calculate the current point and the next point
local p1, p2 = func(i/n, ...), func((i+1)/n, ...)
-- get the distance between them
local dist = (p2 - p1).magnitude
-- store the information we gathered in a table that's indexed by the current distance
ranges[sum] = {dist, p1, p2}
-- store the current sum so we can easily sort through it later
table.insert(sums, sum)
-- update the sum
sum = sum + dist
end
-- return values
return sum, ranges, sums
end
local sum = calcLength(BZ_NUM_SAMPLE_POINTS, cubicBezier, p0, p1, p2, p3)
return sum
end
ReplicatedStorage.RemoteEvent.OnServerEvent:Connect(function(player, spawnPos, targetPos, rotation)
local bullet = spawnBullet(spawnPos)
-- calculate the path
local startingCFrame = CFrame.new(spawnPos, targetPos)
local targetCFrame = CFrame.new(targetPos, spawnPos)
-- calculate the control points as Vector3s
-- p1 and p2 will be right angles to the starting and ending positions, but rotated based on input
local p0 = (startingCFrame).Position
local p1 = (startingCFrame + (startingCFrame:ToWorldSpace(CFrame.Angles(0, 0, math.rad(rotation))).RightVector * BZ_ARC_RADIUS)).Position
local p2 = (targetCFrame + (targetCFrame:ToWorldSpace(CFrame.Angles(0, 0, math.rad(-rotation))).RightVector * -BZ_ARC_RADIUS)).Position
local p3 = (targetCFrame).Position
-- calculate the length of the curve
local distance = cubicBzLength(p0, p1, p2, p3) -- studs
-- calculate the time to travel the entire length
local totalTime = distance / BULLET_VELOCITY -- seconds
local startingTime = tick()
-- start moving it towards the target
local connection
connection = RunService.Heartbeat:Connect(function(step)
-- calculate the percentage complete based on how much time has passed
local passedTime = tick() - startingTime
local alpha = passedTime / totalTime
-- move the bullet
local updatedPos = cubicBezier(alpha, p0, p1, p2, p3)
bullet.Position = updatedPos
-- once we've arrived, disconnect the event and clean up the bullet
if alpha > 1 or totalTime > BULLET_MAX_LIFETIME then
bullet:Destroy()
connection:Disconnect()
end
end)
end)
This allows you to fire projectiles that arc in interesting paths.

- 6,349
- 2
- 16
- 27