Obstacle manager for game cutting out and not functioning properly after a few minutes of simulation running?

basically, i created a script for my game “escape from florida” that would hopefully optimize the performance of obstacles and their spawning mechanics as previous versions of my game relied on several unecessary copies and clones of scripts instead of one centralized manager. this obstacle manager seemed to work for a while (even with the cons being that obstacles no longer damage you in the way that i originally intended), but then mysteriously cut out and made obstacles lag down their physics calculations to about a frame per second. i tried to use :clearallchildren() in order to clean up obstacles when their count reached over 100, but that only led to them being cleared very frequently and making the overall experience unenjoyable. ive left a copy of my script down below. any ideas for how to fix both the slowed calculations and make the damage work by a singular touch and not a frame check?

p.s.
i left the link to the game down below. check it out live if that helps:

local RS = game:GetService("ReplicatedStorage")
local RunS = game:GetService("RunService")
local TS = game:GetService("TweenService")
local DS = game:GetService("Debris")

local spawn_area = script.Parent
local spawners = spawn_area.Parent
local main_map = spawners.Parent
local florida = main_map.Parent
local spawned_obstacles = florida:WaitForChild("spawned obstacles")

local obstacle_delete_Y = workspace.FallenPartsDestroyHeight

local obstacles = RS:WaitForChild("obstacles")

local fireball = obstacles:WaitForChild("fireball")
local gator = obstacles:WaitForChild("gator")
local palm_tree = obstacles:WaitForChild("palm_tree")

local fireball_tick = 0.1
local gator_tick = 0.1
local palm_tree_tick = 0.1

local set_fireball_tick = 0.12
local set_gator_tick = 1.25
local set_palm_tree_tick = 10

RunS.Heartbeat:Connect(function(delta_time)
	
	if fireball_tick > 0 then
		fireball_tick -= delta_time
		
		if fireball_tick <= 0 then
			fireball_tick = set_fireball_tick
			spawnobstacle(fireball,10,spawn_area,1,true,true)
		end
	else
		fireball_tick = set_fireball_tick
		--spawnobstacle(fireball,60,spawn_area,30)
	end
	
	if gator_tick > 0 then
		gator_tick -= delta_time

		if gator_tick <= 0 then
			gator_tick = set_gator_tick
			spawnobstacle(gator,10,spawn_area,2.5,true,true)
		end
	else
		gator_tick = set_gator_tick
		--spawnobstacle(gator,60,spawn_area,75)
	end
	
	if palm_tree_tick > 0 then
		palm_tree_tick -= delta_time

		if palm_tree_tick <= 0 then
			palm_tree_tick = set_palm_tree_tick
			spawnobstacle(palm_tree,15,spawn_area,5,true,false)
		end
	else
		palm_tree_tick = set_palm_tree_tick
		--spawnobstacle(palm_tree,60,spawn_area,100)
	end
	
	for i,obstacle in pairs(spawned_obstacles:GetChildren()) do

		obstacle_damage = nil
		obstacle_sit_bool = nil

		if obstacle:IsA("Part") or obstacle:IsA("MeshPart") or obstacle:IsA("BasePart") or obstacle:IsA("WedgePart") or obstacle:IsA("TrussPart") or obstacle:IsA("PartOperation") then
			--print("recognized "..obstacle.Name.." as an obstacle")
			if obstacle:FindFirstChild("damage") then
				if obstacle:FindFirstChild("damage"):IsA("NumberValue") then
					obstacle_damage = obstacle:FindFirstChild("damage").Value
				else
					obstacle_damage = nil
				end
			end

			if obstacle:FindFirstChild("sit") then
				if obstacle:FindFirstChild("sit"):IsA("BoolValue") then
					obstacle_sit_bool = obstacle:FindFirstChild("sit").Value
				else
					obstacle_sit_bool = nil
				end
			end

			obstacle.Touched:Connect(function(otherpart)
				if otherpart then
					if otherpart.Parent:FindFirstChildOfClass("Humanoid") then

						--print("obstacle "..obstacle.Name.." hit character "..otherpart.Parent.Name)

						local touched_humanoid = otherpart.Parent:FindFirstChildOfClass("Humanoid")
						if obstacle_damage then
							touched_humanoid.Health -= obstacle_damage
						else
							touched_humanoid.Health -= 0.00001
						end
						if obstacle_sit_bool then
							if obstacle_sit_bool == true then
								touched_humanoid.Sit = true
							else

							end
						end

					else
						--print("obstacle "..obstacle.Name.." hit part "..otherpart.Name)
					end
				end
			end)
			
			if obstacle.Position.Y <= obstacle_delete_Y + 10 then
				if obstacle then
					DS:AddItem(obstacle,0)
				end
				if obstacle then
					obstacle:Destroy()
				end
			end
			
			--if i%100==0 then
			--	print("obstacle "..obstacle.Name.." is number "..i.." of this check at "..time().." since start of simulation")
			--end
			
			if i>100 then
				print("attempting full wipe on "..spawned_obstacles.Name)
				spawned_obstacles:ClearAllChildren()
			end

		end
	end
end)

