1

Line Example

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

Kylaaa
  • 6,349
  • 2
  • 16
  • 27
Calextone
  • 11
  • 2

1 Answers1

0

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. An image of the original path from the question, but the starting point has been labeled "p0", a point off at a right angle labeled as "p1", a point at a right angle from the ending point labeled as "p2", and the endpoint labeled as "p3"

As a small working example, I made a simple tool that would spawn a projectile whenever you clicked.

An image of Studio's Explorer and Properties widgets. In ReplicatedStorage, there is a RemoteEvent. Inside the ServerScriptService, there is a Script. Inside the StarterPack there is a Tool. And inside that Tool, there is a LocalScript.

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. An image of a player firing blueberry shaped projectiles at a large target. The projectiles are arcing away from the player and converging on the target where the mouse has clicked.

Kylaaa
  • 6,349
  • 2
  • 16
  • 27