Trying to make A* pathfinding

I am trying to make a pathfinding using A* algorithm. I am not really familiar with it and this is my first attempt doing it. As far as I understood from one of tutorials I have seen on Youtube, there are basically 2 lists. First list is used to store instances that are gonna be checked for neighbors through for i, v in pairs(OpenList) do loop. After checking the instances for its neighbors, it is then moved to ClosedList so it is not used for calculations anymore.

However I am currently facing 2 problems. First of which, is that the part that is checked for neighbors and the part that is being removed in a loop are different. 2nd problem is that I need to determine which way the algorithm should go next. In same Youtube tutorial, the author uses “Manhattan Method” to get the distance from the part to the goal using only straight lines. While I understand that I should simply get the (math.abs(Goal.Position.X - part.Position.X) + math.abs(Goal.Position.Y - part.Position.Y)) to get that distance, I don’t exactly understand where the local for distance should be placed, and how to check that same local on parts, to get shortest one of them all.

Here is my current code. It is not finished though:

local start = script.Parent.Parent
local goal = script.Parent.Parent.Parent.Goal

local OpenList = {}
local ClosedList = {}

local distance

script.Parent.MouseClick:Connect(function()
	table.insert(OpenList, start)
	for i, part in pairs(OpenList) do
		local PositiveXNeighbour = workspace:Raycast(part.Position, Vector3.new(8, 0, 0)).Instance
		local NegativeXNeighbour = workspace:Raycast(part.Position, Vector3.new(-8, 0, 0)).Instance
		local PositiveZNeighbour = workspace:Raycast(part.Position, Vector3.new(0, 0, 8)).Instance
		local NegativeZNeighbour = workspace:Raycast(part.Position, Vector3.new(0, 0, -8)).Instance
		
		if table.find(ClosedList, PositiveXNeighbour) then
			--
		else 
			table.insert(OpenList, PositiveXNeighbour)
			
			if PositiveXNeighbour.Name == "Goal" then
				print("Path found!")
				break
			end
		end
		if table.find(ClosedList, PositiveZNeighbour) then
			--
		else
			table.insert(OpenList, PositiveZNeighbour)
			if PositiveZNeighbour.Name == "Goal" then
				print("Path found!")
				break
			end
		end
		if table.find(ClosedList, NegativeXNeighbour) then
			--
		else
			table.insert(OpenList, NegativeXNeighbour)
			if NegativeXNeighbour.Name == "Goal" then
				print("Path found!")
				break
			end
		end
		if table.find(ClosedList, NegativeZNeighbour) then
			--
		else
			table.insert(OpenList, NegativeZNeighbour)
		end
		
		table.remove(OpenList, 1)
		table.insert(ClosedList, part)
		
		print(OpenList)
		wait(1)
	end	
end)

Would be really great if somebody familiar with A* explained to me how it works again.

3 Likes

image
This is the field BTW, with green being the start and red being the goal. Due to using raycasts to determine neigbors, the code eventually finds no neighbors and refuses to work further, which seems like a problem too.

2 Likes

I found out that just the algorithm was just wrong. When looping it was removing 1st element in the list each time when getting neighbors. This caused it unusual behavior. Changing code right now to fix it.

2 Likes

Still having issues with the algorithm. It seems that some elements of OpenList table are checked for neighbours multiple times! I honestly don’t understand why this is happening, but it is probably something with for i, part loop. I am not really familiar with this type of loops and can’t find the answer myself yet.

Here is a video of how the pathfinding works rn:
robloxapp-20241216-1614406.wmv (2.8 MB)
And this is the code:

local start = script.Parent.Parent
local goal = script.Parent.Parent.Parent.Goal

local OpenList = {}
local ClosedList = {}

local distance

script.Parent.MouseClick:Connect(function()
	table.insert(OpenList, start)
	for i, part in pairs(OpenList) do
		
		part.Color = Color3.fromRGB(255, 85, 0)
		
		wait(0.25)
		
		local PositiveXNeighbour = workspace:Raycast(part.Position, Vector3.new(8, 0, 0)).Instance
		local NegativeXNeighbour = workspace:Raycast(part.Position, Vector3.new(-8, 0, 0)).Instance
		local PositiveZNeighbour = workspace:Raycast(part.Position, Vector3.new(0, 0, 8)).Instance
		local NegativeZNeighbour = workspace:Raycast(part.Position, Vector3.new(0, 0, -8)).Instance
		
		wait(0.25)
		
		table.insert(ClosedList, part)
		
		wait(0.25)
		
		if table.find(ClosedList, PositiveXNeighbour) == nil then
			table.insert(OpenList, PositiveXNeighbour)
			PositiveXNeighbour.Color = Color3.fromRGB(0, 85, 255)
		else
			-- Do nothing
		end
		if table.find(ClosedList, NegativeXNeighbour) == nil then
			table.insert(OpenList, NegativeXNeighbour)
			NegativeXNeighbour.Color = Color3.fromRGB(0, 85, 255)
		else
			-- Do nothing
		end
		if table.find(ClosedList, PositiveZNeighbour) == nil then
			table.insert(OpenList, PositiveZNeighbour)
			PositiveZNeighbour.Color = Color3.fromRGB(0, 85, 255)
		else
			-- Do nothing
		end
		if table.find(ClosedList, NegativeZNeighbour) == nil then
			table.insert(OpenList, NegativeZNeighbour)
			NegativeZNeighbour.Color = Color3.fromRGB(0, 85, 255)
		else
			-- Do nothing
		end
		
		part.Color = Color3.fromRGB(0, 85, 0)
		wait(0.25)
		print(OpenList)
		print(ClosedList)
	end	
end)
1 Like

