How to trace a moving part?

I’m trying to trace the movement of a part. What I would like to achieve is similar to a bullet tracer, but persistent, starting from where the part is when I call ApplyImpulse on it. It would be great to have the tracer 3D in some way (not like a Beam or a Trail that is a flat plane). The part has custom physical properties and the workspace has custom gravity, not sure if that’s important.

I’m trying to achieve the same effect as golf shot tracers. this is a golf game

Right now I have a system that approximately shows the path the part is expected to take when I apply the force, but because of the custom physical properties (I think) the path isn’t exact, so I have to divide by a really precise number to get an approximate result.

How do I show path of projectile? - Help and Feedback / Scripting Support - Developer Forum | Roblox

Code for the calculated full path

Applying the force

local velocity = Vector3.new(0, clubData[club].HeightVelocity, clubData[club].DistanceVelocity)
			
ball:ApplyImpulse(velocity)
	
local gravity = Vector3.new(0, -workspace.Gravity, 0)
createAndSetBeam(gravity, velocity / 4.483, ball.Position, 8) -- I have to divide by 4.483 to get a close enough path to accommodate for the physical properties

Calculating path

local function createBeamInProjectileMotionPath()
	local attach0 = Instance.new("Attachment")
	local attach1 = Instance.new("Attachment")

	local beam = Instance.new("Beam")
	beam.Color = ColorSequence.new(Color3.new(1, 0.035294, 0.035294))
	beam.Attachment0 = attach0
	beam.Attachment1 = attach1

	beam.Parent = workspace.Terrain
	attach1.Parent = workspace.Terrain
	attach0.Parent = workspace.Terrain

	return beam, attach0, attach1
end

local function beamProjectile(g, v0, x0, t1)
	-- calculate the bezier points
	local c = 0.5 * 0.5 * 0.5
	local p3 = 0.5 * g * t1 * t1 + v0 * t1 + x0
	local p2 = p3 - (g * t1 * t1 + v0 * t1) / 3
	local p1 = (c * g * t1 * t1 + 0.5 * v0 * t1 + x0 - c * (x0 + p3)) / (3 * c) - p2

	-- the curve sizes
	local curve0 = (p1 - x0).magnitude
	local curve1 = (p2 - p3).magnitude

	-- build the world CFrames for the attachments
	local b = (x0 - p3).unit
	local r1 = (p1 - x0).unit
	local u1 = r1:Cross(b).unit
	local r2 = (p2 - p3).unit
	local u2 = r2:Cross(b).unit
	b = u1:Cross(r1).unit

	local cf1 = CFrame.new(x0.x, x0.y, x0.z, r1.x, u1.x, b.x, r1.y, u1.y, b.y, r1.z, u1.z, b.z)

	local cf2 = CFrame.new(p3.x, p3.y, p3.z, r2.x, u2.x, b.x, r2.y, u2.y, b.y, r2.z, u2.z, b.z)

	return curve0, -curve1, cf1, cf2
end

local function createAndSetBeam(g, v0, x0, endTime, color)
	local t1 = endTime or 1 -- projectile time when in expires, or any number you want tbh
	local curve0, curve1, cf1, cf2 = beamProjectile(g, v0, x0, t1)
	local beam, attach0, attach1 = createBeamInProjectileMotionPath()
	beam.CurveSize0 = curve0
	beam.CurveSize1 = curve1
	beam.FaceCamera = true
	beam.Segments = 50 * math.round(t1 * 3)
	if color then
		beam.Color = ColorSequence.new(color)
	end
	-- convert world space CFrames to be relative to the attachment parent
	attach0.CFrame = attach0.Parent.CFrame:Inverse() * cf1
	attach1.CFrame = attach1.Parent.CFrame:Inverse() * cf2

	return beam, attach0, attach1
