Client-Sided Projectiles Cause Lag Spikes

Hello! I’ve recently made a projectile system, it’s pretty good surface level and I’m happy with it, but there are some optimization issues.

When more than around 10 active projectiles are in flight, occasional lag spikes will occur, and after around 80 projectiles, consistent lag will occur, with intense lag spikes. Looking at the micro-profiler, as is expected, the projectile system is causing lag, but only the consistent lag, not the lag spikes. The job causing the lag spikes goes by the name, “deleteDeferred.” I don’t know what this means or how to optimize this particular job, so I’m kind of stuck.

I’m not too worried about the consistent lag caused by my projectile system, I know I can easily optimize that, but I am concerned about the lag spikes and, “deleteDeferred.” I can’t really find anything on what this job is, so I started guessing.

I assumed maybe this had something to do with the Garbage Collector deleting deferred events, so I tried to change SignalBehavior under Workspace to Immediate, which didn’t solve the issue.

I then guessed that it might be because there were too many RenderStepped connections caused by adding projectiles, so I swapped it from RenderStepped to BindToRenderStep, which did not work either.

I can’t really include all the code in the module that handles each individual projectile, but I can send the main function that actually does the casting

	function meta:Cast(dt:number)
		self:DeltaTimeSetup(dt) -- Synchronizes the delta time for other functions, ease of access
		self.CurrentRoot = self.CurrentEnd -- Root moves to the previous frames End
		self.CurrentEnd = self.CurrentEnd + (self.Velocity * self.deltaTime) + (0.5 * self.Acceleration * self.deltaTime^2) -- End moves via the equation x=x0+V0t+0.5at^2
		
		local drag = self:Aerodynamics(self.DragCoefficient) -- will auto disable itself if there is no drag coefficient, or drag is 0
		
		self.Velocity += (self.Acceleration - drag) * self.deltaTime -- V = V0 + at
		
		self.Age += self.deltaTime
		
		---- RAYCASTING STARTS HERE
		

		if self.VisualizeDebug then
			self:VisualizeDebg(self.CurrentEnd, 1)
		else
			self:Visualize(self.CurrentEnd)
		end
		
		local params = RaycastParams.new()
		params.FilterType = self.Params.FilterType
		params.FilterDescendantsInstances = self.Params.FilterDescendants
		
		
		self.RayResult = workspace:Raycast(self.CurrentRoot, self.CurrentEnd - self.CurrentRoot, params)
		
		
		-- Terminating the ray, or penetrating --
		if self.RayResult then
			
			self:PenCheck() -- Checks if the projectile can penetrate
			
			SafeFunc(self.Params.OnStrikeFunc, self) -- SafeFunc just avoids errors if a function doesn't exist
		
		elseif self.Age >= self.Lifetime then -- The projectile is too old
			
			self:Terminate()
		else
			
			SafeFunc(self.Params.OnRunFunc, self)
		end
		-- Terminating the ray, or penetrating --
		
		
		self.firstFrameDebounce = true
		
	end
1 Like

show the code of the functions that you’re calling.
when exactly does the lag spike start?

For testing purposes, most of the functions you can see being called here are disabled, or are nil.

For example: The aerodynamics is not actually doing anything because I made sure the Projectile’s drag coefficient was 0.

Other functions that are associated with the Projectile’s parameters are nil, so they don’t run either.

This is 95% of the code, and the functions that are actually running are no more than 5-30 lines of code and I don’t think do anything intensive. So I don’t think it has anything to do with them, I will give you the functions that are actually running though.

	function meta:PenCheck()
		if self.Params.PenFunc then
			local canPen = self.Params.PenFunc(self)
			if not canPen then
				self:Terminate()
			elseif canPen then
				SafeFunc(self.Params.OnPenFunc, self)
			else
				warn("The PenFunc specified does not return a boolean or nil. Make sure it returns a boolean, or at least nil.")
				self:Terminate()
			end
		else
			self:Terminate()
		end
	end


	function meta:Terminate()
		if self.CurrentVisualizer then
			self.CurrentVisualizer:Destroy()
			self.CurrentVisualizer = nil
		end
		runService:UnbindFromRenderStep(self.ID)
		
		SafeFunc(self.Params.OnTerminateFunc, self)
	end


	function meta:Visualize(Pos:Vector3)
		local visual:BasePart? = self.Visualizer

		if not visual then return end
		
		local inst
		
		if self.CurrentVisualizer then
			inst = self.CurrentVisualizer
			
			inst.Parent = game.Workspace.Projectiles
			inst.CanCollide = false
			inst.Anchored = true
			inst.Massless = true
			inst.CFrame = CFrame.new(Pos) * self:Orientation()
		else
			inst = visual:Clone()
			inst.Parent = game.Workspace.Projectiles
			inst.CanCollide = false
			inst.Anchored = true
			inst.Massless = true
			inst.CFrame = CFrame.new(Pos) * self:Orientation()
		end
		
		self.CurrentVisualizer = inst
		
		self:AddToBlacklist(inst)
	end

	function meta:AddToBlacklist(part:BasePart)
		if self.Params.FilterType == Enum.RaycastFilterType.Exclude then
			table.insert(self.Params.FilterDescendants, part)
		end
	end

	function meta:DeltaTimeSetup(dt:number)
		if self.firstFrameDebounce or not deltaTimeOveride then
			self.deltaTime = dt
		else
			self.deltaTime += dt
		end
	end

