How would i get to code a* pathfinding to run far distances without FPS drop?

That is not a dynamic pathfinding. it doenst work like that

That’s why I’m saying look into D* lite. It’s designed for the exact purpose of pathfinding in dynamic environments and you’ll probably find the optimizations you’re looking for while implementing it.

If you are doing it far distances, you probably should just avoid A* altogether honestly. I would recommend using a greedy best first search as it will search far less cells and still give a reasonable outcome

How did roblox pathfinding work with long distanced is it not a*?

I have managed to improve performance a lot by changing how you get the current node, before you were doing a table.sort on the entire openSet and then grabbing the first index, this is very unnessecary as the only one you need sorted is the first one in the array, so as the amount of objects in the open set increased, so did the computation time a lot

How I fixed this was just to do a simple loop through the open set and pick the one with the lowest fScore:

debug.profilebegin("Determine Current New")
local current, lowest = nil, math.huge
local index = nil
for i, v in openSet do
	local score = fScore[v]
	if score < lowest then
		index = i
		current = v
		lowest = score
	end
end
debug.profileend()

Roblox uses A* for their pathfinding, however roblox does not use a grid system for theirs, as with every 3d game with pathfinding, Roblox uses a navmesh, you can view it in studio settings.

For me it works the same, here is what i did:

local function heuristic(PosA, PosB)
	return (PosA - PosB).magnitude
end
local function snap(pos : Vector3)
	return Vector3.new(math.floor(pos.X/3)*3+1.5,math.floor(pos.Y/3)*3+1.5, math.floor(pos.Z/3)*3+1.5)
end
local blocks = {}
for i,v in pairs(workspace.WalkableParts:GetChildren()) do
	local rounder = snap(v.Position)
	blocks[rounder] = 1
end

local function getNeighbors(node, walkableParts)
	local neighbors = {}
	local directions = {
		Vector3.new(3, 0, 0), Vector3.new(-3, 0, 0),
		Vector3.new(0, 0, 3), Vector3.new(0, 0, -3),
		Vector3.new(3, 0, 3), Vector3.new(3, 0, -3),
		Vector3.new(-3, 0, 3), Vector3.new(-3, 0, -3)
	}

	for _, direction in ipairs(directions) do
		local neighborPos = snap(node + direction) 
		if blocks[neighborPos] == nil then
			table.insert(neighbors, neighborPos)


		end
	end

	return neighbors
end

local function reconstructPath(cameFrom, current)
	local totalPath = {current}
	while cameFrom[current] do
		current = cameFrom[current]
		table.insert(totalPath, 1, current)
	end
	return totalPath
end

local function aStar(start, goal, walkableParts)
	local openSet = {start}
	local cameFrom = {}
	local gScore = {[start] = 0}
	local fScore = {[start] = heuristic(start, goal)}
	local s
	while #openSet > 0 do
		local current, lowest = nil, math.huge
		local index = nil
		for i, v in openSet do
			local score = fScore[v]
			if score < lowest then
				index = i
				current = v
				lowest = score
			end
		end		
		current = table.remove(openSet, 1)

		if (current - goal).Magnitude < 1 then
			s = reconstructPath(cameFrom, current)
			return reconstructPath(cameFrom, current)
		end

		for _, neighbor in ipairs(getNeighbors(current, walkableParts)) do
			local tentativeGScore = gScore[current] + (current - neighbor).magnitude

			if tentativeGScore < (gScore[neighbor] or math.huge) then
				cameFrom[neighbor] = current
				gScore[neighbor] = tentativeGScore
				fScore[neighbor] = gScore[neighbor] + heuristic(neighbor, goal)
				if not table.find(openSet, neighbor) then
					table.insert(openSet, neighbor)
				end
			end
		end
	end
	

end

-- Example usage
local function moveToPosition(path)
	for _, part in ipairs(path) do
		print(part.Position)
		part.Color = Color3.new(1, 0, 0.0156863)
	end
end


local startPart = workspace.Start.Position
local goalPart = workspace.Target.Position
while wait(0.1) do
	if startPart and goalPart then
		local path = aStar(startPart, goalPart, blocks)
		if path and path[2] then
			print(path[2])
			startPart = path[2]
			local a = Instance.new("Part", workspace)
			a.Anchored = true
			a.Size = Vector3.new(3,3,3)
			a.CanCollide = false
			a.Color = Color3.new(0, 0, 0.498039)
			a.Position = path[2]
		else
			warn("No path found!")
		end
	else
		warn("Start or goal part not found in walkable parts!")
	end
