How 2 make A* Pathfinding

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

by the way I meant working I missed spelled.

also I do want to point out there’s this error occurs, “exhausted allowed execution time” in your scripts

not for part 2 or anything just part 1, also I know that I’m complaining about the script im self-aware (maybe I am complaining). but I just want to know from your code can maybe improve to were the A* can detect walls and go around them. That’s all I want to know. I can show you because I’ve might’ve found out how to record and show you (also the code is your source the part 2)
No Walls:


With Walls:

(sorry for the lag)

Script To Prove:

local SIZE = 5

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 RoundToNearest(v)
	return Vector3.new(
		math.round(v.X / SIZE) * SIZE,
		math.round(v.Y / SIZE) * SIZE,
		math.round(v.Z / SIZE) * SIZE
	)
end

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

	if collisions[1] then
		return false
	end
	return true
end

local function ConstructNode(worldPos: Vector3, target: Vector3)
	local D = math.sqrt(2) -- A constant

	local function HeuristicFunction(a: Vector3, b: Vector3): number
		local D2 = 1

		local dx = math.abs(a.X - b.X)
		local dy = math.abs(a.Y - b.Y)
		local dz = math.abs(a.Z - b.Z)

		return D * math.min(dx, dy, dz) + (D2 - D) * math.max(0, dx + dy + dz - 2 * math.min(dx, dy, dz))		
	end
	
	local Object = setmetatable({
		Traversable = false;
		Position = worldPos;

		Costs = setmetatable({
			G = 0;
			H = 0;
		}, {
			__index = function(t, k)
				if k == 'F' then
					return rawget(t, 'H') + rawget(t, 'G') -- F is the sum of H and G
				end

				return rawget(t, k)
			end,	
		});

		HeapIndex = 0; -- For our new heap
		Parent = nil; -- No more storing Parents table
	}, {
		__sub = function(a, b)
			local compare = a.Costs.F - b.Costs.F -- Substract a's F and b's F

			if compare == 0 then -- If the F costs are equal, compare the H cost instead
				compare = a.Costs.H - b.Costs.H
			end

			return -compare -- Return the negated comparison
		end,

		__eq = function(a, b)
			return a.Position == b.Position
		end,
	})

	Object.Costs.G = (worldPos - target).Magnitude
	Object.Costs.H = HeuristicFunction and HeuristicFunction(worldPos, target) or 0 -- Heuristic is 0 if no function is set

	Object.Traversable = NodeIsTraversable(worldPos)

	return Object
end

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

	local StartNode = ConstructNode(start, target)
	local TargetNode = ConstructNode(target, target)

	-- Safety precaution checks so we don't waste time computing the path
	assert(StartNode.Traversable, 'Starting Node is intraversable, thus path is intraversable')
	assert(TargetNode.Traversable, 'Target Node is intraversable, thus path is intraversable')

	local Grid = {[start] = StartNode, [target] = TargetNode}
	
	local Heap = require(script:WaitForChild("Heap"))
	local OpenSet = Heap.new()-- :: Heap.Heap<typeof(ConstructNode(Vector3, Vector3))>
	local ClosedSet = {}

	OpenSet:Add(StartNode)

	while OpenSet:Count() > 0 do
		local CurrentNode = OpenSet:RemoveFirst()
		ClosedSet[CurrentNode.Position] = true
		
		local function RetracePath()
			local Path = {}
			local CurrentPathNode = TargetNode

			while CurrentPathNode ~= StartNode do
				table.insert(Path, 1, CurrentPathNode)
				CurrentPathNode = CurrentPathNode.Parent
			end

			return Path
		end

		if CurrentNode == TargetNode then
			return RetracePath()
		end

		for _, direction in DIRECTIONS do
			local NeighborPos = CurrentNode.Position + direction

			-- If neighbor already evaluated/not traversable, skip
			if ClosedSet[NeighborPos] or not NodeIsTraversable(NeighborPos) then
				continue
			end

			-- Get neighbor node
			local NeighborNode = Grid[NeighborPos] or ConstructNode(NeighborPos, target)
			Grid[NeighborPos] = NeighborNode

			-- Get new G cost to the neighbor
			local CostToNeighbor = CurrentNode.Costs.G + (CurrentNode.Position - NeighborPos).Magnitude

			-- If cost turns out to be better or not in openset
			if CostToNeighbor < NeighborNode.Costs.G or not OpenSet:Contains(NeighborNode) then
				NeighborNode.Costs.G = CostToNeighbor
				NeighborNode.Costs.H = (NeighborPos - target).Magnitude

				NeighborNode.Parent = CurrentNode

				if not OpenSet:Contains(NeighborNode) then -- If it doesn't have the neighbor node yet, add the node
					OpenSet:Add(NeighborNode)
				end
			end
		end
	end
end

local function roundToStep(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 ai = workspace.ai
local hum = ai:WaitForChild("Humanoid")

while wait() do

	ai:MoveTo(workspace.Start.Position)

	local path = FindPath(roundToStep(workspace.Start.Position), roundToStep(workspace.Target.Position))
	for _, node in pairs(path) do
		if workspace:FindFirstChild(tostring(_)) then
			workspace:FindFirstChild(tostring(_)):Destroy()
		end

		local a = Instance.new("Part", workspace)
		a.Name = _
		a.Position = node.Position
		a.Size = Vector3.new(1, 1, 1)
		a.Anchored = true
		a.CanCollide = false
	end

	for _, node in pairs(path) do
		hum:MoveTo(node.Position)
		hum.MoveToFinished:Wait()
	end

end also I’m using part 2 because part 1 wasn’t working from your script (for some reason might’ve been an error) so if you can help me with this.