function spawnobstacle(obstacle,debristime,spawnpart,damage,sit_on_hit,random_rotation)
	local selected_spawn_position = spawnpart.Position + Vector3.new(math.random(-spawnpart.Size.X/2, spawnpart.Size.X/2),math.random(-spawnpart.Size.Y/2, spawnpart.Size.Y/2),math.random(-spawnpart.Size.Z/2, spawnpart.Size.Z/2))
	
	local obstacle_clone = obstacle:Clone()
	obstacle_clone.Parent = spawned_obstacles

	if obstacle_clone.ClassName == "Part" or obstacle_clone.ClassName == "MeshPart" then
		
		if random_rotation then
			obstacle_clone.Rotation = Vector3.new(
				math.random(0,360),
				math.random(0,360),
				math.random(0,360)
			)
		end
		
		obstacle_clone.Anchored = false
		obstacle_clone.Position = selected_spawn_position
		
		local damage_value = Instance.new("NumberValue",obstacle_clone)
		damage_value.Value = damage
		damage_value.Name = "damage"
		
		local sit_bool = Instance.new("BoolValue",obstacle_clone)
		if sit_on_hit then
			sit_bool.Value = true
		else
			sit_bool.Value = false
		end
		sit_bool.Name = "sit"
		
	end
end

I’m a beginner at coding but an idea could be that when an obstacle reaches a wall at the beginning, it gets destroyed, so when the wall is touched it can check if the object is a certain name (you can give the cloned part a name in the spawn obstacle function) and if its an obstacle it can destroy it.

If you have any questions regarding this please let me know

Try this updated code. It should help a lot.

local RS = game:GetService("ReplicatedStorage")
local RunS = game:GetService("RunService")
local TS = game:GetService("TweenService")
local DS = game:GetService("Debris")

local connectionCache = {}

local spawn_area = script.Parent
local spawners = spawn_area.Parent
local main_map = spawners.Parent
local florida = main_map.Parent
local spawned_obstacles = florida:WaitForChild("spawned obstacles")

local obstacle_delete_Y = workspace.FallenPartsDestroyHeight

local obstacles = RS:WaitForChild("obstacles")

local fireball = obstacles:WaitForChild("fireball")
local gator = obstacles:WaitForChild("gator")
local palm_tree = obstacles:WaitForChild("palm_tree")

local fireball_tick = 0.1
local gator_tick = 0.1
local palm_tree_tick = 0.1

local set_fireball_tick = 0.12
local set_gator_tick = 1.25
local set_palm_tree_tick = 10

RunS.Heartbeat:Connect(function(delta_time)

	if fireball_tick > 0 then
		fireball_tick -= delta_time

		if fireball_tick <= 0 then
			fireball_tick = set_fireball_tick
			spawnobstacle(fireball,10,spawn_area,1,true,true)
		end
	else
		fireball_tick = set_fireball_tick
		--spawnobstacle(fireball,60,spawn_area,30)
	end

	if gator_tick > 0 then
		gator_tick -= delta_time

		if gator_tick <= 0 then
			gator_tick = set_gator_tick
			spawnobstacle(gator,10,spawn_area,2.5,true,true)
		end
	else
		gator_tick = set_gator_tick
		--spawnobstacle(gator,60,spawn_area,75)
	end

	if palm_tree_tick > 0 then
		palm_tree_tick -= delta_time

		if palm_tree_tick <= 0 then
			palm_tree_tick = set_palm_tree_tick
			spawnobstacle(palm_tree,15,spawn_area,5,true,false)
		end
	else
		palm_tree_tick = set_palm_tree_tick
		--spawnobstacle(palm_tree,60,spawn_area,100)
	end
	
	for i,obstacle in pairs(spawned_obstacles:GetChildren()) do
		if obstacle:GetAttribute("RandomlyRotate") then
			obstacle.Rotation = Vector3.new(
				math.random(0,360),
				math.random(0,360),
				math.random(0,360)
			)
		end
		if obstacle.Position.Y <= obstacle_delete_Y + 10 then
			if obstacle then
				obstacle:Destroy()
			end
			if obstacle.Parent ~= nil then
				DS:AddItem(obstacle,0) --> If we can, avoid Debris service calls since Destroy() should trigger our ChildRemoved cleanup.
			end
		end
	end
end)

