Scripting HelpersARCHIVE
A little piece of Roblox history. Preserved for everyone, one question at a time.Read about the archive →
✓ ANSWER ACCEPTED

How Can I Apply a Sine Wave to a Projectile?

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

✓ Accepted answer

#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.

1 comment

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!

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

1 comment

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.

This discussion is preserved as it was. New answers, comments, and votes are closed.