Scripting Helpers is winding down operations and is now read-only. More info→
Ad
Log in to vote
1

How Can I Apply a Sine Wave to a Projectile?

Asked by 5 years ago

When a certain tool is activated, the Localscript fires an event that activates a Serverscript which duplicates a projectile stored elsewhere and uses BodyVelocities to launch that projectile forward to the mouse's location on the coordinate frame. After having completed that, I started experimenting with CFrame to get those projectiles to have a wave-like pattern as they travel forward. This is what I tried so far, and the projectile ends up doing a wave in the X axis rather than the angle it was shot from.

P = script.Parent
while wait() do
    P.CFrame = CFrame.new(P.Position) * 
    CFrame.new(math.sin(workspace.DistributedGameTime*180), 0, 0)
end

Example

So long story short, my goal is to be able to achieve this (left example), and not that (right example).

2 answers

Log in to vote
0
Answered by
fredfishy 833 Moderation Voter
5 years ago

Problem specification

So what we've sort of got here is three different vectors

  • The travel vector - the way the projectile moves before we start thinking about sine-wave offset
  • The vertical vector - i.e., Vector3.new(0,1,0)
  • The wiggle vector - you want your projectile to wiggle parallel to this vector. It is perpendicular to the other two vectors.

There are two key problems

  1. How do we calculate the offset vector?
  2. Once we have the wiggle vector, how do we use it to get our projectile to move back and forth?

Problem numero uno

The first problem uses something called the cross product. What the cross product does, is it takes two vectors, and then spits out a vector which is perpendicular to both.

Calculating it is a bit weird, and my understanding of vectors isn't good enough to be able to explain to you the intuition behind it. However, you don't need to understand it to be able to use it.

function crossProduct(u, v)
    return Vector3.new( u.y * v.z - u.z * v.y
                      , u.z * v.x - u.x * v.z
                      , u.x * v.y - u.y * v.x
                      )
end

So now finding the wiggle-vector is just a matter of crossProduct(b-a, Vector3.new(0, 1, 0)).unit)

Problem 2

Finding the position required for a sine-wave curve is easy. We could just do position = sin(t) + startPosition

But we're working with velocity here, since the particle needs to be moving along.

To convert from displacement (position) to velocity, we use a process known as integration. Again, understanding this properly is a first-year-at-university-studying-maths kind of a thing.

Since that definitely isn't me, I'm just going to tell you that the integral of sin x is -cos x + c. The + c is basically because just -cos x provides the correct answer, just shifted a constant amount.

So now we can sort of just blend our travel vector with our wiggle vector.

local t = (workspace.DistributedGameTime - t1) / (math.pi * 2) * frequency
local v = (travel * speed) + (wiggle * (-math.cos(t) * amplitude))
p.Velocity = v

Combining it all

My explanation isn't great, so you can probably get a better understanding if you just have a play around with the script. Just pop it in a tool with a handle.

fireConn = nil
colours = { Color3.new(1, 0, 0)
          , Color3.new(0, 1, 0)
          , Color3.new(0, 0, 1)
          , Color3.new(1, 1, 0)
          , Color3.new(1, 0, 1)
          , Color3.new(0, 1, 1)
          , Color3.new(1, 1, 1)
          }