There seem to be a couple things I notice at first glance.

  1. You’re looping through OpenList, but you also add elements to OpenList in the loop. This is generally not a great practice–I suggest using a while loop with the condition that the length of OpenList is greater than 0.
  2. This is probably your main issue. With A*, on the start of every iteration, the node you should be evaluating is the one with the lowest f cost. After you get the node with the lowest f cost, you must remove it from OpenList. You are evaluating every node in the open list, which is not how A* works.
  3. Raycasting to find neighboring nodes is another practice you should avoid. I suggest setting up a Node Grid using a 2d array.
  4. There are many other parts of the A* algorithm that you’re missing, such as finding the heuristic cost.

I suggest watching this video that explains the A* algorithm really well conceptually.

3 Likes

Yeah about everything after first point is true, its just that I am still experimenting with the idea of pathfinding, it rather is just expanding circle right now. I will take your tips in account.

But back to the main issue: I have a theory that the same object can be added to the list twice, and this might be the reason it gets 1 node to check neighbors multiple times. Do you by any chance know if list can have same object twice?

1 Like

When you check for neighbors you need to make sure that the item isn’t already in the open or closed list before you add it in. A neighbors neighbor is itself in a doubly linked graph after all. Make sure you put the current cell in the closed list before processing too so you don’t accidentally make an exception.

And if the item is already in a list you may need to update the values if the new route somehow beat it. Been a while since I’ve really done A* so I don’t remember if that is redundant.

In the code you supplied it looks like you’re not checking if an item is already in the open list which likely leads to duplicates.

1 Like

I should note that for things like this I prefer a dictionary. Actually I prefer dictionaries almost all the time unless I need a numbered list because dictionaries are easier to work with and often faster*. (You can use pretty much anything for a key. Even instances).

*It has a faster time complexity since it’s considered constant** no matter the list size since it goes straight to the memory address the key points to. Whereas your table.find() looks through the whole list which is linear time complexity.

**It’s mostly constant. There are complications, but that’s mostly negligible.

1 Like

If I am not wrong, my script loops through elements of the list, once I delete the instance from the list, all other instances drop with their number by 1. This means, that an element that was once considered to be 2 in the list, is now 1, and because of that, it is being skipped, because the loop is like “nuh uh, I already have gone through 1st element” - this is roughly how I imagine it, and have not yet found confirmation, if it is like that or not.

About the prevention. I tried using list.find(). As far as I am concerned, it returns the number of the instance in the list, and if it is not in the list, it returns nil. So in idea, I did make a prevention with if statements over here

if table.find(ClosedList, PositiveXNeighbour) == nil then -- nil = not in the closed list
			table.insert(OpenList, PositiveXNeighbour) -- add to the open list 
			PositiveXNeighbour.Color = Color3.fromRGB(0, 85, 255)
		else
			-- Do nothing
		end

But at the end it just does not work as intended. It is still quite weird how some parts are duplicated, while some don’t. It is also interesting that the further the code goes, the more duplicates there are, while start and first 4 neighbors don’t have duplicates at all.

And about dictionaries, I might try to use them, its just that I am not really familiar with them yet, as well as lists.

1 Like

A* open list is unordered. You can technically order the list if you ensure all additions are inserted where they should go order wise, but you aren’t doing that and I would say that ordering it is mostly a waste of time and adds complexity for very little added speed (if any).

As such you can’t make any assumptions about where they are in the list. Each time you iterate you need to pull the number that has the lowest projected cost to get from the start to the goal (steps it took to get there + steps it would take to get to goal if it could go straight there).

As such, you should do something like this psuedocode

while #openlist > 0 do
    local current = getLowestScore(openlist)
    openlist.remove(current)
    closedlist.add(current)
    for _, neighbor in pairs(getNeighbors(current)) do
        if openlist.has(neighbor) then continue end --skip it, though I think you should check if this one is a shorter path than what the one already in the openlist thinks and update it
        if closedlist.has(neighbor) then continue end --skip it
        openlist.add(neighbor)
    end
end

You may need to update the values inside one instead of skipping it. I swear one of the cases could get found by a smaller value, but I don’t remember if that’s entirely true since I’ve not really implemented it in so long.

But as for your neighbor confusion I think what’s happening is this
image
All 4 of those can add that center one as a neighbor technically. And if you are implementing the algorithm correctly, you don’t know if that center cell has already been added by another one of it’s neighbors. Checking closed list is enough to make sure that you don’t loop it back to the neighbor it came from, but not enough to make sure there aren’t multiple routes to get it. So you have to check open list as well.

Also it’s been mentioned, but you really, really shouldn’t be using a for loop here.

for i, part in pairs(OpenList) do

It’s a bad practice to loop like that for any list that is actively changing. You should use a while loop instead. This is for a few reasons. First is that it becomes very clear what the exit condition actually is with a while loop. Second is that modifying a list that is being changed is just confusing and might not even be defined behavior depending on what you are doing, in what language, and what optimizations the interpreter or compiler make. Safer to stick to iterating through static lists and use a while loop or create a new table for dynamic content instead of trying to mix them.

Also it’s a bit long but this resource was super helpful for me when I first tried making A*

1 Like

Ok, I will try to change my code and see if it helps anyhow. I will post changes here in case I need help again. Thanks for your help!

And sorry for late reply

1 Like

