Extremely imperformant tracer visualisation

So I’ve been making a simple script for bullet visualisation, but its extremely laggy. I am actually starting to think that this is a roblox/lua issue, and that roblox engine is simply extremely slow compared to unity or other engines. Anyways, here is a video with script performance panel in the left, as you can see, there isnt that many tracers on the screen, but the local script activity is skyrocketing, not even talking about the fact I have about 10 fps while testing it:


Here is the script I use, its nothing complicated really, but I dont think there is any better way to handle it anyway. I know that maybe using tweenservice would help, but I cant use it due to other reasons.

local replicated = game:GetService("ReplicatedStorage")
local projectileEvent = replicated:WaitForChild("Remotes"):WaitForChild("Projectile")
local projectiles = replicated:WaitForChild("Projectiles")
local runService = game:GetService("RunService")


local function projectileVis(bullet, startPos, endPos, projectileSpeed)
	local iterationDistance = (endPos - startPos).Magnitude
	local iterations = math.ceil(iterationDistance / projectileSpeed)
	local incrementVector = (endPos - startPos) / iterations
	
	local projectile = projectiles:FindFirstChild(bullet):Clone()
	projectile.CFrame = CFrame.new(startPos)
	projectile.Parent = workspace
	for i = 1, iterations, 1 do
		projectile.Position = projectile.Position + incrementVector
		runService.RenderStepped:Wait()
	end
	
	task.delay(0.5,function() projectile:Destroy() end)
end

while task.wait(0.033) do
	task.spawn(projectileVis,"SMGBullet", workspace:WaitForChild("FirePart").Position, workspace:WaitForChild("EndPart").Position, 5)
end

projectileEvent.OnClientEvent:Connect(projectileVis)

If someone does have any idea on how I could improve performance of the tracers, I would be extremely grateful.

I would say use FastCast, it handles all the simulation while being so performant, it doesn’t control anything over your gun so it’s great. Maybe that fixes all your problems?

1 Like

I really need to use trails for the tracers, because without them, the whole thing look incedibly plain. The problem with fastcast it that it literally doesnt work with trails, I have tried it in the past but all projectiles “create” somewhere in the sky, which make the trail come also from the sky. I will check fast cast once more and try to implement trails to its projectiles, I will let you know if I succeeded or not

Ah, I know that problem as I’ve came across it too! It’s really annoying and I couldn’t find a fix for it, it may have been fixed, which would be great, but if not, here are some ways to make this more performant:

  1. Simulate on the client, fire event from client → server → all clients and simulate it there, not on the server, make sure to not stimulate it twice on the client that shot it!
  2. Damage on the server, handle raycasting on the server, don’t do it multiple times, just once and apply damage or whatever you want from there.
1 Like

yeh, the whole thing runs on client already, not server. If I would put this in server script it would probably crash lmao

1 Like

I’m quite out of ideas here to be honest with you… My best guess would be to try your best to decrease calculations and maybe using a task.wait() instead of runService.RenderStepped:Wait()? Or even make it wait each n iterations but I think that would cause more lag.

2 Likes

You can replace task.delay(0.5,function() projectile:Destroy() end) with:
task.delay(0.5, projectile.Destroy, projectile)

Place the workspace:WaitForChild("FirePart") outside of the while loop.

local firePart = workspace:WaitForChild("FirePart")
local endPart = workspace:WaitForChild("EndPart")
while task.wait(0.033) do
    if not firePart or not endPart then break end
	task.spawn(projectileVis, "SMGBullet", firePart.Position, endPart.Position, 5)
end

Why not just use Debris service?

I got the information from the following post in this thread. I believe it is faster than Debris since it is said that Debris runs in a 30 hz scheduler like wait, spawn, and delay functions.

Performance is better, it doesn’t error and doesn’t need a whole need thread, you can read the docs about it.

The problem doesn’t really seem to be your code, since it wouldn’t get you to 10 fps normally. The problem might be the bullets and the fact that you are cloning a lot of them. Since we don’t know anything about the bullets besides that it has atleast one trail we can’t really know what is wrong with them. Make sure that the bullet part has CanCollide and CanQuery off, is anchored, doesn’t cast shadows and anything else that could improve its performance.

Though, there are some ways to speed up your code if you really think that’s the problem:

local replicated = game:GetService("ReplicatedStorage")
local projectileEvent = replicated:WaitForChild("Remotes"):WaitForChild("Projectile")
local projectiles = replicated:WaitForChild("Projectiles")
local runService = game:GetService("RunService")
local debris = game:GetService("Debris")

local renderStepped = runService.RenderStepped
local firePart = workspace:WaitForChild("FirePart")
local endPart = workspace:WaitForChild("EndPart")

