#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
- How do we calculate the offset vector?
- 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"
game:GetService("Debris"):AddItem(p, 1)
end
function fireBrick(p1, p2)
local speed = 10
local resolution = 0.1
local amplitude = 5
local frequency = 40
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"
local f = Instance.new("BodyForce", p)
f.Force = Vector3.new(0, 1, 0) * (p:GetMass() * workspace.Gravity)
spawn(function()
local alive = true
p.Touched:Connect(function(hit)
if hit and hit.Name ~= "Trace" then
p:Destroy()
print("boom")
alive = false
end
end)
local travel = (p2 - p1)
local travelDir = travel.unit
local verticalDir = Vector3.new(0, 1, 0)
local perpDir = crossProduct(travelDir, verticalDir)
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)
local travelVector = p2 - p1
local primaryDir = travelVector.unit
local verticalDir = Vector3.new(0, 1, 0)
local perpDir = crossProduct(primaryDir, verticalDir)
local points = {}
if travelVector.magnitude > 1000 then
travelVector = primaryDir * 1000
end
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
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
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!