I gave up trying to learn 2d arrays. But I did do something else that seems to work pretty fine. I remade the field slightly for better presentation. Each node now has 3 indicators, with red being X coordinate, blue being Z coordinate and Black being cost indicator. I slightly remade my script so that it now first goes through all nodes, changing their X and Y indicators. Then it goes through OpenList, and finds neighbors not by raycasting, but by going through all nodes again, comparing their X and Y values of coordinates, so for example the nodes that has +1 or -1 of center coordinates Z while having same coordinates X will be considered UpNeighbor and DownNeighbor. Then the neighbors are put into different list, in which another loop determines the cost of tiles that would be needed to reach finish.

This is a visual, with just start being in OpenList RN:

And this is the current script:

local start = workspace.Nodes.Start
local finish = workspace.Nodes.Finish

local Nodes = workspace.Nodes:GetChildren()
local OpenList = {}
local ClosedList = {}
local Path = {}
local Neighbors = {}

script.Parent.MouseClick:Connect(function()
	table.insert(OpenList, start)
	for i, part in pairs(Nodes) do
		part.SurfaceGui.IndicatorX.Text = math.abs(part.Position.X / 8)
		part.SurfaceGui.IndicatorZ.Text = math.abs(part.Position.Z / 8)
	end 
	
	for i, center in pairs(OpenList) do
		for i, n in pairs(Nodes) do
			if math.abs(n.Position.X / 8) == math.abs(center.Position.X / 8) and math.abs(n.Position.Z / 8) == math.abs(center.Position.Z / 8) + 1 then
				local UpNeighbor = n
				table.insert(Neighbors, UpNeighbor)
			elseif math.abs(n.Position.X / 8) == math.abs(center.Position.X / 8) and math.abs(n.Position.Z / 8) == math.abs(center.Position.Z / 8) - 1  then
				local DownNeighbor = n
				table.insert(Neighbors, DownNeighbor)
			elseif math.abs(n.Position.Z / 8) == math.abs(center.Position.Z / 8) and math.abs(n.Position.X / 8) == math.abs(center.Position.X / 8) + 1 then
				local RightNeighbor = n
				table.insert(Neighbors, RightNeighbor)
			elseif math.abs(n.Position.Z / 8) == math.abs(center.Position.Z / 8) and math.abs(n.Position.X / 8) == math.abs(center.Position.X / 8) - 1 then
				local LeftNeighbor = n
				table.insert(Neighbors, LeftNeighbor)
			end	
		end
	end
	
	print(Neighbors)
	
	for i, c in pairs(Neighbors) do
		local cost = math.abs((finish.Position.X / 8) - (c.Position.X) / 8) + math.abs((finish.Position.Z / 8) - (c.Position.Z) / 8)
		c.SurfaceGui.CostF.Text = cost
	end
end)

However I came across one issue. I want to get the lowest cost of course, bur I don’t know how would a get a smallest number. Anybody has any ideas how I would do that?

1 Like

Nevermind, I found the way out! And the entire thing seems to work fine! If only I could record a good quality video, I would show the thing. But here is a screenshot of how the algorithm have built the path:
image
I might try to add diagonal movement too, but that is for later. RN I will try to add obstacles, so that the algorithm has to find a new way.
Here is the code as of now:

local start = workspace.Nodes.Start
local finish = workspace.Nodes.Finish

local Nodes = workspace.Nodes:GetChildren()
local OpenList = {}
local ClosedList = {}
local Path = {}
local Neighbors = {}