local function projectileVis(bullet, startPos, endPos, projectileSpeed)
    local direction = (endPos - startPos)
	local distance = direction.Magnitude
	local iterations = math.ceil(distance / projectileSpeed)
	local incrementVector = direction / iterations
	
	local projectile = projectiles[bullet]:Clone()

    -- won't change this but are you sure it shouldn't be CFrame.new(startPos, endPos)
    -- to make the bullet point to its destination?
	projectile.CFrame = CFrame.new(startPos)
	projectile.Parent = workspace
	for i = 1, iterations, 1 do
		projectile.Position = projectile.Position + incrementVector
		renderStepped:Wait()
	end
	
    debris:AddItem(projectile, 0.5)
end

while true do
    task.wait(1/30)
	task.spawn(projectileVis,"SMGBullet", firePart.Position, endPart.Position, 5)
end

projectileEvent.OnClientEvent:Connect(projectileVis)

The bullets are super small invisible parts with nothing other than two attachments and a trail. I’ve disabled all unnecessary properties. I will try to create a bullet pool instead of cloning them forever and see if it helps

1 Like

A potential solution to this problem could be to adopt a data-oriented approach, which has been specifically designed for high-performance systems. This paradigm emphasizes data structures and algorithms that are optimized for efficient memory usage and processing speed.

To apply this approach to the problem at hand, each bullet could be treated as a piece of data rather than an object. By organizing the bullets in an array, the program could iterate through the array every frame to update each bullet’s position. However, it is important to note that the Roblox physics engine has its limitations, which could cause issues if the program relies heavily on physics simulations. Therefore, a more efficient solution could involve calculating the bullet trajectory using basic mathematical formulas and updating each bullet’s position.

One of the advantages of a data-oriented approach is its ability to handle large amounts of data efficiently. In this case, if there are a significant number of bullets on the screen, it is critical to ensure that the program can update their positions quickly to maintain a smooth and responsive user experience. This approach can also reduce the amount of memory required to store data, which is especially useful in situations where memory is limited.

Another advantage of the data-oriented approach is the ability to take advantage of parallel processing. By dividing the data into smaller chunks and processing them simultaneously across multiple cores, the program can achieve significant performance gains. This can be particularly useful in situations where the processing time required for each bullet is relatively low, and there are a large number of bullets to update.

In addition to the data-oriented approach, there are other strategies that can be employed to optimize the program’s performance. For example, reducing the number of unnecessary calculations can help to free up processing power that can be used for more critical operations. Additionally, caching frequently used data can reduce the number of memory accesses required, further improving performance.

A data-oriented approach is a potential solution to the problem of updating bullet positions efficiently in a Roblox game. By treating each bullet as a piece of data and organizing them in an array, the program can efficiently update their positions every frame. However, it is important to keep in mind the limitations of the Roblox physics engine and consider alternative approaches such as calculating bullet trajectories using basic mathematical formulas. By employing additional strategies such as parallel processing and reducing unnecessary calculations, the program’s performance can be further optimized.

Here is a theoretical example of a similar approach I coded up.

local replicated = game:GetService("ReplicatedStorage")
local projectileEvent = replicated.Remotes.Projectile
local projectiles = replicated.Projectiles
local workspace = game:GetService("Workspace")

local activeProjectiles = {}

local function projectileVis(bullet, startPos, endPos, projectileSpeed)
	local direction = (endPos - startPos).Unit
	local iterations = (endPos - startPos).Magnitude / projectileSpeed

	local projectile = projectiles:FindFirstChild(bullet):Clone()
	projectile.CFrame = CFrame.new(startPos)
	projectile.Parent = workspace

	table.insert(activeProjectiles, {
		projectile = projectile,
		direction = direction,
		speed = projectileSpeed,
		iterations = iterations,
		currentIteration = 0,
	})
end

projectileEvent.OnClientEvent:Connect(projectileVis)

game:GetService("RunService").RenderStepped:Connect(function(dt)
	for i, data in ipairs(activeProjectiles) do
		data.currentIteration = data.currentIteration + dt
		data.projectile.Position = data.projectile.Position + data.direction * data.speed * dt

		if data.currentIteration >= data.iterations then
			task.delay(0.5, function()
				data.projectile:Destroy()
			end)

			table.remove(activeProjectiles, i)
		end
	end
end)

I am not 100% sure this will work considering its free handed, but it should give you a starting point.

2 Likes

I also thought about this way of doing it. But it just seemed to me that there is not any difference in the actual operations done which would be faster, and it only would be slower bacause of the additional dictionary iteration loop and values assigning. I will definitely try this though!