function tracer(p1, p2)
    local p = Instance.new("Part", Workspace)
    p.Anchored = true
    p.Color = colours[math.random(1, #colours)]
    p.Material = Enum.Material.Neon
    p.Size = Vector3.new(0.1, 0.1, (p2 - p1).magnitude)
    p.CFrame = CFrame.new(midpoint(p1, p2), p2)
    p.Archivable = false
    p.CanCollide = false
    p.Name = "Trace"

    -- Destroy after 1 second
    game:GetService("Debris"):AddItem(p, 1)
end

function fireBrick(p1, p2)
    -- These values aren't really correct, but they're proportional
    -- e.g., an amplitude of 5 doesn't give the wave an amplitude of 5, but increasing it does increase the amplitude
    local speed = 10
    local resolution = 0.1
    local amplitude = 5
    local frequency = 40

    -- Create projectile
    local p = Instance.new("Part", workspace)
    p.Size = Vector3.new(0.5, 0.5, 0.5)
    p.Shape = Enum.PartType.Ball
    p.Color = colours[math.random(1, #colours)]
    p.CanCollide = false
    p.CFrame = CFrame.new(p1, p2)
    p.TopSurface = Enum.SurfaceType.Smooth
    p.BottomSurface = Enum.SurfaceType.Smooth
    p.Name = "Projectile"

    -- Stop the projectile falling
    local f = Instance.new("BodyForce", p)
    f.Force = Vector3.new(0, 1, 0) * (p:GetMass() * workspace.Gravity)

    spawn(function()
        local alive = true

        -- Destroy projectile when it's hit
        p.Touched:Connect(function(hit)
            if hit and hit.Name ~= "Trace" then
                p:Destroy()
                print("boom")
                alive = false
            end
        end)

        -- The vector to get from where we are to where we need to be
        local travel = (p2 - p1)
        -- The directional component of this vector
        local travelDir = travel.unit
        -- The directional component of the upwards vector
        local verticalDir = Vector3.new(0, 1, 0)
        -- We want our projectile moving parallel to the vector perpendicular to both the up and the travel vector
        local perpDir = crossProduct(travelDir, verticalDir)
        -- Integrating sin(t) to get velocity from position gives -cos(x)+c: this is the c
        local t1 = workspace.DistributedGameTime

        while wait(resolution) and alive do
            local t = (workspace.DistributedGameTime - t1) / (math.pi * 2) * frequency
            local v = ((p2 - p1).unit * speed) + (perpDir * (-math.cos(t) * amplitude))
            p.Velocity = v
            tracer(p1, p2)
        end
    end)
end

function tracePoints(points, d)
    if d and d ~= 0 then
        spawn(function()
            for i = 1, #points - 1 do
                tracer(points[i], points[i + 1])
                wait(d)
            end
        end)
    else
        for i = 1, #points - 1 do
            tracer(points[i], points[i + 1])
        end
    end
end

function midpoint(p1, p2)
    return (p1 + p2)/2
end

function crossProduct(u, v)
    return Vector3.new( u.y * v.z - u.z * v.y
                      , u.z * v.x - u.x * v.z
                      , u.x * v.y - u.y * v.x
                      ).unit
end

function calculatePoints(p1, p2)
    -- The vector to get from where we are to where we need to be
    local travelVector = p2 - p1
    -- The directional component of said vector
    local primaryDir = travelVector.unit
    -- The directional component of a vertical vector
    local verticalDir = Vector3.new(0, 1, 0)
    -- We want our projectile to move parallel to both the vertical and the travel vector
    local perpDir = crossProduct(primaryDir, verticalDir)

    local points = {}

    -- Prevent a freeze if we click the skybox, and get an infinite number of points to calculate for
    if travelVector.magnitude > 1000 then
        travelVector = primaryDir * 1000
    end

    -- Calculate each step along the way
    for i = 0, travelVector.magnitude, 1 do
        table.insert( points
                    , p1 + (primaryDir * i) + (perpDir * math.sin(i))
                    )
    end

    return points
end

script.Parent.Equipped:Connect(function(mouse)
    if fireConn then
        fireConn:Disconnect()
    end

    fireConn = mouse.Button1Down:Connect(function()
        local fromP = script.Parent.Parent.Torso.Position
        local toP = mouse.Hit.p

        --[[tracePoints(calculatePoints(fromP, toP), 1/30)]]
        fireBrick(fromP, toP)
    end)
end)

If you uncomment tracePoints, and comment out fireBrick, it should draw the explicit path of the projectile for you.

0
This method involves numerous precise and complicated calculations, and it definitely works flawlessly. Thank you for this knowledge, I will try integrating it into my own tool! UnRandomizing 22 — 5y
Ad
Log in to vote
0
Answered by 5 years ago

Have you tried replacing the Y or Z axis with math.sin(workspace.DistributedTime*180) rather than the X axis?

0
Unfortunately, both didn't work - Z returned the same result, except in the Z axis, and Y would cause it to alternate in latitude which is not my focus. UnRandomizing 22 — 5y

Answer this question