script.Parent.MouseClick:Connect(function()
	table.insert(OpenList, start)
	while #OpenList > 0 do
		for i, part in pairs(Nodes) do
			part.SurfaceGui.IndicatorX.Text = math.abs(part.Position.X / 8)
			part.SurfaceGui.IndicatorZ.Text = math.abs(part.Position.Z / 8)
		end 

		for i, center in pairs(OpenList) do

			local UpNeighbor
			local DownNeighbor
			local RightNeighbor
			local LeftNeighbor

			local UpCost
			local DownCost
			local RightCost
			local LeftCost

			for i, n in pairs(Nodes) do
				if math.abs(n.Position.X / 8) == math.abs(center.Position.X / 8) and math.abs(n.Position.Z / 8) == math.abs(center.Position.Z / 8) + 1 then
					UpNeighbor = n
					UpCost = math.abs((finish.Position.X / 8) - (UpNeighbor.Position.X) / 8) + math.abs((finish.Position.Z / 8) - (UpNeighbor.Position.Z) / 8)
					table.insert(Neighbors, UpNeighbor)
				elseif math.abs(n.Position.X / 8) == math.abs(center.Position.X / 8) and math.abs(n.Position.Z / 8) == math.abs(center.Position.Z / 8) - 1  then
					DownNeighbor = n
					DownCost = math.abs((finish.Position.X / 8) - (DownNeighbor.Position.X) / 8) + math.abs((finish.Position.Z / 8) - (DownNeighbor.Position.Z) / 8)
					table.insert(Neighbors, DownNeighbor)
				elseif math.abs(n.Position.Z / 8) == math.abs(center.Position.Z / 8) and math.abs(n.Position.X / 8) == math.abs(center.Position.X / 8) + 1 then
					RightNeighbor = n
					RightCost = math.abs((finish.Position.X / 8) - (RightNeighbor.Position.X) / 8) + math.abs((finish.Position.Z / 8) - (RightNeighbor.Position.Z) / 8)
					table.insert(Neighbors, RightNeighbor)
				elseif math.abs(n.Position.Z / 8) == math.abs(center.Position.Z / 8) and math.abs(n.Position.X / 8) == math.abs(center.Position.X / 8) - 1 then
					LeftNeighbor = n
					LeftCost = math.abs((finish.Position.X / 8) - (LeftNeighbor.Position.X) / 8) + math.abs((finish.Position.Z / 8) - (LeftNeighbor.Position.Z) / 8)
					table.insert(Neighbors, LeftNeighbor)
				end	
			end

			local upcoming = math.min(UpCost, DownCost, RightCost, LeftCost)
			if upcoming == UpCost then
				table.remove(OpenList, 1)
				table.insert(Path, center)
				Neighbors = {}
				wait(0.1)
				table.insert(OpenList, UpNeighbor)
				if UpNeighbor == finish then
					table.insert(Path, finish)
					print("Path Found")
					print(Path)
					for i, v in pairs(Path) do
						v.BrickColor = BrickColor.new("Bright green")
						wait(0.5)
					end
				end
			elseif upcoming == DownCost then
				table.remove(OpenList, 1)
				table.insert(Path, center)
				Neighbors = {}
				wait(0.1)
				table.insert(OpenList, DownNeighbor)
				if DownNeighbor == finish then
					print("Path Found")
					table.insert(Path, finish)
					print(Path)
					for i, v in pairs(Path) do
						v.BrickColor = BrickColor.new("Bright green")
						wait(0.5)
					end
				end
			elseif upcoming == RightCost then
				table.remove(OpenList, 1)
				table.insert(Path, center)
				Neighbors = {}
				wait(0.1)
				table.insert(OpenList, RightNeighbor)
				if RightNeighbor == finish then
					print("Path Found")
					table.insert(Path, finish)
					print(Path)
					for i, v in pairs(Path) do
						v.BrickColor = BrickColor.new("Bright green")
						wait(0.5)
					end
				end
			elseif upcoming == LeftCost then
				table.remove(OpenList, 1)
				table.insert(Path, center)
				Neighbors = {}
				wait(0.1)
				table.insert(OpenList, LeftNeighbor)
				if LeftNeighbor == finish then
					print("Path Found")
					table.insert(Path, finish)
					print(Path)
					for i, v in pairs(Path) do
						v.BrickColor = BrickColor.new("Bright green")
						wait(0.5)
					end
				end
			end
		end
		wait(1)
	end
end)

115 lines… I could have probably optimized the proccess if I knew 2d arrays, but it is what it is as of now. Didn’t use some locals, but gonna for sure use them for next versions.

2 Likes

Another day, another change. I had a problem where my algorithm would be stuck on one same node all the time, and I tried to fix it. What I had in mind, is to optimize my cost detection thingy. Basically there is a regCost and a ManhattanCost. regCost is basically the cost, that determines how much tiles you would need to walk through to reach another tile, and ManhattanCost is basically approximately equal cost of how much you would need more tiles to get to finish. ManhattenCost does not take in count obstacles and different costs, so it is not 100% reliable. The main Cost is equal to regCost plus ManhattanCost, and the next tile to choose would be the one with the main Cost being the least. Additionally, I utilized OpenList differently from before, now the Currently inspected tile is inside of its own Current table, while the OpenList contains the neighbors of already visited parts. ClosedList is also used: After finishing calculations of where to go, the part that was previously in Current table is moved into ClosedList.

So in my idea, the Closed List prevents part from going back in the already visited nodes, and if the algorithm chooses undesired track, it eventually gets stuck, and after that, it goes through OpenList, looking for new part with lowest main Cost, to then continue its way. However I ran into some problem… There is this error that I never stumbled upon before:

Script timeout: exhausted allowed execution time

And this does not let me properly see if my script works as intended. Any ideas why this could have happened?

Here is my current code:

local start = workspace.Nodes.Start
local finish = workspace.Nodes.Finish

local Nodes = workspace.Nodes:GetChildren()
local OpenList = {}
local ClosedList = {}
local Path = {}
local Neighbors = {}
local Costs = {}
local Current = {}
local ExtraCheckList = {}

