How 2 make A* Pathfinding

hello world! Today, we’re gonna make a simple A* Pathfinder. I have tried to optimize this but I just can’t seem to wrap my head around multithreading this algorithm. I’m not gonna explain the algorithm in this topic in depth. Anyways, lets get to work

DISCLAIMER

This tutorial is meant to give you a base understanding of how the algorithm works, and how you should code it. You should NOT use this in a production environment as it is not optimized.

However, you are free to use the tutorial to get you started and modify it to fit your needs
Next Part

Programming the Algorithm

I have made this pseudocode for the A* algorithm below:

OPEN SET = {} -- SET OF NODES TO BE EVALUATED
CLOSED SET = {} -- SET OF NODES THAT HAS BEEN ALREADY EVALUATED

ADD (START NODE) TO (OPEN SET)

while LOOP
	CURRENT = GET (NODE) IN (OPEN SET) WITH THE LOWEST F COST do
	
	REMOVE (CURRENT) FROM (OPEN SET)
	
	ADD (CURRENT) TO (CLOSED SET)
	
	IF CURRENT IS TARGET NODE
		RETURN
		
	FOR EACH (NEIGHBOUR) OF (CURRENT)
		IF ISTRAVERSABLE(NEIGHBOUR) OR (NEIGHBOUR) IN (CLOSED SET)
			SKIP
			
		IF NEW PATH TO NEIGHBOUR IS SHORTER OR NEIGHBOUR IS NOT IN (OPEN SET)
			SET (F_COST) OF (NEIGHBOUR)
			SET (PARENT) OF (NEIGHBOUR) TO (CURRENT)
			
			IF NEIGHBOUR NOT IN (OPEN SET)
				ADD (NEIGHBOUR) IN (OPEN SET)

First, we need to define the function and the parameters needed

function FindPath(start: Vector3, target: Vector3)
	local OpenSet = {}
	local ClosedSet = {}

end

Now we’re going to need a constructor for the nodes, which is just a table:

local function ConstructNode(a: Vector3, b: Vector3, gc)
	return {
		Position = a;
		Costs = {
			G = gc;
			H = 0; -- placeholder heuristic
			F = gc + hc;
		};
	}
end

Now we add the starting point node to the open set:

function FindPath(start: Vector3, target: Vector3)
	local OpenSet = { ConstructNode(start, target, 0) }
	local ClosedSet = {}


end

We need to round the starting point and the end point to be in a x^2 grid

local SIZE = 5

local function RoundToNearest(v)
	return Vector3.new(
		math.round(v.X / SIZE) * SIZE,
		math.round(v.Y / SIZE) * SIZE,
		math.round(v.Z / SIZE) * SIZE

	)
end
function FindPath(start: Vector3, target: Vector3)
	start = RoundToNearest(start)
	target = RoundToNearest(target)

After that we need to make the while loop and get the node with the lowest cost:

	-- ...
	while OpenSet[1] do	-- checks if theres any nodes left to be evaluated
		table.sort(OpenSet, function(a, b)
			return a.Costs.F <= b.Costs.F and a.Costs.H < b.Costs.H
		end)

		local Node = table.remove(OpenSet, 1)

		if Node.Position == target then -- checks if node is the target
			return
		end

		ClosedSet[Node.Position] = true


	end

Now for the FOR EACH (NEIGHBOUR) OF (CURRENT) in the pseudocode:

-- this should be a constant thus put it in the global scope (directions are in 2d)
-- the more directions, the more time to compute the path
local DIRECTIONS = {
	Vector3.xAxis;
	Vector3.zAxis;

	Vector3.xAxis * -1;
	Vector3.zAxis * -1;

	Vector3.zAxis + Vector3.xAxis;

	Vector3.xAxis * -1 + Vector3.zAxis;

	Vector3.xAxis + Vector3.zAxis * -1;
}

for i, dir in DIRECTIONS do
	DIRECTIONS[i] = dir * SIZE -- scale the directions with the grid size
end
	local function FindNodeInOpenSet(point: Vector3) -- a function to find a node in the open set since we use arrays
		for _, node in OpenSet do
			if node.Position == point then
				return node
			end
		end
	end
		-- ...
		for _, direction in DIRECTIONS do
				local Neighbor = Node.Position + direction

				local IsTraversable = true -- traversable function coming later on

				if not IsTraversable or ClosedSet[Neighbor] then
					return
				end

				local Distance = (Node.Position - Neighbor).Magnitude -- basically the G cost
				local CostToNeighbor = Node.Costs.G + Distance
				local NeighborNode = FindNodeInOpenSet(Neighbor)

				local NeighborGCost = NeighborNode and NeighborNode.Costs.G or 0 -- if neighbour node exists, then get the g cost, else its 0

				if CostToNeighbor < NeighborGCost or not NeighborNode then
					table.insert(OpenSet, ConstructNode(Neighbor, target, CostToNeighbor))
				end
		end

And most of the logic is done! we just need to fill in the placeholder values I left on. First we need to define the heuristic functions we want, currently I use the manhattan distance heuristic:

	function HeuristicFunction(a, b)
		return math.abs(a.X - b.X) + math.abs(a.Y - b.Y) + math.abs(a.Z - b.Z)
	end

Now we need to implement in the construct node func:

local function ConstructNode(a: Vector3, b: Vector3, gc)
	local hc = HeuristicFunction(a, b)
	
	return {
		Position = a;
		Costs = {
			G = gc;
			H = hc;
			F = gc + hc;
		};
	}
end

After that we need to make a IsTraversable function for the nodes:

local function NodeIsTraversable(point: Vector3)
	local collisions = workspace:GetPartBoundsInBox(CFrame.new(point), Vector3.new(SIZE, 0, SIZE))

	if collisions[1] then
		return false
	end