local function ChildRemoved(obstacle)
	if connectionCache[obstacle] then
		for _,Connection in connectionCache[obstacle] do
			pcall(function() Connection:Disconnect() end)
		end
		connectionCache[obstacle] = nil
	end
end

local function ChildAdded(obstacle)
	--> BasePart encompasses all previous checks, meaning if it's a BasePart, it's an obstacle.
	if obstacle:IsA("BasePart") and not connectionCache[obstacle] then --> Ensure we didn't already add this child to the touched events by checking for it in the connectionCache.
		connectionCache[obstacle] = {}
		--> Cache connections (touched) to be forcefully disconnected later to prevent any possible memory leaks
		
		--print("recognized "..obstacle.Name.." as an obstacle")
		--> Insert connections to cache
		table.insert(connectionCache[obstacle],
			obstacle.Touched:Connect(function(otherpart)
				if otherpart then
					local touched_humanoid = otherpart.Parent:FindFirstChildOfClass("Humanoid")
					--> Store Touched_humanoid first and we can check if it exists rather than calling FindFirstChildOfClass twice.
					if touched_humanoid then

						--print("obstacle "..obstacle.Name.." hit character "..otherpart.Parent.Name)
						
						--> Optimize indexing a Value object and other shenanigans with Attributes! :D
						if obstacle:GetAttribute("damage") then
							touched_humanoid.Health -= obstacle:GetAttribute("damage")
						else
							touched_humanoid.Health -= 0.00001
						end
						
						--> More attributes, they're faster (by a LOT) than ValueObjects and are very versatile for such an application.
						if obstacle:GetAttribute("sit") then
							touched_humanoid.Sit = true
						end

					else
						--print("obstacle "..obstacle.Name.." hit part "..otherpart.Name)
					end
				end
			end)
		)

	end
end

--> Move looped indexing (and touched event spam) outside of main RunService loop
--> Run this before leaving it up to the ChildAdded events to ensure we capture every spawned obstacle.
for i,obstacle in pairs(spawned_obstacles:GetChildren()) do
	ChildAdded(obstacle)
end

--> Use events to identify setup for obstacles
spawned_obstacles.ChildAdded:Connect(function(obstacle)
	ChildAdded(obstacle)
end)

--> Use events to identify cleanup for obstacles
spawned_obstacles.ChildRemoved:Connect(function(obstacle)
	ChildRemoved(obstacle)
end)

function spawnobstacle(obstacle,debristime,spawnpart,damage,sit_on_hit,random_rotation)
	local selected_spawn_position = spawnpart.Position + Vector3.new(math.random(-spawnpart.Size.X/2, spawnpart.Size.X/2),math.random(-spawnpart.Size.Y/2, spawnpart.Size.Y/2),math.random(-spawnpart.Size.Z/2, spawnpart.Size.Z/2))

	local obstacle_clone = obstacle:Clone()

	if obstacle_clone.ClassName == "Part" or obstacle_clone.ClassName == "MeshPart" then

		if random_rotation then
			--> Attributes save the day again!
			obstacle_clone:SetAttribute("RandomlyRotate", true)
		end

		obstacle_clone.Anchored = false
		obstacle_clone.Position = selected_spawn_position

		obstacle_clone:SetAttribute("damage", if damage then damage else 0.00001)

		if sit_on_hit then
			obstacle_clone:SetAttribute("sit", if sit_on_hit ~= nil then true else false)
		end

	end
	--> ALWAYS ALWAYS parent instances after applying their properties to them. Prevents subtick/superfluous (unnecessary) property changes in the physics engine as well as the render engine.
	obstacle_clone.Parent = spawned_obstacles
	
	--> Actually implement lightweight self destruction methods (debris time)
	task.delay(debristime, function()
		obstacle_clone:Destroy()
	end)
end

Let me know if this fixes your issues!

take a look at the code. theres a variable in there for a y threshold that when any of the obstacles pass, they immediately get deleted. functions the same way as your idea

this helps a lot, but aside from the comments left in the script, do you mind explaining what you changed? im interested in learning from my mistakes and lapses

Ah thanks for pointing that out

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