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
So long story short, my goal is to be able to achieve this (left example), and not that (right example).
So what we've sort of got here is three different vectors
Vector3.new(0,1,0)
There are two key problems
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)
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
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.
Have you tried replacing the Y or Z axis with math.sin(workspace.DistributedTime*180)
rather than the X axis?