script.Parent.MouseClick:Connect(function()
	table.insert(Current, start)
	while #Current > 0 do
		for i, part in pairs(Nodes) do
			if part:GetAttribute("Occupied") == true then
				table.remove(Nodes, table.find(Nodes, part))
				part.Color = Color3.fromRGB(100, 0, 0)
			elseif table.find(ClosedList, part) then
				table.remove(Nodes, table.find(Nodes, part))
				part.Color = Color3.fromRGB(60, 60, 60)
			end
		end 

		for i, center in pairs(Current) do
			center.Color = Color3.fromRGB(150, 150, 0)

			local UpNeighbor
			local DownNeighbor
			local RightNeighbor
			local LeftNeighbor

			local UpCost
			local DownCost
			local RightCost
			local LeftCost
			
			local regUpCost
			local regDownCost
			local regRightCost
			local regLeftCost

			for i, n in pairs(Nodes) do
				if math.abs(n.Position.X / 8) == math.abs(center.Position.X / 8) and math.abs(n.Position.Z / 8) == math.abs(center.Position.Z / 8) + 1 then
					UpNeighbor = n
					if UpNeighbor then
						if table.find(OpenList, UpNeighbor) then
							regUpCost = center:GetAttribute("regCost") + 1
							if regUpCost < UpNeighbor:GetAttribute("regCost") then
								UpNeighbor:SetAttribute("regCost", regUpCost)
								UpNeighbor:SetAttribute("CameFrom", center.CFrame)
							else
								UpNeighbor:SetAttribute("regCost", UpNeighbor:GetAttribute("regCost"))
							end
						else
							regUpCost = math.abs((n.Position.X / 8) - (start.Position.X / 8)) + math.abs((n.Position.Z / 8) - (start.Position.Z / 8))
							UpCost = math.abs((finish.Position.X / 8) - (UpNeighbor.Position.X / 8)) + math.abs((finish.Position.Z / 8) - (UpNeighbor.Position.Z) / 8) + regUpCost
							table.insert(OpenList, UpNeighbor)
							table.insert(Costs, UpCost)
						end
					end		
				elseif math.abs(n.Position.X / 8) == math.abs(center.Position.X / 8) and math.abs(n.Position.Z / 8) == math.abs(center.Position.Z / 8) - 1 then
					DownNeighbor = n
					if DownNeighbor then
						if table.find(OpenList, DownNeighbor) then
							regDownCost = center:GetAttribute("regCost") + 1
							if regDownCost < DownNeighbor:GetAttribute("regCost") then
								DownNeighbor:SetAttribute("regCost", regDownCost)
								DownNeighbor:SetAttribute("CameFrom", center.CFrame)
							else
								DownNeighbor:SetAttribute("regCost", DownNeighbor:GetAttribute("regCost"))
							end
						else
							regDownCost = math.abs((n.Position.X / 8) - (start.Position.X / 8)) + math.abs((n.Position.Z / 8) - (start.Position.Z / 8))
							DownCost = math.abs((finish.Position.X / 8) - (DownNeighbor.Position.X / 8)) + math.abs((finish.Position.Z / 8) - (DownNeighbor.Position.Z) / 8) + regDownCost
							table.insert(OpenList, DownNeighbor)
							table.insert(Costs, DownCost)
						end
					end
				elseif math.abs(n.Position.X / 8) == math.abs(center.Position.X / 8) + 1 and math.abs(n.Position.Z / 8) == math.abs(center.Position.Z / 8) then
					RightNeighbor = n
					if RightNeighbor then
						if table.find(OpenList, RightNeighbor) then
							regRightCost = center:GetAttribute("regCost") + 1
							if regRightCost < RightNeighbor:GetAttribute("regCost") then
								RightNeighbor:SetAttribute("regCost", regRightCost)
								RightNeighbor:SetAttribute("CameFrom", center.CFrame)
							else
								RightNeighbor:SetAttribute("regCost", RightNeighbor:GetAttribute("regCost"))
							end
						else
							regRightCost = math.abs((n.Position.X / 8) - (start.Position.X / 8)) + math.abs((n.Position.Z / 8) - (start.Position.Z / 8))
							RightCost = math.abs((finish.Position.X / 8) - (RightNeighbor.Position.X / 8)) + math.abs((finish.Position.Z / 8) - (RightNeighbor.Position.Z) / 8) + regRightCost
							table.insert(OpenList, RightNeighbor)
							table.insert(Costs, RightCost)
						end
					end	
				elseif math.abs(n.Position.X / 8) == math.abs(center.Position.X / 8) - 1 and math.abs(n.Position.Z / 8) == math.abs(center.Position.Z / 8) then
					LeftNeighbor = n
					if LeftNeighbor then
						if table.find(OpenList, LeftNeighbor) then
							regLeftCost = center:GetAttribute("regCost") + 1
							if regLeftCost < LeftNeighbor:GetAttribute("regCost") then
								LeftNeighbor:SetAttribute("regCost", regLeftCost)
								LeftNeighbor:SetAttribute("CameFrom", center.CFrame)
							else
								LeftNeighbor:SetAttribute("regCost", LeftNeighbor:GetAttribute("regCost"))
							end
						else
							regLeftCost = math.abs((n.Position.X / 8) - (start.Position.X / 8)) + math.abs((n.Position.Z / 8) - (start.Position.Z / 8))
							LeftCost = math.abs((finish.Position.X / 8) - (LeftNeighbor.Position.X / 8)) + math.abs((finish.Position.Z / 8) - (LeftNeighbor.Position.Z) / 8) + regLeftCost
							table.insert(OpenList, LeftNeighbor)
							table.insert(Costs, LeftCost)
						end
					end
				end	
			end	
			
			if #Costs > 0 then
				local best = math.min(unpack(Costs))
				if best == UpCost then
					table.remove(Current, table.find(Current, center))
					table.insert(ClosedList, center)
					
					table.insert(Current, UpNeighbor)
				elseif best == DownCost then
					table.remove(Current, table.find(Current, center))
					table.insert(ClosedList, center)
					
					table.insert(Current, DownNeighbor)
				elseif best == LeftCost then
					table.remove(Current, table.find(Current, center))
					table.insert(ClosedList, center)
					
					table.insert(Current, LeftNeighbor)
				elseif best == RightCost then
					table.remove(Current, table.find(Current, center))
					table.insert(ClosedList, center)
					
					table.insert(Current, RightNeighbor)
				end
			else
				table.remove(Current, table.find(Current, center))
				table.insert(ClosedList, center)
				
				for i, o in pairs(OpenList) do
					local check = o:GetAttribute("Cost")
					table.insert(ExtraCheckList, check)
				end
				
				local newBest = math.min(unpack(ExtraCheckList))
				for i, o in pairs(OpenList) do
					if o:GetAttribute("Cost") == newBest then
						table.insert(Current, o)
					end
				end
			end
			
			
		end		
	end			
end)	

It used to have some task.wait(0.1)s but I got rid of these for some time. When clicking a button, the game freezes for around 30 seconds and then gives out an error.