Lag spikes occur occasionally, at no certain point during moments where more than 10 or so projectiles are active, they do happen in a sort of frequency, but I can’t identify any lines of code that would be running at such frequency. They get more intense as more projectiles are added.

Interesting, that’s quite a unique problem. Since there’s no documentation on what deleteDeferred actually does, I would personally use blackbox testing to identify what part of your code causes the job to take up so much time.

The idea is that you slowly disable parts of your code until deleteDeferred goes down to a few milliseconds or goes away entirely. Since we know that an empty Roblox game should not take so long to complete the deleteDeferred job, eventually you will disable the part of code that causes the lag. Once you do, you will have identified what the problem is and you will be able to start making a workaround.

Yes, I didn’t know there was a name for that procedure but I’ve been doing something like that in the mean-time, it seems to be something systemic because none of user-inputted functions that the projectile can use are causing the issue. But due to how interconnected everything is, what-with multiple modules being involved, it’s hard to only comment out or disable small parts of the program w/o completely disabling it or breaking it.

Thanks for the suggestion, I’ll keep trying.

1 Like

I seem to have solved the issue. It seemed there were a few issues, all due to the Visualize function.

I still have no idea what deleteDeferred is, nor do I think I will ever, but here were the main issues, for reference this was the function originally:

	function meta:Terminate()
		if self.CurrentVisualizer then
			self.CurrentVisualizer:Destroy()
			self.CurrentVisualizer = nil
		end
		runService:UnbindFromRenderStep(self.ID)
		
		SafeFunc(self.Params.OnTerminateFunc, self)
	end


	function meta:Visualize(Pos:Vector3)
		local visual:BasePart? = self.Visualizer

		if not visual then return end
		
		local inst
		
		if self.CurrentVisualizer then
			inst = self.CurrentVisualizer
			
			inst.Parent = game.Workspace.Projectiles
			inst.CanCollide = false
			inst.Anchored = true
			inst.Massless = true
			inst.CFrame = CFrame.new(Pos) * self:Orientation()
		else
			inst = visual:Clone()
			inst.Parent = game.Workspace.Projectiles
			inst.CanCollide = false
			inst.Anchored = true
			inst.Massless = true
			inst.CFrame = CFrame.new(Pos) * self:Orientation()
		end
		
		self.CurrentVisualizer = inst
		
		self:AddToBlacklist(inst)
	end
  1. I parented the visualizer every frame, regardless if it was already initialized and properly parented, my reasoning for this was if some outside influence, be it a mistake by me, or an exploit, wanted to move the projectile out of workspace.Projectiles they wouldn’t be able to so easily. In retrospect, this can’t be done, because a lot of internal things happen whenever an instance is parented, that’s why it’s recommended to be the last property filled, again, I didn’t adhere to this (I think I just forgot)

  2. Every frame, I would add the visual instance to the blacklist, this is a list of BaseParts that the ray casts will ignore, this is done so that a ray cast doesn’t collide with it’s own visual. However, doing this every frame adds the instance again over and over to the array, which is obviously a big no-no and a memory leak if I’m remembering correctly, this might not be exactly what’s happening, but I moved that function to only happen upon initializing a visual, and it works.

It’s still a bit choppy according to the micro-profiler, but unnoticeable to my eye at least. Here is the revised function:

		local visual:BasePart? = self.Visualizer

		if not visual then return end
		
		local inst
		
		if self.CurrentVisualizer then
			inst = self.CurrentVisualizer
			
			inst.CFrame = CFrame.new(Pos) * self:Orientation()
		else
			inst = visual:Clone()
			inst.Parent = game.Workspace.Projectiles
			inst.CanCollide = false
			inst.Anchored = true
			inst.Massless = true
			inst.CFrame = CFrame.new(Pos) * self:Orientation()
			self.CurrentVisualizer = inst
			self:AddToBlacklist(visual)
		end
	end

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