end
				local IsTraversable = NodeIsTraversable(Neighbor)

Now for the last part! Retracing the path! We need to define another array called Parents:

	local OpenSet = { ConstructNode(start, target, 0) }
	local ClosedSet = {}

	local Parents = {}

In the direction loop, after we add the neighbour node to the open set:

					table.insert(OpenSet, ConstructNode(Neighbor, target, CostToNeighbor))

					Parents[Neighbor] = Node.Position

Now we just need this function to return the path:

	local Parents = {}

	local function RetracePath()
		local Path = {}

		local CurrentNode = target

		while CurrentNode ~= start do
			table.insert(Path, 1, CurrentNode)
			CurrentNode = Parents[CurrentNode]
		end

		return Path
	end

And return the path once its done:

		if Node.Position == target then
			return RetracePath()
		end

And thats it! You should have a working A* Pathfinder. Note that this is unoptimized, I have optimized my own to some extent and I’m pretty happy with it. The pathfinder with debug parts in action:

Optimized Heap Sort Pathfinder:



This was pretty rushed so let me know if there’s any thing I missed!

32 Likes

you said think instead of thing in the post ok?

1 Like

oh yeah, I didn’t notice that; its fixed now

1 Like

I’m sorry but the way the character briefly glitches and jumps out of the field at 0:36 had me rolling. LOL

But what a great tutorial!
I’ve been curious how this system worked.

If I recall correctly, Roblox also uses A* in their engine? Though likely a modified version of it that makes use of the navigation mesh rather than nodes.

I myself have also thought up a concept for a pathfinding algorithm, using raycasts instead of nodes or navmesh.

It’s far easier to multi-thread and should also be cheaper and less expensive to run but has the downside that NPCs might not find their way through an entire maze.
It was more so just designed so NPCs don’t get stuck behind walls or fall down into holes and instead avoid them by walking around them.

2 Likes

Make an article explaining the algorithm in-depth.

2 Likes

There are plenty of videos/articles explaining it better than I do since I’m still trying to understand the algorithm more deeply. I’ll probably do that once I get the hang of it

2 Likes

is there a way to optimize it for long distances, since my fps drops for long distances

Yes, there is a way to optimize it. The most time consuming part of the algorithm is sorting the open list, you can use an efficient sorting algorithm like Heap Sort since roblox’s default sorting function is linear;

The more directions is in the array, the more computationally expensive it gets. So if you are using the pathfinder just to navigate through a flat environment, use the directions in the tutorial; else add the y axis directions

Smaller grid size may result in a finer path for your agent, but it also takes quite a long time. You can use a much larger grid size to get the lag down, and then refine the path to be smooth.

Here are the performance difference between a small grid size, and a bigger grid size; both using the same optimized version:

Grid size of 5:


Grid size of 2:

You could also try to parallelize this algorithm, but that turns out to be quite hard; since A* is dependent on the previous iteration

The performance depends on your heuristic, if you use octile distance heuristic, it may be fast, but it uses more computational power

There are also many variations of the A* algorithm that has their own pros and cons. such as Dynamic A* (D*); which searches from the goal node instead of the usual starting node. Or Theta*; which can find near-optimal path by considering line-of-sight connections with runtime comparable to A*

If you’d like, I can create another tutorial with an optimized version

1 Like

I am creating an a* pathfinding that is dynamic(meaning it will follow a target in runtime).
I have got a performance issue with long distances around 50 studs. the topic is here:

i struggle to actually get a better performing pathfinding

If you’d like, I can create another tutorial with an optimized version

yes please. I would like if you could explain the d* too it seems interesting for performance

1 Like

Yeah, it seems fairly easy to convert my A* into D* or D* Lite. I’ll do a tutorial on optimizing the current A*, and then converting it to D*

(though I have exams coming up in 2 days, so it might take a while)

2 Likes

Hello,

Looking forward to your optimization.

On a flat map, when you say ‘long distance’ what is considered in studs, short med and long distance?

Thanks

Would you consider doing this in the future? It’s a bit hard to follow along without explanations (in my opinion)

I use the term “long distance” loosely to measure how long the path is (pretty much eyeballing it), I define short distances as a path with very few turns with straight path dominating, medium has much more turns, and long distances is a path that you usually get when trying to navigate through complex environment like a maze.

I could try, I can’t really deepdive into A* but I could try to explain the code with a bit more effort instead of rushing

1 Like

I am interested in this when will this be open-source? I’m just asking. Since I could use this for NPCs or something idrk.

I mean the game because i tried your script and I dont know how to do debug or make npcs follow the nodes

For general pathfinding, use the built-in pathfinding service, but if you want to make it more customizable, make your own pathfinder. For example, in mini cities, I believe they made their own pathfinder. Why? Because they need to account for lanes, grid, curve, and alignment to the lane, though they most likely used Djikstra’s

This is just a tutorial on how you can start to make a custom pathfinder. However, I’m currently working on a part 3 where I’ll implement parallelism and D*

I do want to say that the script works but idrk how to make it king on part 3 that’s fine but I just need help with the going the A* having around the walls when its trying to pathfind but it just goes trough the wall. but if your currently still working on the D* I can wait. but I just want to know.

Sorry but I didnt understand what you meant by king. Could you send the script and show the pathfinder goes the wrong way

well its the same script that you’ve made the part 1 and part 2 also i used parts to calculate and see what’s happening but I feel like its definitely maybe part 2. (also idk how to post videos on a computer) but all I’m saying is that part 2 or either part 1 doesn’t seem to be going around walls its just going straight through them I don’t know how yours does it to where it can just go around walls, but it doesn’t work for me