1 Like

NVM, don’t answer to this message, I found out my mistake and gonna try to fix it. I will post changes later.

1 Like

Just as I thought I had a solution in my head, my new code works worse than previous version :sob:

And its always the same error with execution time being exhausted

1 Like
local start = workspace.Nodes.Start
local finish = workspace.Nodes.Finish

local Nodes = workspace.Nodes:GetChildren()
local OpenList = {}
local ClosedList = {}
local Path = {}
local Neighbors = {}
local Costs = {}


local cost
local best
local upcoming
local center

local UpNeighbor
local DownNeighbor
local RightNeighbor
local LeftNeighbor

local UpCost
local DownCost
local RightCost
local LeftCost

local TotalUpCost
local TotalDownCost
local TotalRightCost
local TotalLeftCost

table.insert(OpenList, start)

local function UpdateNodes()
	for i, node in pairs(Nodes) do
		if node:GetAttribute("Occupied") == true and not table.find(ClosedList, node) then
			node.Color = Color3.fromRGB(120, 0, 0)
			table.insert(ClosedList, node)
		end
		
		if table.find(ClosedList, node) then
			node.Color = Color3.fromRGB(60, 60, 60)
		end
		
		if node == upcoming then
			node.Color = Color3.fromRGB(240, 240, 60)
		end
		
		if table.find(OpenList, node) then
			node.Color = Color3.fromRGB(0, 120, 120)
		end
	end
end

local function GetBest()
	for i, node in pairs(OpenList) do
		cost = node:GetAttribute("TotalCost")
		table.insert(Costs, cost)
	end
	
	best = math.min(unpack(Costs))
	
	if best then
		for i, node in pairs(OpenList) do
			if node:GetAttribute("MoveCost") == best then
				upcoming = node
			end
		end
	end
	return upcoming
end

