Need help creating an optimized pathfinding entities

Hello, So I am making a SCIFI PVE style game where the entities will navigate to the player through a complicated path which requires pathfinding, I was going to use roblox pathfinding but decided that using one created by another user wasn’t really a bad idea, And it works perfectly well.. until i implemented it in a heartbeat loop and its glitching very much, the pathfinding module itself has no problems.

Code:

local entities = {
	["Entities"] = {}
}

local allEntities = entities["Entities"]
local entitiesFolder = _G.Services.Rep:WaitForChild("Entities")

local entitiesDB = _G.AllModules.Entities
local PathfindingModule = _G.AllModules.Pathfinding

local entityNumber = 1

function entities.new(name, location)
	if not entitiesDB[name] then return end

	local entity = _G.Functions.deepCopy(entitiesDB[name])
	local clone = entitiesFolder:FindFirstChild(name):Clone()
	clone.Parent = workspace.Entities
	clone.PrimaryPart.CFrame = CFrame.new(location)
	clone.PrimaryPart:SetNetworkOwner(nil)
	clone.Name = entityNumber

	entity["Physical"] = clone
	entity["Target"] = nil
	entity["Waypoints"] = {}
	entity["WaypointIndex"] = 1
	entity["LastPathUpdate"] = 0
	entity["ID"] = entityNumber

	allEntities[entityNumber] = entity
	entityNumber += 1
	
	return entity
end

local UPDATE_INTERVAL = 0.5
local WAYPOINT_REACH_DISTANCE = 3

local function findNearestTarget(entityPosition)
	local closest, shortestDistance = nil, math.huge
	for _, player in ipairs(game.Players:GetPlayers()) do
		local char = player.Character
		if char and char:FindFirstChild("HumanoidRootPart") and char:FindFirstChild("Humanoid") and char.Humanoid.Health > 0 then
			local distance = (char.PrimaryPart.Position - entityPosition).Magnitude
			if distance < shortestDistance then
				shortestDistance = distance
				closest = char
			end
		end
	end
	return closest
end

local function getPath(startPos, endPos)
	local waypoints = PathfindingModule.mp(startPos, endPos)
	if #waypoints > 0 then
		return waypoints
	else
		return nil
	end
end

_G.Services.RS.Heartbeat:Connect(function(dt)
	for _, entity in pairs(allEntities) do
		if not entity then continue end 
		
		local model = entity.Physical
		if not model or not model.PrimaryPart then continue end

		entity._lastUpdate = entity._lastUpdate or 0
		entity._pathIndex = entity._pathIndex or 1

		entity._lastUpdate += dt
		if entity._lastUpdate >= UPDATE_INTERVAL then
			entity._lastUpdate = 0

			local target = findNearestTarget(model.PrimaryPart.Position)
			if target then
				local waypoints = getPath(model.PrimaryPart.Position, target.HumanoidRootPart.Position)
				if waypoints then
					entity._path = waypoints
					entity._pathIndex = 1
				else
					entity._path = nil
				end
			else
				entity._path = nil
			end
		end

		local path = entity._path
		if path and path[entity._pathIndex] then
			local currentWP = path[entity._pathIndex]
			local entityPos = model.PrimaryPart.Position
			
			if (currentWP - entityPos).Magnitude <= WAYPOINT_REACH_DISTANCE then
				entity._pathIndex += 1
			else
				local direction = (currentWP - entityPos).Unit
				model.PrimaryPart.CFrame = model.PrimaryPart.CFrame + direction * entity.Speed * dt
			end
		end
	end
end)

for i = 1, 50 do
	entities.new("Spiderbot", Vector3.new(math.random(10, 60), 10, math.random(1, 50)))
end
return entities

pathfinding module:

local module = {}

local distance = 2
local max = 100000
local diagonal_directions = {
	Vector3.new(1,0,1), Vector3.new(1,0,-1), Vector3.new(1,0,0), Vector3.new(0,0,1), Vector3.new(0,0,-1),
	Vector3.new(-1,0,1), Vector3.new(-1,0,-1), Vector3.new(-1,0,0),
	Vector3.new(1,1,1), Vector3.new(1,1,-1), Vector3.new(1,1,0), Vector3.new(0,1,1), Vector3.new(0,1,-1),
	Vector3.new(-1,1,1), Vector3.new(-1,1,-1), Vector3.new(-1,0,0)
}

local function VecToKey(v)
	return math.floor(v.X) .. "_" .. math.floor(v.Y) .. "_" .. math.floor(v.Z)
end

function module.mp(start: Vector3, finish: Vector3)
	local openarr, closedarr = {}, {}
	local visited_count, nodes_count = 1, 1
	local endpoint, stop, Emergency_stop = nil, false, false

	local startKey = VecToKey(start)
	closedarr[startKey] = {pos = start, h = 0, f = 0, g = 0, origin = nil}

	local function pdist(a, b)
		return math.sqrt((b.X - a.X)^2 + (b.Z - a.Z)^2)
	end

	local function check(pos, origin)
		if pdist(pos, finish) <= distance then
			endpoint = pos
			stop = true
		end

		local posKey = VecToKey(pos)
		if not closedarr[posKey] and not openarr[posKey] then
			local params = OverlapParams.new()
			params.MaxParts = 1
			params.FilterType = Enum.RaycastFilterType.Include
			params.FilterDescendantsInstances = {workspace.Obstacles, workspace.Entities}
			local objects = workspace:GetPartBoundsInBox(CFrame.new(pos), Vector3.new(distance - 0.1, distance - 0.1, distance - 0.1), params)
			if #objects == 1 then return false end

			nodes_count += 1
			return true
		end
		return false
	end

	local function cycle(point)
		local originKey = VecToKey(point)
		for _, dir in ipairs(diagonal_directions) do
			local pos = point + dir * distance
			if check(pos, point) then
				local h = (finish - pos).Magnitude
				local g = (point - pos).Magnitude + closedarr[originKey].h
				local posKey = VecToKey(pos)
				openarr[posKey] = {pos = pos, h = h, f = h + g, g = g, origin = point}
			end
		end
		if visited_count == nodes_count then
			Emergency_stop = true
		end
	end

	local min, minKey, d = nil, nil, start
	cycle(start)

	while not stop and not Emergency_stop and nodes_count < max do
		min = math.huge
		for key, v in pairs(openarr) do
			if v.f < min then
				min = v.f
				minKey = key
			end
		end

		if min ~= math.huge then
			d = openarr[minKey].pos
			closedarr[minKey] = openarr[minKey]
			openarr[minKey] = nil
			visited_count += 1
		end
		cycle(d)
	end

	if Emergency_stop then
		warn("No path found")
		return {}
	elseif nodes_count >= max then
		warn("Path too far")
		return {}
	end

	local waypoints = {}
	table.insert(waypoints, endpoint)
	local cpKey = VecToKey(endpoint)
	local current = closedarr[cpKey]

	while current do
		local origin = current.origin
		if origin then
			table.insert(waypoints, origin)
			current = closedarr[VecToKey(origin)]
		else
			break
		end
	end

	local reversed = {}
	for i = #waypoints, 1, -1 do
		table.insert(reversed, waypoints[i])
	end

	return reversed
end

return module
1 Like

can you show a video, errors in console, or explain how its glitching?

1 Like