end```

How far apart is the start and goal positions? I still had lag spikes from long distances but they weren’t as bad, I don’t really have any more suggestions on how to make it faster though other than just switching to greedy best first search and adding a limit to how long each frame can take and if it goes over that limit, to call task.wait()

50~ studs (qr4124214214214151)

Hey, thanks for the mention.
It seems like the question is already solved, though I’ll give it a look, maybe something was missed.

1 Like

I gave it a quick read, and found some issues, starting with reconstructPath. It has a time complexity of O(n²) and a space complexity of O(n)! I used a more performant function (a generic reverseTable function) which has a time complexity of O(n) and a space complexity of just O(1) making it way more performant, here it is:

--- Reverses a table.
---@param t the table to reverse.
---@return T
local function reverseTable<T>(t: T): T
	for i = 1, math.floor(#t / 2) do
		local j = #t - i + 1
		t[i], t[j] = t[j], t[i]
	end

	return t
end

You are using 2 extra tables for both gScore and fScore, I personally went with a more OOP approach, I had a Node object to store them, it was simple like this:

local server = script.Parent.Parent.Parent -- Ugly, sadly needed.
local types = require(server.FixedModules.types)

local Node = {}
Node.__index = Node

--- Creates a new `Node` object
---@param position table<number, number>
---@param parent? Node
---@return Node
function Node.new(position: {number}, parent: types.Node?): types.Node
	return setmetatable({
		Position = position,
		Parent = parent,

		g = 0,
		h = 0,
		f = 0
	}, Node)
end

return Node

h is the heuristic (distance, not function).


I see you are sorting the openSet every loop, why?


Now for neighbors, you have getNeighbors which has a loop, then u loop again. A better option would be to do the loop inside the aStar function and completely remove getNeighbors, it only adds an extra loop.

Another note on it, you should create directions outside of the function (in the main scope) instead of initializing it every time the function is called, that will be a big boost, especially on large distances.


Also an extra note, I didn’t use Vector3, I needed 2-axis movement only but I didn’t use Vector2 either. I just made a simple table and 2 functions to calculate distance and whether or not 2 nodes are “close enough”, I think that will also help with performance since you won’t need the overhead that these types add, like .Unit and other things. Just a guess though.


Those are stuff I noticed with a quick look, I tried my best to make my points clear.

I would also like to point out, the code I sent was from another A* implementation (not the one linked), which I made very performant, since it was running on the server, I didn’t stress test it or anything but I was running it with 60 NPCs all walking (together, while obeying rules of not hitting each other), and I had no decrease in FPS, at all.

I didnt understand the first code of reverse table. My table hasnt been created until

reconstructPath

How would i need to reverse the table if i dont have a table of points created?

So to start, you will in no real way be able to compete with roblox’s speed. This is a fundamental limitation of making this in lua. Additionally you appear to be going with a grid based method. This often results in more cells existing that you need to search than using something like a navmesh fundamentally.

To start though, I recommend a couple things that might help. First, I would not sort this table

table.sort(openSet, function(a, b) return fScore[a] < fScore[b] end)

All you are sorting that for is to get the smallest number. The trouble is that sorting algorithms fundamentally cannot reliably run in linear time complexity (basically you have to look at the elements more than once or waste a ton of memory or time to realistically sort). You however, only need to look at the elements once to determine which number is smallest. So instead of sorting, you should loop through the list and keep track of the smallest number or create a structure that is more aware of the order to speed this up.

local minimum = fScore[openSet[1]]
local minIndex = 1
for i, cell in pairs(openSet) do
    if fScore[cell] < minimum then
        minimum = fScore[cell]
        minIndex = i
    end
end
--now minIndex is the index of the cell in openSet that is associated with the lowest fScore

(Note at scale a priority queue (heap) will be better)

Additionally removing an element from the beginning of a list like you do right after the sort, requires lua to shift every single element in the list down 1 to retain it’s order. But since order technically doesn’t matter in this case, you could save some work by replacing the item you want to delete with the last item in the list, then just destroy the last item to remove the duplicate and sidestep that whole reindexing code.

local function unsortedQuickRemove(myTable, index) --elements can be removed more efficiently if order doesn't need to be preserved
    myTable[index] = table.remove(myTable)
end

As for optimization, I recommend trying to split up these cells into regions, and pathfinding between regions before pathfinding smaller segments (hierarchical pathfinding). With simple implementations this will likely lead to a few edge cases where the optimal path isn’t found but only a valid path is, but most of the time it will still be accurate while removing a significant amount of processing depending on the regions properties. You could also cache valid paths between regions to remove a lot of the necessary pathfinding there again at a potential slight cost to accuracy or complexity as well. I’m sure these accuracy issues can be resolved with extra thought and complexity, but a naive algorithm can probably get the job done most of the time.

And for dynamic regeneration. I would not try to recalculate the path over and over, but make it so that when something blocks a grid, any path that uses that cell in the path gets told to recalculate. This way you are only recalculating grids that actually change. This might work and might not depending on how you get your data specifically though. But overall, an event based system will let you avoid redundant work if you can find a way to design this as such without excessive overhead.

I’m unsure of what you mean but, if you’re saying that you’re creating an entirely new path like using pfs:CreatePath() you don’t need to do that. You can just do :ComputeAsync() and use break to change the path. (Which would lead to less lag)

No. i use a custom a star pathfinding, there is no ComputePath()

I’ve never used A star before but I’m pretty sure it’s just a module right? Isn’t it still under the same framework as pathfindingservice?

A* is a pathfinding algorithm. It’s how you calculate a path from a graph representing the game world. Modules would use it under the hood.

That’s pretty smart. Although I’ve never needed to use it. Well then idk I can’t really help with the problem sorry.

No, its a custom pathfinding not inside roblox pathfinding service

Alright then, you’re really smart. But I’m not smart enough to help you sorry lol.

Sorry I forgot to mention, you should save the nodes that has been traversed till the current path, I did that in the Node class by storing the parent of the Node but seems like I’ve forgot to mention that part there. Apologies.