end
Club velocities if needed
return {
	{
		Name = "Driver",
		HeightVelocity = 370,
		DistanceVelocity = 660
	},
	{
		Name = "3 Wood",
		HeightVelocity = 390,
		DistanceVelocity = 635
	},
	{
		Name = "5 Wood",
		HeightVelocity = 410,
		DistanceVelocity = 555
	},
	{
		Name = "3 Iron",
		HeightVelocity = 430,
		DistanceVelocity = 505
	},
	{
		Name = "4 Iron",
		HeightVelocity = 450,
		DistanceVelocity = 450
	},
	{
		Name = "5 Iron",
		HeightVelocity = 470,
		DistanceVelocity = 415
	},
	{
		Name = "6 Iron",
		HeightVelocity = 490,
		DistanceVelocity = 375
	},
	{
		Name = "7 Iron",
		HeightVelocity = 490,
		DistanceVelocity = 345
	},
	{
		Name = "8 Iron",
		HeightVelocity = 510,
		DistanceVelocity = 315
	},
	{
		Name = "9 Iron",
		HeightVelocity = 530,
		DistanceVelocity = 285
	},
	{
		Name = "Pitching Wedge",
		HeightVelocity = 550,
		DistanceVelocity = 175
	},
	{
		Name = "Gap Wedge",
		HeightVelocity = 580,
		DistanceVelocity = 100
	},
	{
		Name = "Sand Wedge",
		HeightVelocity = 610,
		DistanceVelocity = 115
	}
}
Ball and workspace properties


More full path resources

Theoretically it should look like this when the path is fully traced, then I fade it out on my own with TweenService

I have a feeling this cannot be achieved with beams, so I’m not sure of any other way to create my desired effect.

You could make it a preset animation, allowing for you to have a preset path to go with it. You could also launch 2 different parts, with one confirming the trail. Both of these don’t exactly solve your problem, but it’s my best answer.

So the vector math you would want to do is
have your starting point.

local lengthvector=10
local traveltime=1
start=CFrame.new(0,0,0)
ending=start:ToWorldSpace(CFrame.new(0,0,-lengthvector))--ha
endsize=Vector3.new(1,1,lengthvector*2)
startsize=Vector3.new(1,1,1)
Bullettrail.Size=startsize
game.TweenService:Create(Bullettrail,TweenInfo.new(traveltime),{Size=endsize,CFrame=ending}):Play()

Something like this would create a part that extends and moves in a way that the back face of the part would remain at the same point. extending from the start point to the end.
Edit: Nevermind this wouldn’t work for a arched path.

Decided to use a brute force approach of creating a new Attachment and Beam every Heartbeat. This works, but I’m not sure about the performance impact. All of this runs on the client to ensure the tracer is smooth (checking the position of the actual ball on the server is very unreliable).

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")

local TweenModule = require(ReplicatedStorage.Modules.Scripts.TweenModule)

local player = game.Players.LocalPlayer

local tracerInstances = 0

ReplicatedStorage.Remotes.TraceShot.OnClientEvent:Connect(function(velocity)
	tracerInstances += 1
	
	task.spawn(function(velocity, tracerInstance)
		print(tracerInstance)
		local ball = workspace.GolfBalls:WaitForChild(player.UserId)

		local tracerRoot = Instance.new("Part")
		tracerRoot.Name = tostring(tracerInstance)
		tracerRoot.CanCollide = false
		tracerRoot.Transparency = 1
		tracerRoot.Anchored = true
		tracerRoot.Parent = workspace.Tracers

		local tracerBall = ball:Clone()
		tracerBall.CanCollide = false
		tracerBall.Transparency = 1
		tracerBall.Name = "TracerBall"
		tracerBall.Anchored = false
		tracerBall.Position = ball.Position
		tracerBall.Parent = tracerRoot

		local attachments = {}
		local beams = {}
		local startingAttachment = Instance.new("Attachment")
		startingAttachment.Parent = tracerRoot
		startingAttachment.WorldCFrame = ball.CFrame
		table.insert(attachments, startingAttachment)

		tracerBall:ApplyImpulse(velocity)
		RunService.Heartbeat:Wait()

		local i = 1
		while tracerBall.AssemblyLinearVelocity.Magnitude > 0.1 do
			local attach = Instance.new("Attachment")
			attach.Parent = tracerRoot
			attach.WorldCFrame = ball.CFrame
			table.insert(attachments, attach)

			local beam = ReplicatedStorage.Objects.Tracers.Tracer:Clone()
			beam.Attachment0 = attachments[i]
			beam.Attachment1 = attachments[i + 1]
			beam.Parent = tracerRoot
			table.insert(beams, beam)

			-- Used to stop the tracer at a certain point
			if tracerBall.Position.Y < -7 then
				tracerBall.Anchored = true
				break
			end

			i += 1
			RunService.Heartbeat:Wait()
		end

		print("ended " .. tracerInstance)
		tracerBall:Destroy()

        -- fade out
     end, velocity, tracerInstances)
end)

this isn’t perfect

sorry for the bad video quality, I’m recording this in studio

Still wondering how I can get the red beam from my original post to have exactly the same path as the ball, but this solves my original problem for now. Let me know if there’s a better way to do this.

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.