local function GetNeighbors(Part)
	
	center = GetBest()
	
	for i, node in pairs(Nodes) do
		if math.abs(node.Position.X / 8) == math.abs(center.Position.X / 8) and math.abs(node.Position.Z / 8) == math.abs(center.Position.Z / 8) + 1 then
			UpNeighbor = node
			if UpNeighbor then
				if table.find(ClosedList, UpNeighbor) or UpNeighbor:GetAttribute("Occupied") == true then
					UpNeighbor = nil
				elseif table.find(OpenList, UpNeighbor) then
					UpCost = center:GetAttribute("MoveCost") + 1
					if UpCost >= UpNeighbor:GetAttribute("MoveCost") then
						UpNeighbor:SetAttribute("MoveCost", UpNeighbor:GetAttribute("MoveCost"))
						UpNeighbor:SetAttribute("TotalCost", UpNeighbor:GetAttribute("TotalCost"))
						UpNeighbor:SetAttribute("CameFrom", UpNeighbor:GetAttribute("CameFrom"))
					elseif UpCost < UpNeighbor:GetAttribute("MoveCost") then
						UpNeighbor:SetAttribute("MoveCost", UpCost)
						TotalUpCost = UpCost + math.abs(finish.Position.X / 8 - UpNeighbor.Position.X / 8) + math.abs(finish.Position.Z / 8 - UpNeighbor.Position.Z / 8)
						UpNeighbor:SetAttribute("TotalCost", TotalUpCost)
						UpNeighbor:SetAttribute("CameFrom", center.CFrame)
					end
				elseif not table.find(OpenList, UpNeighbor) then
					UpCost = center:GetAttribute("MoveCost") + 1
					UpNeighbor:SetAttribute("MoveCost", UpCost)
					TotalUpCost = UpCost + math.abs(finish.Position.X / 8 - UpNeighbor.Position.X / 8) + math.abs(finish.Position.Z / 8 - UpNeighbor.Position.Z / 8)
					UpNeighbor:SetAttribute("TotalCost", TotalUpCost)
					UpNeighbor:SetAttribute("CameFrom", center.CFrame)
					table.insert(OpenList, UpNeighbor)
				end
			end
		elseif math.abs(node.Position.X / 8) == math.abs(center.Position.X / 8) and math.abs(node.Position.Z / 8) == math.abs(center.Position.Z / 8) - 1 then
			DownNeighbor = node
			if DownNeighbor then
				if table.find(ClosedList, DownNeighbor) or DownNeighbor:GetAttribute("Occupied") == true then
					DownNeighbor = nil
				elseif table.find(OpenList, DownNeighbor) then
					DownCost = center:GetAttribute("MoveCost") + 1
					if DownCost >= DownNeighbor:GetAttribute("MoveCost") then
						DownNeighbor:SetAttribute("MoveCost", DownNeighbor:GetAttribute("MoveCost"))
						DownNeighbor:SetAttribute("TotalCost", DownNeighbor:GetAttribute("TotalCost"))
						DownNeighbor:SetAttribute("CameFrom", DownNeighbor:GetAttribute("CameFrom"))
					elseif DownCost < DownNeighbor:GetAttribute("MoveCost") then
						DownNeighbor:SetAttribute("MoveCost", DownCost)
						TotalDownCost = DownCost + math.abs(finish.Position.X / 8 - DownNeighbor.Position.X / 8) + math.abs(finish.Position.Z / 8 - DownNeighbor.Position.Z / 8)
						DownNeighbor:SetAttribute("TotalCost", TotalDownCost)
						DownNeighbor:SetAttribute("CameFrom", center.CFrame)
					end
				elseif not table.find(OpenList, DownNeighbor) then
					DownCost = center:GetAttribute("MoveCost") + 1
					DownNeighbor:SetAttribute("MoveCost", DownCost)
					TotalDownCost = DownCost + math.abs(finish.Position.X / 8 - DownNeighbor.Position.X / 8) + math.abs(finish.Position.Z / 8 - DownNeighbor.Position.Z / 8)
					DownNeighbor:SetAttribute("TotalCost", TotalDownCost)
					DownNeighbor:SetAttribute("CameFrom", center.CFrame)
					table.insert(OpenList, DownNeighbor)
				end
			end
		elseif math.abs(node.Position.X / 8) == math.abs(center.Position.X / 8) + 1 and math.abs(node.Position.Z / 8) == math.abs(center.Position.Z / 8) then
			RightNeighbor = node
			if RightNeighbor then
				if table.find(ClosedList, RightNeighbor) or RightNeighbor:GetAttribute("Occupied") == true then
					RightNeighbor = nil
				elseif table.find(OpenList, RightNeighbor) then
					RightCost = center:GetAttribute("MoveCost") + 1
					if RightCost >= RightNeighbor:GetAttribute("MoveCost") then
						RightNeighbor:SetAttribute("MoveCost", RightNeighbor:GetAttribute("MoveCost"))
						RightNeighbor:SetAttribute("TotalCost", RightNeighbor:GetAttribute("TotalCost"))
						RightNeighbor:SetAttribute("CameFrom", RightNeighbor:GetAttribute("CameFrom"))
					elseif RightCost < RightNeighbor:GetAttribute("MoveCost") then
						RightNeighbor:SetAttribute("MoveCost", RightCost)
						TotalRightCost = RightCost + math.abs(finish.Position.X / 8 - RightNeighbor.Position.X / 8) + math.abs(finish.Position.Z / 8 - RightNeighbor.Position.Z / 8)
						RightNeighbor:SetAttribute("TotalCost", TotalRightCost)
						RightNeighbor:SetAttribute("CameFrom", center.CFrame)
					end
				elseif not table.find(OpenList, RightNeighbor) then
					RightCost = center:GetAttribute("MoveCost") + 1
					RightNeighbor:SetAttribute("MoveCost", RightCost)
					TotalRightCost = RightCost + math.abs(finish.Position.X / 8 - RightNeighbor.Position.X / 8) + math.abs(finish.Position.Z / 8 - RightNeighbor.Position.Z / 8)
					RightNeighbor:SetAttribute("TotalCost", TotalRightCost)
					RightNeighbor:SetAttribute("CameFrom", center.CFrame)
					table.insert(OpenList, RightNeighbor)
				end
			end
		elseif math.abs(node.Position.X / 8) == math.abs(center.Position.X / 8) - 1 and math.abs(node.Position.Z / 8) == math.abs(center.Position.Z / 8) then
			LeftNeighbor = node
			if LeftNeighbor then
				if table.find(ClosedList, LeftNeighbor) or LeftNeighbor:GetAttribute("Occupied") == true then
					LeftNeighbor = nil
				elseif table.find(OpenList, LeftNeighbor) then
					LeftCost = center:GetAttribute("MoveCost") + 1
					if LeftCost >= LeftNeighbor:GetAttribute("MoveCost") then
						LeftNeighbor:SetAttribute("MoveCost", LeftNeighbor:GetAttribute("MoveCost"))
						LeftNeighbor:SetAttribute("TotalCost", LeftNeighbor:GetAttribute("TotalCost"))
						LeftNeighbor:SetAttribute("CameFrom", LeftNeighbor:GetAttribute("CameFrom"))
					elseif LeftCost < LeftNeighbor:GetAttribute("MoveCost") then
						LeftNeighbor:SetAttribute("MoveCost", LeftCost)
						TotalLeftCost = LeftCost + math.abs(finish.Position.X / 8 - LeftNeighbor.Position.X / 8) + math.abs(finish.Position.Z / 8 - LeftNeighbor.Position.Z / 8)
						LeftNeighbor:SetAttribute("TotalCost", TotalLeftCost)
						LeftNeighbor:SetAttribute("CameFrom", center.CFrame)
					end
				elseif not table.find(OpenList, LeftNeighbor) then
					LeftCost = center:GetAttribute("MoveCost") + 1
					LeftNeighbor:SetAttribute("MoveCost", LeftCost)
					TotalLeftCost = LeftCost + math.abs(finish.Position.X / 8 - LeftNeighbor.Position.X / 8) + math.abs(finish.Position.Z / 8 - LeftNeighbor.Position.Z / 8)
					LeftNeighbor:SetAttribute("TotalCost", TotalLeftCost)
					LeftNeighbor:SetAttribute("CameFrom", center.CFrame)
					table.insert(OpenList, LeftNeighbor)
				end
			end
		end
	end
	table.remove(OpenList, table.find(OpenList, center))
	table.insert(ClosedList, center)
end


script.Parent.MouseClick:Connect(function()
	repeat
		UpdateNodes()
		task.wait(1)
		GetNeighbors()
		print(OpenList)
		print(ClosedList)
		task.wait(1)
	until table.find(OpenList, finish)
end)

It almost seems to work, but it for some reason is still staying in place for some reason…

1 Like

Your code is getting rather large with a lot of repetition. It’s getting tricky to read the whole thing to help you.

Fundamentally you shouldn’t need to care about the difference between upNeighbor and rightNeighbor and the others. They all should be equal. You should do something like

for neighbor in pairs(getNeighbors(node)) do
    --if closedList then continue end
    --if openList then check if this one came from a better route and update openList entry if it did, ignore if it didn't
    --if neighbor is the goal then return reconstructed path
