You can use 3D magnitude as your h cost since it is always “less than or equal to the actual shortest path length”. If you choose a different function closer to the actual shortest path length that still satisfies that property, then A* will run faster. If your h cost is ever greater than the shortest path length then A* may not find the shortest path.
I’d consider moving the G, H, and F costs as well as the point to their own tables. Have a couple tables of values instead of a table of tables of values. If you do this, then you lose the overhead of creating a table for every node visited.
The functions inside of findPath should be moved out. Not only does this reduce indentation, promote independence, and prevent accidental manipulation and access but it also will increase performance by preventing closures from being bade when findPath is called. If the functions need access to variables, they can be passed in as arguments or as file level upvalues.
The heavy work of finding in and removing from open can be offloaded to lightning fast C by using an integer point ID as the key. All node references (like neighbors) should use this node ID integer instead. Doing so causes C place the ID in the hash map portion of the table. The result is a O(log(n)) operation for insertion, search, and deletion whereas your array implementation is currently O(1) for insertion but O(n) for search and deletion. Since a search needs to be performed before every insertion in our use case, your insertion is also effectively O(n) (very bad considering A* often performs many times more insertions than deletions).
That should be zero for any valid cost function. In addition, the parsed version of the start and goal points should be cached, especially since you need to check if every node you visit is the goal and get the distance to it every time you insert a point. I also noticed that you use the start point every time you insert a node. Not only is the duplication of parsing being done here a performance issue, but this is a bug.
The G cost should be a a running total of the walked distance to get to a point – not the straight line distance from the point to the start. It is different from the H cost as it not only represents how far we’ve come verses how far we have to go, but it is an exact and known value whereas the H cost is a heuristic (that is why it is called the H cost). In addition when the goal is the current point, the G cost represents the shortest path length to the goal. Looking further down in the code, I believe that cGCost should be set to open[nodeNum].gCost
, and similarly for the other two costs.
Since
We don’t need to check if the H cost is lower than the current HCost. This will lead to inefficient behavior. The total cost (F cost) should be the only one used to determine the next point to expand. In addition, <= rather than a < and then a == is the idiomatic Lua practice.
Since the G, H, and F costs are related, you only need to store two of them and the third and be quickly calculated. This is an option, but may not be best for your uses.
Equality can be checked for Vector3 once you cache the goal point in Vector4 form.
The logic is strong here. Boolean algebra tells us that this is equivalent to
if not found then
...
end
I think you just meant to leave out the inner if statement. Also currenNGCost should only be set if the point was found, and it should be set to the point’s current G cost rather than the straight line distance from the start to the point (which no G cost will ever be less than, due to the properties of the cost function mentioned above).
I just realized that if nodeNum is stored outside the for loop then this call
removeFromOpen(currentNodeInfo.point)
can be turned into this one QuickRemove(open, nodeNum)
which is O(1) instead of O(n). This would also be true if you were to use node IDs instead.
You should consider using a Binary Heap from GitHub for a O(log(n)) pop operation rather than O(n).
Haha, I too have learned not to use Vector3s in hash tables! It turns out that even if two Vector3’s == eachother (due to the __eql metamethod) they they may not hash to the same value. I’m truly sorry if you ran into this and had to debug it. Using integer node ID keys resolves this issue. I wish Roblox would make immutable Vector3s unique like Lua strings, but alas it is a CPU-memory trade-off.
These costs can be computed only if they are needed inside the if statement below.
I gave an excellent implementation of the basics of A* here: Pathfinding in a Cave Network - #7 by IdiomicLanguage