end

This way you can treat every neighbor the same which means you can have less code (less places things can go wrong) and you can update what neighbors do more easily since there is only one place to worry about.

Additionally you should be using dictionaries. I know I’ve mentioned them before and you’ve cited a reason not to use them, but they are really, really useful. Right now you are storing your data as an attribute. This does work, but you can simply store a table in your lists by the part they come from

local openList = {}
openList[game.Workspace.Part] = "this is a string, but any data can be saved here even a table"
print(openList[game.Workspace.Part]

In the above example the part is a key to access the data. You can loop through a dicitonary like this

for key, value in pairs(openList) do
    print(key:GetFullName()) --this was a part, so it would print game.Workspace.Part
    print(value) --This will be the string we stored
end

The reason why I recommend a table so strongly is it’s easier, more localized and supports more types than attributes. Usually when I do A* I am storing 4 things. The distance traveled to get to the node so far. The guessed distance remaining from that node. The combination of currentCost + projectedCost. And finally the parent node from which this neighbor was found.

openList[neighbor] = {
    Cost = currentNode.Cost + 1,
    Heuristic = heuristic(neighbor), --Neighbor is technically a reference to itself because this is in theory in the neighbors loop
    ProjectedCost = currentNode.Cost + 1 + heuristic(neighbor),
    Parent = currentNode
}

This response may be a bit annoying since I’m basically asking you to refactor your code instead of giving a solution, but I honestly can’t tell where this is going wrong. The proposed changes will help narrow it down and overall make it easier to debug.

Minor nitpick

I guess while I’m at it I should recommend not keeping all your variables in the global namespace and instead put them inside functions.

local function aStar(start, end)
    local openList = {}
    local closedList = {}
end

The reason for this technically doesn’t matter while you are learning to use A*, but if you intend to put this into use, you would encounter problems trying to run the algorithms from multiple places at the same time. It’s generally good practice to limit the scope of variables as much as is reasonable. You may need to directly pass extra data to helper functions when doing this but it can be worth it.

I wouldn’t worry about this one at the moment though since it’s more a distraction than anything else, but it does matter especially if you write longer code.

If you provide a place file I can test your code directly with then I could probably help out more since I can do my full debug process without a constant series of “Try this, then this”.

1 Like

Fundamentally you shouldn’t need to care about the difference between upNeighbor and rightNeighbor and the others. They all should be equal. You should do something like.

I don’t exactly understand how I could do that… The first thing that comes to my mind is to put all 4 if statemenets like this one if math.abs(node.Position.X / 8) == math.abs(center.Position.X / 8) and math.abs(node.Position.Z / 8) == math.abs(center.Position.Z / 8) + 1 then into one line. But now that I think of that, that does make sense.

I will try to use dictionaries I guess…

And I will send you a place copy soon.

1 Like

Haven’t quite got to the point I can open it in studio, but I can quickly clarify

By treating all neighbors the same I mean doing something like this

local function getNeighbors(center)
    local neighbors = {}
    for i, node in pairs(Nodes) do
	    if math.abs(node.Position.X / 8) == math.abs(center.Position.X / 8) and math.abs(node.Position.Z / 8) == math.abs(center.Position.Z / 8) + 1 then
            table.insert(neighbors, node)
        elseif math.abs(node.Position.X / 8) == math.abs(center.Position.X / 8) and math.abs(node.Position.Z / 8) == math.abs(center.Position.Z / 8) - 1 then
            table.insert(neighbors, node)
        elseif math.abs(node.Position.X / 8) == math.abs(center.Position.X / 8) + 1 and math.abs(node.Position.Z / 8) == math.abs(center.Position.Z / 8) then
            table.insert(neighbors, node)
        math.abs(node.Position.X / 8) == math.abs(center.Position.X / 8) - 1 and math.abs(node.Position.Z / 8) == math.abs(center.Position.Z / 8)
            table.insert(neighbors, node)
        end
    end

    return neighbors
end

And then you can do somewhere a

for _, neighbor in pairs(getNeighbors(center)) do --Now you handle what neighbor code should do
    if table.find(ClosedList, neighbor) then continue end --we can skip this one since it's already closed
    if table.find(OpenList, neighbor) then
        if neighbor:GetAttribute("MoveCost") and neighbor:GetAttribute("MoveCost") < center:GetAttribute("MoveCost") + 1 then
            continue --If the value of the current data in open list is smaller, we can just leave it alone by skipping it
        end
    end
    --If the node wasn't found in either list OR this one is cheaper than what is in openList, then overwrite the data in the current node to match this new path
    local curCost = center:GetAttribute("MoveCost") + 1
    neighbor:SetAttribute("MoveCost", curCost)

    local heuristic = math.abs(finish.Position.X / 8 - UpNeighbor.Position.X / 8) + math.abs(finish.Position.Z / 8 - UpNeighbor.Position.Z / 8)
    neighbor:SetAttribute("TotalCost", curCost + heuristic)

    neighbor:SetAttribute("CameFrom", center.CFrame)
end
A more compact way of doing getNeighbors
local function getNeighbors(center)
    local neighbors = {}
    for i, node in pairs(Nodes) do
        if node == center then continue end --don't want the currently processed node to be considered a neighbor since it will technically be in range
	    if (node.Position - center.Position).Magnitude <= 8.01 then --Make sure it's not further than a little more than the single axis distance.
            table.insert(neighbors, node)
        end
    end
    return neighbors
end
2 Likes